基于Encoder-Decoder模式的机器翻译模型原理及实现

作者: JackMeGo | 来源:发表于2017-12-07 18:42 被阅读465次

    关键词: 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网络,使得对于输入序列,得到输出序列的条件概率最大化,如下图所示:

    Encoder-Decoder结构

    该论文的另一个创新点是在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,过程更加简单。

    LSTM计算公式 GRU计算公式

    论文《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,
                                                                                time_major=False)
    

    这段代码我们从后往前看,我们使用tf.nn.dynamic_rnn构造Encoder过程的RNN网络,tf.nn.dynamic_rn函数有如下几个重要的输入参数:

    • cell
      RNN网络的隐藏层,可以包含一个或多个隐藏层
    • inputs
      输入数据
    • sequence_length
      输入batch里面每句话的长度

    对于cell,我们通过build_single_cell函数构造了包含一个LSTM节点类型的隐藏层:

    def build_single_cell(self):
        cell = LSTMCell(self.hidden_units)
    
        # 给LSTMCell添加Dropout信息,可选的Wrapper有DropoutWrapper/ResidualWrapper/DeviceWrapper等
        cell = DropoutWrapper(cell, dtype=self.data_type, output_keep_prob=self.dropout_keep_prob)
        return cell
    

    对于输入的cell,可选的隐藏层节点类型有BaseRNNCell/GRUCell/BasicLSTMCell/LSTMCell/MultiRNNCell/_SlimRNNCell等多种类型。如果想构建多层隐藏层,可以使用MultiRNNCell,它包含一个列表,可以在列表中依次添加不同的隐藏层。这里我们使用LSTMCell类型的节点构建单隐藏层并指定隐藏层节点数目,同时,我们通过DropoutWrapper给LSTM节点添加了Dropout信息,通过添加Dropout,可以避免过拟合的现象。

    对于输入的inputs,我们采用将输入句子转换成词嵌入向量(WordEmbedding)的方式,这种方式将每个单词或字表示成一个向量,不同字或词之间的相关性可以通过向量夹角来量化。其中的WordEmbedding表可以是预训练好的,也可以是训练过程逐渐建立起来的。这里我们使用的是后者,在训练过程开始的时候随机初始化,通过训练过程来不停更新Embedding矩阵。

    def init_embedding(self, vocab_size, scope_name):
        with tf.variable_scope(scope_name):
            squrt3 = math.sqrt(3)
            # 从-squrt3到squrt3均匀初始化
            initializer = tf.random_uniform_initializer(-squrt3, squrt3, dtype=self.data_type)
    
            return tf.get_variable(shape=[vocab_size, self.embedding_size],
                                   initializer=initializer, dtype=self.data_type,
                                   name="embedding")
    

    我们并没有直接把句子中每个单词的Embedding向量组成的矩阵直接作为LSTM Cell的输入,而是先经过一个全连接网络进行处理,输出再作为LSTM Cell的输入。这样是因为不同的句子长度不同,我们需要统一截断或者填充到指定的最大处理长度(max_length),这样同样一个Embedding向量,有些位可能是实际的单词对应的值,有些位可能是填充的值。全连接网络的作用相当于对这两种数据进行了归一化,抹平了它们之间的差异,它的意义在这里。全连接网络通过layers.core里的Dense实现。

    对于输入sequence_length,这个很好理解,存储的就是一个batch里每句话的实际长度。通过这个数据可以知道哪些位是真实的单词哪些位是填充的值。

    这样,我们就完成了Encoder网络的构造,共包含两个隐藏层,一个全连接层和一个LSTM层。两个输出,一个encoder_outputs和一个encoder_output_state,这两个值会在Decoder过程中用到。

    Decoder部分代码:

    def build_decoder(self):
            decoder_scope_name = 'decoder'
            with tf.variable_scope('decoder'):
                # 构建用于Decoder的LSTMCell,和Encoder过程使用的cell一样
                self.decoder_cell = self.build_single_cell()
    
                encoder_outputs = self.encoder_outputs  
                decoder_initial_state = self.encoder_output_state
    
                ## 和Encoder的差别在于,Decoder过程添加了Attention机制
                # 使用BahdanauAttention,可选的有LuongAttention/BahdanauAttention等.Encoder的输出作为这里的输入
                self.attention_type = attention_wrapper.BahdanauAttention(num_units=self.hidden_units,
                                                                          memory=encoder_outputs,
                                                                          memory_sequence_length=self.train_encode_inputs_length)
    
                def attn_decoder_input_fn(inputs, attention):
                    return inputs
    
                # 使用AttentionWrapper将attention注入到decoder_cell
                self.decoder_cell = attention_wrapper.AttentionWrapper(
                    cell=self.decoder_cell,
                    attention_mechanism=self.attention_type,
                    attention_layer_size=self.hidden_units,
                    cell_input_fn=attn_decoder_input_fn,
                    initial_cell_state=decoder_initial_state,  # encoder_last_state[-1]  Encoder过程输出的state
                    alignment_history=False,
                    name="AttentionWrapper"
                )
    
                # Encoder过程输出的state作为Decoder过程的输入State
    #           initial_state = encoder_last_state  # [state for state in encoder_last_state] 这里没有用MultiCell,只有一个Cell
                self.decode_embedding = self.init_embedding(decoder_scope_name)
    
                # 将目标输入转换成对应的embedding表示
                self.decoder_inputs_embedded = tf.nn.embedding_lookup(params=self.decode_embedding, ids=self.train_decoder_inputs)
    
                # 构建处理Decoder输入的全连接层
                decoder_input_layer = Dense(self.hidden_units, dtype=self.data_type, name='decoder_input_projection')
                # embedding数据过一遍全连接网络层
                self.decoder_inputs_embedded = decoder_input_layer(self.decoder_inputs_embedded)
    
                # TrainingHelper用于在Decoder过程中自动获取每个batch的数据
                training_helper = seq2seq.TrainingHelper(inputs=self.decoder_inputs_embedded,
                                                        sequence_length=self.train_decoder_inputs_length,
                                                        time_major=False,
                                                        name='training_helper')
    
                # 构建输出层全连接网络,输出的类别数目是目标语言的词汇数
                decoder_output_layer = Dense(self.target_vocab_size, name='decoder_output_projection')
    
                training_decoder = seq2seq.BasicDecoder(cell=self.decoder_cell,                  # 加入Attention的decoder cell
                                                        helper=training_helper,                  # 获取数据的helper函数
                                                        initial_state=decoder_initial_state,  # Encoder过程输出的state作为Decoder过程的输入State
                                                        output_layer=decoder_output_layer)  # Decoder完成之后经过全连接网络映射到最终输出的类别
    
                # 获取一个batch里面最长句子的长度
                max_decoder_length = tf.reduce_max(self.train_decoder_inputs_length)
    
                ## 使用training_decoder进行dynamic_decode操作,输出decoder结果
                self.decoder_outputs, _, _ = seq2seq.dynamic_decode(decoder=training_decoder,
                                                                    impute_finished=True,
                                                                    maximum_iterations=max_decoder_length)
    
                # 生成一个和decoder_outputs.rnn_output结构一样的tensor,代表一次训练的结果
                self.decoder_logits_train = tf.identity(self.decoder_outputs.rnn_output)
                # 选择logits的最大值的位置作为预测选择的结果
                self.decoder_pred_train = tf.argmax(self.decoder_logits_train, axis=-1, name='decoder_pred_train')
    
                # 根据输入batch中每句话的长度,和指定处理的最大长度,填充mask数据,这样可以提高计算效率,同时不影响最终结果
                masks = tf.sequence_mask(lengths=self.train_decoder_inputs_length,
                                         maxlen=max_decoder_length, dtype=self.data_type, name='masks')
    
                # 计算loss
                self.loss = seq2seq.sequence_loss(logits=self.decoder_logits_train,  # 预测值
                                                  targets=self.train_decoder_targets,  # 实际值
                                                  weights=masks,                       # mask值
                                                  average_across_timesteps=True,
                                                  average_across_batch=True, )
    
                ## 接下来手动进行梯度更新
                # 首先获得trainable variables
                trainable_params = tf.trainable_variables()
    
                # 使用gradients函数,计算loss对trainable_params的导数,trainable_params包含各个可训练的参数
                gradients = tf.gradients(self.loss, trainable_params)
    
                # 对可训练参数的梯度进行正则化处理,将权重的更新限定在一个合理范围内,防止权重更新过于迅猛造成梯度爆炸或梯度消失
                clip_gradients, _ = tf.clip_by_global_norm(gradients, self.max_gradient_norm)
    
                # 一次训练结束后更新参数权重
                self.updates = tf.train.AdamOptimizer(learning_rate=self.learning_rate).apply_gradients(
                    zip(clip_gradients, trainable_params), global_step=self.global_step)
                # self.updates = tf.train.AdadeltaOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
    

    Decoder部分代码比较长,我们使用seq2seq.dynamic_decode进行Decoder过程的构建。该函数有这样几个输入:

    • decoder
      decoder指定了解码过程的细节。关键如下,cell是添加了Attention后的LSTM Cell,helper是一个辅助类,可以用GreedyEmbeddingHelper替代TrainingHelper进行贪婪解码。initial_state是Encoder过程输出的状态数据。output_layer是一个全连接层,Decoder中的LSTM隐藏层输出的结果通过这个全连接层转换成最终的翻译结果。
    training_decoder = seq2seq.BasicDecoder(cell=self.decoder_cell, # 加入Attention的decoder cell
                       helper=training_helper,                     # 获取数据的helper函数
                       initial_state=decoder_initial_state,  # Encoder过程输出的state作为Decoder过程的输入State
                       output_layer=decoder_output_layer)  # Decoder完成之后经过全连接网络映射到最终输出的类别
    
    • output_time_major
      没什么用,保持一致都设置False就行
    • maximum_iterations
      最大decoder次数,就是我们指定的max_length

    那如何生成包含Attention的LSTM Cell?首先调用build_single_cell构造一个LSTM隐藏层节点,这一步和和Encoder一样。不同的地方在于,Decoder里的LSTM节点需要添加Attention机制,使得输入单词和输出单词的对应关系集中在当前单词位置周围。这里Attention我们使用的是BahdanauAttention机制,可选的Attention机制包括LuongAttention/BahdanauAttention等。

    self.attention_type = attention_wrapper.BahdanauAttention(num_units=self.hidden_units,
                                                              memory=encoder_outputs,
                                                              memory_sequence_length=self.train_encode_inputs_length)
    
    def attn_decoder_input_fn(inputs, attention):
        return inputs
    
    # 使用AttentionWrapper将attention注入到decoder_cell
    self.decoder_cell = attention_wrapper.AttentionWrapper(
        cell=self.decoder_cell,
        attention_mechanism=self.attention_type,
        attention_layer_size=self.hidden_units,
        cell_input_fn=attn_decoder_input_fn,
        initial_cell_state=encoder_last_state,  # encoder_last_state[-1]  Encoder过程输出的state
        alignment_history=False,
        name="AttentionWrapper"
    )
    

    将encoder_outputs作为参数传入BahdanauAttention,并使用AttentionWrapper包裹一下LSTM Cell。AttentionWrapper的参数包括前面生成的LSTM Cell,Encoder过程输出的encoder_last_state等。这样就给LSTM Cell添加了Attention机制。

    通过seq2seq.dynamic_decode得到Decoder输出decoder_outputs后,利用tf.argmax就可以获取预测结果:

    self.decoder_pred_train = tf.argmax(self.decoder_logits_train, axis=-1, name='decoder_pred_train')
    

    通过seq2seq.sequence_loss,输入预测结果和真实结果,可以计算出损失loss:

    # 计算loss
    self.loss = seq2seq.sequence_loss(logits=self.decoder_logits_train,  # 预测值
                                      targets=self.train_decoder_targets,  # 实际值
                                      weights=masks,                       # mask值
                                      average_across_timesteps=True,
                                      average_across_batch=True, )
    

    计算出loss,最后一步就是梯度更新了,这里我们使用AdadeltaOptimizer进行梯度更新。可以简单的使用AdadeltaOptimizer默认的minimize方法更新梯度,如下:

    self.updates = tf.train.AdadeltaOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
    

    也可以手动进行梯度更新,我们选择手动更新梯度:

    # 获得trainable variables
    trainable_params = tf.trainable_variables()
    gradients = tf.gradients(self.loss, trainable_params)
    clip_gradients, _ = tf.clip_by_global_norm(gradients, self.max_gradient_norm)
    
    # 一次训练结束后更新参数权重
    self.updates = tf.train.AdamOptimizer(learning_rate=self.learning_rate).apply_gradients(
        zip(clip_gradients, trainable_params), global_step=self.global_step)
    

    首先通过tf.trainable_variables获得所有可训练的参数,然后使用gradients函数,计算loss对trainable_params的导数,trainable_params包含各个可训练的参数。
    再通过tf.clip_by_global_norm对可训练参数的梯度进行正则化处理,将权重的更新限定在一个合理范围内,防止权重更新过于迅猛造成梯度爆炸或梯度消失。最后通过AdamOptimizer的apply_gradients函数更新参数,完成一个训练过程。

    这样,通过在所有batch上循环epochs次,就完成了整个模型的训练。由于实际两个语言翻译模型的语聊库很大,词汇数目多,所以想要达到一个良好的效果,需要训练的时间很久(几天),而且需要在GPU上训练,在小样本上训练往往很难有满意的效果。

    参考:
    http://blog.csdn.net/jerr__y/article/details/53749693
    http://blog.csdn.net/malefactor/article/details/50550211
    https://blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html
    https://github.com/JayParks/tf-seq2seq

    相关文章

      网友评论

        本文标题:基于Encoder-Decoder模式的机器翻译模型原理及实现

        本文链接:https://www.haomeiwen.com/subject/qebdbxtx.html