一个超大数与一个超小数求和,会发生什么呢

背景

在计算机求和的过程当中,一个大数和小数的相加会由于浮点数的有限精度,而致使截断偏差的出现。因此在构建计算网格的时候,都要极力避免这样情形的发生,将计算统一在相对较近的数量级上。因此,当须要对一系列的数值作加法时,一个好的技巧是将这些数由大到小作排列,再逐个相加。python

而若是必定要作出这样的大数与小数的求和,一个直观想法就是:大数部分和小数部分的高位相加,将剩余的小数部分做为单独的“补全”部分相加。这种直观想法的官方名称叫作__Kahan求和法__。算法

假设当前的浮点数变量能够保存6位的数值。那么,数值123451.234相加的理论值应该是12346.234。但因为当前只能保存6位数值,这个正确的理论值会被截断为12346.2,这就出现了0.034的偏差。当有不少这样的大数与小数相加时,截断偏差就会逐步累积,致使最后的计算结果出现大的误差。code

Kahan算法:

先上伪代码(看不懂不要着急,后面要逐一解释):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算法作改进

若是咱们用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。

相关文章
相关标签/搜索