L0.3 15 分鐘閱讀 2026年9月

L0.3 霧中下山,一步該走多大:梯度下降的一頁

本篇重用M0.0霧中下山:Gradient 與方向·M0.2導航的三十秒:Taylor 展開與「假設直線」

起點:霧裡下山

霧中下山:Gradient 與方向 那一篇的情境:你在山上,濃霧,能見度一公尺。你只能量出腳下地面往哪個方向最陡。你想下到谷底。

策略很自然:往最陡的下坡方向走一步,停下來,重新量,再走一步。

真正的問題不是方向,是一步走多大

  • 走 10 公分:方向幾乎不會過期,但要走一整天。
  • 走 50 公尺:省時間,但這 50 公尺裡地形早就變了——你可能衝過谷底,衝上對面的山坡。
  • 走 500 公尺:你會在兩側山壁之間來回彈跳,每次都比上次更高。

第三種情況值得注意:它不是「慢一點」,是往反方向去。 而且一旦開始,會越來越快。

這一篇把這件事變成一個可以先算出來的數字:對一個給定的問題,存在一個門檻 η\eta^\star,低於它收斂、高於它發散,中間沒有灰色地帶。實測會看到門檻兩側是 0.04150.04156.6×101226.6\times10^{122}

往負梯度走一步——這一步該走多大?
走太大只是慢一點,
還是會往反方向去?

課堂提問Q1

把「往最陡的下坡走一步」翻成數學:更新規則怎麼寫?「這個方向會讓損失下降」這個保證從哪裡來、只在什麼範圍內成立?「步伐太大」在式子裡是哪個量太大?

先想一想,再展開看整理後的答案

更新規則。 參數 θRp\theta\in\mathbb R^p,目標 L(θ)=R^n(fθ)L(\theta)=\widehat R_n(f_\theta)

θk+1=θkηL(θk).\theta_{k+1}=\theta_k-\eta\,\nabla L(\theta_k).

下降的保證來自 Taylor。θk\theta_k 附近展開(導航的三十秒:Taylor 展開與「假設直線」):

L(θk+Δ)=L(θk)+L(θk)Δ+O(Δ2).L(\theta_k+\Delta)=L(\theta_k)+\nabla L(\theta_k)^\top\Delta+O(\lVert\Delta\rVert^2).

Δ=ηL\Delta=-\eta\nabla L,一階項是 ηL20-\eta\lVert\nabla L\rVert^2\le0——保證下降。但這是一階的保證:它只在二階項還小的時候有效。η\eta 一大,O(Δ2)O(\lVert\Delta\rVert^2) 那一項就翻身,把一階的下降吃掉還有剩。

「太大」是哪個量。 二階項的係數是 Hessian 2L\nabla^2L。所以「安全的步伐」不是絕對的長度,而是相對於曲率的長度:曲率大的方向(山壁陡峭)能走的步伐小,曲率小的方向(谷底平緩)能走的步伐大。麻煩在於只有一個 η\eta 要同時服務所有方向——最陡的那個方向決定你的上限,最平的那個方向決定你要走多久。這兩個數的比值就是條件數,它是這一篇真正的主角。

山谷的形狀決定一切

把損失曲面想成一個橢圓形的碗。碗在某些方向很陡(截面窄),某些方向很平(截面長)。

  • η\eta最陡的方向決定上限:一旦在那個方向上一步跨過谷底、而且跨得比原來還遠,那個方向的誤差就開始每步放大。
  • 走完全程的時間由最平的方向決定:那個方向上梯度很小,每一步只挪一點點。

於是你被夾住:η\eta 不能大於最陡方向允許的值,但你需要的步數由最平方向決定。碗越是又長又窄,這個夾擊越痛苦。 「又長又窄」的量化就是條件數 κ=λmax/λmin\kappa=\lambda_{\max}/\lambda_{\min}

實務上讓碗變窄的最常見原因既不是模型也不是資料量,而是特徵的尺度不一致:一個特徵是「坪數」(10108080),另一個是「房間數」(1155),第三個是「屋齡」(004040)。同樣把權重動一單位,對損失的影響差了幾十倍——碗就被拉成細長的溝。這也是為什麼「標準化特徵」是幾乎所有教學裡的第一步,卻很少解釋為什麼:它在改碗的形狀,不是在美化數字。

一步走多大會開始發散

取最乾淨的情形:線性模型 fw(x)=ϕ(x)wf_w(x)=\phi(x)^\top w、平方損失。令 VV 是設計矩陣(第 ii 列是 ϕ(xi)\phi(x_i)^\top),

L(w)=1nVwy2,L(w)=2nV(Vwy),2L=2nVV  (常數).L(w)=\frac1n\lVert Vw-y\rVert^2, \qquad \nabla L(w)=\frac{2}{n}V^\top(Vw-y), \qquad \nabla^2 L=\frac{2}{n}V^\top V\ \ (\text{常數}).

