NumPyとMatlabのパフォーマンスの違い 質問する

NumPyとMatlabのパフォーマンスの違い 質問する

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

Numpy実装:

Sparse.rho はチューニング パラメーター、sparse.nodes は隠し層のノード数 (25)、sparse.input (64) は入力層のノード数、theta1 と theta2 はそれぞれ次元が 25x64 と 64x25 の第 1 層と第 2 層の重み行列、m は 10000、rhoest の次元は (25,)、x の次元は 10000x64、a3 は 10000x64、a2 は 10000x25 です。

UPDATE: 回答のアイデアのいくつかに従ってコードに変更を加えました。パフォーマンスは、numpy: 0.65 対 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

ベストアンサー1

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

Python ループの何が問題なのかとお思いですか? Python ループのすべての反復は、メソッドの呼び出しです。インデックスnextの使用はすべて、メソッドの呼び出しです。すべてはの呼び出しです。ドットで区切られた属性の検索 ( など) はすべて、関数呼び出しを伴います。これらの関数呼び出しが積み重なると、速度が大幅に低下します。これらのフックにより、Python に表現力が与えられます。たとえば、文字列のインデックスは、辞書のインデックスとは異なる意味を持ちます。同じ構文ですが、意味が異なります。この魔法は、オブジェクトに異なるメソッドを与えることで実現されます。[]__getitem__+=__iadd__np.dot__getitem__

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

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

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

delta3それぞれをi一度に計算することができます:

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 倍の速度向上が期待できます。

以下は、がC_CONTIGUOUS であり、が F_CONTIGUOUS であることを除いてxと同じです。また、 との関係も同じです。xfxxfyyf

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 でのベンチマークについて:

誤解を招く可能性があるPython のコードの速度をベンチマークするには、呼び出しのペアの差を使用しますtime.time()。測定を何度も繰り返す必要があります。自動ガベージ コレクターを無効にした方がよいでしょう。また、クロック タイマーの解像度が低いために発生するエラーを回避し、time.time呼び出しのオーバーヘッドの影響を減らすために、長い時間間隔 (少なくとも 10 秒分の繰り返しなど) を測定することも重要です。すべてのコードを自分で書く代わりに、Python ではtimeit モジュール基本的に、私はこれをコードの断片の時間計測に使用していますが、IPythonターミナル便宜上。

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

おすすめ記事