web-dev-qa-db-fra.com

Différence de performance entre numpy et matlab

Je calcule l'algorithme backpropagation pour un autoencodeur clairsemé. Je l'ai implémenté dans python en utilisant numpy et dans matlab. Le code est presque le même, mais les performances sont très différentes. Le temps que matlab prend pour terminer la tâche est de 0,252454 secondes tandis que numpy 0.973672151566, soit presque quatre fois plus. J'appellerai ce code plusieurs fois plus tard dans un problème de minimisation, de sorte que cette différence entraîne plusieurs minutes de retard entre les implémentations. S'agit-il d'un comportement normal? Comment -Je améliorer les performances en numpy?

Implémentation Numpy:

Sparse.rho est un paramètre de réglage, sparse.nodes est le nombre de nœuds dans la couche cachée (25), sparse.input (64) le nombre de nœuds dans la couche d'entrée, theta1 et theta2 sont les matrices de poids pour la première et deuxième couche respectivement avec les dimensions 25x64 et 64x25, m est égal à 10000, le rhoest a une dimension de (25,), x a une dimension de 10000x64, a3 10000x64 et a2 10000x25.

UPDATE: J'ai introduit des changements dans le code suivant certaines des idées des réponses. La performance est désormais numpy: 0,65 vs matlab: 0,25.

partial_j1 = np.zeros(sparse.theta1.shape)
partial_j2 = np.zeros(sparse.theta2.shape)
partial_b1 = np.zeros(sparse.b1.shape)
partial_b2 = np.zeros(sparse.b2.shape)
t = time.time()

delta3t = (-(x-a3)*a3*(1-a3)).T

for i in range(m):

    delta3 = delta3t[:,i:(i+1)]
    sum1 =  np.dot(sparse.theta2.T,delta3)
    delta2 = ( sum1 + sum2 ) * a2[i:(i+1),:].T* (1 - a2[i:(i+1),:].T)
    partial_j1 += np.dot(delta2, a1[i:(i+1),:])
    partial_j2 += np.dot(delta3, a2[i:(i+1),:])
    partial_b1 += delta2
    partial_b2 += delta3

print "Backprop time:", time.time() -t

Implémentation de Matlab:

tic
for i = 1:m

    delta3 = -(data(i,:)-a3(i,:)).*a3(i,:).*(1 - a3(i,:));
    delta3 = delta3.';
    sum1 =  W2.'*delta3;
    sum2 = beta*(-sparsityParam./rhoest + (1 - sparsityParam) ./ (1.0 - rhoest) );
    delta2 = ( sum1 + sum2 ) .* a2(i,:).' .* (1 - a2(i,:).');
    W1grad = W1grad + delta2* a1(i,:);
    W2grad = W2grad + delta3* a2(i,:);
    b1grad = b1grad + delta2;
    b2grad = b2grad + delta3;
end
toc
22
pabaldonedo

Il serait faux de dire "Matlab est toujours plus rapide que NumPy" ou vice versa. Souvent, leurs performances sont comparables. Lorsque vous utilisez NumPy, pour obtenir de bonnes performances, vous devez garder à l'esprit que la vitesse de NumPy provient de l'appel de fonctions sous-jacentes écrites en C/C++/Fortran. Il fonctionne bien lorsque vous appliquez ces fonctions à des tableaux entiers. En général, vous obtenez de meilleures performances lorsque vous appelez ces fonctions NumPy sur des tableaux ou des scalaires plus petits dans une boucle Python.

Quel est le problème avec une boucle Python que vous demandez? Chaque itération via la boucle Python est un appel à une méthode next. Chaque utilisation de _[]_ l'indexation est un appel à une méthode ___getitem___. Chaque _+=_ est un appel à ___iadd___. Chaque recherche d'attribut en pointillés (comme dans _np.dot_) implique la fonction appels. Ces appels de fonction représentent une entrave importante à la vitesse. Ces crochets donnent Python puissance expressive - l'indexation pour les chaînes signifie quelque chose de différent que l'indexation pour les dict par exemple. Même syntaxe, différentes significations. La magie s'accomplit en donnant aux objets différentes méthodes ___getitem___.

Mais cette puissance expressive a un coût en vitesse. Donc, lorsque vous n'avez pas besoin de toute cette expressivité dynamique, pour obtenir de meilleures performances, essayez de vous limiter aux appels de fonction NumPy sur des tableaux entiers.

