Tensorflow 中 RNN 的相关使用方法说明


目前 Tensorflow中关于rnn的定义有多种,这里仅对目前遇到的版本做一个简单的总结与说明,也只是对自己目前遇到的几种情况简单地总结一下。

1. basic rnn

因为 lstm 和 gru 都是继承自基本的 rnn cell 类,所以首先对基类中需要注意的几个地方进行说明。

tf.nn.rnn_cell.BasicRNNCell

基类的构造方法定义如下:

__init__(
    num_units, # The number of units in the RNN cell
    activation=None,
    reuse=None,
    name=None,
    dtype=None,
    **kwargs
)

其属性state_size就是指 rnn cell 中的 num_units。

1.1 call method

rnn cell 基类具有成员函数,其功能是调用执行一次时间步的计算,参数inputs[batch_size,input_size] 的张量,参数state是指前一时刻的隐藏状态输出。

__call__(
    inputs,
    state,
    scope=None,
    *args,
    **kwargs
)

注意该函数有两个返回值,习惯上来讲通常写作outputh,但根据其源代码可发现,这两个参数实际上是相同的,都是指当前时刻 rnn cell 的隐藏输出,函数返回值设计成这样的形式不过是为了方便输出时选择output参数,通常是在此基础上添加softmax;而沿时间步传播时选择h参数。

1.2 zero_state method

该成员函数的功能是用来初始化隐藏状态,通常初始时间步的隐藏状态为全零向量,可以调用该成员函数。

zero_state(
    batch_size,
    dtype
)

注意其输入参数仅为 batch_size 和 dtype,因为事先创建的 rnn cell 的state_size已知,所以在创建全零的初始时间步隐藏状态时就只需要这两个参数即可,返回的是** [batch_size,state_size]** 的全零张量,这一点在下面代码示例中也有所体现。

1.3 program examples

>>> import tensorflow as tf
>>> import numpy as np
>>> cell = tf.nn.rnn_cell.BasicRNNCell(num_units=128)
>>> print(cell.state_size) # state_size = num_units
128
>>> inputs = tf.placeholder(tf.float32, shape=[32, 100])
>>> h0 = cell.zero_state(32, tf.float32)
>>> print(h0)
Tensor("BasicRNNCellZeroState/zeros:0", shape=(32, 128), dtype=float32)
>>> output, h1 = cell.__call__(inputs, h0)
>>> print(output)
Tensor("basic_rnn_cell/Tanh:0", shape=(32, 128), dtype=float32)
>>> print(h1)
Tensor("basic_rnn_cell/Tanh:0", shape=(32, 128), dtype=float32)
>>> print(output == h1)
True
>>> print(cell.output_size)
128

2. basic lstm

tf.nn.rnn_cell.BasicLSTMCell

最简单的lstm实现,没有peep-hole的功能。其构造函数定义如下:

__init__(
    num_units,
    forget_bias=1.0,
    state_is_tuple=True,
    activation=None,
    reuse=None,
    name=None,
    dtype=None,
    **kwargs
)

一些基本的属性和成员函数与 rnn cell 相同,因为继承自 rnn cell 基类。但是因为__call__函数的性质,因此可以直接通过语句output, h1 = cell(inputs,h0)来调用,没有必要显式调用__call__
另外,需要注意构造函数中的参数state_is_tuple=True,表示__call__函数返回的第二个参数是以LSTMTuple的形式包括了 lstm cell 当前时刻的 c t \mathbf c_t ct h t \mathbf h_t ht

2.1 program examples

>>> cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
>>> inputs = tf.placeholder(tf.float32, [32, 100])
>>> h0 = cell.zero_state(32, tf.float32)
>>> output, h1 = cell(inputs, h0)
>>> print(output == h1.h)
True
>>> print(h1.h)
Tensor("basic_lstm_cell/Mul_2:0", shape=(32, 128), dtype=float32)
>>> print(h1.c)
Tensor("basic_lstm_cell/Add_1:0", shape=(32, 128), dtype=float32)

注意__call__没有显式调用,返回参数有两个,这里的output返回值仍然与 rnn cell 中的相同,其实就是 lstm cell 的隐藏输出,只不过为了方便输出进sosftmax而已;而因为 lstm cell 与基本的 rnn cell 相比多了记忆状态输出 c t \mathbf c_t ct,因此需要把这个参数与隐藏输出 h t \mathbf h_t ht一起由h1返回,可通过语句h1.hh1.c来查看隐藏输出和记忆输出。而且上述代码显示,返回值output其实就是h1.h,只不过是为了沿时间步传播方便而已。

