web-dev-qa-db-ja.com

ndarrayの各行に関数を適用します

ベクトルxのマハラノビス距離の2乗を計算して、次のことを意味するこの関数があります。

def mahalanobis_sqdist(x, mean, Sigma):
   '''
    Calculates squared Mahalanobis Distance of vector x 
    to distibutions' mean 
   '''
   Sigma_inv = np.linalg.inv(Sigma)
   xdiff = x - mean
   sqmdist = np.dot(np.dot(xdiff, Sigma_inv), xdiff)
   return sqmdist

(25, 4)の形をしたnumpy配列があります。したがって、forループを使用せずに、その関数を配列の25行すべてに適用したいと思います。したがって、基本的に、このループのベクトル化された形式をどのように書くことができますか?

for r in d1:
    mahalanobis_sqdist(r[0:4], mean1, Sig1)

ここで、mean1Sig1は次のとおりです。

>>> mean1
array([ 5.028,  3.48 ,  1.46 ,  0.248])
>>> Sig1 = np.cov(d1[0:25, 0:4].T)
>>> Sig1
array([[ 0.16043333,  0.11808333,  0.02408333,  0.01943333],
       [ 0.11808333,  0.13583333,  0.00625   ,  0.02225   ],
       [ 0.02408333,  0.00625   ,  0.03916667,  0.00658333],
       [ 0.01943333,  0.02225   ,  0.00658333,  0.01093333]])

私は以下を試しましたが、うまくいきませんでした:

