经典的RNN结构

这就是最经典的RNN结构,它的输入是:
输出为:
也就是说,输入和输出序列必有相同的时间长度!

假设输入 (
) 是一个长度为
(
) 的列向量:
隐藏层 是一个长度为
(
) 的列向量:
输出 是一个长度为
(
) 的列向量:
其中 ,
,
都是由人工设定的。

时刻输入层-->
时刻隐藏层:
时刻隐藏层-->
时刻隐藏层:
时刻输入层 and
时刻隐藏层-->
时刻隐藏层:
时刻隐藏层-->
时刻输出层:
需要注意的是,对于任意时刻 ,所有的权值(包括
,
,
,
,
,
)都相等,这也就是RNN中的“权值共享”,极大的减少参数量。
其实RNN可以简单的表示为:

还有一个小细节:在 时刻,如果没有特别指定初始状态,一般都会使用全0的
作为初始状态输入到
中
Sequence to Sequence模型

在Seq2Seq结构中,编码器encoder把所有的输入序列都编码成一个统一的语义向量context,然后再由解码器decoder解码。在解码器decoder解码的过程中,不断地将前一个时刻 的输出作为后一个时刻
的输入,循环解码,直到输出停止符为止。

接下来以机器翻译为例,看看如何通过Seq2Seq结构把中文“早上好”翻译成英文“Good morning”:
- 将“早上好”通过encoder编码,并将最后
时刻的隐藏层状态
作为语义向量。
- 以语义向量为decoder的
状态,同时在
时刻输入<start>特殊标识符,开始解码。之后不断的将前一时刻输出作为下一时刻输入进行解码,直接输出<stop>特殊标识符结束。
当然,上述过程只是Seq2Seq结构的一种经典实现方式。Seq2Seq结构不再要求输入和输出序列有相同的时间长度!

进一步来看上面机器翻译例子decoder端的 时刻数据流,如图7:
- 首先对RNN输入大小为
的向量
(红点);
- 然后经过RNN输出大小为
的向量
(蓝点);
- 接着使用全连接fc将
变为大小为
的向量
,其中
代表类别数量;
- 再
经过softmax和argmax获取类别index,再经过int2str获取输出字符;
- 最后将类别index输入到下一状态,直到接收到<stop>标志符停止。
还有一点细节,就是如何将前一时刻输出类别index(数值)送入下一时刻输入(向量)进行解码。假设每个标签对应的类别index如下:
'<start>' : 0,
'<stop>' : 1,
'good' : 2,
'morning' : 3,
...
已知<start>标志符index为0,如果需要将<start>标志符输入到input层,就需要把类别index=0转变为一个 长度的特定对应向量。这时就需要应用嵌入 (embedding) 方法。
- 首先生成一个大小为
embedding随机矩阵:
- 然后通过start标志index=0获取embedding矩阵的第0行,作为start标志对应的输入向量,即通过嵌入恢复维度:
- 把抽出形状
的
作为
时刻输入送入到RNN,获得
时刻形状为
输出
:
- 然后再通过全连接将形状为
的
变为形状为
的
(其中
为
,代表类别数量),之后通过softmax和argmax获取输出good标志对应的
:
- 再抽取embedding的第
行获取
,之后不停循环解码:
可以看到,其实Seq2Seq引入嵌入机制解决从label index数值到输入向量的维度恢复问题。在Tensorflow中上述过程通过以下函数实现:
tf.nn.embedding_lookup
而在pytorch中通过以下接口实现:
torch.nn.Embedding
需要注意的是:train和test阶段必须使用一样的embedding矩阵!否则输出肯定是乱码。
Attention注意力机制

在Seq2Seq结构中,encoder把所有的输入序列都编码成一个统一的语义向量context,然后再由decoder解码。由于context包含原始序列中的所有信息,它的长度就成了限制模型性能的瓶颈。如机器翻译问题,当要翻译的句子较长时,一个context可能存不下那么多信息,就会造成精度的下降。除此之外,如果按照上述方式实现,只用到了编码器的最后一个隐藏层状态,信息利用率低下。
所以如果要改进Seq2Seq结构,最好的切入角度就是:利用encoder所有隐藏层状态 解决context长度限制问题。

接下来了解一下attention注意力机制基本思路(Luong Attention)
考虑这样一个问题:由于encoder的隐藏层状态 代表对不同时刻输入
的编码结果:
即encoder状态 ,
,
对应编码器对“早”,“上”,“好”三个中文字符的编码结果。那么在decoder时刻
通过3个权重
,
,
计算出一个向量
:
然后将这个向量与前一个状态拼接在一起形成一个新的向量输入到隐藏层计算结果:
decoder时刻 :
decoder时刻 和
同理,就可以解决context长度限制问题。由于
,
,
不同,就形成了一种对编码器不同输入
对应
的“注意力”机制(权重越大注意力越强)。

那么到底什么是attention注意力机制?
为了说明注意力机制结构,重新定义符号: 代表encoder状态,
代表decoder状态,
代表attention layer输出的最终decoder状态,如图10。需要说明,
和
是
大小的向量。接下来一起看看注意力机制具体实现方式。
- 首先,计算decoder的
时刻隐藏层状态
对encoder每一个隐藏层状态
权重
数值:
这里的score可以通过以下两种方式计算:
所谓dot就是向量内积,而general通过乘以 权重矩阵进行计算(
是
大小的矩阵)。一般来说general方法好于dot方法。
- 其次,利用权重
计算所有隐藏层状态
加权之和
,即生成新的大小为
的context状态向量:
- 接下来,将通过权重
生成的
与原始decoder隐藏层
时刻状态
拼接在一起:
这里 和
大小都是
,拼接后会变大。由于需要恢复为原来形状,所以乘以全连接
矩阵。
- 最后,对加入“注意力”的decoder状态
乘以
矩阵即可获得输出:
也可以根据需要,把新生成的状态 继续送入RNN继续进行学习。其中
和
参数需要通过学习获得。
需要说明,上述说明只是注意力机制的一种具体实现方式,并不意味着注意力机制仅此一种。

可以看到,整个Attention注意力机制相当于在Seq2Seq结构上加了一层“包装”,内部通过 权重引入注意力机制。无论在机器翻译,语音识别,自然语言处理(NLP),文字识别(OCR),Attention机制对Seq2Seq结构都有很大的提升。
拓展:如何向RNN加入额外信息?
其实对于RNN(包括Seq2Seq结构)的改进本质上都是给网络添加额外信息,从而使得网络有更完整的信息流,如Attention机制就是将之前的隐藏层状态加权后作为额外信息加入。

所以,假设有额外信息 ,给RNN网络添加额外信息主要有以下3种方式:
- ADD:直接将
叠加在输出
上。
- CONCAT:将
拼接在隐藏层
后全连接恢复维度(不恢复维度也可以,但是会造成参数量加倍)。上文Attention机制就使用此种方法。
- MLP:新添加一个对
的感知单元
。
读到这里,你也隐约Get到了Attention机制的10086种姿势。
参考文献
[1508.04025] Effective Approaches to Attention-based Neural Machine Translation参考代码
Pytorch: Translation with a Sequence to Sequence Network and Attention