机器翻译

本教程源代码目录在book/machine_translation,初次使用请您参考Book文档使用说明

说明

  1. 硬件要求 本文可支持在CPU、GPU下运行
  2. 对docker file cuda/cudnn的支持 如果您使用了本文配套的docker镜像,请注意:该镜像对GPU的支持仅限于CUDA 8,cuDNN 5
  3. 文档中代码和seq2seq.py不一致的问题 请注意:为使本文更加易读易用,我们拆分、调整了seq2seq.py的代码并放入本文。本文中代码与seq2seq.py的运行结果一致,如希望直接看到训练脚本输出效果,可运行seq2seq.py

背景介绍

机器翻译(machine translation, MT)是用计算机来实现不同语言之间翻译的技术。被翻译的语言通常称为源语言(source language),翻译成的结果语言称为目标语言(target language)。机器翻译即实现从源语言到目标语言转换的过程,是自然语言处理的重要研究领域之一。

早期机器翻译系统多为基于规则的翻译系统,需要由语言学家编写两种语言之间的转换规则,再将这些规则录入计算机。该方法对语言学家的要求非常高,而且我们几乎无法总结一门语言会用到的所有规则,更何况两种甚至更多的语言。因此,传统机器翻译方法面临的主要挑战是无法得到一个完备的规则集合[1]。

为解决以上问题,统计机器翻译(Statistical Machine Translation, SMT)技术应运而生。在统计机器翻译技术中,转化规则是由机器自动从大规模的语料中学习得到的,而非我们人主动提供规则。因此,它克服了基于规则的翻译系统所面临的知识获取瓶颈的问题,但仍然存在许多挑战:1)人为设计许多特征(feature),但永远无法覆盖所有的语言现象;2)难以利用全局的特征;3)依赖于许多预处理环节,如词语对齐、分词或符号化(tokenization)、规则抽取、句法分析等,而每个环节的错误会逐步累积,对翻译的影响也越来越大。

近年来,深度学习技术的发展为解决上述挑战提供了新的思路。将深度学习应用于机器翻译任务的方法大致分为两类:1)仍以统计机器翻译系统为框架,只是利用神经网络来改进其中的关键模块,如语言模型、调序模型等(见图1的左半部分);2)不再以统计机器翻译系统为框架,而是直接用神经网络将源语言映射到目标语言,即端到端的神经网络机器翻译(End-to-End Neural Machine Translation, End-to-End NMT)(见图1的右半部分),简称为NMT模型。


图1. 基于神经网络的机器翻译系统

本教程主要介绍NMT模型,以及如何用PaddlePaddle来训练一个NMT模型。

效果展示

以中英翻译(中文翻译到英文)的模型为例,当模型训练完毕时,如果输入如下已分词的中文句子:

这些 是 希望 的 曙光 和 解脱 的 迹象 .

如果设定显示翻译结果的条数(即柱搜索算法的宽度)为3,生成的英语句子如下:

0 -5.36816   These are signs of hope and relief . <e>
1 -6.23177   These are the light of hope and relief . <e>
2 -7.7914  These are the light of hope and the relief of hope . <e>
  • 左起第一列是生成句子的序号;左起第二列是该条句子的得分(从大到小),分值越高越好;左起第三列是生成的英语句子。
  • 另外有两个特殊标志:<e>表示句子的结尾,<unk>表示未登录词(unknown word),即未在训练字典中出现的词。

模型概览

本节依次介绍GRU(Gated Recurrent Unit,门控循环单元),双向循环神经网络(Bi-directional Recurrent Neural Network),NMT模型中典型的编码器-解码器(Encoder-Decoder)框架和注意力(Attention)机制,以及柱搜索(beam search)算法。

GRU

我们已经在情感分析一章中介绍了循环神经网络(RNN)及长短时间记忆网络(LSTM)。相比于简单的RNN,LSTM增加了记忆单元(memory cell)、输入门(input gate)、遗忘门(forget gate)及输出门(output gate),这些门及记忆单元组合起来大大提升了RNN处理远距离依赖问题的能力。

