最近在重新搭建seq2seq的模型用于对话系统,发现之前对相关的API用的不是很好,所以又详细看了一下,这个博客对一些重要的API做一个总结。
encoder部分
encoder的建立主要分为两步:建立rnn单元,调用接口计算输出
1. tf.contrib.rnn.LSTMCell
https://tensorflow.google.cn/api_docs/python/tf/nn/rnn_cell/LSTMCell
在不同的tensorflow版本上,这个接口还有不同的名称tf.contrib.rnn.LSTMCell
和 tf.nn.rnn_cell.LSTMCell
,在使用的时候注意一下就好了。
这个接口用于创建一个基本的LSTM单元,参数有:
__init__(
num_units,
use_peepholes=False,
cell_clip=None,
initializer=None,
num_proj=None,
proj_clip=None,
num_unit_shards=None,
num_proj_shards=None,
forget_bias=1.0,
state_is_tuple=True,
activation=None,
reuse=None,
name=None,
dtype=None,
**kwargs
)
这里我选则几个常用的参数作说明。
- num_units:这个参数是最常见的也是必须设置的。但是网上有很多的介绍将这个参数与LSTM中的其他参数给弄混了,初学的时候容易看不清楚。这个参数指的是一个LSTM单元中的隐层单元数量,而不是一层LSTM层有多少个LSTM单元。LSTM单元中进行的基本结构就是简单的神经网络层,num_units这个参数指的是这个神经网络的神经元的数量。只不过lstm为了增加长短期的记忆,在lstm单元中引入了控制门,对输入输出信息进行筛选,这是另一个话题了。另外有一点需要注意的是,这个参数的大小就是lstm单元输出向量的维度,我们可以想,lstm单元中隐层单元的神经元数量就是最后输出的向量维数。
- 说道这里,再多说一点,也是我自己在初学过程中困惑了很久的问题。我们再网上的资料看到对lstm的讲解都会有一张lstm神经网络层的图,其中有多个lstm单元组成,就是下面这张图的右侧部分。但是我们要知道,这个展开的图是按时间顺序展开的,只是为了帮助我们更好的理解lstm的工作原理,但实际上这里面只有一个lstm单元再计算,就像途中的左侧部分,只不过再不同时间节点有不同输入和输出。理解了这一点,就不会产生一下不必要的误解了。num_units这个参数也就不会被理解为lstm层中有多少个lstm单元了。
- initializer:用于初始化lstm单元中的权重矩阵
使用方法如下:
def get_lstm_cell(rnn_size):
lstm_cell = tf.contrib.rnn.LSTMCell(num_units=rnn_size,
initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return lstm_cell
2.tf.contrib.rnn.MultiRNNCell
一般来说,我们不会只建立一层lstm层就结束网络的搭建,通常会使用多层lstm,这时候就要堆叠一下上面通过tf.contrib.rnn.LSTMCell
建立的lstm单元。使用tf.contrib.rnn.MultiRNNCell
这个接口可以很容易完成这件事情。
# 堆叠的 rnn cell
cell = tf.contrib.rnn.MultiRNNCell([get_lstm_cell(rnn_size) for _ in range(num_layers)])
3.1 tf.nn.dynamic_rnn
先来看一下接口的参数:
tf.nn.dynamic_rnn(
cell,
inputs,
sequence_length=None,
initial_state=None,
dtype=None,
parallel_iterations=None,
swap_memory=False,
time_major=False,
scope=None
)
这个接口用来完成lstm的计算得到output和state。
这个接口改进了tf.nn.static_rnn
,相较于前者,动态的rnn允许不同的batch之间可以有不同的输入长度,也是为了节省计算资源,比如做一个文本分类任务,如果数据集中有一个特别长的句子比如长100,其他的句子长度再10左右,如果吧每个句子都padding到100 的长度,那么就增加了很多不必要的计算,动态rnn的解决方式是只把这个特别长的句子所在batch中的句子都padding到100,其他batch中的句子只padding到该batch中最长句子的长度。但是动态rnn要求每个batch中还是要保持一样长度的。但是动态rnn还是不满足,为了进一步优化计算结果,动态rnn还设置了sequence_length这个参数,后面会说。
- cell:我们上面建立好的lstm单元
- inputs:这个参数与下面的
time_major
有关系,如果time_major=False
,那么输入shape应该是[batch_size, max_time, ...]
,否则,输入的shape因该是[max_time, batch_size, ...]
。至于max_time
,这个参数是每一个batch输入的最大长度(为什么是每一个batch后面说),同时,这个参数也用来规定lstm的时间步长,也就是经常见到的time_step
。也就是说,我们用输入来控制时间步长。 - sequence_length:首先这不是一个必须参数,官方文档给他的定义是
“it's more for performance than correctness”
,个人理解,设置这个参数的意义更注重提高计算速度而不是提高准确度。这个参数要求是一个[batch_size]大小的列表,其中的每一个元素是对应的输入的每一个句子的实际长度(padding之前的长度),举个例子,令我门设置sequence_length为[30,13],表示第一个example有效长度为30,第二个example有效长度为13,当我们传入这个参数的时候,对于第二个example,TensorFlow对于13以后的padding就不计算了,其last_states将重复第13步的last_states直至第30步,而outputs中超过13步的结果将会被置零。这其实是类似于mask的一个功能。但是至于为什么不直接去掉padding,我想可能还是有其他的限制吧。
参考:https://blog.csdn.net/u010223750/article/details/71079036
https://www.zhihu.com/question/52200883
然后我门来看一下返回值:
- outputs. outputs是一个tensor
如果time_major True,outputs形状为[max_time, batch_size, cell.output_size ]
(要求rnn输入与rnn输出形状保持一致)
如果time_major False(默认),outputs形状为[ batch_size, max_time, cell.output_size ]
其中,cell.output_size
使我们之前再cell中设定的num_units大小 - state. state是一个tensor。state是最终的状态,也就是序列中最后一个cell输出的状态。一般情况下state的形状为
[batch_size, cell.output_size ]
,但当输入的cell为BasicLSTMCell时,state的形状为[2,batch_size, cell.output_size]
,其中2也对应着LSTM中的cell state和hidden state
使用方法:
encoder_output, encoder_state = tf.nn.dynamic_rnn(cell, encoder_embed_input,
sequence_length=source_sequence_length,
dtype=tf.float32)
参考:https://blog.csdn.net/u010960155/article/details/81707498
3.1 tf.nn.bidirectional_dynamic_rnn
有时候我们还会选择使用双向的RNN进行encode,增加序列信息。这里可以调用这个api。与普通的dynamic_rnn略有不同的是,双向的接口需要输入前向的cell和后向的cell,但实际中通常这两个cell是同样结构的。另一个区别是输出的不同,我们来看一下。
tf.nn.bidirectional_dynamic_rnn(
cell_fw, # 前向cell
cell_bw, # 后向cell
inputs,
sequence_length=None,
initial_state_fw=None,
initial_state_bw=None,
dtype=None,
parallel_iterations=None,
swap_memory=False,
time_major=False,
scope=None
)
输出是一个tuple:(outputs, output_states)
因为是双向的,所以outputs
应该是两个方向的输出,因此这个outputs
是(output_fw, output_bw)
,也是一个tuple。两个output
的shape是一样的,如果time_major
False,则shape=[batch_size, max_time, cell_fw.output_size]
,如果time_major
True,shape=[max_time, batch_size, cell_fw.output_size]
.通常我们会吧两个output进行concatenate,由输出的shape可以看出,我们应该在第3个维度进行concatenate,也就是这样:
encoder_outputs = tf.concat( (encoder_fw_outputs, encoder_bw_outputs), 2)
对于output_states:也是一个tuple,(output_state_fw, output_state_bw)
containing the forward and the backward final states of bidirectional rnn。state是最终的状态,也就是序列中最后一个cell输出的状态。对于多层的情况,我们可以这样处理:
encoder_state = []
for i in range(self.depth): # depth是rnn的层数
encoder_state.append(encoder_fw_state[i])
encoder_state.append(encoder_bw_state[i])
encoder_state = tuple(encoder_state)
decoder部分
decoder 部分要比encoder复杂一点,API更多,我们一点点来说。
在执行具体的解码过程之前,decoder需要知道采样的需求是怎么样的,我们知道seq2seq解码器的输入可以分为两种情况:一个是将上一时刻的输出作为下一时刻的输入,这时候对应的是decoder的inference过程;另一个是不使用上一时刻的输出,而是将正确的target直接作为输入放入decoder中,这对应的是train过程。也就是说decoder需要知道自己应该是怎样去采样选择输入。
tensorflow提供了这中接口来帮助实现控制输入采样,这些接口继承自抽象类Helper
,我这里主要介绍
tf.contrib.seq2seq.GreedyEmbeddingHelper
和tf.contrib.seq2seq.TrainingHelper
。前者用于inference过程,后者用于training过程。
设定好helper后,tensorflow要求我们设置一个decoder作为最终解码接口的参数,这个decoder我理解像是一个封装器,将之前设定好的cell,helper,以及encoder传入的隐层向量做封装,一起传入最终的解码接口。
1. tf.contrib.rnn.LSTMCell
这个API与encoder部分是一样的,建立一个LSTM单元。不多说了,看上面的介绍。
def get_decoder_cell(rnn_size):
decoder_cell = tf.contrib.rnn.LSTMCell(rnn_size,
initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return decoder_cell
2.tf.contrib.rnn.MultiRNNCell
堆叠RNN,与encoder一样,看上面。
cell = tf.contrib.rnn.MultiRNNCell([get_decoder_cell(rnn_size) for _ in range(num_layers)])
3.1 tf.contrib.seq2seq.TrainingHelper
这个接口就是Decoder端用来训练的函数。这个函数不会把t-1阶段的输出作为t阶段的输入,而是把target中的真实值直接输入给RNN。用于training过程
看一下他的参数:没有什么特别的,对他们的说明我直接写在下面
__init__(
inputs, // 输入
sequence_length, //本文encoder部分tf.nn.dynamic_rnn已经进行了介绍
time_major=False, // 规定以时间为主还是以batch为主,一般默认false
name=None
)
再来看一下他的返回。
官方文档里是这么说的:Returned sample_ids are the argmax of the RNN output logits.
就是说,使用这个helper的decoder最终结果是解码输出logits的argmax
使用:
training_helper = tf.contrib.seq2seq.TrainingHelper(inputs=decoder_embed_input,
sequence_length=target_sequence_length,
time_major=False)
3.2 tf.contrib.seq2seq.GreedyEmbeddingHelper
GreedyEmbeddingHelper和TrainingHelper的区别在于它会把t-1下的输出进行embedding后再输入给RNN。用于inference过程
__init__(
embedding,
start_tokens,
end_token
)
- embedding:embedding矩阵。在t-1时刻得到的输出sample_id在t时刻通过对这个矩阵进行embedding_lookup操作得到新的输入值。另外,这个helper的到的sample_id也是通过argmax得到的。
- start_tokens,end_token分别是输入的开始标志位和结束标志位。解码器端需要知道句子的起始与结束是以什么为标志的。不过要注意的是
start_tokens
要求的形状是[batch_size]的,而end_token
只是一个标量。这是因为再decode过程,不同于后面t-1的输出会传递到t时刻的输入,第一个输出之前是没有decoder产生的输出的,因此这里除了需要一个encoder传过来的隐层向量之外,还要有一个人为放入的开始符作为输入,因为每次decode都需要输入这个token,因此一共需要batch_size个。而end_token
只是为了标志结束,所以只要一个就行了。
使用:
predicting_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(decoder_embeddings,
start_tokens,
target_letter_to_int['<EOS>'])
4. tf.contrib.seq2seq.BasicDecoder
建立完helper之后,我们需要建立decoder,这里使用的是这个接口。
__init__(
cell, //前面接口建立好的额cell
helper, // 前面接口建立好的helper
initial_state, //初始化state,指的是encoder最后传到decoder的隐层向量
output_layer=None // 可选,在得到rnn输出前,加一层神经网络,一般使用Dense
)
使用:(以training为例)
training_decoder = tf.contrib.seq2seq.BasicDecoder(cell=cell,
helper=training_helper,
initial_state=encoder_state,
output_layer=output_layer)
5. tf.contrib.seq2seq.dynamic_decode
调用这个接口进行正式的解码阶段。
tf.contrib.seq2seq.dynamic_decode(
decoder,
output_time_major=False,
impute_finished=False,
maximum_iterations=None,
parallel_iterations=32,
swap_memory=False,
scope=None
)
其他几个参数部不说了,前面说过。这里说一下maximum_iterations
这个参数。这个主要是控制最后输出回复句子的长度的,一般情况下,产生的回复句子再遇到
停止标志位时停止,但是如果产生的回复过长,就需要设置这个参数,使得产生的回复长度不超过这个设置的值。
返回值有三个:(final_outputs, final_state, final_sequence_lengths)
一般的话我们只取第一个,就是生成的回复句子。
使用:
training_decoder_output, _, _ = tf.contrib.seq2seq.dynamic_decode(training_decoder,
impute_finished=True,
maximum_iterations=max_target_sequence_length)
好了,到这里我们就把seq2seq模型中建立encoder和decoder需要用到的最主要的API介绍完了。后面如果再遇到比较有用的接口,我会补充上来。