Hessian 是常數,所以這個碗就是二次的,沒有高階項要擔心。令 H=1nVVH=\frac1nV^\top V,其特徵值 0λminλmax0\le\lambda_{\min}\le\dots\le\lambda_{\max},最小點 ww^\star。把誤差 ek=wkwe_k=w_k-w^\star 投影到 HH 的特徵向量上,第 jj 個分量的遞迴是

ek+1(j)=(1ηλj)ek(j).e_{k+1}^{(j)}=(1-\eta\lambda_j)\,e_k^{(j)}.

每一步就是乘上一個常數。 於是一切都清楚了:

收斂    1ηλj<1  j     0<η<2λmax \text{收斂}\iff |1-\eta\lambda_j|<1\ \ \forall j \iff \boxed{\ 0<\eta<\frac{2}{\lambda_{\max}}\ }

而收斂速度由最慢的那個分量決定,也就是 λmin\lambda_{\min} 那個方向。在最佳的 η=2λmax+λmin\eta=\frac{2}{\lambda_{\max}+\lambda_{\min}} 下,每步的收縮率是

κ1κ+1,κ=λmaxλmin.\frac{\kappa-1}{\kappa+1},\qquad \kappa=\frac{\lambda_{\max}}{\lambda_{\min}}.

κ=1\kappa=1(正圓的碗):一步到位。κ=107\kappa=10^7(下面實測會遇到):收縮率是 0.99999980.9999998——每一步只前進千萬分之二。 這就是為什麼一個「有在收斂」的訓練仍然可以慢到沒有用。

注意為什麼發散是指數的,而且看起來很突然

跨過門檻時,某個方向的乘數 1ηλmax|1-\eta\lambda_{\max}|0.990.99 變成 1.041.0420002000 步之後,1.04200010341.04^{2000}\approx10^{34}。損失是誤差的平方,於是你看到 106810^{68} 甚至更大——實測是 6.6×101226.6\times10^{122}

這解釋了兩個實務現象。一,發散通常表現成「訓練了幾百步都好好的,然後突然 NaN」:指數成長在前期看不出來,直到超過浮點數上限。二,門檻附近沒有安全邊際η\eta1.051.05 調到 1.151.15(不到 10%10\%)就從收斂變成炸掉。所以實務建議是「先找會炸的 η\eta,再退回三到十倍」,而不是在門檻邊緣求極限效能。

非二次的目標(真正的神經網路)沒有常數 Hessian,λmax\lambda_{\max} 會隨著訓練變動,於是門檻是會移動的——一個一開始安全的 η\eta 可能在訓練中途變得不安全。這是 warmup 與學習率 schedule 的動機之一,屬於 一個 η 不夠用:動量與 Adam 各買到什麼

回到情境:門檻不是建議值,是懸崖

這一切依賴什麼。 一,損失對參數可微(所以 0011 錯誤率不能直接用,得換成 差一度與差十度:損失函數是一種宣告 裡的替代損失)。二,二次分析要求 Hessian 固定;一般模型只在局部近似成立,因此門檻是局部的。三,這裡算的是全批次梯度——每一步都用上全部資料。實務上用 minibatch,梯度帶噪聲,穩定性的討論要修改,那是 民調只問一千人:小批次與噪聲

一個常見的誤解值得先破。 「學習率調小總是比較安全」在穩定性上對,在實務上錯:κ\kappa 大的時候,太小的 η\eta 會讓你在預算內根本走不到有用的地方——下面 η=0.01\eta=0.01 跑 2000 步的訓練誤差 0.2090.209,比只用一條直線擬合還差。

沒收斂與發散一樣是失敗,只是它不會給你 NaN 這種明顯的訊號。

回到情境:把門檻跑出來

用共用 toy 與五次多項式特徵。這個問題有直接解,所以我們知道終點在哪裡:

import numpy as np
from ml_toy import toy_1d

x, y = toy_1d()
V = np.vander(x, 6, increasing=True)                 # 1, x, …, x^5
print(f"直接解的訓練 MSE = {((V@np.linalg.lstsq(V, y, rcond=None)[0] - y)**2).mean():.4f}")

lam = np.linalg.eigvalsh(V.T @ V / len(x))
print(f"λmax = {lam[-1]:.4f}   門檻 η* = 2/λmax = {2/lam[-1]:.3f}   條件數 = {lam[-1]/lam[0]:.1e}")

for eta in (0.01, 0.1, 0.5, 1.05, 1.15):
    w = np.zeros(6)
    for _ in range(2000):
        w -= eta * (V.T @ (V @ w - y)) / len(x)      # 全批次梯度下降
    print(f"  η={eta:<5}  2000 步後訓練 MSE = {((V@w - y)**2).mean():.4e}")
直接解的訓練 MSE = 0.0147
λmax = 1.8034   門檻 η* = 2/λmax = 1.109   條件數 = 1.6e+07

  η=0.01   2000 步後訓練 MSE = 2.0935e-01
  η=0.1    2000 步後訓練 MSE = 1.4498e-01
  η=0.5    2000 步後訓練 MSE = 7.0104e-02
  η=1.05   2000 步後訓練 MSE = 4.1486e-02
  η=1.15   2000 步後訓練 MSE = 6.6166e+122

