NEWSIO+ 教育系列01 自己訓練一個 AI02 從照片還原 3D03 深度學習
主線 D · 模組 D1動手做 // 四個實驗台

有記憶的網路

網路怎麼「記得」前面的字?為什麼記不了太久?LSTM 的三個閘門各管什麼?

前面的網路一次看一張圖,看完就忘。但句子、語音、股價是一串東西,下一個字要看前面說了什麼。這個模組講最早的解法:讓網路每一步把自己上一步的狀態也當成輸入,這叫 RNN。接著看它的弱點:訊號往回傳時一步一步變小,所以記不久。最後看 LSTM 怎麼用一條「傳送帶」和三個閥門把記憶保住,以及它的簡化版 GRU。

SEC.01 / RECURRENCE

RNN 會偷偷把上一次的狀態也當輸入

一般的網路,這一筆輸入和下一筆沒有關係。RNN 多了一條回頭的線:每一步算出一個隱藏狀態 h,下一步把 h 和新的輸入一起吃進去。這個 h 就是它的「記憶」。最重要的一點:每一步用的是同一組權重,所以句子多長都能處理,模型也不會因為句子變長而變大。下面是一個只有一個數字當記憶的 RNN:ht = tanh(W·ht−1 + U·xt)。

PROBE A / 把 RNN 攤開成 T 步動手做 // 點輸入、拉 W 和 U
最後一步的記憶 hT0
把 x₁ 翻過來(0↔1),hT 差多少(虛線框)0
每一步的 h(從 h₁ 到 hT)0
參數有幾個 / 被重複用了幾次2 / 8
先按「只有第一個是 1」。第一步的 1 寫進記憶,之後每一步記憶都乘上 W 再過 tanh,長條一格一格變短:這就是「記不久」最直接的樣子。把 W 拉到 0.3,x₁ 的影響兩三步就沒了;拉到 1.0 左右,它撐得比較久。橘色框框是同一組 W、U:T 拉到 10,權重還是只有兩個。h₀ 設成 0。這裡的 W、U 和序列都是示意用的數字。
記憶就是一個被傳下去的數字h 每一步被改寫一次:舊的 h 乘上 W,加上新輸入乘上 U,再壓進 −1 到 1。
同一組權重,每一步重複用這是 RNN 的核心:parameter sharing。所以它能處理任何長度,也能用到訓練時沒看過的長度。
攤開就是一個很深的網路T 步的 RNN 攤開後等於 T 層、每層權重一樣的網路。下一節會看到「很深」帶來什麼麻煩。
細看 · Language model、為什麼需要 RNN、序列的幾種對應、深層與雙向

Language model(LM)是什麼:給一串字,算出「下一個字」是每個字的機率。換個角度說,它能替一整段文字打一個機率分數:P(x(1), …, x(T)) = ∏ P(x(t) | x(1), …, x(t−1))。

為什麼重要:講義說你每天都在用它。手機的輸入法預測下一個字、搜尋框的自動完成、語音辨識、手寫辨識、拼字與文法檢查、作者辨識、機器翻譯、摘要、對話,底下都有一個 LM。講義的結論是:NLP 現在幾乎都重建在 LM 上,GPT-3 就是一個 LM。

Fixed-window neural LM 的限制:最早的神經 LM(Bengio 等人 2000/2003)只看前面固定幾個字,例如「the students opened their ___」只看最後 4 個字,把它們的 word embedding 接起來,過一層 hidden layer,輸出下一個字的機率分佈。問題有三個:(1)視窗永遠不夠大,「As the proctor started the clock」被丟掉了;(2)視窗加大,W 就跟著變大;(3)x(1) 和 x(2) 乘的是 W 裡完全不同的權重,同一個字出現在不同位置要各學一次,沒有對稱性。結論:需要一種能吃任意長度輸入的架構。

