web-dev-qa-db-ja.com

numpyとmatlabのパフォーマンスの違い

スパースオートエンコーダのbackpropagationアルゴリズムを計算しています。 python using numpymatlabに実装しました。コードはほとんど同じですが、パフォーマンスは大きく異なります。MATLABがタスクを完了するのは0.252454秒で、numpy 0.973672151566は約4倍です。このコードを後で最小化問題で数回呼び出します。この違いにより、実装間で数分の遅延が発生します。これは通常の動作ですか? numpyでパフォーマンスを向上させますか?

Numpyの実装:

Sparse.rhoは調整パラメーター、sparse.nodesは非表示層のノード数(25)、sparse.input(64)は入力層のノード数、theta1とtheta2は最初の重み行列、寸法がそれぞれ25x64および64x25の2番目のレイヤー、mは10000に等しく、最も低い寸法は(25、)の寸法、xは10000x64、a3 10000x64およびa2 10000x25の寸法です。

UPDATE:応答のアイデアの一部に従って、コードに変更を加えました。パフォーマンスは現在numpy:0.65 vs matlab:0.25です。

partial_j1 = np.zeros(sparse.theta1.shape)
partial_j2 = np.zeros(sparse.theta2.shape)
partial_b1 = np.zeros(sparse.b1.shape)
partial_b2 = np.zeros(sparse.b2.shape)
t = time.time()

delta3t = (-(x-a3)*a3*(1-a3)).T

for i in range(m):

    delta3 = delta3t[:,i:(i+1)]
    sum1 =  np.dot(sparse.theta2.T,delta3)
    delta2 = ( sum1 + sum2 ) * a2[i:(i+1),:].T* (1 - a2[i:(i+1),:].T)
    partial_j1 += np.dot(delta2, a1[i:(i+1),:])
    partial_j2 += np.dot(delta3, a2[i:(i+1),:])
    partial_b1 += delta2
    partial_b2 += delta3

print "Backprop time:", time.time() -t

Matlabの実装:

tic
for i = 1:m

    delta3 = -(data(i,:)-a3(i,:)).*a3(i,:).*(1 - a3(i,:));
    delta3 = delta3.';
    sum1 =  W2.'*delta3;
    sum2 = beta*(-sparsityParam./rhoest + (1 - sparsityParam) ./ (1.0 - rhoest) );
    delta2 = ( sum1 + sum2 ) .* a2(i,:).' .* (1 - a2(i,:).');
    W1grad = W1grad + delta2* a1(i,:);
    W2grad = W2grad + delta3* a2(i,:);
    b1grad = b1grad + delta2;
    b2grad = b2grad + delta3;
end
toc
22
pabaldonedo

「Matlabは常にNumPyよりも速い」と言ったり、その逆を言ったりするのは間違っています。多くの場合、それらのパフォーマンスは同等です。 NumPyを使用する場合、良好なパフォーマンスを得るには、NumPyの速度はC/C++/Fortranで記述された基本的な関数を呼び出すことから生じることを覚えておく必要があります。これらの関数を配列全体に適用すると、うまく機能します。一般に、Pythonループで小さな配列またはスカラーに対してこれらのNumPy関数を呼び出すと、パフォーマンスが低下します。

Pythonループの何が問題になっていますか?Pythonループのすべての反復はnextメソッドの呼び出しです。[]のすべての使用インデックス作成は__getitem__メソッドへの呼び出しです。すべての+=__iadd__への呼び出しです。すべてのドット付き属性ルックアップ(np.dotなど)には関数呼び出しが含まれます。これらの関数呼び出しを追加すると速度が大幅に低下します。これらのフックによりPython表現力-文字列のインデックス付けは、たとえば、dictsのインデックス付けとは異なる意味です。同じ構文、異なる意味です。魔法は、オブジェクトに異なる__getitem__メソッドを与えることによって実現されます。

しかし、その表現力にはスピードが犠牲になります。そのため、動的表現をすべて必要としない場合は、パフォーマンスを向上させるために、配列全体のNumPy関数呼び出しに制限するようにしてください。

