【沉浸式解决问题】浮点数计算精度误差,round后值错误,0.1+0.2不等于0.3?

一、问题描述

早就知道数据类型浮点数的精确度问题,但是一直没有实际遇到错误,没什么感觉,最近在生成小数后进行比较时发现本该相同的值却不相同,导致了判断错误,今天来深入研究一下。


二、场景还原

在python中存储一个统计字典时,key设计为从0到1分100个区间的中点,也就是0.005,0.015,0.025,0.035,,,0.985,0.995。
后面通过迭代统计列表生成统计字典

# 计算每个区间的中心位置
bin_centers = {round(i/100, 3) + 0.005: 0 for i in range(100)}
# 创建频数字典
data = {round(center, 3): int(count) for center, count in zip(bin_centers, counts)}

再生成一个总的统计字典

all_data = {round(i/100, 3) + 0.005: 0 for i in range(100)}

然后把子统计字典加到总的统计字典中

all_data = {bin: all_data[bin] + data[bin] for bin in all_data}

功能就是不断的把子统计数据合并到总的字典中,模拟一个counts展示一下

counts = [20]*100
bin_centers = {round(i/100, 3) + 0.005: 0 for i in range(100)}
data = {round(center, 3): count for center, count in zip(bin_centers, counts)}
all_data = {round(i/100, 3) + 0.005: 0 for i in range(100)}
all_data = {bin: all_data[bin] + data[bin] for bin in all_data}

报错data字典中没有0.034999999999999996这个key,那本来就不该有啊,那就是all_data中有,对比两个字典可以看出子字典多做了一次round保留3位小数
在这里插入图片描述
先打印下两个字典,不止0.035有误差,后面0.075还有好几个都对不上
在这里插入图片描述


三、原因分析

直接原因就是子字典多做了一次round3,改变了小数有效值,底层原因就是IEEE 754规范,浮点数本身就是有误差的,有误差的计算结果有误差也很正常。
大部分十进制小数都是无法用二进制精确表达的,就像十进制也有无限循环小数和无线不循环小数,

但是,为什么有的行,比如0.005就没误差,有的用round保留后就不一致了?

在Java中,float是单精度浮点数,占4字节,精度约6-7位;double是双精度浮点数,占8字节,精度约15-16位
在python中,只有float,代表双精度浮点数,精度约15-16位,

0.005转化为二进制后对应的10进制误差在第16位以后了,类似于0.005000000000000012345678,12345678是我编的,保留三位后是0.005,相当于0.0050000000000000,他们对应的二进制是完全一样的,后面的误差无法在IEEE 754规范中表示了,
而round(3/100, 3) + 0.005=0.034999999999999996,误差在有效精度内,此时保留3位后为0.0350000000000000,保留的误差就对二进制数造成区别了

提一下round(x, n) 的函数内部流程
a) 把二进制浮点 x 转成“足够多位”的十进制字符串(内部算法,常用 Ryū 或 Dragon4)。
b) 按十进制规则保留 n 位小数。
c) 再把得到的十进制结果 重新转成最接近的二进制浮点数。


四、解决方案

浮点数精度有误差,计算就会累加误差,我这里误差主要是round造成的,所以要么给总的统计字典也补一次,更合理的是统一在计算后再进行round

counts = [20]*100
bin_centers = [round(i/100 + 0.005, 3) for i in range(100)]
data = {center: count for center, count in zip(bin_centers, counts)}
all_data = {round(i/100 + 0.005, 3): 0 for i in range(100)}
all_data = {bin: all_data[bin] + data[bin] for bin in all_data}

如果是精确计算,用decimal

from decimal import Decimal

a = Decimal('0.1')
b = Decimal('0.005')
print(a + b)        # 0.105(精确)

五、0.1+0.2和0.3?

测试的时候遇到的float经典例子

print(0.1+0.2)
print(0.3)
print(0.1+0.2==0.3)
print("===")
print(round(0.1+0.2,16))
print(round(0.1+0.2,16)==0.3)
print(round(0.1+0.2,17))
print(round(0.1+0.2,17)==0.3)

可以看出,0.1+0.2造成的误差在第17位,这个和与0.3对应的二进制还不相同,只有当round保留16位以内的时候,两个值才会相等。通过这个例子也可以更好的理解round的作用
在这里插入图片描述

参考文献


喜欢的点个关注吧><!祝你永无bug!

/*
                   _ooOoo_
                  o8888888o
                  88" . "88
                  (| -_- |)
                  O\  =  /O
               ____/`---'\____
             .'  \\|     |//  `.
            /  \\|||  :  |||//  \
           /  _||||| -:- |||||-  \
           |   | \\\  -  /// |   |
           | \_|  ''\---/''  |   |
           \  .-\__  `-`  ___/-. /
         ___`. .'  /--.--\  `. . __
      ."" '<  `.___\_<|>_/___.'  >'"".
     | | :  `- \`.;`\ _ /`;.`/ - ` : | |
     \  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-'======
                   `=---='
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            佛祖保佑       永无BUG
*/
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

荔枝吻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值