本文主要从tensor shape的角度解析google的seq2seq_model的原理,这样理解起来比较直观,比较容易掌握具体细节。

一、概念扫盲

快速过一下encoder-decoder框架,seq2seq模型,attention模型的理论知识。

1、encoder-decoder框架

Encoder-Decoder框架可以看作是一种文本处理领域的研究模式,下图是文本处理领域里常用的Encoder-Decoder框架最抽象的一种表示。

图1

Encoder-Decoder框架可以这么直观地去理解:可以把它看作适合处理由一个句子(或篇章)生成另外一个句子(或篇章)的通用处理模型。对于句子对<X,Y>,我们的目标是给定输入句子X,期待通过Encoder-Decoder框架来生成目标句子Y。X和Y可以是同一种语言,也可以是两种不同的语言。而X和Y分别由各自的单词序列构成:

Encoder顾名思义就是对输入句子X进行编码,将输入句子通过非线性变换转化为中间语义表示C:

对于解码器Decoder来说,其任务是根据句子X的中间语义表示C和之前已经生成的历史信息y1,y2….yi-1来生成i时刻要生成的单词yi

每个yi都依次这么产生,那么看起来就是整个系统根据输入句子X生成了目标句子Y。
Encoder-Decoder是个非常通用的计算框架,至于Encoder和Decoder具体使用什么模型都是由研究者自己定的,常见的比如CNN/RNN/BiRNN/GRU/LSTM/Deep LSTM等。

2、attention模型

2.1、为什么要引入注意力

图1中展示的Encoder-Decoder模型是没有体现出“注意力模型”的,所以可以把它看作是注意力不集中的分心模型。为什么说它注意力不集中呢?请观察下目标句子Y中每个单词的生成过程如下:

其中f是decoder的非线性变换函数。对于上面的公式,在生成目标句子的单词时,不论生成哪个单词,是y1,y2也好,还是y3也好,他们使用的句子X的语义编码C都是一样的,没有任何区别。没有引入注意力的模型在输入句子比较短的时候估计问题不大,但是如果输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因。

引入AM模型的Encoder-Decoder框架如下图所示:

即生成目标句子单词的过程成了下面的形式:

而每个Ci可能对应着不同的源语句子单词的注意力分配概率分布。可以理解为在翻译单词Y1时,输入单词X1、X2、X3、X4对其贡献的程度。

2.2、AM注意力分配概率计算:

对于采用RNN的Decoder来说,如果要生成yi单词,在时刻i,我们是可以知道在生成Yi之前的隐层节点i时刻的输出值Hi的,而我们的目的是要计算生成Yi时的输入句子单词“Tom”、“Chase”、“Jerry”对Yi来说的注意力分配概率分布,那么可以用i时刻的隐层节点状态Hi去一一和输入句子中每个单词对应的RNN隐层节点状态hj进行对比,即通过函数F(hj,Hi)来获得目标单词Yi和每个输入单词对应的对齐可能性,这个F函数在不同论文里可能会采取不同的方法,然后函数F的输出经过Softmax进行归一化就得到了符合概率分布取值区间的注意力分配概率分布数值。

2.3、中间语义Ci的计算:

例如:输入:Tom Chase Jerry 输出:汤姆 追逐 杰瑞

其中,f2函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个f2函数的结果往往是某个时刻输入xi后隐层节点的状态值;g代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般是加权和。对应的注意力模型权值分别是0.6,0.2,0.2。如果形象表示的话,翻译中文单词“汤姆”的时候,数学公式对应的中间语义表示Ci的形成过程类似下图:

3、seq2seq模型

seq2seq 模型是一种基于Encoder-Decoder框架的,就像一个翻译模型,输入是一个序列(比如一个英文句子),输出也是一个序列(比如该英文句子所对应的法文翻译)。这种结构最重要的地方在于输入序列和输出序列的长度是可变的。下图表示一个序列”ABC”被翻译成”WXYZ”, 其中<EOS>代表一句话的结束。

经典的sequence-to-sequence模型由两个RNN网络构成,一个被称为“encoder”,另一个则称为“decoder”,前者负责把序列编码成一个固定长度的向量,这个向量作为输入传给后者,输出可变长度的向量,seq2seq模型按照时间展开的基本网络结构如下,

其中每一个小圆圈代表一个cell,比如GRUcell、LSTMcell、multi-layer-GRUcell、multi-layer-GRUcell等。尽管“encoder”或者“decoder”内部存在权值共享,但encoder和decoder之间一般具有不同的一套参数。

4、数据bucketing

Bucketing是一种有效处理不同长度的句子的方法。例如将英语翻译成法语时,输入具有不同长度的英语句子L1,输出是具有不同长度的法语句子L2,原则上应该为每一对(L1,L2+1)创建一个seq2seq模型。这会导致图很大,包括许多非常相似的子图。另一方面,我们可以用一个特殊的PAD符号填充每个句子,句子中的填充符号在浪费编解码计算资源的同时也会影响隐层状态。作为折中,使用多个buckets 并且将每个句子填充为对应的bucket的长度。
在处理图像时根据宽度创建N个buckets ,也就是说将所有的图像分成N类用了N个seq2seq模型来进行处理的,训练、预测的有所不同。以(encoder_input_length,decoder_input_length)对的形式表示,比如(16, 32)表示编码长度是16,解码长度是32。

二、seq2seq_model.py的encoder-decode解析

