RNN

人类并不是每时每刻都从一片空白的大脑开始他们的思考。在你阅读这篇文章时候,你都是基于自己已经拥有的对先前所见词的理解来推断当前词的真实含义。我们不会将所有的东西都全部丢弃,然后用空白的大脑进行思考。我们的思想拥有持久性。

传统的神经网络并不能做到这点,看起来也像是一种巨大的弊端。例如,假设你希望对电影中的每个时间点的时间类型进行分类。传统的神经网络应该很难来处理这个问题——使用电影中先前的事件推断后续的事件。

RNN 解决了这个问题。RNN 是包含循环的网络,允许信息的持久化。

[译] 理解 LSTM 网络

RNN 包含循环

在上面的示例图中,神经网络的模块,A,正在读取某个输入 x_i,并输出一个值 h_i。循环可以使得信息可以从当前步传递到下一步。这些循环使得 RNN 看起来非常神秘。然而,如果你仔细想想,这样也不比一个正常的神经网络难于理解。RNN 可以被看做是同一神经网络的多次赋值,每个神经网络模块会把消息传递给下一个。所以,如果我们将这个循环展开:

[译] 理解 LSTM 网络

展开的 RNN
链式的特征揭示了 RNN 本质上是与序列和列表相关的。他们是对于这类数据的最自然的神经网络架构。

并且 RNN 也已经被人们应用了!在过去几年中,应用 RNN 在语音识别,语言建模,翻译,图片描述等问题上已经取得一定成功,并且这个列表还在增长。我建议大家参考 Andrej Karpathy 的博客文章—— The Unreasonable Effectiveness of Recurrent Neural Networks 来看看更丰富有趣的 RNN 的成功应用。

而这些成功应用的关键之处就是 LSTM 的使用,这是一种特别的 RNN,比标准的 RNN 在很多的任务上都表现得更好。几乎所有的令人振奋的关于 RNN 的结果都是通过 LSTM 达到的。这篇博文也会就 LSTM 进行展开。

长期依赖(Long-Term Dependencies)问题

RNN 的关键点之一就是他们可以用来连接先前的信息到当前的任务上,例如使用过去的视频段来推测对当前段的理解。如果 RNN 可以做到这个,他们就变得非常有用。但是真的可以么?答案是,还有很多依赖因素。有时候,我们仅仅需要知道先前的信息来执行当前的任务。例如,我们有一个 语言模型用来基于先前的词来预测下一个词。如果我们试着预测 “the clouds are in the sky” 最后的词,我们并不需要任何其他的上下文 —— 因此下一个词很显然就应该是 sky。在这样的场景中,相关的信息和预测的词位置之间的间隔是非常小的,RNN 可以学会使用先前的信息。

[译] 理解 LSTM 网络不太长的相关信息和位置间隔

但是同样会有一些更加复杂的场景。假设我们试着去预测“I grew up in France… I speak fluent French”最后的词。当前的信息建议下一个词可能是一种语言的名字,但是如果我们需要弄清楚是什么语言,我们是需要先前提到的离当前位置很远的 France 的上下文的。这说明相关信息和当前预测位置之间的间隔就肯定变得相当的大。不幸的是,在这个间隔不断增大时,RNN 会丧失学习到连接如此远的信息的能力。

[译] 理解 LSTM 网络相当长的相关信息和位置间隔

在理论上,RNN 绝对可以处理这样的 长期依赖 问题。人们可以仔细挑选参数来解决这类问题中的最初级形式,但在实践中,RNN 肯定不能够成功学习到这些知识。 Bengio, et al. (1994) 等人对该问题进行了深入的研究,他们发现一些使训练 RNN 变得非常困难的相当根本的原因。

然而,幸运的是,LSTM 并没有这个问题!

LSTM 网络

Long Short Term 网络—— 一般就叫做 LSTM ——是一种 RNN 特殊的类型,可以学习长期依赖信息。LSTM 由 Hochreiter & Schmidhuber (1997) 提出,并在近期被 Alex Graves 进行了改良和推广。在很多问题,LSTM 都取得相当巨大的成功,并得到了广泛的使用。

LSTM 通过刻意的设计来避免长期依赖问题。记住长期的信息在实践中是 LSTM 的默认行为,而非需要付出很大代价才能获得的能力!

所有 RNN 都具有一种重复神经网络模块的链式的形式。在标准的 RNN 中,这个重复的模块只有一个非常简单的结构,例如一个 tanh 层。

[译] 理解 LSTM 网络标准 RNN 中的重复模块包含单一的层

LSTM 同样是这样的结构,但是重复的模块拥有一个不同的结构。不同于 单一神经网络层,这里是有四个,以一种非常特殊的方式进行交互。

[译] 理解 LSTM 网络LSTM 中的重复模块包含四个交互的层

不必担心这里的细节。我们会一步一步地剖析 LSTM 解析图。现在,我们先来熟悉一下图中使用的各种元素的图标。

[译] 理解 LSTM 网络

LSTM 中的图标

在上面的图例中,每一条黑线传输着一整个向量,从一个节点的输出到其他节点的输入。粉色的圈代表 pointwise 的操作,诸如向量的和,而黄色的矩阵就是学习到的神经网络层。合在一起的线表示向量的连接,分开的线表示内容被复制,然后分发到不同的位置。

LSTM 的核心思想

LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。

[译] 理解 LSTM 网络Paste_Image.png

LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。

[译] 理解 LSTM 网络Paste_Image.png

Sigmoid 层输出 0 到 1 之间的数值,描述每个部分有多少量可以通过。0 代表“不许任何量通过”,1 就指“允许任意量通过”!

LSTM 拥有三个门,来保护和控制细胞状态。

逐步理解 LSTM

在我们 LSTM 中的第一步是决定我们会从细胞状态中丢弃什么信息。这个决定通过一个称为 忘记门层 完成。该门会读取h_{t-1}和x_t,输出一个在 0 到 1 之间的数值给每个在细胞状态C_{t-1}中的数字。1 表示“完全保留”,0 表示“完全舍弃”。

让我们回到语言模型的例子中来基于已经看到的预测下一个词。在这个问题中,细胞状态可能包含当前 主语 的类别,因此正确的 代词 可以被选择出来。当我们看到新的 代词 ,我们希望忘记旧的代词 。

[译] 理解 LSTM 网络

决定丢弃信息

下一步是确定什么样的新信息被存放在细胞状态中。这里包含两个部分。第一,sigmoid 层称 “输入门层” 决定什么值我们将要更新。然后,一个 tanh 层创建一个新的候选值向量,\tilde{C}_t,会被加入到状态中。下一步,我们会讲这两个信息来产生对状态的更新。

在我们语言模型的例子中,我们希望增加新的代词的类别到细胞状态中,来替代旧的需要忘记的代词。

[译] 理解 LSTM 网络

确定更新的信息

现在是更新旧细胞状态的时间了,C_{t-1}更新为C_t。前面的步骤已经决定了将会做什么,我们现在就是实际去完成。

我们把旧状态与f_t相乘,丢弃掉我们确定需要丢弃的信息。接着加上i_t * \tilde{C}_t。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。

在语言模型的例子中,这就是我们实际根据前面确定的目标,丢弃旧代词的类别信息并添加新的信息的地方。

[译] 理解 LSTM 网络

更新细胞状态

最终,我们需要确定输出什么值。这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。首先,我们运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。

在语言模型的例子中,因为他就看到了一个 代词 ,可能需要输出与一个 动词 相关的信息。例如,可能输出是否代词是单数还是负数,这样如果是动词的话,我们也知道动词需要进行的词形变化。

[译] 理解 LSTM 网络

输出信息

LSTM 的变体

我们到目前为止都还在介绍正常的 LSTM。但是不是所有的 LSTM 都长成一个样子的。实际上,几乎所有包含 LSTM 的论文都采用了微小的变体。差异非常小,但是也值得拿出来讲一下。

其中一个流形的 LSTM 变体,就是由 Gers & Schmidhuber (2000) 提出的,增加了 “peephole connection”。是说,我们让 门层 也会接受细胞状态的输入。

[译] 理解 LSTM 网络

peephole 连接

上面的图例中,我们增加了 peephole 到每个门上,但是许多论文会加入部分的 peephole 而非所有都加。

另一个变体是通过使用 coupled 忘记和输入门。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记。我们仅仅输入新的值到那些我们已经忘记旧的信息的那些状态 。

[译] 理解 LSTM 网络coupled 忘记门和输入门

另一个改动较大的变体是 Gated Recurrent Unit (GRU),这是由 Cho, et al. (2014) 提出。它将忘记门和输入门合成了一个单一的 更新门。同样还混合了细胞状态和隐藏状态,和其他一些改动。最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。

[译] 理解 LSTM 网络

GRU

这里只是部分流行的 LSTM 变体。当然还有很多其他的,如 Yao, et al. (2015) 提出的 Depth Gated RNN。还有用一些完全不同的观点来解决长期依赖的问题,如 Koutnik, et al. (2014) 提出的 Clockwork RNN。

要问哪个变体是最好的?其中的差异性真的重要吗? Greff, et al. (2015) 给出了流行变体的比较,结论是他们基本上是一样的。 Jozefowicz, et al. (2015) 则在超过 1 万中 RNN 架构上进行了测试,发现一些架构在某些任务上也取得了比 LSTM 更好的结果。

结论

刚开始,我提到通过 RNN 得到重要的结果。本质上所有这些都可以使用 LSTM 完成。对于大多数任务确实展示了更好的性能!

由于 LSTM 一般是通过一系列的方程表示的,使得 LSTM 有一点令人费解。然而本文中一步一步地解释让这种困惑消除了不少。

LSTM 是我们在 RNN 中获得的重要成功。很自然地,我们也会考虑:哪里会有更加重大的突破呢?在研究人员间普遍的观点是:“Yes! 下一步已经有了——那就是 注意力 !” 这个想法是让 RNN 的每一步都从更加大的信息集中挑选信息。例如,如果你使用 RNN 来产生一个图片的描述,可能会选择图片的一个部分,根据这部分信息来产生输出的词。实际上, Xu, et al. (2015) 已经这么做了——如果你希望深入探索 注意力 可能这就是一个有趣的起点!还有一些使用注意力的相当振奋人心的研究成果,看起来有更多的东西亟待探索……

注意力也不是 RNN 研究领域中唯一的发展方向。例如, Kalchbrenner, et al. (2015) 提出的 Grid LSTM 看起来也是很有钱途。使用生成模型的 RNN,诸如 Gregor, et al. (2015) Chung, et al.(2015) 和 Bayer & Osendorfer (2015) 提出的模型同样很有趣。在过去几年中,RNN 的研究已经相当的燃,而研究成果当然也会更加丰富!

语言模型

熟悉NLP的应该会比较熟悉,就是将自然语言的一句话『概率化』。具体的,如果一个句子有m个词,那么这个句子生成的概率就是:

P(w1,...,wm)=mi=1P(wiw1,...,wi1)P(w1,…,wm)=∏i=1mP(wi∣w1,…,wi−1)

其实就是假设下一次词生成的概率和只和句子前面的词有关,例如句子『He went to buy some chocolate』生成的概率可以表示为:  P(他喜欢吃巧克力) = P(他喜欢吃) * P(巧克力|他喜欢吃) 。

数据预处理

训练模型总需要语料,这里语料是来自google big query的reddit的评论数据,语料预处理会去掉一些低频词从而控制词典大小,低频词使用一个统一标识替换(这里是UNKNOWN_TOKEN),预处理之后每一个词都会使用一个唯一的编号替换;为了学出来哪些词常常作为句子开始和句子结束,引入SENTENCE_START和SENTENCE_END两个特殊字符。具体就看代码吧:

网络结构

和传统的nn不同,但是也很好理解,rnn的网络结构如下图:

rnn

A recurrent neural network and the unfolding in time of the computation involved in its forward computation.

不同之处就在于rnn是一个『循环网络』,并且有『状态』的概念。

如上图,t表示的是状态, xtxt 表示的状态t的输入, stst 表示状态t时隐层的输出, otot 表示输出。特别的地方在于,隐层的输入有两个来源,一个是当前的 xtxt 输入、一个是上一个状态隐层的输出 st1st−1 , W,U,VW,U,V 为参数。使用公式可以将上面结构表示为:

stŷ t=tanh(Uxt+Wst1)=softmax(Vst)st=tanh⁡(Uxt+Wst−1)y^t=softmax(Vst)

如果隐层节点个数为100,字典大小C=8000,参数的维度信息为:

xtotstUVW80008000100100×80008000×100100×100xt∈R8000ot∈R8000st∈R100U∈R100×8000V∈R8000×100W∈R100×100

初始化

参数的初始化有很多种方法,都初始化为0将会导致『symmetric calculations 』(我也不懂),如何初始化其实是和具体的激活函数有关系,我们这里使用的是tanh,一种推荐的方式是初始化为 [1n,1n][−1n,1n] ,其中n是前一层接入的链接数。更多信息请点击查看更多

 

前向传播

类似传统的nn的方法,计算几个矩阵乘法即可:

预测函数可以写为:

 

损失函数

类似nn方法,使用交叉熵作为损失函数,如果有N个样本,损失函数可以写为:

L(y,o)=1NnNynlogonL(y,o)=−1N∑n∈Nynlog⁡on

下面两个函数用来计算损失:

BPTT学习参数

BPTT( Backpropagation Through Time)是一种非常直观的方法,和传统的BP类似,只不过传播的路径是个『循环』,并且路径上的参数是共享的。

损失是交叉熵,损失可以表示为:

Et(yt,ŷ t)E(y,ŷ )=ytlogŷ t=tEt(yt,ŷ t)=tytlogŷ tEt(yt,y^t)=−ytlog⁡y^tE(y,y^)=∑tEt(yt,y^t)=−∑tytlog⁡y^t

其中 ytyt 是真实值, (̂ yt)(^yt) 是预估值,将误差展开可以用图表示为:

rnn-bptt1

所以对所有误差求W的偏导数为:

EW=tEtW∂E∂W=∑t∂Et∂W

进一步可以将 EtEt 表示为:

E3V=E3ŷ 3ŷ 3V=E3ŷ 3ŷ 3z3z3V=(ŷ 3y3)s3∂E3∂V=∂E3∂y^3∂y^3∂V=∂E3∂y^3∂y^3∂z3∂z3∂V=(y^3−y3)⊗s3

根据链式法则和RNN中W权值共享,可以得到:

E3W=k=03E3ŷ 3ŷ 3s3s3skskW∂E3∂W=∑k=03∂E3∂y^3∂y^3∂s3∂s3∂sk∂sk∂W

下图将这个过程表示的比较形象

rnn-bptt-with-gradients

BPTT更新梯度的代码:

梯度弥散现象

tanh和sigmoid函数和导数的取值返回如下图,可以看到导数取值是[0-1],用几次链式法则就会将梯度指数级别缩小,所以传播不了几层就会出现梯度非常弱。克服这个问题的LSTM是一种最近比较流行的解决方案。

tanh

Gradient Checking

