本篇主要涉及LSTM
,GRU
以及seq2seq_attention
的原理以及非调用API
方式的代码实现,此外,还实现了LSTM
和GRU
的层归一化代码,主要是为了更好的理解相关模型的算法,并且可以将之作为脚手架代码进行更多的应用。转载请表明出处。
RNN
首先已经知道了在进行图像相关处理时,使用cnn
网络可以实现权值共享并且具有平移不变性等优势,CNN
取得了非常好的效果,那么对于序列数据该如何处理呢,序列模型可以看做时间序列数据。

对于普通的RNN
结构来说,接收的输入为当前的时刻的输入序列信息xt,以及上一时刻隐藏层的输出ht−1,当前节点通过输入的信息计算得到当前节点的隐藏层输出ht网络输出yt。
ht=wh∗ht−1+wi∗xt yt=softmax(wo∗ht)
RNN
的模型结构已然了解,使用整个序列的信息作出输出预测,但是会会显著存在一个问题就是距离输出较近的部分对于整个输出影响非常大,并且,传统的RNN容易出现梯度爆炸或者梯度消失的问题,具体原因是由于训练策略基于BTPP
。
LSTM & GRU
LSTM

长短时依赖循环单元,如上所示,简单的说就是在传统的RNN
基础上添加了3个gate
来控制信息的流量,三个gate
分别控制输入input_gate
,输出output_gate
,遗忘forget_gate
,当前的cell
的信息输入为当前的编码向量xt以及上一个时刻的隐藏状态的输出ht−1,并且三个门的计算也是根据这两个变量进行计算得到,网络学习的过程实际上就是学习这些计算参数的过程。计算公式如下
$$
i,f,o=sigmoid(wx_t+wh_{t-1}+b) \
g=tanh(wx_t+wh_{t-1}+b)
$$
其中i,f,o,g分别对应为输入门,遗忘门,输出门及当前的输入,并且式子中的参数w为对应四个输出的不同变量。使用sigmoid
函数是由于该函数的取值范围为0-1
,可以显然的去对应控制信息流的流量。根据这几个门就可以得到当前这个cell
得到的信息,通过上一个时刻的cell
信息cellt−1并结合门控单元的遗忘门以及输入门得到
cellt=f⊙cellt−1+i⊙g
cell
和输出门共同控制当前cell
的隐藏状态输出,实际上这也就是当前时刻经过LSTM
得到的输出并以此向后传播,最终得到整个序列最后一个输出就是这个序列的输出ht=o⊙tanh(cellt)
在每一步的LSTM
计算中,接收上一个时刻的cell
信息和隐藏层状态输出,并通过上述计算得到当前步的cell
信息和隐藏层输出,并将这两个信息传递下去直至序列结束。
激活函数的选择
门控的激活函数是sigmoid
,生成候选输出是tanh
,这两个都是饱和函数,在输入值达到一定情况是,输出不会发生明显的变化,如果使用relu的话,很难实现门控的效果。
sigmoid
的函数控制在0-1之间,符合门控的物理意义,当输入较大或者较小的时候,输出会非常接近1或0,保证了门的开或者关。
tanh
函数控制输出在-1~1
之间,与多数场景下的分布是0中心相吻合,并且在输入为0时相比sigmoid有更大的梯度,是模型更快的收敛。
在计算能力有限的情况下,也可以使用0/1进行门控设计,设定一个阈值控制输出为0或者1
GRU
GRU
也是一种门控单元网络,于2014年提出,相比于LSTM
计算量更小,效果相当。因此在进行网络设计时优先使用GRU

