Le moyen le plus efficace de mapper la fonction sur un tableau numpy


338

Quelle est la façon la plus efficace de mapper une fonction sur un tableau numpy? La façon dont je l'ai fait dans mon projet actuel est la suivante:

import numpy as np 

x = np.array([1, 2, 3, 4, 5])

# Obtain array of square of each element in x
squarer = lambda t: t ** 2
squares = np.array([squarer(xi) for xi in x])

Cependant, cela semble être probablement très inefficace, car j'utilise une compréhension de liste pour construire le nouveau tableau en tant que liste Python avant de le reconvertir en un tableau numpy.

Pouvons-nous faire mieux?


10
pourquoi pas "squares = x ** 2"? Avez-vous une fonction beaucoup plus compliquée à évaluer?
22 degrés

4
Et seulement squarer(x)?
Life

1
Peut-être que cela ne répond pas directement à la question, mais j'ai entendu dire que numba peut compiler le code python existant en instructions machine parallèles. Je revisiterai et réviserai ce post quand j'aurai réellement l'occasion de l'utiliser.
把 友情 留 在 无 盐

x = np.array([1, 2, 3, 4, 5]); x**2travaux
Shark Deng

Réponses:


283

J'ai testé toutes les méthodes suggérées plus np.array(map(f, x))avec perfplot(un petit projet à moi).

Message n ° 1: si vous pouvez utiliser les fonctions natives de numpy, faites-le.