したがって、forループを削除します。可能な場合は、「ベクトル化された」方程式を使用します。たとえば、代わりに

for i in range(m):
    delta3 = -(x[i,:]-a3[i,:])*a3[i,:]* (1 - a3[i,:])    

idelta3を一度に計算できます:

delta3 = -(x-a3)*a3*(1-a3)

for-loopでは、delta3はベクトルですが、ベクトル化された方程式を使用すると、delta3は行列になります。


for-loopの一部の計算はiに依存しないため、ループの外で実行する必要があります。たとえば、sum2は定数のように見えます。

sum2 = sparse.beta*(-float(sparse.rho)/rhoest + float(1.0 - sparse.rho) / (1.0 - rhoest) )

これは、コード(alt)の代替実装(orig)を使用した実行可能な例です。

私のtimeitベンチマークでは、速度が6.8倍向上しています

In [52]: %timeit orig()
1 loops, best of 3: 495 ms per loop

In [53]: %timeit alt()
10 loops, best of 3: 72.6 ms per loop

import numpy as np


class Bunch(object):
    """ http://code.activestate.com/recipes/52308 """
    def __init__(self, **kwds):
        self.__dict__.update(kwds)

m, n, p = 10 ** 4, 64, 25

sparse = Bunch(
    theta1=np.random.random((p, n)),
    theta2=np.random.random((n, p)),
    b1=np.random.random((p, 1)),
    b2=np.random.random((n, 1)),
)

x = np.random.random((m, n))
a3 = np.random.random((m, n))
a2 = np.random.random((m, p))
a1 = np.random.random((m, n))
sum2 = np.random.random((p, ))
sum2 = sum2[:, np.newaxis]

def orig():
    partial_j1 = np.zeros(sparse.theta1.shape)
    partial_j2 = np.zeros(sparse.theta2.shape)
    partial_b1 = np.zeros(sparse.b1.shape)
    partial_b2 = np.zeros(sparse.b2.shape)
    delta3t = (-(x - a3) * a3 * (1 - a3)).T
    for i in range(m):
        delta3 = delta3t[:, i:(i + 1)]
        sum1 = np.dot(sparse.theta2.T, delta3)
        delta2 = (sum1 + sum2) * a2[i:(i + 1), :].T * (1 - a2[i:(i + 1), :].T)
        partial_j1 += np.dot(delta2, a1[i:(i + 1), :])
        partial_j2 += np.dot(delta3, a2[i:(i + 1), :])
        partial_b1 += delta2
        partial_b2 += delta3
        # delta3: (64, 1)
        # sum1: (25, 1)
        # delta2: (25, 1)
        # a1[i:(i+1),:]: (1, 64)
        # partial_j1: (25, 64)
        # partial_j2: (64, 25)
        # partial_b1: (25, 1)
        # partial_b2: (64, 1)
        # a2[i:(i+1),:]: (1, 25)
    return partial_j1, partial_j2, partial_b1, partial_b2


def alt():
    delta3 = (-(x - a3) * a3 * (1 - a3)).T
    sum1 = np.dot(sparse.theta2.T, delta3)
    delta2 = (sum1 + sum2) * a2.T * (1 - a2.T)
    # delta3: (64, 10000)
    # sum1: (25, 10000)
    # delta2: (25, 10000)
    # a1: (10000, 64)
    # a2: (10000, 25)
    partial_j1 = np.dot(delta2, a1)
    partial_j2 = np.dot(delta3, a2)
    partial_b1 = delta2.sum(axis=1)
    partial_b2 = delta3.sum(axis=1)
    return partial_j1, partial_j2, partial_b1, partial_b2

answer = orig()
result = alt()
for a, r in Zip(answer, result):
    try:
        assert np.allclose(np.squeeze(a), r)
    except AssertionError:
        print(a.shape)
        print(r.shape)
        raise

ヒント:コメントにすべての中間配列の形状が残っていることに注意してください。配列の形状を知ることで、コードが何をしているかを理解することができました。配列の形状は、正しいNumPy関数を使用するのに役立ちます。または、少なくとも、形状に注意を払うと、操作が適切かどうかを知るのに役立ちます。たとえば、計算すると