对于GRU
来说,每个cell
接收上一时刻隐藏层状态的输出ht−1和当前时刻的编码向量xt,与普通的RNN
单元相同,当前的GRU
单元会计算得到当前的输出yt和传递给下一个节点的隐藏层状态ht。
不同于LSTM
,GRU
单元具有两个门控单元,分别为重置门r和更新门z,其计算方式和LSTM
相同,同样是使用上一个时刻隐藏状态的输出ht−1和当前时刻的编码向量xt,其计算方法为
r,z=sigmoid(w⊙[xt,ht−1])
hrt−1=ht−1⊙r
tanh
对输入的数据进行缩放htempt=tanh(w⊙[xt,hrt−1])
ht=z⊙ht−1+(1−z)⊙htempt
代码实现LSTM
1 | from __future__ import absolute_import, division, print_function |
代码实现GRU
1 | from __future__ import absolute_import, division, print_function |
如上就实现了一个LSTMcell
和GRU
,其中的计算完全按照公式进行。
在调用的时候,就是先进行LSTMCell
或者GRU
的设置,也就是__init__
中的参数,来建立LSTMcell
和输入数据之间的连接,具体如何设置在上面的代码中已有详细的说明。
举个栗子
1 | # 假设输入的维度是[32,40,256]的维度,用来模拟数据,可以认为是batch=32,n_step=40,input_dim=256 |
直接在命令行启动jupyter notebook
可以进行测试,启动命令如下
1 | jupyter lab |
上面完成了lstm
和gru
单元的编写以及测试

如上完成了创建的LSTM
和GRU
的测试,注意一点,LSTM
要从初始的initial_state
中提取出memory_cell
和hidden
因此,输入的state
的尺度为[batch,2*num_units]
,而对于GRU
来说,直接使用提供的state
进行门控的计算,因此输入的尺度为[batch,2*num_units]
,由于测试时设置的两个cell
的参数一样,可以看到经过cell
的计算得到的序列的维度是相同的。
seq2seq
seq2seq
是sequence to sequence
的缩写顾名思义就是实现序列到序列的模型,主要用于机器翻译,图像描述,语音识别等任务。
核心思想
通过深度神经网络讲一个输入的序列映射为一个作为输出的序列,这个过程中由编码和解码两个过程组成,也就是encoder-decoder
,一般的实现中encoder
和decoder
的部分由循环神经网络完成,并且在seq2seq
模型中是一个端对端的训练过程,最简单的seq2seq模型是将输入的序列进行encoder
,并将encoder
最后的state
作为decoder
部分的初始输入state
。
需要说明的是,对于encoder
部分,完成将输入的数据进行编码的过程,目前有使用RNN
、CNN
的方式,当然也有如论文《Attention is all you need》中所述的全部由attention
完成的;在decoder
部分一般使用的是RNN的方式。
seq2seq解码
Greedy search
对于seq2seq最核心的是解码部分,最基本的解码方法是贪心算法,选择一个度量标准之后,每次都在当前状态下选择最佳的一个结果(如进行softmax
之后使用argmax
返回最大值,类似于对输出的每个点进行一个分类计算),直至整个序列结束,贪心算法的计算代价相对较低,但是这种方法一般获得是一个局部最优解,一般结果不会太好。
beam search
集束搜索是目前常用的改进算法,用于模型的测试阶段,因为在训练过程中,每一个decoder
的输出是有正确答案的,也就不需要beam search
去加大输出的准确率。保存当前b个较好的选择,然后在解码时,每一步根据保存的选择进行下一步的扩展和排序,接着选择前b个进行保存,循环迭代,直到结束的时候选择最佳的一个作为解码的结果。
此外还有解码的时候使用多层堆叠RNN
,增加droupout
机制,与编码器之间建立残差连接等。
Attention
原理公式
由于在原始的seq2seq
模型中,对于输入的训练序列和输出的目标序列之间之间,每个目标序列的生成都是利用了整个训练序列的信息(encoder部分最后得到的state作为decoder的部分的初始输入,可以看做decoder接受了全部的encoder信息),但是在实际我们的生活应用中,对于目标序列的表述一般和训练序列之间存在着关键点的联系的,比如说输入的训练序列为中文”我爱你”,对应的输出的英文目标为’i love you’,显然”我”这个词对于生成目标中的”i”的贡献是最大的,但是传统的seq2seq模型中并没有考虑这层关系,直接使用了”我爱你”这三个字来生成”i”之后在根据”I”生成后面的目标序列,显然这样是不太可靠的。
对于传统seq2seq模型,结构图如下所示