Si la fonction que vous essayez déjà de vectoriser est vectorisée (comme l' x**2exemple dans le message d'origine), son utilisation est beaucoup plus rapide que toute autre chose (notez l'échelle du journal):

entrez la description de l'image ici

Si vous avez réellement besoin de vectorisation, peu importe la variante que vous utilisez.

entrez la description de l'image ici


Code pour reproduire les parcelles:

import numpy as np
import perfplot
import math


def f(x):
    # return math.sqrt(x)
    return np.sqrt(x)


vf = np.vectorize(f)


def array_for(x):
    return np.array([f(xi) for xi in x])


def array_map(x):
    return np.array(list(map(f, x)))


def fromiter(x):
    return np.fromiter((f(xi) for xi in x), x.dtype)


def vectorize(x):
    return np.vectorize(f)(x)


def vectorize_without_init(x):
    return vf(x)


perfplot.show(
    setup=lambda n: np.random.rand(n),
    n_range=[2 ** k for k in range(20)],
    kernels=[f, array_for, array_map, fromiter, vectorize, vectorize_without_init],
    xlabel="len(x)",
)

7
Vous semblez avoir laissé de f(x)côté votre intrigue. Il n'est peut-être pas applicable à tous f, mais il s'applique ici, et c'est facilement la solution la plus rapide lorsqu'elle est applicable.
user2357112 prend en charge Monica

2
En outre, votre intrigue ne prend pas en charge votre affirmation qui vf = np.vectorize(f); y = vf(x)gagne pour des entrées courtes.
user2357112 prend en charge Monica

Après avoir installé perfplot (v0.3.2) via pip ( pip install -U perfplot), je vois le message: AttributeError: 'module' object has no attribute 'save'lors du collage de l'exemple de code.
tsherwen

Et une boucle vanille pour?
Catiger3331

1
@Vlad utilise simplement math.sqrt comme commenté.
Nico Schlömer

138

Que diriez-vous d'utiliser numpy.vectorize.

import numpy as np
x = np.array([1, 2, 3, 4, 5])
squarer = lambda t: t ** 2
vfunc = np.vectorize(squarer)
vfunc(x)
# Output : array([ 1,  4,  9, 16, 25])

36
Ce n'est pas plus efficace.
user2357112 prend en charge Monica

78
De ce document: The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. Dans d'autres questions, j'ai trouvé que cela vectorizepourrait doubler la vitesse d'itération de l'utilisateur. Mais la véritable accélération se fait avec de vraies numpyopérations de tableau.
hpaulj

2
Notez que vectorize fait au moins fonctionner les tableaux non-1d
Eric

Mais squarer(x)fonctionnerait déjà pour les tableaux non 1d. vectorizen'a vraiment aucun avantage sur une compréhension de liste (comme celle de la question), pas sur squarer(x).
user2357112 prend en charge Monica

79

TL; DR

Comme indiqué par @ user2357112 , une méthode "directe" d'application de la fonction est toujours le moyen le plus rapide et le plus simple de mapper une fonction sur des tableaux Numpy:

import numpy as np
x = np.array([1, 2, 3, 4, 5])
f = lambda x: x ** 2
squares = f(x)

Évitez généralement np.vectorize, car il ne fonctionne pas bien et a (ou a eu) un certain nombre de problèmes . Si vous manipulez d'autres types de données, vous souhaiterez peut-être étudier les autres méthodes indiquées ci-dessous.

Comparaison des méthodes

Voici quelques tests simples pour comparer trois méthodes pour mapper une fonction, cet exemple utilisant avec Python 3.6 et NumPy 1.15.4. Tout d'abord, les fonctions de configuration pour tester:

import timeit
import numpy as np

f = lambda x: x ** 2
vf = np.vectorize(f)

def test_array(x, n):
    t = timeit.timeit(
        'np.array([f(xi) for xi in x])',
        'from __main__ import np, x, f', number=n)
    print('array: {0:.3f}'.format(t))

def test_fromiter(x, n):
    t = timeit.timeit(
        'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))',
        'from __main__ import np, x, f', number=n)
    print('fromiter: {0:.3f}'.format(t))

def test_direct(x, n):
    t = timeit.timeit(
        'f(x)',
        'from __main__ import x, f', number=n)
    print('direct: {0:.3f}'.format(t))

def test_vectorized(x, n):
    t = timeit.timeit(
        'vf(x)',
        'from __main__ import x, vf', number=n)
    print('vectorized: {0:.3f}'.format(t))

Test avec cinq éléments (triés du plus rapide au plus lent):

x = np.array([1, 2, 3, 4, 5])
n = 100000
test_direct(x, n)      # 0.265
test_fromiter(x, n)    # 0.479
test_array(x, n)       # 0.865
test_vectorized(x, n)  # 2.906

Avec des centaines d'éléments:

x = np.arange(100)
n = 10000
test_direct(x, n)      # 0.030
test_array(x, n)       # 0.501
test_vectorized(x, n)  # 0.670
test_fromiter(x, n)    # 0.883

Et avec des milliers d'éléments de tableau ou plus:

x = np.arange(1000)
n = 1000
test_direct(x, n)      # 0.007
test_fromiter(x, n)    # 0.479
test_array(x, n)       # 0.516
test_vectorized(x, n)  # 0.945

Les différentes versions de Python / NumPy et l'optimisation du compilateur auront des résultats différents, alors faites un test similaire pour votre environnement.


2
Si vous utilisez l' countargument et une expression de générateur, np.fromiterc'est beaucoup plus rapide.
juanpa.arrivillaga

3
Ainsi, par exemple, utilisez'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'
juanpa.arrivillaga

4
Vous n'avez pas testé la solution directe de f(x), qui bat tout le reste de plus d'un ordre de grandeur .
user2357112 prend en charge Monica

4
Et si fa 2 variables et que le tableau est 2D?
Sigur

2
Je suis confus quant à la façon dont la version 'f (x)' ("directe") est réellement considérée comme comparable lorsque l'OP demandait comment "mapper" une fonction à travers un tableau? Dans le cas de f (x) = x ** 2, le ** est effectué par numpy sur l'ensemble du tableau et non pas par élément. Par exemple, si f (x) est "lambda x: x + x", la réponse est très différente car numpy concatène les tableaux au lieu de faire l'ajout par élément. S'agit-il vraiment de la comparaison prévue? Veuillez expliquer.
Andrew Mellinger

49

Il y a numexpr , numba et cython autour, le but de cette réponse est de prendre ces possibilités en considération.

Mais disons d'abord l'évidence: peu importe comment vous mappez une fonction Python sur un tableau numpy, elle reste une fonction Python, ce qui signifie pour chaque évaluation:

  • L'élément numpy-array doit être converti en objet Python (par exemple a Float).
  • tous les calculs sont effectués avec des objets Python, ce qui signifie avoir la surcharge d'interprète, de répartition dynamique et d'objets immuables.

La machine utilisée pour boucler le tableau ne joue donc pas un grand rôle en raison de la surcharge mentionnée ci-dessus - elle reste beaucoup plus lente que l'utilisation de la fonctionnalité intégrée de numpy.

Jetons un œil à l'exemple suivant:

# numpy-functionality
def f(x):
    return x+2*x*x+4*x*x*x

# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"

np.vectorizeest choisi comme représentant de la classe d'approches de fonction en python pur. En utilisant perfplot(voir le code en annexe de cette réponse) nous obtenons les durées de fonctionnement suivantes:

entrez la description de l'image ici

Nous pouvons voir que l'approche numpy est 10x-100x plus rapide que la version pure python. La diminution des performances pour les tailles de baie plus importantes est probablement due au fait que les données ne correspondent plus au cache.

Il convient également de mentionner, qui vectorizeutilise également beaucoup de mémoire, l'utilisation de la mémoire est donc souvent le goulot d'étranglement (voir la question SO connexe ). Notez également que la documentation de numpy sur np.vectorizeindique qu'elle est "fournie principalement pour des raisons de commodité et non pour des performances".

D'autres outils doivent être utilisés, lorsque des performances sont souhaitées, outre l'écriture d'une extension C à partir de zéro, il existe les possibilités suivantes:


On entend souvent que la performance numpy est aussi bonne que possible, car elle est en pur C sous le capot. Pourtant, il y a encore beaucoup à faire!

La version numpy vectorisée utilise beaucoup de mémoire supplémentaire et d'accès à la mémoire. Numexp-library essaie de paver les tableaux numpy et ainsi obtenir une meilleure utilisation du cache:

# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
    return ne.evaluate("x+2*x*x+4*x*x*x")

Conduit à la comparaison suivante:

entrez la description de l'image ici

Je ne peux pas tout expliquer dans l'intrigue ci-dessus: nous pouvons voir des frais généraux plus importants pour numexpr-library au début, mais parce qu'il utilise mieux le cache, il est environ 10 fois plus rapide pour les tableaux plus gros!


Une autre approche consiste à compiler jit la fonction et à obtenir ainsi un véritable UFunc pur-C. Voici l'approche de numba:

# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
    return x+2*x*x+4*x*x*x

C'est 10 fois plus rapide que l'approche numpy originale:

entrez la description de l'image ici


Cependant, la tâche est embarrassablement parallélisable, nous pourrions donc également l'utiliser prangepour calculer la boucle en parallèle:

@nb.njit(parallel=True)
def nb_par_jitf(x):
    y=np.empty(x.shape)
    for i in nb.prange(len(x)):
        y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
    return y

Comme prévu, la fonction parallèle est plus lente pour les petites entrées, mais plus rapide (presque facteur 2) pour les grandes tailles:

entrez la description de l'image ici


Alors que numba est spécialisé dans l'optimisation des opérations avec les tableaux numpy, Cython est un outil plus général. Il est plus compliqué d'extraire les mêmes performances qu'avec numba - il s'agit souvent de llvm (numba) vs compilateur local (gcc / MSVC):

%%cython -c=/openmp -a
import numpy as np
import cython

#single core:
@cython.boundscheck(False) 
@cython.wraparound(False) 
def cy_f(double[::1] x):
    y_out=np.empty(len(x))
    cdef Py_ssize_t i
    cdef double[::1] y=y_out
    for i in range(len(x)):
        y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
    return y_out

#parallel:
from cython.parallel import prange
@cython.boundscheck(False) 
@cython.wraparound(False)  
def cy_par_f(double[::1] x):
    y_out=np.empty(len(x))
    cdef double[::1] y=y_out
    cdef Py_ssize_t i
    cdef Py_ssize_t n = len(x)
    for i in prange(n, nogil=True):
        y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
    return y_out

Cython entraîne des fonctions un peu plus lentes:

entrez la description de l'image ici


Conclusion

De toute évidence, le test d'une seule fonction ne prouve rien. Il convient également de garder à l'esprit que pour l'exemple de fonction choisi, la bande passante de la mémoire était le col de la bouteille pour les tailles supérieures à 10 ^ 5 éléments - nous avons donc eu les mêmes performances pour numba, numexpr et cython dans cette région.

En fin de compte, la réponse ultime dépend du type de fonction, du matériel, de la distribution Python et d'autres facteurs. Par exemple Anaconda distribution utilise VML d'Intel pour les fonctions de numpy et donc surclasse Numba ( à moins qu'il utilise SVML, voir ce SO-post ) pour les fonctions transcendantes facilement aiment exp, sin, coset même - voir par exemple les éléments suivants SO-post .

Pourtant, à partir de cette enquête et de mon expérience jusqu'à présent, je dirais que le numba semble être l'outil le plus simple avec les meilleures performances tant qu'aucune fonction transcendantale n'est impliquée.


Tracer les temps de fonctionnement avec perfplot -package :

import perfplot
perfplot.show(
    setup=lambda n: np.random.rand(n),
    n_range=[2**k for k in range(0,24)],
    kernels=[
        f, 
        vf,
        ne_f, 
        nb_vf, nb_par_jitf,
        cy_f, cy_par_f,
        ],
    logx=True,
    logy=True,
    xlabel='len(x)'
    )

1
Numba peut généralement utiliser Intel SVML, ce qui entraîne des délais assez comparables par rapport à Intel VML, mais l'implémentation est un peu boguée dans la version (0.43-0.47). J'ai ajouté un graphique de performance stackoverflow.com/a/56939240/4045774 pour comparer à votre cy_expsum.
max9111

29
squares = squarer(x)

Les opérations arithmétiques sur les tableaux sont automatiquement appliquées par élément, avec des boucles de niveau C efficaces qui évitent toute surcharge de l'interpréteur qui s'appliquerait à une boucle ou à une compréhension de niveau Python.

La plupart des fonctions que vous souhaitez appliquer à un tableau NumPy élément par élément fonctionneront, même si certaines peuvent nécessiter des modifications. Par exemple, ifne fonctionne pas par élément. Vous voudriez les convertir pour utiliser des constructions comme numpy.where:

def using_if(x):
    if x < 5:
        return x
    else:
        return x**2

devient

def using_where(x):
    return numpy.where(x < 5, x, x**2)

9

Dans de nombreux cas, numpy.apply_along_axis sera le meilleur choix. Il augmente les performances d'environ 100x par rapport aux autres approches - et pas seulement pour les fonctions de test triviales, mais aussi pour les compositions de fonctions plus complexes de numpy et scipy.

Quand j'ajoute la méthode:

def along_axis(x):
    return np.apply_along_axis(f, 0, x)

au code perfplot, j'obtiens les résultats suivants: entrez la description de l'image ici


Je suis extrêmement choqué par le fait que la plupart des gens ne semblent pas être au courant de cette évidence simple, évolutive et intégrée pendant tant d'années ....
Bill Huang

8

Je crois que dans une version plus récente (j'utilise 1.13) de numpy, vous pouvez simplement appeler la fonction en passant le tableau numpy à la fonction que vous avez écrite pour le type scalaire, il appliquera automatiquement l'appel de fonction à chaque élément sur le tableau numpy et vous renverra un autre tableau numpy

>>> import numpy as np
>>> squarer = lambda t: t ** 2
>>> x = np.array([1, 2, 3, 4, 5])
>>> squarer(x)
array([ 1,  4,  9, 16, 25])

3
Ce n'est pas nouveau à distance - cela a toujours été le cas - c'est l'une des principales fonctionnalités de numpy.
Eric

8
C'est l' **opérateur qui applique le calcul à chaque élément t de t. C'est engourdi ordinaire. L'envelopper dans le lambdane fait rien de plus.
hpaulj

Cela ne fonctionne pas avec les instructions if telles qu'elles sont actuellement affichées.
TriHard8

7

Il semble que personne n'ait mentionné une méthode ufuncintégrée de production en usine dans le paquet numpy: np.frompyfuncque j'ai testé à nouveau np.vectorizeet l'ai surpassé d'environ 20 à 30%. Bien sûr, il fonctionnera bien comme le code C prescrit ou même numba(que je n'ai pas testé), mais il peut être une meilleure alternative quenp.vectorize

f = lambda x, y: x * y
f_arr = np.frompyfunc(f, 2, 1)
vf = np.vectorize(f)
arr = np.linspace(0, 1, 10000)

%timeit f_arr(arr, arr) # 307ms
%timeit vf(arr, arr) # 450ms

J'ai également testé des échantillons plus gros et l'amélioration est proportionnelle. Voir également la documentation ici


1
J'ai répété les tests de synchronisation ci-dessus, et j'ai également constaté une amélioration des performances (par rapport à np.vectorize) d'environ 30%
Julian - BrainAnnex.org

2

Comme mentionné dans cet article , utilisez simplement des expressions de générateur comme ceci:

numpy.fromiter((<some_func>(x) for x in <something>),<dtype>,<size of something>)

2

Toutes les réponses ci-dessus se comparent bien, mais si vous devez utiliser une fonction personnalisée pour le mappage, et vous l'avez numpy.ndarray, et vous devez conserver la forme du tableau.

Je n'ai comparé que deux, mais il conservera la forme de ndarray. J'ai utilisé le tableau avec 1 million d'entrées pour comparaison. Ici, j'utilise la fonction carrée, qui est également intégrée à numpy et a une grande amélioration des performances, car comme il y avait besoin de quelque chose, vous pouvez utiliser la fonction de votre choix.

import numpy, time
def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([x * x for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((x * x for x in y.reshape(-1)), y.dtype).reshape(y.shape)
    print(time.time() - now)
    now = time.time()
    numpy.square(y)  
    print(time.time() - now)

Production

>>> timeit()
1.162431240081787    # list comprehension and then building numpy array
1.0775556564331055   # from numpy.fromiter
0.002948284149169922 # using inbuilt function

ici, vous pouvez clairement voir les numpy.fromitertravaux excellents compte tenu de l'approche simple, et si la fonction intégrée est disponible, veuillez l'utiliser.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.