Die effizienteste Methode zum Zuordnen von Funktionen über ein numpy-Array


337

Was ist der effizienteste Weg, um eine Funktion über ein Numpy-Array abzubilden? Ich habe es in meinem aktuellen Projekt folgendermaßen gemacht:

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

Dies scheint jedoch wahrscheinlich sehr ineffizient zu sein, da ich ein Listenverständnis verwende, um das neue Array als Python-Liste zu erstellen, bevor ich es wieder in ein Numpy-Array konvertiere.

Können wir es besser machen?


10
warum nicht "Quadrate = x ** 2"? Haben Sie eine viel kompliziertere Funktion, die Sie bewerten müssen?
22 Grad

4
Wie wäre es nur squarer(x)?
Leben

1
Vielleicht beantwortet dies die Frage nicht direkt, aber ich habe gehört, dass numba vorhandenen Python-Code in parallele Maschinenanweisungen kompilieren kann. Ich werde diesen Beitrag erneut besuchen und überarbeiten, wenn ich tatsächlich die Möglichkeit habe, ihn zu nutzen.
30 友情 留 在 在 盐

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

Antworten:


280

Ich habe alle vorgeschlagenen Methoden plus np.array(map(f, x))mit perfplot(einem kleinen Projekt von mir) getestet .

Nachricht Nr. 1: Wenn Sie die nativen Funktionen von numpy verwenden können, tun Sie dies.

Wenn die Funktion, die Sie bereits vektorisieren möchten, vektorisiert ist (wie im x**2Beispiel im ursprünglichen Beitrag), ist die Verwendung viel schneller als alles andere (beachten Sie die Protokollskala):

Geben Sie hier die Bildbeschreibung ein

Wenn Sie tatsächlich eine Vektorisierung benötigen, spielt es keine Rolle, welche Variante Sie verwenden.

Geben Sie hier die Bildbeschreibung ein


Code zur Reproduktion der Diagramme:

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
Sie scheinen f(x)von Ihrer Verschwörung ausgelassen zu haben . Es ist möglicherweise nicht für jeden anwendbar f, aber es ist hier anwendbar und es ist leicht die schnellste Lösung, wenn zutreffend.
user2357112 unterstützt Monica

2
Außerdem unterstützt Ihre Handlung nicht Ihre Behauptung, die vf = np.vectorize(f); y = vf(x)für kurze Eingaben gewinnt.
user2357112 unterstützt Monica

Nach der Installation von perfplot (v0.3.2) über pip ( pip install -U perfplot) wird die folgende Meldung angezeigt : AttributeError: 'module' object has no attribute 'save'Beim Einfügen des Beispielcodes.
Tsherwen

Was ist mit einer Vanille für Schleife?
Catiger3331

1
@Vlad benutze einfach math.sqrt wie kommentiert.
Nico Schlömer

138

Wie wäre es mit 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
Das ist nicht effizienter.
user2357112 unterstützt Monica

78
Aus diesem Dokument: The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. In anderen Fragen stellte ich fest, dass dies vectorizedie Iterationsgeschwindigkeit des Benutzers verdoppeln könnte. Die eigentliche Beschleunigung liegt jedoch bei echten numpyArray-Operationen.
hpaulj

2
Beachten Sie, dass Vectorize zumindest dazu führt, dass Dinge für Nicht-1d-Arrays funktionieren
Eric

Aber squarer(x)würde schon arbeiten für Nicht-1d - Arrays. vectorizehat nur wirklich einen Vorteil gegenüber einem Listenverständnis (wie dem in der Frage), nicht über squarer(x).
user2357112 unterstützt Monica

79

TL; DR

Wie von @ user2357112 angegeben , ist eine "direkte" Methode zum Anwenden der Funktion immer die schnellste und einfachste Möglichkeit, eine Funktion über Numpy-Arrays abzubilden:

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

Vermeiden np.vectorizeSie im Allgemeinen , da es nicht gut funktioniert und eine Reihe von Problemen aufweist (oder hatte) . Wenn Sie andere Datentypen verarbeiten, möchten Sie möglicherweise die anderen unten gezeigten Methoden untersuchen.

Methodenvergleich

Im Folgenden finden Sie einige einfache Tests zum Vergleichen von drei Methoden zum Zuordnen einer Funktion. Dieses Beispiel wird mit Python 3.6 und NumPy 1.15.4 verwendet. Zunächst die Setup-Funktionen zum 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 mit fünf Elementen (sortiert vom schnellsten zum langsamsten):

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

Mit Hunderten von 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

Und mit Tausenden von Array-Elementen oder mehr:

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

Verschiedene Versionen von Python / NumPy und die Compiler-Optimierung führen zu unterschiedlichen Ergebnissen. Führen Sie daher einen ähnlichen Test für Ihre Umgebung durch.


2
Wenn Sie das countArgument und einen Generatorausdruck verwenden, np.fromiterist dies erheblich schneller.
juanpa.arrivillaga

3
Verwenden 'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'
Sie

4
Sie haben die direkte Lösung von nicht getestet f(x), die alles andere um eine Größenordnung übertrifft .
user2357112 unterstützt Monica