GRU[2]是Cho等人在LSTM上提出的简化版本,也是RNN的一种扩展,如下图所示。GRU单元只有两个门:

  • 重置门(reset gate):如果重置门关闭,会忽略掉历史信息,即历史不相干的信息不会影响未来的输出。
  • 更新门(update gate):将LSTM的输入门和遗忘门合并,用于控制历史信息对当前时刻隐层输出的影响。如果更新门接近1,会把历史信息传递下去。


图2. GRU(门控循环单元)

一般来说,具有短距离依赖属性的序列,其重置门比较活跃;相反,具有长距离依赖属性的序列,其更新门比较活跃。另外,Chung等人[3]通过多组实验表明,GRU虽然参数更少,但是在多个任务上都和LSTM有相近的表现。

双向循环神经网络

我们已经在语义角色标注一章中介绍了一种双向循环神经网络,这里介绍Bengio团队在论文[2,4]中提出的另一种结构。该结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。

具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步RNN的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵($W_1, W_3$),隐层到隐层自己的权重矩阵($W_2,W_5$),前向隐层和后向隐层到输出层的权重矩阵($W_4, W_6$)。注意,该网络的前向隐层和后向隐层之间没有连接。


图2. 按时间步展开的双向循环神经网络

编码器-解码器框架

编码器-解码器(Encoder-Decoder)[2]框架用于解决由一个任意长度的源序列到另一个任意长度的目标序列的变换问题。即编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用RNN实现。


图3. 编码器-解码器框架

编码器

编码阶段分为三步:

  1. one-hot vector表示:将源语言句子$x=\left { x_1,x_2,...,x_T \right }$的每个词$x_i$表示成一个列向量$w_i\epsilon \left { 0,1 \right }^{\left | V \right |},i=1,2,...,T$。这个向量$w_i$的维度与词汇表大小$\left | V \right |$ 相同,并且只有一个维度上有值1(该位置对应该词在词汇表中的位置),其余全是0。
  2. 映射到低维语义空间的词向量:one-hot vector表示存在两个问题,1)生成的向量维度往往很大,容易造成维数灾难;2)难以刻画词与词之间的关系(如语义相似性,也就是无法很好地表达语义)。因此,需再one-hot vector映射到低维的语义空间,由一个固定维度的稠密向量(称为词向量)表示。记映射矩阵为$C\epsilon R^{K\times \left | V \right |}$,用$s_i=Cw_i$表示第$i$个词的词向量,$K$为向量维度。
  3. 用RNN编码源语言词序列:这一过程的计算公式为$h_i=\varnothing \theta \left ( h_{i-1}, s_i \right )$,其中$h_0$是一个全零的向量,$\varnothing _\theta$是一个非线性激活函数,最后得到的$\mathbf{h}=\left { h_1,..., h_T \right }$就是RNN依次读入源语言$T$个词的状态编码序列。整句话的向量表示可以采用$\mathbf{h}$在最后一个时间步$T$的状态编码,或使用时间维上的池化(pooling)结果。

第3步也可以使用双向循环神经网络实现更复杂的句编码表示,具体可以用双向GRU实现。前向GRU按照词序列$(x_1,x_2,...,x_T)$的顺序依次编码源语言端词,并得到一系列隐层状态$(\overrightarrow{h_1},\overrightarrow{h_2},...,\overrightarrow{h_T})$。类似的,后向GRU按照$(x_T,x_{T-1},...,x_1)$的顺序依次编码源语言端词,得到$(\overleftarrow{h_1},\overleftarrow{h_2},...,\overleftarrow{h_T})$。最后对于词$x_i$,通过拼接两个GRU的结果得到它的隐层状态,即$h_i=\left [ \overrightarrow{h_i^T},\overleftarrow{h_i^T} \right ]^{T}$。


图4. 使用双向GRU的编码器

解码器

