将Python循环遍历NumPy数组向量化

3

这个循环处理非常缓慢,我希望能加速它。但是,由于一个值的结果依赖于前一个值的结果,我不知道如何将其向量化。有什么建议吗?

import numpy as np

sig = np.random.randn(44100)
alpha = .9887
beta = .999

out = np.zeros_like(sig)

for n in range(1, len(sig)):
    if np.abs(sig[n]) >= out[n-1]:
        out[n] = alpha * out[n-1] + (1 - alpha) * np.abs( sig[n] )
    else:
        out[n] = beta * out[n-1]
2个回答

3

Numba的即时编译器应该很好地处理你面临的索引开销,通过在第一次执行期间将函数编译为本机代码:

In [1]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:import numpy as np
:
:sig = np.random.randn(44100)
:alpha = .9887
:beta = .999
:
:def nonvectorized(sig):
:    out = np.zeros_like(sig)
:
:    for n in range(1, len(sig)):
:        if np.abs(sig[n]) >= out[n-1]:
:            out[n] = alpha * out[n-1] + (1 - alpha) * np.abs( sig[n] )
:        else:
:            out[n] = beta * out[n-1]
:    return out
:--

In [2]: nonvectorized(sig)
Out[2]: 
array([ 0.        ,  0.01862503,  0.04124917, ...,  1.2979579 ,
        1.304247  ,  1.30294275])

In [3]: %timeit nonvectorized(sig)
10 loops, best of 3: 80.2 ms per loop

In [4]: from numba import jit

In [5]: vectorized = jit(nonvectorized)

In [6]: np.allclose(vectorized(sig), nonvectorized(sig))
Out[6]: True

In [7]: %timeit vectorized(sig)
1000 loops, best of 3: 249 µs per loop

编辑:根据评论建议,添加jit基准测试。 jit(非矢量化) 创建了一个轻量级的包装器,因此是一项廉价操作。

In [8]: %timeit jit(nonvectorized)
10000 loops, best of 3: 45.3 µs per loop

该函数本身在第一次执行时进行编译(因此是即时编译),这需要一些时间,但可能不会太长:

In [9]: %timeit jit(nonvectorized)(sig)
10 loops, best of 3: 169 ms per loop

请问您能否同时添加 %timeit vectorized = jit(nonvectorized) 的结果吗?谢谢。 - user3666197
我之前尝试过安装numba,但是太麻烦了(需要特定版本的llvm,而我的发行版没有提供,即使安装后也存在版本冲突,尽管网站声称它们可以并行安装)。llvmpy也有构建错误等问题。而且我不确定它是否能解决这种问题,所以我没有继续尝试。但是这个答案鼓励我安装它。它确实很好用。但是另一个答案通过微小的修改和没有繁琐依赖项就给了我几乎相同的加速效果。但还是谢谢! - Christopher Brown
1
@ChristopherBrown,另一个答案提高了3倍的速度,但我观察到了320倍的速度提升,你在你的环境中有不同的数字吗?至于安装的麻烦,我建议使用miniconda。它是免费且易于使用的,我在阅读了你的问题后第一次安装numba只用了大约1.5分钟。 - immerrr
@immerrr:我没有看到你所观察到的加速效果。我的非正式数字是:未优化:8.2秒。语法优化(user3666197回答):3.4秒。Numba优化(你的回答):2.9秒。所以对我来说,numba是最快的,但差别不大。有趣的是,当我结合这两种方法时,我得到的速度接近只使用语法的解决方案而不是只使用numba的解决方案(这表明差异可能是测量误差,尽管我运行了许多次)。我相信你的话,会更仔细地研究numba,并在有更多数据时进行报告… - Christopher Brown
@ChristopherBrown,这很有趣... 我同意由于依赖上一个值,没有什么可以向量化的东西,所以唯一能够加快速度的选择就是更靠近底层,但是更靠近底层的解决方案能够做得更好。如果这些数字在最新版本的numba上仍然存在,我会去github上报告这个问题。不过Cython也是一个选择:通过一些类型提示,它能够生成低级别代码并实现全速运行:http://nbviewer.ipython.org/gist/immerrr/34c532963405eacbea76 - immerrr

