Skip to content

反向傳播:算梯度為什麼只要一次前向的成本|Stanford CS229 第 8 堂

2026年10月12日1 分鐘
TL;DR這堂課從一個定理出發:能用 N 個基本運算算出來的純量函數,梯度也能在 O(N) 時間算完。做法是把鏈鎖律讀成「拿到 ∂J/∂u 就能算 ∂J/∂z,而且只需要中間那個模組的資訊」,再替矩陣乘法與啟動函數各推一個 backward 函數:Wᵀv、vzᵀ、σ′(z)⊙v。本文另外用一個 2-2-1 的小網路把前向與反向手算一遍。

🌏 English version

影片狀態:已附影片。 這篇依 Stanford Online 公開的 Spring 2026 第 8 堂錄影的英文字幕寫成,公式記號對照 2026 主講義第 7 章 7.4 節;這堂幾乎全程板書,我沒看到板書畫面,板書上的式子是照講義補回來的。

上一堂把神經網路拆成矩陣乘法、啟動函數、LayerNorm 這些模組,並留下一個問題:SGD 每一步都要損失對所有參數的梯度,這個梯度怎麼算?第 8 堂整堂就在回答這件事,講者是 Tengyu Ma(他在課堂上稱講義為「my lecture notes」,主講義的作者欄是 Tengyu Ma 與 Andrew Ng),是 Stanford CS229 裡幾乎從頭到尾都在板書推導的一堂。

先備是多變數微積分的鏈鎖律、矩陣乘法與轉置,以及上一堂的 MLP 記號。讀完之後,你應該能對一個小型 MLP 手算每個參數的梯度,並說得出每一步用了哪條式子、為什麼整趟反向的成本跟一次前向同一個量級。

YouTube:Stanford CS229 Machine Learning | Spring 2026 | Lecture 8: Neural Networks 2 (Backprop)

這堂課要解的問題(錄影 00:05–03:40)

講者先複習上一堂的設定。損失函數是每筆樣本損失的平均,第 ii 筆樣本的損失 J(i)(θ)J^{(i)}(\theta) 是把模型輸出 hθ(x(i))h_\theta(x^{(i)}) 跟標籤 y(i)y^{(i)} 丟進某個損失(例如 cross-entropy)得到的數字。最簡單的 SGD 每一步隨機抽一筆樣本 iki_k,更新

θ←θ−η ∇J(ik)(θ).\theta \leftarrow \theta - \eta\,\nabla J^{(i_k)}(\theta).

整堂課只關心式子裡的 ∇J(ik)(θ)\nabla J^{(i_k)}(\theta)。模型是很多層矩陣乘法與啟動函數疊起來的,這個梯度要怎麼有效率地算。

講者對這個問題的定位很有意思。他說學習率怎麼調、資料集怎麼設計,多少都帶主觀成分,反向傳播卻是有標準答案的:

"this kind of questions, is one of the most technical questions you can ask in deep learning these days"(約 02:10)

他還提到研究單位面試曾經很愛考手推反向傳播。現在的框架都會自動微分(auto-differentiation),你不必自己寫,但他認為值得弄懂,理由是之後許多決定「depends on fundamentally, how it works to some degree」(約 03:12)。課堂後半提到的記憶體釋放與計算順序,就是這類決定。

一個定理:算梯度跟算函數值一樣快(錄影 03:40–12:26)

講者沒有直接從神經網路開始,而是先講一個更一般的說法。

可微分電路(differentiable circuit)。 把一個函數的計算過程畫成圖:輸入是 x1,x2,…x_1, x_2, \dots,每個節點是一個算術運算(加、減、乘、除)或一個基本函數(sin、cos、exp、sigmoid 之類)。課堂的例子是先算 x1+x2x_1 + x_2,再乘上 x3x_3 得到 (x1+x2) x3(x_1+x_2)\,x_3;另一條路把同一個 x1+x2x_1+x_2 再加上 x4x_4。電路跟公式的差別只有一點:中間結果可以重複使用。寫成公式時 x1+x2x_1+x_2 用了兩次就得寫兩次,電路裡只算一次。講者說這其實就是一種計算圖(computational graph)。

電路的大小 NN 是運算節點的個數。假設每個基本運算花 O(1)O(1) 時間,算出函數值就是把電路走一遍,花 O(N)O(N) 時間。

定理(講義 Theorem 7.4.1,課堂與講義都說是非正式敘述)。 假設一個大小為 NN 的可微分電路計算一個實數值函數 f:Rℓ→Rf:\mathbb{R}^\ell \to \mathbb{R}。那麼梯度 ∇f\nabla f 可以在 O(N)O(N) 時間內算出來,而且算梯度的過程本身也是一個大小 O(N)O(N) 的電路。

有兩個地方要看仔細。

第一,輸出必須是純量。梯度 ∇f\nabla f 是一個 ℓ\ell 維向量,每個輸入座標一個偏導數。

第二,O(N)O(N) 跟輸入維度 ℓ\ell 沒有相乘的關係。這一點有多划算,可以跟最直覺的做法比(以下這段比較是本文的補充,不是課堂內容):用數值差分估梯度,每個座標都要把那個座標加一點點、重算一次函數值,ℓ\ell 個座標就是 ℓ+1\ell + 1 次前向,總共 O(Nℓ)O(N\ell),而且得到的只是近似值。參數有一億個的模型,差分法要一億次前向,定理說只要常數倍的一次。

