52 votes

Mappage d'un tableau NumPy en place

Est-il possible de mapper un tableau NumPy en place ? Si oui, comment ?

Étant donné que a_values - Tableau 2D - c'est le bout de code qui fait l'affaire pour moi en ce moment :

for row in range(len(a_values)):
    for col in range(len(a_values[0])):
        a_values[row][col] = dim(a_values[row][col])

Mais c'est tellement laid que je soupçonne que quelque part dans NumPy, il doit y avoir une fonction qui fait la même chose avec quelque chose qui ressemble à ça :

a_values.map_in_place(dim)

mais si quelque chose comme ce qui précède existe, je n'ai pas réussi à le trouver.

55voto

senderle Points 41607

Cela ne vaut la peine d'essayer de le faire sur place que si vous avez des contraintes d'espace importantes. Si c'est le cas, il est possible d'accélérer un peu votre code en itérant sur une vue aplatie du tableau. Puisque reshape renvoie une nouvelle vue quand c'est possible les données elles-mêmes ne sont pas copiées (sauf si l'original a une structure inhabituelle).

Je ne connais pas de meilleur moyen de réaliser une application in situ authentique d'une fonction Python arbitraire.

>>> def flat_for(a, f):
...     a = a.reshape(-1)
...     for i, v in enumerate(a):
...         a[i] = f(v)
... 
>>> a = numpy.arange(25).reshape(5, 5)
>>> flat_for(a, lambda x: x + 5)
>>> a

array([[ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24],
       [25, 26, 27, 28, 29]])

Quelques horaires :

>>> a = numpy.arange(2500).reshape(50, 50)
>>> f = lambda x: x + 5
>>> %timeit flat_for(a, f)
1000 loops, best of 3: 1.86 ms per loop

C'est environ deux fois plus rapide que la version en boucle imbriquée :

>>> a = numpy.arange(2500).reshape(50, 50)
>>> def nested_for(a, f):
...     for i in range(len(a)):
...         for j in range(len(a[0])):
...             a[i][j] = f(a[i][j])
... 
>>> %timeit nested_for(a, f)
100 loops, best of 3: 3.79 ms per loop

Bien sûr, vectoriser est toujours plus rapide, donc si vous pouvez faire une copie, utilisez-la :

>>> a = numpy.arange(2500).reshape(50, 50)
>>> g = numpy.vectorize(lambda x: x + 5)
>>> %timeit g(a)
1000 loops, best of 3: 584 us per loop

Et si vous pouvez réécrire dim en utilisant des ufuncs intégrés, alors s'il vous plaît, s'il vous plaît, ne vectorize :

>>> a = numpy.arange(2500).reshape(50, 50)
>>> %timeit a + 5
100000 loops, best of 3: 4.66 us per loop

numpy fait des opérations comme += en place, comme on peut s'y attendre, ce qui vous permet de bénéficier gratuitement de la vitesse d'un ufunc avec une application en place. Parfois, c'est même plus rapide ! Voir aquí pour un exemple.


Au fait, ma réponse originale à cette question, qui peut être consultée dans son historique d'édition, est ridicule, et impliquait de vectoriser plus d'indices en a . Non seulement il a dû faire des trucs bizarres pour contourner vectorize 's mécanisme de détection de type il s'est avéré être aussi lent que la version en boucle imbriquée. Voilà pour l'ingéniosité !

46voto

mac Points 16282

Il s'agit d'un compte rendu des contributions dispersées dans les réponses et les commentaires, que j'ai écrites après avoir accepté la réponse à la question. Les votes positifs sont toujours les bienvenus, mais si vous votez cette réponse, s'il vous plaît ne manquez pas de upvoter aussi celles de senderle et (s)il écrit un) eryksun qui a suggéré les méthodes ci-dessous.

Q : Est-il possible de mapper un tableau numpy en place ?
A : Oui, mais pas avec une seule méthode de tableau . Vous devez écrire votre propre code.

Ci-dessous un script qui compare les différentes implémentations discutées dans le fil de discussion :

import timeit
from numpy import array, arange, vectorize, rint

# SETUP
get_array = lambda side : arange(side**2).reshape(side, side) * 30
dim = lambda x : int(round(x * 0.67328))

