web-dev-qa-db-fra.com

Mapper un tableau NumPy en place

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

Donné a_values - tableau 2D - c'est le bout de code qui me fait l'affaire 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 moche 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_values.map_in_place(dim)

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

50
mac

Cela ne vaut la peine d'essayer de le faire sur place que si vous êtes soumis à 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 lorsque cela 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 authentique sur place d'une fonction arbitraire Python.

>>> 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, la vectorisation est encore plus rapide, donc si vous pouvez en 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, veuillez ne pas vectorize:

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

numpy effectue des opérations comme += en place, comme vous pouvez vous y attendre - vous pouvez ainsi obtenir la vitesse d'un ufunc avec une application sur place sans frais. Parfois, c'est encore plus rapide! Voir ici pour un exemple.


Soit dit en passant, ma réponse originale à cette question, qui peut être consultée dans son historique d'édition, est ridicule et impliquait une vectorisation sur les indices en a. Non seulement il a dû faire des choses géniales pour contourner le mécanisme de détection de type de vectorize , mais il s'est avéré être aussi lent que la version de boucle imbriquée. Voilà pour l'intelligence!

52
senderle

Il s'agit d'un compte rendu de contributions dispersées dans les réponses et les commentaires, que j'ai écrit après avoir accepté la réponse à la question. Les votes positifs sont toujours les bienvenus, mais si vous votez positivement cette réponse, ne manquez pas de voter aussi ceux de senderle et (si (s) il en é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 méthode à tableau unique. Vous devez écrire votre propre code.

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

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 ufunc de numpy augmente la vitesse de plus de 2 et près de 3 ordres de grandeur par rapport aux deuxièmes meilleures et pires alternatives respectivement.

Si l'utilisation de 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!

44
mac

Pourquoi ne pas utiliser l'implémentation numpy et l'ast_ 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_)

eu:

===== ...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%]
=======================================================
3
fabrizioM

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.

2
LBarret

Ceci est juste une version mise à jour de l'écriture de mac, actualisée pour Python 3.x, et avec numba et numpy.frompyfunc ajouté .

numpy.frompyfunc prend une fonction python) et renvoie une fonction qui, lorsqu'elle est castée sur un numpy.array, applique la fonction élément par élément.
Cependant, il change le type de données du tableau en objet, il n'est donc 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é, retournant le type de données à int.

Comme note annexe:
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 en disant qu'il ne peut rien y optimiser. Cependant, étant donné que numba peut souvent accélérer le code écrit dans un style fonctionnel, il est inclus pour l'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%]
=======================================================
1
Sanitiy