J'ai cette fonction pour calculer la distance au carré x de Mahalanobis du vecteur x:
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
J'ai un tableau numpy qui a la forme de (25, 4)
. Donc, je veux appliquer cette fonction aux 25 rangées de mon tableau sans boucle for. Alors, comment puis-je écrire la forme vectorisée de cette boucle:
for r in d1:
mahalanobis_sqdist(r[0:4], mean1, Sig1)
où mean1
et Sig1
sont:
>>> 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]])
J'ai essayé ce qui suit mais cela n'a pas fonctionné:
>>> 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
Pour appliquer une fonction à chaque ligne d'un tableau, vous pouvez utiliser:
np.apply_along_axis(mahalanobis_sqdist, 1, d1, mean1, Sig1)
Dans ce cas, cependant, il existe un meilleur moyen. Vous n'êtes pas obligé d'appliquer une fonction à chaque ligne. Au lieu de cela, vous pouvez appliquer des opérations NumPy à l’ensemble du tableau d1
pour calculer le même résultat. np.einsum peut remplacer le for-loop
et les deux appels à np.dot
:
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)
Voici quelques repères:
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
Ainsi, mahalanobis_sqdist2
est environ 18 fois plus rapide qu'un for-loop
et 26 fois plus rapide que d'utiliser np.apply_along_axis
.
Notez que np.apply_along_axis
, np.vectorize
, np.frompyfunc
sont des fonctions de l'utilitaire Python. Sous le capot, ils utilisent for-
ou while-loop
s. Il n'y a pas de réelle "vectorisation" ici. Ils peuvent fournir une assistance syntaxique, mais ne vous attendez pas à ce que votre code fonctionne mieux qu'un for-loop
que vous écrivez vous-même.
La réponse de @unutbu fonctionne très bien pour appliquer une fonction quelconque aux lignes d'un tableau. Dans ce cas particulier, vous pouvez utiliser certaines symétries mathématiques qui accélèreront considérablement les choses si vous travaillez avec de grands tableaux. .
Voici une version modifiée de votre fonction:
def mahalanobis_sqdist3(x, mean, Sigma):
Sigma_inv = np.linalg.inv(Sigma)
xdiff = x - mean
return (xdiff.dot(Sigma_inv)*xdiff).sum(axis=-1)
Si vous finissez par utiliser une sorte de Sigma
de grande taille, je vous recommanderais de mettre en cache Sigma_inv
et de le transmettre sous forme d'argument à votre fonction. .____.] Je montrerai comment traiter le gros Sigma
de toute façon pour quiconque se heurte à cela.
Si vous n'utilisez pas la même variable Sigma
de manière répétée, vous ne pourrez pas la mettre en cache, donc, au lieu d'inverser la matrice, vous pouvez utiliser une méthode différente pour résoudre le système linéaire. Ici J'utiliserai la décomposition LU intégrée à SciPy. Cela n'améliorera le temps que si le nombre de colonnes de x
est élevé par rapport à son nombre de lignes.
Voici une fonction qui montre cette approche:
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)
Voici quelques moments. J'inclurai la version avec einsum
comme mentionné dans l'autre réponse.
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)
donnant:
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
Cependant, la modification de la taille des matrices impliquées modifie les résultats de la synchronisation. Par exemple, en laissant x = np.random.Rand(2500, 4)
, les synchronisations sont les suivantes:
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
Et en laissant x = np.random.Rand(1000, 1000)
, Sigma1 = np.random.Rand(1000, 1000)
et mean1 = np.random.Rand(1000)
, les horaires sont les suivants:
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
Edit : J'ai remarqué que l'une des autres réponses utilisait la décomposition de Cholesky. Étant donné que Sigma
est symétrique et positif défini, nous pouvons en réalité faire mieux que mes résultats précédents. Il y a quelques bonnes routines de BLAS et de LAPACK disponibles via SciPy pouvant fonctionner avec des matrices symétriques positives définies. Voici deux versions plus rapides.
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)
Le premier inverse toujours Sigma. Si vous pré-calculez l’inverse et le réutilisez, c’est beaucoup plus rapide (le cas 1000x1000 prend 35,6 ms sur ma machine avec l’inverse pré-calculé). J'ai également utilisé einsum pour prendre le produit puis la somme le long du dernier axe. Cela s'est avéré légèrement plus rapide que de faire quelque chose comme (A * B).sum(axis=-1)
. Ces deux fonctions donnent les temps suivants:
Premier cas de test:
10000 loops, best of 3: 55.3 µs per loop
100000 loops, best of 3: 14.2 µs per loop
Deuxième cas de test:
10000 loops, best of 3: 121 µs per loop
10000 loops, best of 3: 79 µs per loop
Troisième cas de test:
10 loops, best of 3: 92.5 ms per loop
10 loops, best of 3: 48.2 ms per loop
Je viens de voir un commentaire vraiment sympa sur reddit qui pourrait accélérer les choses encore un peu plus:
Ce n'est pas surprenant pour ceux qui utilisent régulièrement numpy. Les boucles En python sont terriblement lentes. En fait, einsum est assez lent aussi. Voici une version plus rapide si vous avez beaucoup de vecteurs (500 En 4 dimensions suffisent pour rendre cette version plus rapide que Einsum. sur ma machine):
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)
Si vos points sont aussi de haute dimension, alors l'inverse est lent (et généralement une mauvaise idée) et vous pouvez gagner du temps en résolvant le système directement (500 vecteurs sur 250 dimensions suffisent). .____.] pour que cette version soit la plus rapide sur ma machine):
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)
Le problème est que np.vectorize
vectorise tous les arguments, mais que vous ne devez vectoriser que sur le premier. Vous devez utiliser l'argument de mot clé excluded
pour vectorize
:
np.vectorize(mahalanobis_sqdist, excluded=[1, 2])