對應到深度學習。 講者把記號對起來:

定理裡的東西深度學習裡是什麼
函數 ff單筆樣本的損失 J(i)J^{(i)}
輸入 x∈Rℓx \in \mathbb{R}^\ell參數 θ\theta
ℓ\ell參數個數
算 ff(電路走一遍)forward pass
算 ∇f\nabla fbackward pass

神經網路是可微分電路:矩陣乘法拆開來是一堆乘法和加法,啟動函數是基本函數。所以定理直接適用。

講者接著補了一個觀察:幾乎所有現行的神經網路,前向需要的運算數都跟參數個數成正比。他舉的例子是 d×dd \times d 的矩陣乘上一個向量,參數有 d2d^2 個,矩陣向量乘積也要 d2d^2 次乘法。於是結論可以講得更具體:前向算損失、反向算梯度,兩者都是 O(參數個數)O(\text{參數個數})。

講者說這堂課剩下的時間就是在證這個定理,只是不用最抽象的電路語言,改用貼近神經網路的模組語言來證。

把定理用兩次:二階資訊也拿得到(錄影 12:26–19:20)

進入證明之前,講者先展示定理的兩個進階用途。兩個都靠同一個推理:只要某個純量函數能有效率地算出來,它的梯度就能有效率地算出來,不管那個函數的定義裡有沒有包含梯度。

用途一:對「走一步梯度下降之後的損失」微分。 定義

θ  ↦  J(θ−η ∇J(θ)).\theta \;\mapsto\; J\bigl(\theta - \eta\,\nabla J(\theta)\bigr).

這是 θ\theta 的函數,θ\theta 在裡面出現兩次。算它的值要三步:拿到 θ\theta、算梯度 ∇J(θ)\nabla J(\theta)(定理保證這一步跟一次前向同量級)、在新位置 θ−η∇J(θ)\theta-\eta\nabla J(\theta) 算一次損失。講者說每一步都有效率,加起來大約是參數個數的三倍,所以整個函數是有效率可算的,依定理它對 θ\theta 的梯度也是。

把它展開會看到二階導數(這個展開是本文補的)。令 θ′=θ−η∇J(θ)\theta' = \theta - \eta\nabla J(\theta),鏈鎖律給出

ddθJ(θ′)=(I−η ∇2J(θ)) ∇J(θ′).\frac{d}{d\theta}J(\theta') = \bigl(I - \eta\,\nabla^2 J(\theta)\bigr)\,\nabla J(\theta').

這裡 θ′\theta' 對 θ\theta 的 Jacobian 是 I−η∇2J(θ)I-\eta\nabla^2 J(\theta),照下一節的鏈鎖律要乘上它的轉置;Hessian 是對稱矩陣,轉置後不變,所以式子裡沒有寫轉置。裡面有 Hessian,但我們從頭到尾不需要把 Hessian 寫出來。講者說這類做法用在 meta learning:把「跑某個訓練演算法若干步之後的損失」當成目標,對學習率或初始化做反向傳播。他舉的說法是「如果我用這個演算法跑 10 步,結果如何」可以當作初始化的損失函數。他還提到 test-time training 一類的做法也用到這個想法,並承認沒有上下文很難講清楚,這裡只是先讓你知道有這回事。

用途二:Hessian-vector product。 取任意向量 v∈Rℓv \in \mathbb{R}^\ell,定義

g(x)=⟨∇f(x), v⟩=∑i=1ℓ∂f∂xi vi.g(x) = \langle \nabla f(x),\, v\rangle = \sum_{i=1}^{\ell} \frac{\partial f}{\partial x_i}\,v_i .

對 xjx_j 微分,vv 是常數,所以

∂g∂xj=∑i=1ℓ∂2f∂xj ∂xi vi=(∇2f(x) v)j,\frac{\partial g}{\partial x_j} = \sum_{i=1}^{\ell} \frac{\partial^2 f}{\partial x_j\,\partial x_i}\,v_i = \bigl(\nabla^2 f(x)\,v\bigr)_j ,

也就是 ∇g(x)=∇2f(x) v\nabla g(x) = \nabla^2 f(x)\,v。∇f\nabla f 可以在 O(N)O(N) 算出,內積也便宜,所以 gg 是有效率可算的純量函數;再用一次定理,∇g\nabla g 也是。結論是 Hessian 乘上任意向量可以在 O(N)O(N) 時間算完(講義寫的是 O(N+ℓ)O(N+\ell),多出來的 ℓ\ell 是那個內積)。

講者特別提醒這不代表整個 Hessian 也算得出來:Hessian 有 ℓ×ℓ\ell \times \ell 個元素,光是寫出來就超過這個時間。

有學生追問這是否只跟會用二階導數的最佳化器有關(字幕裡最佳化器的名字聽不清楚)。講者的回答:

"I think most of the optimizers these days that are really useful don't have to actually compute the Hessian vector product."(約 18:35)

所以這一節是讓你知道定理的威力,不是日常訓練必備。

鏈鎖律的另一種讀法(錄影 19:20–27:20)

反向傳播的核心是鏈鎖律,但要換一個角度讀它。

設定。 三個變數串成一條:

