【轉(zhuǎn)載】學會區(qū)分 RNN 的 output 和 state

https://zhuanlan.zhihu.com/p/28919765

學會區(qū)分 RNN 的 output 和 state

七月

6 個月前

寫這篇文章的起因是,有人問調(diào)用 outputs, last_state = tf.nn.static_rnn(cell, inputs) 之后,last_state 和 outputs[-1] 相等嗎?如果不想等,為什么會不等呢?

其實這也是學習 RNN 的一大難點。我個人認為,學習 RNN 最難的地方就是弄明白它的輸入和輸出是什么。一個簡單的回答是,對于一個 RNN 單元,它每次接受一個當前輸入 x_t 和前一步的隱層狀態(tài) s_{t-1},然后產(chǎn)生一個新的隱層狀態(tài) s_t,也即:s_t = f(x_t, s_{t-1}),其中 f 代表某個函數(shù),對應于 RNN 內(nèi)部的運算過程。

這種說法并沒有錯,但是不夠好,因為很多時候,把 RNN 的輸出和狀態(tài)區(qū)分開來,理解起來會更加順暢。也就是說,我們認為 RNN 是這樣一個單元:y_t, s_t = f(x_t, s_{t-1}) ,畫成圖的話,就是這樣:

這也是本文所要強調(diào)的地方:務必要區(qū)分 RNN 的輸出和狀態(tài)。這么做有什么用呢?先看

先看一個最基本的例子,考慮 Vanilla RNN/GRU Cell(vanilla RNN 就是最普通的 RNN,對應于 TensorFlow 里的 BasicRNNCell),工作過程如下:

這時,s_t = y_t = h_t,區(qū)分這倆確實沒用。

但是!如果是 LSTM 呢?對于 LSTM,它的循環(huán)部件其實有兩部分,一個是內(nèi)部 cell 的值,另一個是根據(jù) cell 和 output gate 計算出的 hidden state,輸出層只利用 hidden state 的信息,而不直接利用 cell。這樣一來,LSTM 的工作過程就是:

其中真正用于循環(huán)的狀態(tài) s_t 其實是 (c_t, h_t) 組成的 tuple(就是 TensorFlow 里的 LSTMStateTuple,當然,如果你選擇不用 tuple 表示 LSTM 的內(nèi)部狀態(tài),也可以把 c_t 和 h_t 拼起來,合起來拼成一個 Tensor,這樣的話它的狀態(tài)就是一個 Tensor 了,這時做別的計算可能會方便一些,這個其實就是 TensorFlow 里的 state_is_tuple 這個開關。),而輸出 y_t 僅僅是 h_t(例如網(wǎng)絡后面再接一個全連接層然后用 softmax 做分類,這個全連接層的輸入僅僅是 h_t,而沒有 c_t),這時就可以看到區(qū)分 RNN 的輸出和狀態(tài)的意義了。

當然,這種抽象的意義不止于此。如果是一個多層的 Vanilla RNN/GRU Cell,那么一種簡單的抽象辦法就是,把多層 Cell 當成一個整體,當成一層大的 Cell,然后原先各層之間的關系都當成這個大的 Cell 的內(nèi)部計算過程/數(shù)據(jù)流動過程,這樣對外而言,多層的 RNN 和單層的 RNN 接口就是一模一樣的:在外部看來,多層 RNN 只是一個內(nèi)部計算更復雜的單層 RNN。圖示如下:

大方框表示把多層 RNN 整體視為一層大的 Cell,而里面的小方框則對應于原先的每一層 RNN。這時,如果把大方框視為一個整體,那么這個整體進行循環(huán)所需要的狀態(tài)就是各層的狀態(tài)組成的集合,或者說把各層的狀態(tài)放在一起組成一個 tuple:

?(這里為什么要用 tuple 呢?直接把它們拼成一個 Tensor 不行嗎,tuple 還得一個一個遍歷,這多麻煩?答案是,不行。因為多層 RNN 并不需要每一層都一樣大,例如有可能最底層維度比較高,隱層單元數(shù)較大,越往上則隱層維度越小。這樣一來,每一層的狀態(tài)維度都不一樣,沒法 concat 成一個 Tensor 啊?。欢@個大的 RNN 單元的輸出則只有原先的最上層 RNN 的輸出,即整體的?

?。

在這個例子里,大 RNN 單元的輸出和狀態(tài)顯然不一樣。在這種視角下,多層 RNN 和單層 RNN 可以用一個統(tǒng)一的視角來看待,世界清爽了許多。事實上,在 TensorFlow 里,MultiRNNCell 就是 RNNCell 的一個子類,它把多層的 RNN 當成一個整體、當成一個內(nèi)部結構更復雜的單層 RNN 來看待。我在文章最開始提到的代表 RNN 內(nèi)部計算的函數(shù) f,其實就是 RNNCell 的 __call__ 方法。

再來看最后一個例子,多層 LSTM:

和之前的例子類似,把多層 LSTM 看成一個整體,這個整體的輸出就是最上層 LSTM 的輸出:?

?;而這個整體進行循環(huán)所依賴的狀態(tài)則是每一層狀態(tài)組合成的 tuple,而每一層狀態(tài)本身又是一個 (c, h) tuple,所以最后結果就是一個 tuple 的 tuple,如圖所示。

這樣一來,便可以回答兩個問題:

其一是,outputs, last_state = tf.nn.static_rnn(cell, inputs) 之后,last_state 和 outputs[-1] 相等嗎?

outputs 是 RNN Cell 的 output 組成的列表,假設一共有 T 個時間步,那么 outputs = [y_1, y_2, ..., y_T],因此 outputs[-1] = y_T;而 last_state 則是最后一步的隱層狀態(tài),即 s_T。

那么,到底 outputs[-1] 等不等于 last_state 呢?或者說 y_T 等不等于 s_T 呢?看一下上面四個圖就可以知道,當且僅當使用單層 Vanilla RNN/GRU 的時候,他們才相等。

其二是,LSTM 作為一個 state 和 output 不一樣的奇葩,導致我們經(jīng)常需要操作那個 (c, h) tuple,或者在 tuple 和 Tensor 之間轉(zhuǎn)換。有一段很常用的轉(zhuǎn)換代碼是這樣的:

lstm_state_as_tensor_shape=[num_layers,2,batch_size,hidden_size]initial_state=tf.zeros(lstm_state_as_tensor_shape)unstack_state=tf.unstack(initial_state,axis=0)tuple_state=tuple([tf.contrib.rnn.LSTMStateTuple(unstack_state[idx][0],unstack_state[idx][1])foridxinrange(num_layers)])inputs=tf.unstack(inputs,num=num_steps,axis=1)outputs,state_out=tf.contrib.rnn.static_rnn(cell,inputs,initial_state=tuple_state)

對著上面最后一張多層 LSTM 的圖琢磨琢磨,就知道這幾行代碼到底是如何把 Tensor 轉(zhuǎn)換成多層 LSTM 的狀態(tài)的了。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容