web-dev-qa-db-ja.com

高速Numpyループ

このコードはどのように最適化しますか(withoutこれは、計算のセマンティクスを使用することにつながります。多くの場合、自明ではありません):

slow_lib.py:
import numpy as np

def foo():
    size = 200
    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 += np.outer(val, val)

ポイントは、そのような種類のループは、いくつかのベクトル演算で二重和がある演算にかなり頻繁に対応することです。

これはかなり遅いです:

>>t = timeit.timeit('foo()', 'from slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 41.165681839

では、明日はそれをCynothizeして、型注釈を追加しましょう。明日はありません。

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

...えっ...何? Numbaが助けになりました!

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を呼び出す)、muchより高速なコード。
  • Matlab(R2016a)で同様のものを実装した場合、これはJITにより非常にうまく機能します。
12
ndbd

outerのコードは次のとおりです:

_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?)呼び出しはすべてのオーバーヘッドを持ちますが、すべての200行を含むouterへの1回の呼び出しには1つのオーバーヘッドセットがあり、その後に1つの高速コンパイル操作が続きます。

cythonnumbaはコンパイルしないか、Pythonのコードをouterでバイパスしません。それらができることは、あなたが書いた-そしてそれは多くの時間を消費していません。

詳細に踏み込むことなく、MATLAB jitは「外部」をより高速なコードに置き換えることができなければなりません-反復を書き換えます。しかし、MATLABでの私の経験は、そのjitより前の時代のものです。

cythonnumbaで実際の速度を向上させるには、プリミティブなnumpy/pythonコードを使用する必要があります。またはより良いまだ遅いインナーピースに努力を集中します。

outerを合理化されたバージョンに置き換えると、実行時間が約半分になります。

_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
_

完全な_N=200_では、関数はループごとに17秒かかりました。内側の2行をpass(計算なし)に置き換えると、ループあたりの時間が3msに減少します。言い換えると、外側のループメカニズムは、少なくともouter()への多くの呼び出しと比較して、それほど大きな時間の消費者ではありません。

14
hpaulj

メモリが許せば使用できます np.einsum これらの重い計算をベクトル化された方法で実行するには-

moo = size*np.einsum('ij,ik->jk',bar,bar)

np.tensordot -

moo = size*np.tensordot(bar,bar,axes=(0,0))

または単にnp.dot-

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

Cython、Numbaなどの多くのチュートリアルとデモンストレーションでは、これらのツールがコードを自動的に高速化できるように見えますが、実際にはそうではない場合がほとんどです。コードを少し変更して、最適なコードを抽出する必要があります。パフォーマンス。すでにある程度のベクトル化を実装している場合、通常はすべてのループを書き出すことを意味します。 Numpy配列演算が最適ではない理由は次のとおりです。

  • 多くの一時配列が作成され、ループされます。
  • 配列が小さい場合、呼び出しごとのかなりのオーバーヘッド。
  • 配列は全体として処理されるため、短絡ロジックを実装することはできません。
  • 時々、最適なアルゴリズムは配列式を使用して表現することができず、時間の複雑性がより悪いアルゴリズムに落ち着きます。

NumbaまたはCythonを使用しても、これらの問題は最適化されません。代わりに、これらのツールを使用すると、単純なPythonよりもはるかに高速なループ状のコードを作成できます。

また、Numbaの場合は特に、 "オブジェクトモード"と "nopythonモード" の違いに注意する必要があります。例のタイトなループは、大幅なスピードアップを提供するためにnopythonモードで実行する必要があります。ただし、_numpy.outer_は まだNumbaではサポートされていません であるため、関数はオブジェクトモードでコンパイルされます。 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
_
4
user2379410