numpy.sum()在大数组上给出奇怪的结果

8

我似乎发现了使用numpy数组上的.sum()存在陷阱,但我无法找到解释。基本上,如果我尝试对一个大数组求和,那么我开始得到荒谬的答案,但这种情况会默默地发生,我不能很好地理解输出结果以便通过Google找到原因。

例如,以下内容按预期正常工作:

a = sum(xrange(2000)) 
print('a is {}'.format(a))

b = np.arange(2000).sum()
print('b is {}'.format(b))

为两者提供相同的输出:

a is 1999000
b is 1999000

然而,这并不起作用:
c = sum(xrange(200000)) 
print('c is {}'.format(c))

d = np.arange(200000).sum()
print('d is {}'.format(d))

给出以下输出:
c is 19999900000
d is -1474936480

在更大的数组上,也可能得到正面的结果。这更加隐蔽,因为我可能根本没有意识到有什么异常情况。例如:

e = sum(xrange(100000000))
print('e is {}'.format(e))

f = np.arange(100000000).sum()
print('f is {}'.format(f))

给出这个:
e is 4999999950000000
f is 887459712

我猜想这与数据类型有关,事实上,即使使用Python的float也似乎可以解决问题:

e = sum(xrange(100000000))
print('e is {}'.format(e))

f = np.arange(100000000, dtype=float).sum()
print('f is {}'.format(f))

给予:

e is 4999999950000000
f is 4.99999995e+15

我没有计算机科学背景,发现自己陷入了困境(也许这是一个重复的问题)。 我尝试过以下方法:

  1. numpy数组具有固定大小。 不行; 这个似乎表明我应该首先遇到MemoryError
  2. 我可能以某种方式安装了32位版本(可能不相关); 不行,我遵循这个并确认我的版本是64位的。
  3. 其他奇怪的sum行为示例; 不行()我找到了这个但我看不出它如何适用。

请问有人能简要解释一下我错过了什么,并告诉我需要阅读什么内容吗? 此外,除了记得每次定义dtype之外,是否有一种方法可以停止这种情况或发出警告?

可能相关:

Windows 7

numpy 1.11.3

在Python 2.7.9上运行Enthought Canopy。


1
可能是因为numpy整数依赖于C类型整数,而Python具有无限的整数范围。浮点数是...浮点数。它们可以非常高。 - Jean-François Fabre
1
检查 np.arange(5).dtype。它可能使用的是32位整数而不是64位。此外,请确保您在同一个Python安装上执行所有这些检查。 - user2357112
看起来像是某种溢出...整数的符号似乎也被覆盖了,这可能是你有时会得到负结果的原因。 - Nicolas Heimann
@user2357112 确实打印出了 dtype('int32') - roganjosh
4个回答

7
在Windows上(包括64位系统),如果你将Python整数转换为NumPy的默认整数,则使用的是32位。在Linux和Mac上,使用的是64位。
指定一个64位整数即可解决:
d = np.arange(200000, dtype=np.int64).sum()
print('d is {}'.format(d))

输出:

c is 19999900000
d is 19999900000

虽然不太优雅,但你可以使用猴子补丁functools.partial来进行一些操作:

from functools import partial

np.arange = partial(np.arange, dtype=np.int64)

从现在开始,默认情况下,np.arange 将使用 64 位整数。


1
这对我来说很有意义,但我的一个关键问题是是否有一种方法来标记它。在调试时,这很容易被忽略;有没有一种方法可以识别我错过了这个问题? - roganjosh
添加了一种可能的解决方案。 - Mike Müller
请参见https://dev59.com/WVoV5IYBdhLWcg3wauOP,其默认使用C长整型,在Windows上被定义为32位整数,即使在64位CPU上也是如此。 - Martijn Pieters

6

很明显,这是 numpy 的整型溢出了 32 位。通常情况下,您可以使用 np.seterr 配置 numpy 在此类情况下失败:

>>> import numpy as np
>>> np.seterr(over='raise')
{'divide': 'warn', 'invalid': 'warn', 'over': 'warn', 'under': 'ignore'}
>>> np.int8(127) + np.int8(2)
FloatingPointError: overflow encountered in byte_scalars

然而,sum 的行为被明确记录为“在溢出时不会引发错误”,因此您可能会遇到麻烦。使用numpy通常是方便性与性能之间的权衡!但是,您可以手动指定累加器的数据类型,如下所示:
>>> a = np.ones(129)
>>> a.sum(dtype=np.int8)  # will overflow
-127
>>> a.sum(dtype=np.int64)  # no overflow
129

请关注#593号问题单,因为这是一个公开的问题,numpy开发人员可能会在某个时候修复它。


非常感谢您包含这张票。我怀疑它是一个溢出,但缺乏任何警告让我怀疑实际发生了什么。再加上不知道Windows自动将int转换为32位,让我感到困惑。 - roganjosh

3

我不是numpy专家,但可以在纯Python中重现您的arange(200000)结果:

>>> s = 0
>>> for i in range(200000):
...     s += i
...     s &= 0xffffffff
>>> s
2820030816
>>> s.bit_length()
32
>>> s - 2**32  # adjust for that "the sign bit" is set
-1474936480

换句话说,你所看到的结果是我预期的结果,如果使用有符号2的补码32位整数进行算术运算,numpy将会执行这样的操作。
由于我不是一个numpy专家,所以我无法建议一个好的方法来避免惊奇(我本可以将此作为注释留下,但那时我无法展示漂亮格式的代码)。

1
从“二进制补码”开始搜索。在各种固定位宽(8、16、32、64)中,这是几乎所有计算机硬件用于表示有符号整数的方案。在像C这样的低级语言中(CPython和numpy编写的语言),本地硬件类型就是您要使用的类型。在幕后实现Python无限宽度的二进制补码整数的幻觉需要大量的代码。 - Tim Peters
非常感谢,我正在阅读它。那个“很多代码在底层”让我免受了很多困扰,谢谢 :) 在这种情况下,Wim在他的答案中链接到了一个关于numpy的公开票据;已经认识到在这种特殊情况下没有溢出错误(这就是让我困惑的原因),但是它已经开放了很多年了 :( - roganjosh

2
Numpy 的默认整数类型与 C 语言的 long 类型相同。但在 64 位平台上,这并不能保证是 64 位。实际上,在 Windows 上,long 总是 32 位。因此,numpy 的求和会导致值溢出并循环回来。不幸的是,据我所知,没有办法更改默认的 dtype。您必须每次将其指定为 np.int64。您可以尝试创建自己的 arange:
def arange(*args, **kw):
    return np.arange(dtype=np.int64, *args, **kw)

然后使用该版本而不是numpy的版本。

编辑:如果您想标记此内容,您可以在代码顶部放置以下内容:

assert np.array(0).dtype.name != 'int32', 'This needs to be run with 64-bit integers!'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接