前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单精度浮点数误差与消除方法

单精度浮点数误差与消除方法

作者头像
DechinPhy
发布2024-04-11 08:33:17
2360
发布2024-04-11 08:33:17
举报
文章被收录于专栏:Dechin的专栏Dechin的专栏

技术背景

一个比较容易理解的概念,我们在做计算的过程中,很多时候都要做截断。不同精度的混合计算之间也会有截断,就比如一个float32单精度浮点数,符号占1位,指数占8位,尾数占23位。而一个float64双精度浮点数,符号占1位,指数占11位,尾数占52位。通常情况下,float32的有效数字约7位(按照

2^{23+1}

大小的范围计算),float64的有效数字为16位(按照

2^{52+1}

大小的范围计算)。那么问题就出现了,如果把一个双精度的浮点数转换成一个单精度的浮点数,就相当于舍弃了9位的有效数字,这就是做了一个截断。在一些特定的计算场景中,这种截断误差有可能会被累积,最终导致结果的错误。

累积误差

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

代码语言:javascript
复制
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)

输出结果为:

代码语言:javascript
复制
[50003352.04503618]
[50003708.]

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

大数吃小数

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

代码语言:javascript
复制
import numpy as np
x = np.array([1000000.], np.float32)
y = np.array([0.01], np.float32)
print (x+y)

这个程序的执行输出为:

代码语言:javascript
复制
[1000000.]

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

代码语言:javascript
复制
import numpy as np
x = np.array([1000000.], np.float64)
y = np.array([0.01], np.float64)
print (x+y)

得到的结果为:

代码语言:javascript
复制
[1000000.01]

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

Kahan求和公式

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

代码语言:javascript
复制
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)

该程序输出结果为:

代码语言:javascript
复制
[50003352.04503618]
[50003708.]
[50003352.]

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

代码语言:javascript
复制
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)

输出结果为:

代码语言:javascript
复制
[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/

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术背景
  • 累积误差
  • 大数吃小数
  • Kahan求和公式
  • 总结概要
  • 版权声明
相关产品与服务
腾讯云服务器利旧
云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档