梯度检验是非常有用的,检查的原理是一个点的『梯度』等于这个点的『斜率』,估算一个点的斜率可以通过求极限的方式:

Lθlimh0J(θ+h)J(θh)2h∂L∂θ≈limh→0J(θ+h)−J(θ−h)2h

通过比较『斜率』和『梯度』的值,我们就可以判断梯度计算的是否有问题。需要注意的是这个检验成本还是很高的,因为我们的参数个数是百万量级的。

梯度检验的代码:

 

SGD实现

这个公式应该非常熟悉:

W=WλΔWW=W−λΔW

其中 ΔWΔW 就是梯度,具体代码:

生成文本

生成过程其实就是模型的应用过程,只需要反复执行预测函数即可:

一、RNN网络简介
RNN网络的目的是用来处理序列数据,保存前后序列之间的前后关系,让网络对于信息具有记忆能力。对于传统的神经网络模型,它是从输入层到隐含层再到输出层,层与层之间是全连接的,每层之间的节点是无连接的。但是这种普通的神经网络对于一些问题却无能无力。例如,你要预测句子的下一个单词是什么,一般需要用到前面的单词,因为一个句子中前后单词并不是独立的,再比如你要判断一个人说话的情感,肯定也需要用到全局前后词的信息来进行判断。
在这个背景下,为了记忆前后序列之间的关系,RNN网络被提出。RNN叫循环神经网路,在该网络中一个序列当前的输出与前面的输出有关。具体的表现形式为网络会对前面的信息进行记忆并应用于当前输出的计算中,即隐藏层之间的节点不再无连接而是有连接的,并且隐藏层的输入不仅包括输入层的输出还包括上一时刻隐藏层的输出。理论上,RNN网络能够对任何长度的序列数据进行处理。但是在实践中,为了降低复杂性往往假设当前的状态只与前面的几个状态相关,下图便是一个典型的RNN网络:

这里写图片描述
这里写图片描述

RNN网络包含输入单元(Input units),输入集标记为{x0,x1,…,xt,xt+1,…},而输出单元(Output units)的输出集则被标记为{o0,o1,…,ot,ot+1,…}。RNN网络还包含隐藏单元(Hidden units),我们将其输出集标记为{s0,s1,…,st,st+1,…},这些隐藏单元完成了最为主要的工作。在上图中:有一条单向流动的信息流是从输入单元到达隐藏单元的,与此同时另一条单向流动的信息流从隐藏单元到达输出单元。在某些情况下,RNN网络会打破后者的限制,引导信息从输出单元返回隐藏单元,这些被称为“Back Projections”,并且隐藏层的输入还包括上一隐藏层的状态,即隐藏层内的节点可以自连也可以互连。
上图将循环神经网络进行展开成一个全神经网络。例如,对一个包含5个单词的语句,那么展开的网络便是一个五层的神经网络,每一层代表一个单词。对于该网络的计算过程如下:
(1)xt表示第t,t=1,2,3…步(step)的输入。比如,x1为第二个词的one-hot向量(根据上图,x0为第一个词);
注释:使用计算机对自然语言进行处理,便需要将自然语言处理成为机器能够识别的符号,加上在机器学习过程中,需要将其进行数值化。而词是自然语言理解与处理的基础,因此需要对词进行数值化,词向量(Word Representation,Word embeding)便是一种可行又有效的方法。何为词向量,即使用一个指定长度的实数向量v来表示一个词。有一种种最简单的表示方法,就是使用One-hot vector表示单词,即根据单词的数量|V|生成一个|V| * 1的向量,当某一位为一的时候其他位都为零,然后这个向量就代表一个单词。缺点也很明显:
(a)由于向量长度是根据单词个数来的,如果有新词出现,这个向量还得增加,麻烦;
(b)主观性太强(subjective);
(c)这么多单词,还得人工打labor并且adapt,想想就恐;
(d)最不能忍受的一点便是很难计算单词之间的相似性。
现在有一种更加有效的词向量模式,该模式是通过神经网或者深度学习对词进行训练,输出一个指定维度的向量,该向量便是输入词的表达。如word2vec。
(2)st为隐藏层的第t步的状态,它是网络的记忆单元。 st根据当前输入层的输出与上一步隐藏层的状态进行计算。st=f(U*xt+W*st−1),其中f一般是非线性的激活函数,如tanh或Relu,在计算s0时,即第一个单词的隐藏层状态,需要用到s-1,但是其并不存在,在实现中一般置为0向量;
(3)ot是第t步的输出,如下个单词的向量表示,ot=softmax(V*st)。
注意:(a)隐藏层状态st是网络的记忆单元。st包含了前面所有步的隐藏层状态。而输出层的输出ot只与当前步的st有关,在实践中,为了降低网络的复杂度,往往st只包含前面若干步而不是所有步的隐藏层状态;
(b)在传统神经网络中,每一个网络层的参数是不共享的。而在RNN网络中,每输入一步,每一层各自都共享参数U,V,W。其反应RNN网络中的每一步都在做相同的事,只是输入不同,因此大大地降低了网络中需要学习的参数;这里并没有说清楚,解释一下,传统神经网络的参数是不共享的,并不是表示对于每个输入有不同的参数,而是将RNN是进行展开,这样变成了多层的网络,如果这是一个多层的传统神经网络,那么xt到st之间的U矩阵与xt+1到st+1之间的U是不同的,而在RNN网络中的却是一样的,同理对于s与s层之间的W、s层与o层之间的V也是一样的。
(c)上图中每一步都会有输出,但是每一步都要有输出并不是必须的。比如,我们需要预测一条语句所表达的情绪,我们仅仅需要关系最后一个单词输入后的输出,而不需要知道每个单词输入后的输出。同理,每步都需要输入也不是必须的。RNN网络的关键之处在于隐藏层,隐藏层能够捕捉序列的信息。

二、RNN网络的实际应用
RNN网络已经在实践中被证明对NLP问题的实践是非常成功的。如词向量表达、语句合法性检查、词性标注、句子分类等。在RNN网络中,目前使用最广泛最成功的模型便是LSTMs(Long Short-Term Memory,长短时记忆模型)模型,该模型通常比vanilla RNNs能够更好地对长短时依赖进行表达,该模型相对于一般的RNN网络,只是在隐藏层做了手脚。对于LSTMs,后面会进行详细地介绍。下面对RNN网络在NLP中的应用进行简单的介绍。
1、语言模型与文本生成
给定一个词的序列,我们想预测在前面的词确定之后,每个词出现的概率。语言模型可以度量一个句子出现的可能性,这可以作为机器翻译的一个重要输入(因为出现概率高的句子通常是正确的)。能预测下一个词所带来的额外效果是我们得到了一个生成模型,这可以让我们通过对输出概率采样来生成新的文本。根据训练数据的具体内容,我们可以生成任意东西。在语言模型中,输入通常是词的序列(编码成one hot向量),输出是预测得到的词的序列。
2、机器翻译
机器翻译是将一种源语言语句变成意思相同的另一种源语言语句,如将英语语句变成同样意思的中文语句。与语言模型关键的区别在于,需要将源语言语句序列输入后,才进行输出,即输出第一个单词时,便需要从完整的输入序列中进行获取。机器翻译如下图所示:

这里写图片描述

3、语音识别
语音识别是指给一段声波的声音信号,预测该声波对应的某种指定源语言的语句以及该语句的概率值。
4、图像描述生成
和卷积神经网络(convolutional Neural Networks, CNNs)一样,RNNs已经在对无标图像描述自动生成中得到应用。将CNNs与RNNs结合进行图像描述自动生成。这是一个非常神奇的研究与应用。该组合模型能够根据图像的特征生成描述。如下图所示:

这里写图片描述

三、如何训练RNN网络
对于RNN是的训练和对传统的ANN训练一样。同样使用BP误差反向传播算法,不过有一点区别。如果将RNN网络进行展开,那么参数W,U,V是共享的,而传统神经网络却不是的。并且在使用梯度下降算法中,每一步的输出不仅依赖当前步的网络,并且还以来前面若干步网络的状态。比如,在t=4时,我们还需要向后传递三步,后面的三步都需要加上各种的梯度。该学习算法称为Backpropagation Through Time (BPTT)。后面会对BPTT进行详细的介绍。需要意识到的是,在vanilla RNN训练中,BPTT无法解决长时依赖问题(即当前的输出与前面很长的一段序列有关,一般超过十步就无能为力了),这是因为BPTT会带来所谓的梯度消失或梯度爆炸问题(the vanishing/exploding gradient problem)。当然,有很多方法去解决这个问题,如LSTM便是专门应对这种问题的。

四、RNN的改进模型
4.1 双向RNN网络
Bidirectional RNN(双向网络)的改进之处便是,假设当前的输出(第t步的输出)不仅仅与前面的序列有关,并且还与后面的序列有关。例如:预测一个语句中缺失的词语那么就需要根据上下文来进行预测。Bidirectional RNN是一个相对较简单的RNN网络,是由两个RNN网络上下叠加在一起组成的。输出由这两个RNN网络的隐藏层的状态决定的。如下图所示:

这里写图片描述

4.2 Deep(Bidirectional)RNN网络
Deep(Bidirectional)RNN与Bidirectional RNN相似,只是对于每一步的输入有多层网络。这样,该网络便有更强大的表达与学习能力,但是复杂性也提高了,同时需要更多的训练数据。Deep(Bidirectional)RNN的结构如下图所示:

这里写图片描述

4.3 Gated Recurrent Unit Recurrent Neural Networks(GRU)
GRU是一般的RNN的改良版本,主要是从以下两个方面进行改进。一是,序列中不同的位置处的单词(以单词举例)对当前的隐藏层的状态的影响不同,越前面的影响越小,即每个前面状态对当前的影响进行了距离加权,距离越远,权值越小。二是,在产生误差error时,误差可能是由某一个或者几个单词而引发的,所以应当仅仅对对应的单词weight进行更新。GRU的结构如下图所示。GRU首先根据当前输入单词向量word vector和前一个隐藏层的状态hidden state计算出update gate和reset gate。再根据reset gate、当前word vector以及前一个hidden state计算新的记忆单元内容(new memory content)。当reset gate为1的时候,new memory content忽略之前的所有memory content,最终的memory是之前的hidden state与new memory content的结合。

这里写图片描述

4.4 LSTM网络
RNN是深度学习领域用于解决序列问题的神器,从理论的上来说,RNN是可以实现长时间记忆的。然而RNN反向求导会出现梯度弥散或者梯度爆炸,导致我们很难训练网络,对于长时刻记忆总不尽人意,于是就诞生了LSTM。
LSTM与GRU类似,目前非常流行。它与一般的RNN结构本质上并没有什么不同,只是使用了不同的函数去计算隐藏层的状态。在LSTM中,i结构被称为cells,可以把cells看作是黑盒用以保存当前输入xt和之前的保存的状态ht-1,这些cells加一定的条件决定哪些cell抑制哪些cell兴奋。它们结合前面的状态、当前的记忆与当前的输入。如今已经证明,该网络结构在对长序列依赖问题中非常有效。LSTMs的网络结构如下图所示:

这里写图片描述

具体的变化过程如下图所示:

这里写图片描述

具体的计算公式如下所示:

这里写图片描述

上面的计算公式也可以通过下面二个图来进行理解:

这里写图片描述
这里写图片描述

4.5 LSTM与GRU的区别

这里写图片描述

从上图可以看出,LSTM和GRU之间非常相像,都能通过各种Gate将重要特征保留,保证其在long-term 传播的时候也不会被丢失,不同在于:
(1)new memory的计算方法都是根据之前的state及input进行计算,但是GRU中有一个reset gate控制之前state的进入量,而在LSTM里没有这个gate;
(2)产生新的state的方式不同,LSTM有两个不同的gate,分别是forget gate (f gate)和input gate(i gate),而GRU只有一个update gate(z gate);
(3)LSTM对新产生的state有一个output gate(o gate)可以调节大小,而GRU直接输出无任何调节。

 

 

 

通俗理解RNN

全连接神经网络和卷积神经网络他们都只能单独的取处理一个个的输入,前一个输入和后一个输入是完全没有关系的。但是,某些任务需要能够更好的处理序列的信息,即前面的输入和后面的输入是有关系的。比如,当我们在理解一句话意思时,孤立的理解这句话的每个词是不够的,我们需要处理这些词连接起来的整个序列;当我们处理视频的时候,我们也不能只单独的去分析每一帧,而要分析这些帧连接起来的整个序列。这时,就需要用到深度学习领域中另一类非常重要神经网络:循环神经网络(Recurrent Neural Network)。

RNN种类很多,也比较绕脑子。不过读者不用担心,本文将一如既往的对复杂的东西剥茧抽丝,帮助您理解RNNs以及它的训练算法,并动手实现一个循环神经网络。

语言模型

RNN是在自然语言处理领域中最先被用起来的,比如,RNN可以为语言模型来建模。那么,什么是语言模型呢?

我们可以和电脑玩一个游戏,我们写出一个句子前面的一些词,然后,让电脑帮我们写下接下来的一个词。比如下面这句:

我昨天上学迟到了,老师批评了____。
  • 1
  • 2

我们给电脑展示了这句话前面这些词,然后,让电脑写下接下来的一个词。在这个例子中,接下来的这个词最有可能是『我』,而不太可能是『小明』,甚至是『吃饭』。

语言模型就是这样的东西:给定一个一句话前面的部分,预测接下来最有可能的一个词是什么。

语言模型是对一种语言的特征进行建模,它有很多很多用处。比如在语音转文本(STT)的应用中,声学模型输出的结果,往往是若干个可能的候选词,这时候就需要语言模型来从这些候选词中选择一个最可能的。当然,它同样也可以用在图像到文本的识别中(OCR)。

使用RNN之前,语言模型主要是采用N-Gram。N可以是一个自然数,比如2或者3。它的含义是,假设一个词出现的概率只与前面N个词相关。我们以2-Gram为例。首先,对前面的一句话进行切词:

我 昨天 上学 迟到 了 ,老师 批评 了 ____。
  • 1

如果用2-Gram进行建模,那么电脑在预测的时候,只会看到前面的『了』,然后,电脑会在语料库中,搜索『了』后面最可能的一个词。不管最后电脑选的是不是『我』,我们都知道这个模型是不靠谱的,因为『了』前面说了那么一大堆实际上是没有用到的。如果是3-Gram模型呢,会搜索『批评了』后面最可能的词,感觉上比2-Gram靠谱了不少,但还是远远不够的。因为这句话最关键的信息『我』,远在9个词之前!

现在读者可能会想,可以提升继续提升N的值呀,比如4-Gram、5-Gram…….。实际上,这个想法是没有实用性的。因为我们想处理任意长度的句子,N设为多少都不合适;另外,模型的大小和N的关系是指数级的,4-Gram模型就会占用海量的存储空间。

所以,该轮到RNN出场了,RNN理论上可以往前看(往后看)任意多个词。单向

循环神经网络是啥

循环神经网络种类繁多,我们先从最简单的基本循环神经网络开始吧。

