用图文简单介绍基于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
2
# y(i-1)
embedded = embedding(last_rnn_output)

Attention Layer

输入\(s_{i-1}, h_j\),输出分配能量\(e_{ij}\), 计算出\(a_{ij}\)

1
2
attn_weights[j] = attn_layer(last_hidden, encoder_outputs[j])
attn_weights = normalize(attn_weights)

计算语义向量

求语义向量\(c_i\), 一般是加权求和

1
context = sum(attn_weights * encoder_outputs)

RNN Layer

输入\(s_{i-1}, y_{i-1}, c_i\) ,内部隐层状态,输出\(s_i\)

1
2
rnn_input = concat(embeded, context)
rnn_output, rnn_hidden = rnn(rnn_input, last_hidden)

输出层

输入\(y_{i-1}, s_i, c_i\) ,输出\(y_i\)

1
output = out(embedded, rnn_output, context)