TensorFlow中RNN實現的正確開啟方式

遇見更好的自己發表於2018-01-29

上週寫了一篇文章介紹了一下RNN的幾種結構,今天就來聊一聊如何在TensorFlow中實現這些結構,這篇文章的主要內容為:

  • 一個完整的、循序漸進的學習TensorFlow中RNN實現的方法。這個學習路徑的曲線較為平緩,應該可以減少不少學習精力,幫助大家少走彎路。
  • 一些可能會踩的坑
  • TensorFlow原始碼分析
  • 一個Char RNN實現示例,可以用來寫詩,生成歌詞,甚至可以用來寫網路小說!(專案地址:hzy46/Char-RNN-TensorFlow

一、學習單步的RNN:RNNCell

如果要學習TensorFlow中的RNN,第一站應該就是去了解“RNNCell”,它是TensorFlow中實現RNN的基本單元,每個RNNCell都有一個call方法,使用方式是:(output, next_state) = call(input, state)。

藉助圖片來說可能更容易理解。假設我們有一個初始狀態h0,還有輸入x1,呼叫call(x1, h0)後就可以得到(output1, h1):


再呼叫一次call(x2, h1)就可以得到(output2, h2):


也就是說,每呼叫一次RNNCell的call方法,就相當於在時間上“推進了一步”,這就是RNNCell的基本功能。

在程式碼實現上,RNNCell只是一個抽象類,我們用的時候都是用的它的兩個子類BasicRNNCell和BasicLSTMCell。顧名思義,前者是RNN的基礎類,後者是LSTM的基礎類。這裡推薦大家閱讀其原始碼實現,一開始並不需要全部看一遍,只需要看下RNNCell、BasicRNNCell、BasicLSTMCell這三個類的註釋部分,應該就可以理解它們的功能了。

除了call方法外,對於RNNCell,還有兩個類屬性比較重要:

  • state_size
  • output_size

前者是隱層的大小,後者是輸出的大小。比如我們通常是將一個batch送入模型計算,設輸入資料的形狀為(batch_size, input_size),那麼計算時得到的隱層狀態就是(batch_size, state_size),輸出就是(batch_size, output_size)。

可以用下面的程式碼驗證一下(注意,以下程式碼都基於TensorFlow最新的1.2版本):

import tensorflow as tf
import numpy as np

cell = tf.nn.rnn_cell.BasicRNNCell(num_units=128) # state_size = 128
print(cell.state_size) # 128

inputs = tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size
h0 = cell.zero_state(32, np.float32) # 通過zero_state得到一個全0的初始狀態,形狀為(batch_size, state_size)
output, h1 = cell.call(inputs, h0) #呼叫call函式

print(h1.shape) # (32, 128)

對於BasicLSTMCell,情況有些許不同,因為LSTM可以看做有兩個隱狀態h和c,對應的隱層就是一個Tuple,每個都是(batch_size, state_size)的形狀:

import tensorflow as tf
import numpy as np
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
inputs = tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size
h0 = lstm_cell.zero_state(32, np.float32) # 通過zero_state得到一個全0的初始狀態
output, h1 = lstm_cell.call(inputs, h0)

print(h1.h)  # shape=(32, 128)
print(h1.c)  # shape=(32, 128)

二、學習如何一次執行多步:tf.nn.dynamic_rnn

基礎的RNNCell有一個很明顯的問題:對於單個的RNNCell,我們使用它的call函式進行運算時,只是在序列時間上前進了一步。比如使用x1、h0得到h1,通過x2、h1得到h2等。這樣的h話,如果我們的序列長度為10,就要呼叫10次call函式,比較麻煩。對此,TensorFlow提供了一個tf.nn.dynamic_rnn函式,使用該函式就相當於呼叫了n次call函式。即通過{h0,x1, x2, …., xn}直接得{h1,h2…,hn}。

具體來說,設我們輸入資料的格式為(batch_size, time_steps, input_size),其中time_steps表示序列本身的長度,如在Char RNN中,長度為10的句子對應的time_steps就等於10。最後的input_size就表示輸入資料單個序列單個時間維度上固有的長度。另外我們已經定義好了一個RNNCell,呼叫該RNNCell的call函式time_steps次,對應的程式碼就是:

# inputs: shape = (batch_size, time_steps, input_size) 
# cell: RNNCell
# initial_state: shape = (batch_size, cell.state_size)。初始狀態。一般可以取零矩陣
outputs, state = tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)

