跳轉到

1.2 浮點數與捨入誤差#

概念說明#

你在 Python 裡輸入 0.1 + 0.2 == 0.3,會得到 False。這不是 Python 的 bug,而是浮點數表示法的本質限制。現代電腦幾乎都採用 IEEE 754 標準的浮點數:一個數被表示為

(−1)^s × 1.fraction × 2^(exponent − bias)

其中 s 是正負號、fraction 是小數部分、exponent 是指數。雙精度(double,Python 的 float)有 1 位符號、11 位指數、52 位小數,因此大約只有 15~17 位十進位有效數字。

關鍵在於:十進位小數 0.1 = 1/10 在二進位下是無限循環小數 0.0001100110011…,無法用有限的 52 位完整表示,只能「四捨五入」到最接近的二進位數。於是 0.1 存進電腦的那一刻,就已經帶著微小誤差,而後續的加減乘除會讓這些誤差傳播、累積,甚至放大——這就是捨入誤差(rounding error)。

演算法步驟#

寫數值程式時的防誤差檢查清單:

  1. 不要用 == 比較浮點數,改用 abs(a - b) < tol 或 math.isclose(a, b)。
  2. 避免災難性抵消(catastrophic cancellation):兩個接近的數相減會吃掉有效位數。計算 √(x+1) − √x 時,改寫成 1/(√(x+1) + √x) 可大幅改善。
  3. 小心大數吃小數:把 10^20 加 1 再減 10^20,結果是 0。
  4. 總和運算用 math.fsum 或 Kahan 求和,減少累積誤差。
  5. 需要精確十進位時改用 decimal 模組。

Python 程式碼範例#

import math
from decimal import Decimal, getcontext

# 1. 看看 0.1 的真面目
print(f"{0.1:.20f}")        # 0.10000000000000000555
print(0.1 + 0.2)            # 0.30000000000000004
print(math.isclose(0.1 + 0.2, 0.3))   # True:正確的比較方式

# 2. 災難性抵消
x = 1e8
naive = math.sqrt(x + 1) - math.sqrt(x)
better = 1 / (math.sqrt(x + 1) + math.sqrt(x))
print(f"直接相減: {naive}")        # 5e-09,只有 1 位有效數字
print(f"改寫公式: {better:.15f}")  # 精確得多

# 3. 大數吃小數
big = 1e20
print(big + 1.0 - big)      # 0.0,1 完全被吃掉

# 4. 求和誤差:把 0.1 加一百萬次
data = [0.1] * 1_000_000
print(sum(data))            # 100000.00000133288,有誤差
print(math.fsum(data))      # 100000.00000000001,幾乎正確

# 5. 用 decimal 做精確十進位
getcontext().prec = 50
a = Decimal("0.1") + Decimal("0.2")
print(a == Decimal("0.3"))  # True
print(a)                    # 0.3(精確)

誤差分析#

浮點誤差可用「機器精度 ε」描述:雙精度的 ε = 2^(−52) ≈ 2.22 × 10⁻¹⁶,意思是任何實數存入 float 時,相對誤差至多 ε/2。做一次運算,相對誤差大約再乘上 (1+ε)。若一個演算法做了 n 次運算,誤差可能累積到 nε 量級;但像求和 10⁶ 個 0.1 的例子中,誤差其實隨機遊走累積,量級約 √n·ε·|數值|,這解釋了為何結果誤差是 10⁻⁶ 量級而非 10⁻¹⁰。若演算法會放大誤差(如災難性抵消),即使 ε 極小,結果也可能完全不可用。好的數值演算法的核心目標,就是把誤差放大率(condition number)壓低。

練習#

  1. 用 float.hex() 觀察 0.1、0.5、0.25 的內部表示,說明為何 0.5 與 0.25 可以被精確表示而 0.1 不行。
  2. 計算 1 − cos(θ) 在 θ = 10⁻⁸ 時的值,再用半角公式 2sin²(θ/2) 重算,比較何者可信,並解釋原因。
  3. 實作 Kahan 求和演算法,對 [1e16, 1.0, -1e16, 1.0] 求和,比較 sum、math.fsum 與 Kahan 的結果。