Je voudrais écrire une fonction f qui prend une fonction arbitraire g avec la signature g: R ^ n -> R ^ n -> int qui "soulève" g pour qu'il opère sur (R ^ {nxm}, R ^ {kxm}) en se comportant comme un produit scalaire. Cela signifie que je veux que f ait la signature f: R ^ {nxm} -> R ^ {mxk} -> R ^ {nxk} en appliquant g à toutes les paires de lignes et de colonnes lors de la construction d'une matrice M où M_ij = g (A [i ,:], B [:, j]) . Est-ce possible?
Par exemple, scipy.spatial.distance.cosine attend deux vecteurs. Maintenant, je soulèverais cosinus avec f:
def sim(A, B):
ignored_states = np.seterr(divide='raise')
return 1 - np.divide(np.dot(A, B.T), np.outer(np.linalg.norm(A, axis=1), np.linalg.norm(B, axis=1)))
Cela produirait alors la même sortie que
XXX
Ce qui est identique à sklearn.metrics.pairwise.cosine_similarity plus la partie 1 - bla .
Mais s'il n'y avait pas sklearn.metrics.pairwise.cosine_similarity , je devrais implémenter moi-même cette version levée de cosinus (ce que j'ai bien sûr fait ici ...). Mais je ne veux pas faire cela pour toutes les fonctions qui se comportent fondamentalement de la même manière que le produit scalaire en ce qui concerne la façon dont elles traitent mécaniquement leur argument. Par conséquent, j'aimerais avoir cette fonction f .
3 Réponses :
matmul a été converti en ufunc , et a formellement une signature. np.dot est une version antérieure et n'a pas de signature.
Mais étant donné les tableaux 2d, np.dot est en fait une forme de multiplication diffusée suivie d'une sommation , ou "somme des produits":
In [623]: h = lift(g)
In [624]: h(A,B.T)
Out[624]:
array([[ 14., 38.],
[ 38., 126.],
[ 62., 214.]])
In [625]: timeit h(A,B.T)
102 µs ± 56.5 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
équivalent:
In [616]: timeit f(A[:,None,:], B[None,:,:]) 255 µs ± 6.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) In [617]: timeit np.array([[g(x,y) for y in B] for x in A]) 69.4 µs ± 116 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each) In [618]: timeit np.dot(A, B.T) 3.15 µs ± 128 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Certains trouvent le code einsum > style de signature plus facile à suivre:
In [614]: f = np.vectorize(g, signature='(n),(n)->()')
In [615]: f(A[:,None,:], B[None,:,:])
Out[615]:
array([[ 14, 38],
[ 38, 126],
[ 62, 214]])
où le j répété signale le point comme une sommation.
===
Illustrant l'itération dans mon autre réponse:
In [601]: def g(x,y):
...: return (x*y).sum()
...:
In [602]: A.shape, B.shape
Out[602]: ((3, 4), (2, 4))
In [603]: np.array([[g(x,y) for y in B] for x in A])
Out[603]:
array([[ 14, 38],
[ 38, 126],
[ 62, 214]])
et la version vectorize : p>
In [594]: np.einsum('ij,kj->ik', A, B)
Out[594]:
array([[ 14, 38],
[ 38, 126],
[ 62, 214]])
temps comparatifs:
In [591]: (A[:,None,:]*B[None,:,:]).sum(axis=2)
Out[591]:
array([[ 14, 38],
[ 38, 126],
[ 62, 214]])
et en utilisant @hans ' 2nd lift code>:
In [587]: A = np.arange(12).reshape(3,4)
In [588]: B = np.arange(8).reshape(2,4)
In [589]: np.dot(A, B.T)
Out[589]:
array([[ 14, 38],
[ 38, 126],
[ 62, 214]])
Ok, comment cela résout le problème de l'application d'une fonction générique g à A et B ?
Vous avez insisté sur le fait de vouloir un point comme une signature; et j'ai essayé d'expliquer que cela implique en fait - à moins d'écrire du code compilé (en numba ou cython ). numpy est comme un ensemble LEGO. Il a un grand nombre de formes de blocs de stock qui vous permettent de faire quelque chose rapidement. Mais il n'inclut pas de machine de moulage pour mouler des formes de blocs personnalisées.
Je ne comprends toujours pas très bien. Êtes-vous en train de dire que ce que je veux n'est pas possible?
Demandez-vous une fonction avec une signature aussi simple que ce qui suit (capable de multiplier deux matrices), ou souhaitez-vous émuler toute la surface api np.dot ?
def lift(f):
def dot(A, B):
result = np.empty((A.shape[0], B.shape[1]))
for i,v in enumerate(A):
for j,w in enumerate(zip(*B)):
result[i,j] = f(v,w)
return result
return dot
Une source majeure d'inefficacité dans le code ci-dessus est les allocations pour toutes les listes intermédiaires. Puisque nous connaissons la valeur de retour finale, ceux-ci sont faciles à éviter:
def lift(f):
def dot(A, B):
return np.array([[f(v,w) for w in zip(*B)] for v in A])
return dot
Les boucles sont assez chères en Python, mais puisque f fonctionne sur k éléments, il semble raisonnable de supposer que cette surcharge est faible. Vous pouvez le réduire davantage en compilant avec pypy ou cython.
En principe oui. Mais votre code n'est pas efficace, non? Je suppose que np.dot est optimisé.
Pas super efficace non. En supposant que vous fournissiez f comme une boîte noire plutôt que comme une chose de niveau AST que vous pourriez compiler / en ligne / optimiser, tout ce que vous pourriez faire pour augmenter l'efficacité est de réduire les allocations et de faire la boucle en dehors de python natif (par exemple avec pypy ou cython). En supposant que le coût de f est élevé par rapport à une boucle python, vous pouvez obtenir des performances asymptotiquement comparables en pré-allouant simplement le tableau retourné, puis en le remplissant.
Pour un simple g code>, vos deux ascenseur code> heure de la même chose.
Oui, vous remarqueriez la plus grande différence si g était une fonction rapide (par exemple, une fonction qui ignorait les valeurs d'entrée). Puisque OP a posé des questions sur l'efficacité, il a semblé pertinent de signaler les endroits qui pourraient manquer d'efficacité.
J'ai rédigé mon autre réponse en supposant que votre
f = np.vectorize(g, signature='(n),(n)') # signature syntax may be wrong M = f(A, B.T)
avec les entrées (3,4) et (5,4) était la principale fonctionnalité point que vous étiez essayant d'imiter. En d'autres termes, (3,4), (4,5) => (3,5) avec sommation sur la dimension commune de taille 4. Ma réponse a montré comment ce calcul 2D peut être effectué avec des multiplications élémentaires.
Pour ce que ça vaut, np.dot obtient une grande partie de sa vitesse en passant la tâche à BLAS ( ou similaire) bibliothèques optimisées. Celles-ci ont été écrites en C ou Fortran, et optimisées par des générations de codeurs à analyse numérique.
Mais la description de votre signature peut parler de quelque chose de différent. C'est un peu déroutant.
M = np.array([[g(x,y) for y in B.T] for x in A])
Cela signifie-t-il que g (x, y) prend deux (n,) tableaux de formes et renvoie un entier ? Et il ne peut pas être généralisé pour fonctionner avec des tableaux 2D?
M = np.zeros((N,N)) # (N,M) ok?
for i in range(N):
for j in range(N):
x = A[i,:]; y = B[:,j]
M[i,j] = g(x, y)
Cela signifie-t-il que f (A, B) prend un (n, m ) forme, et une forme (k, m), et renvoie une forme (n, m)? Qu'est-il arrivé à la forme k ? Est-ce que k est une faute de frappe?
Sinon, vous parlez de faire (je crois)
f : R^{nxm} -> R^{kxm} -> R^{nxm}
alternativement:
g : R^n -> R^n -> int
En supposant que g est une fonction python qui ne peut fonctionner qu'avec 2 tableaux 1d (de longueur correspondante), et ne peut pas être généralisé aux tableaux 2d, il n'y en a pas tout mécanisme dans numpy pour compiler la double boucle ci-dessus. g doit être évalué N ** 2 fois. Et en supposant que g n'est pas trivial, ces évaluations N * 2 domineront le temps total d'évaluation, pas le mécanisme d'itération.
np.vectorize prend normalement une fonction qui accepte les entrées scalaires, mais avec un paramètre signature , elle peut fonctionner avec votre g:
np.dot(A, B.T)
mais en mon test de vectorize a toujours été plus lent qu'une itération explicite. Avec une signature, c'est encore plus lent. Alors j'hésite un peu à le mentionner.
Ok je vois, donc ce n'est pas possible. Merci. Et oui k était une faute de frappe. C'était censé être R ^ {nxm} -> R ^ {mxk} -> R ^ {mxk}