那道牆:你付不起的梯度
本階到目前為止的每個方法,都倚賴一個無聲的假設:你能在當前點算出目標函數的梯度。最陡下降法 需要完整的梯度來挑選它的 下降方向;牛頓法 還要再加上梯度與 Hessian。對前幾篇指南裡那些平滑的小測試函數,這幾乎不花成本。但一踏進機器學習,這筆帳就爆炸了。
原因在此。訓練一個模型,意思是把一個「在資料上取平均」的損失最小化:L(w) = (1/N) * 對所有 i 的 loss_i(w) 求和,其中 w 是模型的參數,每個 loss_i 衡量模型在第 i 筆訓練樣本上錯得多離譜。因此完整的梯度也是一個平均:grad L(w) = (1/N) * 對所有 i 的 grad loss_i(w) 求和。要取「一次」這樣的梯度,你得讓全部 N 筆樣本都通過模型再反向傳播一遍。當 N = 10^7 筆樣本(以現代標準算小的)、又有數百萬個參數時,單單一次完整梯度就是一場巨大的計算——而梯度下降想要上千次。
於是你已精通的那些方法,撞上一道殘酷的擴展之牆。牛頓法雙重出局:在數百萬參數上形成並分解 Hessian,以參數數量計是 O(n^3),根本連存都存不下。純粹的完整梯度下降只是慢到不可能、而非邏輯上的不可能,但「慢到不可能」仍意味著一個沒人訓練得起的模型。我們需要一個新念頭,而它來自於看清那個平均究竟是什麼。
核心念頭:用極小的樣本估計梯度
完整梯度是對 N 筆樣本取的平均。而平均,正是那種你能「用一小撮隨機樣本去估計」的東西——這背後的統計直覺,和民調、或積分那一階的蒙地卡羅積分如出一轍。隨機抽幾筆樣本,只把它們的梯度取平均,你就得到一個有雜訊、但不偏的真實梯度猜測。它平均而言指向同一個方向,只是會晃動。隨機梯度下降(SGD) 不過就是在這些便宜、有雜訊的估計上跑梯度下降而已。
在它最純粹的形式裡,你每一步用「一筆」隨機樣本。更新式是 w_{n+1} = w_n - eta * grad loss_i(w_n),其中 i 是新抽出的一個索引、eta 是步長(在機器學習裡稱為 學習率)。注意它的成本:不是碰遍全部 N 筆樣本才走一步,而是碰「一筆」樣本就走一步。在完整方法謹慎地踏出單獨一大步的時間裡,SGD 已經踏出了一百萬個潦草的小步。即使每一步的方向都略有偏差,一百萬個略偏的步,仍勝過一個你根本算不起的完美步。
小批次:實務上的折衷
每步一筆樣本,雜訊很大;每步全部 N 筆,又很慢。主力落在兩者之間:小批次。每一步隨機抽出一個小批次,比方說 B = 32 或 256 筆樣本,並平均它們的梯度。這仍然是 SGD——估計依舊有雜訊、依舊不偏——但雜訊變小了。關鍵的統計事實,就是蒙地卡羅那條 1/sqrt(N) 定律:平均 B 個獨立樣本,會把估計的標準差砍掉 sqrt(B) 倍。所以一個 256 的批次,梯度雜訊約比單筆樣本小 16 倍,代價是 256 倍的工作量。
這個取捨——雜訊少 16 倍、工作多 256 倍——紙面上看很糟,那為何小批次卻無所不在?因為硬體,而非數學。GPU 處理 256 筆樣本的批次,幾乎和處理單筆一樣快:工作是一個大型矩陣乘法,而晶片正是為了平行做這些而打造的。如同你在基礎那一階所見,真實的核心運算往往受限於記憶體流量、而非純粹的浮點運算數——而一個肥厚的批次會讓載入的模型權重在全部 B 筆樣本間重複使用,把受記憶體限制的涓流,變成受計算限制的洪流。因此批次大小是針對機器調校的:大到足以餵飽硬體,又小到讓每一步仍便宜、仍有足夠雜訊去探索。
given parameters w, learning rate eta, batch size B:
shuffle the N training examples
repeat for each epoch (one full pass over the data):
for each mini-batch of B examples:
g = (1/B) * sum of grad loss_i(w) over the batch # noisy gradient
w = w - eta * g # one cheap step
(optionally decay eta after each epoch)動量:借來治療曲折的解藥
回想第 2 篇指南的曲折問題:在一個病態的目標上——一道又長又窄的山谷——純粹的最陡下降會在窄壁間彈來彈去,同時沿著谷底緩慢爬行,而它的速度被 Hessian 的 條件數 給扼住。SGD 不只承襲了這個毛病,還在上頭額外添了梯度雜訊,所以它原始的路徑更加抖動。經典而便宜的解藥是 動量:不再只沿著當前那個有雜訊的梯度走,而是維持一個近期梯度的滑動平均,並沿著它走。
其運作是兩行更新。維持一個速度 v,把新梯度摻進它裡頭,再沿著速度走:v_{n+1} = mu * v_n + grad,接著 w_{n+1} = w_n - eta * v_{n+1},其中 mu 是個像 0.9 的數。這個物理圖像精確且值得記牢:你不再是一個瞬間服從梯度的無重量質點,而是一顆滾下坡的重球。在窄的方向上,連續的梯度來回相反,在平均中互相抵消,於是振盪被阻尼掉。在沿谷的方向上,連續的梯度全指向同一邊而「累加」起來,於是球加速前進。動量悄悄地一次修好了曲折的兩半。
在隨機的情境裡還有第二份、更微妙的禮物。由於動量平均了許多近期的梯度,它也把一大塊 SGD 雜訊給平均掉了——那個滑動的速度,比任何單一小批次梯度都更平滑。所以動量身兼兩職:它馴服了確定性的病態,「同時」也安撫了隨機的抖動,代價只是多存一個與 w 同樣大小的向量。這是一筆極為划算的交易,也正是為何幾乎沒有人跑光禿禿的 SGD。
Adam:每個參數各自的學習率
大多數人實際在跑的主力,還多了一層——Adam 最佳化器。回想牛頓法為何如此強大:它藉由 Hessian 給每個方向各自的縮放,把一道又長又窄的山谷重新拉回成一個圓碗。Adam 在從不形成 Hessian 的情況下,去攫取那份好處的一個影子。它為「每一個」參數各自維持一個近期平方梯度的滑動平均——一個便宜的、對角的估計,說明那個座標一直以來有多陡——並把每個參數的步幅除以那個量的平方根。
其效果是一個「逐座標自適應」的步長,把 步長的選擇 變成自動且個別化的。那些梯度一向又大又飄忽的參數,得到小而謹慎的步;那些梯度一向又小又平穩的參數,得到大而自信的步。把這個與動量式對梯度本身的平均結合起來,你就得到 Adam:一個梯度的滑動平均(管方向,動量的部分)加一個平方梯度的滑動平均(管每個參數的尺度)。它不是牛頓法——它只用到一條對角線,從不觸及 Hessian 裡真正的交叉耦合——但它擷取了牛頓藥方裡便宜的那部分,幾乎不索取任何回報。
為何一階方法勝出
退一步,看看這奇異的翻轉。在前幾篇指南裡,最聰明的方法是用了最多資訊的那個:牛頓法挾其完整曲率,輕鬆擊敗純粹的梯度下降。像 BFGS 這樣的 擬牛頓 方法則是絕妙的折衷,從梯度的歷史去近似那份曲率。然而在現代機器學習的規模下,這個層級秩序顛倒了。勝出的方法是「最笨」的那個——一階、有雜訊、對曲率全盲的 SGD——恰恰因為它是唯一便宜到跑得起的。
這是一個關於「大規模計算」的深刻教訓,而非機器學習的怪癖。當 N 巨大時,對的貨幣不是「每步的準確度」,而是「每單位計算量的進展」。一個你能走十億次的便宜潦草步,勝過一個你只能走一百次的精確昂貴步。同樣的邏輯驅動了迭代求解器那一階——便宜地迭代、而非精確地分解——這也正是為何二階曲率這顆小規模最佳化的皇冠寶石,會被擋在大規模學習的門外。而雜訊甚至不純然是成本:它幫助迭代點跳過淺鞍點與壞的局部最低點,那些是確定性方法可能會卡住的地方。
還剩一條線。這裡的一切都是「自由地」最佳化——w 可以是任何東西。但真實問題往往帶著約束:必須維持非負的權重、不可超出的預算、必須加總為一的機率。SGD 本身對牆壁一無所知。本階最後一篇指南,正是要接下這個問題——拉格朗日乘子 與 KKT 條件——並展示最佳化如何學會尊重一條邊界。你在這裡遇見的那個謙遜、可擴展的 演算法 是引擎;下一篇指南則為它築起圍籬。