z∈Rm  →  g    u=g(z)∈Rn  →  f    J=f(u)∈R.z \in \mathbb{R}^m \;\xrightarrow{\;g\;}\; u = g(z) \in \mathbb{R}^n \;\xrightarrow{\;f\;}\; J = f(u) \in \mathbb{R}.

zz 是輸入,uu 是中間變數,JJ 是最後的純量。

微積分課的鏈鎖律。 JJ 要透過 u1,…,unu_1,\dots,u_n 才會受到 ziz_i 影響,所以把每一條路徑的貢獻加起來:

∂J∂zi=∑j=1n∂J∂uj⋅∂gj∂zi,i=1,…,m.\frac{\partial J}{\partial z_i} = \sum_{j=1}^{n} \frac{\partial J}{\partial u_j}\cdot\frac{\partial g_j}{\partial z_i}, \qquad i = 1,\dots,m.

這裡 gjg_j 是 gg 的第 jj 個輸出,也就是 uju_j。(課堂上講者說到一半換了索引字母,字幕裡 ii、jj 有些混用;上面是講義式 7.57 的寫法。)

新的讀法。 把這條式子看成一個計算問題:有人把 ∂J∂u\frac{\partial J}{\partial u} 這個 nn 維向量交給你,請你算出 ∂J∂z\frac{\partial J}{\partial z} 這個 mm 維向量。式子說每個 ∂J∂zi\frac{\partial J}{\partial z_i} 都是 ∂J∂u1,…,∂J∂un\frac{\partial J}{\partial u_1},\dots,\frac{\partial J}{\partial u_n} 的線性組合,係數是 ∂gj∂zi\frac{\partial g_j}{\partial z_i}。線性組合可以寫成矩陣乘向量:

∂J∂z=(∂g1∂z1⋯∂gn∂z1⋮⋱⋮∂g1∂zm⋯∂gn∂zm)∂J∂u.\frac{\partial J}{\partial z} = \begin{pmatrix} \dfrac{\partial g_1}{\partial z_1} & \cdots & \dfrac{\partial g_n}{\partial z_1}\\ \vdots & \ddots & \vdots\\ \dfrac{\partial g_1}{\partial z_m} & \cdots & \dfrac{\partial g_n}{\partial z_m} \end{pmatrix} \frac{\partial J}{\partial u}.

這個矩陣是 m×nm \times n 的,第 (i,j)(i,j) 個元素是 ∂gj∂zi\frac{\partial g_j}{\partial z_i}。一般定義的 Jacobian 把輸出放在列(row)、輸入放在行(column),是 n×mn \times m,所以這裡是 Jacobian 的轉置。講者在這裡停下來說他得照講義抄,才不會把索引弄反。自己推的時候可以用維度檢查:左邊是 mm 維,右邊的向量是 nn 維,中間只能是 m×nm \times n。

這條式子需要什麼資訊。 矩陣的元素是 gg 的偏導數在 zz 這一點的值,所以只需要知道函數 gg 與 zz 的值。ff 完全沒有出現。ff 可以是後面一百層的組合 f1,f2,f3,…f_1, f_2, f_3, \dots,只要有人把 ∂J∂u\frac{\partial J}{\partial u} 給你,ff 長什麼樣子就不必管了。講者用「Markovian」形容這件事:

"You can forget about history, in some sense. And that's why it's efficient."(約 26:41)

backward 函數。 從 ∂J∂u\frac{\partial J}{\partial u} 到 ∂J∂z\frac{\partial J}{\partial z} 的這個映射,講者稱為模組 gg 的 backward 函數,gg 本身是 forward 函數。他說 PyTorch 裡每個模組都得實作 backward,就是這個東西:它只依賴 gg 與 zz 這些局部資訊,因此可以跟任何後續計算組合。講義替它取了記號

∂J∂z=B[g,z] ⁣(∂J∂u),\frac{\partial J}{\partial z} = \mathcal{B}[g, z]\!\left(\frac{\partial J}{\partial u}\right),

zz 固定時 B[g,z]\mathcal{B}[g,z] 是從 Rn\mathbb{R}^n 到 Rm\mathbb{R}^m 的線性映射(就是上面那個矩陣)。

把 backward 函數串起來(錄影 27:20–33:20)

現在考慮三個模組接成一串:

x  →M1  u1  →M2  u2  →M3  J.x \;\xrightarrow{M_1}\; u_1 \;\xrightarrow{M_2}\; u_2 \;\xrightarrow{M_3}\; J .

前向就是照順序算 u1,u2,Ju_1, u_2, J。反向從最靠近 JJ 的地方開始:

  1. 算 ∂J∂u2\frac{\partial J}{\partial u_2}。 只隔一個模組 M3M_3,直接微分。
  2. 算 ∂J∂u1\frac{\partial J}{\partial u_1}。 把 M3M_3 當成 ff、M2M_2 當成 gg,z=u1z = u_1、u=u2u = u_2。套上一節的結果:∂J∂u1=B[M2,u1](∂J∂u2)\frac{\partial J}{\partial u_1} = \mathcal{B}[M_2, u_1]\bigl(\frac{\partial J}{\partial u_2}\bigr)。
  3. 算 ∂J∂x\frac{\partial J}{\partial x}。 這次把 M2M_2 與 M3M_3 合起來當成 ff,M1M_1 當成 gg。ff 變複雜了,但上一步已經把 ∂J∂u1\frac{\partial J}{\partial u_1} 算好,所以只需要 M1M_1 與 xx:∂J∂x=B[M1,x](∂J∂u1)\frac{\partial J}{\partial x} = \mathcal{B}[M_1, x]\bigl(\frac{\partial J}{\partial u_1}\bigr)。

