原文:https://www.tensorflow.org/versions/r1.5/tutorials/seq2seq
(全文99.9%由google翻译,自学使用)
作者:Thang Luong,Eugene Brevdo,Rui Zhao(Google Research Blogpost,Github)
本教程的这个版本需要TensorFlow Nightly。 为了使用稳定的TensorFlow版本,请考虑其他分支,如tf-1.4。
如果使用这个代码库进行研究,请引用此。
介绍(Introduction)
序列 - 序列(seq2seq)模型(Sutskever et al., 2014, Cho et al., 2014
)在机器翻译,语音识别和文本摘要等各种任务中取得了巨大的成功。 本教程为读者提供了seq2seq模型的全面理解,并展示了如何从头构建一个有竞争力的seq2seq模型。 我们专注于神经机器翻译(NMT)的任务,它是seq2seq模型的第一个测试平台。 包含的代码是轻量级的,高质量的,生产就绪,并结合最新的研究思路。 我们通过以下方式达到这个:
- 使用最近的解码器/注意力包装器(decoder / attention wrapper)API,TensorFlow 1.2数据迭代器
- 结合我们强大的专业知识,建立循环和seq2seq模型
- 为建立最好的NMT模型和重现Google’s NMT (GNMT) 系统提供技巧和窍门。
我们相信提供人们可以轻松重现的benchmarks是非常重要的。 因此,我们提供了完整的实验结果,并在以下公开可用的数据集上对模型进行预训练:
- 小规模:IWSLT Evaluation Campaign提供的英语 - 越南语TED对话语料库(133K个句子对)。
- 大规模:WMT Evaluation Campaign提供的德英平行语料库(4.5M句子对)。
我们首先建立一些关于NMT的seq2seq模型的基本知识,解释如何建立和训练一个vanilla NMT模型。 第二部分将着重于构建具有注意力机制的竞争性NMT模型。 然后,我们将讨论一些技巧和诀窍,以建立最佳NMT模型(包括速度和翻译质量),如TensorFlow最佳实践(批处理,分段),双向RNN,波束搜索(beam search [这个是啥?])以及使用GNMT注意力扩展到多个GPU。
基础知识(Basic)
神经机器翻译的背景(Background on Neural Machine Translation)
在过去,传统的基于短语的翻译系统通过将源语句拆分成多个片段然后将其逐句翻译来执行他们的任务。 这导致了翻译产出的不流畅,并不像我们人类翻译的那样。 我们阅读整个源句子,理解它的意思,然后产生一个翻译。 神经机器翻译(NMT)模仿的就是这种方式!
具体而言,NMT系统首先使用编码器读取源句子来构建“思想”向量,表示句子含义的数字序列; 解码器然后处理句子向量以发出翻译,如图1所示。这通常被称为编码器 - 解码器体系结构。 NMT以这种方式解决了传统的基于短语的方法中的局部翻译问题:它可以捕捉语言的长期依赖性,例如性别协议; 语法结构等; 并通过谷歌神经机器翻译系统(Google Neural Machine Translation systems)展示了更流利的翻译。
NMT模型根据其确切的体系结构而有所不同。 对于序列数据,很自然的选择是大多数NMT模型所使用的递归神经网络(RNN)。 编码器和解码器通常使用RNN。 但是,RNN模型在以下方面有所不同:(a)方向性 - 单向或双向; (b)深度 - 单层或多层; 和(c)类型 - 通常是普通RNN,长期短期记忆(LSTM)或门控循环单元(GRU)。 有兴趣的读者可以在这篇博文中找到关于RNN和LSTM的更多信息。
在本教程中,我们以单向的深度多层RNN为例,将LSTM作为一个递归单元。 我们在图2中展示了这样模型的一个例子。在这个例子中,我们建立了一个模型,将源句子"I am a student"翻译成目标句子"Je suisétudiant"。 在高层次上,NMT模型由两个递归神经网络组成:编码器RNN仅仅消耗输入的源单词而不作任何预测; 另一方面,解码器在预测下一个单词的同时处理目标语句。
欲了解更多信息,我们向读者介绍本教程所基于的Luong (2016)。
安装教程(Installing the Tutorial)
要安装本教程,您需要在系统上安装TensorFlow。 本教程需要TensorFlow Nightly。 要安装TensorFlow,请按照此处的安装说明进行操作。
一旦安装了TensorFlow,您可以运行以下命令来下载本教程的源代码:
git clone https://github.com/tensorflow/nmt/
训练 - 如何建立我们的第一个NMT系统(Training – How to build our first NMT system)
首先,我们将深入探讨用具体的代码片断构建NMT模型的核心,我们将通过它更详细地解释图2。 我们将数据准备和完整的代码推迟到以后。 这部分是指文件model.py。
在底层,编码器和解码器RNNs接收以下输入:首先是源句子,然后是指示从编码转换到解码模式的边界标记"<s>" ,和目标语句。 对于训练,我们会给系统提供以下张量,这些张量在时间上是主要格式,包含文字索引:
- encoder_inputs [max_encoder_time,batch_size]:源输入单词。
- decoder_inputs [max_decoder_time,batch_size]:目标输入单词。
- decoder_outputs [max_decoder_time,batch_size]:目标输出单词,这些是decoder_input向左移动一个时间步,右边添加一个句尾结束标记。
这里为了提高效率,我们一起训练多个句子(batch_size)。 测试稍有不同,所以我们稍后再讨论。
嵌入(Embedding)
鉴于单词的分类性质,模型必须首先查找源和目标的嵌入来检索相应的词表示(word representations)。 为了使这个嵌入层起作用,首先为每种语言选择一个词汇表。 通常,选择词汇量V,只有最频繁的V词才被视为唯一。 所有其他单词都转换为"unknown"标记,并获得相同的嵌入。 嵌入权重,每种语言一套,通常在训练中学习。
# Embedding
embedding_encoder = variable_scope.get_variable(
"embedding_encoder", [src_vocab_size, embedding_size], ...)
# Look up embedding:
# encoder_inputs: [max_time, batch_size]
# encoder_emb_inp: [max_time, batch_size, embedding_size]
encoder_emb_inp = embedding_ops.embedding_lookup(
embedding_encoder, encoder_inputs)
同样,我们可以构建embedding_decoder和decoder_emb_inp。 请注意,可以选择使用预训练词表示(例如word2vec或Glove vectors)来初始化嵌入权重。 一般来说,给定大量的训练数据,我们可以从头学习这些嵌入。
编码器(Encoder)
一旦检索出来,嵌入字就作为输入被输入到主网络中,主网络由两个多层RNN组成 - 一个源语言的编码器和一个目标语言的解码器。 这两个RNN原则上可以分享相同的权重; 然而,在实践中,我们经常使用两个不同的RNN参数(这些模型在拟合大型训练数据集时效果更好)。 编码器RNN使用零矢量作为其起始状态,并且如下构建:
# Build RNN cell
encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Run Dynamic RNN
# encoder_outputs: [max_time, batch_size, num_units]
# encoder_state: [batch_size, num_units]
encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
encoder_cell, encoder_emb_inp,
sequence_length=source_sequence_length, time_major=True)
请注意,为避免浪费计算,句子具有不同的长度,我们通过source_sequence_length告诉dynamic_rnn确切的源句子长度。 由于我们的输入是时序重要的,我们设置time_major = True。 在这里,我们只建立一个单层的LSTM,encoder_cell。 我们将介绍如何构建多层LSTM,添加dropout,并在后面的章节中使用attention。
解码器(Decoder)
解码器也需要访问源信息,一个简单的方法就是用编码器的最后一个隐藏状态encoder_state来初始化它。 在图2中,我们将源语词“student”的隐藏状态传递给解码器端。
# Build RNN cell
decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Helper
helper = tf.contrib.seq2seq.TrainingHelper(
decoder_emb_inp, decoder_lengths, time_major=True)
# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
decoder_cell, helper, encoder_state,
output_layer=projection_layer)
# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)
logits = outputs.rnn_output
在这里,这个代码的核心部分是BasicDecoder对象、解码器,其接收decoder_cell(类似于encoder_cell),帮助器,以及之前的encoder_state作为输入。 通过分离出解码器和帮助器,我们可以重用不同的代码库,例如,可以用GreedyEmbeddingHelper代替TrainingHelper来进行贪婪的解码。 请参阅helper.py。
最后,我们没有提到projection_layer是一个稠密的矩阵,它将顶部隐藏状态转化为维数为V的logit向量。我们在图2的顶部说明了这个过程。
projection_layer = layers_core.Dense(
tgt_vocab_size, use_bias=False)
损失(Loss)
基于上面的logits,我们现在准备计算我们的训练损失:
crossent = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=decoder_outputs, logits=logits)
train_loss = (tf.reduce_sum(crossent * target_weights) /
batch_size)
这里,target_weights是与decoder_outputs相同大小的0-1矩阵。 它将目标序列长度之外的值填充0。
重要说明:值得指出的是,我们用batch_size来分割损失,所以我们的超参数对batch_size是“不变的”。 有些人用(batch_size * num_time_steps)来划分损失,减少了短句的错误。 更微妙的是,我们的超参数(应用于前一种方式)不能用于后一种方式。 例如,如果两种方法都使用SGD来学习1.0,则后一种方法有效地使用1 / num_time_steps的更小的学习速率。
梯度计算和优化(Gradient computation & optimization)
现在我们已经定义了NMT模型的正向传播。 计算反向传播只是几行代码的问题:
# Calculate and clip gradients
params = tf.trainable_variables()
gradients = tf.gradients(train_loss, params)
clipped_gradients, _ = tf.clip_by_global_norm(
gradients, max_gradient_norm)
训练RNNs的重要步骤之一是梯度裁剪。 在这里,我们在全局范围内进行裁剪。 最大值max_gradient_norm通常设置为5或1的值。最后一步是选择优化器。 Adam优化器是一个常用的选择。 我们也选择一个学习率。 learning_rate的值通常可以在0.0001到0.001之间; 随着训练的进行可以减少。
# Optimization
optimizer = tf.train.AdamOptimizer(learning_rate)
update_step = optimizer.apply_gradients(
zip(clipped_gradients, params))
在我们自己的实验中,我们使用标准SGD(tf.train.GradientDescentOptimizer),其学习速率逐步降低,这样可以获得更好的性能。 参见 benchmarks。
未完待续