1. 迴圈神經網路
在介紹迴圈神經網路之前,我們先考慮一個大家閱讀文章的場景。一般在閱讀一個句子時,我們是一個字或是一個詞的閱讀,而在閱讀的同時,我們能夠記住前幾個詞或是前幾句的內容。這樣我們便能理解整個句子或是段落所表達的內容。迴圈神經網路便是採用的與此同樣的原理。
迴圈神經網路(RNN,Recurrent Neural Network)與其他如全連線神經網路、卷積神經網路相比,最大的特點在於:它的內部儲存了一個狀態,其中包含了與已經檢視過的內容的相關資訊。
下面便先以SimpleRNN為例,介紹這一特點。
2. SimpleRNN
SimpleRNN的結構圖如下所示:
Fig. 1. ShusenWang. Simple RNN 模型[2]
可以看到,SimpleRNN的模型比較簡單,在t時刻的輸出,等於t-1 時刻的狀態ht-1與t時刻的輸入Xt的整合。
用公式表示為:
outputt = tanh( (W * Xt) + (U * ht-1) + bias )
其中W為輸入資料X的引數矩陣,U為上一狀態 ht-1的引數矩陣。且這2個引數矩陣全域性共享(也就是說,每個時間步t的W與U矩陣都相同)。
舉個例子,如圖中的文字序列:the cat sat on the mat。假設輸入只有這單個序列,則輸入SimpleRNN時,輸入維度為(1, 6, 32)。這裡1對應的是batch_size(RN也和其他神經網路一樣,可以接收batch資料),6對應的是timesteps(也可以理解為序列長度);32對應的是詞向量維度(這裡假設詞嵌入維度為32維)。所以SimpleRNN的輸入引數shape為(batch_size, timesteps, input_features)。
在第一個單詞the進入RNN後,會進行第一個狀態和輸出h0 的計算。假設單詞the的向量為 Xthe,初始化的狀態為 hfirst(最初始的hfirst取全0),則:
h0 = tanh( (W * Xthe) + (U * hfirst) + bias)
到輸出最後一個狀態 h5 時(此時輸入單詞為mat),即為:
h5 = tanh( (W * Xmat) + (U * h4) + bias)
最終輸出的狀態 h5 即包含了前面輸入的所有狀態(也就是整個序列的資訊),此輸出即可輸入到例如Dense層中用於各類序列任務,如情感分析,文字生成等NLP任務中。
在tensorflow中呼叫SimpleRNN非常簡單,下面是一個簡單的單個SimpleRNN的例子:
from tensorflow.keras import Sequential from tensorflow.keras.layers import Embedding, SimpleRNN model = Sequential() model.add(Embedding(10000, 64)) model.add(SimpleRNN(32)) model.summary() Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding (Embedding) (None, None, 64) 640000 _________________________________________________________________ simple_rnn (SimpleRNN) (None, 32) 3104 ================================================================= Total params: 643,104 Trainable params: 643,104 Non-trainable params: 0 _________________________________________________________________
其中可以看到SimpleRNN層的輸出僅為最終狀態ht的維度。
需要注意的是,給SimpleRNN的引數,我們給的是32。這裡可能剛接觸SimpleRNN時容易弄混的一點是:引數32並非是時間步長數,而是SimpleRNN的輸出維度,也就是ht的維度。
還有之前遇到過的一個問題是:在SimpleRNN中,第一層Embedding的輸出為64,第二層的輸出為32 是如何計算得出的?
對於這個問題,我們看一下這個例子中SimpleRNN層的引數shape:
for w in model.layers[1].get_weights(): print(w.shape) (64, 32) (32, 32) (32,)
從輸出可以看到,這層SimpleRNN有3個引數,分別對應的就是前面提到的公式W,U與bias。在Embedding層的輸出經過了與第一個引數W的矩陣運算後,輸出即轉換為了32維度。
3. RNN
上面提到的SimpleRNN之所以叫SimpleRNN,是因為它相對於普通RNN做了部分簡化。實際上SimpleRNN並非是原始RNN。為了避免讀者對這2個模型產生混淆,下面簡單介紹RNN。
RNN與SimpleRNN的最大區別在於:SimpleRNN少了一個輸出計算步驟。下面是2者的對比:
Fig. 2. Rowel Atienza. Introucing Advanced Deep Learning with Keras[3]
可以看到在,在計算得到timestep t時刻的狀態ht後,相對於SimpleRNN立即將ht輸出到softmax(此處的softmax層並非屬於RNN/SimpleRNN裡的結構),RNN還對輸出進行了進一步處理 ot = V*ht + c,然後再輸出到下一步的softmax中。
4. SimpleRNN的侷限性
前面我們介紹了SimpleRNN可以用於處理序列(或是時序資料),其中每個timestep t 的輸出狀態ht包含了t時刻前的所有輸入資訊。
但是,SimpleRNN有它的侷限性:管理長序列的能力有限。對於長序列,使用SimpleRNN時會帶來2個問題:
- 梯度爆炸&消失問題:隨著序列的長度增長,在反向傳播更新引數的過程中,越靠近頂層的梯度會越來越小。這樣便會導致網路的訓練速度變慢,甚至時無法學習。本質上是由於網路層數增加後,反向傳播中梯度連乘效應導致;
- 忘記最早的輸入資訊:同樣,隨著序列長度的增加,在最終輸出時,越靠近頂部的單詞對最終輸出狀態ht的佔比會越來越小。此原因也是由於引數U的連乘導致的。
由於SimpleRNN對處理長序列的侷限性,後續又提出了更高階的迴圈層:LSTM與GRU。這2個層都是為了解決SimpleRNN所存在的問題而提出。
5. LSTM
LSTM(Long short-term memory)稱為長短記憶,由Hochreiter和Schmidhuber在1997年提出。當今仍在被使用在各類NLP任務中。下面是LSTM的結構圖:
Fig. 3. colah. Understanding LSTM Networks[4]
LSTM也屬於RNN中的一種,所以它的輸入資料也是時序或序列資料。同樣,它在t時間步的輸入也是Xt,輸出為狀態ht。但是它的結果比SimpleRNN要複雜的多,有4個引數矩陣。它最重要的設計是一個傳輸帶向量C(也稱為Cell或Carry):
過去的資訊可以通過傳輸帶向量C送到下一個時刻,並且不會發生太大的變化(僅有上圖中的乘法與加法2種線性變換)。LSTM就是通過傳輸帶來避免梯度消失的問題。
在LSTM中,有幾種型別的門(Gate), 用於控制傳輸帶向量C的狀態。下面分別介紹這幾個Gate,以及輸出狀態的計算方式。
5.1. Forget Gate
Forget Gate 稱為遺忘門,結構如下:
從上圖可以看出,遺忘門是將輸入xt與上一個狀態ht-1 進行concatenate合併後,與Forget Gate引數矩陣Wf進行矩陣乘法,加上偏移量bf。經過啟用函式sigmoid函式進行處理,得出ft。
由於ft為sigmoid函式的結果,所以它的每個元素範圍均為(0,1)。舉個例子,假設a = Wf * [ht-1, xt] + bf,且a的結果為[1, 3, 0, -2],則經過softmax後,ft為:
import tensorflow as tf import numpy as np a = np.array([[1., 3., 0., -2.]]) a = tf.convert_to_tensor(x) f_t = tf.keras.activations.softmax(x) f_t.numpy() array([[0.73105858, 0.95257413, 0.5, 0.11920292]])
然後ft會與傳輸帶向量Ct-1做元素級乘法。舉個例子,假設Ct-1向量為[0.9, 0.2, -0.5, -0.1],ft向量為[0.5, 0, 1, 0.8],則它們的乘積為:
Output = [ (0.9 * 0.5), (0.2 * 0), (-0.5 * 1), (-0.1 * 0.8) ] = [0.45, 0, -0.5, -0.08]
很明顯可以看出,遺忘門ft向量對傳輸帶向量Ct的資訊進行了過濾:
- 對於ft中數值為1的元素,可以讓對應Ct-1位置上的元素通過(如Output中的第3個元素,其值與Ct-1中的值一致)
- 對於ft中數值為0的元素,可以讓對應Ct-1位置上的元素不能通過(如Output中的第2個元素,其值為0)
- 對於ft中數值為 (0, 1) 範圍的元素,可以讓對應Ct-1位置上的元素部分通過(如Output中的第1個元素與第4個元素,其值分別為Ct-1中值的50%與80%)
這樣Forget Gate便對傳輸帶向量C進行了資訊過濾,也可以說決定了傳輸帶向量C需要遺忘的資訊。
5.2. Input Gate
下一步需要決定的是:什麼樣的新資訊被存放在傳輸帶向量C中。這裡引入了另一個門,稱為輸入門(Input Gate)。
這一步的過程圖如下:
可以看到這裡出現了2個新的向量it與C~t。需要注意的是,Input gate僅代表it。
Input Gate 的輸出it 與前面的Forget Gate中ft的計算方法一模一樣,可以理解為最終也是起到一個過濾的作用。
C~t的計算也與it基本一樣,不同的是,啟用函式由sigmoid替換為了tanh。由於使用了tanh,所以C~t向量中所有元素都位於(-1, 1) 之間。
5.3. 更新傳輸帶向量C
在計算得出了ft,it與C~t後,便可更新傳輸帶向量Ct的值。更新過程如下圖所示:
更新過程分為2部分,第1部分是遺忘門ft部分,前面在介紹Forget Gate的作用時已經進行了描述,在此不再闡述。
第2部分為it * C~t,前面Input Gate中提到的作用it也類似與對資訊進行過濾,而C~t也是輸入資訊xt與上一狀態ht-1的另一種整合方法。這2個向量進行矩陣點乘後,將結果資料通過矩陣加法的運算,新增到第1部分的輸出中,便得到了t時刻的傳輸帶向量Ct的值。
簡單地說,Ct就是先通過遺忘門ft忘記了Ct-1中的部分資訊,然後又新增了來自Input Gate中部分新的資訊。
5.4. Output Gate
在更新完傳輸帶向量Ct後,下一步便是計算t時刻的狀態ht,這個過程中引入了最後一個門,稱為輸出門(Output Gate)。
最後輸出ht的計算過程如下圖所示:
從圖中我們可以看到,Output Gate的輸出ot的計算方式與Forget Gate、Input Gate的計算方式完全一樣。
輸出門ot向量由於經過了sigmoid函式,所以其所有元素的範圍均在(0, 1) 之間。
最後在計算ht時,先對傳輸帶向量Ct做tanh變換,這樣其結果中每個元素的範圍便均在(-1, 1) 之間。然後使用輸出門ot向量與此結果做矩陣點乘,便得到t時刻的狀態輸出ht。
ht會有2個副本,1個副本用於輸出,另1個副本用於輸入到下一個時間步t+1中,作為輸入。
5.5. LSTM總結
LSTM與SimpleRNN最大的區別在於:LSTM使用了一個“傳輸帶“,可以讓過去的資訊更容易地傳輸到下一時刻,這樣便使得LSTM對序列的記憶更長。從實際使用上來看,LSTM的效果基本都是優於SimpleRNN。
對於LSTM中3個門的進一步理解,在《Deep Learning with Python》[1]這本書中,作者Francois Chollet提到了非常好的一點:對於這些門的解釋,例如遺忘門用於遺忘傳輸帶向量C中的部分資訊,輸入門用於決定多少資訊輸入到傳輸帶向量C中等。對於這些門的功能解釋並沒有多大意義。因為這些運算的實際效果,是由引數權重決定的。而引數權重矩陣每次都是以訓練的方式,從端到端中學習而來,每次訓練都需要從頭開始,所以不可能為某個運算賦予特定的目的。所以,對RNN中的各類運算組合,最好是將其解釋為對引數搜尋的一組約束,而非是出於工程意義上的一種設計。
前面介紹過,在解決SimpleRNN的問題時,除了LSTM,還有另一種模型稱為GRUs(Gated recurrent units)。GRUs也是引入了Gate的概念,不過相對與LSTM來說更簡單,門也更少。
在實際應用中,大部分場景還是會使用LSTM,而非GRUs。所以本文不會再具體介紹GRUs。
6. Stacked RNN
與其他常規神經網路層一樣,RNN的網路也可以進行堆疊。前面我們介紹SimpleRNN時,提到它的輸出僅為最終的ht向量,但是RNN的輸入是一個序列,無法直接將單個 ht向量輸入到RNN中。
在這種情況下,對RNN進行堆疊,就需要每個時間步t的輸出,如[h0, h1, h2, …, ht],然後將這些狀態h,作為下一層RNN的輸入即可。如下圖所示:
Fig. 5. Deep RecurrentNeuralNetworks[5]
在keras中實現的方式也非常簡單,指定RNN的return_sequences=True引數即可(最後一層RNN不指定),如下所示:
import tensorflow as tf from tensorflow import keras from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Embedding, Dense vocabulary = 10000 embedding_dim = 32 word_num = 500 state_dim = 32 model = Sequential([ Embedding(vocabulary, embedding_dim, input_length=word_num), LSTM(state_dim, return_sequences=True, dropout=0.2), LSTM(state_dim, return_sequences=True, dropout=0.2), LSTM(state_dim, return_sequences=False, dropout=0.2), Dense(1, activation='sigmoid') ])
7. 雙向RNN網路
前面我們看到的SimpleRNN,LSTM都是從左往右,單向地處理序列。在NLP任務中,還常常用到雙向RNN。雙向RNN是RNN的一個變體,在某些任務上比單向RNN效能更好。
在機器學習中,如果一種資料的表示方式不同,但是資料是有價值的話,則是非常值得探索不同的表示方式。若是這種表示方式的差異越大則越好,因為它們提供了其他檢視資料的角度,從而獲取資料資料中被其他方法所忽略的資訊。這個便是整合(ensembling)方法背後的直覺。在影像識別任務中,資料增強的方法也是基於這一理念。
雙向RNN的示例圖如下所示:
Fig. 6. Colah, Neural Networks, Types, and Functional Programming[6]
從上圖中,我們可以看到,雙向神經網路是分別從2個方向(從左到右,從右到左),獨立地訓練了2個神經網路。輸入資料均為X。在得到2個神經網路的輸出狀態hleft, hright後,再將2個向量進行拼接(concatenate)操作,即得到了輸出向量y。這個輸出向量y [y0, y1, y2,… yi] 即可輸入到下一層RNN中。
若是僅需要類似SimpleRNN中ht的單個輸出,則將y向量丟棄,僅將si 與s’I 做拼接後輸出即可。
在keras中,實現雙向RNN的網路也非常簡單,僅需要將layer用Bidirectional() 方法進行包裝即可。例如:
# Bidirectional LSTM vocabulary = 10000 embedding_dim = 32 word_num = 500 state_dim = 32 from tensorflow.keras.layers import Bidirectional model_blstm = Sequential([ Embedding(vocabulary, embedding_dim, input_length=word_num), Bidirectional(LSTM(state_dim, return_sequences=False, dropout=0.2)), Dense(1, activation='sigmoid') ]) model_blstm.summary() Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 500, 32) 320000 _________________________________________________________________ bidirectional (Bidirectional (None, 64) 16640 _________________________________________________________________ dense_1 (Dense) (None, 1) 65 ================================================================= Total params: 336,705 Trainable params: 336,705 Non-trainable params: 0
可以看到,我們給定的LSTM的輸出維度為32,但是在經過了Bidirectional後,輸出維度增加到了64。這是由於Bidirectional RNN的輸出是由2個LSTM(一左一右)的輸出向量的拼接而得出。
總結
本文介紹了常用的迴圈神經網路,其中更有用的是LSTM網路。而雙向RNN在普遍場景下會比單向RNN的效果更好(除非輸入序列需要遵守嚴格的輸入順序),所以可以優先考慮使用雙向RNN。
對於複雜任務,Stacked RNN的引數容量會更多,能解決的問題也會更復雜。如果有足夠的訓練樣本,可以使用Stacked RNN。
另一方面,從現在的趨勢來看,現在的RNN沒有以前流行了。尤其是在NLP問題中,RNN其實顯得有些過時了。在訓練資料足夠多的情況下,已經見到的事實是:RNN的效果不如Transformer模型。不過若是問題是比較小的規模,則RNN還是比較有用的。
下一章節我們會介紹對NLP領域產生變革性提升的Attention機制與Transformer模型。
References
[1] Francois Chollet. Deep Learning with Python. 2017. Chapter 6. Deep learning for text and sequences | Deep Learning with Python (oreilly.com)
[2] RNN模型與NLP應用(3/9):Simple RNN模型_嗶哩嗶哩_bilibili
[4] Understanding LSTM Networks -- colah's blog
[5] 9.3. Deep Recurrent Neural Networks — Dive into Deep Learning 0.17.0 documentation (d2l.ai)
[6] http://colah.github.io/posts/2015-09-NN-Types-FP/