深度學習框架的核心—自動微分 (Automatic Differentiation) 與 計算圖 (Computational Graph) 的底層運作機制。
torch.nn.utils.clip_grad_norm_(安全限速器)完美融入「汽車工廠供應鏈」與「計算圖」的底層時序中,一眼看穿為什麼**「必須先有 backward 的責任追溯,限速器才能發揮作用」的本質。
### 🚗 費曼學習角度切入:汽車工廠的「扣分追溯」與「安全限速器」
想像你開了一家汽車工廠(神經網路),你的目標是組裝一台完美的車。在工廠運作中,為了防止某個部門出大包導致整個工廠破產(Loss = NaN),你聘請了一位 資深技師(clip_grad_norm_)。他負責在問題被調查出來後、機器被調整之前,強制把過大的調整幅度扣回安全範圍(限制梯度範數)。
以下是整個生產與防護流程的時序運行:
1️⃣ 第一階段:前向傳播 (Forward Pass / outputs = model(inputs))
• 工廠實況:原料(輸入資料 X)進入第一道工序(第一層權重 W₁),加工成零件 H;接著進入第二道工序(第二層權重 W₂),組裝成成品車。
以「工廠供應鏈」與「微積分鏈鎖律」的視角,為您解構為什麼 loss.backward() 之前梯度不存在,之後才誕生的學術本質。
費曼物理直覺:工廠供應鏈與「責任追溯」 (The Supply Chain Analogy)
想像你開了一家汽車工廠(神經網路),你的目標是組裝一台完美的車。
1. 前向傳播 (Forward Pass / outputs = model(inputs)):
• 原料(輸入資料 X)進入第一道工序(第一層權重 W₁),加工成零件 H 。
• 零件進入第二道工序(第二層權重 W₂),組裝成成品車 Y pred 。
* 這時,把成品車交給客戶,測量成品與完美標準的落差,得到一個數字(損失值 Loss)。
* 在這個階段,你只知道成品車的落差有多大 (例:扣了 10 分)。但你根本不知道是第一道工序 W₁ 的螺絲鎖太鬆,還是第二道工序 W₂ 的烤漆塗太厚造成的。
|
工廠隱喻 |
PyTorch 程式碼 |
產出 |
|
原料 X 進入 螺絲部門 W₁ → 產生半成品 h |
h = X @ W1 |
h (中間特徵) |
|
h 進入 烤漆部門 W₂ → 組裝成 成品車 Y_pred |
Y_pred = h @ W2 |
Y_pred (預測) |
|
測量 成品與理想規格的差距
→ Loss (扣分) |
loss = criterion(Y_pred, target) |
loss (標量) |
此階段 計算圖 只保存「值」:X, W1, h, W2, Y_pred, loss。 梯度尚未產生,W1.grad、W2.grad 為 None。
2. 反向傳播 (Backward Pass / loss.backward()): 「品管逆向追責」
• 為了找出「是誰的責任」,品管部門拿著扣分報告 (Loss),從最尾端開始逆向追查。
loss.backward() # ← 品管開始追查
• 品管 (Loss) 把扣分報告逆向送到 烤漆部門 (W₂) → W2.grad 為 ∂Loss/∂W₂(直接責任)。
• 再往前送到 螺絲部門 (W₁) → W1.grad 為 ∂Loss/∂W₁(間接責任,需要用到中間梯度 ∂Loss/∂h 乘回去)。
此時 計算圖完成「鏈鎖律」的逆向乘積,梯度正式寫入 W.grad。 沒有這一步,資深技師(限速器)根本沒有「車速」可檢查。
• 先追查第二道工序:「烤漆部門 W₂,因為你們烤漆太厚,導致扣了 3 分」→
**這就是 W₂ 的梯度 (
∂Loss
─────
∂W₂ )**。
* 接著再往前追查第一道工序:「螺絲部門 W₁,因為你們手藝問題,影響了後續烤漆,間接導致扣了 7 分」→
**這就是 W₁ 的梯度 (
∂Loss
─────
∂W₁ )**。
小結:在品管部門執行逆向追查 (backward()) 之前,各個部門(參數 W)根本不知道自己該為最終的扣分承擔多少責任。這就是為什麼在 backward() 之前,參數的梯度欄位 W.grad 是空的 (None)。
──────
💜 計算圖 (Computational Graph) 與微積分鏈鎖律 (Chain Rule)
在數學上,神經網路是一系列複合函數的組合:
Loss = L(f₂(f₁(X·W₁)·W₂))
當我們在 PyTorch 中寫下前向傳播代碼時,PyTorch 會在後台默默建構一張 「有向無環圖 (DAG)」,稱為 計算圖:
[X] ──*W1──> [h] ──*W2──> [Y_pred] ──(計算差值)──> [Loss]
(前向傳播的方向 ───> 只是單純做乘法和加法,算出各節點的數值)
在這個圖中,每一個節點都只記錄了自己的「數值」,例如 Loss = 5.0,h = 2.0。此時只有「值」,沒有「導數」。
#### 鏈鎖律的逆向推導 (The Reverse Mode Auto-Differentiation)
當你調用 loss.backward() 時,PyTorch 啟動反向自動微分,開始反向遍歷這張計算圖:
1. 起點:目標是求出 Loss 對所有權重 W 的偏微分。
2. 第一步:計算
∂Loss
─────
∂W₂ (最終扣分對烤漆部門的影響)。
* 根據微積分:
∂Y
∂Loss ∂Loss pred
───── = ──────·──────
∂W ∂Y ∂W
2 pred 2
* PyTorch 計算出這個數值,並將它寫入變數 `W2` 的記憶體空間中,即 `W2.grad = 數值
對已建好的計算圖 逆向遍歷,根據 鏈鎖律 (Chain Rule) 把局部導數乘回去,產生 全局梯度 (∂Loss/∂W).
3. 第二步:計算
∂Loss
─────
∂W₁ (最終扣分對螺絲部門的影響)。
* 根據鏈鎖律 (Chain Rule),必須利用剛剛算好的中間結果逆向乘回去:
∂Y
∂Loss ∂Loss pred ∂h
───── = ──────·──────·───
∂W ∂Y ∂h ∂W
1 pred 1 * PyTorch 計算出此數值,並寫入 `W1.grad`。
──────
### 💡 核心定錨總結
沒有 backward(),就沒有微積分的「鏈鎖律求導過程」。
• Forward Pass (model(X)):建構計算圖,只算出函數值(建構多米諾骨牌)。
• Backward Pass (loss.backward()):沿著計算圖逆向求導,算出變化率(推倒骨牌,回溯每一張骨牌受到的力道)。
• 如果把 clip_grad_norm_ 移到 loss.backward() 之前,p.grad 全部是 None,函式會拋出 RuntimeError: grad must be a Tensor,因此 必須在梯度已生成 後呼叫。
• 這就是為什麼必須執行完 loss.backward(),參數上才會有 W.grad (梯度),後續的 clip_grad_norm_ 也才有東西可以裁剪。
3️⃣ 限速器 ― clip_grad_norm_ 「資深技師」
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
|
說明項目 |
作用原理 |
|
梯度是車速:clip_grad_norm_ 讀取每個參數的梯度向量長度
‖g‖₂ |
等比例縮放 |
|
若 ‖g‖₂ 超過 max_norm,就等比例縮小整個向量 |
保留方向(梯度指向),只把步幅限制在安全範圍內,避免「爆炸」導致 Loss = NaN |
|
已存在的梯度有意義 |
只有在梯度產生後才能進行裁剪,否則 .grad 為 None,裁剪無效 |
執行時序:
loss.backward() → 梯度產生 → clip_grad_norm_ 檢查並裁剪 → optimizer.step() 更新參數。
4️⃣ 優化步驟 ― 「調整機械」
optimizer.step() # 使用被裁剪過的梯度更新參數
optimizer.zero_grad() # 清除舊梯度,為下一輪做準備
• optimizer.step() 只會看到已被裁剪(或未裁剪)的梯度。
• 若裁剪前梯度過大,直接更新會導致參數爆炸;裁剪後的梯度保證每一步的更新幅度 ≤ max_norm,從而防止NaN、梯度爆炸等問題。
5️⃣ 完整示例 – 從前向到優化(含限速)
import torch
import torch.nn as nn
import torch.optim as optim
# 建立簡易兩層模型
class SimpleNet(nn.Module):
def __init__(self, in_dim, hidden_dim, out_dim):
super().__init__()
self.fc1 = nn.Linear(in_dim, hidden_dim, bias=False) # W1
self.fc2 = nn.Linear(hidden_dim, out_dim, bias=False) # W2
def forward(self, x):
h = self.fc1(x) # 螺絲部門
y = self.fc2(h) # 烤漆部門
return y
model = SimpleNet(in_dim=4, hidden_dim=3, out_dim=2)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
#隨機資料
X = torch.randn(8, 4)
target = torch.randn(8, 2)
# 訓練一步
optimizer.zero_grad() # 清除舊梯度
y_pred = model(X) # 前向傳播 (組裝汽車)
loss = criterion(y_pred, target) # 計算損失 (扣分)
loss.backward() # 逆向追責 → 梯度產生
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 安全限速
optimizer.step() # 更新機械 (參數)
• 關鍵點:如果把 clip_grad_norm_ 移到 loss.backward() 之前,model.parameters() 中的 .grad 仍是 None,限速器根本無事可做 → 無效。
沒有 backward() → 沒有梯度 → 限速器無法工作 → 參數可能爆炸。
──────
## 小結
|
步驟 |
為什麼必須這樣做 |
|
前向 (Forward) |
建構計算圖,只保留值 |
|
loss.backward() |
觸發逆向自動微分,產生每個參數的梯度 (W.grad) |
|
clip_grad_norm_ |
只在梯度存在時檢查並等比例縮放,防止爆炸 |
|
optimizer.step() |
使用已裁剪的梯度更新參數,確保每一步的更新幅度安全 |
完整的四段流程 正是深度學習框架的靈魂:建圖 ( 於 model(x) 時自動完成) →
求導 (loss.backward(),梯度寫入 model.parameters()) →
裁剪 (clip_grad_norm_ 在梯度存在時作用)→
更新 (optimizer.step() 用裁剪後的梯度調整權重)。