基本循环神经网络
下图是一个简单的循环神经网络如,它由输入层、一个隐藏层和一个输出层组成:

这里写图片描述
纳尼?!相信第一次看到这个玩意的读者内心和我一样是崩溃的。因为循环神经网络实在是太难画出来了,网上所有大神们都不得不用了这种抽象艺术手法。不过,静下心来仔细看看的话,其实也是很好理解的。

如果把上面有W的那个带箭头的圈去掉,它就变成了最普通的全连接神经网络。x是一个向量,它表示输入层的值(这里面没有画出来表示神经元节点的圆圈);s是一个向量,它表示隐藏层的值(这里隐藏层面画了一个节点,你也可以想象这一层其实是多个节点,节点数与向量s的维度相同);U是输入层到隐藏层的权重矩阵;o也是一个向量,它表示输出层的值;V是隐藏层到输出层的权重矩阵。那么,现在我们来看看W是什么。循环神经网络的隐藏层的值s不仅仅取决于当前这次的输入x,还取决于上一次隐藏层的值s。权重矩阵 W就是隐藏层上一次的值作为这一次的输入的权重。

如果我们把上面的图展开,循环神经网络也可以画成下面这个样子:

这里写图片描述
这里写图片描述

双向循环神经网络

对于语言模型来说,很多时候光看前面的词是不够的,比如下面这句话:

我的手机坏了,我打算____一部新手机。
  • 1

可以想象,如果我们只看横线前面的词,手机坏了,那么我是打算修一修?换一部新的?还是大哭一场?这些都是无法确定的。但如果我们也看到了横线后面的词是『一部新手机』,那么,横线上的词填『买』的概率就大得多了。

在上一小节中的基本循环神经网络是无法对此进行建模的,因此,我们需要双向循环神经网络,如下图所示:

这里写图片描述
这里写图片描述

深度循环神经网络

前面我们介绍的循环神经网络只有一个隐藏层,我们当然也可以堆叠两个以上的隐藏层,这样就得到了深度循环神经网络。如下图所示:

这里写图片描述
这里写图片描述

循环神经网络的训练

循环神经网络的训练算法:BPTT

BPTT算法是针对循环层的训练算法,它的基本原理和BP算法是一样的,也包含同样的三个步骤:
1. 前向计算每个神经元的输出值;
2. 反向计算每个神经元的误差项值,它是误差函数E对神经元j的加权输入的偏导数;
3. 计算每个权重的梯度。

最后再用随机梯度下降算法更新权重。

循环层如下图所示:

这里写图片描述
前向计算

使用前面的式2对循环层进行前向计算:

[+]

RNN简介

循环神经网络是一类用于处理序列数据的神经网络。就像卷积网络是专门处理网格化数据X(如一个图像)的神经网络,循环神经网络是专门用于处理序列x(1),...,x(τ)的神经网络。正如卷积网络可以很容易地扩展到具有很大宽度和高度的图像,以及处理大小可变的图像,循环网络可以扩展到更长的序列,且大多数循环网络可以处理可变长度的序列。

从多层网络出发到循环网络,我们需要利用20世纪80年代机器学习和统计模型早期思想的优点:在模型的不同部分共享参数。参数共享使得模型能够扩展到不同形式的样本(这里指不同长度的样本)并进行泛华。如果我们在每个时间点都有一个单独的参数,不但不能泛化到训练时没有见过的序列长度,也不能在时间上共享不同序列长度和不同位置的统计强度。

为了简单起见,我们说的RNN是指在序列上的操作,并且该序列在时刻t(1τ)x(t)。在实际情况中,循环网络通常在序列上的小批量上操作,并且小批量的每项具有不同序列长度τ。此外,RNN可以应用于跨越两个维度的空间数据(如图像)。当应用于涉及时间的数据,并且将整个序列提供给网络之前就能观察到整个序列时,网络可具有关于时间向后的连接。



序列建模方法:展开计算图

计算图是形式化一组计算结构的方式,如那些涉及将输入和参数映射到输出和损失的计算。我们对展开(unfolding)递归或循环计算得到的重复结构进行解释,这些重复结构通常对应于一个事件链。展开这个计算图将导致深度网络结构中的参数共享。

如:考虑动态系统的经典形式:

s(t)=f(st1;θ)=f(f(st2;θ);θ)=...

其中s(t)称为系统的状态。s在时刻t的定义需要参考时刻t1时同样的定义,故上式是循环的。
以上述方式展开等式,就能得到不涉及循环的表达。现在我们用传统的有无环计算图表达。

这里写图片描述

另一个例子,考虑外部信号x(t)驱动的动态系统,

s(t)=f(s(t1),x(t);θ)

可以看到,当前状态包含了整个过去序列的信息。很多循环神经网络使用下式或类似的公式定义隐藏单元的值。为了表明状态是网络的隐藏单元,我们使用变量h代表状态重写式:

h(t)=f(h(t1),x(t);θ)

如下图所示,典型RNN会增加额外的架构(我们所说的展开(unfolding)就是这个操作)。

这里写图片描述

当训练循环网络根据过去预测未来时,映射任意长度的序列(x(t),x(t1),...,x(2),x(1))到一固定长度的向量h(t).根据不同的训练准则,摘要可能选择性地精确保留过去序列的某些方面。例如,如果在统计语言建模中使用的RNN,通常给定前一个词预测下一个词,可能没有必要存储时刻t前输入序列中的所有信息;而仅仅存储足够预测句子其余部分的信息。

我们可以用一个函数g(t)代表t步展开后的循环:

h(t)=g(t)(x(t),x(t1),...,x(2),x(1))=f(h(t1),x(t);θ)

函数g(t)将全部的过去序列(x(t),x(t1),...,x(2),x(1))作为输入来生成当前状态,展开的循环架构允许我们将g(t)分解为函数f的重复应用。因此,展开过程引入两个主要优点:

  • 无论序列的长度,学成的模型始终具有相同的的输入大小,因为它指定的是从一种状态到另一种状态的转移,而不是在可变长度的历史状态上操作。
  • 我们可以在每个时间步使用相同参数的相同转移函数f

这两个因素使得学习在所有时间步和所有序列长度上操作单一的模型f 是可能的,而不需要在所有可能时间步学习独立的模型g(t)。学习单一的共享模型允许泛化到没有见过的序列长度(没有出现在训练集中),并且估计模型所需的训练样本远远少于不带参数共享的模型


循环神经网络

基于展开和参数共享的思想,我们可以设计各种循环神经网络。

1. 每个时间步都有输出,并且隐藏单元之间有循环连接的循环网络

这里写图片描述

我们看一下图上的RNN的前向传播公式。这个图没有指定隐藏单元的激活函数。这里假设使用双曲正切激活函数。此外,图中没有明确指定何种形式的输出和损失函数。我们假定输出是离散的,如用于预测词或字符的RNN。表示离散变量的常规方式是把输出o作为每个离散变量可能值的非标准化对数概率。然后,我们可以应用softmax 函数后续处理后,获得标准化后概率的输出向量ŷ 。RNN 从特定的初始状态h(0)开始前向传播。从t=1t=τ的每个时间步,我们应用以下更新方程:

a(t)=b+Wh(t1)+Ux(t);a(t)hidden_unitinput
h(t)=tanh(a(t));Activation_functiontanh
o(t)=c+Vh(t)
ŷ (t)=softmax(o(t))

其中的参数bc连同权重矩阵UVW,分别对应于输入到隐藏、隐藏到输出和隐藏到隐藏的连接。这个循环网络将一个输入序列映射到相同长度的输出序列。与x序列配对的y的总损失就是所有时间步的损失之和。

我们在对模型在训练时,各个参数计算这个损失函数的梯度是计算成本很高的操作。梯度计算涉及执行一次前向传播(从左到右的传播),接着是由右到左的反向传播。运行时间是O(τ),并且不能通过并行化来降低,因为前向传播图是固有循序的;每个时间步只能一前一后地计算。前向传播中的各个状态必须保存,直到它们反向传播中被再次使用,因此内存代价也是O(τ)。应用于展开图且代价为O(τ)的反向传播算法称为通过时间反向传播(back-propagation through time, BPTT).


2.每个时间步都有输出,当前时刻的输出到下个时刻的隐藏单元之间有连接的循环网络

这里写图片描述

仅在一个时间步的输出和下一个时间步的隐藏单元间存在循环连接的网络没有那么强大。因为这个网络缺少隐藏到隐藏的循环,它要求输出单元捕捉用于预测未来的关于过去的所有信息。而输出单元明确地训练成匹配训练集的目标,它们不太能捕获关于过去输入历史的必要信息,除非用户知道如何描述系统的全部状态,并将它作为训练目标的一部分。消除隐藏到隐藏循环的优点在于,任何基于比 时刻t的预测和时刻t的训练目标的损失函数中的所有时间步都解耦了。因此训练可以并行化,即在各时刻t分别计算梯度。因为训练集提供输出的理想值,所以没有必要先计算前一时刻的输出。

这里写图片描述

训练时,直接将上一层的期望输出连接下一层的隐藏单元,这样我们训练的时,是可以并行运算的。


3.隐藏单元之间存在循环连接,读取整个序列后产生单个输出的循环网络

这里写图片描述



双向RNN

目前为止我们考虑的所有循环神经网络有一个”因果”结构,意味着在时刻t的状态只能从过去的序列x(1),x(2),...,x(t1))以及当前的输入x(t)捕获信息。我们还讨论了某些在y可用时,允许过去的y值信息影响当前状态的模型。

然而,在许多应用中,我们要输出的y(t)的预测可能依赖于整个输入序列。例如,在语音识别中,由于协同发音,当前声音作为音素的正确解释可能取决于未来几个音素,甚至潜在的可能取决于未来的几个词,因为词与附近的词之间的存在语义依赖:如果当前的词有两种声学上合理的解释,我们可能要在更远的未来(和过去)寻找信息区分它们。这在手写识别和许多其他序列到序列学习的任务中也是如此.

这里写图片描述


基于编码-解码的序列到序列(Seq2Seq)架构

我们已经在前面的图看到RNN如何将输入序列映射成一个序列、如何将一个输入序列映射到等长的输出序列。本节我们讨论如何训练RNN,使其将输入序列映射到不一定等长的输出序列。这在许多场景中都有应用,如语音识别、机器翻译(汉翻英时长度通常不一致)或问答,其中训练集的输入和输出序列的长度通常不相同(虽然它们的长度可能相关)。

我们经常将RNN的输入称为”上下文”。我们希望产生此上下文的表示C。这个上下文C可能是一个概括输入序列X=x(1),x(2),...,x(nx))向量或者向量序列

这里写图片描述

用于映射可变长度序列到另一可变长度序列的RNN架构称为编码-解码或序列到序列架构,这个想法非常简单:

  1. 编码器(encoder)或读取器(reader) 或输入(input)RNN处理输入序列。
  2. 编码器输出上下文C(通常是最终隐藏状态的简单函数)。
  3. 解码器(decoder)或写入器(writer) 或输出(output) RNN则以固定长度的向量为条件产生输出

这种架构对比本章前几节提出的架构的创新之处在于长度nxny可以彼此不同。在序列到序列的架构中,两个RNN 共同训练以最大化logP(y(1),...,y(ny)|x(1),...,x(nx)))。编码器RNN的最后一个状态hnx通常被当作输入的表示C并作为解码器RNN的输入。

此架构的一个明显不足是,编码器RNN输出的上下文C的维度太小而难以适当地概括一个长序列。我们让C成为可变长度的序列,而不是一个固定大小的向量。此外,引入将序列C的元素和输出序列的元素相关联的注意力机制attention mechanism)。



RNN的依赖和不足

长期依赖的挑战

学习循环网络时,经过许多阶段传播后的梯度倾向于消失(大部分情况)或爆炸(很少,但对优化过程影响很大)。即使我们假设循环网络是参数稳定的(可存储记忆,且梯度不爆炸),但长期依赖的困难来自比短期相互作用指数小的权重(涉及许多Jacobian 相乘)。

特别的是,循环神经网络所使用的函数组合有点像矩阵乘法。我们可以认为,循环联系

h(t)=WTh(t1)

是一个非常简单的、缺少非线性激活函数和输入x的循环神经网络。这种递推关系本质上描述了幂法。它可以被简化为

h(t)=(Wt)Th(0)

而当W符合下列形式的特征分解(可对角化):

W=QΛQT

若其中Q正交,循环性可进一步简化为

h(t)=QTΛtQh(0)

特征值提升到t次后,导致幅值不到一的特征值衰减到零,而幅值大于一的就会激增。任何不与最大特征向量对齐的h(0)的部分将最终被丢弃

这个问题是针对循环网络的,在标量情况下,想象多次乘一个权重w。该乘积wt消失还是爆炸取决于w的幅值。


RNN学习中遇到的问题

RNN学习过程中遇到什么问题了?

我们通俗的来讲为什么RNN网络很难训练,如下图是一个RNN系统多次训练后,训练epoch与cost的关系:

这里写图片描述

我们可以看到sometimes时候,网络的cost抖动大,甚至是爆炸,运气好的时候碰上了收敛的情况。这意味着RNN网络训练起来比较难,工程上实现在比较难的。


为啥RNN学习会有问题?

我们从RNN的循环连接结构上理解为什么cost不稳定,下图是一个极其简化版的RNN网络:

这里写图片描述

这里我们简化了RNN的结构,我们假设有1000个神经元互联,且整个网络的结构极其简单。如图。

可以看到,因为是共享参数当设置权重在超过1和小于1的时候,经过多次循环迭代后,对应的输出会有剧烈的抖动

从数学的角度上来说:强非线性函数(如由许多时间步计算的循环网络)往往倾向于非常大或非常小幅度的梯度。看下图,目标函数存在一个伴随“断崖”的“地形”:宽且相当平坦区域被目标函数变化快的小区域隔开,形成了一种悬崖。

多参数之间的梯度下降关系,可以看到”断崖“的情况非常明显:
这里写图片描述

这导致的困难是,当参数梯度非常大时,梯度下降的参数更新可以将参数抛出很远,进入目标函数较大的区域,到达当前解所作的努力变成了无用功。梯度告诉我们,围绕当前参数的无穷小区域内最速下降的方向。这个无穷小区域之外,代价函数可能开始沿曲线背面而上。更新必须被选择为足够小,以避免过分穿越向上的曲面。我们通常使用衰减速度足够慢的学习率,使连续的步骤具有大致相同的学习率。适合于一个相对线性的地形部分的步长经常在下一步进入地形中更加弯曲的部分时变得不适合,会导致上坡运动。


解决办法: 针对梯度爆炸的情况

一个简单的解决方案已被从业者使用多年: 截断梯度(clipping the gradient)。此想法有不同实例。

  • 一种选择是在参数更新之前,逐元素地截断小批量产生的参数梯度
  • 另一种是在参数更新之前截断梯度g||g||:
    if||g||>v;ggv||g||

