单精度浮点数计算误差与消减方法

技术背景

一个比较容易理解的概念,我们在做计算的过程中,很多时候都要做截断。不同精度的混合计算之间也会有截断,就比如一个float32单精度浮点数,符号占1位,指数占8位,尾数占23位。而一个float64双精度浮点数,符号占1位,指数占11位,尾数占52位。通常情况下,float32的有效数字约7位(按照\(2^{23+1}\)大小的范围计算),float64的有效数字为16位(按照\(2^{52+1}\)大小的范围计算)。那么问题就出现了,如果把一个双精度的浮点数转换成一个单精度的浮点数,就相当于舍弃了9位的有效数字,这就是做了一个截断。在一些特定的计算场景中,这种截断误差有可能会被累积,最终导致结果的错误。

累积误差

我们尝试用Python+Numpy来实现一个这样的示例:

import numpy as np
np.random.seed(1)
sum_1 = np.array([0.], np.float64)
sum_2 = np.array([0.], np.float32)
for _ in range(100000):
    x = np.random.random(1000)
    sum_1 += x.sum()
    sum_2 += x.astype(np.float32).sum()
print (sum_1)
print (sum_2)

输出结果为:

[50003352.04503618]
[50003708.]

虽然同样的都是做一个简单的累加操作,但是最终得到的结果是不同的。至于这个误差能否被接受,其实更多的取决于应用场景,有很多对精度要求比较敏感的计算场景会直接导致结果错误。

大数吃小数

大数吃小数说的是,在两个数字之间运算的时候,如果是两个数字差异比较大,那么其中的小数的有效数字有可能在运算中被截断。同样的我们用Python+Numpy演示一下这个场景:

import numpy as np
x = np.array([1000000.], np.float32)
y = np.array([0.01], np.float32)
print (x+y)

这个程序的执行输出为:

[1000000.]

我们发现y的贡献在这里就完全不体现,但其实如果是使用双精度浮点数进行计算的话:

import numpy as np
x = np.array([1000000.], np.float64)
y = np.array([0.01], np.float64)
print (x+y)

得到的结果为:

[1000000.01]

可以想到,如果在一个大数的基础上不断的去迭代一些小的数字,那么最终的结果也会有较大的误差,甚至有可能结果就是错的。

Kahan求和公式

最简单的来说,要解决这个问题,只要把计算精度改用双精度浮点数就可以了。但是使用双精度浮点数就意味着内存占用的翻倍,计算也会更加的耗时,而且有一些硬件可能根本就不支持使用双精度浮点数。这里还有一个方法,那就是Kahan求和公式。简单来说就是,每一步计算的误差可以归结为:(a+b)-a-b,那么就保存这个误差的计算结果,然后到下一步计算的时候再加回来就可以了。接下来把前面的两个案例都分别实现一下,首先是累加误差问题:

import numpy as np
np.random.seed(1)
sum_1 = np.array([0.], np.float64)
sum_2 = np.array([0.], np.float32)
sum_3 = np.array([0.], np.float32)
tmp_1 = np.array([0.], np.float32)
for _ in range(100000):
    x = np.random.random(1000)
    sum_1 += x.sum()
    sum_2 += x.astype(np.float32).sum()
    tmp_2 = x.astype(np.float32).sum() - tmp_1
    tmp_3 = sum_3 + tmp_2
    tmp_1 = (tmp_3 - sum_3) - tmp_2
    sum_3 = tmp_3
print (sum_1)
print (sum_2)
print (sum_3)

该程序输出结果为:

[50003352.04503618]
[50003708.]
[50003352.]

可以看到,在使用了Kahan求和公式之后,虽然还是使用的float32单精度浮点数,但其实结果精度已经比普通的单精度计算高了两个量级。另外再测试一下大数加小数的问题,这里我们也使用累加的形式测试,结果展示会更加明显一些:

import numpy as np
np.random.seed(1)
sum_1 = np.array([1000000.], np.float64)
sum_2 = np.array([1000000.], np.float32)
sum_3 = np.array([1000000.], np.float32)
tmp_1 = np.array([0.], np.float32)
for _ in range(100000):
    x = np.random.random(1000) * 1e-05
    sum_1 += x.sum()
    sum_2 += x.astype(np.float32).sum()
    tmp_2 = x.astype(np.float32).sum() - tmp_1
    tmp_3 = sum_3 + tmp_2
    tmp_1 = (tmp_3 - sum_3) - tmp_2
    sum_3 = tmp_3
print (sum_1)
print (sum_2)
print (sum_3)

输出结果为:

[1000500.03352045]
[1000000.]
[1000500.06]

这里我们看到一点是,如果不使用Kahan求和公式,这个小数即使被迭代100000次,也还是被忽略。而使用了Kahan求和公式之后,虽然还是略有误差,但是误差位已经超过了float32单精度浮点数第7位有效数字的范围,因此Kahan求和公式的精度还是非常高的。

总结概要

在使用浮点数计算时,尤其是在使用AI框架的过程中,我们往往使用的是float32单精度浮点数,这也跟GPU的硬件架构有关系。但是使用单精度浮点数的过程中,务必要考虑到累加误差和大数吃小数的问题,这两个问题在长时间的迭代过程中,有可能会直接导致计算结果就是错误的。而如果在计算的过程中使用Kahan求和公式,则可以避免这种大数吃小数的问题。Kahan求和公式的本质,就是把大数和小数分开进行计算,这样可以一定程度上达到接近于float64双精度浮点数的运算精度。

版权声明

本文首发链接为:https://www.cnblogs.com/dechinphy/p/float32_error.html

作者ID:DechinPhy

更多原著文章:https://www.cnblogs.com/dechinphy/

请博主喝咖啡:https://www.cnblogs.com/dechinphy/gallery/image/379634.html

posted @ 2024-04-10 16:54  DECHIN  阅读(583)  评论(2编辑  收藏  举报