為什麼不用 MLP 加滑動視窗:講義列的問題是視窗 L 加大參數就暴增、每一步的決定彼此獨立(不管上一步發生什麼)、樣本不夠長時要麻煩地補 padding、不能處理可變長度。CNN 適合格子狀資料(影像),RNN 適合序列;和 MLP、CNN 最大的差別是 RNN 有迴圈。

序列的幾種對應:

  • one-to-one:一般的網路,一個輸入一個輸出。
  • one-to-many:image captioning,一張圖產生一串字。
  • many-to-one:sentiment classification,一串字變成一個情緒。
  • many-to-many:machine translation,一串字變成另一串字。
  • 同步的 many-to-many:影片逐格分類,每一格都要一個答案。

應用:講義舉了 RNN 寫小說、作曲、學會讀門牌號碼和畫門牌號碼,以及 Google Duplex 這種會打電話訂位的語音助理。

Vanilla RNN cell 與 forward:一個 cell 吃 xt 和 ht−1,吐出 ht:ht = tanh(Whht−1 + Wxxt + b),輸出 yt = Wyht。forward 就是從 h0 開始一格一格往右算(PROBE A 的展開圖),每一格的權重都一樣。RNN-LM 的優點:任何長度都能吃、第 t 步理論上能用到很久以前的資訊、模型大小不隨 context 變長而增加、每一步用同一組權重。缺點:遞迴計算很慢(不能平行),實際上很難用到很多步以前的資訊。

深層 RNN:每一步不只一層,可以疊好幾層,下一層的 h 當上一層的輸入。

Elman vs Jordan:Elman network 把隱藏層的輸出 h 傳到下一步(就是上面那種);Jordan network 把最終輸出 y 傳到下一步。

Bidirectional RNN:一個 RNN 從左往右讀,另一個從右往左讀,兩個的隱藏狀態在頂端合起來決定每一格的輸出。適合要看整句才能判斷的任務,例如補中間缺的字、判斷某個字是不是地名。代價是它需要整句先到齊。

SEC.02 / VANISHING & EXPLODING GRADIENTS

訊號往回傳,每一步都乘一次

訓練 RNN 的方法叫 BPTT(backpropagation through time):把它攤開成 T 層,照 A2 的反向傳播從最後一步往回算。chain rule 告訴我們,從第 T 步回到第 1 步,要把每一步的局部斜率連乘起來:∂hT/∂h1 = ∏ W·tanh′(·)。一串小於 1 的數相乘會快速趨近 0(消失);大於 1 的數相乘會爆炸(爆炸)。

PROBE B / 梯度走回第一步還剩多少動手做 // 拉 T 和 W,開關 clipping
∂hT/∂h1(實際算出來的連乘)1
每一步平均乘上多少(W·tanh′ 的幾何平均)1
狀態穩定
clipping 之後拿去更新的大小未開啟
W < 1:梯度一步一步變小。
每個數字都是真的算出來的:照公式跑一遍 forward,再把每一步的 W·tanh′ 乘起來,畫成對數刻度(往上一格是大 10 倍)。W = 0.8、T = 20 時剩下約 0.014;T = 50 只剩約 10⁻⁵。W = 1.2 則反過來,T = 50 時爆到七千多。切到 tanh:輸入是固定的示意序列,h 一飽和 tanh′ 就趨近 0,連 W > 1 也可能變成消失。打開 clipping:太大的梯度被壓到門檻(橘色虛線),方向不變只是步子變小;但它對消失完全沒用,因為它只會縮小,不會放大。
近的聲音大,遠的聲音小遠處傳回來的梯度比近處小好幾個數量級,權重幾乎只根據附近的字更新,學不到長距離的關係。
爆炸會讓一步跨太遠梯度太大,SGD 一步就跳到 loss 很高的地方,最糟會出現 Inf 或 NaN,只能從之前的 checkpoint 重來。
clipping 只治爆炸梯度的 norm 超過門檻就等比例縮小,方向不變。消失要靠改架構解決:LSTM、residual、attention。
細看 · BPTT、線性情況的證明草稿、對 RNN-LM 的影響、clipping

