如何使用双向RNN和pytorch填空?

Posted

技术标签:

【中文标题】如何使用双向RNN和pytorch填空?【英文标题】:How to fill in the blank using bidirectional RNN and pytorch? 【发布时间】:2019-06-16 19:38:06 【问题描述】:

我正在尝试使用双向 RNN 和 pytorch 来填补空白。

输入将类似于:The dog is _____, but we are happy he is okay.

输出会是这样的:

1. hyper (Perplexity score here) 
2. sad (Perplexity score here) 
3. scared (Perplexity score here)

我在这里发现了这个想法:https://medium.com/@plusepsilon/the-bidirectional-language-model-1f3961d1fb27

import torch, torch.nn as nn
from torch.autograd import Variable

text = ['BOS', 'How', 'are', 'you', 'EOS']
seq_len = len(text)
batch_size = 1
embedding_size = 1
hidden_size = 1
output_size = 1

random_input = Variable(
    torch.FloatTensor(seq_len, batch_size, embedding_size).normal_(), requires_grad=False)

bi_rnn = torch.nn.RNN(
    input_size=embedding_size, hidden_size=hidden_size, num_layers=1, batch_first=False, bidirectional=True)

bi_output, bi_hidden = bi_rnn(random_input)

# stagger
forward_output, backward_output = bi_output[:-2, :, :hidden_size], bi_output[2:, :, hidden_size:]
staggered_output = torch.cat((forward_output, backward_output), dim=-1)

linear = nn.Linear(hidden_size * 2, output_size)

# only predict on words
labels = random_input[1:-1]

# for language models, use cross-entropy :)
loss = nn.MSELoss()
output = loss(linear(staggered_output), labels)

我正在尝试重新实现在博客文章底部找到的上述代码。我是 pytorch 和 nlp 的新手,无法理解代码的输入和输出是什么。

关于输入的问题:我猜输入是给出的几个词。为什么在这种情况下需要句首和句尾标签?为什么我看不到输入是像其他经典 NLP 问题一样训练模型的语料库?我想使用安然电子邮件语料库来训练 RNN。

关于输出的问题:我看到输出是张量。我的理解是张量是一个向量,所以在这种情况下可能是一个词向量。你如何使用张量来输出单词本身?

【问题讨论】:

该代码实际上并未训练模型。它只是通过一些层传递一个虚拟输入作为示例。 看看context2vec。除了您提到的教程之外,您还会发现许多更好的教程。您还会发现许多使用 python 和 pytorch 的实现,例如 here。 【参考方案1】:

由于这个问题相当开放,我将从最后几部分开始,对标题中提出的主要问题进行更一般的回答。

快速说明:正如 @Qusai Alothman 在 cmets 中指出的那样,您应该找到关于该主题的更好资源,当涉及到必要信息时,这个资源相当稀疏。

补充说明:上一节中描述的过程的完整代码将占用太多空间来提供准确的答案,它更像是一个博客邮政。我将重点介绍人们应该采取哪些步骤来创建这样一个带有有用链接的网络。

最后说明:如果下面有任何愚蠢的地方(或者您想以任何方式或形式扩展答案,请通过在下面发表评论来纠正我/添加信息)。

关于输入的问题

此处的输入是根据随机正态分布生成的,与实际单词没有联系。它应该代表word embeddings,例如将单词表示为带有语义(这很重要!)含义的数字(有时也取决于上下文(参见当前最先进的方法之一,例如BERT))。

输入的形状

在您的示例中,它提供为:

seq_len, batch_size, embedding_size,

在哪里

seq_len - 表示单个句子的长度(因您的 数据集),我们稍后再讨论。 batch_size - 多少句 应该在forwardpass的一步中处理(如果 PyTorch是类继承的forward方法 torch.nn.Module) embedding_size - 表示一个词的向量(它 范围可能从使用word2vec 的通常100/3004096 或 因此使用提到的 BERT 等更新的方法 以上)

在这种情况下,它都是大小为 1 的硬编码,这对新手来说并没有真正的用处,它只是以这种方式概述了这个想法。

为什么在这种情况下需要句首和句尾标签?