4
Was ist, wenn f2 Variablen vorhanden sind und das Array 2D ist?
Sigur

2
Ich bin verwirrt darüber, wie die 'f (x)' - Version ("direkt") tatsächlich als vergleichbar angesehen wird, als das OP fragte, wie eine Funktion über ein Array hinweg "abgebildet" werden soll. Im Fall von f (x) = x ** 2 wird das ** von numpy für das gesamte Array ausgeführt, nicht auf Elementbasis. Wenn zum Beispiel f (x) 'Lambda x: x + x "ist, ist die Antwort sehr unterschiedlich, da numpy die Arrays verkettet, anstatt pro Element zu addieren. Ist dies wirklich der beabsichtigte Vergleich? Bitte erklären Sie.
Andrew Mellinger

49

Es gibt Numexpr , Numba und Cython . Das Ziel dieser Antwort ist es, diese Möglichkeiten zu berücksichtigen.

Aber lassen Sie uns zuerst das Offensichtliche sagen: Egal wie Sie eine Python-Funktion einem Numpy-Array zuordnen, es bleibt eine Python-Funktion, das heißt für jede Auswertung:

  • Das numpy-Array-Element muss in ein Python-Objekt konvertiert werden (z Float. B. a ).
  • Alle Berechnungen werden mit Python-Objekten durchgeführt, was bedeutet, dass der Overhead von Interpreter, dynamischem Versand und unveränderlichen Objekten anfällt.

Welche Maschinerie verwendet wird, um das Array tatsächlich zu durchlaufen, spielt aufgrund des oben erwähnten Overheads keine große Rolle - sie bleibt viel langsamer als die Verwendung der integrierten Funktionalität von numpy.

Schauen wir uns das folgende Beispiel an:

# 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.vectorizewird als Vertreter der reinen Python-Funktionsklasse von Ansätzen ausgewählt. Mit perfplot(siehe Code im Anhang dieser Antwort) erhalten wir folgende Laufzeiten:

Geben Sie hier die Bildbeschreibung ein

Wir können sehen, dass der Numpy-Ansatz 10x-100x schneller ist als die reine Python-Version. Der Leistungsabfall bei größeren Array-Größen ist wahrscheinlich darauf zurückzuführen, dass Daten nicht mehr in den Cache passen.

Erwähnenswert ist auch, dass vectorizeauch viel Speicher benötigt wird, so dass die Speichernutzung häufig der Flaschenhals ist (siehe verwandte SO-Frage ). Beachten Sie auch, dass in der Dokumentation von numpy angegeben ist np.vectorize, dass es "in erster Linie der Einfachheit halber und nicht der Leistung dient".

Wenn Leistung gewünscht wird, sollten andere Tools verwendet werden. Neben dem Schreiben einer C-Erweiterung von Grund auf gibt es folgende Möglichkeiten:


Man hört oft, dass die Numpy-Performance so gut ist wie es nur geht, weil es reines C unter der Haube ist. Dennoch gibt es viel Raum für Verbesserungen!

Die vektorisierte Numpy-Version verwendet viel zusätzlichen Speicher und Speicherzugriffe. Die Numexp-Bibliothek versucht, die Numpy-Arrays zu kacheln und so eine bessere Cache-Auslastung zu erzielen:

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

Führt zu folgendem Vergleich:

Geben Sie hier die Bildbeschreibung ein

Ich kann nicht alles in der obigen Darstellung erklären: Wir können am Anfang einen größeren Overhead für die numexpr-Bibliothek sehen, aber da der Cache besser genutzt wird, ist er für größere Arrays etwa zehnmal schneller!


Ein anderer Ansatz besteht darin, die Funktion zu kompilieren und so einen echten UFunc mit reinem C zu erhalten. Dies ist Numbas Ansatz:

# 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

Es ist zehnmal schneller als der ursprüngliche Numpy-Ansatz:

Geben Sie hier die Bildbeschreibung ein


Die Aufgabe ist jedoch peinlich parallelisierbar, sodass wir sie auch verwenden könnten prange, um die Schleife parallel zu berechnen:

@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

Wie erwartet ist die Parallelfunktion bei kleineren Eingängen langsamer, bei größeren jedoch schneller (fast Faktor 2):

Geben Sie hier die Bildbeschreibung ein


Während sich numba auf die Optimierung von Operationen mit numpy-Arrays spezialisiert hat, ist Cython ein allgemeineres Werkzeug. Es ist komplizierter, die gleiche Leistung wie bei numba zu extrahieren - oft liegt es an llvm (numba) gegenüber dem lokalen 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 führt zu etwas langsameren Funktionen:

Geben Sie hier die Bildbeschreibung ein


Fazit

Offensichtlich beweist das Testen nur für eine Funktion nichts. Man sollte auch bedenken, dass für das gewählte Funktionsbeispiel die Bandbreite des Speichers der Flaschenhals für Größen größer als 10 ^ 5 Elemente war - daher hatten wir in dieser Region die gleiche Leistung für numba, numexpr und cython.