BPTT:loss 是每一步 loss 的總和。因為每一步用同一個 W,W 的梯度等於「每一步對 W 的梯度」全部加起來。第 i 步的 loss Ji 要影響很早的第 j 步,中間要經過 i − j 次 ∂ht/∂ht−1:

∂Ji/∂hj = ∂Ji/∂hi · ∏j<t≤i ∂ht/∂ht−1

直覺:chain rule 每往回走一步就多乘一個因子。這些因子小,梯度訊號越往回傳越小,這就是 vanishing gradient problem。

線性情況的證明草稿(講義引用 Pascanu 等人 2013):假設沒有 activation,ht = Whht−1 + Wxxt。那麼 ∂ht/∂ht−1 = Wh,所以

∂Ji/∂hj = ∂Ji/∂hi · Whi−j

把 Wh 用 eigenvector 分解,Whℓ 在每個方向上就是 λℓ。只要所有 eigenvalue 的絕對值都小於 1,距離 ℓ = i − j 越大,這一項就指數地縮到 0。最大的 eigenvalue 大於 1 則可能爆炸。有 activation 時,條件變成「最大 eigenvalue 小於 1/γ」,γ 是 activation 導數的上界(tanh 是 1,sigmoid 是 1/4),所以非線性只會讓消失更容易發生。PROBE B 的標量版就是這個證明的一維情況。

為什麼消失是問題:遠處的梯度訊號比近處的小很多,被淹沒了。模型只會根據近的效果更新權重,學不到長期效果。

對 RNN-LM 的影響:講義的例子「When she tried to print her tickets, she found that the printer was out of toner. … After installing the toner into the printer, she finally printed her ___」。要答出 tickets,模型得學到第 7 個字和最後一個字的關係;梯度太小就學不到,測試時也就預測不出類似的長距離依賴。另一個例子「The writer of the books ___」:正確是 is(跟 writer,syntactic recency),但模型常寫 are(跟最近的 books,sequential recency)。講義引用 Linzen 等人 2016:因為梯度消失,RNN-LM 比較會學「最近的字」,而不是「文法上相關的字」。

為什麼爆炸也是問題:SGD 更新是 θ ← θ − η·∇J。梯度太大,一步就跨太遠,跳到 loss 很大的地方;最糟會出現 Inf 或 NaN。

Gradient clipping:如果梯度的 norm 大於門檻(自己選的超參數),在更新前先把它縮小:ĝ ← (門檻 / ‖ĝ‖) · ĝ。直覺是「同一個方向,小一步」。講義引用 Goodfellow 等人《Deep Learning》10.11.1 的圖:一個 hidden state 只有一個數字的 RNN,loss 地形上有一道陡峭的「懸崖」。沒有 clipping,梯度下降一碰到懸崖就跨兩大步,爬上懸崖再被甩到右邊;有 clipping,步子縮小,影響溫和得多。

怎麼修消失:根本問題是 vanilla RNN 的隱藏狀態每一步都被整個改寫,很難把資訊保存很多步。兩條路:給 RNN 一個「用加的」獨立記憶(LSTM,下一節);或做更直接的線性通道(attention、residual connection,D2 會講)。

SEC.03 / LSTM

LSTM:一條傳送帶,三個閥門

LSTM(Long Short-Term Memory,1997 年提出)在 h 之外多了一個 cell state c,像一條傳送帶,專門存長期的資訊。網路可以對它做三件事:擦掉、寫入、讀出。每件事由一個閘門(gate)控制,閘門是一個 0 到 1 之間的數,只決定閥要開多大:1 是全開,0 是全關。關鍵在中間那個 + 號:新的記憶是「留下來的舊記憶加上新寫入的」,不是整個被改寫。