模組再多也是同樣的動作,從後往前一個一個套。模組也不必是同一種:可以是矩陣乘法、啟動函數、再一個矩陣乘法、LayerNorm,每一種有自己的 Jacobian,也就有自己的 backward 函數。把每一種模組的 backward 函數算出來,再照順序組合,就得到整個 backward pass。

這裡順便回答一個自學時常有的疑問:為什麼要從後面往前乘?(以下是本文的補充。)因為 JJ 是純量,從 JJ 這一端出發,手上拿的永遠是一個向量 ∂J∂uk\frac{\partial J}{\partial u_k},每過一個模組只做一次矩陣乘向量。如果從輸入端往後推,手上拿的是「uku_k 對 xx 的 Jacobian」,那是一整個矩陣,每一步都是矩陣乘矩陣。定理要求輸出是純量,原因就在這裡。

我們要的其實是對參數的梯度(錄影 33:20–42:30)

講者自己點出剛才「偷懶」的地方:上面一路算到 ∂J∂x\frac{\partial J}{\partial x},但訓練時通常不在乎對輸入資料的梯度,要的是對參數的梯度。

處理方式是換一個角度看同一個模組。帶參數的模組 u1=M1(x;θ1)u_1 = M_1(x;\theta_1) 有兩個輸入:xx 和 θ1\theta_1。把 xx 當成固定值、把模組看成從 θ1\theta_1 到 u1u_1 的函數,鏈鎖律那一節的推理完全不用改:

∂J∂θ1=B[M1,θ1] ⁣(∂J∂u1).\frac{\partial J}{\partial \theta_1} = \mathcal{B}[M_1, \theta_1]\!\left(\frac{\partial J}{\partial u_1}\right).

講者的說法是,原始資料跟參數只是不同的張量,哪一個當變數由你決定。

於是真正的反向傳播有兩種計算。講者畫的網路是 x→MMθ1→u1→σ→u2→MMθ2→u3→⋯→uk→Jx \to \text{MM}_{\theta_1} \to u_1 \to \sigma \to u_2 \to \text{MM}_{\theta_2} \to u_3 \to \cdots \to u_k \to J,其中 MM 是矩陣乘法模組,uu 是中間變數(activations),參數藏在 MM 裡面。

計算內容需要什麼
對中間變數∂J∂uk→∂J∂uk−1→⋯→∂J∂u1\frac{\partial J}{\partial u_k} \to \frac{\partial J}{\partial u_{k-1}} \to \cdots \to \frac{\partial J}{\partial u_1}上一步的結果、該模組、該模組的輸入
對參數從 ∂J∂u1\frac{\partial J}{\partial u_1} 分支算 ∂J∂θ1\frac{\partial J}{\partial \theta_1},從 ∂J∂u3\frac{\partial J}{\partial u_3} 分支算 ∂J∂θ2\frac{\partial J}{\partial \theta_2}該模組輸出端的梯度、該模組的輸入

第一種計算本質上是循序的,每一步都要等上一步。第二種是從主幹分出去的支線:某一層輸出端的梯度一算好,那一層的參數梯度就可以算,不必等更前面的層。所以兩種計算可以交錯進行,支線也可以平行處理。越靠近損失的參數越早拿到梯度;第一層的參數要等主幹整條走完。(看影片時注意:講者在 40:11–41:17 口頭說的「first」「last」指的是哪一層,前後兩句對不起來;上面是照講義「∂J∂θ[i]\frac{\partial J}{\partial\theta^{[i]}} 只依賴 ∂J∂u[i]\frac{\partial J}{\partial u^{[i]}} 與 u[i−1]u^{[i-1]}」這條依賴關係整理的。)

講者接著談到系統面:某個中間量的下游都算完之後就可以從記憶體丟掉;哪個量依賴哪個量,決定了計算順序、何時釋放記憶體、何時跟其他 GPU 溝通。他的總結是這些最後都取決於計算圖的依賴關係。

矩陣乘法的 backward:對輸入(錄影 42:30–48:26)

到這裡都還是抽象的 gg。接下來把 gg 換成具體模組。第一個是矩陣乘法:

u=g(z)=Wz+b,z∈Rm,  W∈Rn×m,  b∈Rn,J=f(u).u = g(z) = Wz + b, \qquad z\in\mathbb{R}^m,\; W\in\mathbb{R}^{n\times m},\; b\in\mathbb{R}^n,\qquad J = f(u).

目標: 已知 ∂J∂u\frac{\partial J}{\partial u},求 ∂J∂z\frac{\partial J}{\partial z}。

依鏈鎖律那一節,要的矩陣第 (i,j)(i,j) 個元素是 ∂(Wz+b)j∂zi\frac{\partial (Wz+b)_j}{\partial z_i}。先把第 jj 個輸出寫開,這只是矩陣乘法的定義:

(Wz+b)j=bj+∑k=1mWjk zk.(Wz+b)_j = b_j + \sum_{k=1}^{m} W_{jk}\,z_k .

