Rotation tensorielle rapide avec NumPy
au cœur d'une application (écrit en Python et en utilisant NumPy ) je dois tourner un tenseur de 4ème ordre. En fait, j'ai besoin de tourner beaucoup de tenseurs de nombreuses fois et c'est mon goulot d'étranglement. Mon implémentation naïve (ci-dessous) impliquant huit boucles imbriquées semble être assez lente, mais je ne vois pas de moyen de tirer parti des opérations matricielles de NumPy et, espérons-le, d'accélérer les choses. J'ai le sentiment que je devrais utiliser np.tensordot
, mais je ne vois pas comment.
Mathématiquement, les éléments de la rotation du tenseur de, T', sont donnés par: T' ijkl = Σ g ia g jb g kc g ld T abcd avec la somme sur les indices répétés sur le côté droit. T et Tprime sont 3*3*3*3 les tableaux NumPy et la rotation de la matrice g est un 3*3 tableau NumPy. Mon implémentation lente (~0.04 secondes par appel) est ci-dessous.
#!/usr/bin/env python
import numpy as np
def rotT(T, g):
Tprime = np.zeros((3,3,3,3))
for i in range(3):
for j in range(3):
for k in range(3):
for l in range(3):
for ii in range(3):
for jj in range(3):
for kk in range(3):
for ll in range(3):
gg = g[ii,i]*g[jj,j]*g[kk,k]*g[ll,l]
Tprime[i,j,k,l] = Tprime[i,j,k,l] +
gg*T[ii,jj,kk,ll]
return Tprime
if __name__ == "__main__":
T = np.array([[[[ 4.66533067e+01, 5.84985000e-02, -5.37671310e-01],
[ 5.84985000e-02, 1.56722231e+01, 2.32831900e-02],
[ -5.37671310e-01, 2.32831900e-02, 1.33399259e+01]],
[[ 4.60051700e-02, 1.54658176e+01, 2.19568200e-02],
[ 1.54658176e+01, -5.18223500e-02, -1.52814920e-01],
[ 2.19568200e-02, -1.52814920e-01, -2.43874100e-02]],
[[ -5.35577630e-01, 1.95558600e-02, 1.31108757e+01],
[ 1.95558600e-02, -1.51342210e-01, -6.67615000e-03],
[ 1.31108757e+01, -6.67615000e-03, 6.90486240e-01]]],
[[[ 4.60051700e-02, 1.54658176e+01, 2.19568200e-02],
[ 1.54658176e+01, -5.18223500e-02, -1.52814920e-01],
[ 2.19568200e-02, -1.52814920e-01, -2.43874100e-02]],
[[ 1.57414726e+01, -3.86167500e-02, -1.55971950e-01],
[ -3.86167500e-02, 4.65601977e+01, -3.57741000e-02],
[ -1.55971950e-01, -3.57741000e-02, 1.34215636e+01]],
[[ 2.58256300e-02, -1.49072770e-01, -7.38843000e-03],
[ -1.49072770e-01, -3.63410500e-02, 1.32039847e+01],
[ -7.38843000e-03, 1.32039847e+01, 1.38172700e-02]]],
[[[ -5.35577630e-01, 1.95558600e-02, 1.31108757e+01],
[ 1.95558600e-02, -1.51342210e-01, -6.67615000e-03],
[ 1.31108757e+01, -6.67615000e-03, 6.90486240e-01]],
[[ 2.58256300e-02, -1.49072770e-01, -7.38843000e-03],
[ -1.49072770e-01, -3.63410500e-02, 1.32039847e+01],
[ -7.38843000e-03, 1.32039847e+01, 1.38172700e-02]],
[[ 1.33639532e+01, -1.26331100e-02, 6.84650400e-01],
[ -1.26331100e-02, 1.34222177e+01, 1.67851800e-02],
[ 6.84650400e-01, 1.67851800e-02, 4.89151396e+01]]]])
g = np.array([[ 0.79389393, 0.54184237, 0.27593346],
[-0.59925749, 0.62028664, 0.50609776],
[ 0.10306737, -0.56714313, 0.8171449 ]])
for i in range(100):
Tprime = rotT(T,g)
y a-t-il un moyen d'accélérer les choses? Il serait utile de généraliser le code à d'autres niveaux de tenseur, mais c'est moins important.
6 réponses
pour utiliser tensordot
, calculer le produit extérieur des tenseurs g
:
def rotT(T, g):
gg = np.outer(g, g)
gggg = np.outer(gg, gg).reshape(4 * g.shape)
axes = ((0, 2, 4, 6), (0, 1, 2, 3))
return np.tensordot(gggg, T, axes)
sur mon système, c'est environ sept fois plus rapide que la solution de Sven. Si le tenseur g
ne change pas souvent, vous pouvez aussi mettre en cache le tenseur gggg
. Si vous faites cela et que vous activez quelques micro-optimisations( lining le code tensordot
, pas de contrôles, pas de formes génériques), vous pouvez encore le faire deux fois plus vite:
def rotT(T, gggg):
return np.dot(gggg.transpose((1, 3, 5, 7, 0, 2, 4, 6)).reshape((81, 81)),
T.reshape(81, 1)).reshape((3, 3, 3, 3))
résultats de timeit
sur mon ordinateur portable personnel (500 itérations):
Your original code: 19.471129179
Sven's code: 0.718412876129
My first code: 0.118047952652
My second code: 0.0690279006958
les nombres sur ma machine de travail sont:
Your original code: 9.77922987938
Sven's code: 0.137110948563
My first code: 0.0569641590118
My second code: 0.0308079719543
Voici comment le faire avec une seule boucle de Python:
def rotT(T, g):
Tprime = T
for i in range(4):
slices = [None] * 4
slices[i] = slice(None)
slices *= 2
Tprime = g[slices].T * Tprime
return Tprime.sum(-1).sum(-1).sum(-1).sum(-1)
Certes, c'est un peu difficile à comprendre au premier coup d'œil, mais c'est un peu plus rapide :)
grâce au travail acharné de M. Wiebe, la prochaine version de Numpy (qui sera probablement 1,6) va rendre cela encore plus facile:
>>> Trot = np.einsum('ai,bj,ck,dl,abcd->ijkl', g, g, g, g, T)
L'approche de Philipp est actuellement 3 fois plus rapide, mais il y a peut-être place à amélioration. La différence de vitesse est probablement dû principalement à tensordot étant en mesure de dérouler l'ensemble de l'opération comme un produit de matrice simple qui peut être transmise à BLAS, et donc en évitant une grande partie de la charge associée à de petites tableaux - - - - ceci n'est pas possible pour la sommation générale D'Einstein, car toutes les opérations qui peuvent être exprimées sous cette forme ne se résolvent pas en un seul produit de matrice.
par curiosité j'ai comparé Cython mise en œuvre d'un code naïf de la question avec le code nul de réponse de @Philipp . Le code Cython est quatre fois plus rapide sur ma machine:
#cython: boundscheck=False, wraparound=False
import numpy as np
cimport numpy as np
def rotT(np.ndarray[np.float64_t, ndim=4] T,
np.ndarray[np.float64_t, ndim=2] g):
cdef np.ndarray[np.float64_t, ndim=4] Tprime
cdef Py_ssize_t i, j, k, l, ii, jj, kk, ll
cdef np.float64_t gg
Tprime = np.zeros((3,3,3,3), dtype=T.dtype)
for i in range(3):
for j in range(3):
for k in range(3):
for l in range(3):
for ii in range(3):
for jj in range(3):
for kk in range(3):
for ll in range(3):
gg = g[ii,i]*g[jj,j]*g[kk,k]*g[ll,l]
Tprime[i,j,k,l] = Tprime[i,j,k,l] + \
gg*T[ii,jj,kk,ll]
return Tprime
j'ai pensé que j'apporterais un point de données relativement nouveau à ces benchmarks, en utilisant parakeet , l'un des numpy
- compilateurs JIT conscients qui a vu le jour dans les derniers mois. (L'autre que je connais est numba , mais je ne l'ai pas testé ici.)
après que vous le faites à travers le processus d'installation quelque peu labyrinthine pour LLVM, vous pouvez décorer de nombreuses pures - numpy
fonctions pour (souvent) accélérer leur performance:
import numpy as np
import parakeet
@parakeet.jit
def rotT(T, g):
# ...
j'ai seulement essayé d'appliquer le JIT au code D'Andrew dans la question originale, mais il fait assez bien (>10x speedup) pour n'avoir à écrire aucun nouveau code que ce soit:
andrew 10 loops, best of 3: 206 msec per loop
andrew_jit 10 loops, best of 3: 13.3 msec per loop
sven 100 loops, best of 3: 2.39 msec per loop
philipp 1000 loops, best of 3: 0.879 msec per loop
pour ces chronométrages (sur mon ordinateur portable) j'ai exécuté chaque fonction dix fois, pour donner au JIT une chance d'identifier et d'optimiser les chemins de code chauds.
fait intéressant, les suggestions de Sven et Philipp sont encore des ordres de grandeur plus vite !
Approche Prospective et solution de code
pour l'efficacité de la mémoire et par la suite l'efficacité de la performance, nous pourrions utiliser la multiplication par étapes de la matrice tensorielle.
pour illustrer les étapes impliquées, utilisons la plus simple des solutions avec np.einsum
de @pv. -
np.einsum('ai,bj,ck,dl,abcd->ijkl', g, g, g, g, T)
comme vu, nous perdons la première dimension de g
avec tenseur-multiplication entre ses quatre variantes et T
.
faisons ces réductions de somme pour les multiplications de matrice tensorielle par étapes. Commençons par la première variante de g
et T
:
p1 = np.einsum('abcd, ai->bcdi', T, g)
ainsi, nous nous retrouvons avec un tenseur de dimensions en notation chaîne: bcdi
. Les étapes suivantes consisteraient à réduire ce tenseur par rapport au reste des trois variantes g
utilisées dans l'original einsum
de mise en oeuvre. Par conséquent, la réduction suivante serait de -
p2 = np.einsum('bcdi, bj->cdij', p1, g)
comme vu, nous avons perdu les deux premières dimensions avec les notations de chaîne: a
, b
. Nous le continuons pendant deux autres étapes pour se débarrasser de c
et d
aussi et serait laissé avec ijkl
comme la sortie finale, comme so -
p3 = np.einsum('cdij, ck->dijk', p2, g)
p4 = np.einsum('dijk, dl->ijkl', p3, g)
maintenant, nous pourrions utiliser np.tensordot
pour ces réductions de somme, qui serait beaucoup plus efficace.
mise en œuvre finale
ainsi, portant sur np.tensordot
, nous aurions la mise en œuvre finale comme si -
p1 = np.tensordot(T,g,axes=((0),(0)))
p2 = np.tensordot(p1,g,axes=((0),(0)))
p3 = np.tensordot(p2,g,axes=((0),(0)))
out = np.tensordot(p3,g,axes=((0),(0)))
essai D'exécution
nous allons tester toutes les approches basées sur NumPy affiché à travers d'autres postes pour résoudre le problème sur la performance.
Approches comme des fonctions -
def rotT_Philipp(T, g): # @Philipp's soln
gg = np.outer(g, g)
gggg = np.outer(gg, gg).reshape(4 * g.shape)
axes = ((0, 2, 4, 6), (0, 1, 2, 3))
return np.tensordot(gggg, T, axes)
def rotT_Sven(T, g): # @Sven Marnach's soln
Tprime = T
for i in range(4):
slices = [None] * 4
slices[i] = slice(None)
slices *= 2
Tprime = g[slices].T * Tprime
return Tprime.sum(-1).sum(-1).sum(-1).sum(-1)
def rotT_pv(T, g): # @pv.'s soln
return np.einsum('ai,bj,ck,dl,abcd->ijkl', g, g, g, g, T)
def rotT_Divakar(T,g): # Posted in this post
p1 = np.tensordot(T,g,axes=((0),(0)))
p2 = np.tensordot(p1,g,axes=((0),(0)))
p3 = np.tensordot(p2,g,axes=((0),(0)))
p4 = np.tensordot(p3,g,axes=((0),(0)))
return p4
Timings avec l'original de la taille des jeux de données -
In [304]: # Setup inputs
...: T = np.random.rand(3,3,3,3)
...: g = np.random.rand(3,3)
...:
In [305]: %timeit rotT(T, g)
...: %timeit rotT_pv(T, g)
...: %timeit rotT_Sven(T, g)
...: %timeit rotT_Philipp(T, g)
...: %timeit rotT_Divakar(T, g)
...:
100 loops, best of 3: 6.51 ms per loop
1000 loops, best of 3: 247 µs per loop
10000 loops, best of 3: 137 µs per loop
10000 loops, best of 3: 41.6 µs per loop
10000 loops, best of 3: 28.3 µs per loop
In [306]: 6510.0/28.3 # Speedup with the proposed soln over original code
Out[306]: 230.03533568904592
Comme il est mentionné au début de ce post, nous essayons d'atteindre l'efficacité de mémoire et donc de meilleures performances. Testons cela en augmentant la taille des ensembles de données -
In [307]: # Setup inputs
...: T = np.random.rand(5,5,5,5)
...: g = np.random.rand(5,5)
...:
In [308]: %timeit rotT(T, g)
...: %timeit rotT_pv(T, g)
...: %timeit rotT_Sven(T, g)
...: %timeit rotT_Philipp(T, g)
...: %timeit rotT_Divakar(T, g)
...:
100 loops, best of 3: 6.54 ms per loop
100 loops, best of 3: 7.17 ms per loop
100 loops, best of 3: 2.7 ms per loop
1000 loops, best of 3: 1.47 ms per loop
10000 loops, best of 3: 39.9 µs per loop