其中v是范数上界,g用来更新参数。因为所有参数(包括不同的参数组,如权重和偏置)的梯度被单个缩放因子联合重整化,所以后一方法具有的优点是保证了每个步骤仍然是在梯度方向上的,但实验表明两种形式类似。虽然参数更新与真实梯度具有相同的方向梯度,经过梯度范数截断,参数更新的向量范数现在变得有界。这种有界梯度能避免执行梯度爆炸时的有害一步。

这里写图片描述


解决办法:针对梯度消失的情况

梯度截断有助于处理爆炸的梯度,但它无助于消失的梯度。为了解决消失的梯度问题并更好地捕获长期依赖,我们讨论了如下想法:

  • 在展开循环架构的计算图中,沿着与弧度相关联的梯度乘积接近1的部分创建路径。实现这一点的一种方法是使用LSTM以及其他自循环和门控机制(后面会介绍LSTM)。
  • 另一个想法是正则化或约束参数,以引导”信息流”。特别是即使损失函数只对序列尾部的输出作惩罚,我们也希望梯度向量h(t)L在反向传播时能维持其幅度。


门控RNN

长短期记忆(LSTM)

现如今,实际应用中最有效的序列模型称为门控RNN(gated RNN)。包括基于长短期记忆(long short-term memory)和基于门控循环单元(gated recurrent unit)的网络。门控RNN想法是基于生成通过时间的路径,其中导数既不消失也不发生爆炸。门控RNN在每个时间步都可能改变的连接权重


为什么会提出LSTM?

RNN的工作关键点在于使用历史的信息(双向RNN带有整体性)帮助当前的决策。RNN可以更好地利用传统神经网络结构所不能建模的信息,但同时,这也带来了更大的技术挑战–长期依赖(long-term dependencies)问题。

在有些有问题上,模型仅仅需要短期内信息执行当前的任务。例如预测短语“大海的颜色是蓝色”中“蓝色”,模型并不需要记忆这个短语之前之前更长的上下文信息(大海和颜色包含了足够的信息了)。在这样的场景下,相关信息和待预测的词的位置之间的间隔很小,RNN可以较容易地利用先前信息。

但同样也会遇到一些上下文场景复杂的情况。例如做语文的阅读理解问题。仅根据短期依赖无法很好的解决问题。根据上面分析的RNN学习过程会遇到的问题,在复杂场景下,循环网络的学习梯度容易爆炸/消失。

长短时记忆网络(LSTM)可以较好的解决这一问题。与单一的tanh循环体结构不同,LSTM是一种拥有三个“门”结构的特殊网络结构。LSTM靠这些“门”的结构让信息有选择性地影响循神经网络中每个时刻的状态。“门”结构常是由sigmoid神经网络和一个按位做乘法的操作合并而成。


LSTM的结构

LSTM块如图所示,在浅循环网络架构下,LSTM循环网络的除了外部的RNN循环外,还具有内部的“LSTM细胞”循环。因此LSTM不是简单地向输入和循环单元的仿射变换之后施加一个逐元素的非线性。与普通的循环网络类似,每个单元有相同的输入和输出,也有更多的参数和控制信息流动的门控单元系统

这里写图片描述

一个LSTM块有四个输入:

  • 输入(input) : 模块的输入
  • 输入门(input gate): 控制输入
  • 遗忘门(forget gate):控制是否更新记忆单元(memory cell)
  • 输出门(output gate):控制输出

我们可以把LSTM块看成是原RNN循环网络的单个循环单元:

这里写图片描述


LSTM的传播公式

如图是一个简化的LSTM结构图:

这里写图片描述

可以看到每个LSTM Block内的Cell Memory的更新公式是:

c=g(z)f(zi)+cf(zf)

输出为:

a=h(c)f(zo)

对于inputgateforgetgateoutputgate的输出分别为f(zi)f(zf)f(zo): 输入xt连接了zf(遗忘门权重)、zi(输入门权重)、z(输入权重)、zo():
这里写图片描述

LSTM循环网络的整个架构(截取):
这里写图片描述

在多个LSTM连接的循环网络中,单个的LSTM的各个门的控制方式如下:

  • “遗忘门”:根据当前的输入xt、上一时刻状态ct1和上一时刻输出ht1共同决定
  • “输入门”: 根据输入xtct1ht1决定那些部分将进入当前时刻的状态ct
  • “输出门”: 根据当前的状态ct、当前输入xt和上一时刻输出ht1共同决定该时刻输出ht

整个LSTM网络的推导式

这里写图片描述

LSTM中最重要的组成部分是状态单元s(t)i,这是由遗忘门(forget gate)f(t)i控制(时刻t和细胞i),由sigmoid单元将权重设置为0和1之间的值:

f(t)i=σ(bfi+jUfix(t)j+jWfi,jh(t1)j)

其中x(t)是当前输入向量,h(t)是当前隐藏层向量,h(t)包含所有LSTM细胞的输出。b(f)U(f)W(f)分别是偏置、输入权重和遗忘门的循环权重。因此LSTM细胞内部状态以如下方式更新,其中有一个条件的自环权重f(t)i

s(t)i=f(t)is(t1)i+g(t)iσ(bi+jUix(t)j+jWi,jh(t1)j)

其中b,U,W分别是LSTM 细胞中的偏置、输入权重和遗忘门的循环权重。外部输入门(external input gate) 单元g(t)i以类似遗忘门(使用sigmoid获得一个0和1之间的值)的方式更新,但有自身的参数:

g(t)i=σ(bgi+jUgix(t)j+jWgi,jh(t1)j)

LSTM 细胞的输出h(t)i也可以由输出门(output gate)g(t)i关闭(使用sigmoid单元作为门控):

h(t)i=tanh(s(t)i)q(t)i
q(t)i=σ(boi+jUoix(t)j+jWoi,jh(t1)j)

其中bo,Uo,Wo分别是偏置、输入权重和遗忘门的循环权重,在这些变体中,可以选择使用细胞状态s(t)i作为额外的输入(及其权重),输入到第i个单元的三个门,这将需要三个额外的参数。



其他门控RNN

这里主要介绍GRU(门控循环单元),GRU与LSTM的主要区别是,单个门控单元同时控制遗忘因子和更新状态单元的决定。更新公式如下:

h(t)i=u(t1)ih(t1)i+(1u(t1)i)σ(bi+jUti,jx(t)j+jWi,jr(t1)jh(t1)j)

其中u代表”更新”门,r表示”复位”门。它们的值就如通常所定义的:

u(t)i=σ(bui+jUui,jx(t)j+jWui,jh(t)j)

r(t)i=σ(bri+jUri,jx(t)j+jWri,jh(t)j)

复位和更新门能独立地“忽略”状态向量的一部分。更新门像条件渗漏累积器一样可以线性门控任意维度,从而选择将它复制(在sigmoid的一个极端)或完全由新的“目标状态”值(朝向渗漏累积器的收敛方向)替换并完全忽略它(在另一个极端)。复位门控制当前状态中哪些部分用于计算下一个目标状态,在过去状态和未来状态之间引入了附加的非线性效应。

围绕这一主题可以设计更多的变种。例如复位门(或遗忘门)的输出可以在多个隐藏单元间共享。或者,全局门的乘积(覆盖一整组的单元,例如整一层)和一个局部门(每单元)可用于结合全局控制和局部控制。然而,一些调查发现这些LSTM和GRU架构的变种,在广泛的任务中难以明显地同时击败这两个原始架构。关键因素是遗忘门,向LSTM遗忘门加入1的偏置能让LSTM变得与已探索的最佳变种一样健壮。



自然语言建模

自然语言处理(Natural Language Processing,NLP)让计算机能够使用人类语言,例如中文或英文。为了让简单的程序能够高效明确地解析,计算机程序通常读取和发出特殊化的语言。而自然的语言通常是模糊的,并且可能不遵循形式的描述。自然语言处理中的应用如机器翻译,学习者需要读取一种人类语言的句子,并用另一种人类语言发出等同的句子。许多 NLP 应用程序基于语言模型, 语言模型定义了关于自然语言中的字、字符或字节序列的概率分布

为了构建自然语言的有效模型,通常必须使用专门处理序列数据的技术。在很多情况下,我们将自然语言视为一系列词,而不是单个字符或字节序列。因为可能的词总数非常大,基于词的语言模型必须在极高维度和稀疏的离散空间上操作。为使这种空间上的模型在计算和统计意义上都高效,研究者已经开发了几种策略。

ngram

语言模型(language model)定义了自然语言中标记序列的概率分布。根据模型的设计,标记可以是词、字符、甚至是字节。 标记总是离散的实体。最早成功的语言模型基于固定长度序列的标记模型,称为n-gram。一个n-gram是一个包含n个标记的序列。

这里写图片描述

这里写图片描述

注解:
依据(12.5)的公式:

P(THE,DOG,RAN,AWAY)=P2(THE,DOG)P(RAN|THE,DOG)P(AWAY|DOG,RAN)
P2(THE,DOG)P(RAN|THE,DOG)=P3(THE,DOG,RAN)//
P(THE,DOG,RAN,AWAY)=P3(THE,DOG,RAN)P(AWAY|DOG,RAN)

依据(12.6)的公式:

P(AWAY|DOG,RAN)=P3(DOG,RAN,AWAY)P2(DOG,RAN)

联立上述式子:

P(THE,DOG,RAN,AWAY)=P3(THE,DOG,RAN)P3(DOG,RAN,AWAY)P2(DOG,RAN)

这里写图片描述

这里写图片描述

神经语言模型

神经语言模型(Neural Language Model, NLM)是一类用来克服维数灾难的语言模型,它使用词的分布式表示对自然语言序列建模 (Bengio et al., 2001b)。不同于基于类的ngram模型,神经语言模型在能够识别两个相似的词,并且不丧失将每个词编码为彼此不同的能力。 模型为每个词学习的分布式表示,允许模型处理具有类似共同特征的词来实现这种共享。

为什么要将字词转为向量形式?

在神经语言模型出现之前,NLP通常将字词转为离散的单独的符号,例如将“中国”转为编号5178的特征,将“北京”转为3987的特征。即one-hot Encoder。一个词对应一个向量(向量中只有一个值为1,其余为0),可以想象,这样的表示方法表示整个的词汇库是一个超高维矩阵,例如需要将一篇文章中每一个词都转成一个向量,则整篇文章表示成一个稀疏矩阵。

使用One-Hot Encoder有一个问题,即我们对特征的编码往往是随机的,没有提供任何关联信息,没有考虑到字词间可能存在的关系。例如上述的“中国”和“北京”之间的关系在编码过程中丢失了。这不是我们想看见的。同时,将字词存储为稀疏向量的话,我们需要更多的数据来训练,因为稀疏数据训练的效率较低,计算也繁琐。

很自然地,我们就想到使用向量表达字词,向量空间模型可将字词转为连续值的向量表达,其中意思相近的词(属性类似)将被映射到向量空间中相近的位置。

将字词转为向量形式有什么优点?

我们认为神经语言模型是一类可以克服维数灾难的模型,它使用词的分布式表示对自然语言序列建模。神经语言模型在能够识别两个相似的词,并且不丧失将每个词编码为彼此不同的能力。向量空间模型共享一个词(及其上下文)和其他类似词(和上下文之间)的统计强度(即向量空间模型在NLP中主要依赖的假设是Distributional Hypothesis)。

使用这样的模型有许多好处,例如,如果词 dog 和词 cat 映射到具有许多属性的表示,则包含词 cat 的句子可以告知模型对包含词dog的句子做出预测,反之亦然。因为这样的属性很多,所以存在许多泛化的方式,可以将信息从每个训练语句传递到指数数量的语义相关语句。维数灾难需要模型泛化到指数多的句子(指数相对句子长度而言)。该模型通过将每个训练句子与指数数量的类似句子相关联克服这个问题。

我们有时将这些词表示称为词嵌入(word embedding)。在这个解释下,我们将原始符号视为维度等于词表大小的空间中的点。词表示将这些点嵌入到较低维的特征空间中。在原始空间中,每个词由一个one-hot向量表示,因此每对词彼此之间的欧氏距离都是2‾√。在嵌入空间中,经常出现在类似上下文(或共享由模型学习的一些”特征”的任何词对)中的词彼此接近。这通常导致具有相似含义的词变得邻近。图 12.3 放大了学到的词嵌入空间的特定区域,我们可以看到语义上相似的词如何映射到彼此接近的表示。

这里写图片描述

语言模型评价指标–复杂度(perplexity)

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

Word2Vec

循环神经网络在NLP(Nature Language Processing)领域最常使用的神经网络结构,和CNN在图像识别领域的地位类似。而Word2Vec是将语言中的字词转换为计算机可以理解的稠密向量(Dense Vector),进而可以做其他自然语言处理任务,比如文本分类、词性标注、机器翻译等

Word2Vec也称Word Embeddings,Word2Vec是一个可以将语言中字词转为向量形式表达(Vector Representations)的模型。

向量空间模型的分类

大致分为两类:

  • 一类是计数模型,例如Latent Semantic Analysis。计数模型统计在语料库中,相邻出现的词的频率,再把这些计数统计结果转为小而稠密的矩阵;
  • 另一类是预测模型,例如Neural Probabilistic Language Models。预测模型根据一个词周围相邻的词推测出这个词,以及它的空间向量。

Word2Vec即是一种计算高效的、可以从原始语料中学习字词空间向量的预测模型。它主要分为两种模式:

  • CBOW(Continuous Bag of Words),从原始语句推测目标字词,对小型数据比较合适
  • Skip-Gram相反,从目标字词推测出原始语句,在大型语料表现更好

预测模型通常使用最大似然的方法,在给定前面的语句h的情况下,最大化目标词汇wt的概率。这存在一个比较严重的问题是计算量非常大,需要计算词汇表中所有单词出现的可能性。在Word2Vec的CBOW模型中,不需要计算完整的概率模型,只需要训练一个二元的分类模型,用来区分真实的目标词汇和编造的词汇(噪声)这两类。

Skip-Gram模式的Word2Vec

在本节中我们主要使用Skip-Gram模式的Word2Vec,先来看训练样本的构造,以

the quick brown fox jumped over the lazy dog

为例,我们要构造一个语境与目标词汇的映射关系,其中语境包括一个单词左边和右边的词汇,假设我们的滑窗尺寸为1,可以制造的映射关系包括

[the,brown]quick[quick,fox]brown[brown,jumped]fox

因为Skip-Gram模型是从目标词汇预测语境,所有训练样本不再是

[the,brown]quick,quickthequickbrown

我们的训练集变成了

(quick,the)(quick,brown)(borwn,quick)(brown,fox)

我们训练时,希望模型能从目标词汇quick上预测出语境the,需要制造随机的词汇作为负样本(噪声),我们希望预测的概率分布在正样本the上尽可能的大,而在随机产生的负样本上尽可能的小。在实际实现过程中,是通过优化算法例如SGD来更新模型中Word Embedding的参数,让概率分布的损失函数(NCE Loss)尽可能小。这样每个单词的Embedding Vector就会随着循环过程不断调整,直到处于一个最适合语料的空间位置。

Word2Vec在Tensorflow上的实现

代码编写

1. 导入模块,下载数据集并读取到列表中

