浮点数怎样才能没有误差?

2021-03-10 10:45:58 浏览数 (2)

我们都知道,任何数据到了计算机中都只可能是二进制,浮点数也没有例外,正因为如此,有些浮点数在存储过程中会产生精度丢失,比如 0.2。那么有没有什么方式来阻止浮点数的精度丢失,其实很简单,自己实现一个浮点数的类然后定义各种方法不就行了吗?这确实可行,但是就没有别人帮我实现好吗?其实早就有了,它就是模块 decimal。

不要使用浮点数

我们先来初步看看模块 decimal 的使用方法,代码如下:

代码语言:javascript复制
>>> 1.0 // 0.2
4.0
>>> 1.0 % 0.2
0.19999999999999996
>>> import decimal
>>> decimal.Decimal(1.0) // decimal.Decimal(0.2)
Decimal('4')
>>> decimal.Decimal(1.0) % decimal.Decimal(0.2)
Decimal('0.1999999999999999555910790150')
>>> decimal.Decimal('1.0') // decimal.Decimal('0.2')
Decimal('5')
>>> decimal.Decimal('1.0') % decimal.Decimal('0.2')
Decimal('0.0')

上面两个命令我就不给大家讲解了,之前我在讲为什么有些小数在计算机中表示有误差?!为了发现浮点数的表示问题就给大家演示过同样的例子。之后导入模块 decimal,然后我们调用 decimal.Decimal 类分别传入 1.0 和0.2 这两个数创建了两个实例,其值分别是 1.0 和 0.2,但是我们发现整除的结果依旧是 4,求余的结果比上面更接近 0.2,但还是不等于 0.2。什么情况?!decimal 没用?!不不不,我们传入浮点数对应的字符串就会发现它有用了,精度没有丢失。那么为什么浮点数作为参数传进来会有精度的丢失?这是因为浮点数一旦被加载到计算机的存储单元中去它就已经丢失了精度,decimal 模块中的 Decimal 类可不会恢复精度!

当然这里只是演示了两个运算,其他的运算我就不进行演示了,就只是换换运算符而已,一点也不难!

虽然很方便,但是关于模块 decimal 还是有很多地方需要注意的,比如除不尽会怎么样?小数后面有 0 运算的结果后面会不会有 0?……

除不尽与修改精度

接下来我们先来看看除不尽会怎么样,直接上代码。

代码语言:javascript复制
>>> decimal.Decimal('1') / decimal.Decimal('7')
Decimal('0.1428571428571428571428571429')

我们可以发现小数点后面有 28 位,那么这个 28 位表示什么意思呢?是保留 28 位小数还是保留 28 位有效数字呢?

我们再来看一个例子,如下所示。

代码语言:javascript复制
>>> decimal.Decimal('1') / decimal.Decimal('70')
Decimal('0.01428571428571428571428571429')

在这里,我们可以发现小数点后面有 29 位,但是小数点后从第 1 个不是 0 的数往后数就是 28 位,因此,我们可以得出结论,这里的 28 位指的不是是 28 位小数,而是 28 位有效数字!

有效数字

接下来我们穿插一点数学内容,说一说有效数字是个什么鬼东西,有效数字指的是一个小数(表示成科学计数法,格式 a×10^n),其中 a 的位数(不算小数点)就是有效数字的位数,就是这么简单!

修改有效数字位数以及一些问题

修改有效数字位数其实很简单,只需要一行代码就可以了。

代码语言:javascript复制
>>> decimal.getcontext().prec = 1

很简单对不对?在我这里只弄了一位有效数字,我们来看看这样会有什么问题,问题代码如下:

代码语言:javascript复制
>>> decimal.getcontext().prec = 1
>>> decimal.Decimal('11')
Decimal('11')
>>> decimal.Decimal('1.1') * decimal.Decimal('10')
Decimal('1E 1')

我们可以发现,实例化的过程不会被有效数字位数影响,而运算的过程会被有效数字的位数影响!1.1 * 10 = 1.1 * 10¹,有效数字为 2 位,但是实例化之后发现并没有被指定的有效位数影响,下面进行运算可就被影响了,至于这里是如何影响的,需要再来让大家看几个例子。

代码语言:javascript复制
>>> decimal.Decimal('1.1') * decimal.Decimal('10')
Decimal('1E 1')
>>> decimal.Decimal('1.5') * decimal.Decimal('10')
Decimal('2E 1')
>>> decimal.Decimal('1.4') * decimal.Decimal('10')
Decimal('1E 1')
>>> decimal.Decimal('1.49999999999') * decimal.Decimal('10')
Decimal('1E 1')
>>> decimal.Decimal('2.5') * decimal.Decimal('10')
Decimal('2E 1')
>>> decimal.Decimal('2.500000000001') * decimal.Decimal('10')
Decimal('3E 1')

