1. 程式人生 > >Python的浮點數損失精度問題(為什麼說雙精度浮點數有15位十進位制精度)

Python的浮點數損失精度問題(為什麼說雙精度浮點數有15位十進位制精度)

本篇討論的現象可以從下面這段指令碼體現出來:

>>> x = 0.0
>>> for i in range(10):
	x += 0.1
	print(x)

	
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
>>>

即:為什麼有幾行的輸出看起來不對?

因為 Python 中使用雙精度浮點數來儲存小數。在 Python 使用的 IEEE 754 標準(52M/11E/1S)中,8位元組64位儲存空間分配了52位來儲存浮點數的有效數字,11位儲存指數,1位儲存正負號,即這是一種二進位制版的科學計數法格式。雖然52位有效數字看起來很多,但麻煩之處在於,二進位制小數在表示有理數時極易遇到無限迴圈的問題。其中很多在十進位制小數中是有限的,比如十進位制的 1/10,在十進位制中可以簡單寫為 0.1 ,但在二進位制中,他得寫成:0.0001100110011001100110011001100110011001100110011001…..(後面全是 1001 迴圈)。因為浮點數只有52位有效數字,從第53位開始,就舍入了。這樣就造成了標題裡提到的”浮點數精度損失“問題。 舍入(round)的規則為“0 舍 1 入”,所以有時候會稍大一點有時候會稍小一點。

Python 的浮點數型別有一個 .hex()方法,呼叫能夠返回該浮點數的二進位制浮點數格式的十六進位制版本。這話聽著有點繞,其實是這樣的:本來浮點數應該有一個 .bin() 方法,用來返回其二進位制浮點數格式。如果該方法存在的話,它看起來就像這樣(p-4表示×2-4,或者可以簡單理解為小數點 左移 4 位):

>>> (0.1).bin()#本方法其實並不存在
'1.1001100110011001100110011001100110011001100110011010p-4'

但是這個字串太長了,同時因為每 4 位二進位制字元都可以換算成 1 位十六進位制字元,於是Python就放棄了給浮點數提供 .bin() 方法,改為提供 .hex() 方法。這個方法將上面輸出字串的 52 位有效數字轉換成了 13 位十六進位制數字,所以該方法用起來其實是這樣的(注:二進位制浮點數中小數點前的“1”不包含於那 52 位有效數字之中):

>>> (0.1).hex()
'0x1.999999999999ap-4'

前面的 0x 代表十六進位制。p-4 沒變,所以需要注意,這裡的 p-4 還是二進位制版的,也就是說在展開本格式的時候,你不能把小數點往左移 4 位,那樣就相當於二進位制左移 16 位了。前面提到過,小數點前這個“1”是不包含於 52 位有效數字之中的,但它確實是一個有效的數字呀,這是因為,在二進位制浮點數中,第一位肯定是“1”,(是“0”的話就去掉這位,並在指數上-1)所以就不儲存了,這裡返回的這個“1”,是為了讓人看懂而加上的,在記憶體的 8 位空間中並沒有它。所以 .hex() 方法在做進位制轉換的時候,就沒有顧慮到這個“1”,直接把 52 位二進位制有效數字轉換掉就按著原來的格式返回了。因此這個 .hex() 方法即使名義上返回的是一個十六進位制數,它小數點前的那一位也永遠是“1”,看下面示例:

>>> float.fromhex('0x1.8p+1') == float.fromhex('0x3.0p+0')
True

一般我們用十六進位制科學計數法來表示 3.0 這個數時,都會這麼寫“0×3.0p+0”。但是 Python 會這麼寫“0×1.8p+1”,即“1.1000”小數點右移一位變成“11.000”——確實還是 3.0 。就是因為這個 1 是直接遺傳自二進位制格式的。而我一開始沒有理解這個 .hex() 的意義,還畫蛇添足地自定義了一個 hex2bin() 方法,後來看看真是沒必要啊~