模型依照 http://arxiv.org/abs/1412.7449实现。

1、变量定义

 2、encoder模块

编码模块采用双向LSTM,代码在seq2seq_model.py的seq2seq_f方法中。其中,pre_encoder_inputs的shape [max_time, batch_size, cell.output_size],output_state_fw的shape[batch_size, lstm_fw_cell.output_size],output_state_bw的shape [batch_size, lstm_bw_cell.output_size]。

 3、decoder模块

解码模块采用双层LSTM,代码在seq2seq_model.py的seq2seq_f方法和seq2seq.py的embedding_attention_decoder方法中。

3.1、decoder输入预处理

对encoder模块的输出结果(pre_encoder_inputs)和cell状态结果(output_state_fw, output_state_bw)mask和reshape后,作为参数传给核心方法embedding_attention_decoder。

3.2、decoder和attention功能

embedding_attention_decoder()中的attention_decoder()实现了decode和attention两部分功能。

3.2.1、embedding_attention_decoder方法

输入参数:

1)attention_states:为编码模块每个序列的输出;
2)initial_state:编码模块最后序列的state;state=[c,h]
3)decoder_inputs:解码模块每个序列的输入
训练时,feed_previous为false,此时解码模块每个序列的输入为标签,此时,decoder_inputs中的值为标签的编码;
测试时,feed_previous为true,此时解码模块每个序列的输入为上一个解码节点的输出,此时,decoder_inputs中的值只有第一位GO有意义,其余的值都不用。
4)embedding_size:解码中每个序列的输入值用多少位表示。如识别26个小写英文字母和10个数字,embedding_size 设置为10,表示每个字符用10位表示。
5)feed_previous:控制解码中每个序列的输入值是真实标签值还是上一个解码节点的输出值。
6)num_symbols:输出值的类别数。如识别26个小写英文字母和10个数字,那么num_symbols就是39(还有三个标志位:GO,EOS,PADDING)。
7)cell:decoder的rnn模型,即每个序列的结构。如两层LSTM。

输出参数:
1) outputs: 解码模块中每个序列的输出值,即最终的预测结果。
2) state: 解码模块最后的状态值,包括h(t)和C(t)。
3) attention_weights_history: 注意力的权重参数。包括每个解码序列对应编码序列中的权重。

3.2.2、loop_fun和decoder_inputs预处理(embedding_attention_decoder方法中)

首先,根据mode(train or test)初始化loop_function,其中loop_function这个函数如果在训练阶段,就是个None,解码模块下一个序列的输入值就是decoder_inputs中对应的值。如果在测试阶段,是个可以获取前一个序列输出值的函数。用该函数获取前一个序列输出值,然后作为下一个序列的输入值。
然后,将decoder_inputs嵌入到词向量中,生成bucket[1] 个[batch_size, embedding_size]作为attention_decoder方法的输入参数decoder_inputs。

3.2.3、attention_decoder方法

embedding_attention_decoder方法的核心是调用attention_decoder方法,实现了上述的注意力和解码功能。

计算过程:

1)运行cell,参数为decoder_input的某个序列,以及前一个序列的状态,返回值为新序列的输出值和状态。
cell_output, new_state = cell(linear(input, prev_attn), prev_state)
input:解码模块某个序列的输入
prev_attn:解码模块某个序列的前一个序列的注意力
prev_state:解码模块某个序列的前一个序列的状态

代码:

2)计算新序列的注意力权重:

简单画了一下逻辑图。

逻辑:encoder->attention->C编码器->decoder

代码中的实现:
new_at_mk = softmax(V\^T * tanh(W * attention_states + U * new_state))
new_at_mk为编码模块的每个序列的注意力权重。其中:
attention_states:为编码模块的每个序列的输出值,编码模块有多少个序列,这个值就有多少个。
new_state:为当前解码序列的状态
new_attn = new_at_mk* attention_states
new_attn为新序列的注意力值,其中:
new_at_mk:为编码模块的每个序列的注意力权重;
attention_states:为编码模块的每个序列的输出值;
后面有这部分实现的代码。其中,W * attention_states是通过卷积层实现的;U * new_state是通过linear函数实现的;

代码:

3)计算output:
output = linear(cell_output, new_attn) #解码的某个序列的最终预测值。

 三、前后向传播过程

1、forward propagation

Seq2SeqModel的init中根据forword_only(train|test)分别初始化buckets_size个seq2seq模型。
训练时forword_only为false,此时解码模块每个序列的输入为标签,此时,decoder_inputs中的值为标签的编码。
测试时forword_only为true,此时解码模块每个序列的输入为上一个解码节点的输出,此时,decoder_inputs中的值只有第一位GO有意义,其余的值都不用。
model_with_buckets的计算过程:input->encoder->decoder->output(细节见二中的encoder和decoder部分)。

 2、误差信息的backward propagation

用AdadeltaOptimizer优化decoder的bucketing loss数组。

3、模型训练

N批次构建input_feed和output_feed通过sess.run(output_feed, input_feed)训练。

四、参考资料

1、https://arxiv.org/pdf/1412.7449.pdf
2、http://blog.csdn.net/u012223913/article/details/77487610
3、http://blog.csdn.net/malefactor/article/details/50550211
4、https://github.com/da03/Attention-OCR/blob/master/src/model/seq2seq_model.py
5、https://github.com/farizrahman4u/seq2seq/blob/master/seq2seq/models.py