Tensorflow
中关于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。
rnn cell 基类具有成员函数,其功能是调用执行一次时间步的计算,参数inputs
是 [batch_size,input_size] 的张量,参数state
是指前一时刻的隐藏状态输出。
__call__(
inputs,
state,
scope=None,
*args,
**kwargs
)
注意该函数有两个返回值,习惯上来讲通常写作output
和h
,但根据其源代码可发现,这两个参数实际上是相同的,都是指当前时刻 rnn cell 的隐藏输出,函数返回值设计成这样的形式不过是为了方便输出时选择output
参数,通常是在此基础上添加softmax;而沿时间步传播时选择h
参数。
该成员函数的功能是用来初始化隐藏状态,通常初始时间步的隐藏状态为全零向量,可以调用该成员函数。
zero_state(
batch_size,
dtype
)
注意其输入参数仅为 batch_size 和 dtype,因为事先创建的 rnn cell 的state_size
已知,所以在创建全零的初始时间步隐藏状态时就只需要这两个参数即可,返回的是** [batch_size,state_size]** 的全零张量,这一点在下面代码示例中也有所体现。
>>> 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
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。
>>> 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.h
或h1.c
来查看隐藏输出和记忆输出。而且上述代码显示,返回值output
其实就是h1.h
,只不过是为了沿时间步传播方便而已。
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 cellstate_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隐藏输出。
当然很多时候lstm cell 只有一层并不能够满足需要,因此需要堆叠式(stacked1)的循环网络。这个时候就要使用MultiRNNCell
,其构造函数为:
__init__(
cells,
state_is_tuple=True
)
输入参数cells
的说明为 list of RNNCells that will be composed in this order,然后返回的就是按照输入列表定义的各种 cell 的堆叠结构。
# example 1
num_units = [128, 64]
cells = [BasicLSTMCell(num_units=n) for n in num_units]
stacked_rnn_cell = MultiRNNCell(cells)
从这个代码示例可以看出,可以将不同规模的 rnn cell 堆叠在一起。
# 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=, h=), LSTMStateTuple(c=, h=), LSTMStateTuple(c=, h=))
>>> 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=, h=), LSTMStateTuple(c=, h=), LSTMStateTuple(c=, h=))
>>> print(type(h1))
这个代码示例定义了堆叠三层的网络模型,可以看到属性cell.state_size
这时就要输出三层的 c t l \mathbf c_t^l ctl和 h t l \mathbf h_t^l htl的num_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.c
或h1.h
来输出相应的记忆状态或隐藏状态了,但仍然是保存了全部三层记忆状态和隐藏状态信息的tuple,用来沿时间步传播。
# 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 的层次结构。
上述示例仅在一个时间步下对 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的补零部分可以不用计算了,节省了计算开销。
函数返回值为outputs
和state
,前者保存了每个时间步的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。
如下所示的代码示例模拟文本数据的输入,首先要对所有单词嵌入得到词向量表示的形式为张量的文本数据。
>>> 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=, h=), LSTMStateTuple(c=, h=))
>>> 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].h
或state[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。
当然单向的 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_fw
和cell_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,则会将最终时间步每一层的隐藏状态全部输出。
>>> 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))
>>> 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))
>>> print(len(output_states))
2
>>> print(output_states[1])
(LSTMStateTuple(c=, h=), LSTMStateTuple(c=, h=))
>>> 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的元组,保存了前向和后向过程的最终时间步的所有隐藏状态。
代码示例最后一句是将前向和后向过程每个时间步的输出串接起来,用于后续的各种运算。
注意这里用词一定要准确,因为 stack lstm 可能是指 Graham Neubig 提出的用于 dependency parsing 的模型 ↩︎