频道栏目
首页 > 程序开发 > 综合编程 > 其他综合 > 正文
TensorFlow中Sequence-to-Sequence样例代码详解
2016-12-13 09:25:28         来源:为之则易,不为则难  
收藏   我要投稿

在NLP领域,sequence to sequence模型有很多应用,比如机器翻译、自动应答机器人等。在看懂了相关的论文后,我开始研读TensorFlow提供的源代码,刚开始看时感觉非常晦涩,现在基本都弄懂了,我在这里主要介绍Sequence-to-Sequence Models用到的理论,然后对源代码进行详解,也算是对自己这两周的学习进行一下总结,如果也能够对您有所帮助的话,那就再好不过了~

sequence-to-sequence模型

在NLP中最为常见的模型是language model,它的研究对象是单一序列,而本文中的sequence to sequence模型同时研究两个序列。经典的sequence-to-sequence模型由两个RNN网络构成,一个被称为“encoder”,另一个则称为“decoder”,前者负责把variable-length序列编码成fixed-length向量表示,后者负责把fixed_length向量表示解码成variable-length输出,它的基本网络结构如下,

\

其中每一个小圆圈代表一个cell,比如GRUcell、LSTMcell、multi-layer-GRUcell、multi-layer-GRUcell等。这里比较直观的解释就是,encoder的最终隐状态c包含了输入序列的所有信息,因此可以使用c进行解码输出。尽管“encoder”或者“decoder”内部存在权值共享,但encoder和decoder之间一般具有不同的一套参数。在训练sequence-to-sequence模型时,类似于有监督学习模型,最大化目标函数θ?=argmaxθ∑n=N∑t=1TnlogP(ynt|yn  其中p(yt|y1,..,yt?1,c)=g(yt?1,st,c)=1Zexp(wTt?(yt?1,zt,ct)+bt)   其中wt称作输出投影,bt称作输出偏置,标准化常数计算式为Z=∑k:yk∈Vexp(wTk?(yt?1,zt,ct)+bk)   Dzmitry Bahdanau大牛考虑到fixed-length向量表示会限制encoder-decoder架构的性能,于是进行了改进,使得模型在输出单一word时,能够自动查找到有贡献的输入sub-sequence,新的模型架构如下图所示,

\

这里的编码器为双向RNN架构,定义条件概率p(yi|y1,..,yi?1,x)=g(yi?1,si,ci),其中隐状态si计算公式为si=f(si?1,yi?1,ci),上下文向量计算公式为ci=∑j=1Txαijhj   其中,权值参数αij=exp(eij)∑Txk=1exp(eik)   eij=activation(si?1,hj)是一个“alignment model”,用于表征输入序列的第j个位置和输出序列的第i个位置的匹配程度,hj表示双向RNN隐状态的合并,即hj=[hLj;hRj],根据RNN序列的特点,hj中包含了更多的邻域窗序列内的信息,那么显然αij是对eij标准化后的形式,ci的计算公式的几何意义就是,对输入序列中所有位置的信息进行加权求和,从而达到了在输出序列的任一time step,都能够从输入序列中动态获取最为相关的子序列信息的效果,在作者文章中,这种效果被称作为”attention mechanism”。

TensorFlow中seq2seq库函数

尽算上述算法看起来比较复杂,但TensorFlow已经把它们封装成了可以直接调用的函数,官方教程已经对这些库函数做了大体介绍,但我感觉讲的还是不够透彻,故在这里重新叙述一下(还有一些其他的函数,但考虑到它们的接口参数都是相似的,就不做太多介绍了~)。

(1)outputs, states = basic_rnn_seq2seq(encoder_inputs, decoder_inputs, cell)

输入参数 :

encoder_inputs: 它是一个二维tensor构成的列表对象,其中每一个二维tensor代表某一时刻的输入,其尺寸为[batch_size x input_size],这里的batch_size具体指某一时刻输入的单词个数,input_size指encoder的长度;

decoder_inputs: 它是一个二维tensor构成的列表对象,其中每一个二维tensor代表某一时刻的输入,其尺寸为[batch_size x output_size],这里的            batch_size具体指某一时刻输入的单词个数,output_size指decoder的长度;

cell: 它是一个rnn_cell.RNNCell或者multi-layer-RNNCell对象,其中定义了cell函数和hidden units的个数;

输出参数 :

outputs: 它是一个二维tensor构成的列表对象,其中每一个二维tensor代表某一时刻输出,其尺寸为[batch_size x output_size],这里的

batch_size具体指某一时刻输入的单词个数,output_size指decoder的长度;

state: 它是一个二维tensor,表示每一个decoder cell在最后的time-step的状态,其尺寸为[batch_size x cell.state_size],这里的

cell.state_size可以表示一个或者多个子cell的状态,视输入参数cell而定;

(2)outputs, states = embedding_attention_seq2seq(encoder_inputs, decoder_inputs, …)

输入参数 :

encoder_inputs: 与上面的基本函数,它是一个一维tensor构成的列表对象,其中每一个一维tensor的尺寸为[batch_size],代表某一时刻的输入;

decoder_inputs: 与encoder_inputs的解释类似;

cell: 它是一个rnn_cell.RNNCell或者multi-layer-RNNCell对象,其中定义了cell函数和hidden units的个数;

num_encoder_symbols: 具体指输入词库的大小,也即输入单词one-hot表示后的向量长度;

num_decoder_symbols: 具体指输出词库的大小;

embedding_size: 词库中每一个单词“嵌套”后向量的长度;

num_heads: 默认为1(具体的意义我还没弄明白);

output_projection: 为None或者 (W, B) 元组对象,其中W的尺寸为[output_size x num_decoder_symbols],B的尺寸为 [num_decoder_symbols],

显然,解码器每一时刻的输出仅共享偏置参数B,权值参数不共享;

feed_previous: 为True时用于模型测试阶段,基于贪婪算法生成输出序列,为False时用于训练模型参数;

initial_state_attention: 设置初始attention的状态,也即上图中αij的取值;

输出参数 :

outputs: 它是一个二维tensor构成的列表对象,其中每一个二维tensor代表某一时刻输出,其尺寸为[batch_size x num_decoder_symbols];

state: 它是一个二维tensor,表示每一个decoder cell在最后的time-step的状态,其尺寸为[batch_size x cell.state_size],这里的

cell.state_size可以表示一个或者多个子cell的状态,视输入参数cell而定;

sequence-to-sequence模型实现中的技巧

做理论和做工程还是有区别的,在对sequence-to-sequence模型进行实现时,Google的工程师们使用了sample softmax策略和bucketing策略,下面我们分别对其进行讲解。

sample softmax策略

解码器RNN序列在每一时刻的输出层为softmax分类器,在对上面的目标函数求梯度时,表达式中会出现对整个target vocabulary的求和项,显然这样做的计算量是非常大的,于是大牛们想到了用target vocabulary中的一个子集,来近似对整个词库的求和,子集中word的选取采用的是均匀采样的策略,从而降低了每次梯度更新步骤的计算复杂度,在tensorflow中可以采用tf.nn.sampled_softmax_loss函数。

bucketing策略

bucketing策略可以用于处理不同长度的训练样例,如果我们把训练样例的输入和输出长度固定,那么在训练整个网络的时候,必然会引入很多的PAD辅助单词,而这些单词却包含了无用信息;如果不引入PAD辅助单词,每一个样例作为一个graph的话,因为每一个样例的输入尺寸和输出尺寸一般是不一样的,所以每一个样例定义出的graph也是不一样的,因此就会定义出非常多的graph,尽管这些graph有相似的sub-graph,但是在训练的时候不能够进行并行计算,势必会大大降低模型的训练效率。所以,一个折中的方法就是,可以设置若干个buckets,每个bucket指定一个输入和输出长度,比如教程给的例子buckets = [(5, 10), (10, 15), (20, 25), (40, 50)],这样的话,经过bucketing策略处理后,会把所有的训练样例分成4份,其中每一份的输入序列和输出序列的长度分别相同。为了更好地理解源代码中bucketing的使用,我们这里补充讲述一下。TensorFlow是先定义出Graph,模型的训练过程就是对Graph中参数进行更新。对于本例中的Graph而言,Graph中encoder部分的长度为40,decoder部分的长度为50,在每次采用梯度下降法更新模型参数时,会随机地从4个buckets中选择一个,并从中随机选取batch个训练样例,此时相当于对当前Graph中的参数进行优化,但考虑到4个graph之间存在“weight share”,因此每个batch中样例的长度不一样也是可以的。

Github源代码解析

整个工程主要使用了四个源文件,seq2seq.py文件是一个用于创建sequence-to-sequence模型的库,data_utils.py中包含了对原始数据进行预处理的一些操作,seq2seq_model.py用于定义machine translation模型,translate.py用于训练和测试所定义的翻译模型。因为源代码较长,下面仅针对每个.py文件,对理解起来可能有困难的代码块进行解析。

seq2seq.py文件

这个文件中比较重要的两个库函数basic_rnn_seq2seq和embedding_attention_seq2seq已经在上一部分作了介绍,这里主要介绍其它的几个功能函数。

(1)sequence_loss_by_example(logits, targets, weights)

这个函数用于计算所有examples的加权交叉熵损失,logits参数是一个2D Tensor构成的列表对象,每一个2D Tensor的尺寸为[batch_size x num_decoder_symbols],函数的返回值是一个1D float类型的Tensor,尺寸为batch_size,其中的每一个元素代表当前输入序列example的交叉熵。另外,还有一个与之类似的函数sequence_loss,它对sequence_loss_by_example函数返回的结果进行了一个tf.reduce_sum运算,因此返回的是一个标称型float Tensor。(2


# 函数seq2seq有两个返回值,因为tf.nn.seq2seq.embedding_attention_seq2seq函数有两个返回值


这个函数创建了一个支持bucketing策略的sequence-to-sequence模型,它仍然属于Graph的定义阶段。具体来说,这段程序定义了length(buckets)个graph,每个graph的输入为总模型的输入“占位符”的一部分,但这些graphs共享模型参数,函数的返回值outputs和losses均为列表对象,尺寸为[length(buckets)],其中每一个元素为当前graph的bucket_outputs和bucket_loss。

data_utils.py文件

(1)create_vocabulary(vocabulary_path, data_path, max_vocabulary_size)

这个函数用于根据输入文件创建词库,在这里data_path参数表示输入源文件的路径,vocabulary_path表示输出文件的路径,vocabulary_path文件中每一行代表一个单词,且按照其在data_path中的出现频数从大到小排列,比如第1行为r”_EOS”,第2行为r”_UNK”,第3行为r’I’,第4行为r”have”,第5行为r’dream’,……

(2)def data_to_token_ids(data_path, target_path, vocabulary_path)

这个函数用于把字符串为元素的数据文件转换为以int索引为元素的文件,在这里data_path表示输入源数据文件的路径,target_path表示输出索引数据文件的路径,vocabulary_path表示词库文件的路径。整个函数把数据文件中的每一行转换为在词库文件中的索引值,两单词的索引值之间用空格隔开,比如返回值文件的第一行为’1 123 235’,第二行为‘3 1 234 554 879 355’,……

seq2seq_model.py文件

机器学习模型的定义过程,一般包括输入变量定义、输入信息的forward propagation和误差信息的backward propagation三个部分,这三个部分在这个程序文件中都得到了很好的体现,下面我们结合代码分别进行介绍。

(1)输入变量的定义


与前面的几个样例不同,这里输入数据采用的是最常见的“占位符”格式,以self.encoder_inputs为例,这个列表对象中的每一个元素表示一个占位符,其名字分别为encoder0, encoder1,…,encoder39,encoder{i}的几何意义是编码器在时刻i的输入。这里需要注意的是,在训练阶段执行sess.run()函数时会再次用到这些变量名字。另外,跟language model类似,targets变量是decoder inputs平移一个单位的结果,读者可以结合当前模型的损失函数进行理解。

(2)输入信息的forward propagation


从代码中可以看到,输入信息的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时进行模型训练。这里还应用到了一个很重要的函数tf.nn.seq2seq.model_with_buckets,我么在seq2seq文件中对其进行讲解。

(3)误差信息的backward propagation

# 返回所有bucket子graph的梯度和SGD更新操作,这些子graph共享输入占位符变量encoder_inputs,区别在于,

# 对于每一个bucket子图,其输入为该子图对应的长度。


这一段代码主要用于计算损失函数关于参数的梯度。因为只有训练阶段才需要计算梯度和参数更新,所以这里有个if判断语句。并且,由于当前定义除了length(buckets)个graph,故返回值self.updates是一个列表对象,尺寸为length(buckets),列表中第i个元素表示graph{i}的梯度更新操作。


模型已经定义完成了,这里便开始进行模型训练了。上面的两个for循环用于为之前定义的输入占位符赋予具体的数值,这些具体的数值源自于get_batch函数的返回值。当session.run函数开始执行时,当前session会对第bucket_id个graph进行参数更新操作。

参考资料:https://www.tensorflow.org/versions/r0.12/tutorials/seq2seq/index.html

点击复制链接 与好友分享!回本站首页
上一篇:变量的命名规则小结
下一篇:faster rcnn源码理解
相关文章
图文推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站