而為了迴應人們在某些狀況下對這個精度問題難以忍受的心情(霧),Python 提供了另一種數字型別——Decimal 。他並不是內建的,因此使用它的時候需要 import decimal 模組,並使用 decimal.Decimal() 來儲存精確的數字。這裡需要注意的是:使用非整數引數時要記得傳入一個字串而不是浮點數,否則在作為引數的時候,這個值可能就已經是不精確的了:

>>> Decimal(0.1) == Decimal('0.1')
False

在進一步研究到底損失了多少精度,或者說,八位元組浮點數最多可以達到多少精度的問題之前,先來整理一下小數和精度的概念。本篇中討論的小數問題僅限於有理數範圍,其實有理數也是日常程式設計中最常用到的數。有理數(rational number)一詞派生於“比(ratio)”,因此並不是指“有道理”的意思。有理數的內容擴充套件自自然數,由自然數通過有理運算(+ – * /)來得到的數系稱為有理數,因此可以看到它較自然數擴充了:零、負整數和分數的部分。有理數總可以寫成 p/q 的形式,其中 p、q 是整數且 q ≠ 0,而且當 p 和 q 沒有大於 1 的公因子且 q 是正數的時候,這種表示法就是唯一的。這也就是有理數被稱為 rational number 的原因,說白了就是分數。實際上 Python 的 float 型別還有一個 .as_integer_ratio() 的方法,就可以返回這個浮點數的最簡分數表示,以一個元組的形式:

>>> (0.5).as_integer_ratio()
(1, 2)

然後為了更直觀地表現,人們又開始用無限小數的形式表示有理數(分數)。而其中從某一位開始後面全是 0 的特殊情況,被稱為有限小數(沒錯,無限小數才是本體)。但因為很多時候我們並不需要無限長的小數位,我們會將有理數儲存到某一位小數便截止了。後面多餘小數的舍入方式便是“四捨五入”,這種方式較直接截斷(round_floor)的誤差更小。在二進位制中,它表現為“0 舍 1 入”。當我們舍入到某一位以後,我們就可以說該數精確到了那一位。如果仔細體會每一位數字的含義就會發現,在以求得有限小數位下儘可能精確的值為目的情況下,直接截斷的舍入方式其實毫無意義,得到的那最後一位小數也並不精確。例如,將 0.06 舍入成 0.1 是精確到小數點後一位,而把它舍入成 0.0 就不算。因此,不論是在雙精度浮點數保留 52 位有效數字的時候,還是從雙精度浮點數轉換回十進位制小數並保留若干位有效數字的時候,對於最後一位有效數字,都是需要舍入的。

下圖是一個(0,1)之間的數軸,上面用二進位制分割,下面用十進位制分割。比如二進位制的 0.1011 這個數,從小數點後一位一位的來看每個數字的意義:開頭的 1 代表真值位於 0.1 的右側,接下來的 0 代表真值位於 0.11 的左側,再接下來的 1 代表真值位於 0.101 的右側,最後的 1 代表真值位於 0.1011 的右側(包含正好落在 0.1011 上這種情況)。使用 4 位二進位制小數表示的 16 個不同的值,除去 0,剩下的 15 個數字正好可以平均分佈在(0,1)這個區間上,而十進位制只能平均分佈 9 個數字。顯然 4 位二進位制小數較於 1 位十進位制小數將此區間劃分的更細,即精度更高。

未標題-1_thumb[4]

把 0.1 的雙精度版本(0×1.999999999999ap-4)展開成十進位制。這裡使用了 Decimal 型別,在給他賦值的時候,他會完整儲存引數,但是要注意的是,使用 Decimal 進行運算是會舍入的,保留的位數由上下文決定。使用 decimal 模組的 getcontext() 方法可以得到上下文物件,其中的 prec 屬性就是精度。下面還使用了 print() 方法,這是為了好看:

>>> print(Decimal(0.1))
0.1000000000000000055511151231257827021181583404541015625