np.dot(A, B)

およびA.shape = (n, m)およびB.shape = (m, p)の場合、np.dot(A, B)は形状(n, p)の配列になります。


C_CONTIGUOUSの順序で配列を構築するのに役立ちます(少なくとも、np.dotを使用している場合)。そうすることで3倍ものスピードアップがあるかもしれません:

以下では、xxfと同じですが、xはC_CONTIGUOUSであり、xfはF_CONTIGUOUSであり、yについても同じ関係です。およびyf

import numpy as np

m, n, p = 10 ** 4, 64, 25
x = np.random.random((n, m))
xf = np.asarray(x, order='F')

y = np.random.random((m, n))
yf = np.asarray(y, order='F')

assert np.allclose(x, xf)
assert np.allclose(y, yf)
assert np.allclose(np.dot(x, y), np.dot(xf, y))
assert np.allclose(np.dot(x, y), np.dot(xf, yf))

%timeitベンチマークは、速度の違いを示しています。

In [50]: %timeit np.dot(x, y)
100 loops, best of 3: 12.9 ms per loop

In [51]: %timeit np.dot(xf, y)
10 loops, best of 3: 27.7 ms per loop

In [56]: %timeit np.dot(x, yf)
10 loops, best of 3: 21.8 ms per loop

In [53]: %timeit np.dot(xf, yf)
10 loops, best of 3: 33.3 ms per loop

Pythonのベンチマークについて:

誤解を招く可能性がありますtime.time()呼び出しのペアの違いを使用して、Pythonのコードの速度をベンチマークします。測定を何度も繰り返す必要があります。自動ガベージコレクターを無効にすることをお勧めします。クロックタイマーの不十分な解決によるエラーを回避し、time.time呼び出しのオーバーヘッドの重要性を減らすために、長い時間間隔(少なくとも10秒に相当する繰り返しなど)を測定することも重要です。すべてのコードを自分で書く代わりに、Pythonは timeit module を提供します。私は本質的にそれを使用してコードの時間を計っていますが、 IPythonターミナル から呼び出すと便利です。

これがベンチマークに影響しているかどうかはわかりませんが、違いが生じる可能性があることに注意してください。 私がリンクした質問 では、time.timeによると、2つのコードは1.7倍の違いがありましたが、timeitを使用したベンチマークは、本質的に同じ時間で実行されたコードを示しました。

46
unutbu

私は毎回新しい配列を割り当てることを避けるためにインプレース操作から始めます:

partial_j1 += np.dot(delta2, a1[i,:].reshape(1,a1.shape[1]))
partial_j2 += np.dot(delta3, a2[i,:].reshape(1,a2.shape[1]))
partial_b1 += delta2
partial_b2 += delta3

この式を置き換えることができます:

a1[i,:].reshape(1,a1.shape[1])

よりシンプルで高速に(Bi Ricoのおかげで):

a1[i:i+1]

また、この行:

sum2 = sparse.beta*(-float(sparse.rho)/rhoest + float(1.0 - sparse.rho) / (1.0 - rhoest))

各ループで同じように見えるので、再計算する必要はありません。

そして、おそらくマイナーな最適化として、x[i,:]をすべてx[i]に置き換えることができます。

最後に、m倍のメモリを割り当てる余裕がある場合は、nutbの提案に従い、ループをベクトル化できます。

for m in range(m):
    delta3 = -(x[i]-a3[i])*a3[i]* (1 - a3[i])

と:

delta3 = -(x-a3)*a3*(1-a3)

また、常にNumbaを使用して、ベクトル化せずに(さらにメモリを使用せずに)大幅に速度を上げることができます。

3
user2304916

Numpyとmatlabのパフォーマンスの違いは常に私を苛立たせてきました。結局のところ、最終的には基礎となるlapackライブラリーにまで煮詰められます。私の知る限り、matlabはデフォルトでフルアトラスlapackを使用していますが、numpyはラパックライトを使用しています。 Matlabは、スペースや大きさを気にしないと考えています。 同様の質問 良い答え。

1
Philliproso