1.1 符號計算 vs 數值計算#
本節目標#
- 理解「符號計算」與「數值計算」的根本差異
- 親眼看到浮點數誤差如何悄悄發生
- 知道為什麼學數學時應該使用符號計算
計算機說 \(0.6666\cdots\) ,SymPy 說 \(\frac{2}{3}\)#
拿起你的計算機(或手機)輸入 \(1 \div 3\) ,你會看到:
再乘以 \(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,讓每個人都能免費使用符號計算。
為什麼學數學要用符號計算?#
- 驗算:手算積分、解方程後,讓 SymPy 幫你確認答案對不對。
- 看見結構:
factor(x**3 - 1)直接給你 \((x - 1)(x^2 + x + 1)\) ,比數字答案更能揭示數學結構。 - 不怕算錯:展開 \((a+b)^6\) 這種繁瑣計算,交給電腦保證不出錯。
- 橋接 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。
練習
- 用
float與 SymPy 各自計算 \(\frac{1}{7} + \frac{2}{7}\) ,比較結果的呈現方式。 - 找出一個會讓
x * x == 2為False的方式之外,再找一個新的浮點誤差例子(提示:試試1e308 * 10)。 - 說明在什麼情況下你會選擇數值計算而非符號計算。