Python 快速Numpy循环

Python 快速Numpy循环,python,numpy,vectorization,cython,Python,Numpy,Vectorization,Cython,如何优化此代码(而不进行矢量化,因为这会导致使用计算的语义,这通常远远不是一件小事): 关键是这类循环经常对应于在某些向量运算上有双和的运算 这相当缓慢: >>t = timeit.timeit('foo()', 'from slow_lib import foo', number = 10) >>print ("took: "+str(t)) took: 41.165681839 好的,那么让我们将其虚拟化并添加类型注释,就像没有明天一样: c_slow_lib.py

如何优化此代码(而不进行矢量化,因为这会导致使用计算的语义,这通常远远不是一件小事):

关键是这类循环经常对应于在某些向量运算上有双和的运算

这相当缓慢:

>>t = timeit.timeit('foo()', 'from slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 41.165681839
好的,那么让我们将其虚拟化并添加类型注释,就像没有明天一样:

c_slow_lib.pyx:
import numpy as np
cimport numpy as np
import cython
@cython.boundscheck(False)
@cython.wraparound(False)

def foo():
    cdef int size = 200
    cdef int i,j
    np.random.seed(1000031212)
    cdef np.ndarray[np.double_t, ndim=2] bar = np.random.rand(size, size)
    cdef np.ndarray[np.double_t, ndim=2] moo = np.zeros((size,size), dtype = np.float)
    cdef np.ndarray[np.double_t, ndim=1] val
    for i in xrange(0,size):
        for j in xrange(0,size):
            val = bar[j]
            moo += np.outer(val, val)


>>t = timeit.timeit('foo()', 'from c_slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 42.3104710579
。。。ehr。。。什么?努巴去营救

numba_slow_lib.py:
import numpy as np
from numba import jit

size = 200
np.random.seed(1000031212)

bar = np.random.rand(size, size)

@jit
def foo():
    bar = np.random.rand(size, size)
    moo = np.zeros((size,size), dtype = np.float)
    for i in range(0,size):
        for j in range(0,size):
            val = bar[j]
            moo += np.outer(val, val)

>>t = timeit.timeit('foo()', 'from numba_slow_lib import foo', number = 10)
>>print("took: "+str(t))
took: 40.7327859402
那么,真的没有办法加快速度吗?重点是:

  • 如果我将内部循环转换为矢量化版本(构建一个表示内部循环的较大矩阵,然后在较大的矩阵上调用np.outer),我会得到更快的代码
  • 如果我在Matlab(R2016a)中实现类似的东西,由于JIT,它的性能相当好。

    • 如果内存允许,您可以使用以矢量化方式执行这些繁重的计算,如下所示-

      moo = size*np.einsum('ij,ik->jk',bar,bar)
      
      也可以使用-

      或者干脆
      np.dot
      -

      moo = size*bar.T.dot(bar)
      

      这是
      外部
      的代码:

      def outer(a, b, out=None):    
          a = asarray(a)
          b = asarray(b)
          return multiply(a.ravel()[:, newaxis], b.ravel()[newaxis,:], out)
      
      因此,对
      outer
      的每次调用都涉及许多python调用。它们最终调用编译后的代码来执行乘法。但每种方法都会产生与数组大小无关的开销

      因此,对
      outer
      的200(200**2?)调用将有所有的开销,而对
      outer
      的所有200行调用都有一个开销集,然后是一个快速编译操作

      cython
      numba
      不编译或绕过
      outer
      中的Python代码。他们所能做的就是简化您编写的迭代代码,而这不会花费太多时间

      在不深入细节的情况下,MATLAB jit必须能够用更快的代码替换“外部”——它重写了迭代。但是我对MATLAB的经验可以追溯到jit之前

      要使用
      cython
      numba
      实现真正的速度提升,您需要一直使用基本的numpy/python代码。或者最好把精力集中在缓慢的内部部分

      使用精简版替换
      外部
      ,可将运行时间缩短一半左右:

      def foo1(N):
              size = N
              np.random.seed(1000031212)
              bar = np.random.rand(size, size)
              moo = np.zeros((size,size), dtype = np.float)
              for i in range(0,size):
                      for j in range(0,size):
                              val = bar[j]
                              moo += val[:,None]*val   
              return moo
      

      使用full
      N=200
      时,您的函数每循环花费17秒。如果我将内部两行替换为
      pass
      (无计算),则每个循环的时间将降至3ms。换句话说,外循环机制不是一个大的时间消费者,至少与许多调用
      outer()

      的Cython、Numba等的教程和演示相比,它似乎可以自动加速代码,但在实践中,通常情况并非如此:您需要稍微修改代码以获得最佳性能。如果已经实现了某种程度的矢量化,通常意味着写出所有循环。Numpy阵列操作非最佳的原因包括:

      • 创建了大量临时阵列并进行循环
      • 如果阵列较小,则每次呼叫的开销较大
      • 不能实现短路逻辑,因为阵列是作为一个整体处理的
      • 有时,最优算法无法用数组表达式表示,而您只能选择时间复杂度更差的算法
      使用Numba或Cython不会优化这些问题!相反,这些工具允许您编写比普通Python快得多的循环代码

      另外,特别是对于Numba,您应该意识到两者之间的区别。示例中的紧密循环必须在nopython模式下运行,以提供任何显著的加速。但是,
      numpy.outer
      是,导致函数以对象模式编译。使用
      jit(nopython=True)
      进行装饰,让这种情况引发异常

      举例说明加速确实可行:

      import numpy as np
      from numba import jit
      
      @jit
      def foo_nb(bar):
          size = bar.shape[0]
          moo = np.zeros((size, size))
          for i in range(0,size):
              for j in range(0,size):
                  val = bar[j]
                  moo += np.outer(val, val)
          return moo
      
      @jit
      def foo_nb2(bar):
          size = bar.shape[0]
          moo = np.zeros((size, size))
          for i in range(size):
              for j in range(size):
                  for k in range(0,size):
                      for l in range(0,size):
                          moo[k,l] += bar[j,k] * bar[j,l]
          return moo
      
      size = 100
      bar = np.random.rand(size, size)
      
      np.allclose(foo_nb(bar), foo_nb2(bar))
      # True
      
      %timeit foo_nb(bar)
      # 1 loop, best of 3: 816 ms per loop
      %timeit foo_nb2(bar)
      # 10 loops, best of 3: 176 ms per loop
      

      您向我们展示的示例是一种低效的算法,因为您多次计算同一外积。由此产生的时间复杂度为O(n^4)。它可以减少到n^3

      for i in range(0,size):
          val = bar[i]
          moo += size * np.outer(val, val)
      

      cython和jit都没有加速,因为您已经在运行C代码(通过np.outer)。这里的问题实际上是循环本身,你需要改变它的内部结构,这样这些方法实际上可以被加速。我知道对内部(或两个)循环进行矢量化将显著加快代码的速度。我的观点是,显然,循环产生了一些不应该存在的重要开销。换句话说:为什么调用np.outer要比在矩阵上调用一次np.outer慢200倍,比如说200行(矢量化),而不是说Matlab循环,这不是问题。。。如何克服这个问题呢?我想我不能再帮上什么忙了,但是看看这个关于每个(Python和Matlab)如何处理循环的答案:有一点是调用它200次的函数开销。这在Python和MATLAB级别都会减慢速度。JIT已经显著地改善了这一点,不过最近NumPy可能需要跟上这一点(没有太多关于它的信息)。此外,您没有调用
      np.outer
      200次。你称它为40000次。谢谢,但我已经知道矢量化代码可以加快计算速度。有时很容易看到如何对代码进行矢量化(就像这里使用einsum所做的那样),但有时需要深入了解底层问题,并且在循环中编写代码要容易得多。那么该怎么办?@ndbd如果你想问一个关于如何加速代码的通用案例,我会说这要看情况而定。但根据我个人的经验,我发现NumPy UFUNC和函数(如
      einsum
      和基于点积的FUNC)在处理Python级别的向量化方法的乘法和约化时非常有用。对于一般情况,我真的不能说任何值得注意的话,对不起!
      import numpy as np
      from numba import jit
      
      @jit
      def foo_nb(bar):
          size = bar.shape[0]
          moo = np.zeros((size, size))
          for i in range(0,size):
              for j in range(0,size):
                  val = bar[j]
                  moo += np.outer(val, val)
          return moo
      
      @jit
      def foo_nb2(bar):
          size = bar.shape[0]
          moo = np.zeros((size, size))
          for i in range(size):
              for j in range(size):
                  for k in range(0,size):
                      for l in range(0,size):
                          moo[k,l] += bar[j,k] * bar[j,l]
          return moo
      
      size = 100
      bar = np.random.rand(size, size)
      
      np.allclose(foo_nb(bar), foo_nb2(bar))
      # True
      
      %timeit foo_nb(bar)
      # 1 loop, best of 3: 816 ms per loop
      %timeit foo_nb2(bar)
      # 10 loops, best of 3: 176 ms per loop
      
      for i in range(0,size):
          val = bar[i]
          moo += size * np.outer(val, val)