Valutazione efficiente di una funzione in ogni cella di un array NumPy


124

Dato un array NumPy A , qual è il modo più veloce / più efficiente per applicare la stessa funzione, f , a ogni cella?

  1. Supponiamo che assegneremo ad A (i, j) la f (A (i, j)) .

  2. La funzione, f , non ha un output binario, quindi le operazioni di maschera (ing) non aiuteranno.

L'iterazione "ovvia" a doppio loop (attraverso ogni cella) è la soluzione ottimale?


Risposte:


165

Potresti semplicemente vettorializzare la funzione e quindi applicarla direttamente a un array Numpy ogni volta che ne hai bisogno:

import numpy as np

def f(x):
    return x * x + 3 * x - 2 if x > 0 else x * 5 + 8

f = np.vectorize(f)  # or use a different name if you want to keep the original f

result_array = f(A)  # if A is your Numpy array

Probabilmente è meglio specificare un tipo di output esplicito direttamente durante la vettorializzazione:

f = np.vectorize(f, otypes=[np.float])

19
Temo che la funzione vettorializzata non possa essere più veloce dell'iterazione "manuale" a doppio loop e dell'assegnazione attraverso tutti gli elementi dell'array. Soprattutto perché memorizza il risultato in una variabile appena creata (e non direttamente nell'input iniziale). Grazie mille per la risposta :)
Peter

1
@Peter: Ah, ora vedo che hai menzionato l'assegnazione del risultato all'array precedente nella domanda originale. Mi dispiace di averlo perso la prima volta che l'ho letto. Sì, in quel caso il doppio loop deve essere più veloce. Ma hai anche provato un singolo loop sulla vista appiattita dell'array? Potrebbe essere leggermente più veloce, poiché si risparmia un piccolo overhead del ciclo e Numpy deve fare una moltiplicazione e un'aggiunta in meno (per calcolare l'offset dei dati) ad ogni iterazione. Inoltre funziona per array di dimensioni arbitrarie. Potrebbe essere più lento su array molto piccoli, però.
Blubberdiblub,

45
Notare l'avvertimento fornito nella vectorizedescrizione della funzione: La funzione vectorize è fornita principalmente per comodità, non per prestazioni. L'implementazione è essenzialmente un ciclo for. Quindi molto probabilmente questo non accelererà affatto il processo.
Gabriel,

Prestare attenzione a come vectorizedetermina il tipo di reso. Ciò ha prodotto bug. frompyfuncè un po 'più veloce, ma restituisce un array di oggetti dtype. Entrambi i feed scalari, non righe o colonne.
hpaulj

1
@Gabriel L'avvio della np.vectorizemia funzione (che utilizza RK45) mi dà una velocità di un fattore di ~ 20.
Suuuehgi,



0

Credo di aver trovato una soluzione migliore. L'idea di cambiare la funzione in funzione universale di Python (vedi documentazione ), che può esercitare un calcolo parallelo sotto il cofano.

Si può scrivere il proprio personalizzato ufuncin C, che sicuramente è più efficiente, o invocando np.frompyfunc, che è il metodo di fabbrica incorporato. Dopo il test, questo è più efficiente di 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 f_arr(arr, arr) # 450ms

Ho anche testato campioni più grandi e il miglioramento è proporzionale. Per il confronto delle prestazioni di altri metodi, vedere questo post


0

Quando l'array 2d (o nd-array) è contiguo C o F, allora questo compito di mappare una funzione su un array 2d è praticamente lo stesso del compito di mappare una funzione su un array 1d: devono vederlo in questo modo, ad es np.ravel(A,'K'). via .

La possibile soluzione per 1d-array è stata discussa per esempio qui .

Tuttavia, quando la memoria del 2d-array non è contigua, la situazione è un po 'più complicata, perché si vorrebbe evitare possibili errori della cache se gli assi vengono gestiti in ordine errato.

Numpy dispone già di un macchinario per elaborare gli assi nel miglior ordine possibile. Una possibilità di usare questo macchinario è np.vectorize. Tuttavia, la documentazione di numpy np.vectorizeafferma che è "fornita principalmente per comodità, non per prestazioni" - una funzione Python lenta rimane una funzione Python lenta con l'intero sovraccarico associato! Un altro problema è il suo enorme consumo di memoria - vedi ad esempio questo post SO .

