用图文简单介绍基于RNN的Encoder-Decoder中注意力机制
Encoder-Decoder
基本介绍
举个翻译的例子,原始句子\(X = (x_1, x_2, \cdots, x_m)\) ,翻译成目标句子\(Y = (y_1, y_2, \cdots, y_n)\) 。
现在采用Encoder-Decoder
架构模型,如下图
Encoder会利用整个原始句子生成一个语义向量
,Decoder再利用这个向量翻译成其它语言的句子。这样可以把握整个句子的意思、句法结构、性别信息等等。具体框架可以参考Encoder-Decoder框架。
Encoder对\(X\) 进行非线性变换得到中间语义向量c
: \[
c = G(x_1, x_2, \cdots, x_n)
\] Decoder根据语义\(c\) 和生成的历史单词\((y_1, y_2, \cdots, y_{i-1})\) 来生成第\(i\) 个单词 \(y_i\): \[
y_i = f(c, y_1, y_2, \cdots, y_{i-1})
\] Encoder-Decoder是个创新大杀器,是个通用的计算框架。Encoder和Decoder具体使用什么模型,都可以自己选择。通常有CNN,RNN,BiRNN,GRU,LSTM, Deep LSTM。上面的内容任意组合,只要得到的效果好,就是一个创新,就可以毕业了。(当然别人没有提出过)
缺点
在生成目标句子\(Y\)的单词时,所有的单词\(y_i\)使用的语义编码\(c\) 都是一样的。而语义编码\(c\)是由句子\(X\) 的每个单词经过Encoder编码产生,也就是说每个\(x_i\)对所有\(y_j\)的影响力都是相同的,没有任何区别的。所以上面的是注意力不集中的分心模型。
句子较短时问题不大,但是较长时,所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,会丢失更多的细节信息。
例子
比如输入\(X\)是Tom chase Jerry
,模型翻译出 汤姆 追逐 杰瑞
。在翻译“杰瑞”的时候,“Jerry”对“杰瑞”的贡献更重要。但是显然普通的Encoder-Decoder模型中,三个单词对于翻译“Jerry-杰瑞”的贡献是一样的。
解决方案应该是,每个单词对于翻译“杰瑞”的贡献应该不一样,如翻译“杰瑞”时: \[ (Tom, 0.3), \; (Chase, 0.2), \; (Jerry, 0.5) \]
Attention Model
基本架构
Attention Model的架构如下:
如图所示,生成每个单词\(y_i\)时,都有各自的语义向量\(C_i\),不再是统一的\(C\) 。 \[ y_i = f(C_i, y_1, \cdots, y_{i-1}) \] 例如,前3个单词的生成: \[ \begin{align} & y_1 = f(C_1) \\ & y_2 = f(C_2, y_1) \\ & y_3 = f(C_3, y_1, y_2) \\ \end{align} \]
语义向量的计算
注意力分配概率
\(a_{ij}\) 表示 \(y_i\)收到\(x_j\) 的注意力概率。
例如\(X=(Tom, Chase, Jerry)\),\(Y = (汤姆, 追逐, 杰瑞)\) 。\(a_{12}=0.2\)表示汤姆
收到来自Chase
的注意力概率是0.2。
有下面的注意力分配矩阵: \[ A = [a_{ij}] = \begin {bmatrix} 0.6 & 0.2 & 0.2 \\ 0.2 & 0.7 & 0.1 \\ 0.3 & 0.1 & 0.5 \\ \end {bmatrix} \] 第\(i\)行表示\(y_i\) 收到的所有来自输入单词的注意力分配概率。\(y_i\) 的语义向量\(C_i\) 由这些注意力分配概率和Encoder对单词\(x_j\)的转换函数相乘,计算而成,例如: \[ \begin {align} & C_1 = C_{汤姆} = g(0.6 \cdot h(Tom),\; 0.2 \cdot h(Chase),\; 0.2 \cdot h(Jerry)) \\ & C_2 = C_{追逐} = g(0.2 \cdot h(Tom) ,\;0.7 \cdot h(Chase) ,\;0.1 \cdot h(Jerry)) \\ & C_3 = C_{汤姆} = g(0.3 \cdot h(Tom),\; 0.2 \cdot h(Chase) ,\;0.5 \cdot h(Jerry)) \\ \end {align} \] \(\color{blue}{h(x_j)}\) 就表示Encoder对输入英文单词的某种变换函数。比如Encoder使用RNN的话,\(h(x_j)\)往往都是某个时刻输入\(x_j\) 后隐层节点的状态值。
g函数
表示注意力分配后的整个句子的语义转换信息,一般都是加权求和,则有语义向量计算公式: \[
C_i = \sum_{j=1}^{T_x} a_{ij} \cdot h_j, \quad h_j = h(x_j)
\] 其中\(\color{blue}{T_x}\) 代表输入句子的长度。形象来看计算过程如下图:
注意力分配概率计算
语义向量需要注意力分配概率和Encoder输入单词变换函数来共同计算得到。
但是比如汤姆
收到的分配概率\(a_1 = (0.6, 0.2, 0.2)\)是怎么计算得到的呢?
这里采用RNN作为Encoder和Decoder来说明。
注意力分配概率如下图计算
对于\(a_{ij}\) 其实是通过一个对齐函数F
来进行计算的,两个参数:输入节点\(j\),和输出节点\(i\),当然一般是取隐层状态。 \[
a_i = F(i, j), \quad j \in [1, T_x], \quad h(j)\,Encoder, \; H(i)\,Decoder
\] \(\color{blue}{F(i, j)}\)代表\(y_i\)和\(x_j\)的对齐可能性。一般F输出后,再经过softmax
就得到了注意力分配概率。
AM模型的意义
一般地,会把AM模型看成单词对齐模型,输入句子单词和这个目标生句子成单词的对齐概率。
其实,理解为影响力模型
也是合理的。就是在生成目标单词的时候,输入句子中的每个单词,对于生成当前目标单词有多大的影响程度。
AM模型有很多的应用,思想大都如此。
文本摘要例子
比如文本摘要
的例子,输入一个长句,提取出重要的信息。
输入"russian defense minister ivanov called sunday for the creation of a joint front for combating global terrorism"。
输出"russia calls for joint front against terrorism"。
下图代表着输入单词对输出单词的影响力,颜色越深,影响力越大,注意力分配概率也越大。
PyTorch翻译AM实现
思想
参考这篇论文 。
生成目标单词\(y_i\) 的计算概率是 \[ p(y_i \mid (y_1,\cdots, y_{i-1}), x) = g(y_{i-1}, s_i, c_i) \] 符号意义说明
- \(y_i\) 当前应该生成的目标单词,\(y_{i-1}\) 上一个节点的输出单词
- \(s_i\) 当前节点的隐藏状态
- \(c_i\) 生成当前单词应该有的语义向量
- \(g\) 全连接层的函数
隐层状态\(s_i\)
求当前Decoder隐层状态\(s_i\):由上一层的隐状态\(s_{i-1}\),输出单词\(y_{i-1}\) ,语义向量\(c_i\) \[ s_i = f(s_{i-1}, y_{i-1}, c_i) \] 语义向量\(c_i\)
语义向量:分配权值\(a_{ij}\),Encoder的输出 \[ c_i = \sum_{j=1}^{T_x} a_{ij} \cdot h_j, \quad h_j = h(x_j) \] 分配概率\(a_{ij}\)
注意力分配概率\(a_{ij} ,\) \(y_i\) 收到\(x_j\) 的注意力:分配能量\(e_{ij}\) \[ a_{ij} = \frac{\exp(e_{ij})} {\sum_{k=1}^{T_x} \exp (e_{ik})} \] 分配能量\(e_{ij}\)
\(x_j\) 注意\(y_i\) 的能量,由encoder的隐状态\(h_j\) 和 decoder的上一层的隐状态\(s_{i-1}\) 计算而成。a函数就是一个线性层。也就是上面的F函数。 \[ e_{ij} = a(s_{i-1}, h_j) \]
实现
Decoder由4层组成
- embedding : word2vec
- attention layer: 为每个encoder的output计算Attention
- RNN layer:
- output layer:
Decoder输入 \(s_{i-1}\) , \(y_{i-1}\) 和encoder的所有outputs \(h_*\)
Embedding Layer
输入\(y_{i-1}\),对其进行编码
1 | # y(i-1) |
Attention Layer
输入\(s_{i-1}, h_j\),输出分配能量\(e_{ij}\), 计算出\(a_{ij}\)
1 | attn_weights[j] = attn_layer(last_hidden, encoder_outputs[j]) |
计算语义向量
求语义向量\(c_i\), 一般是加权求和
1 | context = sum(attn_weights * encoder_outputs) |
RNN Layer
输入\(s_{i-1}, y_{i-1}, c_i\) ,内部隐层状态,输出\(s_i\)
1 | rnn_input = concat(embeded, context) |
输出层
输入\(y_{i-1}, s_i, c_i\) ,输出\(y_i\)
1 | output = out(embedded, rnn_output, context) |