對 ziz_i 微分。bjb_j 跟 ziz_i 無關,導數是 0。總和裡 k≠ik \neq i 的項跟 ziz_i 無關,導數也是 0。只剩 k=ik = i 那一項 WjiziW_{ji}z_i,它是 ziz_i 的線性函數,導數就是係數:

∂(Wz+b)j∂zi=Wji.\frac{\partial (Wz+b)_j}{\partial z_i} = W_{ji}.

矩陣第 (i,j)(i,j) 個元素是 WjiW_{ji},這正是 W⊤W^\top 的定義。所以

∂J∂z=W⊤ ∂J∂u.\frac{\partial J}{\partial z} = W^\top\,\frac{\partial J}{\partial u}.

"So the backward function is just only a transpose. You just transpose your forward matrix and you multiply that, and that's it."(約 47:26)

維度也對得上:W⊤W^\top 是 m×nm\times n,乘上 nn 維向量得到 mm 維向量。

矩陣乘法的 backward:對權重(錄影 48:26–55:43)

同一個模組,這次把 zz 當固定、WW 當變數。目標是從 ∂J∂u\frac{\partial J}{\partial u} 算 ∂J∂W\frac{\partial J}{\partial W}。

講者說這裡記號比較麻煩。變數 WW 是矩陣,要把「uu 對 WW 的 Jacobian」寫成緊湊的形式得動用張量記號(講義說那會是一個四階張量),所以他不用矩陣形式,直接對每個元素 WijW_{ij} 用求和形式的鏈鎖律:

∂J∂Wij=∑k=1n∂J∂uk⋅∂(Wz+b)k∂Wij.\frac{\partial J}{\partial W_{ij}} = \sum_{k=1}^{n} \frac{\partial J}{\partial u_k}\cdot\frac{\partial (Wz+b)_k}{\partial W_{ij}} .

把第 kk 個輸出寫開:

(Wz+b)k=∑s=1mWks zs+bk.(Wz+b)_k = \sum_{s=1}^{m} W_{ks}\,z_s + b_k .

這個式子裡出現的權重是 Wk1,…,WkmW_{k1},\dots,W_{km},也就是 WW 的第 kk 列。對 WijW_{ij} 微分時:

  • 如果 k≠ik \neq i,式子裡根本沒有 WijW_{ij},導數是 0。
  • 如果 k=ik = i,只有 s=js = j 那一項 WijzjW_{ij}z_j 含有它,導數是係數 zjz_j。

所以對 kk 的總和只留下 k=ik=i 一項:

∂J∂Wij=∂J∂ui⋅zj.\frac{\partial J}{\partial W_{ij}} = \frac{\partial J}{\partial u_i}\cdot z_j .

怎麼記。 講者說他念博士時是這樣記的:把矩陣乘法畫成輸入節點 z1,…,zmz_1,\dots,z_m 連到輸出節點 u1,…,unu_1,\dots,u_n 的圖,WijW_{ij} 是連接 zjz_j 與 uiu_i 的那條邊上的權重。這條邊的梯度等於「輸出端 uiu_i 的梯度」乘上「輸入端 zjz_j 的值」。一條邊只需要它兩端的量。

他說生物學把這種形式叫 Hebbian rule:突觸的更新量等於一端的誤差乘上另一端的數值。(本文補一句:一般說的 Hebbian rule 是兩端神經元的活性相乘;這裡輸出端換成了梯度,兩者相同的地方是「一條邊的更新只看它兩端的局部量」。)

矩陣形式與秩 1。 把所有 (i,j)(i,j) 收成一個矩陣,第 (i,j)(i,j) 個元素是 ∂J∂ui zj\frac{\partial J}{\partial u_i}\,z_j,這是一個行向量(n×1n\times1)乘一個列向量(1×m1\times m)的外積:

∂J∂W=∂J∂u  z⊤∈Rn×m.\frac{\partial J}{\partial W} = \frac{\partial J}{\partial u}\; z^\top \in \mathbb{R}^{n\times m}.

外積的每一列都是 z⊤z^\top 的倍數,所以這個矩陣的秩是 1(兩個向量裡有一個是零向量時,整個矩陣是 0)。

"So the gradient for one example and for one weight matrix is always rank 1."(約 54:42)

講者補充兩點:這個事實被一些提升效率的方法拿來用;而它只對單筆樣本成立,實際訓練會對多筆樣本取平均,許多個秩 1 矩陣加起來就不再是秩 1。

bias 的梯度。 課堂提到還要算對 bb 的關係,但板書只推了 WW。照講義式 7.70 補上:(Wz+b)j(Wz+b)_j 對 bib_i 的導數在 i=ji=j 時是 1、其餘是 0,矩陣是單位矩陣,所以

∂J∂b=∂J∂u.\frac{\partial J}{\partial b} = \frac{\partial J}{\partial u}.

啟動函數的 backward(錄影 55:43–59:00)

第二種模組是逐元素的啟動函數(activation function):u=σ(z)u = \sigma(z),意思是 ui=σ(zi)u_i = \sigma(z_i),σ\sigma 是一個單變數函數,套在每個座標上,這裡輸入與輸出同為 mm 維。