使用urllib.urlretrieve下载数据的压缩文件并校验文件是否完整.
如果已经下载了数据原下载地址(在filename找到了,就跳过下载了,数据集text8.zip大小31.3M,如果网络不好,可以在点这里下载)

 import collections
 import math
 import os
 import random
 import zipfile

 import numpy as np
 import urllib
 import tensorflow as tf

 # Step 1: 下载数据集
 url = 'http://mattmahoney.net/dc/'

 def maybe_download(filename, expected_bytes):
   '''
     下载数据集,如果已下载,确保数据集完整
   :param filename: 数据集地址
   :param expected_bytes: 数据集大小
   :return: 数据集
   '''
   if not os.path.exists(filename):
     filename, _ = urllib.urlretrieve(url + filename, filename)
   statinfo = os.stat(filename) # 返回文件的信息

   if statinfo.st_size == expected_bytes:
     print('Found and verified', filename)
   else:
     print(statinfo.st_size)
     raise Exception(
         'Failed to verify ' + filename + '. Can you get to it with a browser?')
   return filename

 filename = maybe_download('text8.zip', 31344016)

 # 读取数据到一个strings list
 def read_data(filename):
   '''
     解压数据并读取到words中
   :param filename:
   :return:
   '''
   with zipfile.ZipFile(filename) as f:
     data = tf.compat.as_str(f.read(f.namelist()[0])).split() # 按空格分割
   return data

 words = read_data(filename)
 print('Data size', len(words))

输出为:

 ('Found and verified', 'text8.zip')
 ('Data size', 17005207)

 

函数 description
urlretrieve属于urllib包的,而urllib在Python2和Python3上的实现是不同的。

py2:
urllib.urlretrieve(url[, filename[, reporthook[, data]]])

py3:
urllib.request.urlretrieve(url, file=None, repo=None, data=None)

copy一个由URL描述的网络对象到本地,如果URL指向一个本地文件,则对象不被copy除非提供文件名
返回一个元组对象(filename,tuple)
class zipfile.ZipFile(file, mode=’r’,
compression=ZIP_STORED, allowZip64=True)with ZipFile(‘spam.zip’) as myzip:
参数file可以是一个文件的路径(字符串)或者是文件对象.
ZipFile 通过配合with关键字获得上下文管理器使用
ZipFile.namelist() 返回一个archive members by name列表 .
tf.compat.as_str 转换任何bytes或Unicode bytes,使用utf8编码文本

2. 创建数据集dict,统计单词频率,处理数据

创建vocabulary词汇表,使用collections.Counter统计单词列表中单词的频数,取前50000到vocabulary中。再把vocabulary词汇表转存到一个dict上用于快速查询(dict时间复杂度为O(1))。并统计这类词汇的数量。

下面遍历单词列表,对其中的每一个单词,先判断是否在vocabulary词汇表,是则转换为编号,不是就是0(UNK,unknown).

 # Step 2: 建立数据集的dictionary并将不常出现的单词用UNK代替
 vocabulary_size = 50000

 def build_dataset(words):
   count = [['UNK', -1]]
   # count记录出现频率最高的词汇  形式为"element":frequency.
   count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
   dictionary = dict()   # dictionary记录前出现频率最高的单词的rank
   for word, _ in count:
     dictionary[word] = len(dictionary)  # 按出现频率存入dict中,并排序
   data = list() # data记录数据集(以单词出现频率rank来表示,不在rank内就记录为0-UNK)
   unk_count = 0
   for word in words:
     if word in dictionary:  # data以dict统计单词频率形式表现,不在前50000的记为UNK(unknown)
       index = dictionary[word]
     else:
       index = 0  # 不在dict内的都转换为dictionary['UNK']
       unk_count += 1
     data.append(index)
   count[0][1] = unk_count
   # 翻转dict,即记录数据形式为rank:'element'
   reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
   return data, count, dictionary, reverse_dictionary

 data, count, dictionary, reverse_dictionary = build_dataset(words)
 del words  # Hint to reduce memory.
 print('Most common words (+UNK)', count[:5])
 # Sanple data:
 # anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used'
 print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]])

 data_index = 0

输出为:

    ('Most common words (+UNK)', [['UNK', 418391], ('the', 1061396), ('of', 593677), ('and', 416629), ('one', 411764)])
   ('Sample data', [5239, 3084, 12, 6, 195, 2, 3137, 46, 59, 156], ['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against'])
  • 1
  • 2
函数 description
class collections.Counter([iterable-or-mapping]):

Counter.most_common([n])

Dict的子类,用于计算hashable items.
每个元素的elements存储记为dictionary keys,对应的elements出现的次数存储记为dictionary values.返回一个列表的常见的元素和对应的出现次数
Counter(‘abracadabra’).most_common(3)
>>[(‘a’, 5), (‘r’, 2), (‘b’, 2)]
zip([iterable, …]) 返回一个tuple的列表
>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> zipped = zip(x, y)
>>> zipped
[(1, 4), (2, 5), (3, 6)]

3. 生成Word2Vec的训练样本

由上述数据采样可知,这里展示了以下样本:

sample:anarchism originated as a term of abuse first used

取样本的过程应该是:

  1. 判断batch_size和skip_window和num_skips参数的合理性
  2. 创建队列deques,队列长度为奇数,中间元素为目标词汇,两边对应目标词汇在队列中的元素排列
  3. 依据batch_size和skip_window决定采样的起始位置
  4. 对目标词汇两边随机采样(,引入一个targets_to_avoid保证采样不重复)
  5. 采样完一个目标词汇,更新队列deques,转为下一个目标词汇,直到满足batch_size个目标词汇

例如我们设置batch_size=8.num_skips=2,skip_window=1.则取出来的数据集应该为

(originated,anarchism);(originated,as)
(as,originated);(as,a)
(a,as);(a,term)
(term,a);(term,of)
 # Step 3: 生成训练数据(batch for the skip-gram model.)
 def generate_batch(batch_size, num_skips, skip_window):
   '''
     生成训练数据
   :param batch_size: batch大小
   :param num_skips:    对每个单词生成的样本数
   :param skip_window:   滑窗大小
   :return:
   '''
   # data_index单词序号,我们会反复调用generate_batch,要确保data_index可以在函数generate_batch中修改
   global data_index

   # batch_size必须是num_skips的整数倍,保证每个batch包含了一个词汇对应的所有样本
   assert batch_size % num_skips == 0
   assert num_skips <= 2 * skip_window  # 样本数小于2倍的滑窗大小
   batch = np.ndarray(shape=(batch_size), dtype=np.int32)    # batch和labels转为array
   labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)

   # 定义span为对某个单词创建相关样本时会使用到的单词数量,包括目标单词本身和它前后的单词
   span = 2 * skip_window + 1 # [ skip_window target skip_window ]
   # 创建一个最大容量为span的deque(双向队列,在对deque使用append方法添加数据时,只会保留最后插入的span变量)
   buffer = collections.deque(maxlen=span)

   # 填充满buffer,后续数据将替换掉前面的数据
   for _ in range(span):
     buffer.append(data[data_index])
     data_index = (data_index + 1) % len(data)

   # 每次循环对一个目标单词生成样本。现在buffer内是目标单词和所有相关单词
   for i in range(batch_size // num_skips):  # //除法取整
     target = skip_window  # target label at the center of the buffer
     targets_to_avoid = [ skip_window ]  # 用于过滤已使用的单词
     for j in range(num_skips):  # 对一个单词生成num_skips个样本
       while target in targets_to_avoid: #随机出一个满足整数(顺序不定但不重复)
         target = random.randint(0, span - 1)
       targets_to_avoid.append(target)   # 单词已经使用了,过滤掉
       batch[i * num_skips + j] = buffer[skip_window]
       labels[i * num_skips + j, 0] = buffer[target]
     buffer.append(data[data_index]) # 读入下一个单词,会自动抛弃一个单词
     data_index = (data_index + 1) % len(data)
   return batch, labels

 batch, labels = generate_batch(batch_size=8, num_skips=2, skip_window=1)



 # 'anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used'
 # 'originated'->'anarchism', 'originated'->'as'
 # 'as'->'originated', 'as'->'a' ;  'a'->'as', 'a'->'term' ...
 for i in range(8):
   print(batch[i], reverse_dictionary[batch[i]],
       '->', labels[i, 0], reverse_dictionary[labels[i, 0]])

输出:

 (3084, 'originated', '->', 12, 'as')
 (3084, 'originated', '->', 5239, 'anarchism')
 (12, 'as', '->', 3084, 'originated')
 (12, 'as', '->', 6, 'a')
 (6, 'a', '->', 12, 'as')
 (6, 'a', '->', 195, 'term')
 (195, 'term', '->', 6, 'a')
 (195, 'term', '->', 2, 'of')
函数 description
class collections.deque([iterable[, maxlen]]) 返回一个新的deque(队列)对象,通过迭代器从左到右(使用append())完成初始化

4. 构建训练参数和网络模型

使用tf.nn.embedding_lookup查找输入train_inputs对应的向量labels.
这里我们采用NCE Loss作为训练目标。

# Step 4: 建立训练模型 Build and train a skip-gram model.
 batch_size = 128      #训练时batch_size为128
 embedding_size = 128  # embedding_size即将单词转为稠密向量的维度,一般取50~1000这个范围内的值
 skip_window = 1       # How many words to consider left and right.
 num_skips = 2         # How many times to reuse an input to generate a label.

 # We pick a random validation set to sample nearest neighbors. Here we limit the
 # validation samples to the words that have a low numeric ID, which by
 # construction are also the most frequent.
 valid_size = 16     # 验证的单词数
 valid_window = 100  # 验证单词只从频率最高的100个单词中抽取
 valid_examples = np.random.choice(valid_window, valid_size, replace=False)
 num_sampled = 64    # 训练时用来做负样本的噪声单词的数量


 graph = tf.Graph()
 with graph.as_default():

   # Input data.
   train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
   train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
   valid_dataset = tf.constant(valid_examples, dtype=tf.int32)

   # 限定所有操作在CPU上执行,因为有的操作在GPU上还没有实现
   # Ops and variables pinned to the CPU because of missing GPU implementation
   with tf.device('/cpu:0'):
     # Look up embeddings for inputs.
     embeddings = tf.Variable(
         tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
     embed = tf.nn.embedding_lookup(embeddings, train_inputs)

     # Construct the variables for the NCE loss
     nce_weights = tf.Variable(
         tf.truncated_normal([vocabulary_size, embedding_size],
                             stddev=1.0 / math.sqrt(embedding_size)))
     nce_biases = tf.Variable(tf.zeros([vocabulary_size]))

   # 计算NCE loss(计算学习出的词向量embedding在训练数据上的loss,并使用tf.reduce_mean汇总)
   # Compute the average NCE loss for the batch.
   # tf.nce_loss automatically draws a new sample of the negative labels each
   # time we evaluate the loss.
   loss = tf.reduce_mean(
       tf.nn.nce_loss(weights=nce_weights,
                      biases=nce_biases,
                      labels=train_labels,
                      inputs=embed,
                      num_sampled=num_sampled,
                      num_classes=vocabulary_size))

   # 使用SGD优化器,学习率为1
   # Construct the SGD optimizer using a learning rate of 1.0.
   optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)

   # Compute the cosine similarity between minibatch examples and all embeddings.
   # 计算嵌入向量embeddings的L2范数
   norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
   # 将embeddings除以其L2范数得到标准化后的normalized_embeddings
   normalized_embeddings = embeddings / norm
   # 使用embedding_lookup查询验证单词的嵌入向量,并计算验证单词的嵌入向量与词汇表中所有单词的相似性
   valid_embeddings = tf.nn.embedding_lookup(
       normalized_embeddings, valid_dataset)
   similarity = tf.matmul(
       valid_embeddings, normalized_embeddings, transpose_b=True)

   # Add variable initializer.
   init = tf.global_variables_initializer()
函数 description
numpy.random.choice(a,
size=None, replace=True, p=None)np.random.choice(5, 3, replace=False)
array([3,1,0])
>>> #等同于np.random.permutation(np.arange(5))[:3]
从a中随机采样得到一个1维的array

a:1-D array-like or int
如果为一个ndarray,则采样数据从该ndarray中获取。如果为整数,采样数据从np.arange(a)获取
size:int or tuple of ints, optional
输出的shape.如果给了一个shape(m,n,k),则采样出来为(m * n * k)。如果为空则返回单个数字

5. 训练网络

 # Step 5: Begin training.
 num_steps = 100001

 with tf.Session(graph=graph) as session:
   # We must initialize all variables before we use them.
   init.run()
   print("Initialized")

   average_loss = 0
   for step in range(num_steps):
     batch_inputs, batch_labels = generate_batch(
         batch_size, num_skips, skip_window)
     feed_dict = {train_inputs : batch_inputs, train_labels : batch_labels}

     # We perform one update step by evaluating the optimizer op (including it
     # in the list of returned values for session.run()
     _, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)
     average_loss += loss_val

     if step % 2000 == 0:
       if step > 0:
         average_loss /= 2000
       # 每2000次计算一下平均的loss并显示
       # The average loss is an estimate of the loss over the last 2000 batches.
       print("Average loss at step ", step, ": ", average_loss)
       average_loss = 0

     # 每10000次计算一次验证单词与全部单词的相似度,将最相似的8个单词打印出来
     # Note that this is expensive (~20% slowdown if computed every 500 steps)
     if step % 10000 == 0:
       sim = similarity.eval()
       for i in range(valid_size):
         valid_word = reverse_dictionary[valid_examples[i]]
         top_k = 8 # number of nearest neighbors
         nearest = (-sim[i, :]).argsort()[1:top_k+1]
         log_str = "Nearest to %s:" % valid_word
         for k in range(top_k):
           close_word = reverse_dictionary[nearest[k]]
           log_str = "%s %s," % (log_str, close_word)
         print(log_str)
   final_embeddings = normalized_embeddings.eval()

输出:

以下展示的是模型训练100000次后,认为平均损失,以及与验证单词相似度最高的单词,可以看到模型对各种类型的单词的相似词汇的识别都较为准确。

   ('Average loss at step ', 92000, ': ', 4.7085344190597533)
    ('Average loss at step ', 94000, ': ', 4.6158797936439511)
    ('Average loss at step ', 96000, ': ', 4.7306651622056961)
    ('Average loss at step ', 98000, ': ', 4.6274294868111614)
    ('Average loss at step ', 100000, ': ', 4.6817108399868008)
    Nearest to history: cegep, lillian, list, extraction, akita, felis, tsar, imran,
    Nearest to of: microcebus, akita, callithrix, wct, including, yum, ssbn, dasyprocta,
    Nearest to up: out, thaler, them, him, daley, chlorophyll, back, hler,
    Nearest to his: their, her, its, the, s, my, ssbn, microcebus,
    Nearest to use: thaler, thibetanus, callithrix, akita, unassigned, victoriae, abitibi, shops,
    Nearest to seven: eight, six, five, nine, four, three, zero, callithrix,
    Nearest to d: b, r, p, layer, circ, six, bront, thaler,
    Nearest to he: it, she, they, who, there, never, microcebus, tamarin,
    Nearest to and: or, but, dasyprocta, while, agouti, akita, microcebus, when,
    Nearest to four: five, six, seven, eight, three, two, nine, zero,
    Nearest to not: they, usually, you, callithrix, now, it, still, often,
    Nearest to new: toole, alembert, trinomial, antennae, somers, aldiss, edward, cubism,
    Nearest to at: in, during, on, within, microcebus, dasyprocta, with, after,
    Nearest to called: UNK, enclosure, imran, and, used, microsite, specialises, webpages,
    Nearest to may: can, would, will, could, might, should, must, cannot,
    Nearest to people: rfcs, aalto, thaler, aorta, reservation, regulators, forces, access,