我们可以发现,它是靠到最接近的那一个有效数字,如果有两个有效数字离它一样近,最低有效位必须是偶数!

关于有效数字位数,最后我只说一件事:修改有效数字位数改的是有效数字位数的最大值!

小数末尾有 0 以及运算

我们接下来看看小数末尾有 0,运算之后 0 会不会丢,直接给代码。

代码语言:javascript复制
>>> decimal.Decimal('1.00')
Decimal('1.00')
>>> decimal.Decimal('1.00')   decimal.Decimal('0.20')
Decimal('1.20')
>>> decimal.Decimal('1.00') - decimal.Decimal('0.20')
Decimal('0.80')
>>> decimal.Decimal('1.00') * decimal.Decimal('0.20')
Decimal('0.2000')
>>> decimal.Decimal('1.00') / decimal.Decimal('0.20')
Decimal('5')
>>> decimal.Decimal('1.000') / decimal.Decimal('0.20')
Decimal('5.0')
>>> decimal.Decimal('1.0000') / decimal.Decimal('0.20')
Decimal('5.00')

我们可以发现实例化之后末尾的 0 不丢,运算的时候 和 - 一样,小数点对齐算,末尾多余的 0 不丢,乘法末尾对齐,算完之后移动小数点,末尾的 0 不丢。至于除法嘛,和小学讲的一样,同时扩倍,把除数变成一个整数,小数位数看扩倍之后的被除数有几位小数,以倒数第二行输入为例,扩倍之后被除数为 100.0(1 位小数),因此商值也应该有 1 位小数,所以结果为 5.0。

如果有效数字的最大位数不允许,0 还会不会丢?下面我们以乘法为例,看看有效数字的最大位数太小,小数末尾的 0 会不会丢,代码如下:

代码语言:javascript复制
>>> decimal.getcontext().prec = 1
>>> decimal.Decimal('1.00') * decimal.Decimal('0.20')
Decimal('0.2')
>>> decimal.getcontext().prec = 2
>>> decimal.Decimal('1.00') * decimal.Decimal('0.20')
Decimal('0.20')

我们可以发现,当有效数字的最大位数不允许时,末尾的 0 超出有效数字的最大位数时自然丢失,只留有效数字的最大位数的一部分(末尾的 0 也要留下)。

负数整除和求余

接下来我们来看看负数整除和求余,是不是和之前一样,我们先来看看之正常的整除和求余。

代码语言:javascript复制
>>> 10 // -3
-4
>>> 10 % -3
-2
>>> -10 // 3
-4
>>> -10 % 3
2
>>> -10 // -3
3
>>> -10 % -3
-1

整除是返回不大于商的最大整数,整除的结果乘上除数再加上余数就是被除数。把它们都换成 decimal.Decimal 实例还一样吗?

代码语言:javascript复制
>>> decimal.Decimal(10) // decimal.Decimal(-3)
Decimal('-3')
>>> decimal.Decimal(10) % decimal.Decimal(-3)
Decimal('1')
>>> decimal.Decimal(-10) // decimal.Decimal(3)
Decimal('-3')
>>> decimal.Decimal(-10) % decimal.Decimal(3)
Decimal('-1')
>>> decimal.Decimal(-10) // decimal.Decimal(-3)
Decimal('3')
>>> decimal.Decimal(-10) % decimal.Decimal(-3)
Decimal('-1')

在这里整除变掉了,变成和 C 语言整数除是差不多一回事了,向 0 走,求余的逻辑并没有变,结果变是因为整除的结果变了。

结论

最后做个总结,我们可以发现 decimal 虽好,但它不能时时刻刻如你所愿。第一,你实例化的时候传入浮点数精度不会恢复;第二,除不尽不会一直除(有效数字的最大位数给了限制);第三,小数末尾的 0 只要有效数字的最大位数允许,运算之后就不会丢;第四,负数的整除变成向 0 走。

当然,关于模块 decimal 还有很多东西。大家可以自己参考一下官方文档,我就不一一讲解了。

当然,我从今年开始已经入驻 B 站了!下面给出 B 站账号:新时代的运筹帷幄,喜欢的可以关注一下,看完视频不要忘记一键三连啊!

今天的文章有不懂的可以后台回复“加群”,备注:Python 机器学习算法说书人,不备注可是会被拒绝的哦~!

0 人点赞