ベクトル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)
ここで、mean1
とSig1
は次のとおりです。
>>> 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
配列の各行に関数を適用するには、次を使用できます。
np.apply_along_axis(mahalanobis_sqdist, 1, d1, mean1, Sig1)
ただし、この場合、より良い方法があります。各行に関数を適用する必要はありません。代わりに、d1
配列全体にNumPy演算を適用して、同じ結果を計算できます。 np.einsumfor-loop
とnp.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_sqdist2
はfor-loop
よりも約18倍高速であり、np.apply_along_axis
を使用するよりも26倍高速です。
np.apply_along_axis
、np.vectorize
、np.frompyfunc
はPythonユーティリティ関数です。内部では、for-
またはwhile-loop
sを使用します。ここでは実際の「ベクトル化」は行われていません。構文支援を提供できますが、自分で記述したfor-loop
よりもコードのパフォーマンスが向上することは期待できません。
@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
_
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)
問題はそれです np.vectorize
すべての引数をベクトル化しますが、最初の引数のみをベクトル化する必要があります。 excluded
にvectorize
キーワード引数を使用する必要があります。
np.vectorize(mahalanobis_sqdist, excluded=[1, 2])