一般的網路,這一筆輸入和下一筆沒有關係。RNN 多了一條回頭的線:每一步算出一個隱藏狀態 h,下一步把 h 和新的輸入一起吃進去。這個 h 就是它的「記憶」。最重要的一點:每一步用的是同一組權重,所以句子多長都能處理,模型也不會因為句子變長而變大。下面是一個只有一個數字當記憶的 RNN:ht = tanh(W·ht−1 + U·xt)。
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 有迴圈。
序列的幾種對應:
應用:講義舉了 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 從左往右讀,另一個從右往左讀,兩個的隱藏狀態在頂端合起來決定每一格的輸出。適合要看整句才能判斷的任務,例如補中間缺的字、判斷某個字是不是地名。代價是它需要整句先到齊。
訓練 RNN 的方法叫 BPTT(backpropagation through time):把它攤開成 T 層,照 A2 的反向傳播從最後一步往回算。chain rule 告訴我們,從第 T 步回到第 1 步,要把每一步的局部斜率連乘起來:∂hT/∂h1 = ∏ W·tanh′(·)。一串小於 1 的數相乘會快速趨近 0(消失);大於 1 的數相乘會爆炸(爆炸)。
BPTT:loss 是每一步 loss 的總和。因為每一步用同一個 W,W 的梯度等於「每一步對 W 的梯度」全部加起來。第 i 步的 loss Ji 要影響很早的第 j 步,中間要經過 i − j 次 ∂ht/∂ht−1:
直覺:chain rule 每往回走一步就多乘一個因子。這些因子小,梯度訊號越往回傳越小,這就是 vanishing gradient problem。
線性情況的證明草稿(講義引用 Pascanu 等人 2013):假設沒有 activation,ht = Whht−1 + Wxxt。那麼 ∂ht/∂ht−1 = Wh,所以
把 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 會講)。
LSTM(Long Short-Term Memory,1997 年提出)在 h 之外多了一個 cell state c,像一條傳送帶,專門存長期的資訊。網路可以對它做三件事:擦掉、寫入、讀出。每件事由一個閘門(gate)控制,閘門是一個 0 到 1 之間的數,只決定閥要開多大:1 是全開,0 是全關。關鍵在中間那個 + 號:新的記憶是「留下來的舊記憶加上新寫入的」,不是整個被改寫。
每一步的六行(所有向量長度都是 n;σ 是 sigmoid,所以 gate 都在 0 到 1;⊙ 是逐元素相乘):
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 和機器翻譯上特別明顯。
GRU(Gated Recurrent Unit,2014 年提出)把 LSTM 簡化:沒有獨立的 cell state,只有 h;閘門只有兩個。update gate u 一個人同時管「留多少舊的」和「寫多少新的」,兩者加起來是 1;reset gate r 決定算新內容時要參考多少舊的 h。少一組權重,所以參數比 LSTM 少四分之一,算得也比較快。
reset_after=True 會多一組偏移,所以實際印出來是 3(dn + d² + 2d)。GRU 每一步(講義用 u 表示 update gate):
和 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 時一樣。
這個模組最常被講錯的幾句話。