如果我错了,请纠正我,但如果你的输入被分成句子,你不需要它。如果您向模型提供多个句子,并希望明确指示每个句子的开头和结尾(与依赖于前一个/下一个句子的模型一起使用,这里似乎不是这种情况)。它们由特殊标记(整个语料库中不存在的标记)编码,因此神经网络“可以学习”它们表示句子的结尾和开头(这种方法的一个特殊标记就足够了)。

如果您要使用严肃的数据集,我建议您使用 spaCy 或 nltk 之类的库来拆分您的文本(第一个是使用 IMO 的乐趣),它们在这项任务上做得非常好。

您的数据集可能已经被拆分成句子,在这些情况下,您已经准备好了。

为什么我没有看到输入是像其他经典 NLP 问题一样训练模型的语料库?

我不记得在语料库上训练的模型按原样,例如使用字符串。通常这些由浮点数表示,使用:

简单的方法,例如Bag Of Words 或 TF-IDF 更复杂的,提供有关单词的一些信息 关系(例如 king 在语义上与 queen 更相关 而不是一个,比如说,banana)。那些已经在上面链接了,一些 其他值得注意的可能是 GloVe 或 ELMo 和大量其他创意 接近。

关于输出的问题

应该将索引输出到embeddings,这又对应于由向量表示的单词(上面提到了更复杂的方法)。

