web-dev-qa-db-fra.com

Boucles Numpy rapides

Comment optimisez-vous ce code ( sans vectorisation, car cela conduit à utiliser la sémantique du calcul, ce qui est assez souvent loin d'être non trivial):

slow_lib.py:
import numpy as np

def foo():
    size = 200
    np.random.seed(1000031212)
    bar = np.random.Rand(size, size)
    moo = np.zeros((size,size), dtype = np.float)
    for i in range(0,size):
        for j in range(0,size):
            val = bar[j]
            moo += np.outer(val, val)

Le fait est que de telles boucles de type correspondent assez souvent à des opérations où vous avez des doubles sommes sur une opération vectorielle.

C'est assez lent:

>>t = timeit.timeit('foo()', 'from slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 41.165681839

Ok, alors cynothize et ajoutons des annotations de type comme il n'y a pas de lendemain:

c_slow_lib.pyx:
import numpy as np
cimport numpy as np
import cython
@cython.boundscheck(False)
@cython.wraparound(False)

def foo():
    cdef int size = 200
    cdef int i,j
    np.random.seed(1000031212)
    cdef np.ndarray[np.double_t, ndim=2] bar = np.random.Rand(size, size)
    cdef np.ndarray[np.double_t, ndim=2] moo = np.zeros((size,size), dtype = np.float)
    cdef np.ndarray[np.double_t, ndim=1] val
    for i in xrange(0,size):
        for j in xrange(0,size):
            val = bar[j]
            moo += np.outer(val, val)


>>t = timeit.timeit('foo()', 'from c_slow_lib import foo', number = 10)
>>print ("took: "+str(t))
took: 42.3104710579

... ehr ... quoi? Numba à la rescousse!

numba_slow_lib.py:
import numpy as np
from numba import jit

size = 200
np.random.seed(1000031212)

bar = np.random.Rand(size, size)

@jit
def foo():
    bar = np.random.Rand(size, size)
    moo = np.zeros((size,size), dtype = np.float)
    for i in range(0,size):
        for j in range(0,size):
            val = bar[j]
            moo += np.outer(val, val)

>>t = timeit.timeit('foo()', 'from numba_slow_lib import foo', number = 10)
>>print("took: "+str(t))
took: 40.7327859402

Alors, n'y a-t-il vraiment aucun moyen d'accélérer cela? Le point est:

  • si je convertis la boucle intérieure en une version vectorisée (en construisant une matrice plus grande représentant la boucle intérieure puis en appelant np.outer sur la plus grande matrice) j'obtiens beaucoup un code plus rapide.
  • si j'implémente quelque chose de similaire dans Matlab (R2016a) cela fonctionne assez bien grâce à JIT.
12
ndbd

Voici le code pour outer:

def outer(a, b, out=None):    
    a = asarray(a)
    b = asarray(b)
    return multiply(a.ravel()[:, newaxis], b.ravel()[newaxis,:], out)

Ainsi, chaque appel à outer implique un certain nombre d'appels python. Ceux-ci finissent par appeler du code compilé pour effectuer la multiplication. Mais chacun entraîne une surcharge qui n'a rien à voir avec la taille de vos tableaux.

Ainsi, 200 (200 ** 2?) Appels à outer auront tout ce surdébit, tandis qu'un appel à outer avec les 200 lignes a un surdébit défini, suivi d'une opération de compilation rapide.

cython et numba ne compilent pas ou ne contournent pas le code Python dans outer. Tout ce qu'ils peuvent faire est de rationaliser le code d'itération qui vous avez écrit - et cela ne prend pas beaucoup de temps.

Sans entrer dans les détails, le jit MATLAB doit pouvoir remplacer le 'externe' par du code plus rapide - il réécrit l'itération. Mais mon expérience avec MATLAB remonte à une époque antérieure à sa vie.

Pour des améliorations réelles de la vitesse avec cython et numba, vous devez utiliser du code numpy/python primitif jusqu'en bas. Ou mieux encore concentrez vos efforts sur les pièces intérieures lentes.

Le remplacement de votre outer par une version simplifiée réduit de moitié le temps d'exécution:

def foo1(N):
        size = N
        np.random.seed(1000031212)
        bar = np.random.Rand(size, size)
        moo = np.zeros((size,size), dtype = np.float)
        for i in range(0,size):
                for j in range(0,size):
                        val = bar[j]
                        moo += val[:,None]*val   
        return moo

Avec le N=200 Complet, votre fonction prenait 17 secondes par boucle. Si je remplace les deux lignes intérieures par pass (pas de calcul), le temps passe à 3 ms par boucle. En d'autres termes, le mécanisme de boucle externe n'est pas un gros consommateur de temps, du moins pas comparé à de nombreux appels à outer().

14
hpaulj

Si la mémoire le permet, vous pouvez utiliser np.einsum pour effectuer ces calculs lourds de manière vectorisée, comme ceci -

moo = size*np.einsum('ij,ik->jk',bar,bar)

On peut également utiliser np.tensordot -

moo = size*np.tensordot(bar,bar,axes=(0,0))

Ou simplement np.dot -

moo = size*bar.T.dot(bar)
9
Divakar

De nombreux tutoriels et démonstrations de Cython, Numba, etc. donnent l'impression que ces outils peuvent accélérer votre code de manière automatique, mais en pratique, ce n'est souvent pas le cas: vous devrez modifier un peu votre code pour en extraire le meilleur. performance. Si vous avez déjà implémenté un certain degré de vectorisation, cela signifie généralement écrire TOUTES les boucles. Les raisons pour lesquelles les opérations du tableau Numpy ne sont pas optimales sont les suivantes:

  • De nombreux tableaux temporaires sont créés et bouclés;
  • Surcharge importante par appel si les tableaux sont petits;
  • La logique de court-circuitage ne peut pas être implémentée, car les tableaux sont traités dans leur ensemble;
  • Parfois, l'algorithme optimal ne peut pas être exprimé à l'aide d'expressions de tableau et vous vous contentez d'un algorithme avec une complexité temporelle pire.

Utiliser Numba ou Cython n'optimise pas ces problèmes! Au lieu de cela, ces outils vous permettent d'écrire du code en boucle qui est beaucoup plus rapide que le Python ordinaire.

De plus, pour Numba en particulier, vous devez être conscient de la différence entre "mode objet" et "mode nopython" . Les boucles serrées de votre exemple doivent s'exécuter en mode nopython pour fournir une accélération significative. Cependant, numpy.outer Est pas encore pris en charge par Numba , ce qui entraîne la compilation de la fonction en mode objet. Décorez avec jit(nopython=True) pour permettre à de tels cas de lever une exception.

Un exemple pour démontrer une accélération est en effet possible:

import numpy as np
from numba import jit

@jit
def foo_nb(bar):
    size = bar.shape[0]
    moo = np.zeros((size, size))
    for i in range(0,size):
        for j in range(0,size):
            val = bar[j]
            moo += np.outer(val, val)
    return moo

@jit
def foo_nb2(bar):
    size = bar.shape[0]
    moo = np.zeros((size, size))
    for i in range(size):
        for j in range(size):
            for k in range(0,size):
                for l in range(0,size):
                    moo[k,l] += bar[j,k] * bar[j,l]
    return moo

size = 100
bar = np.random.Rand(size, size)

np.allclose(foo_nb(bar), foo_nb2(bar))
# True

%timeit foo_nb(bar)
# 1 loop, best of 3: 816 ms per loop
%timeit foo_nb2(bar)
# 10 loops, best of 3: 176 ms per loop
4
user2379410