1. 程式人生 > >Tensorflow 中 RNN 的相關使用方法說明

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 當前時刻的ct\mathbf c_tht\mathbf h_t

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 相比多了記憶狀態輸出ct\mathbf c_t,因此需要把這個引數與隱藏輸出ht\mathbf h_t一起由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就是指ht\mathbf h_t隱藏輸出。

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這時就要輸出三層的ctl\mathbf c_t^lhtl\mathbf h_t^lnum_units。然後定義的初始時間步全零狀態和輸出的隱藏狀態都必須儲存全部三層的ct\mathbf c_tht\mathbf h_t資訊。但呼叫__call__(隱式呼叫)的返回輸出引數中,output就只是最高層的隱藏輸出,這裡就是指ht3\mathbf h_t^3,因為需要輸出是就只需要將該資訊輸入進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 狀態,包括ct\mathbf c_tht\mathbf h_t。若構建的 lstm 模型為 multi-layer,則輸出的最終時間步隱藏狀態包括每一層的ctl\mathbf c_t^lhtl\mathbf h_t^l

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,而每個元組又包含了兩個元素,分別是ct\mathbf c_tht\mathbf h_t。可通過語句state[1].hstate[1].c來檢視最終時間步的每一層的隱藏狀態。

再來看另一個輸出引數outputs,是形狀為 [batch_size,max_time,cell.output_size] 的張量,記錄了每個時間步的 lstm cell 的輸出,可以直接用來輸入 softmax 層。注意若構建的 lstm 為多層結構,則outputs只包括最高層的隱藏狀態hiL\mathbf h_i^L,並且 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本身同樣為元組型別,包含了前向和後向過程的最終時間步的隱藏輸出ct\mathbf c_tht\mathbf h_t,若是 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 的模型 ↩︎