Python的浮點數損失精度問題
本篇討論的現象可以從下面這段指令碼體現出來:
>>> 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 位十進位制小數將此區間劃分的更細,即精度更高。
把 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 離的很近。因此一般我們可以通過這個關係來近似推導精度關係。
相關推薦
Python的浮點數損失精度問題
本篇討論的現象可以從下面這段指令碼體現出來:>>> 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
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
python學習03:Python浮點數精度問題(包含解決方案)
>>> 0.1+0.2 0.30000000000000004 >>> 0.1+0.1-0.2 0.0 >>> 0.1+0.1+0.1-0.3 5.551115123125783e-17 >>> 0.1+0.1+0.1-0
golang 浮點數 取精度的效率對比
浮點數 pre span now() 其他 shift pow 效率 log 需求 浮點數取2位精度輸出 實現 代碼 package main import ( "time" "log" "strconv" "fmt" )
python 浮點數保留小數
方式 decimal round logs gen 輸出 浮點數 .cn 保留 http://www.cnblogs.com/Raymon-Geng/p/5784290.html 這裏有三種方法, round(a,2) ‘%.2f‘ % a Decimal(‘5.000‘)
浮點數的精度問題
在生活中解決實際問題時,存在誤差是在所難免的,當然,浮點型別的數也存在誤差,當 遇到float,double和整型數比較大小時,並不僅僅時簡單的將兩個數放在一起比較了。要考慮到誤差。 例如 : 求 ax^2+bx+c=0 的實根 程式碼: #include <stdio.h
python 浮點數轉分數
limit rac rom () mina from pre class pan from fractions import Fraction value = 4.2 print(Fraction(value).limit_denominator())
關於浮點數的精度
程式碼: #include<iostream>using namespace std; //precision problems with floatint main(){ float a = 2.34E+22f; float
Python浮點數range
問題提出:最近在用matplotlib定義座標軸,需要會出現型如包含[0.1,0.2...]這樣的等差序列,實現這個函式略有難度,以下是我的解決過程,和解決辦法,希望對大家有所幫助。 最初覺得這個問題很簡單, 雖然python的range是不支援浮點數的,np可以實現出現下面的程式碼`
PHP浮點數的精度
2017年01月26日 17:38:25 加菲貓IN 閱讀數:261 個人分類: PHP
Python浮點數格式化-Decimal
從功能上說,小數物件decimal就像浮點數,只不過它們有固定的位數和小數點,因此小數是有固定的精度的浮點值。因此,常常使用Decimal小數型別來處理固定精度的數字。 常用方法 1. 通過呼叫在dec
python 浮點數和整數的比較
舉個栗子: #!/usr/bin/env python # encoding: utf-8 a = 10 b = 10 c = 100 d = 100 e = 10.0 f = 10.0 print(a is b) print(c is d) print(e
圖形程式設計中常用浮點數及其精度剖析
圖形學中經常要涉及使用各種格式的浮點數型別,如float,half,也會經常用到各種格式的浮點數型別PixelFormat,例如R10G10B10A2,R11G11B10,RGBHalf...。在合適的情況下使用合適的浮點數型別,是保證效率和效果的基礎。雖然有些是大學
【JS】關於js浮點數計算精度不準確問題的解決辦法
在專案中計算商品價格的時候再次遇到js浮點數計算出現誤差的問題,以前一碰到這個問題就用tofixed方法進行處理一下,這對於一個程式設計師來說是及其不嚴謹的。 1、發現問題: 在測試js浮點數進行加減乘除計算時,都可能出現問題,如下: console.log(0.1
關於js浮點數計算精度不準確問題的解決辦法
* ** method ** * add / subtract / multiply /divide * * ** explame ** * 0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004) * 0.2 + 0.4 ==
MYSQL浮點數float精度問題
遇到mysql設計的是6位float數經緯度,賦值的時候,明明是準去的資料insert進去,但是卻發現,資料自動變了,精度明顯丟失。檢視官方文件才發現 https://dev.mysql.com/doc/refman/5.7/en/problems-with-float.h
親歷浮點數計算精度導致錯誤
v0->v1是一條朝右下的線段,用下面程式碼計算點v3在直線上方還是下方 v0x = -254.923599 v0z = 365.348907 v1x = -249.52359 v1z =
js浮點數運算精度問題
大多數語言在處理浮點數的時候都會遇到精度問題,但是在JS裡似乎特別嚴重,來看一個例子 alert(45.6*13); 結果居然是592.800000000001,當然加法之類的也會有這個問題 那這是js的錯誤嗎? 當然不是,你的電腦做著正確的二進位制
浮點數計算精度丟失問題#W01
前幾日電話面試,被問到一個問題: “浮點數計算精度丟失的原因是什麼?”啊,啥? 我一臉懵逼,“就是在irb中,0.2+0.4不等於0.6,為什麼?“,面試官又補了一句。我沒有真正get到問題是什麼,胡說了一通。顯然,這是問到了我的知識盲區了,我從來沒有遇到過這個問題。於是我開
浮點數計算精度控制
進一步除錯我發現,浮點數計算錯誤發生在一個函式(InitEngine)呼叫後,在此函式呼叫前就不會出這個問題。這個函式是一個引擎的初時化函式,無法知道他的具體實現,是什麼原因造成了浮點計算錯誤呢? 編譯選項設定不當?應該不會吧,因為生成的彙編程式碼是正確的呀。在彙編程式碼正確的情況下,浮點計算精度太小,唯一