在計算機求和的過程當中,一個大數和小數的相加會由於浮點數的有限精度,而致使截斷偏差的出現。因此在構建計算網格的時候,都要極力避免這樣情形的發生,將計算統一在相對較近的數量級上。因此,當須要對一系列的數值作加法時,一個好的技巧是將這些數由大到小作排列,再逐個相加。python
而若是必定要作出這樣的大數與小數的求和,一個直觀想法就是:大數部分和小數部分的高位相加,將剩餘的小數部分做爲單獨的「補全」部分相加。這種直觀想法的官方名稱叫作__Kahan求和法__。算法
假設當前的浮點數變量能夠保存6位
的數值。那麼,數值12345
與1.234
相加的理論值應該是12346.234
。但因爲當前只能保存6位數值,這個正確的理論值會被截斷爲12346.2,這就出現了0.034的偏差。當有不少這樣的大數與小數相加時,截斷偏差就會逐步累積,致使最後的計算結果出現大的誤差。code
先上僞代碼(看不懂不要着急,後面要逐一解釋):ci
def KahanSum(input): var sum = 0.0 var c = 0.0 for i = 1 to input.length do var y = input[i] + c # 在新的輸入中,加上以前累積的補所有分. var t = sum + y # 此時,將這個新的輸入加入到求和變量sum裏時,會形成低位部分被截斷 c = y - (t - sum) # 恢復上一步求和時被截斷的低位部分,累積進入補全變量 sum = t next i return sum
在上述僞代碼中,變量c
表示的便是小數的補所有分compensation,更嚴格地說,應該是__負的__補所有分。隨着這個補所有分的不斷積累,當這些截斷偏差積累到必定量級,它們在求和的時候也就不會被截斷了,從而可以相對好地控制整個求和過程的精度。input
如下,先用一個具體的理論例子來講明。好比,用it
$$10000.0 + \pi + \mathcal{e}$$io
來講明。class
咱們依舊假設浮點型變量只能保存6位數值。此時,具體寫出求和算式應該是:基礎
$$10000.0 + 3.14159 + 2.71828$$變量
它們的理論結果應該是10005.85987
,約等於10005.9
。
但因爲截斷偏差,第一次求和
$$10000.0 + 3.14159$$
只能獲得結果10003.1
;這個結果再與2.71828
相加,獲得10005.81828
,被截斷爲10005.8
。此時結果就相差了0.1
。
運用Kahan求和法,咱們的運行過程是(記住,咱們的浮點型變量__保存6位數值__),
y = 3.14159 + 0.00000 t = 10000.0 + 3.14159 = 10003.14159 = 10003.1 # 低位部分被截斷,丟失 c = 3.14159 - (10003.1 - 10000.0) = 3.14159 - 3.10000 = (.0415900) # 恢復丟失的截斷部分 sum = 1003.1
y = 2.71828 + (.0415900) = 2.75985 # 將上一步的補所有分增長在新的輸入上 t = 10003.1 + 2.75987 = 10005.85987 = 10005.9 # 當低位部分被累積得足夠大後,它就不會被大數的求和所截斷消失 c = 2.75987 - (10005.9 - 10003.1) = 2.75987 - 2.80000 = -.040130 sum = 10005.9
以上是理論分析。下面用一個能夠運行的Python代碼作示範,方便感興趣的朋友作研究。
這個例子曾經出現於Google的首席科學家_Vincent Vanhoucke_在Udacity上開設的__Deep Learning__課程。
這個求和算式是:在$10^9$的基礎上,加上$10^{-6}$,總共重複$10^6$次這個加法,再減去$10^9$,即
$$10^9 + 10^6*10^{-6} - 10^9$$
理論值顯然應該爲1
。
summ = 10**9 for indx in range(10**6): summ += 10**(-6) summ -= 10**9 print(summ) # output: 0.95367431640625
運行後,能夠貌似驚訝地看到結果居然不是1
,而是0.95367431640625
!
這能夠說明,在$10^6$次求和後,截斷偏差的累積量已經很是可觀了。
若是咱們用Kahan求和法來作改進,能夠獲得:
summ = 10**9 c = 0.0 for indx in range(10**6): y = 10**(-6) + c t = summ + y c = y - (t - summ) summ = t summ -= 10**9 print(summ) # output: 1.0
運行後,咱們能夠欣喜地看到正確結果:1.0。