三件事:門檻是真的1.051.05 收斂、1.151.151012210^{122},理論值 1.1091.109 正好夾在中間);η\eta 在門檻以下一律比較好0.010.51.050.01\to0.5\to1.05 單調變好);但即使用了幾乎最大的合法步伐,2000 步後仍然是 0.04150.0415 而不是 0.01470.0147——被條件數 1.6×1071.6\times10^7 卡住。加步數也救不了:以那個收縮率,要靠近直接解需要的步數是天文數字。

刻意違反:只換單位。 同一個問題,把 xx 從「0011」改成「民國 100100110110 年」——這在真實資料裡是完全正常的事,沒有人會覺得自己做了什麼:

t = 100 + 10*x                                        # 同一份資料,換一個單位
Vt = np.vander(t, 6, increasing=True)
lam_t = np.linalg.eigvalsh(Vt.T @ Vt / len(t))
print(f"λmax = {lam_t[-1]:.3e}   門檻 η* = {2/lam_t[-1]:.3e}")
for eta in (0.01, 1e-12, 1e-24):
    w = np.zeros(6)
    for _ in range(2000):
        w -= eta * (Vt.T @ (Vt @ w - y)) / len(t)
    print(f"  η={eta:<8}  2000 步後訓練 MSE = {((Vt@w - y)**2).mean():.4e}")
λmax = 1.750e+20   門檻 η* = 1.143e-20
  η=0.01      2000 步後訓練 MSE = nan
  η=1e-12     2000 步後訓練 MSE = nan
  η=1e-24     2000 步後訓練 MSE = 3.9465e-01

可用的學習率縮小了二十個數量級。 因為 t51010t^5\approx10^{10},那一欄的尺度支配了整個 Hessian。η=0.01\eta=0.01 直接 NaN;勉強能跑的 102410^{-24} 在 2000 步後幾乎沒有移動(0.390.39,比直線還差)。模型沒變、資料沒變、目標沒變,只是換了一個單位,訓練就完全壞掉。

處方是把每一欄拉回同一個尺度:

Vz = np.vander((t - t.mean())/t.std(), 6, increasing=True)
Vz = Vz / np.linalg.norm(Vz, axis=0) * np.sqrt(len(t))     # 每一欄標準化
lam_z = np.linalg.eigvalsh(Vz.T @ Vz / len(t))
print(f"λmax = {lam_z[-1]:.3f}   η* = {2/lam_z[-1]:.3f}   條件數 = {lam_z[-1]/lam_z[0]:.1e}")
w = np.zeros(6)
for _ in range(2000):
    w -= 0.3 * (Vz.T @ (Vz @ w - y)) / len(t)
print(f"  η=0.3   2000 步後訓練 MSE = {((Vz@w - y)**2).mean():.4f}")
λmax = 3.936   η* = 0.508   條件數 = 1.6e+03
  η=0.3   2000 步後訓練 MSE = 0.0155

條件數從 1.6×1071.6\times10^7 掉到 1.6×1031.6\times10^320002000 步就走到 0.01550.0155——距離直接解的 0.01470.0147 只差一點點,比原始尺度的 0.04150.0415 好得多。之後遇到 BatchNorm、LayerNorm、殘差連接時,它們的第一層動機都是同一件事——讓碗變圓。

標準化不是清潔工作,它是在改變最佳化問題的幾何。

拖學習率,看軌跡與門檻

互動 demo:拉學習率越過門檻就發散,而那個門檻由最陡的方向決定;收斂要等最平的方向。

先消化一下

想一想

訓練跑了 800 步一切正常,第 830 步損失變成 NaN。最可能的原因與最實際的第一個處方是:

想一想

在門檻以下的實驗裡,η\eta0.010.01 加到 1.051.05,訓練誤差從 0.2090.209 降到 0.04150.0415。有人據此主張「學習率越大越好,逼近門檻就對了」。最合適的評論是:

想一想

兩個問題的 λmax\lambda_{\max} 相同,但條件數一個是 1010、一個是 10610^6。關於它們的訓練:

參考文獻

  1. Boyd, S., Vandenberghe, L. Convex Optimization. Cambridge University Press 2004, Ch. 9.(梯度法的收斂分析、條件數與收縮率、步長選擇。)
  2. Nocedal, J., Wright, S. Numerical Optimization, 2nd ed. Springer 2006, Ch. 3.(線搜尋、步長條件,以及為什麼固定步長是特例。)
  3. LeCun, Y., Bottou, L., Orr, G., Müller, K.-R. Efficient BackProp. Neural Networks: Tricks of the Trade, Springer 1998.(輸入標準化、λmax\lambda_{\max} 與學習率上限的關係,以及為什麼這些「小技巧」其實是幾何。)