6. 可视化Word2Vec

在上面代码中,我们将50000个种类的vocabulary展成128维(embedding_size)的向量.为了便于观察,使用sklearn.manifold.TSNE实现数据降维,直接把128维降维到2维。这样就能在二维图像上汇出对应的vocabulary了(为了便于观察,这里只取出频率最高的50个vocabulary)

 # Step 6: 用来可视化Word2Vec效果的函数
 # Visualize the embeddings.
 def plot_with_labels(low_dim_embs, labels, filename='tsne.png'):
   '''
     可视化Word2Vec效果的函数
   :param low_dim_embs: 降维到2维的单词的空间向量
   :param labels:
   :param filename:
   :return:
   '''
   assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings"
   plt.figure(figsize=(18, 18))  #in inches
   for i, label in enumerate(labels):
     x, y = low_dim_embs[i, :]
     plt.scatter(x, y)   # 显示散点图
     plt.annotate(label, # 显示单词本身
                  xy=(x, y),
                  xytext=(5, 2),
                  textcoords='offset points',
                  ha='right',
                  va='bottom')

   plt.savefig(filename)

   #%%
 try:
   from sklearn.manifold import TSNE
   import matplotlib.pyplot as plt

   # 使用sklearn.manifold.TSNE实现数据降维,从原始的128维降到2维,在展示50个频率高的单词
   tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
   plot_only = 50
   low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only,:])
   labels = [reverse_dictionary[i] for i in range(plot_only)]
   plot_with_labels(low_dim_embs, labels)

 except ImportError:
   print("Please install sklearn, matplotlib, and scipy to visualize embeddings.")

输出:

距离相近的单词在语义上有很高的相似性。

这里写图片描述

这里写图片描述

函数 description
class sklearn.manifold.TSNE(n_components=2, perplexity=30.0, early_exaggeration=12.0, learning_rate=200.0, n_iter=1000, n_iter_without_progress=300, min_grad_norm=1e-07, metric=’euclidean’, init=’random’, verbose=0, random_state=None, method=’barnes_hut’, angle=0.5)[source] t-SNE是一个高维数据可视化的工具。

more details:
http://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html

TSNE.fit_transform(X[, y]) 训练X直到an embedded space 并返回transformed output. |

Tensorflow上的实现基于LSTM的语言模型

数据集

Penn Tree Bank(PTB)是在语言模型训练中经常使用的一个数据集。它的质量比较高,可用来评测语言模型的准确率,同时数据集不大,训练速度也快。

我们下载PTB数据集并解压,确保解压后的文件路径与后面的工程Python路径一致。这个数据集本身已经做了一些预处理,它包含了1万个不同的单词,有句尾的标记,同时将罕见的词汇统一处理为特殊字符。

在Liunx上直接下载并解压

    wget http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz # 下载
    tar xvf simple-examples.tgz  # 解压
  • 1
  • 2

或者去网站上下载,再解压

http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
  • 1

为了让PTB数据集使用起来更方便,TensorFlow的Models包中的PTB Reader,借助它可以很方便的读取数据内容。

如果TensorFlow中已经安装了Models模块,则直接导入

from tensorflow.models.tutorials.rnn.ptb import reader
  • 1

如果没有Models包,则直接使用git工具下载

    git clone https://github.com/tensorflow/models.git
    cd models/tutorials/rnn/ptb  # 保证工程Python与解压文件与ptb文件在同一目录下
  • 1
  • 2

PTB Readert 提供了ptb_raw_data函数用来读取PTB的原始数据,并将原始数据中的单词转换为单词ID.

    # coding:utf8

    import reader

    # 存放PTB数据集和位置
    DATA_PATH = '/root/PycharmProjects/RNN/LSTM/simple-examples/data/'

    # 读取PTB数据
    train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH)

    print(len(train_data))
    print(train_data[:100])

    '''
    程序输出:
    929589
    [9970, 9971, 9972, 9974, 9975, 9976, 9980, 9981, 9982, 9983, 9984, 9986, 9987, 9988, 9989, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999, 2, 9256, 1, 3, 72, 393, 33, 2133, 0, 146, 19, 6, 9207, 276, 407, 3, 2, 23, 1, 13, 141, 4, 1, 5465, 0, 3081, 1596, 96, 2, 7682, 1, 3, 72, 393, 8, 337, 141, 4, 2477, 657, 2170, 955, 24, 521, 6, 9207, 276, 4, 39, 303, 438, 3684, 2, 6, 942, 4, 3150, 496, 263, 5, 138, 6092, 4241, 6036, 30, 988, 6, 241, 760, 4, 1015, 2786, 211, 6, 96, 4]

    '''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

可以看到训练数据共包含了929589个单词,而这些单词被组成了一个非常长的序列。这个序列通过特殊的标识符给出了每句话结束的位置。在这个数据集中,句子结束的标识符ID为2.

在实际训练时需要按照某个固定的长度截取序列,为了实现截断并将数据组织成batch,Tensorflow提供了ptb_iterator函数。

    # coding:utf8
    import reader

    # 存放PTB数据集和位置
    DATA_PATH = '/root/PycharmProjects/RNN/LSTM/simple-examples/data/'

    # 读取PTB数据
    train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH)

    x, y = reader.ptb_producer(train_data, 4, 5)
    print(x)
    print(y)

    '''
    输出:
    Tensor("PTBProducer/StridedSlice:0", shape=(4, 5), dtype=int32)
    Tensor("PTBProducer/StridedSlice_1:0", shape=(4, 5), dtype=int32)
    '''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

