您的位置 首页 PyTorch 教程

PyTorch 实现序列模型和基于LSTM的循环神经网络

PyTorch入门实战教程

由于近期在熟悉Pytorch,看了不少其官网的教程,又回顾了不少深度学习的知识。其中有些内容写得非常好,既解释了基本概念,又一步一步引导你动手实践,对于入门来说是非常有帮助的,非常建议大家阅读这些库的教程。本文大体上即是Pytorch官网教程关于LSTM的翻译,原文地址:Sequence Models and Long-Short Term Memory Networks 。不过其中夹杂了我个人的一些想法和体会。

阅读本文,你将初步了解Pytorch的架构、时序数据的特点、循环神经网络的类型、词向量的生成、Pytorch进行简单的循环神经网络的训练。

对于不了解LSTM-RNN的读者,如果您对其感兴趣同时具备一定的英语阅读能力,我强烈建议您阅读下面两篇文章:

  1. 循环神经网络(RNN):karpathy.github.io/2015(哎怎么又引用了Karpathy的文章啊,谁叫这家伙对问题理解的深刻而且又有能力深入浅出地讲解呢,牛人就是牛人啊)
  2. 长短期记忆单元(LSTM):Understanding LSTM Networks

这两篇文章配合许多精彩的图片,详细地解释了传统RNN的缺点,LSTM-RNN是什么,它能解决什么问题以及具体是如何解决问题的。

由于知识背景的不同,我喜欢从神经科学的角度来看待一个深度学习模型。对于LSTM结构单元,我的理解是它通过引入各种门机制对一个神经元的输出进行修饰,基本上模拟了具备释放脉冲电信号(动作电位)的神经元,从而一定程度的具备了被动的长短期记忆功能。由于是矩阵运算,它使用矩阵形式一次性模拟了多个具备记忆功能的神经元。说它是“被动记忆”的主要因为这种记忆与我们常说的人类的记忆还是有很大差别的,套用一句谚语,LSTM的“长短期记忆”仍然停留在“小和尚念经——有口无心”的阶段,就像让一个小小孩背乘法口诀表一样,她有可能记得住“三五”后面是“十五”,但是它并不知道这“三五十五”背后的意思。可以说LSTM并不具备实质的思考和决策能力。如果说得严厉点,目前所有的基于神经网络的深度学习,其单个网络都只是一种对输入数据的特征检测和表示,普通的深度学习网络是这样、卷积神经网络以及循环神经网络都是如此,这些网络至多也就近似模拟了人类神经系统的外周感受器部分,离模拟人的大脑功能还差得远,因而可以认为传统的深度学习网络还远远谈不上模拟思维、决策。但这不能否定深度学习带来的突破,况且这些特征表示的基础性的工作是必须要做的。

在此基础上,近年来,我们看到了一些积极的进展,比如生成式对抗网络(GAN),它不在是单个的神经网络,而是构建了一个由2个网络组成的复合网络;它不在简单的停留在特征表示层面,而是希望建立了一个反馈机制去自主学习。未来可能会出现更多的像搭积木式的把简单的网络组合在一起形成一个大的复杂网络的类似研究。这其中,我个人觉得强化学习不可缺少,因为在模拟人类决策方式上,强化学习可以说是非常接近人类神经系统的工作方式的,它趋利避害,试图最大化奖励,这一方面人类在处理简单问题上的条件反射原理,另一方面也很接近人类在处理复杂问题上的思考和决策过程,强化学习里的记忆表示在一定程度上更接近人类大脑对于知识的记忆,这也是我很花力气学习强化学习的原因。可以预见有了深度学习的特征检测,把这些特征送入强化学习网络中,会不可避免的碰擦出一些思想的火花并结出果实。我们已经看到了卷积神经网络在强化学习中应用的例子,通过对游戏画面的直接拍摄,把拍摄的到的图片送入卷积神经网络,将卷积神经网络的输出作为强化学习中的状态特征表示,通过不断学习产生出一个具备一定智能的个体。

同样基于LSTM的循环神经网络(RNN)也可以应用于强化学习中,这相当于给强化学习方法送入了基于信息状态(value of information)的特征表示。强化学习当然也有缺点:个体对于一些简单的不好的陷阱仍然需要通过多次尝试才能确定,这和人类的“一朝被蛇咬十年怕井绳”不太一样。期待强化学习在各领域的突破。好像蒙特卡罗树搜索已经能够避免类似的情况发生了。