首先输入序列和目标序列都要进行embeding转化为词向量,对输入序列进行encoder,并得到序列的final_state作为dencoder的初始输入,认为final_state包含了全部的输入序列的信息,也就是将输入序列的全部信息都压缩到了最后一个state之中,在decoder部分,将encoder的state作为decoder的初始state,目标序列为该部分的输入,至此一步步传递得到了最终输出序列。对于该模型,存在一个显然的问题就是encodr的最后的state显然无法对长度较长的序列进行信息的保存,此外,在预测输出的时候将全部的输入序列信息全部传入decoder,无法进行输入序列中不同词语输出词之间的对应关系。由此引入了attention结构,实际上也是一种序列对齐的方式。
attention结构

对于attention
结构,在decoder_cell
接收的输入为目标序列的当前输入,上一个时刻的状态,以及上一个时刻的label,以及encoder
部分得到的context
向量,对于contex
由attention
向量和encoder
的输出进行向量想乘并加和得到,attention
向量可以理解为一个encoder
部分输出向量的一个权重分布,说道权重,我们就应该想到了softmax
,可以很显然的将所有的权重归一化到0−1之间,那么attention
的权重分布由何计算得到呢?前面我们说了,引入attention
是为了将输入序列和输出序列对齐,那么在当前时刻,我们可以使用decoder
部分当前时刻的state
和encoder
部分的输出进行计算得到encoder
输出向量和当前时刻decoder
的对齐程度,实际上是一中条件概率,在得到encoder
输出前提下,得到当前attention
的概率。

接下来看一下各部分公式Reference


Here, the function
score
is used to compared the target hidden state ht with each of the source hidden states ¯hs, and the result is normalized to produced attention weights (a distribution over source positions). There are various choices of the scoring function; popular scoring functions include the multiplicative and additive forms given in Eq. (4). Once computed, the attention vector atis used to derive the softmax logit and loss. This is similar to the target hidden state at the top layer of a vanilla seq2seq model. The functionf
can also take other forms.
翻译一下:score
方程用于计算当前decoder隐藏层encoder部分所有的输出向量之间的对齐程度,常用的有直接向量相城的方程,求和的方式(两种attention计算方式);将score
计算的得分使用softmax的方式归一化为权重分布,也就是方程(1)中的attention权重的计算,attention权重和所有的encoder部分的输出进行向量相乘,得到的当前的contex向量,使用contex向量和当前decoder的输出向量计算得到当前时刻的decoder的输出,该输出即可与目标label进行损失计算。
实现
和上面实现LSTM
和GRU
类似,按照公式进attention
的设计,atttention
和decoder
进行连接,因此实际上可以设计一个cell
实现attention
机制的decoder_Cell
。计算attention
的权重可以通过线性计算的方式,之后在进行softmax
,或者使用直接加和然后进行归一化,得到分布,两种方式只是实现的方式不同,本人没感觉有什么不一样。
对于seq2seq模型的attention设计
1 | from __future__ import absolute_import, division, print_function |
对于ImageCaption的attention设计
对IimageCaption
任务来说,输入的是图片,输出的是该图片对应的描述,可以先将图像使用CNN
的方式进行编码得到图片的序列编码,将该编码输入到Attention
中,之后的方式与普通的seq2seq
模型相似,每一步输入图片的label编码向量,并使用图片序列计算得到对应的Attention
权重,并使用RNN
解码得到输出。
1 | import collections |
层归一化的实现
对于LSTM
和GRU
来说,目前并没有相应实现层归一化的api可以调用,不像CNN
中的BN
那样方便,基于此,现编写相关的代码以实现层归一化处理。
LN_GRU代码实现
1 | from __future__ import absolute_import, division, print_function |
LN_LSTM代码实现
1 | class LNLSTMCell(RNNCell): |
总结
可以看到在实现LN
的过程中,在标准LSTM/GRU
的基础上增加了LN
的处理,就是减去变量的均值并处以标准差,很简单即可完成。这两个cell
完全可以在模型设计中取代标准的GRU
或者LSTM
,以提高模型的鲁棒性。
v1.5.2