3. lstm

tf.nn.rnn_cell.LSTMCell

实现了peep-hole的功能,并且允许对cell的输出进行修剪,其构造函数为:

__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:The number of units in the LSTM cell
  • state_is_tuple:If True, accepted and returned states are 2-tuples of the c_state and m_state
  • reuse:(optional) Python boolean describing whether to reuse variables in an existing scope

这些参数说明来自官方文档,其中m_state就是指 h t \mathbf h_t ht隐藏输出。

4. multi layers

当然很多时候lstm cell 只有一层并不能够满足需要,因此需要堆叠式(stacked1)的循环网络。这个时候就要使用MultiRNNCell,其构造函数为:

__init__(
    cells,
    state_is_tuple=True
)

输入参数cells的说明为 list of RNNCells that will be composed in this order,然后返回的就是按照输入列表定义的各种 cell 的堆叠结构。

4.1 program example 1

# example 1
num_units = [128, 64]
cells = [BasicLSTMCell(num_units=n) for n in num_units]
stacked_rnn_cell = MultiRNNCell(cells)

从这个代码示例可以看出,可以将不同规模的 rnn cell 堆叠在一起。

4.2 program example 2

# example 2
>>> def get_cell():
	return tf.nn.rnn_cell.BasicLSTMCell(num_units=128)

>>> cell = tf.nn.rnn_cell.MultiRNNCell([get_cell() for _ in range(3)])
>>> print(cell.state_size)
(LSTMStateTuple(c=128, h=128), LSTMStateTuple(c=128, h=128), LSTMStateTuple(c=128, h=128))

>>> inputs = tf.placeholder(tf.float32, [32, 100])
>>> h0 = cell.zero_state(32, tf.float32)
>>> print(h0)
(LSTMStateTuple(c=<tf.Tensor 'MultiRNNCellZeroState/BasicLSTMCellZeroState/zeros:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'MultiRNNCellZeroState/BasicLSTMCellZeroState/zeros_1:0' shape=(32, 128) dtype=float32>), LSTMStateTuple(c=<tf.Tensor 'MultiRNNCellZeroState/BasicLSTMCellZeroState_1/zeros:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'MultiRNNCellZeroState/BasicLSTMCellZeroState_1/zeros_1:0' shape=(32, 128) dtype=float32>), LSTMStateTuple(c=<tf.Tensor 'MultiRNNCellZeroState/BasicLSTMCellZeroState_2/zeros:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'MultiRNNCellZeroState/BasicLSTMCellZeroState_2/zeros_1:0' shape=(32, 128) dtype=float32>))

>>> output, h1 = cell(inputs, h0)
>>> print(output)
Tensor("multi_rnn_cell/cell_2/basic_lstm_cell/Mul_2:0", shape=(32, 128), dtype=float32)
>>> print(h1)
(LSTMStateTuple(c=<tf.Tensor 'multi_rnn_cell/cell_0/basic_lstm_cell/Add_1:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'multi_rnn_cell/cell_0/basic_lstm_cell/Mul_2:0' shape=(32, 128) dtype=float32>), LSTMStateTuple(c=<tf.Tensor 'multi_rnn_cell/cell_1/basic_lstm_cell/Add_1:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'multi_rnn_cell/cell_1/basic_lstm_cell/Mul_2:0' shape=(32, 128) dtype=float32>), LSTMStateTuple(c=<tf.Tensor 'multi_rnn_cell/cell_2/basic_lstm_cell/Add_1:0' shape=(32, 128) dtype=float32>, h=<tf.Tensor 'multi_rnn_cell/cell_2/basic_lstm_cell/Mul_2:0' shape=(32, 128) dtype=float32>))

>>> print(type(h1))
<class 'tuple'>

这个代码示例定义了堆叠三层的网络模型,可以看到属性cell.state_size这时就要输出三层的 c t l \mathbf c_t^l ctl h t l \mathbf h_t^l htlnum_units。然后定义的初始时间步全零状态和输出的隐藏状态都必须保存全部三层的 c t \mathbf c_t ct h t \mathbf h_t ht信息。但调用__call__(隐式调用)的返回输出参数中,output就只是最高层的隐藏输出,这里就是指 h t 3 \mathbf h_t^3 ht3,因为需要输出是就只需要将该信息输入进softmax;而h1就不再像单层模型结构那样能够通过h1.ch1.h来输出相应的记忆状态或隐藏状态了,但仍然是保存了全部三层记忆状态和隐藏状态信息的tuple,用来沿时间步传播。

4.3 program example 3

# example 3
>>> cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicRNNCell(num_units) for num_units in [32, 64]])
>>> inputs = tf.placeholder(tf.float32, [32, 100])
>>> h0 = cell.zero_state(32, tf.float32)
>>> output, h1 = cell(inputs, h0)
>>> print(output)
Tensor("multi_rnn_cell_1/cell_1/basic_rnn_cell/Tanh:0", shape=(32, 64), dtype=float32)