一樣套鏈鎖律的矩陣,第 (i,j)(i,j) 個元素是 ∂σ(zj)∂zi\frac{\partial \sigma(z_j)}{\partial z_i}。第 jj 個輸出只看第 jj 個輸入,所以 i≠ji\neq j 時是 0,對角線上是 σ′(zi)\sigma'(z_i)。整個矩陣是對角矩陣:

∂J∂z=diag⁡(σ′(z1),…,σ′(zm)) ∂J∂u.\frac{\partial J}{\partial z} = \operatorname{diag}\bigl(\sigma'(z_1),\dots,\sigma'(z_m)\bigr)\,\frac{\partial J}{\partial u}.

對角矩陣乘向量,結果的第 ii 個座標就是 σ′(zi)⋅∂J∂ui\sigma'(z_i)\cdot\frac{\partial J}{\partial u_i}。所以不必真的建出矩陣,寫成逐元素乘積就好:

∂J∂z=σ′(z)⊙∂J∂u.\frac{\partial J}{\partial z} = \sigma'(z)\odot\frac{\partial J}{\partial u}.

講者提醒 ⊙\odot 跟內積不一樣:內積是逐元素相乘之後全部加起來,得到一個數;⊙\odot 不加,結果還是一個向量。

回頭核對成本,以及課末問答(錄影 59:00–1:02:07)

最後講者把三個 backward 函數的成本跟各自的前向比一遍,確認定理說的事在模組層級成立。下表用前面推導的維度(W∈Rn×mW\in\mathbb{R}^{n\times m}):

模組與變數前向backward 函數backward 成本
矩陣乘法,對 zzWz+bWz+b,O(mn)O(mn)W⊤vW^\top vO(mn)O(mn),轉置矩陣乘向量跟原矩陣乘向量一樣
矩陣乘法,對 WW同上v z⊤v\,z^\topO(mn)O(mn),把 mnmn 個元素填滿
啟動函數,對 zzσ(z)\sigma(z),O(m)O(m)σ′(z)⊙v\sigma'(z)\odot vO(m)O(m)

其中 vv 代表傳進來的 ∂J∂u\frac{\partial J}{\partial u}。每個模組的反向都跟前向同一個量級,整個網路加起來就是定理的結論。

課末有學生問到 sigmoid。講者說現在常用的啟動函數有好幾種,他一時記不全名稱(字幕在這裡也聽不清楚),重點是「一個可以快速微分的單變數函數」。他還提到現在也有人用 gating:做兩個矩陣乘法,再把兩個結果逐元素相乘,當成另一種非線性;細節與參考資料在講義裡。

完整例子:手算一個 2-2-1 網路

以下是本文自擬的例子,不是課堂內容。課堂只推了單一模組的 backward 函數,沒有把整個網路串起來算一次;串接的順序依講義 7.4.4 節的 Algorithm 3,損失用講義 7.4.3 節給的平方損失 ℓMSE(z,y)=12(z−y)2\ell_{\text{MSE}}(z,y)=\frac12(z-y)^2,它的 backward 是乘上 (z−y)(z-y)。啟動函數用 ReLU,σ(t)=max⁡(t,0)\sigma(t)=\max(t,0),t>0t>0 時導數是 1、t<0t<0 時是 0。

網路與數字。

x=(12),W[1]=(1−121),b[1]=(0−1),W[2]=(32),b[2]=1,y=5.x=\begin{pmatrix}1\\2\end{pmatrix},\quad W^{[1]}=\begin{pmatrix}1&-1\\2&1\end{pmatrix},\quad b^{[1]}=\begin{pmatrix}0\\-1\end{pmatrix},\quad W^{[2]}=\begin{pmatrix}3&2\end{pmatrix},\quad b^{[2]}=1,\quad y=5.

前向。 每個中間值都要留著,反向會用到。

z[1]=W[1]x+b[1]=(1⋅1+(−1)⋅2+02⋅1+1⋅2−1)=(−13),a[1]=σ(z[1])=(03),z^{[1]} = W^{[1]}x+b^{[1]} = \begin{pmatrix}1\cdot1+(-1)\cdot2+0\\ 2\cdot1+1\cdot2-1\end{pmatrix}=\begin{pmatrix}-1\\3\end{pmatrix}, \qquad a^{[1]}=\sigma(z^{[1]})=\begin{pmatrix}0\\3\end{pmatrix}, z[2]=W[2]a[1]+b[2]=3⋅0+2⋅3+1=7,J=12(7−5)2=2.z^{[2]} = W^{[2]}a^{[1]}+b^{[2]} = 3\cdot0+2\cdot3+1 = 7, \qquad J=\tfrac12(7-5)^2=2 .

反向。 從 JJ 往回走,每一步標出用的是哪條式子。

  1. 損失模組:∂J∂z[2]=z[2]−y=2\dfrac{\partial J}{\partial z^{[2]}} = z^{[2]}-y = 2。
  2. 第二層參數(支線,用 vz⊤vz^\top 與 bias 那條):
∂J∂W[2]=2⋅a[1]⊤=(06),∂J∂b[2]=2.\frac{\partial J}{\partial W^{[2]}} = 2\cdot a^{[1]\top} = \begin{pmatrix}0&6\end{pmatrix},\qquad \frac{\partial J}{\partial b^{[2]}} = 2 .
  1. 穿過第二層矩陣乘法(主幹,用 W⊤vW^\top v):
