15. 浮點數算術:問題和限制¶
浮點數在計算機硬體中以 2 為底(二進位制)的分數形式表示。例如,**十進位制**小數 0.625
的值為 6/10 + 2/100 + 5/1000,同樣地,**二進位制**小數 0.101
的值為 1/2 + 0/4 + 1/8。這兩個小數的值相同,唯一的真正區別是第一個以 10 為底的小數表示法書寫,第二個以 2 為底書寫。
不幸的是,大多數十進位制小數不能精確地表示為二進位制小數。一個結果是,通常,您輸入的十進位制浮點數只是近似於實際儲存在機器中的二進位制浮點數。
這個問題最初在以 10 為底的情況下更容易理解。考慮分數 1/3。您可以將其近似為以 10 為底的分數
0.3
或者,更好的是,
0.33
或者,更好的是,
0.333
等等。無論您願意寫下多少位數字,結果永遠不會完全是 1/3,而是越來越接近 1/3 的近似值。
同樣,無論您願意使用多少個以 2 為底的數字,十進位制值 0.1 都無法精確地表示為以 2 為底的分數。在以 2 為底的情況下,1/10 是無限重複的小數
0.0001100110011001100110011001100110011001100110011...
在任意有限的位數處停止,您都會得到一個近似值。在當今大多數機器上,浮點數使用二進位制分數來近似,其中分子使用從最高有效位開始的前 53 位,分母為 2 的冪。在 1/10 的情況下,二進位制分數為 3602879701896397 / 2 ** 55
,它接近但不完全等於 1/10 的真實值。
許多使用者沒有意識到這種近似,因為值的顯示方式。Python 只打印機器儲存的二進位制近似值的真實十進位制值的十進位制近似值。在大多數機器上,如果 Python 要列印為 0.1 儲存的二進位制近似值的真實十進位制值,它將必須顯示
>>> 0.1
0.1000000000000000055511151231257827021181583404541015625
這比大多數人認為有用的數字多,因此 Python 透過顯示舍入值來保持數字的可管理性
>>> 1 / 10
0.1
請記住,即使列印的結果看起來像 1/10 的精確值,但實際儲存的值是最接近的可表示二進位制分數。
有趣的是,有許多不同的十進位制數共享相同的最接近近似二進位制分數。例如,數字 0.1
和 0.10000000000000001
和 0.1000000000000000055511151231257827021181583404541015625
都近似於 3602879701896397 / 2 ** 55
。由於所有這些十進位制值都共享相同的近似值,因此可以顯示其中任何一個值,同時仍然保持不變 eval(repr(x)) == x
。
歷史上,Python 提示符和內建的 repr()
函式會選擇具有 17 位有效數字的 0.10000000000000001
。從 Python 3.1 開始,Python(在大多數系統上)現在能夠選擇其中最短的一個並簡單地顯示 0.1
。
請注意,這是二進位制浮點數的本質:這不是 Python 中的錯誤,也不是您程式碼中的錯誤。您將在所有支援硬體浮點運算的語言中看到同樣的情況(儘管某些語言可能預設不 *顯示* 差異,或者在所有輸出模式中)。
為了獲得更令人愉悅的輸出,您可能希望使用字串格式來生成有限數量的有效數字
>>> format(math.pi, '.12g') # give 12 significant digits
'3.14159265359'
>>> format(math.pi, '.2f') # give 2 digits after the point
'3.14'
>>> repr(math.pi)
'3.141592653589793'
重要的是要意識到,這在某種意義上是一種錯覺:您只是在對真實機器值的 *顯示* 進行舍入。
一種錯覺可能會導致另一種錯覺。例如,由於 0.1 不完全等於 1/10,因此將三個 0.1 的值相加也可能不會精確地產生 0.3
>>> 0.1 + 0.1 + 0.1 == 0.3
False
此外,由於 0.1 不能更接近 1/10 的精確值,並且 0.3 不能更接近 3/10 的精確值,因此使用 round()
函式進行預舍入也無濟於事
>>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
False
雖然這些數字不能更接近其預期的精確值,但 math.isclose()
函式可用於比較不精確的值
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
True
或者,可以使用 round()
函式來比較粗略的近似值
>>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)
True
二進位制浮點算術會帶來許多這樣的驚喜。“0.1” 的問題在下面的“表示誤差”部分中進行了精確的詳細解釋。請參閱 浮點問題示例,瞭解二進位制浮點數的工作原理以及實踐中常見的各種問題的簡明總結。另請參閱 浮點的危險,瞭解其他常見意外情況的更完整說明。
正如最後所說,“沒有簡單的答案”。儘管如此,不要對浮點數過於警惕!Python 浮點運算中的誤差是從浮點硬體繼承的,在大多數機器上,每次運算的誤差不超過 1/2**53。這對於大多數任務來說綽綽有餘,但您確實需要記住,這不是十進位制算術,並且每次浮點運算都可能遭受新的舍入誤差。
雖然確實存在病理情況,但對於大多數浮點算術的隨意使用,如果您只是將最終結果的顯示舍入到您期望的十進位制位數,您最終會看到您期望的結果。str()
通常就足夠了,而對於更精細的控制,請參閱 str.format()
方法在 格式字串語法 中的格式說明符。
對於需要精確十進位制表示的用例,請嘗試使用 decimal
模組,該模組實現適用於會計應用程式和高精度應用程式的十進位制算術。
另一種精確算術形式由 fractions
模組支援,該模組實現基於有理數的算術(因此像 1/3 這樣的數字可以精確表示)。
如果您是浮點運算的重度使用者,您應該檢視 NumPy 包以及 SciPy 專案提供的許多其他用於數學和統計運算的包。請參閱 <https://scipy.org>。
Python 提供了工具,可以在那些罕見的情況下幫助您真正 *想* 知道浮點數的精確值。float.as_integer_ratio()
方法將浮點數的值表示為分數
>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)
由於該比率是精確的,因此可以無損地重新建立原始值
>>> x == 3537115888337719 / 1125899906842624
True
float.hex()
方法以十六進位制(以 16 為底)表示浮點數,再次給出計算機儲存的精確值
>>> x.hex()
'0x1.921f9f01b866ep+1'
這種精確的十六進位制表示可以用於精確地重建浮點數值
>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True
由於表示是精確的,因此它對於在不同版本的 Python 之間可靠地移植值(平臺獨立性)以及與其他支援相同格式的語言(例如 Java 和 C99)交換資料非常有用。
另一個有用的工具是 sum()
函式,該函式有助於減輕求和期間的精度損失。它在將值新增到執行總計時,對中間舍入步驟使用擴充套件精度。這可能會對整體準確性產生影響,因此誤差不會累積到影響最終總和的程度
>>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
False
>>> sum([0.1] * 10) == 1.0
True
math.fsum()
更進一步,它在將值新增到執行總計時跟蹤所有“丟失的數字”,因此結果只有一個舍入。這比 sum()
慢,但在大型輸入相互抵消導致最終總和接近於零的罕見情況下會更準確
>>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,
... -143401161400469.7, 266262841.31058735, -0.003244936839808227]
>>> float(sum(map(Fraction, arr))) # Exact summation with single rounding
8.042173697819788e-13
>>> math.fsum(arr) # Single rounding
8.042173697819788e-13
>>> sum(arr) # Multiple roundings in extended precision
8.042178034628478e-13
>>> total = 0.0
>>> for x in arr:
... total += x # Multiple roundings in standard precision
...
>>> total # Straight addition has no correct digits!
-0.0051575902860057365
15.1. 表示誤差¶
本節將詳細解釋“0.1”的示例,並展示如何自行對類似情況進行精確分析。假設您已基本熟悉二進位制浮點數表示法。
表示誤差是指某些(實際上是大多數)十進位制小數無法精確地表示為二進位制(以 2 為底)小數。這是 Python(或 Perl、C、C++、Java、Fortran 以及許多其他語言)通常無法顯示您期望的精確十進位制數的主要原因。
這是為什麼呢?1/10 無法精確地表示為二進位制小數。自 2000 年以來,幾乎所有機器都使用 IEEE 754 二進位制浮點算術,並且幾乎所有平臺都將 Python 的浮點數對映到 IEEE 754 binary64 “雙精度”值。IEEE 754 binary64 值包含 53 位精度,因此在輸入時,計算機會盡力將 0.1 轉換為最接近的 J/2**N 形式的分數,其中 J 是一個正好包含 53 位的整數。重寫
1 / 10 ~= J / (2**N)
為
J ~= 2**N / 10
並回顧 J 正好有 53 位(即 >= 2**52
但 < 2**53
),N 的最佳值是 56
>>> 2**52 <= 2**56 // 10 < 2**53
True
也就是說,56 是唯一能使 J 正好有 53 位的 N 值。J 的最佳值是該商的舍入值
>>> q, r = divmod(2**56, 10)
>>> r
6
由於餘數大於 10 的一半,因此透過向上舍入獲得最佳近似值
>>> q+1
7205759403792794
因此,IEEE 754 雙精度中 1/10 的最佳近似值是
7205759403792794 / 2 ** 56
分子和分母都除以 2 可以將分數簡化為
3602879701896397 / 2 ** 55
請注意,由於我們向上舍入,這實際上比 1/10 略大;如果我們沒有向上舍入,商將略小於 1/10。但無論如何它都不可能正好是 1/10!
因此,計算機永遠不會“看到” 1/10:它看到的是上面給出的精確分數,這是它可以獲得的 IEEE 754 雙精度的最佳近似值
>>> 0.1 * 2 ** 55
3602879701896397.0
如果我們將該分數乘以 10**55,我們可以看到該值到 55 位小數
>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625
這意味著儲存在計算機中的確切數字等於十進位制值 0.1000000000000000055511151231257827021181583404541015625。許多語言(包括舊版本的 Python)不是顯示完整的十進位制值,而是將結果四捨五入為 17 位有效數字
>>> format(0.1, '.17f')
'0.10000000000000001'
fractions
和 decimal
模組使這些計算變得容易
>>> from decimal import Decimal
>>> from fractions import Fraction
>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'