# TIMER
def best(fname, reps, side):
    global a
    a = get_array(side)
        t = timeit.Timer('%s(a)' % fname,
                     setup='from __main__ import %s, a' % fname)
    return min(t.repeat(reps, 3))  #low num as in place --> converge to 1

# FUNCTIONS
def mac(array_):
    for row in range(len(array_)):
        for col in range(len(array_[0])):
            array_[row][col] = dim(array_[row][col])

def mac_two(array_):
    li = range(len(array_[0]))
    for row in range(len(array_)):
        for col in li:
            array_[row][col] = int(round(array_[row][col] * 0.67328))

def mac_three(array_):
    for i, row in enumerate(array_):
        array_[i][:] = [int(round(v * 0.67328)) for v in row]

def senderle(array_):
    array_ = array_.reshape(-1)
    for i, v in enumerate(array_):
        array_[i] = dim(v)

def eryksun(array_):
    array_[:] = vectorize(dim)(array_)

def ufunc_ed(array_):
    multiplied = array_ * 0.67328
    array_[:] = rint(multiplied)

# MAIN
r = []
for fname in ('mac', 'mac_two', 'mac_three', 'senderle', 'eryksun', 'ufunc_ed'):
    print('\nTesting `%s`...' % fname)
    r.append(best(fname, reps=50, side=50))
    # The following is for visually checking the functions returns same results
    tmp = get_array(3)
    eval('%s(tmp)' % fname)
    print tmp
tmp = min(r)/100
print('\n===== ...AND THE WINNER IS... =========================')
print('  mac (as in question)       :  %.4fms [%.0f%%]') % (r[0]*1000,r[0]/tmp)
print('  mac (optimised)            :  %.4fms [%.0f%%]') % (r[1]*1000,r[1]/tmp)
print('  mac (slice-assignment)     :  %.4fms [%.0f%%]') % (r[2]*1000,r[2]/tmp)
print('  senderle                   :  %.4fms [%.0f%%]') % (r[3]*1000,r[3]/tmp)
print('  eryksun                    :  %.4fms [%.0f%%]') % (r[4]*1000,r[4]/tmp)
print('  slice-assignment w/ ufunc  :  %.4fms [%.0f%%]') % (r[5]*1000,r[5]/tmp)
print('=======================================================\n')

La sortie du script ci-dessus - au moins dans mon système - est :

  mac (as in question)       :  88.7411ms [74591%]
  mac (optimised)            :  86.4639ms [72677%]
  mac (slice-assignment)     :  79.8671ms [67132%]
  senderle                   :  85.4590ms [71832%]
  eryksun                    :  13.8662ms [11655%]
  slice-assignment w/ ufunc  :  0.1190ms [100%]

Comme vous pouvez le constater, en utilisant la fonction ufunc augmente la vitesse de plus de 2 et presque 3 ordres de grandeur par rapport à la deuxième meilleure et à la pire solution respectivement.

Si vous utilisez ufunc n'est pas une option, voici une comparaison des autres alternatives uniquement :

  mac (as in question)       :  91.5761ms [672%]
  mac (optimised)            :  88.9449ms [653%]
  mac (slice-assignment)     :  80.1032ms [588%]
  senderle                   :  86.3919ms [634%]
  eryksun                    :  13.6259ms [100%]

HTH !

3voto

fabrizioM Points 11498

Pourquoi ne pas utiliser l'implémentation de numpy, et l'astuce out_ ?

from numpy import array, arange, vectorize, rint, multiply, round as np_round 

def fmilo(array_):
    np_round(multiply(array_ ,0.67328, array_), out=array_)

obtenu :

===== ...AND THE WINNER IS... =========================
  mac (as in question)       :  80.8470ms [130422%]
  mac (optimised)            :  80.2400ms [129443%]
  mac (slice-assignment)     :  75.5181ms [121825%]
  senderle                   :  78.9380ms [127342%]
  eryksun                    :  11.0800ms [17874%]
  slice-assignment w/ ufunc  :  0.0899ms [145%]
  fmilo                      :  0.0620ms [100%]
=======================================================

2voto

LBarret Points 631