机器翻译任务的训练过程中,解码阶段的目标是最大化下一个正确的目标语言词的概率。思路是:

  1. 每一个时刻,根据源语言句子的编码信息(又叫上下文向量,context vector)$c$、真实目标语言序列的第$i$个词$u_i$和$i$时刻RNN的隐层状态$z_i$,计算出下一个隐层状态$z_{i+1}$。计算公式如下:

其中$\phi _{\theta '}$是一个非线性激活函数;$c$是源语言句子的上下文向量,在不使用注意力机制时,如果编码器的输出是源语言句子编码后的最后一个元素,则可以定义$c=h_T$;$u_i$是目标语言序列的第$i$个单词,$u_0$是目标语言序列的开始标记<s>,表示解码开始;$z_i$是$i$时刻解码RNN的隐层状态,$z_0$是一个全零的向量。

  1. 将$z_{i+1}$通过softmax归一化,得到目标语言序列的第$i+1$个单词的概率分布$p_{i+1}$。概率分布公式如下:

其中$W_sz_{i+1}+b_z$是对每个可能的输出单词进行打分,再用softmax归一化就可以得到第$i+1$个词的概率$p_{i+1}$。

  1. 根据$p_{i+1}$和$u_{i+1}$计算代价。
  2. 重复步骤1~3,直到目标语言序列中的所有词处理完毕。

机器翻译任务的生成过程,通俗来讲就是根据预先训练的模型来翻译源语言句子。生成过程中的解码阶段和上述训练过程的有所差异,具体介绍请见柱搜索算法

注意力机制

如果编码阶段的输出是一个固定维度的向量,会带来以下两个问题:1)不论源语言序列的长度是5个词还是50个词,如果都用固定维度的向量去编码其中的语义和句法结构信息,对模型来说是一个非常高的要求,特别是对长句子序列而言;2)直觉上,当人类翻译一句话时,会对与当前译文更相关的源语言片段上给予更多关注,且关注点会随着翻译的进行而改变。而固定维度的向量则相当于,任何时刻都对源语言所有信息给予了同等程度的关注,这是不合理的。因此,Bahdanau等人[4]引入注意力(attention)机制,可以对编码后的上下文片段进行解码,以此来解决长句子的特征学习问题。下面介绍在注意力机制下的解码器结构。

与简单的解码器不同,这里$z_i$的计算公式为:


可见,源语言句子的编码向量表示为第$i$个词的上下文片段$c_i$,即针对每一个目标语言中的词$u_i$,都有一个特定的$c_i$与之对应。$c_i$的计算公式如下:


从公式中可以看出,注意力机制是通过对编码器中各时刻的RNN状态$h_j$进行加权平均实现的。权重$a_{ij}$表示目标语言中第$i$个词对源语言中第$j$个词的注意力大小,$a_{ij}$的计算公式如下:


其中,$align$可以看作是一个对齐模型,用来衡量目标语言中第$i$个词和源语言中第$j$个词的匹配程度。具体而言,这个程度是通过解码RNN的第$i$个隐层状态$z_i$和源语言句子的第$j$个上下文片段$h_j$计算得到的。传统的对齐模型中,目标语言的每个词明确对应源语言的一个或多个词(hard alignment);而在注意力模型中采用的是soft alignment,即任何两个目标语言和源语言词间均存在一定的关联,且这个关联强度是由模型计算得到的实数,因此可以融入整个NMT框架,并通过反向传播算法进行训练。


图6. 基于注意力机制的解码器

柱搜索算法

柱搜索(beam search)是一种启发式图搜索算法,用于在图或树中搜索有限集合中的最优扩展节点,通常用在解空间非常大的系统(如机器翻译、语音识别)中,原因是内存无法装下图或树中所有展开的解。如在机器翻译任务中希望翻译“<s>你好<e>”,就算目标语言字典中只有3个词(<s>, <e>, hello),也可能生成无限句话(hello循环出现的次数不定),为了找到其中较好的翻译结果,我们可采用柱搜索算法。