另外想说的是机器学习库选择的问题。pytorch的出现让人眼前一亮,这里极力推荐大家在试验新想法时使用pytorch,极容易上手,而且非常方便。Pytorch的官方教程和示例也提供了一个强化学习的代码,值得学习。

闲话少说,开始进入Pytorch的LSTM-RNN应用于自然语言处理(NLP)之旅。

循环神经网络简要介绍

时序数据

深度学习通常接受大量的数据作为输入,这里的大量不仅体现在每一次被送入网络输入层的数据大,网络的输入层有多少个神经元(节点)就意味着网络一次能接受多少输入数据,这些输入数据又被称为原始输入特征,其数量一般用字母 \(n\) 表示;同时也体现在训练过程中会接受许许多多次的输入,这里的许许多多次即表示的数据集的大小或样本数量,一般用字母 \(m\) 来表示。用 n 表示特征数、m 表示数据集大小深受Andrew Ng其在Coursera上的Machine Learning公开课的影响(最近他又开设了深度学习公开课系列)。如果数据集中前后样本之间存在着明显的时间联系,那么这类数据就称为时序数据了。典型的时序数据有:音频数据、视频数据、多个单词组成的有意义的一句话。在循环神经网络出现之前,解决这类问题最常用的办法是确定一个时间窗口,把这个时间窗口里包含的多个样本作为一个整体形成一个新的样本来分析。使用这种方法需要平衡窗口的大小,窗口过小不能抓住时序特征,窗口过大问题有可能复杂到无法解决。而循环神经网络则不需要设置这样的窗口,只需要依照原来的次序依次将样本送入网络即可,网络本身具有记住样本先后次序的能力。

循环神经网络的类型

循环神经网络能够记住样本的先后次序这一点听起来很新奇,同时很多人好奇它是如何做到、如何体现出来的。这里借用一下Karpathy的一张图解来作简单解释:

上图一共有五个神经网络,每一个网络里红色矩形表示某个输入数据,由于一次送入网络的数据数据是由n个特征组成,因此又可以以向量来称呼它;蓝色的则表示隐藏层的输出向量,输出向量的维度不一定是n,可以根据要解决的问题本身确定大小;绿色的则表示隐藏层。箭头则表示向量的变换,例如矩阵乘法等。这五个网络各有特点,从左到右依次看:

  1. 第一个网络不是循环网络,它接受一次输入向量,给出一个输出向量。这类网络最典型的应用就是图片分类,将一张图片数据送入网络,网络给出该图片包含猫、狗、人、汽车等图案的可能性。
  2. 第二个网络是一次输入多个输出,可以把他看成是输入一张图片,输出一句话,这句话中的每一个词是图中的一个蓝色输出,由于输出是有序的,如果是一个训练好的网络,它给出的这些有序输出的词连起来形成一句话,而这句话可以用来描述图片,例如送入网络一张狗正在啃骨头的照片,网络可以给出输入:“一只 正在 啃 骨头 的 狗”。
  3. 第三个网络则是多输入单输出,比较典型的例子是,送入网络一定数量有序的单词,网络输出这些有序的词能够组成合乎语法和日常表达的一句话的可能性。比如我们送给网络的输入是如下三个词:”我”、“喜欢” 、“你”,一个训练好的网络输出可以接近1,表明网络输入的是一个正确的句子。当然这只是这类网络的最简单的应用,你可以根据任务的不同设置不同的输入输出。
  4. 第四个网络则是多输入多输出,但是输出与输入不同步。这一类比较典型的应用就是机器翻译了。比如说,我们送入网络的由英语单词组成的句子 I like you. 通过额外设置一个句子终止标记(EOS,实际上人们也是用标点来表示句子终止,对于网络需要用一次输入来表示句子终止)。当网络接收到句子终止的输入后,开始给出输出。一个训练好会英汉翻译的网络应该在其输出层输出:“我 喜欢 你 EOS”。请不要认为这是对单个词的逐个翻译,这里只是恰好词的次序相同罢了。如果网络实现的是英法翻译,那么其输出将是 “Je t\\\’aime EOS”. 这里可以只有两次有意义的输出。
  5. 最后一个网络同样也是多输入多输出,与前一个不同的是当第一个输入送入网络时,网络就给出了输出,这种应用比较典型的例子是用来对一个视频的每一帧进行标签,对后续帧的标签一定程度上依赖于前面多帧的画面,或者是对于一句话中出现的每一个词进行词性标注。

