动手搭建seq2seq-----API介绍

最近在重新搭建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.LSTMCelltf.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.GreedyEmbeddingHelpertf.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只是为了标志结束,所以只要一个就行了。
    这里在说一下这两个token的问题。在数据处理部分,我们会把数据处理成"一句话+end_token"的形式,注意这是对training和inference过程一样的原始形式。在training阶段,作为decoder的输入,为了给每一句话都加一个开始标志符,所以会要求有batch_size个开始标志符;同样在inference阶段,为了decode开始,会要求有batch_size个开始标志符作为解码起点。而结束标志符在每一句话中已经存在了,我们只要告诉解码器哪一个标志是结束的符号就行了,所以只要一个end_token,而不是batch_size个。

使用:

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这个参数。这个主要是控制最后输出回复句子的长度的,一般情况下,产生的回复句子再遇到 &lt; E O S &gt; &lt;EOS&gt; <EOS>停止标志位时停止,但是如果产生的回复过长,就需要设置这个参数,使得产生的回复长度不超过这个设置的值。

返回值有三个:(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介绍完了。后面如果再遇到比较有用的接口,我会补充上来。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值