官方教程使用seq2seq模型实现了英语-法语的翻译系统。经典的sequence-to-sequence模型由两个RNN网络构成,一个被称为“encoder”,另一个则称为“decoder”,前者负责把序列编码成一个固定长度的向量,这个向量作为输入传给后者,输出可变长度的向量,它的基本网络结构如下,
其中每一个小圆圈代表一个cell,比如GRUcell、LSTMcell、multi-layer-GRUcell、multi-layer-GRUcell等。尽管“encoder”或者“decoder”内部存在权值共享,但encoder和decoder之间一般具有不同的一套参数。
考虑到encoder将输入seq编码成一个向量,基本的decoder与encoder的交互仅在decoder初始的输入上,这样对于decoder而言,只能看到源信息的一个总体概要,会限制encoder-decoder架构的性能。
基于这个缺点进行了改进, 在翻译阶段, 准备生成每个新的词的时候, 注意力机制可以将注意力集中在输入的某个或某几个词上,重点关注这几个词, 使得翻译更精准。新的模型架构如下图所示。
这个模型依照 http://arxiv.org/abs/1412.7449 实现。
机器学习模型的定义过程,一般包括输入变量定义、输入信息的forward propagation和误差信息的backward propagation三个部分,这三个部分在这个程序文件中都得到了很好的体现,下面我们结合代码分别进行介绍。
先来看一下seq2seq函数:
def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):
return tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
encoder_inputs,# tensor of input seq
decoder_inputs,# tensor of decoder seq
cell, #自定义的cell,可以是GRU/LSTM, 设置multilayer等
num_encoder_symbols=source_vocab_size, # 英语词典大小 40000
num_decoder_symbols=target_vocab_size, # 法语词典大小 40000
embedding_size=size, # embedding 维度
output_projection=output_projection, # 不设定的话输出维数可能很大(取决于词表大小),设定的话投影到一个低维向量
feed_previous=do_decode, # false: 训练 ;True: 测试
dtype=dtype)
# Feeds for inputs.
self.encoder_inputs = []
self.decoder_inputs = []
self.target_weights = []
for i in xrange(buckets[-1][0]): # Last bucket is the biggest one.
self.encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
#encoder_inputs 这个列表对象中的每一个元素表示一个占位符,其名字分别为encoder0, encoder1,…,encoder39,encoder{i}的几何意义是编码器在时刻i的输入。
for i in xrange(buckets[-1][1] + 1):
self.decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
self.target_weights.append(tf.placeholder(dtype, shape=[None],
name="weight{0}".format(i)))
#target_weights 是一个与 decoder_outputs 大小一样的 0-1 矩阵。该矩阵将目标序列长度以外的其他位置填充为标量值 0。
# Our targets are decoder inputs shifted by one.
targets = [self.decoder_inputs[i + 1]
for i in xrange(len(self.decoder_inputs) - 1)]
# 跟language model类似,targets变量是decoder inputs平移一个单位的结果,
# 区别在于seq2seq_f函数的参数feed previous是True还是false
if forward_only: # 测试阶段
self.outputs, self.losses = tf.contrib.legacy_seq2seq.model_with_buckets(#??
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets, lambda x, y: seq2seq_f(
x, y, True),
softmax_loss_function=softmax_loss_function)
# If we use output projection, we need to project outputs for
# decoding.
if output_projection is not None:
for b in xrange(len(buckets)):
self.outputs[b] = [
tf.matmul(output, output_projection[
0]) + output_projection[1]
for output in self.outputs[b]
]
else:#训练阶段
self.outputs, self.losses = tf.contrib.legacy_seq2seq.model_with_buckets(
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets,
lambda x, y: seq2seq_f(x, y, False),
softmax_loss_function=softmax_loss_function)
从代码中可以看到,输入信息的forward popagation分成了两种情况,这是因为整个sequence to sequence模型在训练阶段和测试阶段信息的流向是不一样的,这一点可以从seq2seqf函数的do_decode参数值体现出来,而do_decoder取值对应的就是tf.nn.seq2seq.embedding_attention_seq2seq函数中的feed_previous参数,forward_only为True也即feed_previous参数为True时进行模型测试,为False时进行模型训练。
params = tf.trainable_variables()
if not forward_only:# 只有训练阶段才需要计算梯度和参数更新
self.gradient_norms = []
self.updates = []
opt = tf.train.GradientDescentOptimizer(self.learning_rate) # 用梯度下降法优化
for b in xrange(len(buckets)):
gradients = tf.gradients(self.losses[b], params) #计算损失函数关于参数的梯度
clipped_gradients, norm = tf.clip_by_global_norm(gradients,
max_gradient_norm)# clip gradients 防止梯度爆炸
self.gradient_norms.append(norm)
self.updates.append(opt.apply_gradients(
zip(clipped_gradients, params), global_step=self.global_step))#更新参数
这一段代码主要用于计算损失函数关于参数的梯度。因为只有训练阶段才需要计算梯度和参数更新,所以这里有个if判断语句。并且,由于当前定义除了length(buckets)个graph,故返回值self.updates是一个列表对象,尺寸为length(buckets),列表中第i个元素表示graph{i}的梯度更新操作。
训练 RNN 的一个重要步骤是梯度截断(gradient clipping)。这里,我们使用全局范数进行截断操作。最大值 max_gradient_norm
通常设置为 5 或 1。
# Input feed: encoder inputs, decoder inputs, target_weights, as
# provided.
input_feed = {}
for l in xrange(encoder_size):
input_feed[self.encoder_inputs[l].name] = encoder_inputs[l]
for l in xrange(decoder_size):
input_feed[self.decoder_inputs[l].name] = decoder_inputs[l]
input_feed[self.target_weights[l].name] = target_weights[l]
# Since our targets are decoder inputs shifted by one, we need one
# more.
last_target = self.decoder_inputs[decoder_size].name
input_feed[last_target] = np.zeros([self.batch_size], dtype=np.int32)
# Output feed: depends on whether we do a backward step or not.
if not forward_only:
output_feed = [self.updates[bucket_id], # Update Op that does SGD.
self.gradient_norms[bucket_id], # Gradient norm.
self.losses[bucket_id]] # Loss for this batch.
else:
output_feed = [self.losses[bucket_id]] # Loss for this batch.
for l in xrange(decoder_size): # Output logits.
output_feed.append(self.outputs[bucket_id][l])
outputs = session.run(output_feed, input_feed)
if not forward_only:
# Gradient norm, loss, no outputs.
return outputs[1], outputs[2], None
else:
# No gradient norm, loss, outputs.
return None, outputs[0], outputs[1:]
模型已经定义完成了,这里便开始进行模型训练了。上面的两个for循环用于为之前定义的输入占位符赋予具体的数值,这些具体的数值源自于get_batch函数的返回值。当session.run函数开始执行时,当前session会对第bucket_id个graph进行参数更新操作。
Reference:
1. https://www.tensorflow.org/tutorials/seq2seq
2. http://www.2cto.com/kf/201612/575911.html
3. http://www.jianshu.com/p/58ef2b990d3f