数据共享的隐藏层循环单元

上图非常直观的解释了RNN网络的应用。这里有一个问题:上图的五个图中,其表示隐藏层的绿色矩形在每一个网络中的数量不一样,在解决问题时也不知道输入和输出序列的长度,那么我们如何选择隐藏层矩形的数量呢?其实,上图只是为了说明的方便才如此绘制的,真正的一个隐藏层只有一个矩形,该矩形同时接受来自输入层的向量和来自隐藏层本身的输出向量,图中绘制的多个隐藏层只是单个隐藏层在时间层面的展开而已。这里不得不借用下这篇文章里的图了:

等式左侧是一个典型的循环网络构成,读者可以用上下位置关系和前一张图中的网络对应起来理解该图中 \(x_t\) ,\(A\) 和 \(h_t\) 。可以看出这些输入输出数据都带了个下标 t,表明其是随时间变化的时序数据。而隐藏层 A 并没有带 t 下标,表明其在不同时刻使用的数据都是一套数据,不随时间变化。

搞清楚了循环神经网络的大体结构,我们至少可以回答一个问题,那就是一个循环神经网络能够接受多长的时间序列?答案是“想多长有多长”,只要你确定了每一次输入向量的维度,按照这个维度来设计循环神经网络的输入即可。您可以持续地把这个维度的数据作为输入送入网络观察或训练其输出。理论上来说循环神经网络可以记住任何长度内发生的有意义的事情,但实际效果怎样就要个别看待了。那么我们如何来具体设计一个循环神经网络呢,比如说如果我的输出是n_in维、输出是n_out维的,那么我隐藏层应该选用多大的矩阵呢?这些矩阵的参数又是如何学习的呢?要回答这两个问题就要看你使用什么样的循环单元了,不同的循环单元内保存了不同套数的不同维度的矩阵,这部分就是值得研究而且已经被大量研究了的地方,主要有LSTM和GRU两大类,具体就不介绍了,读者自行阅读相关文章来理解吧。

通过使用成熟的机器学习库,我们不一定非要完全理解了RNN循环单元的工作机制才能写出RNN的应用。我们现在开始进入pytorch官方提供的LSTM-RNN应用于NLP处理的教程。

简单认识Pytorch里的LSTM-RNN

下文假设你对基本的Python使用以及numpy库有一定的认识,但不需要你熟悉Pytorch的使用,我会结合例子来讲解如何使用pytorch提供的关于RNN的模块来快速构建模型并查看运行效果,遇到Pytorch的基本方法,我也会对其做一个简单的解释。

值得注意的是:Pytorch里的LSTM单元接受的输入都必须是3维的张量(Tensors).每一维代表的意思不能弄错。第一维体现的是序列(sequence)结构,第二维度体现的是小块(mini-batch)结构,第三位体现的是输入的元素(elements of input)。如果在应用中不适用小块结构,那么可以将输入的张量中该维度设为1,但必须要体现出这个维度。下文的例子中,我们就不打算使用小块结构。

假如我们要把下面这句话“The cow junped”送入网络,那么输入看起来像下面这样:

\begin{split}\begin{bmatrix} \overbrace{q_\text{The}}^\text{row vector} \ q_\text{cow} \ q_\text{jumped} \end{bmatrix}\end{split}

注:上式中的 q 后面跟一个单词,表示该单词的一定维度的向量表示,该维度即是LSTM接受的张量中的第三个维度。

记住这里存在一个尺寸为1的第二维度。此外,如果你希望一次在网络中走完整个序列,你可以将第一个维度的尺寸也设为1。

我们来快速看一个例子。

先导入需要的常用模块

我们设置一个LSTM单元,准备一些输入数据,运行一下看看这些数据的结构是什么样的。

如果你是第一次接触Pytorch,可能对这段代码有点陌生。第一句生成了一个LSTM对象,通过观察LSTM实现的代码发现,这里的LSTM不是RNN里面的一个结构,而是继承自RNN类的子类,而RNN又有一个父类RNNBase。再向上追溯,我们发现RNNBase还有一个父类Module类。事实上Module类是Pytorch中所有完成一定网络功能的基类。你也可以通过继承该类而定义自己的神经网络。在实现自己的神经网络时,一般需要重写其forward方法。同样Module类实现了__call__方法,意味着你在使用该类对象时可以像把他看成一个方法来调用,就像在上面代码的for循环里那样。

