比较NumPy arange和自定义范围函数,以生成具有小数增量的范围

cs9*_*s95 8 python arrays numpy range

这是一个自定义函数,允许单步执行十进制增量:

def my_range(start, stop, step):
    i = start
    while i < stop:
        yield i
        i += step
Run Code Online (Sandbox Code Playgroud)

它的工作原理如下:

out = list(my_range(0, 1, 0.1))
print(out)

[0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, 0.8999999999999999, 0.9999999999999999]
Run Code Online (Sandbox Code Playgroud)

现在,这并不奇怪.这是可以理解的,这是因为浮点不准确并且0.1在内存中没有精确的表示.因此,这些精度误差是可以理解的.

就拿numpy在另一方面:

import numpy as np

out = np.arange(0, 1, 0.1)
print(out)
array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9]) 
Run Code Online (Sandbox Code Playgroud)

有趣的是,这里没有引入明显的不精确度.我认为这可能与__repr__节目有关,所以要确认,我试过这个:

x = list(my_range(0, 1.1, 0.1))[-1]
print(x.is_integer())

False

x = list(np.arange(0, 1.1, 0.1))[-1]
print(x.is_integer())

True
Run Code Online (Sandbox Code Playgroud)

所以,我的函数返回一个不正确的上限值(它应该是,1.0但它实际上是1.0999999999999999),但np.arange它是否正确.

我知道浮点数学是否破碎了?但这个问题的重点是:

numpy怎么做到这一点?

use*_*ica 10

端点的差异是因为NumPy预先计算长度而不是ad hoc,因为它需要预先分配数组.您可以在_calc_length帮助器中看到这一点.它不会在它到达结束参数时停止,而是在达到预定长度时停止.

预先计算长度不会使您免于出现非整数步骤的问题,并且您经常会得到"错误"的终点,例如numpy.arange(0.0, 2.1, 0.3):

In [46]: numpy.arange(0.0, 2.1, 0.3)
Out[46]: array([ 0. ,  0.3,  0.6,  0.9,  1.2,  1.5,  1.8,  2.1])
Run Code Online (Sandbox Code Playgroud)

使用它更安全numpy.linspace,而不是步长,你说你想要多少元素,以及是否要包括正确的端点.


看起来NumPy在计算元素时没有出现舍入错误,但这只是由于不同的显示逻辑.NumPy比float.__repr__没有更积极地截断显示的精度.如果您使用tolist普通的Python标量列表(以及普通的float显示逻辑),您可以看到NumPy也遇到了舍入错误:

In [47]: numpy.arange(0, 1, 0.1).tolist()
Out[47]: 
[0.0,
 0.1,
 0.2,
 0.30000000000000004,
 0.4,
 0.5,
 0.6000000000000001,
 0.7000000000000001,
 0.8,
 0.9]
Run Code Online (Sandbox Code Playgroud)

它的舍入误差略有不同 - 例如,在.6和.7而不是.8和.9 - 因为它还使用了不同的计算元素的fill方法,在相关dtype 的函数中实现.

fill功能实现具有它使用的优点start + i*step,而不是反复加入步骤中,这避免了在每次加入累积误差.然而,它的缺点是(由于没有令人信服的理由我可以看到)它从前两个元素重新计算步骤而不是将步骤作为参数,因此它在前面的步骤中会失去很大的精度.


hpa*_*ulj 6

虽然arange以稍微不同的方式逐步调整范围,但仍然存在浮点表示问题:

In [1358]: np.arange(0,1,0.1)
Out[1358]: array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9])
Run Code Online (Sandbox Code Playgroud)

印刷品隐藏了; 将其转换为列表以查看血淋淋的细节:

In [1359]: np.arange(0,1,0.1).tolist()
Out[1359]: 
[0.0,
 0.1,
 0.2,
 0.30000000000000004,
 0.4,
 0.5,
 0.6000000000000001,
 0.7000000000000001,
 0.8,
 0.9]
Run Code Online (Sandbox Code Playgroud)

或者与另一次迭代

In [1360]: [i for i in np.arange(0,1,0.1)]  # e.g. list(np.arange(...))
Out[1360]: 
[0.0,
 0.10000000000000001,
 0.20000000000000001,
 0.30000000000000004,
 0.40000000000000002,
 0.5,
 0.60000000000000009,
 0.70000000000000007,
 0.80000000000000004,
 0.90000000000000002]
Run Code Online (Sandbox Code Playgroud)

在这种情况下,每个显示的项目都是a np.float64,其中第一个是第一个float.


MSe*_*ert 5

除了列表和数组的不同表示之外,NumPys arange通过乘法而不是重复添加来工作.它更像是:

def my_range2(start, stop, step):
    i = 0
    while start+(i*step) < stop:
        yield start+(i*step)
        i += 1
Run Code Online (Sandbox Code Playgroud)

然后输出完全相等:

>>> np.arange(0, 1, 0.1).tolist() == list(my_range2(0, 1, 0.1))
True
Run Code Online (Sandbox Code Playgroud)

重复添加后,您将"累积"浮点舍入错误.乘法仍然受到舍入的影响,但误差不会累积.


正如评论中指出的那样,它并不是真正发生的事情.据我所知,它更像是:

def my_range2(start, stop, step):
    length = math.ceil((stop-start)/step)
    # The next two lines are mostly so the function really behaves like NumPy does
    # Remove them to get better accuracy...
    next = start + step
    step = next - start
    for i in range(length):
        yield start+(i*step)
Run Code Online (Sandbox Code Playgroud)

但不确定这是否完全正确,因为NumPy还有很多事情要发生.

  • 它可以.例如,如果开始为100且步长参数为0.1,则计算"(100 + 0.1) - 100"会导致NumPy使用实际步骤0.09999999999999432.这导致`numpy.arange(0,1000,0.1)[ - 1]`比`numpy.arange(100,1000,0.1)[ - 1]`更准确. (2认同)