Meest efficiënte manier om functies toe te wijzen aan een numpy array

Wat is de meest efficiënte manier om een functie toe te wijzen aan een numpy-array? De manier waarop ik het in mijn huidige project heb gedaan, is als volgt:

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])

Dit lijkt echter waarschijnlijk erg inefficiënt te zijn, aangezien ik een lijstbegrip gebruik om de nieuwe array als een Python-lijst te construeren voordat ik deze terug converteer naar een numpy-array.

Kunnen we het beter doen?


Antwoord 1, autoriteit 100%

Ik heb alle voorgestelde methoden plus np.array(map(f, x))getest met perfplot(een klein project van mij).

Bericht #1: Als je de native functies van numpy kunt gebruiken, doe dat dan.

Als de functie die u probeert te vectoriseren al isgevectoriseerd (zoals het voorbeeld van x**2in de oorspronkelijke post), is het gebruik van die functie veel sneller dan wat dan ook (let op de logschaal):

Als je vectorisatie echt nodig hebt, maakt het niet zoveel uit welke variant je gebruikt.


Code om de plots te reproduceren:

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=np.random.rand,
    n_range=[2 ** k for k in range(20)],
    kernels=[f, array_for, array_map, fromiter,
             vectorize, vectorize_without_init],
    xlabel="len(x)",
)

2, Autoriteit 43%

Hoe zit het met het gebruik van 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])

3, Autoriteit 23%

TL; DR

Zoals opgemerkt door @ user2357112 , een “Direct” -methode voor het toepassen van de functie is altijd de snelste en eenvoudigste manier om een Functie over numpy arrays:

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

Vermijd over het algemeen np.vectorize, zoals het niet goed presteert, en (of had) een aantal Kwesties . Als u andere gegevenstypen afhandelt, wilt u mogelijk de onderstaande andere methoden onderzoeken.

Vergelijking van methoden

Hier zijn enkele eenvoudige tests om drie methoden te vergelijken om een functie toe te wijzen, in dit voorbeeld met Python 3.6 en NumPy 1.15.4. Eerst de set-up functies voor het testen:

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))

Testen met vijf elementen (gesorteerd van snelst naar langzaamst):

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

Met honderden elementen:

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

En met duizenden array-elementen of meer:

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

Verschillende versies van Python/NumPy en compileroptimalisatie hebben verschillende resultaten, dus doe een vergelijkbare test voor uw omgeving.


Antwoord 4, autoriteit 15%

Er zijn numexpr, numbaen cythonrond, het doel van dit antwoord is om deze mogelijkheden in overweging.

Maar laten we eerst het voor de hand liggende stellen: hoe je een Python-functie ook toewijst aan een numpy-array, het blijft een Python-functie, dat betekent voor elke evaluatie:

  • numpy-array element moet worden geconverteerd naar een Python-object (bijvoorbeeld een Float).
  • alle berekeningen worden gedaan met Python-objecten, wat betekent dat ze de overhead hebben van interpreter, dynamische verzending en onveranderlijke objecten.

Dus welke machine wordt gebruikt om daadwerkelijk door de array te lussen, speelt geen grote rol vanwege de hierboven genoemde overhead – het blijft veel langzamer dan het gebruik van de ingebouwde functionaliteit van numpy.

Laten we het volgende voorbeeld eens bekijken:

# 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.vectorizewordt gekozen als een vertegenwoordiger van de pure-python functieklasse van benaderingen. Met behulp van perfplot(zie code in de bijlage van dit antwoord) krijgen we de volgende looptijden:

We kunnen zien dat de numpy-aanpak 10x-100x sneller is dan de pure Python-versie. De daling van de prestaties voor grotere array-maten is waarschijnlijk omdat gegevens niet langer bij de cache passen.

Het is de moeite waard om te vermelden, dat vectorizeook veel geheugen gebruikt, dus vaak geheugengebruik is de fles-hals (zie gerelateerd SO-vraag ). Noteer ook, die Numpy’s documentatie op np.vectorizestelt dat het “voornamelijk voor het gemak, niet voor prestaties” wordt verstrekt “.

Andere gereedschappen moeten worden gebruikt, wanneer de prestaties gewenst zijn, naast het schrijven van een C-extensie van de kras, zijn er de volgende mogelijkheden:


Men hoort vaak, dat de numpy-uitvoering zo goed is als het wordt, omdat het zuiver C onder de motorkap is. Toch is er een veel ruimte voor verbetering!

De Fured-numpy-versie gebruikt veel extra geheugen en geheugen-toegang. NumExP-library probeert de numpy-arrays tegelijk te maken en dus een beter cache-gebruik te krijgen:

