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

关键词: 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容