【问题标题】:Vectorize a python loop over a numpy array在 numpy 数组上矢量化 python 循环
【发布时间】:2014-10-01 18:35:49
【问题描述】:

我需要加快这个循环的处理速度,因为它非常慢。但我不知道如何对其进行矢量化,因为一个值的结果取决于前一个值的结果。有什么建议吗?

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]

【问题讨论】:

    标签: python numpy


    【解决方案1】:

    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(nonvectorized) 正在创建一个轻量级包装器,因此是一种廉价的操作。

    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) 的结果?谢谢
    • 我之前尝试过安装 numba,但是太麻烦了(需要特定版本的 llvm,但我的发行版不可用,尽管网站上有声明,但安装后我遇到了版本冲突它们应该(暗示可以)并行安装。llvmpy 也有构建错误等)。而且我不相信它会帮助解决这类问题,所以我并没有真正说服它。但是这个答案鼓励我安装它。它工作得很好。但是另一个答案给了我几乎相同的速度,只需进行少量编辑并且没有繁琐的依赖。不过谢谢!
    • @ChristopherBrown,另一个答案加快了 3 倍,我观察到 320 倍,您的环境中有不同的数字吗?至于安装的麻烦,我建议使用 miniconda。它免费且简单,我在阅读您的问题后大约 1.5 分钟内安装了 numba - 也是第一次。
    • @immerrr:我没有看到你观察到的那种加速。我的非正式数字是:未优化:8.2 秒。语法优化(user3666197 答案):3.4 秒。 Numba 优化(你的答案):2.9 秒。所以对我来说,numba 是最好的速度,但不是很多。有趣的是,当我将这两种方法结合起来时,我的速度更接近纯语法解决方案而不是纯 numba 解决方案(这表明差异可能是测量误差,尽管我每个都运行了很多次)。我会相信你的话,并更仔细地研究 numba。当我有更多数据时会报告...
    • @ChristopherBrown,这很有趣......我同意由于依赖于以前的值,没有什么可以矢量化的,所以大幅加速的唯一选择是“更接近金属”,但是更接近金属的解决方案能够做得更好。如果这些数字在最新版本的 numba 上仍然存在,我会去 github 报告。不过,Cython 也是一种选择:通过一些类型提示,它也能够生成低级代码并全速运行:nbviewer.ipython.org/gist/immerrr/34c532963405eacbea76
    【解决方案2】:

    “前向依赖循环”代码的低向量化潜力

    一旦分析了依赖关系,您的“矢量化”并行性中的大部分都将被排除在外。 (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 倍的加速,请参阅Vectorised code for a PNG picture processing ( an OpenGL shader pipeline) 中的 RGBA-2D 矩阵加速处理

    性能仍提高了约 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 中看到的那么多,但非常接近。谢谢!
    • 在矢量化潜力最大的情况下,可能会看到另一组加速数据。 “...使用代码从 22.14 秒变为 0.1624 秒!”(Numpy >>> stackoverflow.com/a/26111541/3666197 和 MATLAB 的示例以及计算方法的复杂重新安排 >>> @987654323 @)
    猜你喜欢
    • 1970-01-01
    • 2018-10-04
    • 2021-04-29
    • 1970-01-01
    • 2011-02-09
    • 1970-01-01
    • 1970-01-01
    • 2018-10-15
    • 2021-11-22
    相关资源
    最近更新 更多