web-dev-qa-db-fra.com

Appliquer une fonction à chaque ligne d'un ndarray

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)

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
13
Vahid Mir

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-loops. 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.

20
unutbu

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
8
IanH

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)
5
Sebastian

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])
0
abacabadabacaba