快速对NumPy数组进行逐元素求和

12

假设我想对一个NumPy数组列表执行逐元素求和操作:

tosum = [rand(100,100) for n in range(10)]

我一直在寻找最好的方法来做这件事。看起来numpy.sum很糟糕:

timeit.timeit('sum(array(tosum), axis=0)',
              setup='from numpy import sum; from __main__ import tosum, array',
              number=10000)
75.02289700508118
timeit.timeit('sum(tosum, axis=0)',
              setup='from numpy import sum; from __main__ import tosum',
              number=10000)
78.99106407165527

Reduce的速度要快得多(几乎相当于两个数量级):

timeit.timeit('reduce(add,tosum)',
              setup='from numpy import add; from __main__ import tosum',
              number=10000)
1.131795883178711

看起来,使用reduce比非NumPy的sum有更明显的优势(请注意,这些是针对1e6次运行而不是上面时间的1e4次运行):

timeit.timeit('reduce(add,tosum)',
              setup='from numpy import add; from __main__ import tosum',
              number=1000000)
109.98814797401428

timeit.timeit('sum(tosum)',
              setup='from __main__ import tosum',
              number=1000000)
125.52461504936218

还有其他方法我应该尝试吗?有人可以解释一下排名吗?


编辑

如果列表先转换为numpy数组,则numpy.sum显然更快:

tosum2 = array(tosum)
timeit.timeit('sum(tosum2, axis=0)',
              setup='from numpy import sum; from __main__ import tosum2',
              number=10000)
1.1545608043670654

然而,我只有兴趣做一次求和,所以将数组转换为numpy数组仍会带来实际的性能惩罚。


2
我猜np.sum首先创建一个数组,然后对其求和,这可能解释了它的性能不佳... 我猜如果一开始就传递了一个np.ndarray,那么它应该是最快的。 - mgilson
1
我期望reduce比sum快大约1/11,因为它跳过了在sum中隐含的“0 + tosum [0]”。 - mgilson
这很有道理。我开始有一堆单独的数组,所以首先将它们转换为numpy数组会产生与让sum为我执行相同的性能惩罚(因为我只做一次求和)相同的性能惩罚。 - lnmaurer
2个回答

5
以下代码与reduce功能相似,如果tosum列表足够长,则速度更快。但它并不比reduce(add, tosum) 快很多,并且代码更复杂。
def loop_inplace_sum(arrlist):
    # assumes len(arrlist) > 0
    sum = arrlist[0].copy()
    for a in arrlist[1:]:
        sum += a
    return sum

对于原始的 tosum 来说,reduce(add, tosum) 更快:

In [128]: tosum = [rand(100,100) for n in range(10)]

In [129]: %timeit reduce(add, tosum)
10000 loops, best of 3: 73.5 µs per loop

In [130]: %timeit loop_inplace_sum(tosum)
10000 loops, best of 3: 78 µs per loop

处理更长的数组列表时,现在loop_inplace_sum更快。

In [131]: tosum = [rand(100,100) for n in range(500)]

In [132]: %timeit reduce(add, tosum)
100 loops, best of 3: 5.09 ms per loop

In [133]: %timeit loop_inplace_sum(tosum)
100 loops, best of 3: 4.4 ms per loop

有趣。你认为这种加速是从哪里来的?也许它比reduce的开销小?(我只是在将大约10个大数组相加,所以我可能会坚持使用reduce,但这种方法对于未来了解很有好处。) - lnmaurer
是的,开销更小:原地加法消除了一些对象的创建。如果你用sum += a替换成sum = sum + a,它会比reduce慢一点。 - Warren Weckesser

1
Numpy求和并不糟糕,你只是在错误地使用numpy。如果将普通的Python函数(包括reduce!)、循环和列表与numpy数组结合使用,就无法利用numpy的速度优势。如果你想让你的代码快速运行,必须仅使用numpy
由于你在代码片段中没有指定任何导入,我不确定randn函数在做什么或它来自哪里,所以我只是假设tosum应该表示一些随机数的10个矩阵的列表。下面的代码片段显示了numpy绝对不像你声称的那么慢:
import numpy as np
import timeit

def test_np_sum(n=10):
    # n represents the numbers of matrices to sum up element wise
    tosum = np.random.randint(0, 100, size=(n, 10, 10)) # n 10x10 matrices, shape = (n, 10, 10)
    summed = np.sum(tosum, axis=0) # shape = (10, 10)

然后进行测试:

timeit.timeit('test_np_sum()', number=10000, setup='from __main__ import test_np_sum')

0.8418250999999941

这并没有回答我的问题,也没有提供任何新信息。我明确指出我想要对numpy数组列表求和,而不是一个三维的numpy数组。你正在做后者。我已经在“编辑”部分调查过使用三维numpy数组了。此外,我提供的基准测试结果并非虚构。代码的速度正如我所声称的那样慢。(这篇文章现在已经超过7年了,也许事情已经发生了改变。) - lnmaurer
好的,你说得对,我没有添加任何新信息。如果将列表转换为numpy数组会导致如此大的性能损失,为什么不一开始就只使用numpy数组呢? - Kevin Südmersen

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