Letztendlich hängt die endgültige Antwort von der Art der Funktion, der Hardware, der Python-Verteilung und anderen Faktoren ab. Zum Beispiel Anaconda-Distribution verwendet Intels VML für Funktionen numpy ist und damit übertrifft numba (es sei denn , es SVML verwendet, finden Sie diese SO-post ) leicht für transzendente Funktionen wie exp, sin, cosund ähnlich - siehe zum Beispiel die folgenden SO-Post .

Aufgrund dieser Untersuchung und meiner bisherigen Erfahrungen würde ich jedoch feststellen, dass Numba das einfachste Werkzeug mit der besten Leistung zu sein scheint, solange keine transzendentalen Funktionen beteiligt sind.


Laufzeit mit Perfplot-Paket zeichnen :

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 kann normalerweise Intel SVML verwenden, was zu vergleichbaren Timings im Vergleich zu Intel VML führt, aber die Implementierung ist in der Version (0.43-0.47) etwas fehlerhaft. Ich habe ein Leistungsdiagramm stackoverflow.com/a/56939240/4045774 zum Vergleich zu Ihrem cy_expsum hinzugefügt.
max9111

29
squares = squarer(x)

Arithmetische Operationen auf Arrays werden automatisch elementweise angewendet, mit effizienten Schleifen auf C-Ebene, die den gesamten Interpreter-Overhead vermeiden, der für eine Schleife oder ein Verständnis auf Python-Ebene gelten würde.

Die meisten Funktionen, die Sie elementweise auf ein NumPy-Array anwenden möchten, funktionieren nur, einige müssen jedoch möglicherweise geändert werden. Funktioniert beispielsweise ifnicht elementweise. Sie möchten diese konvertieren, um Konstrukte wie die folgenden zu verwenden numpy.where:

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

wird

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

8

Ich glaube, in einer neueren Version (ich verwende 1.13) von numpy können Sie die Funktion einfach aufrufen, indem Sie das numpy-Array an die Funktion übergeben, die Sie für den Skalartyp geschrieben haben. Es wendet den Funktionsaufruf automatisch auf jedes Element über das numpy-Array an und gibt Sie zurück ein weiteres numpy Array

>>> 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
Dies ist nicht im entferntesten neu - das war schon immer so - es ist eines der Hauptmerkmale von numpy.
Eric

8
Es ist der **Operator, der die Berechnung auf jedes Element t von anwendet t. Das ist gewöhnliche Numpy. Das Einwickeln in lambdamacht nichts extra.
hpaulj

Dies funktioniert nicht mit if-Anweisungen, wie sie derzeit angezeigt werden.
TriHard8

8

In vielen Fällen ist numpy.apply_along_axis die beste Wahl. Es erhöht die Leistung um etwa das 100-fache im Vergleich zu den anderen Ansätzen - und zwar nicht nur für triviale Testfunktionen, sondern auch für komplexere Funktionszusammensetzungen aus Numpy und Scipy.

Wenn ich die Methode hinzufüge:

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

Zum Perfplot-Code erhalte ich folgende Ergebnisse: Geben Sie hier die Bildbeschreibung ein


Hervorragender Trick!
Felipe SS Schneider

Ich bin äußerst schockiert über die Tatsache, dass die meisten Menschen sich dieses einfachen, skalierbaren und eingebauten Kinderspiels seit so vielen Jahren nicht mehr bewusst zu sein scheinen ...
Bill Huang

7

Es scheint, dass niemand eine eingebaute Fabrikmethode zur Herstellung ufuncin Numpy-Verpackungen erwähnt hat, np.frompyfuncdie ich erneut getestet np.vectorizeund um etwa 20 bis 30% übertroffen habe. Natürlich funktioniert es gut wie vorgeschriebener C-Code oder sogar numba(was ich nicht getestet habe), aber es kann eine bessere Alternative sein alsnp.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

Ich habe auch größere Proben getestet und die Verbesserung ist proportional. Siehe auch die Dokumentation hier


1
Ich wiederholte die obigen Timing-Tests und fand auch eine Leistungsverbesserung (gegenüber np.vectorize) von etwa 30%
Julian - BrainAnnex.org

2

Wie in diesem Beitrag erwähnt , verwenden Sie einfach Generatorausdrücke wie folgt:

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

2

Alle obigen Antworten lassen sich gut vergleichen, aber wenn Sie eine benutzerdefinierte Funktion für die Zuordnung verwenden müssen und dies auch tun, müssen numpy.ndarraySie die Form des Arrays beibehalten.

Ich habe nur zwei verglichen, aber es wird die Form von behalten ndarray. Ich habe das Array mit 1 Million Einträgen zum Vergleich verwendet. Hier verwende ich die quadratische Funktion, die ebenfalls in Numpy eingebaut ist und eine große Leistungssteigerung aufweist, da Sie, da etwas benötigt wurde, eine Funktion Ihrer Wahl verwenden können.

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)

Ausgabe

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

Hier können Sie deutlich sehen numpy.fromiter, dass die Arbeit in Anbetracht des einfachen Ansatzes großartig ist. Wenn eine eingebaute Funktion verfügbar ist, verwenden Sie diese bitte.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.