柱搜索算法使用广度优先策略建立搜索树,在树的每一层,按照启发代价(heuristic cost)(本教程中,为生成词的log概率之和)对节点进行排序,然后仅留下预先确定的个数(文献中通常称为beam width、beam size、柱宽度等)的节点。只有这些节点会在下一层继续扩展,其他节点就被剪掉了,也就是说保留了质量较高的节点,剪枝了质量较差的节点。因此,搜索所占用的空间和时间大幅减少,但缺点是无法保证一定获得最优解。

使用柱搜索算法的解码阶段,目标是最大化生成序列的概率。思路是:

  1. 每一个时刻,根据源语言句子的编码信息$c$、生成的第$i$个目标语言序列单词$u_i$和$i$时刻RNN的隐层状态$z_i$,计算出下一个隐层状态$z_{i+1}$。
  2. 将$z_{i+1}$通过softmax归一化,得到目标语言序列的第$i+1$个单词的概率分布$p_{i+1}$。
  3. 根据$p_{i+1}$采样出单词$u_{i+1}$。
  4. 重复步骤1~3,直到获得句子结束标记<e>或超过句子的最大生成长度为止。

注意:$z_{i+1}$和$p_{i+1}$的计算公式同解码器中的一样。且由于生成时的每一步都是通过贪心法实现的,因此并不能保证得到全局最优解。

数据介绍

本教程使用WMT-16新增的multimodal task中的translation task的数据集。该数据集为英德翻译数据,包含29001条训练数据,1000条测试数据。

数据预处理

我们的预处理流程包括两步:

  • 将每个源语言到目标语言的平行语料库文件合并为一个文件:
  • 合并每个XXX.srcXXX.trg文件为XXX
  • XXX中的第$i$行内容为XXX.src中的第$i$行和XXX.trg中的第$i$行连接,用'\t'分隔。
  • 创建训练数据的“源字典”和“目标字典”。每个字典都有DICTSIZE个单词,包括:语料中词频最高的(DICTSIZE - 3)个单词,和3个特殊符号<s>(序列的开始)、<e>(序列的结束)和<unk>(未登录词)。

示例数据

为了验证训练流程,PaddlePaddle接口paddle.dataset.wmt16中提供了对该数据集预处理后的版本,调用该接口即可直接使用,因为数据规模限制,这里只作为示例使用,在相应的测试集上具有一定效果但在更多测试数据上的效果无法保证。

模型配置说明

下面我们开始根据输入数据的形式配置模型。首先引入所需的库函数以及定义全局变量:

from __future__ import print_function
import os
import six

import numpy as np
import paddle
import paddle.fluid as fluid
import paddle.fluid.layers as layers

dict_size = 30000  # 词典大小
bos_id = 0  # 词典中start token对应的id
eos_id = 1  # 词典中end token对应的id
source_dict_size = target_dict_size = dict_size  # 源/目标语言字典大小
word_dim = 512  # 词向量维度
hidden_dim = 512  # 编码器中的隐层大小
decoder_size = hidden_dim  # 解码器中的隐层大小
max_length = 256  # 解码生成句子的最大长度
beam_size = 4  # beam search的柱宽度
batch_size = 64  # batch 中的样本数

model_save_dir = "machine_translation.inference.model"

接着定义所需要的数据输入:

def data_func(is_train=True):
    # 源语言source数据
    src = fluid.data(name="src", shape=[None, None], dtype="int64")
    src_sequence_length = fluid.data(name="src_sequence_length",
                                     shape=[None],
                                     dtype="int64")
    inputs = [src, src_sequence_length]
    # 训练时还需要目标语言target和label数据
    if is_train:
        trg = fluid.data(name="trg", shape=[None, None], dtype="int64")
        trg_sequence_length = fluid.data(name="trg_sequence_length",
                                        shape=[None],
                                        dtype="int64")
        label = fluid.data(name="label", shape=[None, None], dtype="int64")
        inputs += [trg, trg_sequence_length, label]
    # data loader
    loader = fluid.io.DataLoader.from_generator(feed_list=inputs,
                                                capacity=10,
                                                iterable=True,
                                                use_double_buffer=True)
    return inputs, loader

然后如下实现使用双向GRU的编码器:

def encoder(src_embedding, src_sequence_length):
    # 使用GRUCell构建前向RNN
    encoder_fwd_cell = layers.GRUCell(hidden_size=hidden_dim)
    encoder_fwd_output, fwd_state = layers.rnn(
        cell=encoder_fwd_cell,
        inputs=src_embedding,
        sequence_length=src_sequence_length,
        time_major=False,
        is_reverse=False)
    # 使用GRUCell构建反向RNN
    encoder_bwd_cell = layers.GRUCell(hidden_size=hidden_dim)
    encoder_bwd_output, bwd_state = layers.rnn(
        cell=encoder_bwd_cell,
        inputs=src_embedding,
        sequence_length=src_sequence_length,
        time_major=False,
        is_reverse=True)
    # 拼接前向与反向GRU的编码结果得到h
    encoder_output = layers.concat(
        input=[encoder_fwd_output, encoder_bwd_output], axis=2)
    encoder_state = layers.concat(input=[fwd_state, bwd_state], axis=1)
    return encoder_output, encoder_state

再实现基于注意力机制的解码器:

  • 首先通过 Cell 定义解码器中单步的计算,即$z_{i+1}=\phi _{\theta '}\left ( c_i,u_i,z_i \right )$,这里使用 GRU 并加上注意力机制(Additive Attention),代码如下:

    class DecoderCell(layers.RNNCell):
        def __init__(self, hidden_size):
            self.hidden_size = hidden_size
            self.gru_cell = layers.GRUCell(hidden_size)
    
        def attention(self, hidden, encoder_output, encoder_output_proj,
                      encoder_padding_mask):
            # 定义attention用以计算context,即 c_i,这里使用Bahdanau attention机制
            decoder_state_proj = layers.unsqueeze(
                layers.fc(hidden, size=self.hidden_size, bias_attr=False), [1])
            mixed_state = fluid.layers.elementwise_add(
                encoder_output_proj,
                layers.expand(decoder_state_proj,
                            [1, layers.shape(decoder_state_proj)[1], 1]))
            attn_scores = layers.squeeze(
                layers.fc(input=mixed_state,
                        size=1,
                        num_flatten_dims=2,
                        bias_attr=False), [2])
            if encoder_padding_mask is not None:
                attn_scores = layers.elementwise_add(attn_scores,
                                                    encoder_padding_mask)
            attn_scores = layers.softmax(attn_scores)
            context = layers.reduce_sum(layers.elementwise_mul(encoder_output,
                                                            attn_scores,
                                                            axis=0),
                                        dim=1)
            return context
    
        def call(self,
                 step_input,
                 hidden,
                 encoder_output,
                 encoder_output_proj,
                 encoder_padding_mask=None):
            # Bahdanau attention
            context = self.attention(hidden, encoder_output, encoder_output_proj,
                                    encoder_padding_mask)
            step_input = layers.concat([step_input, context], axis=1)
            # GRU
            output, new_hidden = self.gru_cell(step_input, hidden)
            return output, new_hidden
    
  • 基于定义的单步计算,使用 fluid.layers.rnnfluid.layers.dynamic_decode 分别实现用于训练和预测生成的多步循环解码器,如下:

    def decoder(encoder_output,
                encoder_output_proj,
                encoder_state,
                encoder_padding_mask,
                trg=None,
                is_train=True):
        # 定义 RNN 所需要的组件
        decoder_cell = DecoderCell(hidden_size=decoder_size)
        decoder_initial_states = layers.fc(encoder_state,
                                        size=decoder_size,
                                        act="tanh")
        trg_embeder = lambda x: fluid.embedding(input=x,
                                                size=[target_dict_size, hidden_dim],
                                                dtype="float32",
                                                param_attr=fluid.ParamAttr(
                                                    name="trg_emb_table"))
        output_layer = lambda x: layers.fc(x,
                                           size=target_dict_size,
                                           num_flatten_dims=len(x.shape) - 1,
                                           param_attr=fluid.ParamAttr(name=
                                                                      "output_w"))
        if is_train:  # 训练
            # 训练时使用 `layers.rnn` 构造由 `cell` 指定的循环神经网络
            # 循环的每一步从 `inputs` 中切片产生输入,并执行 `cell.call`
            decoder_output, _ = layers.rnn(
                cell=decoder_cell,
                inputs=trg_embeder(trg),
                initial_states=decoder_initial_states,
                time_major=False,
                encoder_output=encoder_output,
                encoder_output_proj=encoder_output_proj,
                encoder_padding_mask=encoder_padding_mask)
            decoder_output = output_layer(decoder_output)
        else:  # 基于 beam search 的预测生成
            # beam search 时需要将用到的形为 `[batch_size, ...]` 的张量扩展为 `[batch_size* beam_size, ...]`
            encoder_output = layers.BeamSearchDecoder.tile_beam_merge_with_batch(
                encoder_output, beam_size)
            encoder_output_proj = layers.BeamSearchDecoder.tile_beam_merge_with_batch(
                encoder_output_proj, beam_size)
            encoder_padding_mask = layers.BeamSearchDecoder.tile_beam_merge_with_batch(
                encoder_padding_mask, beam_size)
            # BeamSearchDecoder 定义了单步解码的操作:`cell.call` + `beam_search_step`
            beam_search_decoder = layers.BeamSearchDecoder(cell=decoder_cell,
                                                           start_token=bos_id,
                                                           end_token=eos_id,
                                                           beam_size=beam_size,
                                                           embedding_fn=trg_embeder,
                                                           output_fn=output_layer)
            # 使用 layers.dynamic_decode 动态解码
            # 重复执行 `decoder.step()` 直到其返回的表示完成状态的张量中的值全部为True或解码步骤达到 `max_step_num`
            decoder_output, _ = layers.dynamic_decode(
                decoder=beam_search_decoder,
                inits=decoder_initial_states,
                max_step_num=max_length,
                output_time_major=False,
                encoder_output=encoder_output,
                encoder_output_proj=encoder_output_proj,
                encoder_padding_mask=encoder_padding_mask)
    
        return decoder_output
    

接着就可以使用编码器和解码器定义整个网络,如下:

def model_func(inputs, is_train=True):
    # 源语言输入
    src = inputs[0]
    src_sequence_length = inputs[1]
    src_embeder = lambda x: fluid.embedding(
        input=x,
        size=[source_dict_size, hidden_dim],
        dtype="float32",
        param_attr=fluid.ParamAttr(name="src_emb_table"))
    src_embedding = src_embeder(src)

    # 编码器
    encoder_output, encoder_state = encoder(src_embedding, src_sequence_length)

    encoder_output_proj = layers.fc(input=encoder_output,
                                    size=decoder_size,
                                    num_flatten_dims=2,
                                    bias_attr=False)
    src_mask = layers.sequence_mask(src_sequence_length,
                                    maxlen=layers.shape(src)[1],
                                    dtype="float32")
    encoder_padding_mask = (src_mask - 1.0) * 1e9

    # 目标语言输入,训练时有、预测生成时无该输入
    trg = inputs[2] if is_train else None

    # 解码器
    output = decoder(encoder_output=encoder_output,
                     encoder_output_proj=encoder_output_proj,
                     encoder_state=encoder_state,
                     encoder_padding_mask=encoder_padding_mask,
                     trg=trg,
                     is_train=is_train)
    return output

为了进行训练还需要定义损失函数和优化器,如下:

def loss_func(logits, label, trg_sequence_length):
    probs = layers.softmax(logits)
    # 使用交叉熵损失函数
    loss = layers.cross_entropy(input=probs, label=label)
    # 根据长度生成掩码,并依此剔除 padding 部分计算的损失
    trg_mask = layers.sequence_mask(trg_sequence_length,
                                    maxlen=layers.shape(logits)[1],
                                    dtype="float32")
    avg_cost = layers.reduce_sum(loss * trg_mask) / layers.reduce_sum(trg_mask)
    return avg_cost

def optimizer_func():
    # 设置梯度裁剪
    fluid.clip.set_gradient_clip(
        clip=fluid.clip.GradientClipByGlobalNorm(clip_norm=5.0))
    # 定义先增后降的学习率策略
    lr_decay = fluid.layers.learning_rate_scheduler.noam_decay(hidden_dim, 1000)
    return fluid.optimizer.Adam(
        learning_rate=lr_decay,
        regularization=fluid.regularizer.L2DecayRegularizer(
            regularization_coeff=1e-4))

训练模型

定义数据生成器

使用内置的paddle.dataset.wmt16.train接口定义数据生成器,其每次产生一条样本,shuffle和组完batch后对batch内的样本进行padding作为训练的输入;同时定义预测使用的数据生成器,如下:

def inputs_generator(batch_size, pad_id, is_train=True):
    data_generator = fluid.io.shuffle(
        paddle.dataset.wmt16.train(source_dict_size, target_dict_size),
        buf_size=10000) if is_train else paddle.dataset.wmt16.test(
            source_dict_size, target_dict_size)
    batch_generator = fluid.io.batch(data_generator, batch_size=batch_size)

    # 对 batch 内的数据进行 padding
    def _pad_batch_data(insts, pad_id):
        seq_lengths = np.array(list(map(len, insts)), dtype="int64")
        max_len = max(seq_lengths)
        pad_data = np.array(
            [inst + [pad_id] * (max_len - len(inst)) for inst in insts],
            dtype="int64")
        return pad_data, seq_lengths

    def _generator():
        for batch in batch_generator():
            batch_src = [ins[0] for ins in batch]
            src_data, src_lengths = _pad_batch_data(batch_src, pad_id)
            inputs = [src_data, src_lengths]
            if is_train:  #训练时包含 target 和 label 数据
                batch_trg = [ins[1] for ins in batch]
                trg_data, trg_lengths = _pad_batch_data(batch_trg, pad_id)
                batch_lbl = [ins[2] for ins in batch]
                lbl_data, _ = _pad_batch_data(batch_lbl, pad_id)
                inputs += [trg_data, trg_lengths, lbl_data]
            yield inputs

    return _generator

构建训练程序

定义用于训练的Program,在其中创建训练的网络结构并添加优化器。同时还要定义用于初始化的Program,在创建训练网络的同时隐式的加入参数初始化的操作。

train_prog = fluid.Program()
startup_prog = fluid.Program()
with fluid.program_guard(train_prog, startup_prog):
    with fluid.unique_name.guard():
        # 训练时:
        # inputs = [src, src_sequence_length, trg, trg_sequence_length, label]
        inputs, loader = data_func(is_train=True)
        logits = model_func(inputs, is_train=True)
        loss = loss_func(logits, inputs[-1], inputs[-2])
        optimizer = optimizer_func()
        optimizer.minimize(loss)

定义训练环境

定义您的训练环境,包括指定所用的设备、绑定训练使用的数据源和定义执行器。

# 设置训练设备
use_cuda = False
places = fluid.cuda_places() if use_cuda else fluid.cpu_places()
# 设置数据源
loader.set_batch_generator(inputs_generator(batch_size,
                                            eos_id,
                                            is_train=True),
                            places=places)
# 定义执行器,初始化参数并绑定Program
exe = fluid.Executor(places[0])
exe.run(startup_prog)
prog = fluid.CompiledProgram(train_prog).with_data_parallel(
    loss_name=loss.name)

训练主循环

通过训练循环数(EPOCH_NUM)来进行训练循环,并且每次循环都保存训练好的参数。注意,循环训练前要首先执行初始化的Program来初始化参数。另外作为示例这里EPOCH_NUM设置较小,该数据集上实际大概需要20个epoch左右收敛。

EPOCH_NUM = 2
for pass_id in six.moves.xrange(EPOCH_NUM):
    batch_id = 0
    for data in loader():
        loss_val = exe.run(prog, feed=data, fetch_list=[loss])[0]
        print('pass_id: %d, batch_id: %d, loss: %f' %
                (pass_id, batch_id, loss_val))
        batch_id += 1
    # 保存模型
    fluid.io.save_params(exe, model_save_dir, main_program=train_prog)

应用模型

构建预测程序

定义用于预测的Program,在其中创建预测的网络结构。

infer_prog = fluid.Program()
startup_prog = fluid.Program()
with fluid.program_guard(infer_prog, startup_prog):
    with fluid.unique_name.guard():
        inputs, loader = data_func(is_train=False)
        predict_seqs = model_func(inputs, is_train=False)

定义预测环境

定义您的预测环境,和训练类似,包括指定所用的设备、绑定训练使用的数据源和定义执行器。

use_cuda = False
# 设置训练设备
places = fluid.cuda_places() if use_cuda else fluid.cpu_places()
# 设置数据源
loader.set_batch_generator(inputs_generator(batch_size,
                                            eos_id,
                                            is_train=False),
                            places=places)
# 定义执行器,加载参数并绑定Program
exe = fluid.Executor(places[0])
exe.run(startup_prog)
fluid.io.load_params(exe, model_save_dir, main_program=infer_prog)
prog = fluid.CompiledProgram(infer_prog).with_data_parallel()

测试

循环测试数据进行预测,生成过程对于每个测试数据都会将源语言句子和beam_size个生成句子打印输出,为打印出正确的句子还需要使用id到word映射的词典。如下:

# 获取 id 到 word 映射的词典
src_idx2word = paddle.dataset.wmt16.get_dict(
    "en", source_dict_size, reverse=True)
trg_idx2word = paddle.dataset.wmt16.get_dict(
    "de", target_dict_size, reverse=True)
# 循环测试数据
for data in loader():
    seq_ids = exe.run(prog, feed=data, fetch_list=[predict_seqs])[0]
    for ins_idx in range(seq_ids.shape[0]):
        print("Original sentence:")
        src_seqs = np.array(data[0]["src"])
        print(" ".join([
            src_idx2word[idx] for idx in src_seqs[ins_idx][1:]
            if idx != eos_id
        ]))
        print("Translated sentence:")
        for beam_idx in range(beam_size):
            seq = [
                trg_idx2word[idx] for idx in seq_ids[ins_idx, :, beam_idx]
                if idx != eos_id
            ]
            print(" ".join(seq).encode("utf8"))

可以观察到如下的预测结果输出:

Original sentence:
A man in an orange hat starring at something .
Translated score and sentence:
Ein Mann mit einem orangen Schutzhelm starrt auf etwas .
Ein Mann mit einem gelben Schutzhelm starrt auf etwas .
Ein Mann mit einem gelben Schutzhelm starrt etwas an .
Ein Mann mit einem orangen Schutzhelm starrt etwas an .

总结

端到端的神经网络机器翻译是近几年兴起的一种全新的机器翻译方法。本章中,我们介绍了NMT中典型的“编码器-解码器”框架。由于NMT是一个典型的Seq2Seq(Sequence to Sequence,序列到序列)学习问题,因此,Seq2Seq中的query改写(query rewriting)、摘要、单轮对话等问题都可以用本教程的模型来解决。

参考文献

  1. Koehn P. Statistical machine translation[M]. Cambridge University Press, 2009.
  2. Cho K, Van Merriënboer B, Gulcehre C, et al. Learning phrase representations using RNN encoder-decoder for statistical machine translation[C]//Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), 2014: 1724-1734.
  3. Chung J, Gulcehre C, Cho K H, et al. Empirical evaluation of gated recurrent neural networks on sequence modeling[J]. arXiv preprint arXiv:1412.3555, 2014.
  4. Bahdanau D, Cho K, Bengio Y. Neural machine translation by jointly learning to align and translate[C]//Proceedings of ICLR 2015, 2015.
  5. Papineni K, Roukos S, Ward T, et al. BLEU: a method for automatic evaluation of machine translation[C]//Proceedings of the 40th annual meeting on association for computational linguistics. Association for Computational Linguistics, 2002: 311-318.

知识共享许可协议
本教程PaddlePaddle 创作,采用 知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。