搜尋課程與筆記

搜尋筆記

輸入主題、關鍵字或單元編號,例如 Flow Matching、U4。

    按 Esc 關閉 · ⌘ / Ctrl K 開啟搜尋

    L0.3 14 分鐘閱讀 2026年10月

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

    本篇重用2 篇

    霧裡下山

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

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

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

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

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

    方向只告訴我們局部怎麼下降,還沒回答步長。能不能先算出,這一步多大才不會把下降變成上升? 我們先用正定二次目標推導一條明確門檻,再分清這個結論能否用在一般神經網路。

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

    課堂提問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,一階項是 −η∥∇L∥2≤0-\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}。

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

    同一個碗,三種步伐

    圖說: 最陡方向限制步長,最平方向決定步數;發散在 log 損失圖上是一條往上的直線。

    一步走多大會開始發散

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

    L(w)=12n∥Vw−y∥2,∇L(w)=1nV⊤(Vw−y),∇2L=1nV⊤V  (常數).L(w)=\frac1{2n}\lVert Vw-y\rVert^2, \qquad \nabla L(w)=\frac{1}{n}V^\top(Vw-y), \qquad \nabla^2 L=\frac{1}{n}V^\top V\ \ (\text{常數}).

    這裡用半個 MSE,與下面程式的梯度一致;若改回完整 MSE,同一 η\eta 的更新會大兩倍。令 H=V⊤V/nH=V^\top V/n,先假設 full column rank,所以 0<λmin⁡≤λmax⁡0<\lambda_{\min}\le\lambda_{\max},最小點唯一。從 ek=wk−w⋆e_k=w_k-w^\star 與 ∇L(wk)=Hek\nabla L(w_k)=He_k 出發,投影到第 jj 個 eigenvector 就得到

    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.04。20002000 步之後,1.042000≈10341.04^{2000}\approx10^{34}。損失是誤差的平方,於是你看到 106810^{68} 甚至更大——實測是 6.6×101226.6\times10^{122}。

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

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

    門檻不是建議值,是懸崖

    這一切依賴什麼。 一,損失對參數可微(所以 00–11 錯誤率不能直接用,得換成 差一度與差十度:損失函數是一種宣告 裡的替代損失)。二,二次分析要求 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

    這次實驗中,大於門檻的步長使誤差放大;幾個較大的安全步長在 2000 步時表現較好。但即使是二次目標,也不是門檻以下越大越快:跨過最佳步長後,最陡方向的震盪會變慢。條件數很大則表示要在所有方向都收斂,可能需要很多步;不是增加步數在數學上完全無效。

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

    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

    可用的學習率縮小了二十個數量級。 因為 t5≈1010t^5\approx10^{10},那一欄的尺度支配了整個 Hessian。η=0.01\eta=0.01 直接 NaN;勉強能跑的 10−2410^{-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^3,20002000 步就走到 0.01550.0155——距離直接解的 0.01470.0147 只差一點點,比原始尺度的 0.04150.0415 好得多。之後遇到 BatchNorm、LayerNorm、殘差連接時,它們的第一層動機都是同一件事——讓碗變圓。

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

    拖學習率,看軌跡與門檻

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

    檢查理解

    想一想

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

    想一想

    在門檻以下的實驗裡,η\eta 從 0.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} 與學習率上限的關係,以及為什麼這些「小技巧」其實是幾何。)