Donc, supprimez la boucle for; utiliser des équations "vectorisées" lorsque cela est possible. Par exemple, au lieu de

_for i in range(m):
    delta3 = -(x[i,:]-a3[i,:])*a3[i,:]* (1 - a3[i,:])    
_

vous pouvez calculer _delta3_ pour chaque i à la fois:

_delta3 = -(x-a3)*a3*(1-a3)
_

Alors que dans le _for-loop_ _delta3_ est un vecteur, l'utilisation de l'équation vectorisée _delta3_ est une matrice.


Certains des calculs dans _for-loop_ ne dépendent pas de i et doivent donc être levés en dehors de la boucle. Par exemple, _sum2_ ressemble à une constante:

_sum2 = sparse.beta*(-float(sparse.rho)/rhoest + float(1.0 - sparse.rho) / (1.0 - rhoest) )
_

Voici un exemple exécutable avec une implémentation alternative (alt) de votre code (orig).

Mon repère de temps montre une amélioration de la vitesse de 6,8x :

_In [52]: %timeit orig()
1 loops, best of 3: 495 ms per loop

In [53]: %timeit alt()
10 loops, best of 3: 72.6 ms per loop
_

_import numpy as np


class Bunch(object):
    """ http://code.activestate.com/recipes/52308 """
    def __init__(self, **kwds):
        self.__dict__.update(kwds)

m, n, p = 10 ** 4, 64, 25

sparse = Bunch(
    theta1=np.random.random((p, n)),
    theta2=np.random.random((n, p)),
    b1=np.random.random((p, 1)),
    b2=np.random.random((n, 1)),
)

x = np.random.random((m, n))
a3 = np.random.random((m, n))
a2 = np.random.random((m, p))
a1 = np.random.random((m, n))
sum2 = np.random.random((p, ))
sum2 = sum2[:, np.newaxis]

def orig():
    partial_j1 = np.zeros(sparse.theta1.shape)
    partial_j2 = np.zeros(sparse.theta2.shape)
    partial_b1 = np.zeros(sparse.b1.shape)
    partial_b2 = np.zeros(sparse.b2.shape)
    delta3t = (-(x - a3) * a3 * (1 - a3)).T
    for i in range(m):
        delta3 = delta3t[:, i:(i + 1)]
        sum1 = np.dot(sparse.theta2.T, delta3)
        delta2 = (sum1 + sum2) * a2[i:(i + 1), :].T * (1 - a2[i:(i + 1), :].T)
        partial_j1 += np.dot(delta2, a1[i:(i + 1), :])
        partial_j2 += np.dot(delta3, a2[i:(i + 1), :])
        partial_b1 += delta2
        partial_b2 += delta3
        # delta3: (64, 1)
        # sum1: (25, 1)
        # delta2: (25, 1)
        # a1[i:(i+1),:]: (1, 64)
        # partial_j1: (25, 64)
        # partial_j2: (64, 25)
        # partial_b1: (25, 1)
        # partial_b2: (64, 1)
        # a2[i:(i+1),:]: (1, 25)
    return partial_j1, partial_j2, partial_b1, partial_b2


def alt():
    delta3 = (-(x - a3) * a3 * (1 - a3)).T
    sum1 = np.dot(sparse.theta2.T, delta3)
    delta2 = (sum1 + sum2) * a2.T * (1 - a2.T)
    # delta3: (64, 10000)
    # sum1: (25, 10000)
    # delta2: (25, 10000)
    # a1: (10000, 64)
    # a2: (10000, 25)
    partial_j1 = np.dot(delta2, a1)
    partial_j2 = np.dot(delta3, a2)
    partial_b1 = delta2.sum(axis=1)
    partial_b2 = delta3.sum(axis=1)
    return partial_j1, partial_j2, partial_b1, partial_b2

answer = orig()
result = alt()
for a, r in Zip(answer, result):
    try:
        assert np.allclose(np.squeeze(a), r)
    except AssertionError:
        print(a.shape)
        print(r.shape)
        raise
_

Astuce: Notez que j'ai laissé dans les commentaires la forme de tous les tableaux intermédiaires. Connaître la forme des tableaux m'a aidé à comprendre ce que faisait votre code. La forme des tableaux peut vous guider vers les bonnes fonctions NumPy à utiliser. Ou du moins, faire attention aux formes peut vous aider à savoir si une opération est sensée. Par exemple, lorsque vous calculez