PROBE C / 手動開關三個閘門動手做 // 拉閘門,按「走一步」
現在的 cell state ct−1 / 走了幾步0.000 / 0
走一步後 ct = f·ct−1 + i·c̃t0
輸出 ht = o·tanh(ct)0
梯度沿傳送帶往回流:已走的步數 ∏f / 照現在的 f 走 50 步1
對照:vanilla RNN 每步 ×0.9,同樣 50 步0.0052
照這個順序試。(1)先走幾步把 c 寫到 1 以上,按「全記住」(f = 1、i = 0)再連走 10 步:c 一動也不動,∏f 一直是 1,梯度沿傳送帶往回流完全不衰減。(2)按「全忘掉」(f = 0):一步就清空。(3)按「只寫入」(f = 0、i = 1):舊的丟掉,c 直接換成候選值。(4)把 o 拉到 0:c 還在,只是不輸出到 h,記憶可以「先藏著不說」。真實的 LSTM 裡,三個閘門和 c̃ 都是由 ht−1 和 xt 算出來的,每一步都不一樣;這裡改成手拉,數值是示意。
forget 管擦掉多少f 決定舊記憶留幾成。f = 1 是全部保留,f = 0 是全部擦掉。
input 管寫入多少i 決定這一步的新內容 c̃ 寫進去幾成。output o 決定 c 裡有多少被讀出來變成 h。
+ 號是秘密ct 對 ct−1 的斜率就是 f,不必每步都乘一次權重矩陣。f 接近 1,梯度就能流很多步。
細看 · 三個 gate 的公式、為什麼解決梯度消失、它不保證什麼

每一步的六行(所有向量長度都是 n;σ 是 sigmoid,所以 gate 都在 0 到 1;⊙ 是逐元素相乘):

ft = σ(Wfht−1 + Ufxt + bf) forget gate:舊的 cell 留下哪些
it = σ(Wiht−1 + Uixt + bi) input gate:新內容寫入哪些
ot = σ(Woht−1 + Uoxt + bo) output gate:cell 的哪些部分輸出到 h
c̃t = tanh(Wcht−1 + Ucxt + bc) 要寫進 cell 的新內容
ct = ft ⊙ ct−1 + it ⊙ c̃t 擦掉一些,寫入一些
ht = ot ⊙ tanh(ct) 從 cell 讀出一些

Gate 是動態的:它們的值由目前的 context(ht−1 和 xt)算出來,每個元素各自可以全開、全關或介於之間。所以網路學到的是「看到什麼內容時該記、該忘、該說」。

為什麼能解決梯度消失:vanilla RNN 的隱藏狀態更新是乘法互動,每往回一步都要乘一次 W;LSTM 的 cell state 是加法互動。反向傳播時,梯度沿著 c 這條路往回走,每一步只乘上 ft,不會被權重矩陣反覆相乘。如果 forget gate 被設成每一步都全記住,cell 裡的資訊就能無限期保存。原作者把這個機制叫做「恆定誤差傳送帶」(Constant Error Carousel,CEC),讓誤差可以往回傳上千步。相比之下,vanilla RNN 要學出一個剛好能保存資訊的 Wh 難得多。

它不保證什麼:講義特別強調,LSTM 不保證沒有梯度消失或爆炸,它只是給模型一條比較容易學會長距離依賴的路。f 學成 0.9,50 步後也只剩 0.950 ≈ 0.005;而 gate 本身、c̃ 那條路的梯度還是會經過權重矩陣。講義後面提到,LSTM 的強項在 question answering 和機器翻譯上特別明顯。

SEC.04 / GRU

GRU:兩個閘門的簡化版

GRU(Gated Recurrent Unit,2014 年提出)把 LSTM 簡化:沒有獨立的 cell state,只有 h;閘門只有兩個。update gate u 一個人同時管「留多少舊的」和「寫多少新的」,兩者加起來是 1;reset gate r 決定算新內容時要參考多少舊的 h。少一組權重,所以參數比 LSTM 少四分之一,算得也比較快。

