本章內容
11.1 序列資料與 RNN
11.2 隨時間反向傳播
11.3 梯度爆炸與消失
11.4 長短期記憶
11.5 閘控循環單元
11.6 LSTM 與 GRU 的比較
11.7 後續發展
11.8 參考資料
文字、語音、感測器讀數、交易紀錄……許多資料天生就是序列:順序本身帶有資訊,長度也不固定。前饋網路與第 10 章 的 CNN 都沒有「先後」的概念,前饋網路還要求輸入大小固定。循環神經網路(Recurrent Neural Network, RNN)以一個隱藏狀態(hidden state)作為記憶,逐步讀入序列,每一步都用同一組權重更新這個狀態。
本章從最基本的 RNN 開始,說明它的訓練方式「隨時間反向傳播」,以及隨之而來的根本困難:梯度爆炸與梯度消失。接著介紹兩種以閘門(gate)緩解長期依賴問題的架構 LSTM 與 GRU,並比較兩者的取捨。
11.1 序列資料與 RNNSequences and Vanilla RNN
最基本的 RNN(常稱為 vanilla RNN)在第 t t t 步讀入輸入 x t x_t x t ,結合上一步的隱藏狀態 h t − 1 h_{t-1} h t − 1 ,計算新的隱藏狀態與輸出:
h t = tanh ( W h h h t − 1 + W x h x t + b h ) y t = W h y h t + b y
\begin{aligned}
h_t &= \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h) \\
y_t &= W_{hy} h_t + b_y
\end{aligned}
h t y t = tanh ( W hh h t − 1 + W x h x t + b h ) = W h y h t + b y h t h_t h t 摘要了到目前為止看過的所有輸入,初始狀態 h 0 h_0 h 0 通常設為零向量。輸出 y t y_t y t 視任務而定,例如在字元級語言模型中,y t y_t y t 經過 softmax 後就是「下一個字元」的機率分布。
這組式子的關鍵是權重在時間上共享 :W h h W_{hh} W hh 、W x h W_{xh} W x h 、W h y W_{hy} W h y 在每一個時間步都相同。這帶來三個好處:參數量與序列長度無關;同一個模型可以處理任意長度的序列;在某個位置學到的模式,也能套用在其他位置。這與 CNN 讓同一個濾波器在空間上滑動是同樣的想法,只是共享的維度從空間換成了時間。
依輸入與輸出的對應方式,RNN 可以組成不同的結構:一對多(例如由一張影像產生描述文字)、多對一(例如判斷一段評論的情緒)、多對多(例如機器翻譯,或為每一個時間步標註類別)。
11.2 隨時間反向傳播Backpropagation Through Time
訓練 RNN 時,把它沿時間展開(unroll),就變成一個很深的前饋網路,每一層對應一個時間步,而且所有層共用同一組權重。在這個展開的網路上套用反向傳播,就稱為隨時間反向傳播(Backpropagation Through Time, BPTT)。
總損失是各時間步損失的和 L = ∑ t = 1 T L t L = \sum_{t=1}^{T} L_t L = ∑ t = 1 T L t 。由於權重是共享的,梯度要把每一個時間步的貢獻加總。以 W h h W_{hh} W hh 為例:
∂ L ∂ W h h = ∑ t = 1 T ∑ k = 1 t ∂ L t ∂ h t ∂ h t ∂ h k ∂ + h k ∂ W h h
\frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^{T} \sum_{k=1}^{t} \frac{\partial L_t}{\partial h_t} \, \frac{\partial h_t}{\partial h_k} \, \frac{\partial^{+} h_k}{\partial W_{hh}}
∂ W hh ∂ L = t = 1 ∑ T k = 1 ∑ t ∂ h t ∂ L t ∂ h k ∂ h t ∂ W hh ∂ + h k 其中 ∂ + h k / ∂ W h h \partial^{+} h_k / \partial W_{hh} ∂ + h k / ∂ W hh 只計算第 k k k 步「直接」用到 W h h W_{hh} W hh 的部分。中間的 ∂ h t / ∂ h k \partial h_t / \partial h_k ∂ h t / ∂ h k 描述第 k k k 步的狀態對第 t t t 步的影響,它是下一節梯度爆炸與消失的根源。
截斷式 BPTT
對很長的序列(例如整本書),完整的 BPTT 要把整條序列的激勵值都存在記憶體中,而且讀完整條序列才能更新一次參數。截斷式 BPTT(truncated BPTT)把序列切成長度固定的區段(例如 25 個字元):
前向傳播 時,隱藏狀態從上一個區段延續下來,不重新歸零,因此模型仍能「看到」更早的資訊。
反向傳播 只在區段內進行,梯度不會穿越區段邊界。
代價是模型難以透過梯度學到比區段更長的依賴關係,但記憶體用量固定,更新也更頻繁。Andrej Karpathy 的 min-char-rnn.py 是很好的最小參考實作:約一百行 NumPy 程式,完整包含字元級 RNN 的前向傳播、截斷式 BPTT、梯度裁剪與 Adagrad 更新。它的訓練迴圈結構大致如下:
h = np.zeros((hidden_size, 1)) # 隱藏狀態跨區段延續
for start in range(0, len(data) - seq_length, seq_length):
inputs = data[start : start + seq_length]
targets = data[start + 1 : start + seq_length + 1] # 目標是下一個字元
loss, grads, h = forward_backward(inputs, targets, h) # 只在這一段內做 BPTT
for g in grads:
np.clip(g, -5, 5, out=g) # 逐元素裁剪,避免梯度爆炸
update(params, grads)
11.3 梯度爆炸與消失Exploding and Vanishing Gradients
由 h j = tanh ( W h h h j − 1 + W x h x j + b h ) h_j = \tanh(W_{hh} h_{j-1} + W_{xh} x_j + b_h) h j = tanh ( W hh h j − 1 + W x h x j + b h ) 可得,狀態對更早狀態的梯度是一連串的乘積:
∂ h t ∂ h k = ∏ j = k + 1 t ∂ h j ∂ h j − 1 = ∏ j = k + 1 t diag ( 1 − h j 2 ) W h h
\frac{\partial h_t}{\partial h_k} = \prod_{j=k+1}^{t} \frac{\partial h_j}{\partial h_{j-1}} = \prod_{j=k+1}^{t} \operatorname{diag}\!\left(1 - h_j^{2}\right) W_{hh}
∂ h k ∂ h t = j = k + 1 ∏ t ∂ h j − 1 ∂ h j = j = k + 1 ∏ t diag ( 1 − h j 2 ) W hh 其中 h j 2 h_j^2 h j 2 是逐元素平方。每往回一步,梯度就要再乘一次同一個矩陣 W h h W_{hh} W hh ,就像純量 w t − k w^{t-k} w t − k 一樣,會隨距離呈指數變化。由於 tanh 的導數不超過 1,行為主要取決於 W h h W_{hh} W hh 的最大奇異值(largest singular value)σ max \sigma_{\max} σ m a x :
σ max < 1 \sigma_{\max} < 1 σ m a x < 1 :梯度消失(vanishing gradients) 。梯度的範數不超過 σ max t − k \sigma_{\max}^{\,t-k} σ m a x t − k ,也就是至少以這個指數速度衰減,遠處的時間步幾乎收不到誤差訊號,模型學不到長期依賴。這個條件足以保證梯度消失。
σ max > 1 \sigma_{\max} > 1 σ m a x > 1 :可能梯度爆炸(exploding gradients) 。梯度可能指數成長,損失突然跳到極大值或變成 NaN。這是爆炸的必要條件,但不一定會發生,因為 tanh 的導數可能把它壓回來。
梯度爆炸:梯度裁剪
梯度爆炸容易偵測,也容易處理。最常用的是依範數裁剪(gradient clipping by norm):梯度 g g g 的範數超過門檻 θ \theta θ 時,等比例縮小:
∥ g ∥ > θ ⟹ g ← θ ∥ g ∥ g
\lVert g \rVert > \theta \quad \Longrightarrow \quad g \leftarrow \frac{\theta}{\lVert g \rVert} \, g
∥ g ∥ > θ ⟹ g ← ∥ g ∥ θ g 這樣保留了梯度的方向,只限制更新步伐的大小。另一種做法是逐元素裁剪(clip by value),把每個分量限制在 [ − c , c ] [-c, c] [ − c , c ] 之內,例如上面 min-char-rnn.py 的做法;它實作簡單,但會稍微改變梯度的方向。其他最佳化技巧請見第 8 章 。
梯度消失:閘門單元
梯度消失則麻煩得多。微弱的梯度無法靠縮放救回來,因為來自遠處的訊號早已淹沒在近處時間步的梯度之中。根本的解法是改變架構,讓資訊與梯度有一條不必反覆乘上 W h h W_{hh} W hh 的路徑,這正是 LSTM 與 GRU 的閘門設計。
11.4 長短期記憶LSTM
長短期記憶(Long Short-Term Memory, LSTM)由 Hochreiter 與 Schmidhuber 在 1997 年提出。它在隱藏狀態 h t h_t h t 之外,另外加入一條細胞狀態(cell state)C t C_t C t 。細胞狀態像一條沿著時間運行的輸送帶,只透過逐元素的乘法與加法被少量修改,資訊可以在上面長距離傳遞;要擦除什麼、寫入什麼、輸出什麼,則由閘門控制。每一步從上一步接收兩個輸入:C t − 1 C_{t-1} C t − 1 與 h t − 1 h_{t-1} h t − 1 。
LSTM 有四個由 [ h t − 1 , x t ] [h_{t-1}, x_t] [ h t − 1 , x t ] 計算出的元件:
元件
符號
激勵函數
作用
遺忘閘(forget gate)
f t f_t f t
σ \sigma σ
擦除:決定 C t − 1 C_{t-1} C t − 1 的每個分量保留多少
輸入閘(input gate)
i t i_t i t
σ \sigma σ
決定是否寫入、寫入多少
候選值(gate gate)
C ~ t \tilde{C}_t C ~ t (或 g t g_t g t )
tanh \tanh tanh
決定要寫入什麼內容
輸出閘(output gate)
o t o_t o t
σ \sigma σ
決定細胞狀態有多少輸出到 h t h_t h t
完整的式子如下,其中 [ h t − 1 , x t ] [h_{t-1}, x_t] [ h t − 1 , x t ] 表示把兩個向量串接,⊙ \odot ⊙ 表示逐元素乘積(Hadamard product):
f t = σ ( W f [ h t − 1 , x t ] + b f ) i t = σ ( W i [ h t − 1 , x t ] + b i ) C ~ t = tanh ( W C [ h t − 1 , x t ] + b C ) o t = σ ( W o [ h t − 1 , x t ] + b o ) C t = f t ⊙ C t − 1 + i t ⊙ C ~ t h t = o t ⊙ tanh ( C t )
\begin{aligned}
f_t &= \sigma(W_f [h_{t-1}, x_t] + b_f) \\
i_t &= \sigma(W_i [h_{t-1}, x_t] + b_i) \\
\tilde{C}_t &= \tanh(W_C [h_{t-1}, x_t] + b_C) \\
o_t &= \sigma(W_o [h_{t-1}, x_t] + b_o) \\
C_t &= f_t \odot C_{t-1} + i_t \odot \tilde{C}_t \\
h_t &= o_t \odot \tanh(C_t)
\end{aligned}
f t i t C ~ t o t C t h t = σ ( W f [ h t − 1 , x t ] + b f ) = σ ( W i [ h t − 1 , x t ] + b i ) = tanh ( W C [ h t − 1 , x t ] + b C ) = σ ( W o [ h t − 1 , x t ] + b o ) = f t ⊙ C t − 1 + i t ⊙ C ~ t = o t ⊙ tanh ( C t ) 三個閘門都使用 sigmoid,輸出介於 0 與 1,相當於對每一個維度各自做「開多少」的軟性開關;候選值使用 tanh,輸出介於 −1 與 1,代表要寫入的內容。實作上通常把四個權重矩陣疊成一個大矩陣,一次矩陣乘法就算出四個元件。
常見誤區Common pitfall
輸出閘 o t o_t o t 和其他閘門一樣,有自己的權重 W o W_o W o 與偏差 b o b_o b o ,不能省略;tanh 是作用在細胞狀態 C t C_t C t 上,再由 o t o_t o t 逐元素控制輸出,即 h t = o t ⊙ tanh ( C t ) h_t = o_t \odot \tanh(C_t) h t = o t ⊙ tanh ( C t ) 。另外,四個元件中只有候選值使用 tanh,三個閘門都使用 sigmoid。
為什麼 LSTM 能緩解梯度消失
關鍵在細胞狀態的更新是加法 :C t = f t ⊙ C t − 1 + i t ⊙ C ~ t C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t C t = f t ⊙ C t − 1 + i t ⊙ C ~ t 。沿著細胞狀態這條路徑往回看(暫不考慮經由閘門的間接路徑),
∂ C t ∂ C t − 1 = diag ( f t )
\frac{\partial C_t}{\partial C_{t-1}} = \operatorname{diag}(f_t)
∂ C t − 1 ∂ C t = diag ( f t ) 梯度往回傳遞許多步時,只會乘上一連串的遺忘閘數值,而不是反覆乘上同一個權重矩陣,也不必每一步都經過 tanh 的導數壓縮。只要遺忘閘接近 1,梯度就能幾乎不衰減地傳到很遠的過去,形成一條梯度的「高速公路」,與第 10 章 ResNet 的跳躍連接是相同的精神。實務上常把遺忘閘的偏差 b f b_f b f 初始化為 1 或更大,讓模型一開始傾向於「記住」。
LSTM 並不能防止梯度爆炸,訓練時通常仍搭配梯度裁剪。
補充Note
1997 年最初的 LSTM 並沒有遺忘閘,細胞狀態只能累加。遺忘閘是 Gers、Schmidhuber 與 Cummins 在 2000 年加入的,讓模型能主動清除不再需要的資訊。今天所說的 LSTM,一般都是指包含遺忘閘的版本。
11.5 閘控循環單元GRU
閘控循環單元(Gated Recurrent Unit, GRU)由 Cho et al. 在 2014 年提出,可以看成 LSTM 的簡化版:它沒有獨立的細胞狀態,只保留隱藏狀態 h t h_t h t ,閘門也從三個減為兩個。
r t = σ ( W r [ h t − 1 , x t ] + b r ) z t = σ ( W z [ h t − 1 , x t ] + b z ) h ~ t = tanh ( W h [ r t ⊙ h t − 1 , x t ] + b h ) h t = ( 1 − z t ) ⊙ h t − 1 + z t ⊙ h ~ t
\begin{aligned}
r_t &= \sigma(W_r [h_{t-1}, x_t] + b_r) \\
z_t &= \sigma(W_z [h_{t-1}, x_t] + b_z) \\
\tilde{h}_t &= \tanh(W_h [r_t \odot h_{t-1}, x_t] + b_h) \\
h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t
\end{aligned}
r t z t h ~ t h t = σ ( W r [ h t − 1 , x t ] + b r ) = σ ( W z [ h t − 1 , x t ] + b z ) = tanh ( W h [ r t ⊙ h t − 1 , x t ] + b h ) = ( 1 − z t ) ⊙ h t − 1 + z t ⊙ h ~ t
重設閘(reset gate)r t r_t r t :計算候選狀態 h ~ t \tilde{h}_t h ~ t 時,要參考多少過去的狀態。r t r_t r t 接近 0 時,模型忽略過去,像是從這一步重新開始。
更新閘(update gate)z t z_t z t :新狀態中,候選狀態所佔的比例。z t z_t z t 接近 0 時,h t ≈ h t − 1 h_t \approx h_{t-1} h t ≈ h t − 1 ,直接沿用舊狀態;接近 1 時,以候選狀態取代。
更新式是舊狀態與候選狀態的凸組合,同時扮演 LSTM 中遺忘閘與輸入閘的角色,只是兩者被綁定為和等於 1:保留得越多,寫入得就越少。當 z t z_t z t 接近 0 時,∂ h t / ∂ h t − 1 \partial h_t / \partial h_{t-1} ∂ h t / ∂ h t − 1 接近單位矩陣,梯度同樣能長距離傳遞,這就是 GRU 緩解梯度消失的方式。部分實作(例如 cuDNN 與 PyTorch)把 r t r_t r t 乘在 W h W_h W h 與 h t − 1 h_{t-1} h t − 1 相乘之後,而不是之前,細節略有不同,但概念相同。
常見誤區Common pitfall
GRU 的更新式有兩種寫法。本章採用 h t = ( 1 − z t ) ⊙ h t − 1 + z t ⊙ h ~ t h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t h t = ( 1 − z t ) ⊙ h t − 1 + z t ⊙ h ~ t ,z t z_t z t 越大代表更新越多;Cho et al. (2014) 原論文與 PyTorch 等框架則寫成 h t = z t ⊙ h t − 1 + ( 1 − z t ) ⊙ h ~ t h_t = z_t \odot h_{t-1} + (1 - z_t) \odot \tilde{h}_t h t = z t ⊙ h t − 1 + ( 1 − z t ) ⊙ h ~ t ,z t z_t z t 越大代表保留越多。兩者只是把 z t z_t z t 換成 1 − z t 1 - z_t 1 − z t ,表達能力相同,但閱讀文獻或程式碼時要先確認慣例,不能混用。另外,每個閘門各有自己的偏差(b r b_r b r 、b z b_z b z ),並不共用。
11.6 LSTM 與 GRU 的比較LSTM vs. GRU
每一組「權重矩陣加偏差」的參數量為 n h ( n h + n x ) + n h n_h(n_h + n_x) + n_h n h ( n h + n x ) + n h ,其中 n h n_h n h 是隱藏狀態的維度、n x n_x n x 是輸入的維度。LSTM 有四組(三個閘門加候選值),GRU 有三組(兩個閘門加候選狀態):
項目
LSTM
GRU
狀態
h t h_t h t 與 C t C_t C t
只有 h t h_t h t
閘門
遺忘、輸入、輸出
重設、更新
參數量
4 ( n h ( n h + n x ) + n h ) 4\,(n_h(n_h+n_x)+n_h) 4 ( n h ( n h + n x ) + n h )
3 ( n h ( n h + n x ) + n h ) 3\,(n_h(n_h+n_x)+n_h) 3 ( n h ( n h + n x ) + n h )
適合情境
長序列、資料量大時的穩健預設
參數較少、運算較快,資料少時較不易過擬合
例如 n h = 128 n_h = 128 n h = 128 、n x = 64 n_x = 64 n x = 64 時,每一組有 128 ⋅ 192 + 128 = 24,704 128 \cdot 192 + 128 = 24{,}704 128 ⋅ 192 + 128 = 24 , 704 個參數,LSTM 共 98,816 個,GRU 共 74,112 個,約少四分之一。(部分框架為每個閘門設兩組偏差,實際數字會略多一些。)
實證研究(例如 Chung et al., 2014;Jozefowicz et al., 2015)發現,兩者都明顯優於 vanilla RNN,但彼此之間沒有一致的勝負,表現差異往往小於超參數調校帶來的差異。實務上可以先用 LSTM 作為預設,在運算資源有限、或想加快實驗迭代時改用 GRU 比較看看。
11.7 後續發展Beyond RNNs
RNN 有兩個結構上的限制:計算必須沿時間逐步進行,難以在 GPU 上平行化;而且即使有閘門,相隔很遠的兩個位置之間仍要經過許多步才能傳遞資訊。注意力機制(attention; Bahdanau et al., 2015)讓模型在每一步直接查看輸入序列的所有位置;Transformer(Vaswani et al., 2017)則完全移除循環結構,只用自注意力(self-attention),訓練時可以完全平行化,任兩個位置之間的路徑長度也是常數。在機器翻譯、語言模型等長序列任務上,Transformer 已大致取代了 RNN;不過 RNN 每一步只需要固定大小的狀態,在串流處理或資源受限的情境中仍有用處。
11.8 參考資料References
Goodfellow, Bengio & Courville, Deep Learning , MIT Press, 2016,第 10 章 Sequence Modeling: Recurrent and Recursive Nets。
Stanford CS231n, Convolutional Neural Networks for Visual Recognition ,Recurrent Neural Networks 單元講義。
deeplearning.ai, Sequence Models (Coursera 深度學習專項課程第五門課)。
Andrej Karpathy, min-char-rnn.py(GitHub Gist, 2015),以及 "The Unreasonable Effectiveness of Recurrent Neural Networks"(部落格文章, 2015)。
Christopher Olah, "Understanding LSTM Networks"(部落格文章, 2015)。
Hochreiter & Schmidhuber, "Long Short-Term Memory", Neural Computation , 1997;Gers, Schmidhuber & Cummins, "Learning to Forget: Continual Prediction with LSTM", Neural Computation , 2000。
Cho et al., "Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation", EMNLP 2014。
Pascanu, Mikolov & Bengio, "On the Difficulty of Training Recurrent Neural Networks", ICML 2013。
Chung, Gulcehre, Cho & Bengio, "Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling", arXiv 2014;Jozefowicz, Zaremba & Sutskever, "An Empirical Exploration of Recurrent Network Architectures", ICML 2015。
Bahdanau, Cho & Bengio, "Neural Machine Translation by Jointly Learning to Align and Translate", ICLR 2015;Vaswani et al., "Attention Is All You Need", NIPS 2017。