2026年8月27日 星期四

自動微分 (Automatic Differentiation) 與 計算圖 (Computational Graph) 的底層運作機制。

深度學習框架的核心—自動微分 (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() 用裁剪後的梯度調整權重)。

對應影音資訊