Si les ufuncs ne sont pas possibles, vous devriez peut-être envisager d'utiliser cython. Il est facile à intégrer et donne de grandes accélérations sur l'utilisation spécifique des tableaux numpy.

2voto

Sanitiy Points 108

Il s'agit simplement d'une version mise à jour de l'article de mac, actualisée pour Python 3.x, et avec numba y numpy.frompyfunc ajouté.

numpy.frompyfunc prend une fonction python abitraire et retourne une fonction qui, lorsqu'elle est appliquée à un tableau numpy.array, applique la fonction par élément.
Cependant, cela change le type de données du tableau en objet, donc il n'est pas en place, et les futurs calculs sur ce tableau seront plus lents.
Pour éviter cet inconvénient, dans le test numpy.ndarray.astype sera appelé, renvoyant le type de données à int.

En guise d'aparté :
Numba n'est pas inclus dans les bibliothèques de base de Python et doit être téléchargé en externe si vous voulez le tester. Dans ce test, il ne fait rien, et s'il avait été appelé avec @jit(nopython=True) il aurait donné un message d'erreur disant qu'il ne peut rien optimiser à cet endroit. Cependant, comme numba peut souvent accélérer du code écrit dans un style fonctionnel, il est inclus par souci d'intégrité.

import timeit
from numpy import array, arange, vectorize, rint, frompyfunc
from numba import autojit

# SETUP
get_array = lambda side : arange(side**2).reshape(side, side) * 30
dim = lambda x : int(round(x * 0.67328))

# TIMER
def best(fname, reps, side):
    global a
    a = get_array(side)
    t = timeit.Timer('%s(a)' % fname,
                     setup='from __main__ import %s, a' % fname)
    return min(t.repeat(reps, 3))  #low num as in place --> converge to 1

# FUNCTIONS
def mac(array_):
    for row in range(len(array_)):
        for col in range(len(array_[0])):
            array_[row][col] = dim(array_[row][col])

def mac_two(array_):
    li = range(len(array_[0]))
    for row in range(len(array_)):
        for col in li:
            array_[row][col] = int(round(array_[row][col] * 0.67328))

def mac_three(array_):
    for i, row in enumerate(array_):
        array_[i][:] = [int(round(v * 0.67328)) for v in row]

def senderle(array_):
    array_ = array_.reshape(-1)
    for i, v in enumerate(array_):
        array_[i] = dim(v)

def eryksun(array_):
    array_[:] = vectorize(dim)(array_)

@autojit
def numba(array_):
    for row in range(len(array_)):
        for col in range(len(array_[0])):
            array_[row][col] = dim(array_[row][col])

def ufunc_ed(array_):
    multiplied = array_ * 0.67328
    array_[:] = rint(multiplied)

def ufunc_frompyfunc(array_):
    udim = frompyfunc(dim,1,1)
    array_ = udim(array_)
    array_.astype("int")

# MAIN
r = []
totest = ('mac', 'mac_two', 'mac_three', 'senderle', 'eryksun', 'numba','ufunc_ed','ufunc_frompyfunc')
for fname in totest:
    print('\nTesting `%s`...' % fname)
    r.append(best(fname, reps=50, side=50))
    # The following is for visually checking the functions returns same results
    tmp = get_array(3)
    eval('%s(tmp)' % fname)
    print (tmp)
tmp = min(r)/100
results = list(zip(totest,r))
results.sort(key=lambda x: x[1])

print('\n===== ...AND THE WINNER IS... =========================')
for name,time in results:
    Out = '{:<34}: {:8.4f}ms [{:5.0f}%]'.format(name,time*1000,time/tmp)
    print(Out)
print('=======================================================\n')

Et enfin, les résultats :

===== ...AND THE WINNER IS... =========================
ufunc_ed                          :   0.3205ms [  100%]
ufunc_frompyfunc                  :   3.8280ms [ 1194%]
eryksun                           :   3.8989ms [ 1217%]
mac_three                         :  21.4538ms [ 6694%]
senderle                          :  22.6421ms [ 7065%]
mac_two                           :  24.6230ms [ 7683%]
mac                               :  26.1463ms [ 8158%]
numba                             :  27.5041ms [ 8582%]
=======================================================

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X