Learning Objectives
- 用連鎖律算出單一權重的梯度,並用數值微分驗證
- 用 numpy 寫出 2-2-1 網路的反向傳播
- 用梯度檢查找出反向傳播程式裡的錯誤
- 說明反向傳播為什麼比逐一做數值微分省時間
每個權重該往哪裡調
上一課的權重都是亂數,輸出沒有意義。要訓練,就要知道:每個權重動一點點,損失會變大還是變小?這個斜率叫梯度(gradient),知道之後就可以用第 4 課的梯度下降,往損失變小的方向走。
最直接的做法是數值微分:把一個權重真的加一點點,重算一次損失,看變多少。但每個參數都要做兩次前向傳播;只要網路有幾千個參數,更新一步就要跑幾千次。
反向傳播(backpropagation)只要一次前向、一次反向,就能得到全部參數的梯度。Rumelhart、Hinton 與 Williams 在 1986 年發表於 Nature 的論文說明了這個方法,並示範隱藏層可以藉此學到有用的特徵。
它的數學只有一條:連鎖律(chain rule)。計算一路串下來時,整體的斜率等於每一段斜率相乘:
∂L/∂w = ∂L/∂p × ∂p/∂z × ∂z/∂w
如果 w 動一點點會讓 z 變 2 倍那麼多,z 動一點點會讓 p 變 0.2 倍,那 w 對 p 的影響就是 2 × 0.2。
逐步看:只有一個權重的連鎖律
先看最小的情況:一個輸入、一個權重、一個輸出,正確答案是 1,所以損失是 -log(p)。先由左到右算出每一段,再由右到左算斜率、相乘,最後用數值微分驗證。
多層時由右往左傳
多層網路的權重很多,但連鎖律的前面幾段是共用的。把共用的部分算好存起來,傳給左邊,就不必重算。這個共用的值這裡叫 delta:損失對某個節點的 z 的斜率。
以 tanh 隱藏層、sigmoid 輸出層、交叉熵損失為例:
| 算什麼 | 公式 | 說明 |
|---|---|---|
| 輸出節點的 delta | delta2 = p − y |
sigmoid 配交叉熵時,斜率相乘後剛好這麼簡單 |
| 接到輸出的權重 | delta2 × 隱藏層的值 |
這條線送進來多少,權重的影響就有多大 |
| 隱藏節點的 delta | (W2 × delta2) × (1 − h²) |
右邊的 delta × 這條權重 × tanh 的斜率 |
| 接到隱藏層的權重 | delta1 × 輸入 |
一樣是 delta × 送進來的值 |
每個權重的梯度都是「這個節點的 delta × 這條線送進來的值」,偏差項的梯度就是 delta 本身(它送進來的值可以當成 1)。
逐步看:2-2-1 網路的前向與反向
權重是寫死的,方便和下面的小工具對照。注意前向傳播留下的 h 和 p,反向傳播時都會再用到。
梯度檢查:抓反向傳播的錯
反向傳播的程式寫錯時,不會報錯,只是訓練不起來,很難查。梯度檢查(gradient checking)就是拿慢但可靠的數值微分,對照反向傳播的結果:
數值微分 ≈ (L(w + h) − L(w − h)) / (2h)
兩邊差距應該非常小;差很多,就是反向傳播寫錯了。數值微分太慢,不拿來訓練,只拿來驗證:通常在幾個參數、幾筆資料上檢查一次就好。下面故意漏掉 tanh 的斜率,看檢查抓不抓得到。
逐步看:梯度檢查抓到漏掉的斜率
四筆 XOR 資料,損失是四筆的平均。numeric 對 W1 的每一格做數值微分;backprop 分成正確與 bug=True(故意漏掉 tanh 的斜率)。
逐步看:用反向傳播訓練 XOR
把前面的計算放進迴圈:前向、算損失、反向、更新。這裡挑了一個 12 輪內學得起來的種子與較大的學習率,這樣才看得完每一步;一般訓練要跑幾百幾千輪,換一個種子也可能卡住。
實作:補上反向傳播
完成 backprop:回傳 W1、b1、W2、b2 的梯度(四筆資料的平均)。程式最後會用數值微分檢查,正確時最後一行印出「通過」。
提示:d2 是 (4, 1),W2.T 是 (1, 2);h 是 (4, 2)。
小測驗:反向傳播
計算一路串成 w → z → p → L。∂z/∂w 是 3,∂p/∂z 是 0.2,∂L/∂p 是 −2,∂L/∂w 是多少?
下面程式會印出什麼?
import numpy as np
x = np.array([0.0, 2.0])
delta = np.array([0.5])
print(np.outer(x, delta).ravel().tolist())
網路有 1000 個參數。一次反向傳播和逐一做中央差分,分別要跑幾次前向傳播?
梯度檢查時,數值微分和反向傳播的差距和梯度本身差不多大。最合理的判斷是?
為什麼不把數值微分直接拿來訓練?
重點整理
- 梯度告訴你每個權重動一點點時損失怎麼變;連鎖律說整體斜率等於每一段斜率相乘。
- 反向傳播:先前向算出每個節點的值,再從輸出往左傳 delta。每個權重的梯度=這個節點的 delta × 這條線送進來的值,偏差項的梯度=delta。
- 一次前向加一次反向就得到全部梯度;數值微分每個參數要跑兩次前向,太慢,只用來梯度檢查。
- 反向傳播寫錯不會報錯,只是學不起來,所以先做梯度檢查。
- 權重要用亂數初始化,全部相同時同一層的神經元永遠學成一樣。
- 這一課每一輪都用全部資料更新一次。下一課看實際訓練要調的東西:批次、學習率、過擬合,以及怎麼用
MLPClassifier和 PyTorch 寫。