得到的這個十進位制浮點數有效數字足有 55 位。雖然從二進位制到十進位制這個過程是完全精確的,但因為在儲存這個二進位制浮點數的時候進行了舍入,所以這個 55 位的十進位制數,較於最初的 0.1 並不精確。至於到底能精確到原十進位制數的哪一位,可以這麼算: 2**53 = 9007199254740992 ≈ 10**16 ,(這裡 53 算上了開頭的“1”),即轉換後的十進位制小數的第 16 位有效數字很可能是精確的(第 15 位肯定是精確的)。換句話說,如果要唯一表示一個 53 位二進位制數,我們需要一個 17 位的十進位制數(但即使這樣,我們也不能說對應的十進位制和二進位制數完全相等,他們只不過在互相轉換的時候在特定精度下可以得到相同的的值罷了。就像上面例子中顯示的,精確表示”0.1“的雙精度版本,需要一個 55 位的十進位制小數)。

不過可以看到,如果要保證轉換回來的十進位制小數與原值相等,那麼只能保證到 15 位,第 16 位只是“很可能是精確的”。而且第 15 位的精確度也要依賴於第 16 位的舍入。實際上在 C++ 中,我看到有別人講,double 型別的十進位制小數就是保留 15 位的(這點我自己並不清楚)。所以如果 Python 的 float 型別的 __str__() 和 __repr__() 方法選擇返回一個 15 位的小數,那麼就不會出現本文討論的第一個問題了。不論是早期的“0.10000000000000001”還是本文中出現的“0.30000000000000004”或者“0.7999999999999999”,我們可以看到它的不精確都是因為儲存了過多位的有效數字,16 或 17 。從下面的指令碼中可以看得更加清楚:

>>> a=0.0
>>> for i in range(10):
	a += 0.1
	print(a)
	print('%.17f'%a)
	print('-'*19)

	
0.1
0.10000000000000001
-------------------
0.2
0.20000000000000001
-------------------
0.30000000000000004
0.30000000000000004
-------------------
0.4
0.40000000000000002
-------------------
0.5
0.50000000000000000
-------------------
0.6
0.59999999999999998
-------------------
0.7
0.69999999999999996
-------------------
0.7999999999999999
0.79999999999999993
-------------------
0.8999999999999999
0.89999999999999991
-------------------
0.9999999999999999
0.99999999999999989
-------------------

上面短橫線對齊的是第 17 位。雖然在這裡第 16 位全部是精確的,但如果為了保證 100% 的準確率的話,還是需要舍入到第 15 位。另外一個細節,上面的例子其實有一個問題,就是使用 0.1++ 這種方式的時候,實際累加的是一個不精確的數字,所以有可能造成誤差的放大。不過這裡依然沒有改正,是因為 0.5 那行,突然恢復真值了。這也不是因為後面藏了其他數字沒有顯示出來,我們來看一下:

>>> '%.60f'%(0.1+0.1+0.1+0.1+0.1)
'0.500000000000000000000000000000000000000000000000000000000000'
>>> print(Decimal(0.1+0.1+0.1+0.1+0.1))
0.5

這裡使用了一個格式限定符的示例。它的作用類似於 print Decimal。區別僅在於 Decimal 自己知道應該顯示多少位,而格式化限定符不知道。(一般雙精度浮點數轉換過來不超過 100 位)。因為不打算繼續深究了,所以就當這個“0.5”是個意外吧~如果想避免誤差疊加,可以寫成“i/10”的格式。

所以對於兩種,不像十六進位制和二進位制般正好是指數關係的進位制,永遠都無法在各自的某一位上具有相同的精度。即 2m = 10n 這個等式沒有使 m,n 同時為整數的解。但至少還可以構建一個精度包含的關係,比如上面 24 > 101 ,那麼我們就說“4 位二進位制精度高於 1 位十進位制精度”從而通過 4 位二進位制數轉儲 1 位十進位制數的時候,總是精確的,反之則不然。同理根據這個不等式:1015 < 253 <1016 ,雙精度浮點數的精度最高也就蘊含(不是等價)到十進位制的 15 位了。另外雖然這種轉化看起來浪費了很大的精度(第 16 位在很大概率上也是精確的),有趣的是,210 = 1024,卻和 103 = 1000 離的很近。因此一般我們可以通過這個關係來近似推導精度關係。