上述代码示例表明,multi-layer 的网络结构创建顺序,可见MultiRNNCell输入的 cell 列表中按先后顺序依次从低层向高层搭建 multilayer 的层次结构。

5. multi time steps

上述示例仅在一个时间步下对 rnn cell 进行说明,显然是不够的。对于 rnn 的应用场景,都需要利用多个时间步,以利用足够多的历史信息对当前时刻输出做出预测。但显然不可能通过循环调用内置__call__函数来执行 rnn,因此若需要定义好的 lstm cell (可以是 single-layer 也可以是 multi-layer)沿时间步展开,这就需要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
)

首先介绍参数cell就是事先定义好的 lstm cell,而inputs就是输入数据,此外还有相当重要的参数sequence_length。参数inputs默认为 [batch_size, max_time,…] 的张量形式,具体来说在文本应用中,参数inputs其实就是 [batch_size, max_time, emb_size] 形式的张量;显然 max_time 就是指的是输入文本序列的最大长度,或者称作最大时间步,这也是所有batch在一起的序列经过padding后的长度。而为了节省计算资源,可以设置前面提到的参数sequence_length,指定batch在一起的每个文本序列的实际长度,也就是说设置该参数后,就说明padding的补零部分可以不用计算了,节省了计算开销。

函数返回值为outputsstate,前者保存了每个时间步的lstm cell输出,为 [batch_size,max_time,cell.output_size] 的张量;后者为最终时间步的lstm 状态,包括 c t \mathbf c_t ct h t \mathbf h_t ht。若构建的 lstm 模型为 multi-layer,则输出的最终时间步隐藏状态包括每一层的 c t l \mathbf c_t^l ctl h t l \mathbf h_t^l htl

5.1 program example

如下所示的代码示例模拟文本数据的输入,首先要对所有单词嵌入得到词向量表示的形式为张量的文本数据。

>>> import numpy as np
>>> import tensorflow as tf
>>> vocab_size = 20
>>> emb_size = 5
>>> batch_size = 7
>>> embedding = tf.get_variable("embedding", shape=[vocab_size, emb_size], dtype=tf.float32) # embedding matrix

>>> import random
>>> max_time = 10 # 假定经过padding后的文本序列长度统一为 max_time
>>> inputs = [[random.randint(0, 20) for _ in range(max_time)] for _ in range(batch_size)] # 模拟产生输入batched文本序列

>>> embedded = tf.nn.embedding_lookup(embedding, inputs)

>>> cell = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(num_units) for num_units in [16, 32]]) # 两层 lstm cell
>>> initial_state = cell.zero_state(batch_size, tf.float32)
>>> outputs, state = tf.nn.dynamic_rnn(cell, inputs=embedded, initial_state=initial_state)

>>> print(len(state))
2
>>> print(state)
(LSTMStateTuple(c=<tf.Tensor 'rnn/while/Exit_3:0' shape=(7, 16) dtype=float32>, h=<tf.Tensor 'rnn/while/Exit_4:0' shape=(7, 16) dtype=float32>), LSTMStateTuple(c=<tf.Tensor 'rnn/while/Exit_5:0' shape=(7, 32) dtype=float32>, h=<tf.Tensor 'rnn/while/Exit_6:0' shape=(7, 32) dtype=float32>))
>>> print(state[1].h)
Tensor("rnn/while/Exit_6:0", shape=(7, 32), dtype=float32)

>>> print(outputs.shape)
(7, 10, 32)
>>> print(outputs)
Tensor("rnn/transpose_1:0", shape=(7, 10, 32), dtype=float32)

注意输出参数state的长度为2,说明lstm网络结构为两层,state的两个元素类型都为LSTMTuple,而每个元组又包含了两个元素,分别是 c t \mathbf c_t ct h t \mathbf h_t ht。可通过语句state[1].hstate[1].c来查看最终时间步的每一层的隐藏状态。