RNNBase类里的__init__()和forward()方法非常值得一读,可以说理解了这两段代码,基本上也就理解了分别基于“LSTM”和“GRU”循环网络的区别和他们实现记忆功能的具体机制。这部分留给读者自己去体会吧。

上段代码中在生成一个输入数据时使用到了Pytorch的两个基本概念:Pytorch的张量和变量。读者可以把Pytorch的张量理解为numpy库中的张量,而且大部分对于张量的操作与numpy内对张量的操作方法一致。当你使用语句:

时,就生成了一个维度为(1,1,3)的pytorch以一定高斯分布随机生成的张量,与TensorFlow不同的时,该语句一经执行,该张量的值也就生成了,你可以较为方便的去监视里面的数据。也有很多其他生成Pytorch张量的方法。对于Pytorch变量,则是在Pytorch张量基础上的一层包装,其目的主要是为了配合实现梯度的自动计算。关于Pytorch张量与变量的关系,可以阅读该文章,这里不详细展开。

上面代码的第三句声明了一个hidden的变量,该变量是一个元组,其第一个元素是LSTM的隐藏层输出,另一个元素维护了隐藏层的状态。

随后的代码通过循环将输入数据依次送入LSTM网络内运算,最终输出结果和hidden变量的数据。

读者可以自己对部分维度参数进行修改来了解这些参数之间的相互关系。下面的代码则从另一个角度做了之前差不多的事情:一次对序列里的所有数据进行运算。

对比两次输出的out和hidden的结构,发现后一次out的数据包括整个序列长度,而前一次out的数据仅是最后一个输入样本对应的输出。而hidden的数据结构是一样的。

以上只是对Pytorch里LSTM类的简单认识,接下来我们要正式把它用来做一些自然语言处理方面的事情了。

示例——使用LSTM对文本进行词性标注

在这一节中,我们将使用LSTM来对一句话里涉及的词进行词性标注,具体问题是这样的。

我们会给网络输入一个由 \(w_1, w_2, …, w_M\) 表示的时序数据,其中 \(w_i \in V\) ,V 是我们的词汇表。同样我们有一个标签数据集合:T,同时用 \(y_i\) 表示标注 单词  \(w_i\) ,表示该单词的一个性质,比如这里我们使用“词性”这个性质。我们的网络同时也会给出关于某个单词的词性的预测 \(\hat{y_i}\) 。这相当于一个结构预测模型,其中输出也是一个序列 \(\hat{y_1}, \hat{y_2}, …, \hat{y_M}\) ,其中 \(\hat{y_i} \ in T\) 。

这相当于我们一开始展示的RNN类型中的最后一类。即将一个序列送入RNN网络,对于每一个输入给出一个输出,但是输出的给出不是基于当时的输入而是基于所有已送入网络的输入的。如果我们用 \(h_i\) 表示第 i 个时间步网络的隐藏层的输出,并且用一个索引来表示该输出的类型,那么网络预测的输出可以是隐藏层输出的Softmax回归:

$$\hat y_i =argmax_j(log Softmax(Ah_i b))_j$$

可以将最大输出值的神经元索引代表当时输入单词的词性标注。可以看出隐藏层输出的维度是标签书记集合的大小。

为了完成这个小小的示例,我们先要准备一些输入数据和输出数据,我们准备了两个句子,并且为这两个句子进行了词性标注,作为训练数据:

代码中的”DET”, “NN”, “V”分别代表的是冠词、名词、和动词。我们要给句子中出现的每一个单词一个索引,依次来构建整个词汇表 V ;同样也需要对每一个出现的词性做一个索引,建立一个标签数据集 T:

接下来我们就开始建立这个RNN模型,这个模型将继承自Module类:

这段代码不难理解,其中有一个nn.Embedding(vocab_size, embed_dim)类,它是Module类的子类,这里它接受最重要的两个初始化参数:词汇量大小,每个词汇向量表示的向量维度。Embedding类返回的是一个以索引表示的大表,表内每一个索引对应的元素都是表示该索引指向的单词的向量表示,大表具体是以矩阵的形式存储的。这就是著名的词向量。

类似的,上段代码中还出现了nn.linear(),它也是Module的子类,其forward()方法执行的是线性变换。还记得Module类实现了__call__方法,使得这些类的对象可以像函数方法一样被调用,调用的输出就是对应变换的结果。