1

"前向依赖循环"代码的向量化潜力较低

一旦分析了依赖关系,大部分“向量化”并行性就被排除在外了。(即使JIT编译器也无法“反对”这种依赖障碍进行向量化)

您可以以向量化方式预先计算一些重复使用的值,但是没有直接的Python语法方式(没有外部JIT编译器的解决方法),将前向移位依赖循环计算安排到CPU向量寄存器对齐的共同并行计算中:

from zmq import Stopwatch    # ok to use pyzmq 2.11 for [usec] .Stopwatch()
aStopWATCH =    Stopwatch()  # a performance measurement .Stopwatch() instance

sig    = np.abs(sig)         # self-destructive calc/assign avoids memalloc-OPs
aConst = ( 1 - alpha )       # avoids many repetitive SUB(s) in the loop

for thisPtr in range( 1, len( sig ) ): # FORWARD-SHIFTING-DEPENDENCE LOOP:
    prevPtr = thisPtr - 1              # prevPtr->"previous" TimeSlice in out[] ( re-used 2 x len(sig) times )
    if sig[thisPtr] < out[prevPtr]:                                    # 1st re-use
       out[thisPtr] = out[prevPtr] * beta                              # 2nd
    else:
       out[thisPtr] = out[prevPtr] * alpha + ( aConst * sig[thisPtr] ) # 2nd

一个很好的向量化加速示例可以在这种情况下看到,即计算策略可以沿着本地numpy数组的1D、2D甚至3D结构并行/广播。要获得大约100倍的加速效果,请参见RGBA-2D矩阵加速处理的向量化代码PNG图片处理(OpenGL着色器管道)

性能增加仍然约为3倍

即使这个简单的python代码修订已经将速度提高了大约2.8倍(现在,即没有进行安装以允许使用特定的JIT优化编译器):

>>> def aForwardShiftingDependenceLOOP(): # proposed code-revision
...     aStopWATCH.start()                # ||||||||||||||||||.start
...     for thisPtr in range( 1, len( sig ) ):
...         #        |vvvvvvv|------------# FORWARD-SHIFTING-LOOP DEPENDENCE
...         prevPtr = thisPtr - 1  #|vvvvvvv|--STEP-SHIFTING avoids Numpy syntax
...         if ( sig[ thisPtr] < out[prevPtr] ):
...             out[  thisPtr] = out[prevPtr] * beta
...         else:
...             out[  thisPtr] = out[prevPtr] * alpha + ( aConst * sig[thisPtr] )
...     usec = aStopWATCH.stop()          # ||||||||||||||||||.stop
...     print usec, " [usec]"

>>> aForwardShiftingDependenceLOOP()
57593  [usec]
57879  [usec]
58085  [usec]

>>> def anOriginalForLOOP():
...     aStopWATCH.start()
...     for n in range( 1, len( sig ) ):
...         if ( np.abs( sig[n] ) >= out[n-1] ):
...             out[n] = out[n-1] * alpha + ( 1 - alpha ) * np.abs( sig[n] )
...         else:
...             out[n] = out[n-1] * beta
...     usec = aStopWATCH.stop()
...     print usec, " [usec]"

>>> anOriginalForLOOP()
164907  [usec]
165674  [usec]
165154  [usec]

我通过这些简单的改变得到的加速效果并不像使用numba看到的那样显著,但非常接近。谢谢! - Christopher Brown
在某些情况下,可以看到另一组加速比例的数据,其中矢量化潜力达到最大。例如,在使用该代码的情况下,执行时间从22.14秒减少到了0.1624秒!(有关Numpy的示例,请参见>>> http://stackoverflow.com/a/26111541/3666197;有关MATLAB并采用复杂重新排列计算方法的示例,请参见>>> https://dev59.com/dYPba4cB1Zd3GeqPxdKu#26102440) - user3666197

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