∂J∂a[1]=W[2]⊤⋅2=(64).\frac{\partial J}{\partial a^{[1]}} = W^{[2]\top}\cdot 2 = \begin{pmatrix}6\\4\end{pmatrix}.
  1. 穿過 ReLU(主幹,用 σ′(z)⊙v\sigma'(z)\odot v)。z[1]=(−1,3)z^{[1]}=(-1,3),所以 σ′(z[1])=(0,1)\sigma'(z^{[1]})=(0,1):
∂J∂z[1]=(01)⊙(64)=(04).\frac{\partial J}{\partial z^{[1]}} = \begin{pmatrix}0\\1\end{pmatrix}\odot\begin{pmatrix}6\\4\end{pmatrix}=\begin{pmatrix}0\\4\end{pmatrix}.
  1. 第一層參數(支線):
∂J∂W[1]=(04)(12)=(0048),∂J∂b[1]=(04).\frac{\partial J}{\partial W^{[1]}} = \begin{pmatrix}0\\4\end{pmatrix}\begin{pmatrix}1&2\end{pmatrix}=\begin{pmatrix}0&0\\4&8\end{pmatrix},\qquad \frac{\partial J}{\partial b^{[1]}}=\begin{pmatrix}0\\4\end{pmatrix}.

訓練只需要到這裡。如果還想要對輸入的梯度,再用一次 W⊤vW^\top v:∂J∂x=W[1]⊤(0,4)⊤=(8,4)⊤\frac{\partial J}{\partial x}=W^{[1]\top}(0,4)^\top=(8,4)^\top。

驗算。 挑 W22[1]W^{[1]}_{22} 用差分檢查:把它從 1 改成 1.001,z2[1]=2+2.002−1=3.002z^{[1]}_2 = 2+2.002-1=3.002,z[2]=2⋅3.002+1=7.004z^{[2]}=2\cdot3.002+1=7.004,J=12(2.004)2=2.008008J=\frac12(2.004)^2=2.008008。損失增加 0.008008,除以 0.001 得 8.008,跟反向傳播給的 8 相符(差的 0.008 是二階項)。我另外用 NumPy 對 W[1]W^{[1]} 四個元素都做了差分,結果是 (0,0;4,8)(0,0;4,8)。

這些數字代表什麼。

  • W[1]W^{[1]} 的第一列梯度全是 0。第一個隱藏單元的 z1[1]=−1z^{[1]}_1=-1 被 ReLU 截成 0,往回傳的梯度在第 4 步被乘上 0。在這筆樣本上,不管怎麼微調第一列的權重,損失都不會變。
  • ∂J∂W[2]\frac{\partial J}{\partial W^{[2]}} 的第一個元素也是 0,原因不同:那條邊的輸入端 a1[1]=0a^{[1]}_1=0。這正是「輸出端梯度乘輸入端數值」的規則,兩端任何一個是 0,那條邊就沒有梯度。
  • ∂J∂W[1]\frac{\partial J}{\partial W^{[1]}} 的兩列分別是 (1,2)(1,2) 的 0 倍與 4 倍,秩是 1,跟課堂說的一致。
  • 用學習率 η=0.01\eta=0.01 把四組參數都照梯度更新一步,重新做前向得到 z[2]=6.3344z^{[2]}=6.3344、J≈0.890J\approx0.890,損失從 2 降到 0.89。
  • 成本:前向做了兩次矩陣乘向量、一次 ReLU、一次損失;反向做了一次轉置矩陣乘向量(要 ∂J/∂x\partial J/\partial x 的話是兩次)、兩次外積、一次逐元素乘。兩邊是同一個量級。

自我檢查

1. g:R3→R2g:\mathbb{R}^3\to\mathbb{R}^2,J=f(g(z))J=f(g(z))。backward 函數裡那個矩陣是幾乘幾?第 (3,1)(3,1) 個元素是什麼?

答:3×23\times2。它把 2 維的 ∂J∂u\frac{\partial J}{\partial u} 變成 3 維的 ∂J∂z\frac{\partial J}{\partial z}。第 (3,1)(3,1) 個元素是 ∂g1∂z3\frac{\partial g_1}{\partial z_3},列對應輸入、行對應輸出,跟一般的 Jacobian 相反。

2. W=(120−103)W=\begin{pmatrix}1&2&0\\-1&0&3\end{pmatrix},z=(1,1,1)⊤z=(1,1,1)^\top,已知 ∂J∂u=(2,1)⊤\frac{\partial J}{\partial u}=(2,1)^\top,其中 u=Wz+bu=Wz+b。求 ∂J∂z\frac{\partial J}{\partial z}、∂J∂W\frac{\partial J}{\partial W}、∂J∂b\frac{\partial J}{\partial b}。

答:∂J∂z=W⊤(2,1)⊤=(2−1,  4+0,  0+3)⊤=(1,4,3)⊤\frac{\partial J}{\partial z}=W^\top(2,1)^\top=(2-1,\;4+0,\;0+3)^\top=(1,4,3)^\top。∂J∂W=(2,1)⊤(1,1,1)=(222111)\frac{\partial J}{\partial W}=(2,1)^\top(1,1,1)=\begin{pmatrix}2&2&2\\1&1&1\end{pmatrix}。∂J∂b=(2,1)⊤\frac{\partial J}{\partial b}=(2,1)^\top。注意算 ∂J∂z\frac{\partial J}{\partial z} 完全沒用到 zz 的值,算 ∂J∂W\frac{\partial J}{\partial W} 完全沒用到 WW 的值。

3. 把上面例子的 b[1]b^{[1]} 改成 (2,−1)⊤(2,-1)^\top,其餘不變,重算所有梯度。

答:z[1]=(1,3)z^{[1]}=(1,3),a[1]=(1,3)a^{[1]}=(1,3),z[2]=3+6+1=10z^{[2]}=3+6+1=10,J=12.5J=12.5。∂J∂z[2]=5\frac{\partial J}{\partial z^{[2]}}=5;∂J∂W[2]=(5,15)\frac{\partial J}{\partial W^{[2]}}=(5,15),∂J∂b[2]=5\frac{\partial J}{\partial b^{[2]}}=5;∂J∂a[1]=(15,10)\frac{\partial J}{\partial a^{[1]}}=(15,10);兩個 ReLU 都開著,∂J∂z[1]=(15,10)\frac{\partial J}{\partial z^{[1]}}=(15,10);∂J∂W[1]=(15301020)\frac{\partial J}{\partial W^{[1]}}=\begin{pmatrix}15&30\\10&20\end{pmatrix},∂J∂b[1]=(15,10)\frac{\partial J}{\partial b^{[1]}}=(15,10)。第一列不再是 0,但矩陣仍是秩 1。

4. 前向算出的中間值,反向時哪些一定要留著?以 MLP 為例逐一說明。

判準:要能說出每個 backward 函數各自需要什麼。矩陣乘法對輸入的 backward 需要 WW(參數本來就在);對權重的 backward 需要該層的輸入 zz(MLP 裡是前一層的 aa);啟動函數的 backward 需要它自己的輸入 zz 來算 σ′(z)\sigma'(z)。所以每一層的 z[k]z^{[k]} 與 a[k]a^{[k]} 都要存到該層的反向做完為止,這是訓練比推論吃記憶體的原因之一。

5. Hessian-vector product 可以在 O(N)O(N) 算出來,為什麼完整的 Hessian 不行?

判準:要提到輸出大小。∇2f(x) v\nabla^2 f(x)\,v 是 ℓ\ell 維向量,而且它是純量函數 g(x)=⟨∇f(x),v⟩g(x)=\langle\nabla f(x),v\rangle 的梯度,可以套定理。完整 Hessian 有 ℓ2\ell^2 個元素,光寫出來就要 ℓ2\ell^2 的時間;要用這個技巧湊出整個 Hessian,得對 ℓ\ell 個基底向量各做一次,每次 O(N+ℓ)O(N+\ell),總共 O((N+ℓ) ℓ)O((N+\ell)\,\ell);在 N≥ℓN\ge\ell 的常見情況就是 O(Nℓ)O(N\ell),等於 ℓ\ell 次前向。

對應閱讀

主講義第 7 章 7.4 節 Backpropagation,約第 99–111 頁:

  • 定理與兩個推論:7.4 節開頭(Theorem 7.4.1)
  • 鏈鎖律與 B[g,z]\mathcal{B}[g,z] 記號:7.4.1 節,式 7.56–7.63
  • 串接模組、對中間變數與對參數兩種計算:7.4.2 節,式 (B1)、(B2) 與圖 7.5
  • 矩陣乘法與啟動函數的 backward 函數:7.4.3 節,式 7.66–7.73

課堂與講義的差異:

  • 課堂沒講到的。 損失函數的 backward(平方損失、logistic loss、cross-entropy,式 7.74–7.75)、7.4.4 節把所有模組串成 MLP 完整演算法的 Algorithm 3,以及 bias 梯度的推導,課堂都沒有上板。本文的手算例子用到這三塊,是依講義補的。7.5 節的跨樣本向量化這堂也沒有講。
  • 課堂多講的。 Hebbian rule 的記法、單筆樣本的權重梯度是秩 1、平行計算與記憶體釋放的系統觀點、meta learning 與 test-time training 的用途、課末關於啟動函數與 gating 的問答,講義 7.4 節沒有或只帶過。
  • 細節不同。 Hessian-vector product 的成本,課堂說 O(N)O(N),講義寫 O(N+ℓ)O(N+\ell)。講義還有一個註腳說明輸出不依賴的輸入座標梯度預設為 0,所以 N≤ℓN\le\ell 時定理仍成立。
  • granularity。 講義 Remark 7.4.3 說明定理的嚴格證明是把網路拆到最小的加法、乘法層級;實作上用矩陣乘法這種較大的模組比較方便,成本結論不變。課堂最後的成本核對對應這段。

系列導覽:上一堂 第 7 堂:神經網路的架構|下一堂 第 9 堂:k-means 與高斯混合模型

課程影片來源

原始影片:Stanford CS229 Machine Learning | Spring 2026 | Lecture 8: Neural Networks 2 (Backprop)

查核日期:2026-10-12。

內容核對:已依字幕核對(2026-10-12):整份英文字幕(約 62 分鐘)讀完,各節時間區段與引句都出自字幕。沒有看板書畫面,板書上的公式依主講義 7.4 節還原;字幕中索引字母混用或聽不清楚的地方(鏈鎖律的 ii、jj,以及問答裡的最佳化器與啟動函數名稱)以講義為準或略過不寫。

參考資料