# 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")

leidt naar de volgende vergelijking:

Ik kan niet alles in het plot hierboven uitleggen: we kunnen aan het begin grotere overhead zien voor Numexpr-bibliotheek, maar omdat het de cache beter gebruikt, is het ongeveer 10 keer sneller voor grotere arrays!


Een andere benadering is om de functie te jit-compileren en zo een echte pure-C UFunc te krijgen. Dit is de aanpak van 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

Het is 10 keer sneller dan de originele numpy-aanpak:


De taak is echter beschamend parallelleerbaar, dus we zouden ook prangekunnen gebruiken om de lus parallel te berekenen:

@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

Zoals verwacht is de parallelle functie langzamer voor kleinere invoer, maar sneller (bijna factor 2) voor grotere formaten:


Terwijl numba gespecialiseerd is in het optimaliseren van bewerkingen met numpy-arrays, is Cython een meer algemeen hulpmiddel. Het is ingewikkelder om dezelfde uitvoering te extraheren als met numba – vaak is het te wijten aan llvm (numba) versus lokale compiler (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 resulteert in wat tragere functies:


Conclusie

Het is duidelijk dat het testen van slechts één functie niets bewijst. Houd er ook rekening mee dat voor het gekozen functievoorbeeld de bandbreedte van het geheugen de bottleneck was voor formaten groter dan 10^5 elementen – dus we hadden dezelfde prestaties voor numba, numexpr en cython in deze regio.

Uiteindelijk hangt het ultieme antwoord af van het type functie, hardware, Python-distributie en andere factoren. Anaconda-distribution gebruikt bijvoorbeeld Intel’s VML voor de functies van numpy en presteert dus beter dan numba (tenzij het SVML gebruikt, zie deze SO-post) gemakkelijk voor transcendentale functies zoals exp, sin, cosen dergelijke – zie bijv. de volgende SO-post.

Maar uit dit onderzoek en uit mijn ervaring tot dusver, zou ik zeggen dat numba het gemakkelijkste hulpmiddel lijkt te zijn met de beste prestaties, zolang er geen transcendentale functies bij betrokken zijn.


Rijtijden plotten met perfplot-pakket:

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)'
    )

Antwoord 5, autoriteit 9%

squares = squarer(x)

Rekenkundige bewerkingen op arrays worden automatisch elementsgewijs toegepast, met efficiënte lussen op C-niveau die alle overhead van de interpreter vermijden die van toepassing zou zijn op een lus of begrip op Python-niveau.

De meeste functies die u elementsgewijs op een NumPy-array wilt toepassen, werken gewoon, hoewel sommige mogelijk moeten worden gewijzigd. Bijvoorbeeld, ifwerkt niet elementsgewijs. Je zou die willen converteren om constructies te gebruiken zoals numpy.where:

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

wordt

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

Antwoord 6, autoriteit 3%

Bewerken:het oorspronkelijke antwoord was misleidend,np.sqrtwerd rechtstreeks op de array toegepast, alleen met een kleine overhead .

In multidimensionale gevallen waarin u een ingebouwde functie wilt toepassen die werkt op een 1d-array, numpy.apply_along_axisis een goede keuze, ook voor complexere functiecomposities van numpy en scipy.

Vorige misleidende verklaring:

De methode toevoegen:

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

naar de perfplot-code geeft prestatieresultaten in de buurt van np.sqrt.


Antwoord 7, autoriteit 3%

Het lijkt erop dat niemand een ingebouwde fabrieksmethode heeft genoemd voor het produceren van ufuncin een numpy-pakket: np.frompyfuncdie ik opnieuw heb getest np.vectorizeen hebben het met ongeveer 20~30% beter gepresteerd. Natuurlijk zal het goed presteren als voorgeschreven C-code of zelfs numba(die ik niet heb getest), maar het kan een beter alternatief zijn dan 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

Ik heb ook grotere steekproeven getest en de verbetering is proportioneel. Zie ook hier


Antwoord 8, autoriteit 2%

Ik geloof in een nieuwere versie (ik gebruik 1.13) van numpy, je kunt de functie eenvoudig aanroepen door de numpy-array door te geven aan de fuction die je hebt geschreven voor scalair type, het zal automatisch de functie-aanroep toepassen op elk element over de numpy-array en je een andere numpy-array teruggeven

>>> 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])

Antwoord 9

Zoals vermeld in Dit bericht , gebruik gewoon generatoruitdrukkingen zoals SO:

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

10

Gebruik numpy.fromfunction(function, shape, **kwargs)

Zie “https://docs.scipy.org /doc/numpy/reference/generated/numpy.fromfunction.html

Other episodes