每一點都有斜率,卻沒有路徑
許多科學都以這種形式登場:「我知道變化的速率,卻不知道那個東西本身。」一杯冷卻中的咖啡,散熱速率與它比室溫高出多少成正比;一個族群的成長速率,由它目前的大小決定;一顆行星的速度,依它此刻所受的重力而改變。這每一個都是一個常微分方程(ODE):一條形如 y'(t) = f(t, y) 的規則,給定你目前所在 y,它就交給你任意時刻 t 的斜率 y'。把那條規則配上一個起始值 y(t_0) = y_0——咖啡、族群、行星在一開始所在之處——你就有了一個初值問題。問題永遠相同:由斜率法則與起點,重建出整條未來路徑 y(t)。
對少數幸運的方程,你可以用微積分把它解出來,把 y(t) 寫成一個工整的公式。但只要 f 稍微貼近現實一點——一個非線性項、一個會開開關關的外力、兩三個量彼此回饋——封閉形式的解就消失了,正如這座學習階梯前面那些求根問題一樣。於是我們做這裡一向做的事:放棄精確公式,改為計算路徑的一個數值近似。訣竅是把這個方程想成不是某個要符號積分的東西,而是一整片小箭頭組成的場。在平面的每一點,f 都告訴你一個方向——一條經過那點的解必須具有的斜率。解這個常微分方程,意思就是找一條曲線,使它沿著全長處處都與那些箭頭相切。
就朝斜率的方向踏一小步
整個想法就在這裡,而且簡單到幾乎令人不好意思。你站在起點,在點 (t_0, y_0)。方程告訴你就在那裡的斜率:它是 f(t_0, y_0)。如果你只在一小段時間 h 內——一個小小的步長——相信那個斜率,那麼在那一小段裡,路徑近似是一條具有那個斜率的直線。於是你沿著那條直線走:你的新時間是 t_1 = t_0 + h,你的新高度是 y_1 = y_0 + h * f(t_0, y_0)。你剛剛把一條未知曲線,在一步之內用它的切線取代了。如今你來到一個新的點,你向方程詢問那裡的斜率,然後再踏一步。重複,你就描出一條由小直線段相連而成的鏈,緊貼著那條真正的曲線。
這就是尤拉法,即顯式或前向尤拉法,一般步驟為 y_{n+1} = y_n + h * f(t_n, y_n),其中 t_{n+1} = t_n + h。時間步進這個詞所命名的正是這種節奏:把時間區間切成寬度為 h 的小段,一段接一段地向前行軍,每一步的答案餵給下一步。要看清這條規則從何而來,有一個乾淨的方式。導數是斜率的極限,y'(t) = (y(t+h) - y(t))/h 在 h 趨於零時的極限;若你不取極限、就用一個有限的 h,你便得到有限差分近似 y'(t_n) 約等於 (y_{n+1} - y_n)/h。把它等於斜率法則 f(t_n, y_n),解出 y_{n+1},尤拉公式就直接掉了出來。尤拉法,就是當你拒絕把極限取到底時,一個導數所長的樣子。
forward_euler(f, t0, y0, h, N): # N steps of size h
t = t0
y = y0
for n in 1..N:
y = y + h * f(t, y) # step along the current slope
t = t + h
record (t, y)
return y
# example: y' = y, y(0) = 1, true answer is e^t = 2.71828...
# h = 1, one step : y1 = 1 + 1*(1) = 2.0 (error ~0.72)
# h = 0.5, 2 steps: y2 = 2.25 -> 2.25 (error ~0.47)
# h = 0.25,4 steps: y4 = (1.25)^4 = 2.4414 (error ~0.28)一步錯多少——又是怎麼堆積起來的?
尤拉法是錯的,而把錯在哪裡講精確是值得的。在單獨一步裡,你把一條彎曲的解用它的直切線取代了,所以在曲線拐離直線之處,你會略微偏差。那一步的偏差大小,就是局部截斷誤差,而看一眼 y(t+h) 的泰勒展開就把它釘住了:真正的一步是 y_n + h*y'_n + (h^2/2)*y''_n + 更高次項,而尤拉法只留下 y_n + h*y'_n。它最先丟掉的是 (h^2/2)*y''_n 這一項,所以單獨一步所犯的誤差與 h^2 成正比。把步長減半,單獨一步就準上四倍。解越彎(y'' 越大),單獨一步就做得越糟,這與直覺相符:一條直線能很好地追蹤一條緩彎的曲線,卻很差地追蹤一條急彎的曲線。
但你不是只踏一步——你踏很多步,而那些小偏差會累積起來。這是關鍵、且略帶意外的部分。要橫越一段長度固定為 T 的時間區間,你需要 N = T/h 步;若每一步貢獻約 h^2 大小的誤差,那麼天真地說,N 個合起來可能達到 (T/h)*h^2 = T*h。而事實上,全域誤差——終點處數值答案與真值之間的差距——確實與 h 成正比,而非 h^2。整整一個 h 的次方被丟失了,純粹是因為每一步越小、你就得踏越多步。所以尤拉法是一階的:每步的局部誤差是 O(h^2),但你真正在乎的全域誤差卻是 O(h)。把步長減半,大致把最終誤差減半——是好些了,但只是線性地好些,而你要用兩倍多的步數去換它。
前向與後向:誰來提供那個斜率?
有一個現在值得認識的同胞,因為它悄悄主宰了這一級剩下的全部。前向尤拉法用的是步首的斜率,你已經所在之處:y_{n+1} = y_n + h * f(t_n, y_n)。而後向(隱式)尤拉法改用步尾的斜率,你即將落腳之處:y_{n+1} = y_n + h * f(t_{n+1}, y_{n+1})。讀兩遍——未知的 y_{n+1} 出現在等號兩邊。你無法只是算出右邊就讀出答案;你必須在每一步都解一個關於 y_{n+1} 的方程。這就是「隱式」的意思,而且是真功夫:若 f 是非線性的,你通常每一步都用牛頓法去解那個方程,這也是為什麼隱式方法每一步比顯式方法貴。
若後向尤拉法更貴、又不更準——它同樣只是一階——那為什麼會有人用它?答案是穩定性,這一級最深的主題,我們在此只先預告。想像一個方程,它真正的解迅速而平滑地衰減到零,像 y' = -100*y。前向尤拉法在這類問題上會做出令人警覺的事:除非步長 h 夠小,否則它的數值答案不衰減,反而以越來越大的擺幅振盪,爆成一團胡言亂語,儘管真正的解正平靜地朝零而去。後向尤拉法在完全相同的問題上,無論你選什麼步長,都平滑地衰減。你所信任的那個斜率——步首的還是步尾的——竟然主宰了誤差是會消亡、還是會爆炸。
這個差異有一個我們稍後會細究的名字:前向尤拉法只是條件穩定,意思是它的步長受問題所限制,而後向尤拉法在這類衰減問題上是無條件穩定的。那些「安全的顯式步長小到懲罰人」的方程,被稱為剛性方程,而它們恰恰就是你被迫掏錢買隱式方法之處。所以前向與後向尤拉法不只是把幾乎相同的公式寫成兩種樣子;它們是整個學科中那個核心權衡的兩端——每步便宜卻脆弱,相對於每步昂貴卻穩健。
幹嘛費心用這麼粗糙的方法?
我們直說吧:沒有人會用樸素的前向尤拉法跑正經的模擬。它的一階準確度很差——要多得一個正確數字,你就得踏十倍多的步數——而它脆弱的穩定性意味著剛性問題能把它毀掉。這一級後面的方法之所以存在,正是為了修補這些缺陷。龍格-庫塔法,尤其是著名的 RK4,在每一步內部於幾個精心選定的點上採樣斜率並加以混合,以不多的額外代價買到高得多的階;多步法重複利用過去的斜率以求效率;自適應求解器則一邊跑一邊放大縮小 h,以命中目標準確度。這每一個,本質上都是對尤拉法最先問的那個問題,一個更聰明的回答:給定我所在之處與斜率法則,我該如何向前踏出漂亮的一步?
那為什麼從這裡開始?因為尤拉法是那副乾淨、誠實的骨架,後面所有的血肉都掛在它上面。一旦你真正看清一個常微分方程求解器無非就是「求斜率、踏步、重複」,這一級剩下的就不再是一場魔法公式的遊行,而變成對你如今能自問的清晰問題的一連串回答。單獨一步的誤差有多大,又如何在許多步上累加?那是下一篇,講局部與全域誤差及階。我該如何在一步之內更聰明地採樣斜率,好遠勝過一條直切線?那是龍格-庫塔法。我該如何自動選擇 h、並倚靠歷史?那是自適應與多步法。而顯式步何時會爆炸、逼我轉向隱式?那是剛性,這一級的高潮。尤拉法很小,卻是通往這一切的那道門。