关键词: Encoder-Decoder, LSTM, WordEmbedding
在机器学习领域,有很多任务是把一种样式的序列映射成另外一种样式的序列,比如把一种语言翻译成另一种语言,把一段语音转换成一段文本,给一段文字生成一句话简介,或者把一张图片转换成一段对图片内容的文字描述等。这些任务都可以看作是Seq2Seq类型的任务,也就是从一个Sequence转换成另一个Sequence。对Seq2Seq类型的任务,经常采用Encoder-Decoder模型来解决。
理论背景
Encoder-Decoder方法最早在论文《Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation》中提出,该论文使用了两个RNN网络来完成机器翻译(Statistical Machine Translation: SMT)工作,第一个RNN网络把一串符号序列编码成一个固定长度的向量表示,第二个RNN网络把这个固定长度的向量解码成目标符号序列。通过联合训练这两个RNN网络,使得对于输入序列,得到输出序列的条件概率最大化,如下图所示:
该论文的另一个创新点是在RNN网络中首次使用了GRU网络节点(Gated Recurrent Unit),GRU节点和LSTM以及Bi-LSTM节点同属于RNN类型网络节点的变种,它们在计算输入序列中某个元素的输出时,会同时考虑这一步的输入,前一步或者后一步的输出等信息,相当于把序列中时序的信息包括进来,对于像是SMT这种有时序概念的输入效果显著。相比如LSTM和Bi-LSTM的三个状态门input-gate、forget-gate、output-gate,GRU简化成了两个状态门reset-gate和update-gate,过程更加简单。
论文《Sequence to Sequence Learning with Neural Networks》在论文1的基础上,将Encoder过程的一层GRU隐藏层,改成四层的LSTM隐藏层,使得模型在处理长句子时效果更好,这很容易理解,LSTM相比GRU能学到序列中距离更远的两个元素之间的关联信息,而且层数越多,能记忆的信息也越多,所以处理长句子效果优于GRU。同时,该论文通过将输入序列倒序作为Encoder过程输入的方式,如输入序列“I like cat”倒序成“cat like I”,而Decoder顺序不变的方式,提高了Encoder的输出向量和Decoder结果之间的相关性,也起到了提升效果的作用。
前面提到的两篇论文有个共同点,它们通过Encoder处理输入序列生成固定长度的中间向量,该中间向量对Decoder过程输出序列中任意一个元素的作用都是均等的,比如将“I like cat”翻译成“我喜欢猫”时,翻译目标为“喜欢”或翻译目标为“猫”时,中间向量的作用是相同的,也就是一种注意力分散模型。
而实际上,翻译目标“喜欢”时“like”分量起的作用要大于“I”或者“cat”,同理翻译目标“猫”时“cat”分量作用要大于其他单词。为了弥补这个缺陷,论文《Neural Machine Translation by Jointly Learning to Align and Translate》提出了Attention机制,使得经过Encoder生成的中间向量包含了位置信息,在Decoder过程中处理不同输出序列时,不同位置的输入分量所占的权重不同,距离越近的元素权重越大,也就是所谓的注意力(Attention)越高。
通过这种方式,使得距离某个单词距离近的单词影响力高于距离远的单词的影响力,从而解决了这个问题。如将“Tom Chase Jerry”翻译成“汤姆追逐杰瑞”,在翻译“杰瑞”时,“Jerry”起到的作用肯定要比“Tom”“Chase”都要高。
代码实现
接下来我们利用Tensorflow实现一个基于Encoder-Decoder架构的机器翻译模型,并对代码进行简要分析。
代码分为两个大的部分,train过程和predict过程,train过程代表训练过程,predict过程代表预测过程。本文将介绍train过程,predict过程类似。
Train过程根据Encoder-Decoder结构又分为两部分,Encoder部分和Decoder部分。
Encoder部分代码:
def build_encoder(self):
encode_scope_name = 'encoder'
with tf.variable_scope(encode_scope_name):
# 构建单个的LSTMCell,同时添加了Dropout信息
encode_cell = self.build_single_cell()
## 首先将原始输入替换成embedding表示,然后经过一个全连接的网络层,然后作为tf.nn.dynamic_rnn的输入
# 生成初始化embedding矩阵
self.encode_embedding = self.init_embedding(encode_scope_name)
# 将输入句子转换成embedding表示
self.encoder_inputs_embedded = tf.nn.embedding_lookup(params=self.encode_embedding, ids=self.train_encode_inputs)
# 构造一个全连接层,含有hidden_units个隐藏节点
fully_connected_input_layer = Dense(self.hidden_units, dtype=self.data_type, name='encoder_input_projection')
# 将embedding数据过一遍全连接层,计算过程大致是:outputs = activation(inputs.kernel + bias)
self.encoder_inputs_embedded = fully_connected_input_layer(self.encoder_inputs_embedded)
# 将这个embedding信息作为tf.nn.dynamic_rnn的输入
self.encoder_outputs, self.encoder_output_state = tf.nn.dynamic_rnn(cell=encode_cell,
inputs=self.encoder_inputs_embedded,
sequence_length=self.train_encode_inputs_length, # 存储每句话的实际长度
dtype=self.data_type