L0.3 霧中下山,一步該走多大:梯度下降的一頁
本篇重用M0.0霧中下山:Gradient 與方向·M0.2導航的三十秒:Taylor 展開與「假設直線」
起點:霧裡下山
霧中下山:Gradient 與方向 那一篇的情境:你在山上,濃霧,能見度一公尺。你只能量出腳下地面往哪個方向最陡。你想下到谷底。
策略很自然:往最陡的下坡方向走一步,停下來,重新量,再走一步。
真正的問題不是方向,是一步走多大。
- 走 10 公分:方向幾乎不會過期,但要走一整天。
- 走 50 公尺:省時間,但這 50 公尺裡地形早就變了——你可能衝過谷底,衝上對面的山坡。
- 走 500 公尺:你會在兩側山壁之間來回彈跳,每次都比上次更高。
第三種情況值得注意:它不是「慢一點」,是往反方向去。 而且一旦開始,會越來越快。
這一篇把這件事變成一個可以先算出來的數字:對一個給定的問題,存在一個門檻 ,低於它收斂、高於它發散,中間沒有灰色地帶。實測會看到門檻兩側是 與 。
往負梯度走一步——這一步該走多大?
走太大只是慢一點,
還是會往反方向去?
課堂提問Q1
把「往最陡的下坡走一步」翻成數學:更新規則怎麼寫?「這個方向會讓損失下降」這個保證從哪裡來、只在什麼範圍內成立?「步伐太大」在式子裡是哪個量太大?
先想一想,再展開看整理後的答案
更新規則。 參數 ,目標 ,
下降的保證來自 Taylor。 在 附近展開(導航的三十秒:Taylor 展開與「假設直線」):
取 ,一階項是 ——保證下降。但這是一階的保證:它只在二階項還小的時候有效。 一大, 那一項就翻身,把一階的下降吃掉還有剩。
「太大」是哪個量。 二階項的係數是 Hessian 。所以「安全的步伐」不是絕對的長度,而是相對於曲率的長度:曲率大的方向(山壁陡峭)能走的步伐小,曲率小的方向(谷底平緩)能走的步伐大。麻煩在於只有一個 要同時服務所有方向——最陡的那個方向決定你的上限,最平的那個方向決定你要走多久。這兩個數的比值就是條件數,它是這一篇真正的主角。
山谷的形狀決定一切
把損失曲面想成一個橢圓形的碗。碗在某些方向很陡(截面窄),某些方向很平(截面長)。
- 由最陡的方向決定上限:一旦在那個方向上一步跨過谷底、而且跨得比原來還遠,那個方向的誤差就開始每步放大。
- 走完全程的時間由最平的方向決定:那個方向上梯度很小,每一步只挪一點點。
於是你被夾住: 不能大於最陡方向允許的值,但你需要的步數由最平方向決定。碗越是又長又窄,這個夾擊越痛苦。 「又長又窄」的量化就是條件數 。
實務上讓碗變窄的最常見原因既不是模型也不是資料量,而是特徵的尺度不一致:一個特徵是「坪數」(–),另一個是「房間數」(–),第三個是「屋齡」(–)。同樣把權重動一單位,對損失的影響差了幾十倍——碗就被拉成細長的溝。這也是為什麼「標準化特徵」是幾乎所有教學裡的第一步,卻很少解釋為什麼:它在改碗的形狀,不是在美化數字。
一步走多大會開始發散
取最乾淨的情形:線性模型 、平方損失。令 是設計矩陣(第 列是 ),
Hessian 是常數,所以這個碗就是二次的,沒有高階項要擔心。令 ,其特徵值 ,最小點 。把誤差 投影到 的特徵向量上,第 個分量的遞迴是
每一步就是乘上一個常數。 於是一切都清楚了:
而收斂速度由最慢的那個分量決定,也就是 那個方向。在最佳的 下,每步的收縮率是
(正圓的碗):一步到位。(下面實測會遇到):收縮率是 ——每一步只前進千萬分之二。 這就是為什麼一個「有在收斂」的訓練仍然可以慢到沒有用。
注意為什麼發散是指數的,而且看起來很突然
跨過門檻時,某個方向的乘數 從 變成 。 步之後,。損失是誤差的平方,於是你看到 甚至更大——實測是 。
這解釋了兩個實務現象。一,發散通常表現成「訓練了幾百步都好好的,然後突然 NaN」:指數成長在前期看不出來,直到超過浮點數上限。二,門檻附近沒有安全邊際: 從 調到 (不到 )就從收斂變成炸掉。所以實務建議是「先找會炸的 ,再退回三到十倍」,而不是在門檻邊緣求極限效能。
非二次的目標(真正的神經網路)沒有常數 Hessian, 會隨著訓練變動,於是門檻是會移動的——一個一開始安全的 可能在訓練中途變得不安全。這是 warmup 與學習率 schedule 的動機之一,屬於 一個 η 不夠用:動量與 Adam 各買到什麼。
回到情境:門檻不是建議值,是懸崖
這一切依賴什麼。 一,損失對參數可微(所以 – 錯誤率不能直接用,得換成 差一度與差十度:損失函數是一種宣告 裡的替代損失)。二,二次分析要求 Hessian 固定;一般模型只在局部近似成立,因此門檻是局部的。三,這裡算的是全批次梯度——每一步都用上全部資料。實務上用 minibatch,梯度帶噪聲,穩定性的討論要修改,那是 民調只問一千人:小批次與噪聲。
一個常見的誤解值得先破。 「學習率調小總是比較安全」在穩定性上對,在實務上錯: 大的時候,太小的 會讓你在預算內根本走不到有用的地方——下面 跑 2000 步的訓練誤差 ,比只用一條直線擬合還差。
沒收斂與發散一樣是失敗,只是它不會給你 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 步後仍然是 而不是 ——被條件數 卡住。加步數也救不了:以那個收縮率,要靠近直接解需要的步數是天文數字。
刻意違反:只換單位。 同一個問題,把 從「 到 」改成「民國 到 年」——這在真實資料裡是完全正常的事,沒有人會覺得自己做了什麼:
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
可用的學習率縮小了二十個數量級。 因為 ,那一欄的尺度支配了整個 Hessian。 直接 NaN;勉強能跑的 在 2000 步後幾乎沒有移動(,比直線還差)。模型沒變、資料沒變、目標沒變,只是換了一個單位,訓練就完全壞掉。
處方是把每一欄拉回同一個尺度:
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
條件數從 掉到 , 步就走到 ——距離直接解的 只差一點點,比原始尺度的 好得多。之後遇到 BatchNorm、LayerNorm、殘差連接時,它們的第一層動機都是同一件事——讓碗變圓。
標準化不是清潔工作,它是在改變最佳化問題的幾何。
拖學習率,看軌跡與門檻
互動 demo:拉學習率越過門檻就發散,而那個門檻由最陡的方向決定;收斂要等最平的方向。
先消化一下
參考文獻
- Boyd, S., Vandenberghe, L. Convex Optimization. Cambridge University Press 2004, Ch. 9.(梯度法的收斂分析、條件數與收縮率、步長選擇。)
- Nocedal, J., Wright, S. Numerical Optimization, 2nd ed. Springer 2006, Ch. 3.(線搜尋、步長條件,以及為什麼固定步長是特例。)
- LeCun, Y., Bottou, L., Orr, G., Müller, K.-R. Efficient BackProp. Neural Networks: Tricks of the Trade, Springer 1998.(輸入標準化、 與學習率上限的關係,以及為什麼這些「小技巧」其實是幾何。)