再来看另一个输出参数outputs,是形状为 [batch_size,max_time,cell.output_size] 的张量,记录了每个时间步的 lstm cell 的输出,可以直接用来输入 softmax 层。注意若构建的 lstm 为多层结构,则outputs只包括最高层的隐藏状态 h i L \mathbf h_i^L hiL,并且 cell.output_size 其实就是最高层 lstm cell 的 num_units 参数,在上述代码示例中为32。

6. bi-lstm

当然单向的 lstm 网络并不能够很好地胜任机器翻译、序列标注等任务,因此需要双向的网络结构。首先还是来看下函数tf.nn.bidirectional_dynamic_rnn定义

tf.nn.bidirectional_dynamic_rnn(
    cell_fw,
    cell_bw,
    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
)

参数cell_fwcell_bw分别是定义好的 lstm cell,通常定义成相同的size。其余参数与前面介绍的相同,这里不再赘述。需要注意的是返回参数是元组(outputs,output_states),而outputs本身也是元组类型,包含了前向过程和后向过程的输出张量,每个张量的形状和tf.nn.dynamic_rnn的输出参数outputs相同,可以直接输入进 softmax 层。输出元组的另一个元素output_states本身同样为元组类型,包含了前向和后向过程的最终时间步的隐藏输出 c t \mathbf c_t ct h t \mathbf h_t ht,若是 multi-layer,则会将最终时间步每一层的隐藏状态全部输出。

6.1 program example

>>> import tensorflow as tf
>>> vocab_size = 20
>>> emb_size = 5
>>> batch_size = 7
>>> embedding = tf.get_variable("embedding", shape=[vocab_size, emb_size], dtype=tf.float32)
>>> import random
>>> max_time = 10
>>> inputs = [[random.randint(0, 20) for _ in range(max_time)] for _ in range(batch_size)]
>>> embedded = tf.nn.embedding_lookup(embedding, inputs)

>>> cell_fw = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(num_units) for num_units in [16, 32]])
>>> cell_bw = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.BasicLSTMCell(num_units) for num_units in [16, 32]])
>>> initial_state_fw = cell_fw.zero_state(batch_size, dtype=tf.float32)
>>> initial_state_bw = cell_bw.zero_state(batch_size, dtype=tf.float32)

>>> (outputs, output_states) = tf.nn.bidirectional_dynamic_rnn(cell_fw, cell_bw, inputs=embedded, initial_state_fw=initial_state_fw, initial_state_bw=initial_state_bw)

>>> print(type(outputs))
<class 'tuple'>
>>> print(len(outputs))
2
>>> print(outputs[0])
Tensor("bidirectional_rnn/fw/fw/transpose_1:0", shape=(7, 10, 32), dtype=float32)

>>> print(type(output_states))
<class 'tuple'>
>>> print(len(output_states))
2
>>> print(output_states[1])
(LSTMStateTuple(c=<tf.Tensor 'bidirectional_rnn/bw/bw/while/Exit_3:0' shape=(7, 16) dtype=float32>, h=<tf.Tensor 'bidirectional_rnn/bw/bw/while/Exit_4:0' shape=(7, 16) dtype=float32>), LSTMStateTuple(c=<tf.Tensor 'bidirectional_rnn/bw/bw/while/Exit_5:0' shape=(7, 32) dtype=float32>, h=<tf.Tensor 'bidirectional_rnn/bw/bw/while/Exit_6:0' shape=(7, 32) dtype=float32>))
>>> print(output_states[1][1].h)
Tensor("bidirectional_rnn/bw/bw/while/Exit_6:0", shape=(7, 32), dtype=float32)

>>> concat = tf.concat(outputs, 2)
>>> print(concat)
Tensor("concat:0", shape=(7, 10, 64), dtype=float32)

从代码示例可以看到,输出参数outputs是长度为2的元组,保存了前向和后向过程的输出张量,而前向和后向的每个输出张量又都是 [batch_size,max_time,cell.output_size] 形式。另一输出参数output_states也是长度为2的元组,保存了前向和后向过程的最终时间步的所有隐藏状态。

代码示例最后一句是将前向和后向过程每个时间步的输出串接起来,用于后续的各种运算。


  1. 注意这里用词一定要准确,因为 stack lstm 可能是指 Graham Neubig 提出的用于 dependency parsing 的模型 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值