Numpy以不同于python的方式进行回合

问题描述 投票:7回答:4

代码

import numpy as np
a = 5.92270987499999979065
print(round(a, 8))
print(round(np.float64(a), 8))

5.92270987
5.92270988

知道为什么吗?

在numpy来源中找不到任何相关内容。

更新: 我知道处理这个问题的正确方法是以这种差异无关紧要的方式构建程序。我做的。我在回归测试中偶然发现了它。

UPDATE2: 关于@VikasDamodar评论。人们不应该相信repr()功能:

>>> np.float64(5.92270987499999979065)
5.922709875
>>> '%.20f' % np.float64(5.92270987499999979065)
'5.92270987499999979065'

UPDATE3: 测试了python3.6.0 x32,numpy 1.14.0,win64。另外在python3.6.4 x64,numpy 1.14.0,debian。

UPDATE4: 只是要确定:

import numpy as np
a = 5.92270987499999979065
print('%.20f' % round(a, 8))
print('%.20f' % round(np.float64(a), 8))

5.92270987000000026512
5.92270988000000020435

Update5: 以下代码演示了在不使用str的情况下在哪个阶段发生的差异:

>>> np.float64(a) - 5.922709874
1.000000082740371e-09
>>> a - 5.922709874
1.000000082740371e-09
>>> round(np.float64(a), 8) - 5.922709874
6.000000496442226e-09
>>> round(a, 8) - 5.922709874
-3.999999442783064e-09

很明显,在应用'round'之前,它们是相同的数字。

Update6: 与@ user2357112的答案相反,np.round比圆形慢大约4倍:

%%timeit a = 5.92270987499999979065
round(a, 8)

1.18 µs ± 26.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)  

%%timeit a = np.float64(5.92270987499999979065)
round(a, 8)

4.05 µs ± 43.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

另外在我看来np.round做得更好,甚至比内置round更近:最初我得到这个5.92270987499999979065数字,除以11.84541975两个。

python numpy rounding-error rounding
4个回答
4
投票

float.__round__特别注意使用正确舍入的双字符串算法生成正确的舍入结果。

NumPy没有。 NumPy docs提到了这一点

由于IEEE浮点标准[R9]中的小数部分的不精确表示以及以10的幂进行缩放时引入的误差,结果也可能是令人惊讶的。

这样更快,但会产生更多的舍入误差。它会导致您所观察到的错误,以及错误,其中数字甚至更明确地低于截止值仍会被四舍五入:

>>> x = 0.33499999999999996
>>> x
0.33499999999999996
>>> x < 0.335
True
>>> x < Decimal('0.335')
True
>>> x < 0.67/2
True
>>> round(x, 2)
0.33
>>> numpy.round(x, 2)
0.34000000000000002

NumPy舍入的时间越来越慢,但这与舍入算法的速度没有任何关系。 NumPy和常规Python数学之间的任何时间比较都归结为NumPy针对整个阵列操作进行了优化。在单个NumPy标量上进行数学运算有很多开销,但使用numpy.round对整个数组进行舍入可以轻松地使用round舍入一系列浮点数:

In [6]: import numpy

In [7]: l = [i/7 for i in range(100)]

In [8]: a = numpy.array(l)

In [9]: %timeit [round(x, 1) for x in l]
59.6 µs ± 408 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [10]: %timeit numpy.round(a, 1)
5.27 µs ± 145 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

至于哪一个更准确,那肯定是float.__round__。你的数字更接近于5.92270987而不是5.92270988,而且它是圆形的,甚至是圆形 - 一切都是均匀的。这里没有关系。


0
投票

是的,另一种处理这类事情的方法是使用在python3中不那么慢的Decimal:

%%timeit d = D('11.84541975'); q = D('0.00000001')
(d/2).quantize(q)

485 ns ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

-1
投票

tldr

Builtin roundnumpy.round使用不同的舍入算法。对于大多数数字,它们的结果是相同的,但对于某些角落情况则大不相同

两者都适用于某些用途。

round对于标量更快,np.round对于阵列更快。

说明

•Builtin round使用直接的方法检查所请求的十进制数字。无论发生什么事情(即使它是...... 499999),它向下四舍五入,并且将5舍入到最近的偶数,除非之后有一个1(例如...... 500001),在这种情况下它会向上舍入。

np.round将数字乘以所请求的10的幂,通过普通规则舍入到最接近的int,然后以10的相同幂除去。

它为0.33 / 2等案例提供了更可预测的结果:

>>> 0.33/2
0.165
>>> round(0.33/2, 2)
0.17
>>> np.round(0.33/2, 2)
0.16

这里0.165应该四舍五入到最近的偶数,即0.16。

更新: 然而,对于像1.09 / 2这样的案件,它会遭遇四舍五入的错误(正如Mark Dickinson在评论中指出的那样):

>>> 1.09/2
0.545
>>> round(1.09/2, 2)
0.55
>>> np.round(1.09/2, 2)
0.55

我能想到的唯一解决方法是

>>> round(round(1.09*100)/2)/100
0.54

哪个有效,但远非普遍。


-2
投票

在我的特殊情况下,一种非常直接的方法来解决这两个函数之间的差异以获得一致的结果是通过乘法和除法。

对于我的应用程序,它似乎比本机round工作得更好,给出与np.round相同的结果:

'%.20f' % (round(a*1e8)/1e8)
'5.92270988000000020435'
'%.20f' % (round(np.float64(a)*1e8)/1e8)
'5.92270988000000020435'

更新 感谢@ user2357112我发现它正是np.round内部发生的事情(multiarray/calculation.c#L665),所以除非你在numpy和native python之间交叉测试你的结果,否则使用numpy版本的圆形没有那些额外的划分是安全的。 python级别的乘法。

UPDATE2 处理标量时,这种在python级别上的除法和乘法方法比本地round慢一些(~30%)但比np.round快得多(~3倍)(给出与np.round相同的结果):

%%timeit c = 11.84541975
round(c/2)
349 ns ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit c = 11.84541975
round(c*1e8/2)/1e8
519 ns ± 13 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit c = np.float64(11.84541975)
round(c/2)
1.67 µs ± 20.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit c = np.float64(11.84541975)
round(c*1e8/2)/1e8
2.01 µs ± 37.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

UPDATE3 Python的内置round使用了一种直接的方法,它只对完全可表示的二元有理数(如0.375(整数除以2的精确幂)进行“舍入到偶数”规则),从而有效地将此规则替换为所有其他数字'围绕领带号,恰好有49999表示下来,恰好以50001结束'。我不确定这个算法是好还是坏,但绝对不太适合手动检查。

© www.soinside.com 2019 - 2024. All rights reserved.