此時,得到的outputs就是time_steps步裡所有的輸出。它的形狀為(batch_size, time_steps, cell.output_size)。state是最後一步的隱狀態,它的形狀為(batch_size, cell.state_size)。

此處建議大家閱讀tf.nn.dynamic_rnn的文件做進一步瞭解。

三、學習如何堆疊RNNCell:MultiRNNCell

很多時候,單層RNN的能力有限,我們需要多層的RNN。將x輸入第一層RNN的後得到隱層狀態h,這個隱層狀態就相當於第二層RNN的輸入,第二層RNN的隱層狀態又相當於第三層RNN的輸入,以此類推。在TensorFlow中,可以使用tf.nn.rnn_cell.MultiRNNCell函式對RNNCell進行堆疊,相應的示例程式如下:

import tensorflow as tf
import numpy as np

# 每呼叫一次這個函式就返回一個BasicRNNCell
def get_a_cell():
    return tf.nn.rnn_cell.BasicRNNCell(num_units=128)
# 用tf.nn.rnn_cell MultiRNNCell建立3層RNN
cell = tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)]) # 3層RNN
# 得到的cell實際也是RNNCell的子類
# 它的state_size是(128, 128, 128)
# (128, 128, 128)並不是128x128x128的意思
# 而是表示共有3個隱層狀態,每個隱層狀態的大小為128
print(cell.state_size) # (128, 128, 128)
# 使用對應的call函式
inputs = tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size
h0 = cell.zero_state(32, np.float32) # 通過zero_state得到一個全0的初始狀態
output, h1 = cell.call(inputs, h0)
print(h1) # tuple中含有3個32x128的向量

通過MultiRNNCell得到的cell並不是什麼新鮮事物,它實際也是RNNCell的子類,因此也有call方法、state_size和output_size屬性。同樣可以通過tf.nn.dynamic_rnn來一次執行多步。

此處建議閱讀MutiRNNCell原始碼中的註釋進一步瞭解其功能。

四、可能遇到的坑1:Output說明

在經典RNN結構中有這樣的圖:



在上面的程式碼中,我們好像有意忽略了呼叫call或dynamic_rnn函式後得到的output的介紹。將上圖與TensorFlow的BasicRNNCell對照來看。h就對應了BasicRNNCell的state_size。那麼,y是不是就對應了BasicRNNCell的output_size呢?答案是否定的

找到原始碼中BasicRNNCell的call函式實現:

def call(self, inputs, state):
    """Most basic RNN: output = new_state = act(W * input + U * state + B)."""
    output = self._activation(_linear([inputs, state], self._num_units, True))
    return output, output

這句“return output, output”說明在BasicRNNCell中,output其實和隱狀態的值是一樣的。因此,我們還需要額外對輸出定義新的變換,才能得到圖中真正的輸出y。由於output和隱狀態是一回事,所以在BasicRNNCell中,state_size永遠等於output_size。TensorFlow是出於儘量精簡的目的來定義BasicRNNCell的,所以省略了輸出引數,我們這裡一定要弄清楚它和圖中原始RNN定義的聯絡與區別。

再來看一下BasicLSTMCell的call函式定義(函式的最後幾行):

new_c = (
    c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

if self._state_is_tuple:
  new_state = LSTMStateTuple(new_c, new_h)
else:
  new_state = array_ops.concat([new_c, new_h], 1)
return new_h, new_state

我們只需要關注self._state_is_tuple == True的情況,因為self._state_is_tuple == False的情況將在未來被棄用。返回的隱狀態是new_c和new_h的組合,而output就是單獨的new_h。如果我們處理的是分類問題,那麼我們還需要對new_h新增單獨的Softmax層才能得到最後的分類概率輸出。

還是建議大家親自看一下原始碼實現來搞明白其中的細節。

五、可能遇到的坑2:因版本原因引起的錯誤

在前面我們講到堆疊RNN時,使用的程式碼是:

# 每呼叫一次這個函式就返回一個BasicRNNCell
def get_a_cell():
    return tf.nn.rnn_cell.BasicRNNCell(num_units=128)
# 用tf.nn.rnn_cell MultiRNNCell建立3層RNN
cell = tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)]) # 3層RNN

這個程式碼在TensorFlow 1.2中是可以正確使用的。但在之前的版本中(以及網上很多相關教程),實現方式是這樣的:

one_cell =  tf.nn.rnn_cell.BasicRNNCell(num_units=128)
cell = tf.nn.rnn_cell.MultiRNNCell([one_cell] * 3) # 3層RNN

如果在TensorFlow 1.2中還按照原來的方式定義,就會引起錯誤!