工程代码

 #coding:utf8
 #%%
 # Copyright 2016 The TensorFlow Authors. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
 #     http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ==============================================================================


 import time
 import numpy as np
 import tensorflow as tf
 import reader

 #flags = tf.flags
 #logging = tf.logging



 #flags.DEFINE_string("save_path", None,
 #                    "Model output directory.")
 #flags.DEFINE_bool("use_fp16", False,
 #                  "Train using 16-bit floats instead of 32bit floats")

 #FLAGS = flags.FLAGS


 #def data_type():
 #  return tf.float16 if FLAGS.use_fp16 else tf.float32

 #
 class PTBInput(object):
   '''
     The input data.
     config中有batch_size.num_steps
         num_steps :是LSTM的展开步数(unrolled steps of LSTM)
         epoch_size:为每个epoch内需要多少轮训练的迭代
   '''
   def __init__(self, config, data, name=None):
     self.batch_size = batch_size = config.batch_size
     self.num_steps = num_steps = config.num_steps

     self.epoch_size = ((len(data) // batch_size) - 1) // num_steps
     self.input_data, self.targets = reader.ptb_producer(
         data, batch_size, num_steps, name=name)

 # 通过一个PTBModel类来描述模型,方便维护循环神经网络中的状态
 class PTBModel(object):
   '''
     The PTB model.
     语言模型.
     input_: batch_size和num_steps
     config: hidden_size(LSTM节点数)和vocab_size(词汇表大小)
   '''
   def __init__(self, is_training, config, input_):
     self._input = input_

     batch_size = input_.batch_size
     num_steps = input_.num_steps
     size = config.hidden_size   # LSTM节点数
     vocab_size = config.vocab_size    # 词汇表大小

     # Slightly better results can be obtained with forget gate biases
     # initialized to 1 but the hyperparameters of the model would need to be
     # different than reported in the paper.
     # 使用tf.contrib.rnn.BasicLSTMCell设置默认的LSTM单元
     def lstm_cell():
       return tf.contrib.rnn.BasicLSTMCell(
           size, forget_bias=0.0, state_is_tuple=True)
     attn_cell = lstm_cell

     # 如果训练状态且Dropout的keep_prob小于1,则在前面的lstm_cell之后接一个Dropout层
     # 调用tf.contrib.rnn.DropoutWrapper,
     if is_training and config.keep_prob < 1:
       def attn_cell():
         return tf.contrib.rnn.DropoutWrapper(
             lstm_cell(), output_keep_prob=config.keep_prob)

     # 使用RNN的堆叠函数tf.contrib.rnn.MultiRNNCell将前面构造的lstm_cell多层堆叠得到cell
     cell = tf.contrib.rnn.MultiRNNCell(
         [attn_cell() for _ in range(config.num_layers)], state_is_tuple=True)

     self._initial_state = cell.zero_state(batch_size, tf.float32)

     # 指定在cpu上执行
     # embedding_lookup是将单词的ID转换为单词向量,这里embedding的维度为VOCAB_SIZE * SIZE.(行数为词汇表数,列数为hidden_size)
     # 从embedding_lookup上获得输入单词向量,并在训练时添加dropout    
     with tf.device("/cpu:0"):
       embedding = tf.get_variable(
           "embedding", [vocab_size, size], dtype=tf.float32)
       inputs = tf.nn.embedding_lookup(embedding, input_.input_data)

     if is_training and config.keep_prob < 1:
       inputs = tf.nn.dropout(inputs, config.keep_prob)

     # 定义输出列表 将不同时刻的LSTM输出记录到一起,再通过一个全连接层得到最终的输出
     outputs = []
     state = self._initial_state
     with tf.variable_scope("RNN"):
       # 为了控制训练,我们会限制梯度在反向传播时可以展开的步数为一个固定的值num_steps
       for time_step in range(num_steps):
         if time_step > 0 :
           tf.get_variable_scope().reuse_variables()  # 设置复用变量
         # 给cell传入inputs和state
         #  inputs的三个维度,第一个维度代表batch的第几个样本,第二个维度代表样本中第几个单词,第三个维度是单词的向量表达的维度
         # inputs[:,time_step,:]代表所有样本的第time_step个单词
         (cell_output, state) = cell(inputs[:, time_step, :], state)
         # 将当前的输出加入到outputs列表
         outputs.append(cell_output)

     # 将输出队列展开成[batch,size*num_steps]的形状,再reshape成[batch*num_steps,size]
     # 使用concat将所有输出接到一起并转为一维向量
     output = tf.reshape(tf.concat(outputs, 1), [-1, size])
     # 定义softmax层
     # 从size的向量转换为vocab_size的单词ID
     softmax_w = tf.get_variable(
         "softmax_w", [size, vocab_size], dtype=tf.float32)
     softmax_b = tf.get_variable("softmax_b", [vocab_size], dtype=tf.float32)
     # 得到网络的输出
     logits = tf.matmul(output, softmax_w) + softmax_b
     # 直接使用tf.contrib.legacy_seq2seq.sequence_loss_by_example计算输出logits和targets的交叉熵
     loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example(
         [logits],       # 预测结果
         [tf.reshape(input_.targets, [-1])], # 期待结果,这里将[batch_size,num_steps]二维数组展开成一维数组
         [tf.ones([batch_size * num_steps], dtype=tf.float32)])
     # 计算得到每个batch的损失
     self._cost = cost = tf.reduce_sum(loss) / batch_size
     self._final_state = state

     # 只在训练的时候定义反向传播操作,不是训练状态就返回
     if not is_training:
       return

     # 定义学习速率_lr
     self._lr = tf.Variable(0.0, trainable=False)
     # 获取所有可训练的参数tvars,针对前面得到的cost,计算tvars梯度
     # 并tf.clip_by_global_norm设置梯度的最大范数,某种程度上起到了正则化的作用
     # 这就是Gradient Clipping的方法,控制梯度的最大范数,防止Gradient Explosion梯度爆炸
     tvars = tf.trainable_variables()
     grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),
                                       config.max_grad_norm)
     optimizer = tf.train.GradientDescentOptimizer(self._lr)
     # 用optimizer.apply_gradients将前面clip过的梯度应用到所有可训练的参数tvars上
     # 然后使用tf.contrib.framework.get_or_create_global_step()生成全局统一的训练步数
     self._train_op = optimizer.apply_gradients(
         zip(grads, tvars),
         global_step=tf.contrib.framework.get_or_create_global_step())

     # 创建_new_lr控制学习率  创建_lr_update使用tf.assign将_new_lr传给当前的学习率_lr
     self._new_lr = tf.placeholder(
         tf.float32, shape=[], name="new_learning_rate")
     self._lr_update = tf.assign(self._lr, self._new_lr)

   # assign_lr用于外部控制模型的学习速率
   def assign_lr(self, session, lr_value):
     session.run(self._lr_update, feed_dict={self._new_lr: lr_value})

   # 定义PTBModel class的一些property  @property装饰器可以将返回变量设为只读,防止修改变量引发不必要问题

   @property
   def input(self):
     return self._input

   @property
   def initial_state(self):
     return self._initial_state

   @property
   def cost(self):
     return self._cost

   @property
   def final_state(self):
     return self._final_state

   @property
   def lr(self):
     return self._lr

   @property
   def train_op(self):
     return self._train_op


 '''
 定义不同大小的模型的参数
   init_scale  网络中权重值的初始scale
   learning_rate 学习率的初始值
   max_grad_norm 梯度的最大范数
   num_layers LSTM堆叠的层数
   num_steps  LSTM梯度反向传播展开步数
   hidden_size  LSTM内隐含节点数
   max_epoch  初始学习率可训练的epoch
   max_max_epoch  总工可训练的epoch
   keep_prob  dropout比率
   lr_decay  学习率衰减速度
   batch_size  batch中样本数量
   vocab_size  

 '''

 class SmallConfig(object):
   """Small config."""
   init_scale = 0.1
   learning_rate = 1.0
   max_grad_norm = 5
   num_layers = 2
   num_steps = 20
   hidden_size = 200
   max_epoch = 4
   max_max_epoch = 13
   keep_prob = 1.0 # 设置为1即不用dropout
   lr_decay = 0.5
   batch_size = 20
   vocab_size = 10000



 class MediumConfig(object):
   """
   Medium config.
   我们减少了init_scale.希望权重初值不要太大,这样有利于温和的训练
   增添hidden_size到650,训练次数也增大
   设置dropout为0.5,即开始使用dropout
   """
   init_scale = 0.05
   learning_rate = 1.0
   max_grad_norm = 5
   num_layers = 2
   num_steps = 35
   hidden_size = 650
   max_epoch = 6
   max_max_epoch = 39
   keep_prob = 0.5
   lr_decay = 0.8
   batch_size = 20
   vocab_size = 10000


 class LargeConfig(object):
   """
   Large config.
   我们继续减少了init_scale.
   放大了最大梯度范数到10
   增添hidden_size到1500,训练次数也增大
   设置dropout为0.5,即开始使用dropout
   """
   init_scale = 0.04
   learning_rate = 1.0
   max_grad_norm = 10
   num_layers = 2
   num_steps = 35
   hidden_size = 1500
   max_epoch = 14
   max_max_epoch = 55
   keep_prob = 0.35
   lr_decay = 1 / 1.15
   batch_size = 20
   vocab_size = 10000


 class TestConfig(object):
   """Tiny config, for testing."""
   init_scale = 0.1
   learning_rate = 1.0
   max_grad_norm = 1
   num_layers = 1
   num_steps = 2
   hidden_size = 2
   max_epoch = 1
   max_max_epoch = 1
   keep_prob = 1.0
   lr_decay = 0.5
   batch_size = 20
   vocab_size = 10000


 def run_epoch(session, model, eval_op=None, verbose=False):
   """Runs the model on the given data."""
   start_time = time.time()
   costs = 0.0
   iters = 0
   state = session.run(model.initial_state)

   fetches = {
       "cost": model.cost,
       "final_state": model.final_state,
   }

   if eval_op is not None:
     fetches["eval_op"] = eval_op

   for step in range(model.input.epoch_size):
     feed_dict = {}
     # 使用当前数据训练或预测模型
     for i, (c, h) in enumerate(model.initial_state): # 将全部的状态加入
       feed_dict[c] = state[i].c
       feed_dict[h] = state[i].h

     vals = session.run(fetches, feed_dict) # 训练
     cost = vals["cost"]
     state = vals["final_state"]

     # 将不同时刻.不同batch的概率加起来再做指数运算就得到perplexity
     costs += cost
     iters += model.input.num_steps

     if verbose and step % (model.input.epoch_size // 10) == 10:
       print("%.3f perplexity: %.3f speed: %.0f wps" %
             (step * 1.0 / model.input.epoch_size, np.exp(costs / iters),
              iters * model.input.batch_size / (time.time() - start_time)))

   return np.exp(costs / iters) # 返回perplexity




 raw_data = reader.ptb_raw_data('simple-examples/data/')
 train_data, valid_data, test_data, _ = raw_data

 config = SmallConfig()
 eval_config = SmallConfig()
 eval_config.batch_size = 1
 eval_config.num_steps = 1

 with tf.Graph().as_default():
   initializer = tf.random_uniform_initializer(-config.init_scale,
                                               config.init_scale)

   with tf.name_scope("Train"):
     train_input = PTBInput(config=config, data=train_data, name="TrainInput")
     with tf.variable_scope("Model", reuse=None, initializer=initializer):
       m = PTBModel(is_training=True, config=config, input_=train_input)
       #tf.scalar_summary("Training Loss", m.cost)
       #tf.scalar_summary("Learning Rate", m.lr)

   with tf.name_scope("Valid"):
     valid_input = PTBInput(config=config, data=valid_data, name="ValidInput")
     with tf.variable_scope("Model", reuse=True, initializer=initializer):
       mvalid = PTBModel(is_training=False, config=config, input_=valid_input)
       #tf.scalar_summary("Validation Loss", mvalid.cost)

   with tf.name_scope("Test"):
     test_input = PTBInput(config=eval_config, data=test_data, name="TestInput")
     with tf.variable_scope("Model", reuse=True, initializer=initializer):
       mtest = PTBModel(is_training=False, config=eval_config,
                        input_=test_input)

   # 创建训练的管理器,默认session
   sv = tf.train.Supervisor()
   with sv.managed_session() as session:
     for i in range(config.max_max_epoch):
       lr_decay = config.lr_decay ** max(i + 1 - config.max_epoch, 0.0) # 计算累计的衰减值
       m.assign_lr(session, config.learning_rate * lr_decay)

       print("Epoch: %d Learning rate: %.3f" % (i + 1, session.run(m.lr)))
       train_perplexity = run_epoch(session, m, eval_op=m.train_op,
                                    verbose=True)
       print("Epoch: %d Train Perplexity: %.3f" % (i + 1, train_perplexity))
       valid_perplexity = run_epoch(session, mvalid)
       print("Epoch: %d Valid Perplexity: %.3f" % (i + 1, valid_perplexity))

     test_perplexity = run_epoch(session, mtest)
     print("Test Perplexity: %.3f" % test_perplexity)

      # if FLAGS.save_path:
      #   print("Saving model to %s." % FLAGS.save_path)
      #   sv.saver.save(session, FLAGS.save_path, global_step=sv.global_step)


 #if __name__ == "__main__":
 #  tf.app.run()

输出:
使用小型网络,可以达到30000单词每秒。在第13epoch上,训练集上可以达到41的perplexity,测试集和验证集上可以得到114和119的perplexity.这相当于在训练过程中,将选择下一个单词的范围缩减到41个。

    Epoch: 11 Train Perplexity: 41.423
    Epoch: 11 Valid Perplexity: 119.596
    Epoch: 12 Learning rate: 0.004
    0.004 perplexity: 62.332 speed: 31880 wps
    0.104 perplexity: 45.575 speed: 30970 wps
    0.204 perplexity: 50.202 speed: 31076 wps
    0.304 perplexity: 48.064 speed: 31048 wps
    0.404 perplexity: 47.202 speed: 31007 wps
    0.504 perplexity: 46.485 speed: 30982 wps
    0.604 perplexity: 44.948 speed: 30964 wps
    0.703 perplexity: 44.271 speed: 30917 wps
    0.803 perplexity: 43.513 speed: 30895 wps
    0.903 perplexity: 42.061 speed: 30912 wps
    Epoch: 12 Train Perplexity: 41.149
    Epoch: 12 Valid Perplexity: 119.290
    Epoch: 13 Learning rate: 0.002
    0.004 perplexity: 62.055 speed: 31557 wps
    0.104 perplexity: 45.379 speed: 30842 wps
    0.204 perplexity: 50.000 speed: 30992 wps
    0.304 perplexity: 47.882 speed: 31186 wps
    0.404 perplexity: 47.030 speed: 31151 wps
    0.504 perplexity: 46.318 speed: 31090 wps
    0.604 perplexity: 44.788 speed: 30990 wps
    0.703 perplexity: 44.114 speed: 30884 wps
    0.803 perplexity: 43.359 speed: 30846 wps
    0.903 perplexity: 41.911 speed: 30833 wps
    Epoch: 13 Train Perplexity: 41.002
    Epoch: 13 Valid Perplexity: 119.096
    Test Perplexity: 114.660




RNN 调参经验

转载 2016年08月26日 09:31:46
作者:萧瑟
链接:https://www.zhihu.com/question/41631631/answer/94816420
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

    1. uniform
      W = np.random.uniform(low=-scale, high=scale, size=shape)
    2. glorot_uniform
      scale = np.sqrt(6. / (shape[0] + shape[1]))
      np.random.uniform(low=-scale, high=scale, size=shape)
    3. 高斯初始化:
      w = np.random.randn(n) / sqrt(n),n为参数数目
      激活函数为relu的话,推荐
      w = np.random.randn(n) * sqrt(2.0/n)
    4. svd ,对RNN效果比较好,可以有效提高收敛速度.
  1. 数据预处理方式
    1. zero-center ,这个挺常用的.
      X -= np.mean(X, axis = 0) # zero-center
      X /= np.std(X, axis = 0) # normalize
    2. PCA whitening,这个用的比较少.
  2. 训练技巧
    1. 要做梯度归一化,即算出来的梯度除以minibatch size
    2. clip c(梯度裁剪): 限制最大梯度,其实是value = sqrt(w1^2+w2^2….),如果value超过了阈值,就算一个衰减系系数,让value的值等于阈值: 5,10,15
    3. dropout对小数据防止过拟合有很好的效果,值一般设为0.5,小数据上dropout+sgd效果更好. dropout的位置比较有讲究, 对于RNN,建议放到输入->RNN与RNN->输出的位置.关于RNN如何用dropout,可以参考这篇论文:arxiv.org/abs/1409.2329
    4. adam,adadelta等,在小数据上,我这里实验的效果不如sgd,如果使用sgd的话,可以选择从1.0或者0.1的学习率开始,隔一段时间,在验证集上检查一下,如果cost没有下降,就对学习率减半. 我看过很多论文都这么搞,我自己实验的结果也很好. 当然,也可以先用ada系列先跑,最后快收敛的时候,更换成sgd继续训练.同样也会有提升.
    5. 除了gate之类的地方,需要把输出限制成0-1之外,尽量不要用sigmoid,可以用tanh或者relu之类的激活函数.
    6. rnn的dim和embdding size,一般从128上下开始调整. batch size,一般从128左右开始调整.batch size合适最重要,并不是越大越好.
    7. word2vec初始化,在小数据上,不仅可以有效提高收敛速度,也可以可以提高结果.
    8. 尽量对数据做shuffle
    9. LSTM 的forget gate的bias,用1.0或者更大的值做初始化,可以取得更好的结果,来自这篇论文:jmlr.org/proceedings/pa, 我这里实验设成1.0,可以提高收敛速度.实际使用中,不同的任务,可能需要尝试不同的值.
  3. Ensemble: 论文刷结果的终极核武器,深度学习中一般有以下几种方式
    1. 同样的参数,不同的初始化方式
    2. 不同的参数,通过cross-validation,选取最好的几组
    3. 同样的参数,模型训练的不同阶段
    4. 不同的模型,进行线性融合. 例如RNN和传统模型.

参数初始化,下面几种方式,随便选一个,结果基本都差不多.




TensorFlow中关于RNN的API

在前面我们讲了RNN的基本结构和LSTM循环神经网络,本节我们详解查看一下TensorFlow中提供的有关于RNN的API.

Tensorflow中关于RNN的api主要分布tf.nn和tf.contrib.rnn两个模块.


tf.nn下有关rnn的api

Embedding

TensorFlow提供的有关于embedding 相关的api(常用于NLP)

  • tf.nn.embedding_lookup
    依据inputs_ids的id来寻找embedding_params中对应的元素.(详解参见embedding_lookup函数详解)
  • tf.nn.embedding_lookup_sparse
    功能和embedding_lookup类似.

Recurrent Neural Networks

TensorFlow提供的一些构建RNN的方法。这些方法大多数接收的是RNNCell-subclassed object.

  • tf.nn.dynamic_rnn
  • tf.nn.bidirectional_dynamic_rnn
  • tf.nn.raw_rnn

tf.nn.dynamic_rnn

创建一个RNN.其中参数inputs为[batch_size, max_time,embedding_size].对于普通的RNN,要求输入数据的sequence_leng相同,如果不同需要通过padding补零,从而达到相同长度。

例如:针对一个RNN网络,inputs = [3,10,128],即一个batch内有3条输入数据,假设第1条数据长度为10,第2条为5,第3条为6.则第2和第3条数据需要padding到10(补零直到够10个数据长度)。这只是在一个batch上,假设在整个数据集上数据长度浮动在10-40之间,那样会有很多补零操作(太多的补零浪费模型性能)。针对这一问题dynamic_rnn可以让不同迭代下的传入的batch的数据长度是不同的,而rnn要求batch的数据长度还是固定的。

dynamic_rnn有一个参数:sequence_length.这个参数可以指定每个batch的数据长度,模型会根据这个参数进行反padding操作(遇到padding的数据进行删除)。

dynamic_rnn介绍
    '''
    详解:
    Creates a recurrent neural network specified by RNNCell cell.
    Performs fully dynamic unrolling of inputs.
    '''
    dynamic_rnn(
        cell,
        inputs,
        sequence_length=None,
        initial_state=None,
        dtype=None,
        parallel_iterations=None,
        swap_memory=False,
        time_major=False,
        scope=None
    )
    '''
Args:

cell: An instance of RNNCell.
inputs: The RNN inputs.

  • If time_major == False (default),inputs的shape为:[batch_size, max_time, …],或是此类元素的嵌套元组.
  • If time_major == True, inputs的shape为:[max_time, batch_size, …], 或是此类元素的嵌套元组.

sequence_length: (optional) An int32/int64 vector sized [batch_size]. 针对不同batch下数据长度可能不同,可以通过sequence_length指定inputs中不同batch对应的size.

initial_state: (optional) 针对RNN的一组初始状态.

  • 如果 cell.state_size是整型,这必须是类似的类型且shape为[batch_size, cell.state_size].
  • 如果 cell.state_size是元组,这必须是tensor元组且shape为 shapes [batch_size, s] for s in cell.state_size.

time_major: 输入输出的shape格式.

  • If true, 输入输出的shape格式为[max_time, batch_size, depth].
  • If false, 输入输出的shape格式为 [batch_size, max_time, depth].

Connectionist Temporal Classification (CTC)

CTC是一种改进的RNN模型。在一般RNN模型中,输入序列和标注序列是一一对应的。在某些序列建模问题上,输入序列和输出序列不对等(输入序列远长度输出序列)。CTC解决这一问题的方法是在标注序列上添加空白符号blank. 利用RNN标注,最后把blank符号和预测出的重复符号消除

  • tf.nn.ctc_loss
  • tf.nn.ctc_greedy_decoder
  • tf.nn.ctc_beam_search_decoder

tf.contrib.rnn下有关rnn的api

Base interface for all RNN Cells(针对所有RNN Cells的Basic 接口)

  • tf.contrib.rnn.RNNCell
    Class RNNCell是 RNN cell的一个abstract object(即程序架构上的基类,定义了一些基本的方法属性)

Core RNN Cells for use with TensorFlow’s core RNN methods(针对使用RNN methods需要的Core RNN Cells)

  • tf.contrib.rnn.BasicRNNCell
    RNN的基本单元。
  • tf.contrib.rnn.BasicLSTMCell
    LSTM网络的基本单元,下面搭建LSTM循环神经网络有详解
  • tf.contrib.rnn.GRUCell
    Gated Recurrent Unit Cell.
  • tf.contrib.rnn.LSTMCell
    LSTM网络的基本单元(相对于BasicLSTMCell参数更多,功能更强大)
  • tf.contrib.rnn.LayerNormBasicLSTMCell
    带有layer normalization and recurrent dropout的LSTM单元.

Classes storing split RNNCell state

  • tf.contrib.rnn.LSTMStateTuple
    在其他RNNCell的state_is_tuple=True时使用,
    格式为 tuple used by LSTM Cells for state_size, zero_state, and output state.Stores two elements: (c, h), in that order.

Core RNN Cell wrappers (RNNCells that wrap other RNNCells)

  • tf.contrib.rnn.MultiRNNCell
    堆叠不同的RNNCells.
  • tf.contrib.rnn.LSTMBlockWrapper
    一个helper class,用来为LSTM cells提供housekeeping.
  • tf.contrib.rnn.DropoutWrapper
    对于给定的cell添加dropouts.
  • tf.contrib.rnn.EmbeddingWrapper
    对于给定的cell添加input embedding操作(用embedding_lookup好一点)
  • tf.contrib.rnn.InputProjectionWrapper
    对给定的cell添加输入映射.(不常用)
  • tf.contrib.rnn.OutputProjectionWrapper
    对给定的cell添加输出映射.(不常用)
  • tf.contrib.rnn.DeviceWrapper
    确保RNNCells运行在particular device.
  • tf.contrib.rnn.ResidualWrapper
    确保cell的input直接前馈到输出(Residual残差网络)

Block RNNCells

  • tf.contrib.rnn.LSTMBlockCell
    设置LSTM的forget_gate的forget_bias为1 (default: 1),这是为了减少在训练初期forgetting的计算量(设置为1,让多数的forget gate处于开启状态便于训练)
  • tf.contrib.rnn.GRUBlockCell
    Block GRU cell implementation(详解参见 http://arxiv.org/abs/1406.1078)

Fused RNNCells

融合RNN的相关API.

  • tf.contrib.rnn.FusedRNNCell
  • tf.contrib.rnn.FusedRNNCellAdaptor
  • tf.contrib.rnn.TimeReversedFusedRNN
  • tf.contrib.rnn.LSTMBlockFusedCell

LSTM-like cells

LSTM网络拓展单元。

  • tf.contrib.rnn.CoupledInputForgetGateLSTMCell
  • tf.contrib.rnn.TimeFreqLSTMCell
  • tf.contrib.rnn.GridLSTMCell

RNNCell wrappers

RNNCell的拓展单元。

  • tf.contrib.rnn.AttentionCellWrapper
  • tf.contrib.rnn.CompiledWrapper


搭建LSTM循环神经网络

在TensorFlow中,提供了tf.contrib.rnn.BasicLSTMCell类快速搭建LSTM模块。

tf.contrib.rnn.BasicLSTMCell

Class BasicLSTMCell:创建一个基本的LSTM循环神经网络cell.

我们将biases内的forget gate的forget_bias设置为1,这是为了减少训练初期的forgetting sacle.

BasicLSTMCell**不支持cell clipping**(梯度截断,防止梯度爆炸), a projection layer, 也没有使用 peep-hole connections。
对于高级模式的LSTM,请使用tf.nn.rnn_cell.LSTMCell.


属性

variables/weights: Returns the list of all layer variables/weights.

Returns: A list of variables/weights.

函数

_init_

初始化the basic LSTM cell.

当从CudnnLSTM-trained恢复checkpoints,必须使用CudnnCompatibleLSTMCell代替.

    __init__(
        num_units,
        forget_bias=1.0,
        state_is_tuple=True,
        activation=None,
        reuse=None
    )
参数名称 description
num_units int, The number of units in the LSTM cell.
forget_bias float, forget gates上的偏置单元. 当从CudnnLSTM-trained checkpoints复原时必须设置为0.
state_is_tuple If True, 接收和返回的states是包括c_state and m_state的2-tuples.
If False, they are concatenated along the column axis. The latter behavior will soon be deprecated.
activation inner states的激活函数. 默认为: tanh.
reuse (optional) Python boolean describing whether to reuse variables in an existing scope. If not True, and the existing scope already has the given variables, an error is raised.

add_loss/add_update

增加loss/updates tensor(s).potentially dependent on layer inputs.

一些损失/更新可能依赖于调用layer时通过的输入。
因此,对于使用不同的输入A和B的同一层,layer.loss/updates可能独立依赖与a或b,这个方法会自动的keeps track of dependencies.

add_loss/update(
    losses/updates,
    inputs=None
)
参数名称 description
losses/updates Loss/updates tensor, or list/tuple of tensors.
inputs Optional input tensor(s) that the loss(es)/update depend on.
在losses创建的时候必须匹配通过的Inputs参数.
如果没有数据通过,the losses/update are assumed to be unconditional, and will apply across all dataflows of the layer (e.g. weight regularization losses)

call

Long short-term memory cell (LSTM).

call(
    inputs,
    state
)
参数名称 description
inputs 2-D tensor with shape [batch_size x input_size].
state An LSTMStateTuple of state tensors, each shaped [batch_size x self.state_size], if state_is_tuple has been set to True. Otherwise, a Tensor shaped [batch_size x 2 * self.state_size].

返回值:
一对包含着hidden state和new state的元素(either a LSTMStateTuple or a concatenated state, depending on state_is_tuple).

zero_state

Return zero-filled state tensor(s).

zero_state(
    batch_size,
    dtype
)
参数名称 description
batch_size int, float, or unit Tensor representing the batch size.
dtype the data type to use for the state.

返回值:

  • If state_size is an int or TensorShape, then the return value is a N-D tensor of shape [batch_size x state_size] filled with zeros.
  • If state_size is a nested list or tuple, then the return value is a nested list or tuple (of the same structure) of 2-D tensors with the shapes [batch_size x s] for each s in state_size.

tf.contrib.rnn.MultiRNNCell

Class MultiRNNCell:用于堆叠多少RNN cell(堆叠的cell可以是普通的RNN cell,LSTM cell,GRU cell等).

    __init__(
    cells,
    state_is_tuple=True
)
参数名称 description
cells list, 将要被组合成一组RNN网络的RNN cell单元列表
state_is_tuple If True, 接收和返回的states为n-tuples, where n = len(cells).
If False, 所有的states会被按找列方向依次连接。

TensorFlow实现LSTM的demo


 #coding=utf-8
 #简单LSTM 结构的RNN 的前向传播过程实现
 import tensorflow as tf

 lstm_hidden_size=1
 batch_size=20
 num_steps=20

 # 定义一个LSTM结构
 lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_hidden_size)

 # BasicLSTMCell类提供了zero_state函数来生成全零的初始状态
 state = lstm.zero_state(batch_size,tf.float32)

 # 定义损失函数
 loss=0.0

 # 理论上循环神经网络可以处理任意长度的序列,在训练时为了避免
 # 梯度消失问题,会规定一个最大序列长度,我们用num_steps表示这个长度
 for i in range(num_steps):
    if i > 0:
        tf.get_variable_scope().reuse_variables()
        # 每一步处理时间序列中的一个时刻。将当前输入(current_input)和
        # 上一时刻状态(state)传入定义的LSTM结构得到当前LSTM结构的输出lstm_output
        # 和更新后的状态state
        lstm_output, state = lstm(current_input, state)

        # 将当前时刻LSTM结构的输出传入一个全连接层得到最后的输出
        final_output = fully_connected(lstm_output)

        # 计算当前时刻输出的损失
        loss+=calc_loss(final_output, expected_output)



Gated Recurrent Unit Recurrent Neural Networks[6]

GRUs也是一般的RNNs的改良版本,主要是从以下两个方面进行改进。一是,序列中不同的位置处的单词(已单词举例)对当前的隐藏层的状态的影响不同,越前面的影响越小,即每个前面状态对当前的影响进行了距离加权,距离越远,权值越小。二是,在产生误差error时,误差可能是由某一个或者几个单词而引发的,所以应当仅仅对对应的单词weight进行更新。GRUs的结构如下图所示。GRUs首先根据当前输入单词向量word vector已经前一个隐藏层的状态hidden state计算出update gate和reset gate。再根据reset gate、当前word vector以及前一个hidden state计算新的记忆单元内容(new memory content)。当reset gate为1的时候,new memory content忽略之前的所有memory content,最终的memory是之前的hidden state与new memory content的结合。
GRU

LSTM Netwoorks[7]

LSTMs与GRUs类似,目前非常流行。它与一般的RNNs结构本质上并没有什么不同,只是使用了不同的函数去去计算隐藏层的状态。在LSTMs中,i结构被称为cells,可以把cells看作是黑盒用以保存当前输入xt之前的保存的状态ht1,这些cells更加一定的条件决定哪些cell抑制哪些cell兴奋。它们结合前面的状态、当前的记忆与当前的输入。已经证明,该网络结构在对长序列依赖问题中非常有效。LSTMs的网络结构如下图所示。对于LSTMs的学习,参见 this post has an excellent explanation
LSTM_1
LSTM_2
LSTM_3
LSTMs解决的问题也是GRU中所提到的问题,如下图所示:
LSTM-GRU_1
LSTMs与GRUs的区别如图所示[8]:
LSTM-GRU_2

从上图可以看出,它们之间非常相像,不同在于:

  • new memory的计算方法都是根据之前的state及input进行计算,但是GRUs中有一个reset gate控制之前state的进入量,而在LSTMs里没有这个gate;
  • 产生新的state的方式不同,LSTMs有两个不同的gate,分别是forget gate (f gate)和input gate(i gate),而GRUs只有一个update gate(z gate);
  • LSTMs对新产生的state又一个output gate(o gate)可以调节大小,而GRUs直接输出无任何调节。

Clockwork RNNs(CW-RNNs)[9]

CW-RNNs是较新的一种RNNs模型,其论文发表于2014年Beijing ICML。在原文[8]中作者表示其效果较SRN与LSTMs都好。
CW-RNNs也是一个RNNs的改良版本,是一种使用时钟频率来驱动的RNNs。它将隐藏层分为几个块(组,Group/Module),每一组按照自己规定的时钟频率对输入进行处理。并且为了降低标准的RNNs的复杂性,CW-RNNs减少了参数的数目,提高了网络性能,加速了网络的训练。CW-RNNs通过不同的隐藏层模块工作在不同的时钟频率下来解决长时间依赖问题。将时钟时间进行离散化,然后在不同的时间点,不同的隐藏层组在工作。因此,所有的隐藏层组在每一步不会都同时工作,这样便会加快网络的训练。并且,时钟周期小的组的神经元的不会连接到时钟周期大的组的神经元,只会周期大的连接到周期小的(认为组与组之间的连接是有向的就好了,代表信息的传递是有向的),周期大的速度慢,周期小的速度快,那么便是速度慢的连速度快的,反之则不成立。现在还不明白不要紧,下面会进行讲解。
CW-RNNs与SRNs网络结构类似,也包括输入层(Input)、隐藏层(Hidden)、输出层(Output),它们之间也有向前连接,输入层到隐藏层的连接,隐藏层到输出层的连接。但是与SRN不同的是,隐藏层中的神经元会被划分为若干个组,设为g,每一组中的神经元个数相同,设为k,并为每一个组分配一个时钟周期Ti{T1,T2,...,Tg},每一个组中的所有神经元都是全连接,但是组j到组i的循环连接则需要满足Tj大于Ti。如下图所示,将这些组按照时钟周期递增从左到右进行排序,即T1<T2<...<Tg,那么连接便是从右到左。例如:隐藏层共有256个节点,分为四组,周期分别是[1,2,4,8],那么每个隐藏层组256/4=64个节点,第一组隐藏层与隐藏层的连接矩阵为64*64的矩阵,第二层的矩阵则为64*128矩阵,第三组为64*(3*64)=64*192矩阵,第四组为64*(4*64)=64*256矩阵。这就解释了上一段的后面部分,速度慢的组连到速度快的组,反之则不成立。
CW-RNNs的网络结构如下图所示:
CW-RNN
在传统的RNN中,按照下面的公式进行计算:

st=fs(Wst1+Winxt)
 
ot=fo(Woutst)

  其中,W为隐藏层神经元的自连接矩阵,Win为输入层到隐藏层的连接权值矩阵,Wout是隐藏层到输出层的连接权值矩阵 ,xt是第t步的输入,st1为第t1步隐藏层的输出,st为第t步隐藏层的输出,ot为第t步的输出,fs为隐藏层的激活函数,fo为输出层的激活函数。 
  与传统的RNNs不同的是,在第t步时,只有那些满足(tmodTi)=0的隐藏层组才会执行。并且每一隐藏层组的周期{T1,T2,...,Tg}都可以是任意的。原文中是选择指数序列作为它们的周期,即Ti=2i1i[1,...,g]。 
  因此WWin将被划分为g个块。如下: 
W=⎡⎣⎢⎢⎢W1...Wg⎤⎦⎥⎥⎥
 
Win=⎡⎣⎢⎢⎢Win1...Wing⎤⎦⎥⎥⎥

其中W是一个上三角矩阵,每一个组行Wi被划分为列向量{W1i,...,Wii,0(i+1)i,...,0gi}TWji,j[1,...,g]表示第i个组到第j个组的连接权值矩阵。在每一步中,WWin只有部分组行处于执行状态,其它的为0: 
Wi={Wi0,for(tmodTi)=0,otherwise
 
Wini={Wini0,for(tmodTi)=0,otherwise

  为了使表达不混淆,将Win写成Win。并且执行的组所对应的o才会有输出。处于非执行状态下的隐藏层组仍保留着上一步的状态。下图是含五个隐藏层组在t=6时的计算图: 
CW-RNN 
  在CW-RNNs中,慢速组(周期大的组)处理、保留、输出长依赖信息,而快速组则会进行更新。CW-RNNs的误差后向传播也和传统的RNNs类似,只是误差只在处于执行状态的隐藏层组进行传播,而非执行状态的隐藏层组也复制其连接的前面的隐藏层组的后向传播。即执行态的隐藏层组的误差后向传播的信息不仅来自与输出层,并且来自与其连接到的左边的隐藏层组的后向传播信息,而非执行态的后向传播信息只来自于其连接到的左边的隐藏层组的后向传播数据。 
  下图是原文对三个不同RNNs模型的实验结果图: 
CW-RNN 
  上图中,绿色实线是预测结果,蓝色散点是真实结果。每个模型都是对前半部分进行学习,然后预测后半部分。LSTMs模型类似滑动平均,但是CW-RNNs效果更好。其中三个模型的输入层、隐藏层、输出层的节点数都相同,并且只有一个隐藏层,权值都使用均值为0,标准差为0.1的高斯分布进行初始化,隐藏层的初始状态都为0,每一个模型都使用

Nesterov-style
momentum SGD(Stochastic Gradient Descent,随机梯度下降算法)[10]

进行学习与优化。

总结

到目前为止,本文对RNNs进行了基本的介绍,并对常见的几种RNNs模型进行了初步讲解。下一步将基于Theano与Python实现一个RNNs语言模型并对上面的一些RNNs模型进行详解。这里有更多的RNNs模型

后面将陆续推出:

  • 详细介绍RNNs中一些经常使用的训练算法,如Back Propagation Through Time(BPTT)、Real-time Recurrent Learning(RTRL)、Extended Kalman Filter(EKF)等学习算法,以及梯度消失问题(vanishing gradient problem)
  • 详细介绍Long Short-Term Memory(LSTM,长短时记忆网络);
  • 详细介绍Clockwork RNNs(CW-RNNs,时钟频率驱动循环神经网络);
  • 基于Python和Theano对RNNs进行实现,包括一些常见的RNNs模型;

本系列将实现一个基于循环神经网络的语言模型(recurrent neural network based language model)。该实现包含两个方面:一是能够得到任意语句在现实中成立的得分,其提供了判断语法与语义的正确性的度量方式。该模型是机器翻译中的典型应用。二是模型能够产生新的文本,这是一个非常棒的应用。比如,对莎士比亚的文章进行训练,能够产生一个新的类似莎士比亚的文本,目前,这个有趣的想法已经被Andrew Karpathy基于RNNs的字符级别的语言模型实现了。
由于实在很忙,后面都没进行更新,抱歉。


Leave a Reply

Your email address will not be published. Required fields are marked *