Il modo più efficiente per mappare la funzione su matrice numpy


338

Qual è il modo più efficiente per mappare una funzione su un array intorpidito? Il modo in cui l'ho fatto nel mio progetto attuale è il seguente:

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

Tuttavia, questo sembra probabilmente molto inefficiente, dal momento che sto usando una comprensione della lista per costruire il nuovo array come un elenco Python prima di riconvertirlo in un array intorpidito.

Possiamo fare di meglio?


10
perché non "quadrati = x ** 2"? Hai una funzione molto più complicata che devi valutare?
22 gradi

4
Che ne dici solo squarer(x)?
Vita dal

1
Forse questo non risponde direttamente alla domanda, ma ho sentito che numba può compilare il codice python esistente in istruzioni macchina parallele. Rivederò e rivedrò questo post quando avrò effettivamente la possibilità di usarlo.
把 友情 留 在 无 盐

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

Risposte:


283

Ho testato tutti i metodi suggeriti più np.array(map(f, x))con perfplot(un mio piccolo progetto).

Messaggio n. 1: se puoi usare le funzioni native di numpy, fallo.

Se la funzione che stai già tentando di vettorializzare è già vettorizzata (come x**2nell'esempio nel post originale), l'utilizzo è molto più veloce di qualsiasi altra cosa (nota la scala del registro):

inserisci qui la descrizione dell'immagine

Se hai davvero bisogno della vettorializzazione, non importa molto quale variante usi.

inserisci qui la descrizione dell'immagine


Codice per riprodurre i grafici:

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
Sembra che tu abbia lasciato f(x)fuori la trama. Potrebbe non essere applicabile per tutti f, ma è applicabile qui ed è facilmente la soluzione più veloce quando applicabile.
user2357112 supporta Monica

2
Inoltre, la trama non supporta l'affermazione che vf = np.vectorize(f); y = vf(x)vince per input brevi.
user2357112 supporta Monica

Dopo aver installato perfplot (v0.3.2) tramite pip ( pip install -U perfplot), vedo il messaggio: AttributeError: 'module' object has no attribute 'save'quando si incolla il codice di esempio.
Tsherwen,

Che dire di una vaniglia per loop?
Catiger3331,

1
@Vlad usa semplicemente math.sqrt come commentato.
Nico Schlömer,

138

Che ne dici di usare 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
Questo non è più efficiente.
user2357112 supporta Monica il

78
Da quel documento: The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. In altre domande ho scoperto che vectorizepotrebbe raddoppiare la velocità di iterazione dell'utente. Ma la vera velocità è con numpyoperazioni di array reali .
hpaulj,

2
Si noti che vectorize fa almeno funzionare le cose per array non 1d
Eric,

Ma squarer(x)funzionerebbe già per array non 1d. vectorizeha davvero qualche vantaggio rispetto alla comprensione di una lista (come quella nella domanda), non alla fine squarer(x).
user2357112 supporta Monica

79

TL; DR

Come notato da @ user2357112 , un metodo "diretto" per applicare la funzione è sempre il modo più rapido e semplice per mappare una funzione su matrici Numpy:

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

Generalmente evita np.vectorize, poiché non funziona bene e ha (o ha avuto) una serie di problemi . Se stai gestendo altri tipi di dati, potresti voler esaminare gli altri metodi mostrati di seguito.

Confronto di metodi

Ecco alcuni semplici test per confrontare tre metodi per mappare una funzione, questo esempio usando con Python 3.6 e NumPy 1.15.4. Innanzitutto, le funzioni di impostazione per i test:

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 con cinque elementi (ordinati dal più veloce al più lento):

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

Con centinaia di elementi:

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

E con migliaia di elementi array o più:

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

Versioni diverse di Python / NumPy e l'ottimizzazione del compilatore avranno risultati diversi, quindi esegui un test simile per il tuo ambiente.


2
Se usi l' countargomento e un'espressione del generatore, allora np.fromiterè significativamente più veloce.
juanpa.arrivillaga,

3
Quindi, per esempio, usa'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'
juanpa.arrivillaga il

4
Non hai testato la soluzione diretta di f(x), che batte tutto il resto di un ordine di grandezza .
user2357112 supporta Monica

4
Che dire se fha 2 variabili e l'array è 2D?
Sigur,

2
Sono confuso come la versione 'f (x)' ("diretta") sia effettivamente considerata comparabile quando l'OP stava chiedendo come "mappare" una funzione attraverso un array? Nel caso di f (x) = x ** 2, ** viene eseguito da numpy sull'intero array non su una base per elemento. Ad esempio, se f (x) è "lambda x: x + x", la risposta è molto diversa perché numpy concatena le matrici invece di eseguire l'aggiunta per elemento. È davvero il confronto previsto? Spiegare.
Andrew Mellinger

49

Ci sono numexpr , numba e cython in giro, l'obiettivo di questa risposta è di prendere in considerazione queste possibilità.

Ma prima diciamo l'ovvio: non importa come si mappa una funzione Python su un array numpy, rimane una funzione Python, questo significa per ogni valutazione:

  • L'elemento numpy-array deve essere convertito in un oggetto Python (ad es Float. a).
  • tutti i calcoli vengono eseguiti con oggetti Python, il che significa avere l'overhead di interprete, invio dinamico e oggetti immutabili.

Pertanto, quale macchinario viene effettivamente utilizzato per eseguire il loop nell'array non gioca un ruolo importante a causa del sovraccarico sopra menzionato: rimane molto più lento rispetto all'utilizzo della funzionalità integrata di numpy.

Diamo un'occhiata al seguente esempio:

# 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.vectorizeviene scelto come rappresentante della classe di approcci della funzione pure-python. Usando perfplot(vedi il codice nell'appendice di questa risposta) otteniamo i seguenti tempi di esecuzione:

inserisci qui la descrizione dell'immagine

Possiamo vedere che l'approccio intorpidito è 10x-100x più veloce della versione pura di Python. La riduzione delle prestazioni per array di dimensioni maggiori è probabilmente dovuta al fatto che i dati non si adattano più alla cache.

Vale anche la pena ricordare che vectorizeutilizza anche molta memoria, quindi spesso l'utilizzo della memoria è il collo di bottiglia (vedi la relativa domanda SO ). Si noti inoltre che la documentazione di quel numpy np.vectorizeafferma che è "fornita principalmente per comodità, non per prestazioni".

Altri strumenti dovrebbero essere usati, quando si desiderano prestazioni, oltre a scrivere un'estensione C da zero, ci sono le seguenti possibilità:


Si sente spesso che le prestazioni intorpidite sono buone quanto si ottiene, perché è pura C sotto il cofano. Eppure c'è molto margine di miglioramento!

La versione numpy vettorizzata utilizza molta memoria aggiuntiva e accessi alla memoria. La libreria Numexp tenta di affiancare gli array numpy e quindi ottenere un migliore utilizzo della 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")

Porta al seguente confronto:

inserisci qui la descrizione dell'immagine

Non posso spiegare tutto nella trama sopra: all'inizio possiamo vedere un overhead più grande per la libreria numexpr, ma poiché utilizza meglio la cache è circa 10 volte più veloce per array più grandi!


Un altro approccio è compilare jit la funzione e ottenere così un vero UFunc in puro C. Questo è l'approccio di 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

È 10 volte più veloce dell'approccio intorpidito originale:

inserisci qui la descrizione dell'immagine


Tuttavia, il compito è parallelamente imbarazzante, quindi potremmo anche usarlo prangeper calcolare il ciclo in parallelo:

@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

Come previsto, la funzione parallela è più lenta per input più piccoli, ma più veloce (quasi fattore 2) per dimensioni più grandi:

inserisci qui la descrizione dell'immagine


Mentre numba è specializzata nell'ottimizzazione delle operazioni con array numpy, Cython è uno strumento più generale. È più complicato estrarre le stesse prestazioni di numba - spesso dipende da compilatore locale (gcc / MSVC) vs llvm (numba):

%%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 comporta funzioni leggermente più lente:

inserisci qui la descrizione dell'immagine


Conclusione

Ovviamente, testare solo per una funzione non dimostra nulla. Si dovrebbe anche tenere presente che, per l'esempio di funzione scelto, la larghezza di banda della memoria era il collo di bottiglia per dimensioni superiori a 10 ^ 5 elementi - quindi abbiamo avuto le stesse prestazioni per numba, numexpr e cython in questa regione.

Alla fine, la risposta definitiva dipende dal tipo di funzione, hardware, distribuzione Python e altri fattori. Per esempio Anaconda-distribuzione utilizza VML di Intel per le funzioni di NumPy e quindi Sorpassa numba (a meno che non usa SVML, vedere questo SO-post ) facilmente per funzioni trascendenti piace exp, sin, cose simili - si veda ad esempio il seguente SO-post .

Tuttavia, da questa indagine e dalla mia esperienza finora, direi che numba sembra essere lo strumento più semplice con le migliori prestazioni purché non siano coinvolte funzioni trascendentali.


Tracciare i tempi di esecuzione con 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 può fare uso di Intel SVML di solito, il che si traduce in tempi abbastanza comparabili rispetto a Intel VML, ma l'implementazione è un po 'buggy nella versione (0.43-0.47). Ho aggiunto un complotto prestazioni stackoverflow.com/a/56939240/4045774 per comparsion al vostro cy_expsum.
max9111

29
squares = squarer(x)

Le operazioni aritmetiche sugli array vengono applicate automaticamente in modo elementale, con efficienti loop di livello C che evitano tutto l'overhead dell'interprete che si applicherebbe a un loop o comprensione di livello Python.

La maggior parte delle funzioni che si desidera applicare a un array NumPy elementally funzionerà semplicemente, anche se alcune potrebbero aver bisogno di modifiche. Ad esempio, ifnon funziona in modo elementare. Vorresti convertirli per usare costrutti come numpy.where:

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

diventa

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

9

In molti casi, numpy.apply_along_axis sarà la scelta migliore. Aumenta le prestazioni di circa 100x rispetto agli altri approcci - e non solo per banali funzioni di test, ma anche per composizioni di funzioni più complesse da intorpidimento e scipy.

Quando aggiungo il metodo:

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

al codice perfplot, ottengo i seguenti risultati: inserisci qui la descrizione dell'immagine


Sono estremamente scioccato dal fatto che la maggior parte delle persone non sembra essere a conoscenza di questo gioco semplice, scalabile e integrato per così tanti anni ....
Bill Huang

8

Credo nella versione più recente (io uso 1.13) di numpy puoi semplicemente chiamare la funzione passando l'array numpy al fuction che hai scritto per il tipo scalare, applicherà automaticamente la chiamata di funzione a ciascun elemento sull'array numpy e ti restituirà un altro array intorpidito

>>> 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
Questa non è una novità remota - è sempre stato il caso - è una delle caratteristiche principali di numpy.
Eric,

8
È l' **operatore che applica il calcolo a ciascun elemento t di t. È normale insensibile. Avvolgerlo nel lambdanon fa nulla in più.
hpaulj,

Questo non funziona con le istruzioni if ​​come è attualmente mostrato.
TriHard8

7

Sembra che nessuno abbia menzionato un metodo di produzione ufuncintegrato per produrre in pacchetti intorpiditi: np.frompyfuncche ho testato di nuovo np.vectorizee lo ho superato di circa il 20 ~ 30%. Ovviamente funzionerà bene come prescritto dal codice C o anche numba(che non ho testato), ma può un'alternativa migliore dinp.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

Ho anche testato campioni più grandi e il miglioramento è proporzionale. Vedi la documentazione anche qui


1
Ho ripetuto i suddetti test di cronometraggio e ho anche riscontrato un miglioramento delle prestazioni (oltre np.vectorize) di circa il 30%
Julian - BrainAnnex.org il

2

Come menzionato in questo post , usa le espressioni del generatore in questo modo:

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

2

Tutte le risposte di cui sopra si confrontano bene, ma se è necessario utilizzare la funzione personalizzata per la mappatura, e si ha numpy.ndarray, e è necessario mantenere la forma dell'array.

Ne ho confrontati solo due, ma manterrà la forma di ndarray. Ho usato l'array con 1 milione di voci per il confronto. Qui uso la funzione quadrata, anch'essa incorporata in numpy e che ha un grande aumento delle prestazioni, dato che in base alle necessità c'era qualcosa, puoi usare la funzione che preferisci.

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)

Produzione

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

qui puoi vedere chiaramente le numpy.fromiteropere fantastiche considerando un approccio semplice, e se la funzione integrata è disponibile ti preghiamo di usarla.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.