跳轉到

1.1 符號計算 vs 數值計算#

本節目標#

  • 理解「符號計算」與「數值計算」的根本差異
  • 親眼看到浮點數誤差如何悄悄發生
  • 知道為什麼學數學時應該使用符號計算

計算機說 \(0.6666\cdots\) ,SymPy 說 \(\frac{2}{3}\)#

拿起你的計算機(或手機)輸入 \(1 \div 3\) ,你會看到:

\[ 0.3333333333333333 \]

再乘以 \(3\) ,理論上應該回到 \(1\) ,但結果常是 \(0.9999999999999999\) 或勉強顯示成 \(1\) 。問題不在數學,而在「表示方式」:計算機只能用有限位數的小數近似 \(\frac{1}{3}\) ,而 \(\frac{1}{3}\) 是無限循環小數,永遠寫不完整。

SymPy 的做法完全不同——它根本不把 \(\frac{1}{3}\) 化成小數,而是原封不動地保存這個分數本身:

from sympy import Rational

Rational(1, 3) * 3        # 結果:1(精確!)
Rational(1, 3) + Rational(1, 6)   # 結果:1/2(自動通分約分)

這就是符號計算(symbolic computation):操作的對象是數學表達式本身,而不是近似值。與之相對的數值計算(numerical computation)則是算出一個(可能有些微誤差的)數字。

浮點誤差實例#

Python 內建的 float 遵循 IEEE 754 標準,用 64 位元儲存小數,大約只有 15~17 位有效數字。看一個經典例子—— \((\sqrt{2})^2\) 應該等於 \(2\) ,但:

import math

x = math.sqrt(2)
print(x)          # 1.4142135623730951
print(x * x)      # 2.0000000000000004   ← 不是 2!
print(x * x == 2) # False

再看看 \(0.1 + 0.2\) 這個著名的例子:

print(0.1 + 0.2)          # 0.30000000000000004
print(0.1 + 0.2 == 0.3)   # False

原因: \(0.1\) 在二進位下是無限循環小數,就像 \(\frac{1}{3}\) 在十進位下寫不完一樣。電腦只能存下它的近似值,加法之後誤差就現形了。

用 SymPy 則完全沒有這個問題:

from sympy import sqrt, Rational, simplify

simplify(sqrt(2)**2)      # 2(精確回到 2)
Rational(1, 10) + Rational(2, 10)   # 3/10,不是 0.3000...004

更嚴重的例子——大數吃小數(catastrophic cancellation 的近親):

print(1e16 + 1 - 1e16)    # 0.0,但數學上應該是 1

\(10^{16} + 1\) 的 \(1\) 在 64 位元浮點數中被「吃掉了」。這類誤差在科學計算中可能造成災難(例如飛彈導引、財務計算),所以專業的數值程式設計師必須時時提防;而符號計算從源頭避開了它。

符號 vs 數值對照表#

面向 符號計算(SymPy) 數值計算(Python float / NumPy)
表示方式 精確的數學表達式,如 \(\frac{2}{3}\) 、 \(\sqrt{2}\) 、 \(\pi\) 有限位數的近似值,如 \(0.6666\cdots 7\)
結果正確性 絕對精確(在數學意義上) 可能有浮點誤差
速度 較慢,複雜問題可能很慢或算不完 非常快
符號運算 化簡、展開、因式分解、積分、微分皆可 完全不會處理符號
輸出形式 數學式(可轉 LaTeX) 一串數字
適用場合 學習數學、推導公式、驗算 大量資料、模擬、工程計算

一句話總結:符號計算求「真」,數值計算求「快」。兩者互補,不是互斥。

CAS 的小歷史#

電腦代數系統(Computer Algebra System, CAS)的歷史:1960 年代 MIT 開發了 Macsyma,是 CAS 的鼻祖;之後 Mathematica(1988)與 Maple(1980 年代)成為商業主流;2006 年,波蘭開發者 Ondřej Čertík 發起了純 Python 的開源 CAS——SymPy,讓每個人都能免費使用符號計算。

為什麼學數學要用符號計算?#

  1. 驗算:手算積分、解方程後,讓 SymPy 幫你確認答案對不對。
  2. 看見結構:factor(x**3 - 1) 直接給你 \((x - 1)(x^2 + x + 1)\) ,比數字答案更能揭示數學結構。
  3. 不怕算錯:展開 \((a+b)^6\) 這種繁瑣計算,交給電腦保證不出錯。
  4. 橋接 Python:SymPy 是純 Python,學會它同時也在練程式,一舉兩得。

動手實驗#

實驗 1:找出更多浮點誤差。試著在 Python 中檢查下列式子是否成立,並解釋原因:

print(0.1 + 0.7 == 0.8)
print(sum([0.1] * 10) == 1.0)
print((1/49) * 49 == 1)

實驗 2:用 SymPy 重做實驗 1,比較結果:

from sympy import Rational

print(Rational(1, 10) + Rational(7, 10) == Rational(8, 10))
print(sum([Rational(1, 10)] * 10) == 1)
print(Rational(1, 49) * 49 == 1)

實驗 3: \(\sqrt{2}\) 的浮點誤差會隨運算放大。執行以下程式碼,觀察誤差如何累積:

import math

x = math.sqrt(2)
for _ in range(10):
    x = x * x
    x = math.sqrt(x)
print(x)   # 應該還是 sqrt(2),結果對嗎?

小結與練習#

  • 數值計算用有限位數近似,會產生浮點誤差;符號計算保存精確的數學物件。
  • 典型浮點陷阱: \((\sqrt{2})^2 \ne 2\) 、 \(0.1 + 0.2 \ne 0.3\) 、大數吃小數。
  • CAS 從 Macsyma 到 Mathematica、Maple,再到 2006 年誕生的開源 SymPy。

練習

  1. 用 float 與 SymPy 各自計算 \(\frac{1}{7} + \frac{2}{7}\) ,比較結果的呈現方式。
  2. 找出一個會讓 x * x == 2 為 False 的方式之外,再找一個新的浮點誤差例子(提示:試試 1e308 * 10)。
  3. 說明在什麼情況下你會選擇數值計算而非符號計算。