背景
这篇论文是第一个在NLP中使用attention机制的工作。翻译任务是典型的seq2seq问题。那么,什么是seq2seq问题?简单的说就是,根据输入序列X,生成一个输出序列Y,序列的长度不固定。当输入序列X和输出序列Y是不同的语言时,就是机器翻译;当输入序列X是问题,输出序列Y是答案时,就是问答系统或者对话系统。根据输入和输出序列的特征,seq2seq主要应用在机器翻译、会话建模、文本摘要等。
解决seq2seq问题的基本框架是encoder-decoder模型,即编码-解码模型。编码就是将输入序列X转化成一个固定长度的向量;解码就是将之前生成的固定向量转换成输出序列Y。encoder和decoder可以看作两个自动编码器,使用常用的模型RNN,LSTM,GRU进行编码。本文encoder使用BiRNN,隐藏层的输出包含了输入序列中的词以及前后一些词的信息,decoder输入时加入attention。下面首先接受encoder-decoder模型,重点介绍如何在encoder-decoder模型上加入attention。
encoder-decoder模型
基本的seq2seq模型,包括三个部分,即Encoder、Decoder以及连接两者的中间状态向量State Vector,Encoder通过学习输入,将其编码成一个固定大小的状态向量C(也叫上下文向量),继而将C传给Decoder,Decoder再通过对状态向量C的学习来进行输出。如下图:
得到C的方式很多,最简单的方法是把Encoder的最后一个隐状态赋值给C,也可以对最后一个隐状态做一个变换得到C,还可以对所有的隐状态做变换,如下图:
拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。具体做法就是将c当做之前的初始状态h0输入到Decoder中:
根据给定的语义向量C和之前已经生成的输出序列来预测下一个输出的单词:
也可以写作:
在RNN中,上式可以简化成:
其中是RNN中的隐藏层,C是语义向量,是上个时间段的输出,反过来作为这个时间段的输入。g则可以是一个非线性的多层的神经网络,产生词典中各个词语属于的概率。
还有一种做法将c作为每一步的输入:
encoder-decoder模型的优点是:输入和输出序列可以长度不同。
encoder-decoder模型的局限性
最大的局限性就在于编码和解码之间的唯一联系就是一个固定长度的语义向量C。也就是说,编码器要将整个序列的信息压缩进一个固定长度的向量中去。
这样做有两个弊端,一是语义向量无法完全表示整个序列的信息,还有就是先输入的内容携带的信息会被后输入的信息稀释掉,或者说,被覆盖了。输入序列越长,这个现象就越严重。这就使得在解码的时候一开始就没有获得输入序列足够的信息, 那么解码的准确度自然也就要打个折扣了。
由于基础Seq2Seq的种种缺陷,随后引入了Attention的概念以及Bi-directional encoder layer等。
attention
为了解决这个问题,作者提出了Attention模型,或者说注意力模型。简单的说,这种模型在产生输出的时候,还会产生一个“注意力范围”表示接下来输出的时候要重点关注输入序列中的哪些部分,然后根据关注的区域来产生下一个输出,如此往复。模型的大概示意图如下所示:
相比于之前的encoder-decoder模型,attention模型最大的区别就在于它不在要求编码器将所有输入信息都编码进一个固定长度的向量之中。相反,此时编码器需要将输入编码成一个向量的序列,而在解码的时候,每一步都会选择性的从向量序列中挑选一个子集进行进一步处理。这样,在产生每一个输出的时候,都能够做到充分利用输入序列携带的信息。而且这种方法在翻译任务中取得了非常不错的成果。
在这篇文章中,作者提出了一个用于翻译任务的结构。解码部分使用了attention模型,而在编码部分,则使用了BiRNN(bidirectional RNN,双向RNN)。
解码
解码部分使用了attention模型。我们可以将之前定义的条件概率写作
其中表示解码器时刻的隐藏状态。计算公式:
注意这里的条件概率与每个目标输出相对应的内容向量有关。而在传统的方式中,只有一个内容向量C。那么这里的内容向量又该怎么算呢?其实是由编码时的隐藏向量序列按权重相加得到的。
由于编码是双向RNN,因此可以认为中包含输入序列中第i个词以及 前后一些词的信息。将隐藏权重向量按权重相加,表示在生成第i个内容向量对第j个输出的注意力分配是不同的。越高表示第个输出在第个输入上分配的注意力越多,在生成第个输出的时候受第个输入的影响也就越大。新的问题是如何得到?
由第个输出隐藏状态和输入中各个隐藏状态共同决定的,公式如下:
也就是说跟每一个分别计算一个数值,然后使用softmax得到时刻的输出在个输入隐藏状态中的注意力分配向量。这个分配向量,也就是计算的权重。
把公式按执行顺序汇总:
attention:
def get_att_score(dec_output, enc_output): # enc_output [n_step, n_hidden]
score = tf.squeeze(tf.matmul(enc_output, attn), 0) # score : [n_hidden]
dec_output = tf.squeeze(dec_output, [0, 1]) # dec_output : [n_hidden]
return tf.tensordot(dec_output, score, 1) # inner product make scalar value
def get_att_weight(dec_output, enc_outputs):
attn_scores = [] # list of attention scalar : [n_step]
enc_outputs = tf.transpose(enc_outputs, [1, 0, 2]) # enc_outputs : [n_step, batch_size, n_hidden]
for i in range(n_step):
attn_scores.append(get_att_score(dec_output, enc_outputs[i]))
# Normalize scores to weights in range 0 to 1
return tf.reshape(tf.nn.softmax(attn_scores), [1, 1, -1]) # [1, 1, n_step]
编码器:
model = []
Attention = []
with tf.variable_scope('decode'):
dec_cell = tf.nn.rnn_cell.BasicRNNCell(n_hidden)
dec_cell = tf.nn.rnn_cell.DropoutWrapper(dec_cell, output_keep_prob=0.5)
inputs = tf.transpose(dec_inputs, [1, 0, 2]) #decoder的输入
hidden = enc_hidden #encoder每一层最后一个step的输出,将encoder的最后一个隐状态赋值给hidden
for i in range(n_step):
# time_major True mean inputs shape: [max_time, batch_size, ...]
dec_output, hidden = tf.nn.dynamic_rnn(dec_cell, tf.expand_dims(inputs[i], 1),
initial_state=hidden, dtype=tf.float32, time_major=True)
#拿到hidden以后,将hidden当作之前的初始状态h0输入到decoser中
#dec_output 最后一层每个step的输出
#hidden 每一层最后一个step的输出
attn_weights = get_att_weight(dec_output, enc_outputs) # attn_weights : [1, 1, n_step]
Attention.append(tf.squeeze(attn_weights))
# matrix-matrix product of matrices [1, 1, n_step] x [1, n_step, n_hidden] = [1, 1, n_hidden]
context = tf.matmul(attn_weights, enc_outputs)
dec_output = tf.squeeze(dec_output, 0) # [1, n_step]
context = tf.squeeze(context, 1) # [1, n_hidden]
model.append(tf.matmul(tf.concat((dec_output, context), 1), out)) # [n_step(i), batch_size(=1), n_class]
编码
编码比较普通,只是传统的单向的RNN中,数据是按顺序输入的,因此第j个隐藏状态只能携带第j个单词本身以及之前的一些信息;而如果逆序输入,则包含第j个单词及之后的一些信息。如果把这两个结合起来,就包含了第j个输入和前后的信息。
with tf.variable_scope('encode'):
enc_cell = tf.nn.rnn_cell.BasicRNNCell(n_hidden) #n_hidden 隐藏层神经单元的个数
enc_cell = tf.nn.rnn_cell.DropoutWrapper(enc_cell, output_keep_prob=0.5)
# enc_outputs : [batch_size(=1), n_step(=decoder_step), n_hidden(=128)] 最后一层每个step的输出
# enc_hidden : [batch_size(=1), n_hidden(=128)] 每一层最后一个step的输出
enc_outputs, enc_hidden = tf.nn.dynamic_rnn(enc_cell, enc_inputs, dtype=tf.float32)
实验
注意力矩阵:
参考:
https://www.jianshu.com/p/1c6b1b0cd202
https://blog.csdn.net/u014595019/article/details/52826423
https://github.com/graykode/nlp-tutorial/blob/master/4-2.Seq2Seq(Attention)/Seq2Seq(Attention)-Tensor.py