求導數的三條路——以及前兩條留下的缺口
到目前為止,這一級已經給你看了兩種方法,去微分一個你只能求值的函數,也都對它們提出了警告。用有限差分做數值微分既便宜、又對任何黑箱都管用,但它要繳一筆稅:相減幾乎相等的值,再除以一個微小的 h,就撞上步長取捨,所以一條樸素的中心差分最好也只能達到你那 16 位雙精度裡的約 11 位。另一條古典路線是符號微分:把 f 的公式餵進電腦代數系統,精確套用微分規則,讀出一個閉式的 f'(x)。這給出一個精確的表達式——但它受表達式膨脹之苦:用乘法律去微分一個層層相乘的式子,每多一層,項數大約翻倍,所以一個深度複合函數的符號導數可能膨脹成數百萬項,慢如蝸牛。
於是這裡有個缺口。我們想要符號微分擁有的——一個精確的導數,沒有 h、沒有截斷誤差——但要用數值微分的成本來換,是區區幾個算術運算,而不是一個膨脹的公式。自動微分(AD,有時稱演算法微分)正好填補這個缺口。它的關鍵洞見是:一個計算 f 的程式,仔細看不過就是一長串基本運算——加、乘、sin、exp、sqrt——而我們早已知道每一個的導數。如果我們讓導數資訊伴著數值一起,穿過這每一個步驟,在每一步機械地套用鏈鎖律,就能抵達整個複合函數的精確導數。從頭到尾沒寫出任何公式,也沒取任何極限。
前向模式:讓導數伴著每個值一起前進
認識自動微分最乾淨的方式是前向模式,而實作它最乾淨的方式是對偶數。設想一種算術,其中每個值都帶著第二格:一個數 a 被換成數對 (a, a'),讀作「值 a,帶導數 a'」。把運算定義成讓第二格永遠追蹤導數。加法:(a, a') + (b, b') = (a + b, a' + b')。乘法,依乘法律:(a, a') * (b, b') = (a*b, a'*b + a*b')。對 sine 這類基本函數:sin of (a, a') = (sin a, cos(a) * a'),這正是鏈鎖律。每一條規則,都是微積分裡那條熟悉的求導法則,套用在第二格上。
要在某點 x 微分 f,把輸入播種為 (x, 1)——值 x、導數 dx/dx = 1——再以對偶數算術去跑 f 那段普通的程式碼。常數以 (c, 0) 進入。第一格如往常般精確算出 f(x);第二格則藉著對每個基本步驟做歸納,累積出 df/dx。程式跑完時,輸出的第二格就是 f'(x),準到機器精度。沒有步長要調、沒有抵消、沒有 V 形誤差曲線——纏著第一篇的那場取捨,在這裡根本不會出現。
Differentiate f(x) = x * sin(x) at x = 2, forward mode (dual numbers)
seed: x = (2, 1) # value 2, derivative 1
s = sin(x)
= (sin 2, cos(2) * 1) # chain rule in slot 2
f = x * s
= (2*sin 2, 1*sin 2 + 2*cos 2)
\______/ \_______________/
f(2) f'(2) = sin 2 + 2 cos 2 (exact)反向模式:一次掃過,全部導數一次到手
當只有一個輸入、而你想知道它對一切的影響時,前向模式堪稱完美。但那些重頭戲的應用——擬合一個有成千上萬參數的模型、訓練神經網路、為系統上的牛頓法算梯度——把形狀翻轉了過來:多個輸入、單一純量輸出(一個損失、一個能量)。跑前向模式就意味著每個輸入掃一次,當輸入數以百萬計時這毫無指望。解法是反向模式,它正是機器學習裡所謂的反向傳播那同一套演算法。
反向模式分兩趟進行。前向趟照常跑程式,記下基本運算的圖、並儲存每個中間值(這份紀錄就是「磁帶」或計算圖)。接著反向趟倒著走過那張圖,把敏感度——最終輸出對每個中間量的導數,稱為伴隨量——從輸出往回傳到輸入,沿途乘上每一個局部導數。這套帳目的魔法在於:對所有輸入的梯度,不管有多少個,都從單單一次反向掃過裡掉出來。整個梯度只花求值 f 一次成本的一個小常數倍——與輸入個數無關。正是這一個事實,讓訓練動輒數十億參數的模型成為可能。
誠實的界線:自動微分承諾與不承諾什麼
自動微分是精確的,但要把什麼精確說清楚。自動微分微分的,是你的程式碼所計算的那個精確函數,而非你本意想寫的那個數學函數。由此有兩個推論。第一,自動微分仍是浮點的:它不除以一個微小的 h,因而逃離了第一篇講的捨入地板,但每個運算仍要捨入,所以一段病態計算的導數仍可能丟失位數——自動微分給的是對那些已捨入的中間值的精確鏈鎖律答案。第二,自動微分微分的是你的控制流實際執行過的那條路。在 f 有折點之處(想想 abs(x) 在 0、或一個 if 敘述裡的分支),自動微分回傳它剛好走過的那條分支的導數,那可能是個單側值,而真正的導數在該處並不存在。誠實的總結是:自動微分移除的是截斷誤差,不是條件數。
還有一筆要拿來和有限差分權衡的成本。自動微分並非免費:前向模式大約讓求值 f 的工作量與儲存翻倍;反向模式則多出磁帶與一趟反向掃過。一個有限差分導數只是多一次函數求值、兩行程式碼——有時這真的就夠好了,動用自動微分的機器反倒是過度設計。這份判斷,正是這整門學問反覆教你的同一件事:讓工具去匹配你真正需要的成本與精度。當你需要許多個精確導數——一個完整梯度、一個給系統牛頓法的雅可比矩陣、給最佳化用的敏感度——自動微分通常是正確答案;但對一次性的斜率估計,一條調好的中心差分也許更簡單。
為何這篇為這一級收尾
回望這一級的弧線。它以有限差分開場,帶來一個令人清醒的教訓:把 h 縮小會撞上一道捨入之牆;接著理查森外推部分地打破了那道牆,靠巧妙抵消主導誤差項。求積那幾篇講的是同一個故事的積分那一半——牛頓—寇茲,再到高斯求積從每個函數值裡榨出最大精度,再到自適應細化,全都在有限精度的世界裡對抗截斷誤差。自動微分則為這一級收尾,方式是在微分這一側徹底跳出這場戰鬥之外:它不在截斷與捨入之間求平衡,而是直接刪去截斷項。它交出的那個又精確又便宜的導數,正是這一級一路繞行所趨向的最乾淨答案。
而它也是一座通往前方的橋。緊接著的幾級不斷倚賴導數:牛頓法及其多變數的表親需要雅可比矩陣;最佳化需要梯度與黑塞矩陣;敏感度分析與反問題需要去微分整個模擬。在這每一處,自動微分——尤其是反向模式——都是那台安靜的引擎,以區區幾次函數求值的成本供應精確導數,且沒有步長要讓你苦惱。如今你手握全部三種微分工具,而同樣重要的是,你誠實地知道每一種何時才配得上它的位置。