六、一個練手專案:Char RNN

上面的內容實際上就是TensorFlow中實現RNN的基本知識了。這個時候,建議大家用一個專案來練習鞏固一下。此處特別推薦Char RNN專案,這個專案對應的是經典的RNN結構,實現它使用的TensorFlow函式就是上面說到的幾個,專案本身又比較有趣,可以用來做文字生成,平常大家看到的用深度學習來寫詩寫歌詞的基本用的就是它了。

Char RNN的實現已經有很多了,可以自己去Github上面找,我這裡也做了一個實現,供大家參考。專案地址為:hzy46/Char-RNN-TensorFlow。程式碼的部分實現來自於這篇專欄,在此感謝 @天雨粟 。

我主要向程式碼中新增了embedding層,以支援中文,另外重新整理了程式碼結構,將API改成了最新的TensorFlow 1.2版本。

可以用這個專案來寫詩(以下詩句都是自動生成的):

何人無不見,此地自何如。
一夜山邊去,江山一夜歸。
山風春草色,秋水夜聲深。
何事同相見,應知舊子人。
何當不相見,何處見江邊。
一葉生雲裡,春風出竹堂。
何時有相訪,不得在君心。

還可以生成程式碼:

static int page_cpus(struct flags *str)
{
        int rc;
        struct rq *do_init;
};

/*
 * Core_trace_periods the time in is is that supsed,
 */
#endif

/*
 * Intendifint to state anded.
 */
int print_init(struct priority *rt)
{       /* Comment sighind if see task so and the sections */
        console(string, &can);
}

此外生成英文更不是問題(使用莎士比亞的文字訓練):

LAUNCE:
The formity so mistalied on his, thou hast she was
to her hears, what we shall be that say a soun man
Would the lord and all a fouls and too, the say,
That we destent and here with my peace.

PALINA:
Why, are the must thou art breath or thy saming,
I have sate it him with too to have me of
I the camples.

最後,如果你腦洞夠大,還可以來做一些更有意思的事情,比如我用了著名的網路小說《鬥破蒼穹》訓練了一個RNN模型,可以生成下面的文字:

聞言,蕭炎一怔,旋即目光轉向一旁的那名灰袍青年,然後目光在那位老者身上掃過,那裡,一個巨大的石臺上,有著一個巨大的巨坑,一些黑色光柱,正在從中,一道巨大的黑色巨蟒,一股極度恐怖的氣息,從天空上暴射而出 ,然後在其中一些一道道目光中,閃電般的出現在了那些人影,在那種靈魂之中,卻是有著許些強者的感覺,在他們面前,那一道道身影,卻是如同一道黑影一般,在那一道道目光中,在這片天地間,在那巨大的空間中,瀰漫而開……

“這是一位鬥尊階別,不過不管你,也不可能會出手,那些傢伙,可以為了這裡,這裡也是能夠有著一些異常,而且他,也是不能將其他人給你的靈魂,所以,這些事,我也是不可能將這一個人的強者給吞天蟒,這般一次,我們的實力,便是能夠將之擊殺……”

“這裡的人,也是能夠與魂殿強者抗衡。”

蕭炎眼眸中也是掠過一抹驚駭,旋即一笑,旋即一聲冷喝,身後那些魂殿殿主便是對於蕭炎,一道冷喝的身體,在天空之上暴射而出,一股恐怖的勁氣,便是從天空傾灑而下。

“嗤!”

還是挺好玩的吧,另外還嘗試了生成日文等等。

七、學習完整版的LSTMCell

上面只說了基礎版的BasicRNNCell和BasicLSTMCell。TensorFlow中還有一個“完全體”的LSTM:LSTMCell。這個完整版的LSTM可以定義peephole,新增輸出的投影層,以及給LSTM的遺忘單元設定bias等,可以參考其原始碼瞭解使用方法。

八、學習最新的Seq2Seq API

Google在TensorFlow的1.2版本(1.3.0的rc版已經出了,貌似正式版也要出了,更新真是快)中更新了Seq2Seq API,使用這個API我們可以不用手動地去定義Seq2Seq模型中的Encoder和Decoder。此外它還和1.2版本中的新資料讀入方式Datasets相容。可以閱讀此處的文件學習它的使用方法。

九、總結

最後簡單地總結一下,這篇文章提供了一個學習TensorFlow RNN實現的詳細路徑,其中包括了學習順序、可能會踩的坑、原始碼分析以及一個示例專案hzy46/Char-RNN-TensorFlow,希望能對大家有所幫助。

相關文章