螢幕會撒一點小謊——而且這沒關係
在上一篇我們說過,計算數學的整個重點,就是用一個數值演算法去解問題、吐出一個數字。但這裡有個定義整門學問的關鍵:那個數字幾乎永遠是個近似值,而不是精確解。當你計算 sqrt(2),機器給你 1.4142135623730951——這是個很好用的值,但 sqrt(2) 是無理數、沒有有限小數展開,所以在它被存進記憶體的那一瞬間,真值就已經被四捨五入掉了。這門領域誠實的心態,不是問「我的答案對不對?」,而是問「它錯得多嚴重,我能不能把它框起來?」
這正是它和你之前學過的純數學、符號數學最深刻的分野。符號計算可以永遠把 sqrt(2) 當成精確符號 sqrt(2)、一點都不損失——但它為了這份精確付出的代價是「運算式膨脹」,中間的式子會失控地越長越大。數值計算則用這份完美換來速度和規模:它接受每一步都有一點點受控的錯誤,好讓自己能處理上億個未知數的問題。其中的藝術,就在於把這份錯誤壓小、並且知道它到底有多大。
誤差走進來的四道門
誤差不是一種東西;它從四道不同的門走進來,而清楚指認出你正在對付的是哪一道,會幫上大忙。第一道是建模誤差:在任何計算發生之前,你就先把雜亂的現實換成了一道方程式。建模誤差就是真實世界和模型之間的落差——忽略空氣阻力、假設樑是完美筆直的、假裝人口剛好按指數成長。無論演算法多聰明,都救不回模型一開始就丟掉的東西。
第二道門是資料誤差:你餵進去的數字本身是量出來的,而量測都有雜訊。如果你只知道溫度準到正負 0.5 度,那再怎麼算也不能假裝輸出精準到十位數。第三道是截斷誤差,它最能代表數值數學的特色。為了讓微積分變得可計算,我們把無窮的過程砍短:在幾項之後就停下一個級數,或是用一個有限步長上的差商去取代導數。這個刻意的截斷,就是截斷誤差。當被砍掉的「無窮」是把連續區域切成一格一格有限網格時,同樣的概念換了個名字叫離散化誤差。
第四道門是捨入誤差,在任何真實機器上都躲不掉。電腦用浮點數存數字,那是一組有限的數值,所以大多數實數都會被推到最接近的那個可表示的鄰居身上——每一次運算都可能發生捨入。關鍵在於,0.1 沒有精確的二進位表示(它在二進位裡是循環小數),而且浮點加法連結合律都不成立:(a + b) + c 可能不等於 a + (b + c)。捨入誤差通常極小,大約是機器 epsilon的等級,雙精度下約為 1e-16——直到災難性抵消把兩個幾乎相等的數相減,才會把它戲劇性地放大。
有多大?絕對誤差對上相對誤差
一旦我們承認答案是錯的,就得衡量它錯得多。如果真值是 x、算出來的值是 x_hat,那絕對誤差就是 |x - x_hat|——原始的差距。但是差距為 1 這件事,對 x = 3 和 x = 30 億來說意義天差地別。所以我們通常偏好用相對誤差 |x - x_hat| / |x|,它與尺度無關,可以很自然地讀成百分比、或讀成正確位數的個數:相對誤差接近 1e-6 大約就代表有六位正確的有效數字。
absolute error = |x - x_hat|
relative error = |x - x_hat| / |x| (x != 0)
x = 100.0, x_hat = 100.1
absolute = 0.1 relative = 0.001 (~3 digits)
x = 0.001, x_hat = 0.0011
absolute = 0.0001 relative = 0.1 (~1 digit!)相對誤差直接連到雙精度一個冷靜的極限:你一開始只有大約 16 位正確數字,而這是一筆只能花、不能補的預算。如果一個問題是病態的——意思是它的條件數很大,比方說大約 1e8——那麼輸入的微小晃動就會被放大那麼多倍,於是在任何演算法跑起來之前,你 16 位裡就先掉了大約 8 位。這是我們接下來會遇到的那條規則的預告:準確度取決於問題的條件數,跟取決於你的方法一樣多;這個我們留到第三篇和條件數那一階再談。
大 O,兩種完全不同的用法
這裡有個悄悄讓每個新手都搞混的點:同樣的大 O 記號,在這門學問裡被用在兩件完全不同的事情上,你必須隨時問清楚是哪一件。第一件是準確度的階數——當你把問題加細時,誤差縮小得多快。一個有限差分格式對它的步長 h 可能有誤差 O(h^p):一階方法(p = 1)在你把 h 減半時誤差也減半,而二階方法(p = 2)會把誤差縮成四分之一。p 越大好太多了,因為誤差是按一個你正驅使它趨近於零的量的次方在下降。
第二種用法是成本——當問題變大時,工作量怎麼成長。這就是你在任何演算法課裡會遇到的那個計算複雜度:用消去法解一個稠密線性系統 A x = b,對 n×n 的矩陣大約要 O(n^3) 次運算,而快速傅立葉轉換把一個 O(n^2) 的計算變成了著名的 O(N log N)。注意這兩者方向上的對比。談準確度時,你要的是一個小量(h)趨近於零;談成本時,你要的是一個大量(n)趨向無窮。它們甚至指向相反的方向:每一步更準的方法,往往每一步也更貴。
收斂:一步一步追著答案跑
許多數值方法不會一口氣給你答案;它們產生一串猜測 x_0, x_1, x_2, ...,你希望它們會逐漸逼近真值 x。如果誤差 e_n = |x - x_n| 趨近於零,這個方法就收斂了。但兩個都會收斂的方法,賽跑的速度可能天差地別,而收斂階數會告訴你是誰快。粗略地說,如果 e_{n+1} 正比於 (e_n)^q,那 q 就是階數:q = 1 是線性收斂(誤差每步縮小一個固定倍率),而 q = 2 是二次收斂——正確位數大約每一步就翻倍。
經典的例子是用來解 f(x) = 0 的牛頓法,我們稍後會深入研究它。它的更新公式是 x_{n+1} = x_n - f(x_n)/f'(x_n),在單根附近它是二次收斂的:快得驚人。但請誠實面對附帶條款——那個二次速度只在單根附近、而且有個好的起始猜測時才成立。如果起點不好,或落在 f'(x) 接近零的平坦處,牛頓法可能會劇烈衝過頭、永遠繞圈,或乾脆發散。快速收斂是一個局部的承諾,不是從任何地方出發都成立的保證。
最後,有個值得趁早打破的迷思:面對像 O(h^p) 這樣的截斷誤差,人很容易想「那我就把 h 取到能取的最小,誤差不就消失了嗎?」在真實機器上並不會。當 h 縮小時,截斷誤差確實會降——但相減兩個幾乎相等的數所產生的捨入誤差卻會增大,於是總誤差會觸底、然後反而變糟。存在一個最佳的 h,一個步長的取捨,過了那一點以後,更小的步長反而有害。這篇文章裡的每一種誤差來源都同時活著,而要算得好,靠的是平衡它們,而不是把任何單一一種追到零。