_np.dot(A, B)
_

et A.shape = (n, m) et B.shape = (m, p), puis np.dot(A, B) sera un tableau de forme _(n, p)_.


Cela peut aider à construire les tableaux dans l'ordre C_CONTIGUOUS (au moins, si vous utilisez _np.dot_). Il pourrait y avoir jusqu'à 3 fois plus de vitesse en procédant ainsi:

Ci-dessous, x est identique à xf sauf que x est C_CONTIGUOUS et xf est F_CONTIGUOUS - et la même relation pour y et yf.

_import numpy as np

m, n, p = 10 ** 4, 64, 25
x = np.random.random((n, m))
xf = np.asarray(x, order='F')

y = np.random.random((m, n))
yf = np.asarray(y, order='F')

assert np.allclose(x, xf)
assert np.allclose(y, yf)
assert np.allclose(np.dot(x, y), np.dot(xf, y))
assert np.allclose(np.dot(x, y), np.dot(xf, yf))
_

_%timeit_ les repères montrent la différence de vitesse:

_In [50]: %timeit np.dot(x, y)
100 loops, best of 3: 12.9 ms per loop

In [51]: %timeit np.dot(xf, y)
10 loops, best of 3: 27.7 ms per loop

In [56]: %timeit np.dot(x, yf)
10 loops, best of 3: 21.8 ms per loop

In [53]: %timeit np.dot(xf, yf)
10 loops, best of 3: 33.3 ms per loop
_

Concernant le benchmarking en Python:

Cela peut être trompeur d'utiliser la différence par paires d'appels time.time() pour comparer la vitesse du code en Python. Vous devez répéter la mesure plusieurs fois. Il vaut mieux désactiver le ramasse-miettes automatique. Il est également important de mesurer de grandes périodes de temps (comme au moins 10 secondes de répétitions) pour éviter les erreurs dues à une mauvaise résolution dans le chronomètre et pour réduire l'importance de la surcharge d'appel de _time.time_. Au lieu d'écrire tout ce code vous-même, Python vous fournit le module timeit . Je l'utilise essentiellement pour chronométrer les morceaux de code, sauf que je suis l'appel via un terminal IPython pour plus de commodité.

Je ne sais pas si cela affecte vos repères, mais sachez que cela pourrait faire une différence. Dans la question à laquelle j'ai lié , selon _time.time_ deux morceaux de code différaient d'un facteur 1,7x tandis que les tests de performance utilisant timeit montraient que les morceaux de code étaient essentiellement identiques quantités de temps.

46
unutbu

Je commencerais par des opérations sur place pour éviter d'allouer de nouvelles baies à chaque fois:

partial_j1 += np.dot(delta2, a1[i,:].reshape(1,a1.shape[1]))
partial_j2 += np.dot(delta3, a2[i,:].reshape(1,a2.shape[1]))
partial_b1 += delta2
partial_b2 += delta3

Vous pouvez remplacer cette expression:

a1[i,:].reshape(1,a1.shape[1])

avec un moyen plus simple et plus rapide (grâce à Bi Rico):

a1[i:i+1]

Aussi, cette ligne:

sum2 = sparse.beta*(-float(sparse.rho)/rhoest + float(1.0 - sparse.rho) / (1.0 - rhoest))

semble être le même à chaque boucle, vous n'avez pas besoin de le recalculer.

Et, une optimisation probablement mineure, vous pouvez remplacer toutes les occurrences de x[i,:] avec x[i].

Enfin, si vous pouvez vous permettre d'allouer la m fois plus de mémoire, vous pouvez suivre la suggestion nutb et vectoriser la boucle:

for m in range(m):
    delta3 = -(x[i]-a3[i])*a3[i]* (1 - a3[i])

avec:

delta3 = -(x-a3)*a3*(1-a3)

Et vous pouvez toujours utiliser Numba et gagner en vitesse de manière significative sans vectorisation (et sans utiliser plus de mémoire).

3
user2304916

La différence de performance entre numpy et matlab m'a toujours frustré. Ils finissent souvent par se résumer aux bibliothèques lapack sous-jacentes. Pour autant que je sache, matlab utilise par défaut l'atlas complet lapack tandis que numpy utilise une lampe lapack. Matlab estime que les gens ne se soucient pas de l'espace et de l'encombrement, tandis que numpy estime que les gens le font. Question similaire avec une bonne réponse.

1
Philliproso