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

往下坡走

模型怎麼知道該把哪個權重往哪邊調?

A1 的權重是手動調的。真實的網路有幾百萬到幾千億個權重,沒人調得動。這個模組講那個自動的方法:把 loss 想成一座山,現在站的位置是一組權重,往最陡的下坡方向走一小步,重複。問題只剩兩個:怎麼知道哪邊是下坡,一步要多大。

SEC.01 / SLOPE

先只有一個權重:看斜率,往反方向走

想像模型只有一個權重 w,loss 是 w 的函數,畫出來是一條起伏的曲線。你站在某個 w,想往低處走。最笨的方法是隨便亂試、留最好的;講義說這在 CIFAR-10 上能到 15.5%(亂猜是 10%)。好一點的方法是看腳下的斜率:斜率是正的就往左走,負的就往右走,走多遠由 learning rate 決定。

PROBE A / 一維的山坡動手做 // 拖球,按「走一步」
現在的 w—
這裡的斜率 dL/dw(用 (L(w+h) − L(w−h)) / 2h 算,h = 0.001)—
Loss—
下一步 w ← w − η · dL/dw—
算了幾次 loss / 走了幾步0 / 0
三件事值得試。(1)η 調到 0.5 以上再自動走:球跳來跳去甚至飛出去,這叫發散。(2)η 調到 0.02:走得很慢,30 步走不到底。(3)把球放在右邊那個凹槽自動走:它會停在局部最低點,不知道左邊更低。這三個問題就是 A3 整個模組要處理的事。藍點是隨機搜尋試過的位置:它每試一次都要算一次 loss,而且不知道該往哪邊。
梯度 = 每個方向的斜率一個權重時是一個斜率;一百萬個權重時是一百萬個斜率排成一排,這一排就叫梯度。
往梯度的反方向走梯度指向 loss 上升最快的方向,所以減掉它。w ← w − η·∇L,一行就是整個訓練。
η 是第一個要調的超參數太大會發散,太小太慢。B3 會講怎麼選、怎麼隨時間變。
細看 · 數值梯度為什麼不能用、SGD 的名字、非凸地形

數值梯度:對每一個權重都做一次「加一點 h,看 loss 變多少」。CIFAR-10 的線性分類器有 30,730 個權重,算一次梯度要算 30,731 次 loss,才能更新一次。現代網路動輒幾千萬到幾千億個參數,完全不可行。用中央差分 (L(w+h) − L(w−h)) / 2h 比單邊差分準,但還是慢。這是 SEC.02 要解的問題。

SGD:stochastic gradient descent。「隨機」指的是每一步只用一小批資料(mini-batch)算梯度,不是整個資料集。A3 會講 batch size 的影響。

非凸:真實的 loss 地形有幾百萬維、到處是局部最低點和鞍點(某些方向往上、某些往下)。理論上很多壞的局部最低點,但實務上深網路通常能找到接近最佳的點(LeCun et al. 2015),這件事至今仍有爭論(Goldblum et al. 2020)。B3 會談 flat minima。

SEC.02 / COMPUTATIONAL GRAPH

一次算出所有斜率:計算圖與反向傳播

把一個函數拆成一步一步的小運算,畫成圖:節點是數值,邊是運算。往前算一遍得到輸出(forward)。然後從輸出往回走,每經過一條邊就乘上那條邊的「局部斜率」,走到哪裡就知道輸出對那個節點的斜率(backward)。這就是 chain rule,也就是反向傳播。講義的例子:e = (a + b) × (b + 1)。

PROBE B / e = (a+b)(b+1)動手做 // 改 a、b,按「backward」
0 forward:a=3、b=2 → c=a+b=5、d=b+1=3 → e=c·d=15
1 從輸出開始:∂e/∂e = 1
2 往回一條邊:∂e/∂c = d,∂e/∂d = c
3 再往回:∂e/∂a = ∂e/∂c · ∂c/∂a = d · 1
4 b 有兩條路,加起來:∂e/∂b = d·1 + c·1
結果—
注意 b 的梯度是兩條路徑加起來:b 同時影響 c 和 d,兩邊的影響都要算。這就是為什麼反向模式划算:從輸出往回走一遍,每個節點對輸出的斜率全部拿到。如果反過來從輸入往前推(forward mode),一百萬個輸入就要推一百萬遍。深度學習是「一百萬個參數、一個 loss」,所以反向模式是主流。
PROBE B′ / 算一次梯度要花多少動手做 // 拉參數數量
數值梯度:每個參數各算一次 loss—次 loss 計算,才更新一次
反向傳播:一次 forward + 一次 backward2次,和參數數量無關
代價是 backward 需要把 forward 的中間值全部留著(要記得每個節點的值才能算局部斜率),所以訓練比推論吃記憶體得多。這件事在 A3 講 optimizer state 時會再出現。
局部斜率都很簡單加法的斜率是 1,乘法的斜率是「另一個因子」。複雜的函數只是很多簡單運算串起來。
反向傳播 = 沿路相乘,分岔相加一條路上的局部斜率相乘;一個節點有好幾條路到輸出,各路加總。
它只算梯度,不決定怎麼走反向傳播給你每個參數的斜率。拿到斜率後走多大步、往哪裡修正,是 optimizer 的事(A3)。這是最常被搞混的一點。
細看 · credit assignment、forward 與 reverse mode、PyTorch autograd