这种嵌入中的每一行代表一个唯一的单词,并且它各自的列是它们的唯一表示(在 PyTorch 中,第一个索引可能保留给表示未知的单词[如果使用预训练嵌入],您也可以删除那些词,或将它们表示为句子/文档的平均数,还有一些其他可行的方法。

示例中提供的损失

# for language models, use cross-entropy :)
loss = nn.MSELoss()

对于这项任务,它没有任何意义,因为Mean Squared Error 是一种回归指标,而不是分类指标。

我们想用一个来分类,所以softmax应该用于多类案例(我们应该输出跨越[0, N]的数字,其中N是我们语料库中唯一词的数量)。

PyTorch 的 CrossEntropyLoss 已经采用 logits(最后一层的输出没有激活,如 softmax)并返回每个示例的损失值。我会推荐这种方法,因为它在数值上是稳定的(我喜欢它是最小的一种)。

我正在尝试使用双向 RNN 和 pytorch 来填补空白

这是一篇很长的文章,我将只强调我将采取的步骤,以创建一个模型,其想法代表了帖子中概述的模型。

数据集的基本准备

您可以使用上面提到的那个,或者从 scikit-learn 中的20 newsgroups 等更简单的方法开始。

第一步应该大致是这样的:

从数据集中抓取元数据(如果有的话)(可能是 html 标记、一些标题等) 使用预制库(如上所述)将文本拆分成句子

接下来,您想在每个句子中创建目标(例如要填写的单词)。 每个单词都应替换为特殊标记(例如<target-token>)并移至目标。

例子:

句子:Neural networks can do some stuff.

会给我们以下句子及其各自的目标:

句子:<target-token> networks can do some stuff. 目标:Neural 句子:Neural <target-token> can do some stuff. 目标:networks 句子:Neural networks <target-token> do some stuff. 目标:can 句子:Neural networks can <target-token> some stuff. 目标:do 句子:Neural networks can do <target-token> stuff. 目标:some 句子:Neural networks can do some <target-token>. 目标:some 句子:Neural networks can do some stuff <target-token> 目标:.

您应该通过更正拼写错误(如果有)、标记化、词形还原和其他方法来调整这种方法来解决手头的问题!

嵌入

每个句子中的每个单词都应该替换为一个整数,而整数又指向它的嵌入。

我建议您使用预先训练过的。 spaCy 提供了词向量,但我强烈推荐的另一种有趣的方法是在开源库flair 中。

你可以自己训练,但是无监督训练需要很多时间+大量数据,我认为这超出了这个问题的范围。

数据批处理

应该使用 PyTorch 的 torch.utils.data.Dataset 和 torch.utils.data.DataLoader。

就我而言,一个好主意是向DataLoader 提供自定义collate_fn,它负责创建填充的数据批次(或已经表示为torch.nn.utils.rnn.PackedSequence)。

重要提示:目前,您必须按长度(逐字)对批次进行排序,并保持索引能够将批次“取消排序”为原始形式,在实施过程中您应该记住这一点。您可以使用torch.sort 来完成该任务。在 PyTorch 的未来版本中,有机会,可能不必这样做,请参阅 this issue。

哦,记得使用 DataLoader 打乱您的数据集,而我们正在使用它。

型号

您应该通过从torch.nn.Module 继承来创建合适的模型。我建议您创建一个更通用的模型,您可以在其中提供 PyTorch 的细胞(如 GRU、LSTM 或 RNN)、多层和双向(如帖子中所述)。

在模型构建方面遵循这些原则:

import torch


class Filler(torch.nn.Module):
    def __init__(self, cell, embedding_words_count: int):
        self.cell = cell
        # We want to output vector of N
        self.linear = torch.nn.Linear(self.cell.hidden_size, embedding_words_count)

    def forward(self, batch):
        # Assuming batch was properly prepared before passing into the network
        output, _ = self.cell(batch)
        # Batch shape[0] is the length of longest already padded sequence
        # Batch shape[1] is the length of batch, e.g. 32
        # Here we create a view, which allows us to concatenate bidirectional layers in general manner
        output = output.view(
            batch.shape[0],
            batch.shape[1],
            2 if self.cell.bidirectional else 1,
            self.cell.hidden_size,
        )

        # Here outputs of bidirectional RNNs are summed, you may concatenate it
        # It makes up for an easier implementation, and is another often used approach
        summed_bidirectional_output = output.sum(dim=2)
        # Linear layer needs batch first, we have to permute it.
        # You may also try with batch_first=True in self.cell and prepare your batch that way
        # In such case no need to permute dimensions
        linear_input = summed_bidirectional_output.permute(1, 0, 2)
        return self.linear(embedding_words_count)

如您所见,可以通过一般方式获取有关形状的信息。这种方法将允许您创建一个包含多少层的模型,无论是否双向(batch_first 参数是有问题的,但您也可以以一般方式绕过它,为了提高清晰度而忽略它),见下文:

model = Filler(
    torch.nn.GRU(
        # Size of your embeddings, for BERT it could be 4096, for spaCy's word2vec 300
        input_size=300,
        hidden_size=100,
        num_layers=3,
        batch_first=False,
        dropout=0.4,
        bidirectional=True,
    ),
    # How many unique words are there in your dataset
    embedding_words_count=10000,
)

您可以将torch.nn.Embedding 传递到您的模型中(如果经过预训练并且已经填充),使用 numpy 矩阵或其他多种方法创建它,这高度依赖于您的代码结构。不过,请让您的代码更通用,不要对形状进行硬编码,除非它是完全必要的(通常不是)。

请记住,这只是一个展示,您必须自己调整和修复它。 此实现返回 logits 并且不使用 softmax 层。如果您想计算困惑度,您可能必须添加它才能在所有可能的向量中获得正确的概率分布。

顺便说一句:Here 是有关 RNN 双向输出连接的一些信息。

模型训练

我会强烈推荐PyTorch ignite,因为它非常可定制,您可以使用它记录大量信息,执行验证和抽象杂乱部分,如训练中的for循环。

哦,将您的模型、训练和其他模块拆分为单独的模块,不要将所有内容都放在一个不可读的文件中。

最后的笔记

这是我将如何解决这个问题的大纲,使用注意力网络而不是像本例中那样仅使用最后一个输出层可能会更有趣,尽管你不应该从那个开始。

请查看 PyTorch 的 1.0 文档,不要盲目地遵循您在网上看到的教程或博客文章,因为它们可能很快就过时了,而且代码的质量差异很大。例如,torch.autograd.Variable 已弃用,如链接所示。

【讨论】:

这是一个写得很好的答案,也许是迄今为止最好的答案。感谢您花时间传递您的知识。我希望我在开始从事 NLP 任务时能早早地阅读那个答案。

以上是关于如何使用双向RNN和pytorch填空?的主要内容,如果未能解决你的问题,请参考以下文章

PyTorch笔记 - Recurrent Neural Network(RNN) 循环神经网络

PyTorch笔记 - Recurrent Neural Network(RNN) 循环神经网络

PyTorch笔记 - Recurrent Neural Network(RNN) 循环神经网络

python - 如何在pytorch上实现stacked rnn (num layers > 1)?

如何在 Pytorch LSTM/GRU/RNN 中指定不同的层大小

84. 双向循环神经网络