F.log_softmax()执行的是一个Softmax回归的对数。

这样一个RNN网络模型就建立起来了,接下来我们要训练这个模型。由于模型只接受特定格式的输入,我们需要把以句子形式表示的输入转换成模型接受的输入格式,为此定义一个方法:

我们生成一个模型对象,同时与其它机器学习库类似,训练一个模型需要指出使用的损失函数、优化器等:


为了对比,我们先看看没有训练的模型其输入在准备前后以及网络输出分别是什么样子的,执行下列代码:

给出的输出如下:

在理解模型的输出时,我们要比较输出张量的每一行大小,用最大的数值对应的索引号去标签数据集 T 中找对应的词性。在我们的 T 中,一共存有3个词性,分别是:0 冠词DET;1 名词NN;2 动词V。在没有训练前,The dog ate the apple 这几个词对应的词性索引分别为: 0 0 2 2 2,也就是分别是: 冠词 冠词 动词 动词 动词,这明显是不符合的其在实际句子中的词性的,因为网络没有经过训练,使用的都是随机化参数得到的结果,不正确也是很正常的。

我们现在来训练这个网络模型,执行下面的代码:

这一次网络输出的结果是下面的样子:

和一开始对结果分析一样,这次我们测试的 The dog ate the apple 这句话中每个单词对应的词性索引分别是: 0 1 2 0 1,也就是分别是: 冠词 名词 动词 冠词 名词,这是他们正确的词性。当然,由于这里我们只是举个例子演示模型的构建和训练,我们使用的是训练集中的数据来测试。

从上面的示例中,读者应该能体会到循环神经网络在处理时序数据时是一种比较有效的监督学习方法。通过合理的设计网络模型的输入和输出,我们可以完成许多不同类别的监督学习任务,而Pytorch则提供了一套较为完善的模块工具,使得搭建和训练循环神经网络变得非常简单,读者的精力则可以聚焦在问题的设计上。这比几年前单纯依靠numpy库从头搭建一个LSTM-RNN要简单的多了。

期待基于深度学习人工智能继续不断取得突破。

文章来源:知乎专栏

PyTorch入门实战教程

发表评论

电子邮件地址不会被公开。 必填项已用*标注

评论列表(9)

  1. 最后执行代码的时候,为什么运行不出来?

    —————————————————————————
    TypeError Traceback (most recent call last)
    in ()
    9 targets = prepare_sequence(tags, tag_to_ix)
    10 # 运行我们的模型,直接将模型名作为方法名看待即可
    —> 11 tag_scores = model(sentence_in)
    12 # 计算损失,反向传递梯度及更新模型参数
    13 loss = loss_function(tag_scores, targets)

    /usr/local/lib/python3.5/dist-packages/torch/nn/modules/module.py in __call__(self, *input, **kwargs)
    222 for hook in self._forward_pre_hooks.values():
    223 hook(self, input)
    –> 224 result = self.forward(*input, **kwargs)
    225 for hook in self._forward_hooks.values():
    226 hook_result = hook(self, input, result)

    in forward(self, sentence)
    28 embeds.view(len(sentence), 1, -1), self.hidden)
    29 tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))
    —> 30 tag_scores = F.log_softmax(tag_space, dim=1)
    31 return tag_scores

    TypeError: log_softmax() got an unexpected keyword argument ‘dim’

  2. 请问怎么看出来这个结果的
    -0.7188 -2.0862 -0.9455
    -4.7905 -0.0240 -4.1756
    -2.8409 -3.4065 -0.0960
    -0.0747 -3.8882 -2.9658
    -4.2077 -0.0218 -5.0084

  3. 请问怎么看出来这个结果的
    -0.7188 -2.0862 -0.9455
    -4.7905 -0.0240 -4.1756
    -2.8409 -3.4065 -0.0960
    -0.0747 -3.8882 -2.9658
    -4.2077 -0.0218 -5.0084
    就是他们对应的结果为0 1 2 0 1

    1. 从上往下取最大值的位置,oneHot编码了解一下
      -0.7188 -2.0862 -0.9455 –>0
      -4.7905 -0.0240 -4.1756 —>1
      -2.8409 -3.4065 -0.0960 —>2
      -0.0747 -3.8882 -2.9658 ->0
      -4.2077 -0.0218 -5.0084 —->1

返回顶部