>>> vecdist = np.vectorize(mahalanobis_sqdist)
>>> vecdist(d1, mean1, Sig1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/dist-packages/numpy/lib/function_base.py", line 1862, in __call__
    theout = self.thefunc(*newargs)
  File "<stdin>", line 6, in mahalanobis_sqdist
  File "/usr/lib/python2.7/dist-packages/numpy/linalg/linalg.py", line 445, in inv
    return wrap(solve(a, identity(a.shape[0], dtype=a.dtype)))
IndexError: Tuple index out of range
12
Vahid Mirjalili

配列の各行に関数を適用するには、次を使用できます。

np.apply_along_axis(mahalanobis_sqdist, 1, d1, mean1, Sig1)    

ただし、この場合、より良い方法があります。各行に関数を適用する必要はありません。代わりに、d1配列全体にNumPy演算を適用して、同じ結果を計算できます。 np.einsumfor-loopnp.dotへの2つの呼び出しを置き換えることができます。


def mahalanobis_sqdist2(d, mean, Sigma):
   Sigma_inv = np.linalg.inv(Sigma)
   xdiff = d - mean
   return np.einsum('ij,im,mj->i', xdiff, xdiff, Sigma_inv)

ここにいくつかのベンチマークがあります:

import numpy as np
np.random.seed(1)

def mahalanobis_sqdist(x, mean, Sigma):
   '''
   Calculates squared Mahalanobis Distance of vector x 
   to distibutions mean 
   '''
   Sigma_inv = np.linalg.inv(Sigma)
   xdiff = x - mean
   sqmdist = np.dot(np.dot(xdiff, Sigma_inv), xdiff)
   return sqmdist

def mahalanobis_sqdist2(d, mean, Sigma):
   Sigma_inv = np.linalg.inv(Sigma)
   xdiff = d - mean
   return np.einsum('ij,im,mj->i', xdiff, xdiff, Sigma_inv)

def using_loop(d1, mean, Sigma):
    expected = []
    for r in d1:
        expected.append(mahalanobis_sqdist(r[0:4], mean1, Sig1))
    return np.array(expected)

d1 = np.random.random((25,4))
mean1 = np.array([ 5.028,  3.48 ,  1.46 ,  0.248])
Sig1 = np.cov(d1[0:25, 0:4].T)

expected = using_loop(d1, mean1, Sig1)
result = np.apply_along_axis(mahalanobis_sqdist, 1, d1, mean1, Sig1)
result2 = mahalanobis_sqdist2(d1, mean1, Sig1)
assert np.allclose(expected, result)
assert np.allclose(expected, result2)

In [92]: %timeit mahalanobis_sqdist2(d1, mean1, Sig1)
10000 loops, best of 3: 31.1 µs per loop
In [94]: %timeit using_loop(d1, mean1, Sig1)
1000 loops, best of 3: 569 µs per loop
In [91]: %timeit np.apply_along_axis(mahalanobis_sqdist, 1, d1, mean1, Sig1)
1000 loops, best of 3: 806 µs per loop

したがって、mahalanobis_sqdist2for-loopよりも約18倍高速であり、np.apply_along_axisを使用するよりも26倍高速です。


np.apply_along_axisnp.vectorizenp.frompyfuncはPythonユーティリティ関数です。内部では、for-またはwhile-loopsを使用します。ここでは実際の「ベクトル化」は行われていません。構文支援を提供できますが、自分で記述したfor-loopよりもコードのパフォーマンスが向上することは期待できません。

19
unutbu

@unutbuによる回答は、配列の行に関数を適用する場合に非常にうまく機能します。この特定のケースでは、大きな配列で作業している場合に大幅に高速化する、使用できる数学的な対称性がいくつかあります。

関数の修正バージョンは次のとおりです。

_def mahalanobis_sqdist3(x, mean, Sigma):
    Sigma_inv = np.linalg.inv(Sigma)
    xdiff = x - mean
    return (xdiff.dot(Sigma_inv)*xdiff).sum(axis=-1)
_

大きなSigmaを使用することになった場合は、_Sigma_inv_をキャッシュして、代わりに関数の引数として渡すことをお勧めします。この例では4x4なので、これは問題ではありません。これに出くわした他の人のために、とにかく大きなSigmaに対処する方法を示します。

同じSigmaを繰り返し使用しない場合は、キャッシュすることができないため、行列を反転する代わりに、別の方法を使用して線形システムを解くことができます。ここでは、SciPyに組み込まれているLU分解を使用します。これにより、xの列数が行数に比べて多い場合にのみ時間が改善されます。

そのアプローチを示す関数は次のとおりです。

_from scipy.linalg import lu_factor, lu_solve
def mahalanobis_sqdist4(x, mean, Sigma):
    xdiff = x - mean
    Sigma_inv = lu_factor(Sigma)
    return (xdiff.T*lu_solve(Sigma_inv, xdiff.T)).sum(axis=0)
_

ここにいくつかのタイミングがあります。他の回答で述べたように、einsumのバージョンを含めます。

_import numpy as np
Sig1 = np.array([[ 0.16043333,  0.11808333,  0.02408333,  0.01943333],
                 [ 0.11808333,  0.13583333,  0.00625   ,  0.02225   ],
                 [ 0.02408333,  0.00625   ,  0.03916667,  0.00658333],
                 [ 0.01943333,  0.02225   ,  0.00658333,  0.01093333]])
mean1 = np.array([ 5.028,  3.48 ,  1.46 ,  0.248])
x = np.random.Rand(25, 4)
%timeit np.apply_along_axis(mahalanobis_sqdist, 1, x, mean1, Sig1)
%timeit mahalanobis_sqdist2(x, mean1, Sig1)
%timeit mahalanobis_sqdist3(x, mean1, Sig1)
%timeit mahalanobis_sqdist4(x, mean1, Sig1)
_

与える:

_1000 loops, best of 3: 973 µs per loop
10000 loops, best of 3: 36.2 µs per loop
10000 loops, best of 3: 40.8 µs per loop
10000 loops, best of 3: 83.2 µs per loop
_

ただし、関連する配列のサイズを変更すると、タイミングの結果が変わります。たとえば、x = np.random.Rand(2500, 4)とすると、タイミングは次のようになります。

_10 loops, best of 3: 95 ms per loop
1000 loops, best of 3: 355 µs per loop
10000 loops, best of 3: 131 µs per loop
1000 loops, best of 3: 337 µs per loop
_

そして、x = np.random.Rand(1000, 1000)Sigma1 = np.random.Rand(1000, 1000)、およびmean1 = np.random.Rand(1000)とすると、タイミングは次のようになります。

_1 loops, best of 3: 1min 24s per loop
1 loops, best of 3: 2.39 s per loop
10 loops, best of 3: 155 ms per loop
10 loops, best of 3: 99.9 ms per loop
_

編集:他の回答の1つがコレスキー分解を使用していることに気づきました。 Sigmaが対称で正定値であることを考えると、実際には上記の結果よりもうまくいく可能性があります。 SciPyを介して利用できるBLASおよびLAPACKの優れたルーチンがいくつかあり、対称的な正定行列を処理できます。これが2つの高速バージョンです。

_from scipy.linalg.fblas import dsymm
def mahalanobis_sqdist5(x, mean, Sigma_inv):
    xdiff = x - mean
    Sigma_inv = la.inv(Sigma)
    return np.einsum('...i,...i->...',dsymm(1., Sigma_inv, xdiff.T).T, xdiff)
from scipy.linalg.flapack import dposv
def mahalanobis_sqdist6(x, mean, Sigma):
    xdiff = x - mean
    return np.einsum('...i,...i->...', xdiff, dposv(Sigma, xdiff.T)[1].T)
_

最初のものはまだシグマを反転させます。インバースを事前に計算して再利用すると、はるかに高速になります(1000x1000の場合、事前に計算されたインバースを使用すると、私のマシンでは35.6msかかります)。また、einsumを使用して製品を取得し、最後の軸に沿って合計しました。これは、_(A * B).sum(axis=-1)_のようなことをするよりもわずかに速くなりました。これらの2つの関数は、次のタイミングを提供します。

最初のテストケース:

_10000 loops, best of 3: 55.3 µs per loop
100000 loops, best of 3: 14.2 µs per loop
_

2番目のテストケース:

_10000 loops, best of 3: 121 µs per loop
10000 loops, best of 3: 79 µs per loop
_

3番目のテストケース:

_10 loops, best of 3: 92.5 ms per loop
10 loops, best of 3: 48.2 ms per loop
_
8
IanH

reddit について本当に素晴らしいコメントを見ただけで、もう少しスピードアップするかもしれません:

これは、numpyを定期的に使用する人にとっては驚くべきことではありません。 pythonのforループはひどく遅いです。実際、einsumもかなり遅いです。これは、ベクトルがたくさんある場合に高速なバージョンです(このバージョンを高速化するには、4次元で500個のベクトルで十分です)。私のマシンのeinsumよりも):

def no_einsum(d, mean, Sigma):
    L_inv = np.linalg.inv(numpy.linalg.cholesky(Sigma))
    xdiff = d - mean
    return np.sum(np.dot(xdiff, L_inv.T)**2, axis=1)

ポイントも高次元の場合、逆の計算は遅く(そして一般的には悪い考えです)、システムを直接解くことで時間を節約できます(250次元の500ベクトルで、このバージョンを私のマシンで最速にするのに十分です):

def no_einsum_solve(d, mean, Sigma):
    L = numpy.linalg.cholesky(Sigma)
    xdiff = d - mean
    return np.sum(np.linalg.solve(L, xdiff.T)**2, axis=0)
5
user2489252

問題はそれです np.vectorizeすべての引数をベクトル化しますが、最初の引数のみをベクトル化する必要があります。 excludedvectorizeキーワード引数を使用する必要があります。

np.vectorize(mahalanobis_sqdist, excluded=[1, 2])
0
abacabadabacaba