信用分配(credit assignment):多層網路最早的核心難題是「最後輸出錯了,前面每一層該負多少責任?」1986 年的反向傳播用 chain rule 回答它:∂L/∂Wl = ∂L/∂hL · ∂hL/∂hL−1 ⋯ ∂hl/∂Wl。中間層不需要自己的標籤,整個網路可以端到端一起訓練。

它留下的問題:梯度可能消失或爆炸(一串小於 1 的數相乘會趨近 0,大於 1 會爆);必須儲存 forward 的 activations;不可微的決策需要 surrogate gradient 或 RL;而且「梯度算對」不代表「optimization 容易」。

Forward mode vs reverse mode:forward mode 從一個輸入出發、追蹤它影響每個節點的程度,適合「輸入少、輸出多」;reverse mode 從一個輸出出發、追蹤每個節點影響它的程度,適合「輸入多、輸出一個」。深度學習是後者。

PyTorch autograd:requires_grad=True 的 tensor 會被記錄進計算圖;loss.backward() 跑 reverse mode,把梯度累加到 .grad,所以每步之前要 optimizer.zero_grad()。要對同一張圖 backward 兩次需要 retain_graph=True;推論時用 torch.no_grad()。model.train() 和 model.eval() 只影響 BatchNorm 和 Dropout。

SEC.03 / IT ACTUALLY LEARNS

把兩樣東西接起來:一個兩層網路真的學會 XOR

A1 最後那個 XOR,隱藏層的權重是手寫的。現在讓網路自己學:隨機初始化,forward 算 loss,backward 算所有權重的梯度,往反方向走一步,重複。講義裡這件事用 20 行 numpy 就寫完了。看決策區域怎麼從一團亂變成兩塊,看 loss 怎麼掉。

PROBE C / 訓練一個 2 → H → 1 的網路動手做 // 按「跑」,看它學

輸入空間與決策區域

Loss 隨步數

Loss(binary cross-entropy)—
步數 / 分對幾個0 / —
這一步各層梯度的大小(backward 算出來的)
輸出層 W₂
—
隱藏層 W₁
—
—
每一步都是真的在算:forward、backward(chain rule)、更新。隱藏層用 tanh,輸出用 sigmoid,loss 是 A1 的 cross-entropy 的二分類版本。試試 H = 2 多重新初始化幾次:有時會卡在 loss 0.35 左右不動,那是局部最低點;H = 4 幾乎每次都成功。也試試 η = 3:loss 曲線會抖。注意隱藏層的梯度常常比輸出層小,這是「越往前梯度越小」的第一次露面。
細看 · 講義裡那 20 行、每一行對應的步驟

講義用 numpy 寫的兩層網路(1000 → 100 → 10,sigmoid,MSE loss),結構和上面的實驗台一模一樣:

# forward
h = sigmoid(x @ w1)          # 隱藏層
y_pred = h @ w2              # 輸出
loss = ((y_pred - y)**2).sum()
# backward(chain rule,從 loss 往回)
grad_y_pred = 2 * (y_pred - y)         # ∂L/∂y_pred
grad_w2 = h.T @ grad_y_pred            # ∂L/∂w2 = h · 上游
grad_h = grad_y_pred @ w2.T            # 往回傳到 h
grad_w1 = x.T @ (grad_h * h * (1 - h)) # 乘上 sigmoid 的局部斜率
# update
w1 -= lr * grad_w1
w2 -= lr * grad_w2

「上游梯度 × 局部梯度」就是 PROBE B 裡沿路相乘的那件事;h * (1 − h) 是 sigmoid 的導數,B2 會講為什麼它最大只有 0.25,所以越往前梯度越小。

為什麼用 torch.nn:講義最後說,上面這些手寫的東西 PyTorch 的 nn 模組都幫你做了:定義層、autograd 算梯度、optimizer 更新。用它。

SEC.04 / MISCONCEPTIONS

常見誤解

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

反向傳播就是 optimizer。反向傳播只算梯度。SGD、Adam 決定「拿到梯度之後怎麼更新」。兩件事分開(A3)。
梯度算對了,訓練就會順。梯度正確不代表 optimization 容易:狹長山谷會 zig-zag、鞍點走不動、梯度會消失。
訓練和推論吃的記憶體差不多。訓練要留著所有 forward 的中間值給 backward 用,還要放 optimizer 的狀態。訓練記憶體是推論的好幾倍。
loss 曲線卡住就是程式寫錯。也可能是局部最低點、η 太小、或初始化不好。先看 B3 的診斷流程再改程式。