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


    技术背景

    一个比较容易理解的概念,我们在做计算的过程中,很多时候都要做截断。不同精度的混合计算之间也会有截断,就比如一个float32单精度浮点数,符号占1位,指数占8位,尾数占23位。而一个float64双精度浮点数,符号占1位,指数占11位,尾数占52位。通常情况下,float32的有效数字约7位(按照223+1" role="presentation">223+1大小的范围计算),float64的有效数字为16位(按照252+1" role="presentation">252+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

  • 相关阅读:
    Milvus向量数据库:处理和分析大规模向量数据
    执行jar包中指定main方法
    51单片机之串口通信例程
    Java初始化顺序
    C++之设计模式
    DBW*的trace文件过大的bug
    重装系统后电脑图片显示不出来怎么办
    0 杂项知识
    JavaSE——方法、递归
    CSS详细基础(六)边框样式
  • 原文地址:https://www.cnblogs.com/dechinphy/p/18126246/float32_error