Quando si vuole avere una prestazione di una funzione C ma usare il macchinario di numpy, una buona soluzione è usare numba per la creazione di ufuncs, ad esempio:

# 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

Batte facilmente, np.vectorizema anche quando la stessa funzione sarebbe eseguita come moltiplicazione / addizione numpy-array, ad es

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

Vedi l'appendice di questa risposta per il codice di misurazione del tempo:

inserisci qui la descrizione dell'immagine

La versione di Numba (verde) è circa 100 volte più veloce della funzione python (cioè np.vectorize), il che non sorprende. Ma è anche circa 10 volte più veloce della funzionalità numpy, perché la versione numbas non ha bisogno di array intermedi e quindi utilizza la cache in modo più efficiente.


Mentre l'approccio ufunc di numba è un buon compromesso tra usabilità e prestazioni, non è ancora il massimo che possiamo fare. Eppure non esiste un proiettile d'argento o un approccio migliore per qualsiasi attività: bisogna capire quali sono i limiti e come possono essere mitigati.

Ad esempio, per le funzioni trascendentali (ad es exp. sin, cos) Numba non offre alcun vantaggio rispetto a numpy np.exp(non sono stati creati array temporanei - la fonte principale dell'accelerazione). Tuttavia, la mia installazione di Anaconda utilizza VML di Intel per vettori più grandi di 8192 - semplicemente non può farlo se la memoria non è contigua. Quindi potrebbe essere meglio copiare gli elementi in una memoria contigua per poter usare il VML di Intel:

import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp(x):
    return np.exp(x)

def np_copy_exp(x):
    copy = np.ravel(x, 'K')
    return np.exp(copy).reshape(x.shape) 

Per correttezza del confronto, ho disattivato la parallelizzazione di VML (vedi codice in appendice):

inserisci qui la descrizione dell'immagine

Come si può vedere, una volta attivato VML, l'overhead della copia è più che compensato. Tuttavia, una volta che i dati diventano troppo grandi per la cache L3, il vantaggio è minimo poiché l'attività diventa di nuovo legata alla larghezza di banda della memoria.

D'altra parte, numba potrebbe usare anche SVML di Intel, come spiegato in questo post :

from llvmlite import binding
# set before import
binding.set_option('SVML', '-vector-library=SVML')

import numba as nb

@nb.vectorize(target="cpu")
def nb_vexp_svml(x):
    return np.exp(x)

e usando VML con rese di parallelizzazione:

inserisci qui la descrizione dell'immagine

La versione di numba ha un sovraccarico minore, ma per alcune dimensioni VML batte SVML anche nonostante l'overhead di copiatura aggiuntivo - il che non è un po 'una sorpresa poiché gli ufunc di numba non sono parallelizzati.


Annunci:

A. confronto della funzione polinomiale:

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

B. confronto di exp:

import perfplot
import numexpr as ne # using ne is the easiest way to set vml_num_threads
ne.set_vml_num_threads(1)
perfplot.show(
    setup=lambda n: np.random.rand(n,n)[::2,::2],
    n_range=[2**k for k in range(0,12)],
    kernels=[
        nb_vexp, 
        np.exp,
        np_copy_exp,
        ],
    logx=True,
    logy=True,
    xlabel='len(x)',
    )

0

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. Sto presentando il caso generale per la matrice n dimensionale. Per due dimensioni, basta creare iter2D.

import numpy, time

def A(e):
    return e * e

def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([A(x) for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((A(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 numpy.fromiterla funzione quadrata dell'utente, usare una qualsiasi delle tue scelte. Se la tua funzione dipende da i, j che è indici di array, itera su dimensioni dell'array come for ind in range(arr.size), usa numpy.unravel_indexper ottenere i, j, ..basato sull'indice 1D e sulla forma dell'array numpy.unravel_index

Questa risposta è ispirata dalla mia risposta su un'altra domanda qui

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.