作者: [Thang Luong, Eugene Brevdo, Rui Zhao] (Google Research Blogpost, Github)
译者: 宗道明
If make use of this codebase for your research, please cite this.
Introduction
为了更深入地了解神经机器翻译和seq2seq模型,我们推荐以下材料:Luong, Cho, Manning, (2016); Luong, (2016); and Neubig, (2017). 有很多构建seq2seq模型的工具,所以我们选择每种语言中的一个:
- tf-seq2seq (https://github.com/google/seq2seq)*[TensorFlow]*
- OpenNMT http://opennmt.net/ *[Torch]*
- OpenNMT-py https://github.com/OpenNMT/OpenNMT-py *[PyTorch]*
Sequence-to-sequence (seq2seq) 模型在机器翻译、语音识别、文本摘要等领域取得了不小的成功。我们旨在构建一个competitive seq2seq模型。我们将从一下三个方面着手实施我们的模型
使用最新的解码器/注意力包装器和 Tensorflow 1.2 data iterator
构建完整的seq2seq模型
提供技巧以建立最好的NMT模型和复现谷歌NMT (GNMT)系统。
我们认为,提供人们可以复现的基准非常重要。因此,我们提供了完整的实验结果,并以下公开可用数据集对我们的模型进行了预训练:
samll-scale: 由IWSLT Evaluation Campaign提供的英语-越语平行TED演讲语料库(133K句子对)
large-scale: 由WMT Evaluation Campaign提供的德语-英语平行语料库(450万对句子)。
我们首先建立了一些关于NMT的seq2seq模型的基本知识,解释如何建立和训练一个普通的NMT模型。第二部分将详细介绍建立具有注意机制的竞争性的NMT模型。然后,我们将讨论一些技巧和技巧,以构建尽可能好的NMT模型(在速度和翻译质量方面),比如TensorFlow最佳实践(批处理、分段)
、双向RNNs
、束搜索
,以及使用GNMT注意力
扩展到多个gpu。
Basic
神经机器翻译的背景
在过去,传统的基于短语(phrase-based)的翻译系统通过将源句分成多个块,然后逐句翻译来完成任务。这导致了翻译输出的不流畅,与我们人类的翻译方式不太一样。我们通读整篇文章,理解它的意思,然后再翻译出来。神经机器翻译(NMT)模仿了这一点!
Figure 1. Encoder-decoder architecture – example of a general approach for
NMT. An encoder converts a source sentence into a "meaning" vector which is
passed through a decoder to produce a translation.
具体地说,NMT系统首先使用编码器读取源语句来构建一个“thought”向量,即表示句子含义的数字序列;然后,解码器处理句子向量以发出翻译,如图1所示。这通常称为编解码器体系结构。通过这种方式,NMT以传统的基于短语的方法解决了本地翻译问题:它可以捕获语言中的长期依赖关系(long-range dependicies),例如性别协议;语法结构;如谷歌神经机器翻译系统所演示的那样,生成更流畅的翻译。
NMT模型
根据其确切的体系结构而有所不同。序列数据的自然选择是循环神经网络(RNN)
,大多数NMT模型都使用这种网络。通常RNN用于编码器和解码器。然而,RNN模型在以下方面有所不同:(a)方向性——单向或双向;(b)深度-单层或多层;(c)类型——通常是一个普通的RNN,一个长短时记忆(LSTM),或者一个门控递归单元(GRU)。
在本教程中,我们以一个单向的、使用LSTM作为递归单元的多层RNN为例。在图2中我们展示了这样一个模型。在这个例子中,我们建立了一个模型,将源句“I am a student ”翻译成目标句“”Je suis étudiant”。在较高的层次上,NMT模型由两个循环神经网络组成:编码器RNN只消耗输入源词,不做任何预测;另一方面,解码器在处理目标句子
的同时预测下一个单词。
Figure 2. Neural machine translation – example of a deep recurrent
architecture proposed by for translating a source sentence "I am a student" into
a target sentence "Je suis étudiant". Here, "<s>" marks the start of the
decoding process while "</s>" tells the decoder to stop.
训练——如何建立我们的第一个NMT系统训练
```让我们首先深入到用具体的代码片段构建NMT模型的核心,通过这些代码片段,我们将更详细地解释图2。这部分引用[**model.py**]().
对于训练,我们将向系统提供以下张量,这些张量格式如下:
- **encoder_inputs** [max_encoder_time, batch_size]: source input words.
- **decoder_inputs** [max_decoder_time, batch_size]: target input words.
- **decoder_outputs** [max_decoder_time, batch_size]: target output words, these are decoder_inputs shifted to the left by one time step with an end-of-sentence tag appended on the right.
为了提高效率,我们一次训练多个句子(batch_size)。测试时略有不同,所以我们将在稍后讨论它。
## Embedding
考虑到单词的分类特性,模型必须首先查找源和目标嵌入(look up source and target embddings),以检索相应的单词表示。要使这个嵌入层起作用,首先为每种语言选择一个词汇表。通常,选择词汇量为V的单词,只有最频繁的V单词才被视为惟一的。所有其他单词都转换为“unknown” token,并得到相同的嵌入。嵌入权重(每种语言一组)通常是在训练中学习的。
```py
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向量
)初始化嵌入权重。通常,给定大量的训练数据,我们可以从头开始学习这些嵌入。
Encoder
一旦检索到单词embeddings,就将其作为输入输入到主网络中,主网络由两个多层RNNs组成——源语言的编码器和目标语言的解码器。这两个RNNs在原则上可以共享相同的权重;然而,在实践中,我们经常使用两个不同的RNN参数(当拟合大型训练数据集时,这样的模型做得更好)。编码器RNN以零向量作为起始状态(starting state),构造如下:
# 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)# 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
,所以设置time_major=True
。在这里,我们只构建了单层LSTM encoder_cell。我们将在后面的部分中描述如何构建多层LSTMs、添加dropout和使用注意力。
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
,它接收decoder_cell
(类似于encoder_cell)、一个helper
和前面的encoder_state
作为输入。通过分离decoders和helpers,我们可以重用不同的代码库,例如,TrainingHelper可以用GreedyEmbeddingHelper
替换来进行贪婪解码。请参阅helper.py。
最后,我们还没有提到projection_layer,它是一个将最上面的隐藏状态转换为维度$V$的logit向量的权重矩阵(dense matrix)。
projection_layer = layers_core.Dense(tgt_vocab_size,use_bias =False)
Loss
根据上面的逻辑,我们现在可以计算我们的训练损失:
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_output大小相同的0 - 1(mask)矩阵。它用值0掩盖目标序列长度之外的填充位置。
Note:值得指出的是,我们将损失除以batch_size,因此我们的超参数对于batch_size是“invarant”。有些人将损失除以(batch_size * num_time_steps),这样可以减少短句子中的错误。更微妙的是,我们的超参数(应用于前一种方法)不能用于后一种方法。例如,如果两种方法都使用学习1.0的SGD,那么后一种方法有效地使用1 / num_time_steps的学习速率要小得多。
Gradient computation & optimization
我们现在已经定义了NMT模型的正向传递。计算反向传播传递只需几行代码:
params = tf.trainable_variables()
gradients = tf.gradients(train_loss,params)
clipped_gradients, _ = tf.clip_by_global_norm(gradients, max_gradient_norm)
梯度裁剪是训练神经网络的重要步骤之一。这里,我们按global norm, max_gradient_norm通常设置为5或1之类的值。最后一步是选择优化器。Adam优化器是一种常见的选择。我们还选择了一个学习率。learning_rate can的值通常在0.0001到0.001之间;并且可以随着训练步数的而减少。
optimizer = tf.train.Adamoptimizer(learning_rate)
update_step = optimizer.apply_gradients(zip(clipped_gradients,params))
训练一个NMT模型
我们将使用一个小型的并行TED演讲语料库(133K个训练示例)来进行这个练习。我们在这里使用的所有数据都可以在1中找到。我们将使用tst2012作为开发集,tst2013作为测试集。
运行以下命令下载训练NMT模型的数据:
nmt/scripts/download_iwslt15.sh /tmp/nmt_data
开始训练:
mkdir /tmp/nmt_model
python -m nmt.nmt \
--src=vi --tgt=en \
--vocab_prefix=/tmp/nmt_data/vocab \
--train_prefix=/tmp/nmt_data/train \
--dev_prefix=/tmp/nmt_data/tst2012 \
--test_prefix=/tmp/nmt_data/tst2013 \
--out_dir=/tmp/nmt_model \
--num_train_steps=12000 \
--steps_per_stats=100 \
--num_layers=2 \
--num_units=128 \
--dropout=0.2 \
--metrics=bleu
上面的命令训练了一个2层的LSTM seq2seq模型。隐藏层单元为128,训练的epoch为12轮。
# First evaluation, global step 0
eval dev: perplexity 17193.66
eval test: perplexity 17193.27
# Start epoch 0, step 0, lr 1, Tue Apr 25 23:17:41 2017
sample train data:
src_reverse: Điều đó , dĩ nhiên , là câu chuyện trích ra từ học thuyết của Karl Marx .
ref: That , of course , was the distilled from the theories of Karl Marx .
epoch 0 step 100 lr 1 step-time 0.89s wps 5.78K ppl 1568.62 bleu 0.00
epoch 0 step 200 lr 1 step-time 0.94s wps 5.91K ppl 524.11 bleu 0.00
epoch 0 step 300 lr 1 step-time 0.96s wps 5.80K ppl 340.05 bleu 0.00
epoch 0 step 400 lr 1 step-time 1.02s wps 6.06K ppl 277.61 bleu 0.00
epoch 0 step 500 lr 1 step-time 0.95s wps 5.89K ppl 205.85 bleu 0.00
我们可以启动Tensorboard查看训练时模型的总结:
tensorboard --port 22222 --logdir /tmp/nmt_model/
推理:如何得到产生翻译
当您在训练NMT模型(以及一旦您训练了模型)时,您可以获得给定的未见源语句的翻译。这个过程叫做推理。训练和推理(测试)有明显的区别:在推理时,我们只能访问源句,即 encoder_inputs
。执行解码的方法有很多。译码方法包括贪婪解码
、采样解码
和波束解码
译码。在这里,我们将讨论贪婪解码
策略。
这个想法很简单,我们在图3中进行了说明:
我们仍然使用与训练期间相同的方法对源语句进行编码,以获得
encoder_state
,并且这个encoder_state
用于初始化解码器
。解码器
一收到起始符号“< s >”(在我们的代码中称为tgt_sos_id),即开始解码(翻译)过程;</ s>< s >对于解码器端的每个时间步,我们都将RNN的输出作为一组logits。我们选择最有可能的单词,即与最大logit值关联的id作为发出的单词(这是“贪婪”行为)。例如,在图3中,单词“moi”在第一个解码步骤中具有最高的翻译概率。然后我们将这个单词作为输入输入到下一个时间步中。</ s >
< s >这个过程一直持续到句末标记“</ s >”作为输出符号生成(在我们的代码中称tgt_eos_id)。
Figure 3. Greedy decoding – example of how a trained NMT model produces a
translation for a source sentence "Je suis étudiant" using greedy search.
第三步是推理和训练的不同之处。推理并不总是输入正确的目标单词(teacher forcing),而是使用模型预测的单词。下面是实现贪心解码
的代码。它与训练解码器非常相似。
# Helper
helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(
embedding_decoder,
tf.fill([batch_size], tgt_sos_id), tgt_eos_id)
# Decoder
decoder = tf.contrib.seq2seq.BasicDecoder(
decoder_cell,helper,encoder_state,
output_layer = projection_layers
)
# Dynamic decoding
outputs, _ = tf.contrib.seq2seq.dynamic_decoder(
decoder, maximum_iterations = maximum_iterations
translations = outputs.sample_id
)
tf.fill(dims,values,name=None): 创建一个维度为dims,值为value的tensor对象.该操作会创建一个维度为dims的tensor对象,并将其值设置为value,该tensor对象中的值类型和value一致
当value为0时,该方法等同于tf.zeros()
当value为1时,该方法等同于tf.ones()
参数:
- dims: 类型为int32的tensor对象,用于表示输出的维度(1-D, n-D),通常为一个int32数- 组,如:1, [2,3]等
- value: 常量值(字符串,数字等),该参数用于设置到最终返回的tensor对象值中
- name: 当前操作别名(可选)
返回: - tensor对象,类型和value一致
在这里,我们使用greedembeddinghelper而不是traininghelper。由于我们事先不知道import tensorflow as tf sess = tf.InteractiveSession() dim = [2,3] tf.fill(dim, 5) # [[5 5 5] # [5 5 5]] tf.fill(dim, 5.0) # [[ 5. 5. 5.] # [ 5. 5. 5.]] tf.fill(dim, "5.0") # [['5.0' '5.0' '5.0'] # ['5.0' '5.0' '5.0']]
目标序列
的长度,所以我们使用最大迭代次数来限制翻译长度。一种启发式方法是解码长度最多是源语句
的两倍。maximum_iteration = tf.round(tf.reduce_max(source_length)*2)
在训练了一个模型之后,我们现在可以创建一个推理文件并翻译一些句子:
cat > /tmp/my_infer_file.vi
# (copy and paste some sentences from /tmp/nmt_data/tst2013.vi)
python -m nmt.nmt \
--out_dir=/tmp/nmt_model \
--inference_input_file=/tmp/my_infer_file.vi \
--inference_output_file=/tmp/nmt_model/output_infer
cat /tmp/nmt_model/output_infer # To view the inference as output
注意,只要有训练检查点,上述命令也可以在模型仍在训练时运行。有关详细信息,请参见[inference.py]
Intermediate
在经历了最基本的seq2seq模型之后,让我们更进一步!为了构建最先进的神经机器翻译系统,我们需要更多的“secret source”:注意力机制,它最初由Bahdanau等人在2015年提出,后来由Luong等人在2015年等人完善。注意机制的核心思想是通过在翻译过程中“注意”相关的源内容,在目标和源之间建立直接的捷径连接。注意力机制的一个很好的副产品是源句和目标句之间的一个易于可视化的对齐矩阵(如图4所示)。
Figure 4. Attention visualization – example of the alignments between source
and target sentences. Image is taken from (Bahdanau et al., 2015).
在普通的seq2seq模型中,在开始解码过程时,我们将最后一个源状态从编码器传递给解码器。这对于中短句很有效;然而,对于长句,单一的固定大小的隐藏状态成为信息瓶颈。注意力机制没有丢弃源RNN中计算的所有隐藏状态,而是提供了一种方法,允许解码器查看它们(将它们视为源信息的动态内存)。通过这样做,注意机制可以提高长句的翻译。如今,注意力机制已经成为事实上的标准 defacto standard,并已成功地应用于许多其他任务(包括图像标题生成(caption generation)、语音识别(speech recognition)和文本摘要(text summarization))。
注意力机制的背景
我们现在描述Luong et al.中提出的注意机制的一个实例,该实例已在多个最先进的系统中使用,包括OpenNMT等开源工具包和本教程中的 TF seq2seq API
。我们还将提供与注意机制的其他变体的连接。
Figure 5. Attention mechanism – example of an attention-based NMT system
as described in (Luong et al., 2015) . We highlight in detail the first step of
the attention computation. For clarity, we don't show the embedding and
projection layers in Figure (2).
如图5所示,注意计算发生在每个解码器的时间步长。它包括下列各阶段:
- 将当前
目标隐状态
与所有源状态
进行比较,以获得注意力权重
(可以如图4所示)。
$$\alpha_{t s}=\frac{\exp \left(\operatorname{score}\left(\boldsymbol{h}{t}, \overline{\boldsymbol{h}}{s}\right)\right)}{\sum_{s^{\prime}=1}^{S} \exp \left(\operatorname{score}\left(\boldsymbol{h}{t}, \overline{\boldsymbol{h}}{s^{\prime}}\right)\right)} \quad \quad \text { [Attention weights ] }$$
根据
注意力权值
计算上下文向量
作为源状态的加权平均
。 $$\boldsymbol{c}{t}=\sum{s} \alpha_{t s} \overline{\boldsymbol{h}}_{s} \quad \quad \text{ [Context vector] }$$结合上下文向量和当前目标隐藏状态,生成最终的注意力向量(注意力隐状态)
$$\boldsymbol{\tilde{h}}{t}=\tanh(\boldsymbol{W}{c}[\boldsymbol{c}{t};\boldsymbol{h}{t}]) \quad \quad \text{ [Attention vector ]}$$将注意力向量作为输入(input feeding)输入到下一个时间步。
这里,score function
用于将目标隐状态
$\boldsymbol{h}t$与每个源状态
$\overline{h}{s}$进行比较,并将结果规范化为产生的注意权重(分布在源位置上)。评分函数
有多种选择; 常用的评分函数
包括式(4)中给出的乘法和加法形式。一旦计算完毕,注意力向量$\boldsymbol{\tilde{h}}{t}$将用于推导softmax logit 和 loss。这类似于普通seq2seq模型顶层的目标隐状态。注意力机制还可以有其他的选择,注意力机制的各种实现可以在attention_wrapper.py中找到。
$$
\operatorname{score}\left(\boldsymbol{h}{t}, \overline{\boldsymbol{h}}{s}\right)=\left{\begin{array}{ll}{\boldsymbol{h}{t}^{\top} \boldsymbol{W} \overline{\boldsymbol{h}}{s}} & {\text { [Luong’s multiplicative style] }} \ {\boldsymbol{v}{a}^{\top} \tanh \left(\boldsymbol{W}{1} \boldsymbol{h}{t}+\boldsymbol{W}{2} \overline{\boldsymbol{h}}{s}\right)} & {[\text { Bahdanau’s additive style }]}\end{array}\right.
$$
在注意机制中什么是重要的?
正如上面的等式所暗示的,注意力有许多不同的变体。这些变体取决于评分函数
和注意力函数
的形式,以及是否如(Bahdanau et al., 2015)论文中最初建议的那样,在评分函数
中使用之前的目标隐状态$h_{t-1}$而不是当前的目标隐状态$h_t$。根据经验,我们发现只有特定的选择才重要。第一,注意力的基本形式,需要目标和源之间的直接连接。其次,重要的是将注意力向量提供给下一个时间步骤,以便将过去的注意力决策告知网络(Luong et al., 2015)。最后,评分函数的选择常常会导致不同的性能。更多信息请参见基准测试结果部分。
Attention Wrapper API
在实现AttentionWrapper时,我们借用了(Weston et al., 2015)](https://arxiv.org/abs/1410.3916)中关于内存网络的工作中的一些术语。本教程中介绍的注意机制是只读内存[read-only memory](),而不是可读和可写内存。具体地说,源隐状态集(或它们的转换版本,例如Luong评分风格中的$W\overline{h}_s$或Bahdanau评分风格的$W_2\overline{h}_s$)被称为“内存”。在每个时间步中,我们使用当前目标隐状态作为“查询”query来决定读取内存的哪些部分。通常,需要将“查询”与对应于各个内存槽的键key进行比较。在上面介绍注意机制时,我们碰巧使用了一组源隐状态(或者它们的转换版本,例如Bahdanau评分风格中的$W_1h_t$)作为“键”。我们可以从这个记忆网络术语中得到启发,衍生出其他形式的注意力!
有了注意力包装器,用注意力扩展我们的普通seq2seq代码将变得非常简单。这部分引用了代码attention_model.py. 首先,我们需要定义一个注意机制,例如,from (Luong et al., 2015):
# attention_states: [batch_size, max_time, num_units]
attention_states = tf.transpose(encoder_outputs, [1, 0, 2])
# Create an attention mechanism
attention_mechanism = tf.contrib.seq2seq.LuongAttention(
num_units, attention_states,
memory_sequence_length=source_sequence_length)
在前面的编码器部分中,encoder_output是顶层的所有源隐状态
的集合(encoder_outputs is the set of all source hidden states at the top layer),其形状为max_time、batch_size、num_units,因为我们使用dynamic_rnn,为了提高效率,将time_major设置为True。对于注意机制,我们需要确保传入的“内存”是批处理主内存,因此需要转置attention_states。我们将source_sequence_length传递给注意机制,以确保注意力权重得到适当的规范化(仅针对非填充位置)。
定义了注意里机制后,我们使用AttentionWrapper来包装解码单元:
decoder_cell = tf.contrib.seq2seq.AttentionWrapper(
decoder_cell,attention_mechanism,
attention_layer_size = num_units
)
剩下的代码几乎与解码器中的代码相同!
动手-建立一个基于注意力的NMT模型
为了提高注意力,我们需要使用luong、scaled_luong、bahdanau或normed_bahdanau中的一个作为训练期间注意力机制。。此外,我们需要为注意力模型创建一个新目录,因此我们不重用以前训练过的基本NMT模型。
运行以下命令开始训练:
mkdir /tmp/nmt_attention_model
python -m nmt.nmt \
-- attention = scaled_luong \
-- src = vi --tgt = en \
-- vocab_prefix = /tmp/nmt_data/vocab \
-- train_prefix = /tmp/nmt_data/train \
-- dev_prefix = /tmp/nmt_data/tst2012 \
-- test_prefix = /tmp/nmt_data/tst2013 \
-- out_dir = /tmp/nmt_attention_model \
-- num_train_steps = 12000 \
-- steps_per_stats = 100 \
-- num_layers = 2 \
-- num_units = 128 \
-- dropout = 0.2 \
-- metrics = bleu
经过训练,我们可以使用与新的out_dir相同的推理命令进行推理:
python -m nmt.nmt \
--out_dir=/tmp/nmt_attention_model \
--inference_input_file=/tmp/my_infer_file.vi \
--inference_output_file=/tmp/nmt_attention_model/output_infer
Tips & Tricks
Building Training, Eval, and Inference Graphs
在Tensorflow中构建机器学习模型时,通常最好构建三张单独的图:
训练图(Training Graph):
- Batches, buckets, and possibly subsamples input data from a set of
files/external inputs. - Includes the forward and backprop ops.
- Constructs the optimizer, and adds the training op.
- Batches, buckets, and possibly subsamples input data from a set of
评估图(Eval Graph):
- Batches and buckets input data from a set of files/external inputs.
- Includes the training forward ops, and additional evaluation ops that
aren’t used for training.
推理图(Inference Graph):
- May not batch input data.
- Does not subsample or bucket input data.
- Reads input data from placeholders (data can be fed directly to the graph
via feed_dict or from a C++ TensorFlow serving binary). - Includes a subset of the model forward ops, and possibly additional
special inputs/outputs for storing state between session.run calls.
构建单独的图有几个好处:
推理图通常与其他两个图非常不同,因此单独构建它是有意义的。 评估图(Eval Graph)变得更简单了,因为它不再有所有额外的支持操作。
可以为每个图分别实现数据输入。 变量重用要简单得多。例如,在评估图中,不需要使用
reuse=True
重新打开变量作用域,因为训练模型已经创建了这些变量。因此,可以重用相同的代码,而不需要到处散布resue=arguments
。在分布式训练中,让单独的工作人员执行训练、计算和推理是很常见的。无论如何,它们都需要构建自己的图。因此,以这种方式构建系统为分布式训练做好了准备。
复杂性的主要来源是如何在一个机器设置中跨三个图共享变量。这可以通过为每个图使用单独的会话来解决。训练会话(training Session)定期保存检查点,评估会话(eval session)和推断会话(infer session)从检查点恢复参数。下面的示例显示了这两种方法之间的主要区别。
例子1:一个图(a single graph)中的三个模型(three models)和共享一个会话 (a single session)
with tf.variable_scope("root"):
train_inputs = tf.placeholder()
train_op, loss = BuildTrainModel(train_inputs)
initializer = tf.global_variables_initiazlizer()
with tf.variable_scope("root",reuse=True):
eval_inputs = tf.placeholder()
eval_loss = BuilderEvalModel(eval_inputs)
with tf.variable_scope("root",reuse=True):
infer_inputs = tf.placeholder()
inference_output = BuildInference()
sess = tf.Session()
sess.run(initializer)
for i in itertools.count():
train_input_data = ...
sess.run([loss,train_op],feed_dict={train_inputs:train_input_data})
if i % EVAL_STEPS == 0:
while data_to_eval:
eval_input_data = ...
sess.run([eval_loss],feed_dict={eval_inputs:eval_input_data})
if i % INFER_STEPS == 0:
sess.run(inference_output, feed_dict ={infer_inputs: infer_input_data})
sess = tf.Session()
sess.run(initializer)
for i in itertools.count():
train_input_data = ...
sess.run([loss, train_op], feed_dict={train_inputs: train_input_data})
if i % EVAL_STEPS == 0:
while data_to_eval:
eval_input_data = ...
sess.run([eval_loss], feed_dict={eval_inputs: eval_input_data})
if i % INFER_STEPS == 0:
sess.run(inference_output, feed_dict={infer_inputs: infer_input_data})
注意后一种方法是如何“准备”转换为分布式版本的。
新方法的另一个区别是,我们使用有状态迭代器对象,而不是在每个session.run
调用中使用feed
指令来提供数据(从而执行我们自己的批处理、bucketing
和数据操作)。这些迭代器使输入管道在单机和分布式设置中都更加容易。我们将在下一节介绍新的输入数据管道(如tensorflow 1.2中介绍的)。
例子2:三个图中的三个模型,三个会话共享相同的变量
train_graph = tf.Graph()
eval_graph = tf.Graph()
infer_graph = tf.Graph()
with train_graph.as_default():
train_iterator = ...
train_model = BuildTrainModel(train_iterator)
initializer = tf.global_variables_initializer()
with eval_graph.as_default():
eval_iterator = ...
eval_model = BuildEvalModel(eval_iterator)
with infer_graph.as_default():
infer_iterator, infer_inputs = ...
infer_model = BuildInferenceModel(infer_iterator)
checkpoints_path = "/tmp/model/checkpoints"
train_sess = tf.Session(graph = train_graph)
eval_sess = tf.Session(graph = eval_graph)
infer_sess = tf.Session(graph = infer_graph)
train_sess.run(initializer)
train_sess.run(train_iterator.initializer)
for i in itertools.count():
train_model.train(train_sess)
if i % EVAL_STEPS == 0:
checkpoint_path = train_model.saver.save(train_sess, checkpoints_path, global_step=i)
eval_model.saver.restore(eval_sess, checkpoint_path)
eval_sess.run(eval_iterator.initializer)
while data_to_eval:
eval_model.eval(eval_sess)
if i % INFER_STEPS == 0:
checkpoint_path = train_model.saver.save(train_sess, checkpoints_path, global_step=i)
infer_model.saver.restore(infer_sess,checkpoint_path)
infer_sess.run(infer_iterator.initializer, feed_dict={infer_inputs:infer_input_data})
while data_to_infer:
infer_model.infer(infer_sess)
Other details for better NMT models
Bidirectional RNNs
编码器端的双向性通常会提供更好的性能(随着使用更多层,速度会有所下降)。这里,我们给出了一个简单的例子,说明如何构建一个具有单个双向层的编码器:
# Construct forward and backward cells
forward_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
backward_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
bi_outputs, encoder_state = tf.nn.bidirectional_dynamic_rnn(
forward_cell, backward_cell, encoder_emb_inp,
sequence_length = source_sequence_length, time_major = True)
)
encoder_outputs = tf.concat(bi_outputs,-1)
变量encoder_outputs和encoder_state使用方法与之前章节的编码器使用方法相同。请注意,对于多个双向层,我们需要稍微操作编码器状态,有关详细信息,请参见model.py。
Beam search
贪婪解码
可以提供相当合理的翻译质量,而波束搜索解码
可以进一步提高性能。beam搜索的思想是,在我们翻译的时候,通过保留一小部分优秀的候选词,更好地探索所有可能翻译的搜索空间。束的大小称为束宽;最小光束宽(如尺寸10)通常就足够了。下面是一个如何进行波束搜索的示例:
Data Input Pipeline
在TensorFlow 1.2之前,用户有两个选项可以将数据提供给TensorFlow training 和eval pipeline:
- 在每个训练会话中直接通过feed_dict提供数据。
- 使用tf中的排队机制。训练(例如tf.train.batch)和tf.contrib.train。
- 使用来自更高级别框架(如tf.contrib)的帮助程序。学习或tf.contrib。slim(有效地使用了#2)。
第一种方法对于不熟悉TensorFlow或需要进行外来输入修改(即),这只能在Python中完成。第二和第三种方法更标准,但灵活性稍差; 它们还需要启动多个python线程(队列运行器)。此外,如果使用不当,队列可能导致死锁或不透明的错误消息
。然而,队列比使用feed_dict
效率高得多,并且是单机和分布式训练的标准。
从Tensorflow 1.2开始,有一个新的系统可用于将数据读入tensorflow模型:dataset迭代器,如tf.data模块中所示。数据迭代器是灵活的,易于推理和操作,并通过利用Tensorflow C++ 运行时提供效率和多线程。
可以从批处理数据张量、文件名或包含多个文件名的张量创建数据集。一些例子:
# Training dataset consists of multiple files.
train_dataset = tf.data.TextLineDataset(train_files)
# Evaluation dataset uses a single file, but we may
# point to a different file for each evaluation round.
eval_file = tf.placeholder(tf.string, shape=())
eval_dataset = tf.data.TextLineDataset(eval_file)
# For inference, feed input data to the dataset directly via feed_dict.
infer_batch = tf.placeholder(tf.string, shape=(num_infer_examples,))
infer_dataset = tf.data.Dataset.from_tensor_slices(infer_batch)
所有数据集都可以通过输入处理进行类似的处理。这包括读取和清理数据、bucketing(在训练和评估的情况下)、过滤和批处理。
例如,要将每个句子转换为字符串向量,我们使用数据集映射转换:
dataset = dataset.map(lambda string: tf.string_split([string]).values)
然后,我们可以将每个句子向量
转换为包含向量
及其动态长度
的元组
:
dataset = dataset.map(lambda words: (words, tf.size(words))
最后,我们可以对每个句子执行词汇查找。给定一个查找表对象表,此映射将第一个元组元素从字符串向量
转换为整数向量
。
dataset = dataset.map(lambda words, size:(table.looktup(words),size))
连接两个数据集也很容易。如果两个文件包含彼此的逐行翻译,并且每个文件都读入自己的数据集,则可以通过以下方式创建包含压缩行元组的新数据集:
source_target_data = tf.data.Dateset.zip((source_dataset,target_dataset))
句子的变长批处理非常简单。以下转换将批处理source_target_data
数据集中的batch_size
元素,并分别将源向量
和目标向量
填充到每个批中最长的源向量
和目标向量
的长度。
batched_dataset = source_target_dataset.padded_batch(
batch_size,
padded_shapes=((tf.TensorShape([None]), # source vectors of unknown size
tf.TensorShape([])), # size(source)
(tf.TensorShape([None]), # target vectors of unknown size
tf.TensorShape([]))), # size(target)
padding_values=((src_eos_id, # source vectors padded on the right with src_eos_id
0), # size(source) -- unused
(tgt_eos_id, # target vectors padded on the right with tgt_eos_id
0))) # size(target) -- unused
从该数据集中发出的值将是嵌套元组,其张量的最左维度为size batch_size
。结构如下:
- 迭代器
[0][0]
具有批处理和填充的源语句矩阵。 - 迭代器
[0][1]
具有批处理的源大小向量。 - 迭代器
[1][0]
具有批处理和填充的目标句子矩阵。 - 迭代器
[1][1]
具有批处理的目标大小向量。
最后,将大小相似的源语句批量放在一起也是可能的。有关详细信息和完整的实现,请参阅文件utils/iterator_utils.py。
从数据集中读取数据需要三行代码:创建迭代器、获取其值并初始化它。
batched_iterator = batched_dataset.make_initializable_iterator ()
((source, source_length), (target, target_length)) = batched_iterator.get_next()
# At initialization time
session.run (batched_iterator.initializer feed_dict = {…})
初始化迭代器之后,访问源或目标张量的每个session.run调用都将从底层数据集中请求下一个mini批处理。
附录 Tensorflow使用技巧
How to build the data Pipeline
- 学习如何使用tf.data和最佳实践
- 建立一个有效的管道来加载图像并对其进行预处理
- 为文本构建一个有效的管道,包括如何构建词汇表(build a vocabulary)
Tensorflow 入门手册中一般介绍的是采用 feed_dict
方法,在tf.Session.run()
会话运行或 tf.Tensor.eval()
函数调用时,将数据加载进模型. 然而,还有另一种更加有效和更简单的方式,即,采用 tf.data
API,只需几行代码即可实现高效的数据管道(pipelines).
在 feed_dict
管道中,GPU 存在等待时间,需要等 CPU 提供下一个 batch 的数据. 如图:
Dataset API允许构建一个异步的、高度优化的数据管道
,以防止GPU的数据匮乏。它从磁盘(图像或文本)加载数据,应用优化转换,创建批并将其发送到GPU。以前的数据管道让GPU等待CPU加载数据,导致性能问题。在tf.data管道中,可以异步地拉取下一个batches的数据,以最小化闲置时间,而且还可以并行化数据加载和预处理操作,进一步加速数据管道。
Tensorflwo中变量管理reuse参数使用
Tensorflow中两个用于变量管理的函数
- tf.get_variable() :用于创建和获取变量的值
- tf.variable_scope(): 用于生成上下文管理器,创建命名空间,命名空间可以嵌套
其中, tf.get_variable()既可以创建变量,也可以获取变量。控制创建还是获取的开关来自函数tf.variable.scope()中的参数reuse=True
orreuse = Flase
。
- 设置 reuse = False时,函数 get_variable()表示创建变量
在tf.variable_scope()函数中,设置reuse=False(默认reuse=None)时,在其命名空间”foo”中执行函数get_variable()时,表示创建变量”v”,若在该命名空间中已经有了变量”v”,则在创建时会报错,如下面的例子:with tf.variable_scope("foo", reuse = False): v = tf.get_variable("v",[1],initializer = tf.constant_initialzier(1.0))
import tensorflow as tf
with tf.variable_scope("foo"):
v = tf.get_variable(name="v",shape=[1],initializer=tf.constant_initializer(1.0))
v1 = tf.get_variable(name="v",shape=[1],initializer=tf.constant_initializer(1.0))
ValueError: Variable foo/v already exists, disallowed. Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope? Originally defined at:
- 设置reuse = True 时,函数 get_variable() 表示获取变量
import tensorflow as tf with tf.variable_scope("foo"): v = tf.get_variable(name="v",shape=[1],initializer=tf.constant_initializer(1.0)) with tf.variable_scope("foo",reuse=True): v_test = tf.get_variable("v",shape=[1]) print(v_test==v) True
- 在tf.variable_scope()函数中,设置reuse=True时,在其命名空间”foo”中执行函数get_variable()时,表示获取变量”v”。若在该命名空间中还没有该变量,则在获取时会报错,如下面的例子
import tensorflow as tf with tf.variable_scope("foo",reuse=True): v_test = tf.get_variable("v",shape=[1]) print(v_test) ValueError: Variable foo/v does not exist, or was not created with tf.get_variable(). Did you mean to set reuse=tf.AUTO_REUSE in VarScope?
BibTex
@article{luong17,
author = {Minh{-}Thang Luong and Eugene Brevdo and Rui Zhao},
title = {Neural Machine Translation (seq2seq) Tutorial},
journal = {https://github.com/tensorflow/nmt},
year = {2017},
}