本教程源代码目录在book/machine_translation,初次使用请您参考Book文档使用说明。
机器翻译(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模型。
本教程主要介绍NMT模型,以及如何用PaddlePaddle来训练一个NMT模型。
以中英翻译(中文翻译到英文)的模型为例,当模型训练完毕时,如果输入如下已分词的中文句子:
这些 是 希望 的 曙光 和 解脱 的 迹象 .
如果设定显示翻译结果的条数(即柱搜索算法的宽度)为3,生成的英语句子如下:
0 -5.36816 These are signs of hope and relief .1 -6.23177 These are the light of hope and relief . 2 -7.7914 These are the light of hope and the relief of hope .
表示句子的结尾,
表示未登录词(unknown word),即未在训练字典中出现的词。本节依次介绍GRU(Gated Recurrent Unit,门控循环单元),双向循环神经网络(Bi-directional Recurrent Neural Network),NMT模型中典型的编码器-解码器(Encoder-Decoder)框架和注意力(Attention)机制,以及柱搜索(beam search)算法。
我们已经在情感分析一章中介绍了循环神经网络(RNN)及长短时间记忆网络(LSTM)。相比于简单的RNN,LSTM增加了记忆单元(memory cell)、输入门(input gate)、遗忘门(forget gate)及输出门(output gate),这些门及记忆单元组合起来大大提升了RNN处理远距离依赖问题的能力。
GRU[2]是Cho等人在LSTM上提出的简化版本,也是RNN的一种扩展,如下图所示。GRU单元只有两个门:
一般来说,具有短距离依赖属性的序列,其重置门比较活跃;相反,具有长距离依赖属性的序列,其更新门比较活跃。另外,Chung等人[3]通过多组实验表明,GRU虽然参数更少,但是在多个任务上都和LSTM有相近的表现。
我们已经在语义角色标注一章中介绍了一种双向循环神经网络,这里介绍Bengio团队在论文[2,4]中提出的另一种结构。该结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。
具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步RNN的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵(W1,W3W1,W3),隐层到隐层自己的权重矩阵(W2,W5W2,W5),前向隐层和后向隐层到输出层的权重矩阵(W4,W6W4,W6)。注意,该网络的前向隐层和后向隐层之间没有连接。
编码器-解码器(Encoder-Decoder)[2]框架用于解决由一个任意长度的源序列到另一个任意长度的目标序列的变换问题。即编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用RNN实现。
编码器¶
编码阶段分为三步:
第3步也可以使用双向循环神经网络实现更复杂的句编码表示,具体可以用双向GRU实现。前向GRU按照词序列(x1,x2,...,xT)(x1,x2,...,xT)的顺序依次编码源语言端词,并得到一系列隐层状态(h1→,h2→,...,hT−→)(h1→,h2→,...,hT→)。类似的,后向GRU按照(xT,xT−1,...,x1)(xT,xT−1,...,x1)的顺序依次编码源语言端词,得到(h1←,h2←,...,hT←−)(h1←,h2←,...,hT←)。最后对于词xixi,通过拼接两个GRU的结果得到它的隐层状态,即hi=[hTi−→,hTi←−]Thi=[hiT→,hiT←]T。
解码器¶
机器翻译任务的训练过程中,解码阶段的目标是最大化下一个正确的目标语言词的概率。思路是:
其中ϕθ′ϕθ′是一个非线性激活函数;cc是源语言句子的上下文向量,在不使用注意力机制时,如果编码器的输出是源语言句子编码后的最后一个元素,则可以定义c=hTc=hT;uiui是目标语言序列的第ii个单词,u0u0是目标语言序列的开始标记
,表示解码开始;zizi是ii时刻解码RNN的隐层状态,z0z0是一个全零的向量。
softmax
归一化,得到目标语言序列的第i+1i+1个单词的概率分布pi+1pi+1。概率分布公式如下:其中Wszi+1+bzWszi+1+bz是对每个可能的输出单词进行打分,再用softmax归一化就可以得到第i+1i+1个词的概率pi+1pi+1。
机器翻译任务的生成过程,通俗来讲就是根据预先训练的模型来翻译源语言句子。生成过程中的解码阶段和上述训练过程的有所差异,具体介绍请见柱搜索算法。
如果编码阶段的输出是一个固定维度的向量,会带来以下两个问题:1)不论源语言序列的长度是5个词还是50个词,如果都用固定维度的向量去编码其中的语义和句法结构信息,对模型来说是一个非常高的要求,特别是对长句子序列而言;2)直觉上,当人类翻译一句话时,会对与当前译文更相关的源语言片段上给予更多关注,且关注点会随着翻译的进行而改变。而固定维度的向量则相当于,任何时刻都对源语言所有信息给予了同等程度的关注,这是不合理的。因此,Bahdanau等人[4]引入注意力(attention)机制,可以对编码后的上下文片段进行解码,以此来解决长句子的特征学习问题。下面介绍在注意力机制下的解码器结构。
与简单的解码器不同,这里zizi的计算公式为:
可见,源语言句子的编码向量表示为第ii个词的上下文片段cici,即针对每一个目标语言中的词uiui,都有一个特定的cici与之对应。cici的计算公式如下:
从公式中可以看出,注意力机制是通过对编码器中各时刻的RNN状态hjhj进行加权平均实现的。权重aijaij表示目标语言中第ii个词对源语言中第jj个词的注意力大小,aijaij的计算公式如下:
其中,alignalign可以看作是一个对齐模型,用来衡量目标语言中第ii个词和源语言中第jj个词的匹配程度。具体而言,这个程度是通过解码RNN的第ii个隐层状态zizi和源语言句子的第jj个上下文片段hjhj计算得到的。传统的对齐模型中,目标语言的每个词明确对应源语言的一个或多个词(hard alignment);而在注意力模型中采用的是soft alignment,即任何两个目标语言和源语言词间均存在一定的关联,且这个关联强度是由模型计算得到的实数,因此可以融入整个NMT框架,并通过反向传播算法进行训练。
柱搜索(beam search)是一种启发式图搜索算法,用于在图或树中搜索有限集合中的最优扩展节点,通常用在解空间非常大的系统(如机器翻译、语音识别)中,原因是内存无法装下图或树中所有展开的解。如在机器翻译任务中希望翻译“
”,就算目标语言字典中只有3个词(你好
,
, hello
),也可能生成无限句话(hello
循环出现的次数不定),为了找到其中较好的翻译结果,我们可采用柱搜索算法。
柱搜索算法使用广度优先策略建立搜索树,在树的每一层,按照启发代价(heuristic cost)(本教程中,为生成词的log概率之和)对节点进行排序,然后仅留下预先确定的个数(文献中通常称为beam width、beam size、柱宽度等)的节点。只有这些节点会在下一层继续扩展,其他节点就被剪掉了,也就是说保留了质量较高的节点,剪枝了质量较差的节点。因此,搜索所占用的空间和时间大幅减少,但缺点是无法保证一定获得最优解。
使用柱搜索算法的解码阶段,目标是最大化生成序列的概率。思路是:
softmax
归一化,得到目标语言序列的第i+1i+1个单词的概率分布pi+1pi+1。
或超过句子的最大生成长度为止。注意:zi+1zi+1和pi+1pi+1的计算公式同解码器中的一样。且由于生成时的每一步都是通过贪心法实现的,因此并不能保证得到全局最优解。
本教程使用WMT-16新增的multimodal task中的translation task的数据集。该数据集为英德翻译数据,包含29001条训练数据,1000条测试数据。
我们的预处理流程包括两步:
XXX.src
和XXX.trg
文件为XXX
。XXX
中的第ii行内容为XXX.src
中的第ii行和XXX.trg
中的第ii行连接,用'\t'分隔。
(序列的开始)、
(序列的结束)和
(未登录词)。
为了验证训练流程,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 定义解码器中单步的计算,即zi+1=ϕθ′(ci,ui,zi)zi+1=ϕθ′(ci,ui,zi),这里使用 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.rnn
和 fluid.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)、摘要、单轮对话等问题都可以用本教程的模型来解决。
本教程 由 PaddlePaddle 创作,采用 知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。