web-dev-qa-db-fra.com

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

Quel est le moyen le plus efficace de mapper une fonction sur un tableau numpy? Voici comment je l'ai fait dans mon projet actuel:

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 probablement très inefficace, car j’utilise une compréhension de liste pour construire le nouveau tableau sous forme de liste Python avant de le reconvertir en tableau numpy.

Pouvons-nous faire mieux?

228
Ryan

J'ai testé toutes les méthodes suggérées ainsi que np.array(map(f, x)) avec perfplot (un de mes petits projets).

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'exemple x**2 de l'article original), utilisez beaucoup plus rapide (notez l'échelle du journal):

enter image description here

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

enter image description here


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
        ],
    logx=True,
    logy=True,
    xlabel='len(x)',
    )
197
Nico Schlömer

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)
array([ 1,  4,  9, 16, 25])

https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html

107
satomacoto

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 gérez d'autres types de données, vous pouvez explorer les autres méthodes présentées ci-dessous.

Comparaison de méthodes

Voici quelques tests simples permettant de comparer trois méthodes pour mapper une fonction. Cet exemple utilise avec Python 3.6 et NumPy 1.15.4. Premièrement, 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. Faites donc un test similaire pour votre environnement.

56
Mike T

Depuis que cette question a reçu une réponse, il s'est passé beaucoup de choses - il y a numexpr , numba et cython environ. Le but de cette réponse est de prendre en compte ces possibilités.

Mais d'abord, énonçons ce qui est évident: quelle que soit la manière dont vous mappez une fonction Python sur un tableau numpy, il reste une fonction Python, ce qui signifie pour chaque évaluation:

  • l'élément numpy-array doit être converti en objet Python (par exemple, un Float).
  • tous les calculs sont effectués avec des objets Python, ce qui signifie qu’il doit y avoir un surcoût en interprète, en envoi dynamique et en objets immuables.

Ainsi, la machinerie utilisée pour effectuer une boucle dans le tableau ne joue pas un rôle important en raison de la surcharge évoquée ci-dessus - elle reste beaucoup plus lente que la vectorisation numérique.

Jetons un coup d'oeil à 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.vectorize est choisi en tant que représentant de la classe d'approches fonction Python. En utilisant perfplot (voir le code en annexe de cette réponse), nous obtenons les temps d'exécution suivants:

enter image description here

Nous pouvons voir que l'approche numpy est 10x-100x plus rapide que la version pure python. La diminution des performances pour les baies plus grandes est probablement due au fait que les données ne sont plus compatibles avec le cache.

On entend souvent dire que la performance numpy est aussi bonne que possible, car c'est du C pur 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 essaye de paver numpy-arrays en mosaïque et d'obtenir ainsi 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:

enter image description here

Je ne peux pas tout expliquer dans l'intrigue ci-dessus: nous pouvons voir une charge plus importante pour numexpr-library au début, mais comme elle utilise mieux le cache, il est environ 10 fois plus rapide pour les baies plus grandes!


Une autre approche consiste à jit-compiler la fonction et à obtenir ainsi un véritable UFunc pur-C. C'est 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:

enter image description here


Cependant, la tâche est embarrassante parallélisable, nous pourrions donc aussi utiliser prange pour 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 entrées plus petites, mais plus rapide (presque 2 fois plus) pour les tailles plus grandes:

enter image description here


Alors que numba est spécialisé dans l'optimisation des opérations avec numpy-arrays, Cython est un outil plus général. Il est plus compliqué d'extraire les mêmes performances qu'avec numba - il est souvent nécessaire d'utiliser llvm (numba) ou un 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 donne des fonctions un peu plus lentes:

enter image description here


Conclusion

Évidemment, tester seulement une fonction ne prouve rien. Il faut également garder à l’esprit que, pour l’exemple choisi, la bande passante de la mémoire était le goulot d’embouteillage pour les tailles supérieures à 10 ^ 5 éléments. Nous avions donc les mêmes performances pour numba, numexpr et cython dans cette région.

Cependant, d'après cette enquête et mon expérience jusqu'ici, le numba semble être l'outil le plus simple et le plus performant.


Tracé des temps d'exécution 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)'
    )
23
ead
_squares = squarer(x)
_

Les opérations arithmétiques sur les tableaux sont automatiquement appliquées élément par élément, avec des boucles de niveau C efficaces évitant tout le surcoût 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, mais certaines peuvent nécessiter des modifications. Par exemple, if ne fonctionne pas élément par élément. Vous voudriez convertir ceux-ci 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)
_
22
user2357112

Je crois que dans la version plus récente (j'utilise 1.13) de numpy, vous pouvez simplement appeler la fonction en transmettant le tableau numpy à la fonction que vous avez écrite pour le type scalaire; 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])
9
Peiti Li

Il semble que personne n'ait mentionné une méthode d'usine intégrée pour la production de ufunc in numpy: np.frompyfunc que j'ai testée à nouveau np.vectorize et l'ai surperformée d'environ 20 à 30%. Bien sûr, il fonctionnera bien comme code C prescrit ou même numba (que je n’ai pas testé), mais il peut être une meilleure alternative que np.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é de plus grands échantillons, et l'amélioration est proportionnelle. Voir aussi la documentation ici

2
Wunderbar

Comme mentionné dans this post , utilisez simplement des expressions de générateur telles que:

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