PROBE D / 參數量:RNN、GRU、LSTM動手做 // 拉隱藏維度 d、輸入維度 n
一組權重 dn + d² + d(U、W、b)0
LSTM:4 組(f、i、o、c̃)0
GRU:3 組(u、r、h̃)0
GRU 比 LSTM 少0
每一個 gate(還有候選內容)都要自己的一組權重:乘輸入的 U 是 d×n,乘上一步 h 的 W 是 d×d,加上偏移 b 有 d 個。LSTM 有 4 組,GRU 有 3 組,vanilla RNN 只有 1 組(灰色長條)。比例永遠是 4 : 3,跟 d、n 多大無關。注意 d² 那一項:d 加倍,參數大約變四倍。這裡的維度是示意;Keras 的 GRU 預設 reset_after=True 會多一組偏移,所以實際印出來是 3(dn + d² + 2d)。
u 一個人管兩件事ht = (1 − u)·ht−1 + u·h̃t。u = 0 全部保存,u = 1 全部更新。忘多少就補多少。
r 管「參考多少過去」r = 1 舊的 h 全部拿來算新內容,r = 0 全部不看,像是從頭開始。
先用 LSTM,要快再換 GRU講義的經驗法則。沒有決定性證據證明哪個一定比較好。
細看 · GRU 的公式、為什麼也能撐住梯度、LSTM vs GRU、Keras 寫法

GRU 每一步(講義用 u 表示 update gate):

ut = σ(Wuht−1 + Uuxt + bu) update gate:哪些部分更新、哪些保存
rt = σ(Wrht−1 + Urxt + br) reset gate:舊的 h 哪些拿來算新內容
h̃t = tanh(Wh(rt ⊙ ht−1) + Uhxt + bh) 新的隱藏內容
ht = (1 − ut) ⊙ ht−1 + ut ⊙ h̃t 同時遺忘和記憶

和 LSTM 的對應:GRU 把 forget gate 和 input gate 合成一個 update gate(1 − u 就像 forget,u 就像 input),並把 cell state 和 hidden state 合成一個。forget 和 input 是連動的:忘掉多少,就用新內容補多少,保持一種「恆定」。

為什麼也能撐住梯度:和 LSTM 一樣,ht 對 ht−1 有一條「加法」的直通路。把 u 設成 0,h 每一步原封不動傳下去(可能不是好主意,但很容易做到),長距離的資訊就保住了。

LSTM vs GRU(講義):研究者提過很多種有閘門的 RNN,最常用的是這兩個。最大差別是 GRU 算得快、參數少。沒有決定性證據證明哪個一定比較好。資料很多、或依賴特別長時,LSTM 是好的預設。經驗法則:先用 LSTM,想要更有效率再換 GRU。

Keras 寫法:講義說預設值就夠好用了。

# 文字情緒分類:many-to-one
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
    layers.Embedding(input_dim=10000, output_dim=100),
    layers.LSTM(128),            # 換成 layers.GRU(128) 就是 GRU
    layers.Dense(1, activation="sigmoid"),
])
# 每一步都要輸出(many-to-many):LSTM(128, return_sequences=True)
# 雙向:layers.Bidirectional(layers.LSTM(128))

用 model.summary() 看 LSTM(128) 那一層:4 × (100·128 + 128² + 128) = 117,248 個參數,和 PROBE D 調到 d = 128、n = 100 時一樣。

SEC.05 / MISCONCEPTIONS

常見誤解

這個模組最常被講錯的幾句話。

RNN 有記憶,所以它能記得很久以前的字。理論上可以,實際上很難。記憶每一步都被改寫,梯度往回傳每步乘一次,遠處的訊號幾步後就小到學不到(PROBE A、B)。
LSTM 完全解決了梯度消失。它只是給了一條比較容易保存資訊的加法路徑。forget gate 不是剛好 1 時還是會衰減,也不保證不爆炸。
Gradient clipping 能解決梯度消失。clipping 只把太大的梯度縮小,從不放大。它治爆炸;消失要靠 LSTM、GRU、residual、attention 這類架構上的改變。
雙向 RNN 比較強,生成文字也該用它。雙向 RNN 需要整句先到齊,才能從右往左讀。生成時下一個字還不存在,沒有「右邊」可讀;它適合分類、標記這類整句已知的任務。