NLP與深度學習(二)迴圈神經網路

ZacksTang發表於2021-08-28

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個問題:

  1. 梯度爆炸&消失問題:隨著序列的長度增長,在反向傳播更新引數的過程中,越靠近頂層的梯度會越來越小。這樣便會導致網路的訓練速度變慢,甚至時無法學習。本質上是由於網路層數增加後,反向傳播中梯度連乘效應導致;
  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的資訊進行了過濾:

  1. 對於ft中數值為1的元素,可以讓對應Ct-1位置上的元素通過(如Output中的第3個元素,其值與Ct-1中的值一致)
  2. 對於ft中數值為0的元素,可以讓對應Ct-1位置上的元素不能通過(如Output中的第2個元素,其值為0)
  3. 對於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

[3] Introducing Advanced Deep Learning with Keras | Advanced Deep Learning with TensorFlow 2 and Keras - Second Edition (oreilly.com)

[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/

 

相關文章