it-swarm.com.de

Effizienteste Möglichkeit, die Funktion über ein numpy Array abzubilden

Was ist der effizienteste Weg, um eine Funktion über ein Numpy-Array abzubilden? So habe ich es in meinem aktuellen Projekt 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 sehr ineffizient zu sein, da ich ein Listenverständnis verwende, um das neue Array als Python -Liste zu konstruieren, bevor ich es zurück in ein Numpy-Array konvertiere.

Können wir es besser machen?

228
Ryan

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

Nachricht 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 Beispiel x**2 im ursprünglichen Beitrag), verwenden Sie diese Funktion viel schneller als alles andere (beachte die Log-Skala):

enter image description here

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

enter image description here


Code zur Reproduktion der Darstellungen:

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

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

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

107
satomacoto

TL; DR

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

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

Vermeiden Sie im Allgemeinen np.vectorize, da es keine gute Leistung erbringt 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

In den folgenden einfachen Tests werden drei Methoden zum Zuordnen einer Funktion verglichen. In diesem Beispiel wird mit Python 3.6 und NumPy 1.15.4 gearbeitet. Erstens, 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 (vom schnellsten zum langsamsten sortiert):

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.

56
Mike T

Seitdem diese Frage beantwortet wurde, ist viel passiert - es gibt numexpr , numba und cython . Ziel dieser Antwort ist es, diese Möglichkeiten zu berücksichtigen.

Aber lassen Sie uns zuerst das Offensichtliche festhalten: Egal wie Sie eine Python-Funktion auf ein Numpy-Array abbilden, es bleibt eine Python -Funktion, das bedeutet für jede Auswertung:

  • das Element numpy-array muss in ein Python-Objekt konvertiert werden (z. B. ein Float).
  • alle Berechnungen werden mit Python-Objekten durchgeführt, was bedeutet, den Overhead von Interpreter, dynamischem Versand und unveränderlichen Objekten zu haben.

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

enter image description here

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

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

Die vektorisierte Numpy-Version benötigt viel zusätzlichen Speicher und Speicherzugriffe. Die Numexp-Bibliothek versucht die Numpy-Arrays zu kacheln und somit 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:

enter image description here

Ich kann nicht alles in der obigen Grafik erklären: Wir können am Anfang einen höheren Overhead für die numexpr-Bibliothek sehen, aber da der Cache besser genutzt wird, ist er für größere Arrays ungefähr 10-mal schneller!


Ein anderer Ansatz besteht darin, die Funktion zu kompilieren und so eine echte C-UFunc 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 10-mal schneller als der ursprüngliche Numpy-Ansatz:

enter image description here


Die Aufgabe ist jedoch peinlich parallelisierbar, daher könnten wir auch prange verwenden, 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):

enter image description here


Während sich numba auf die Optimierung von Operationen mit Numpy-Arrays spezialisiert hat, ist Cython ein allgemeineres Werkzeug. Es ist komplizierter, dieselbe Leistung wie mit numba zu extrahieren - oft liegt es an llvm (numba) im Vergleich zum 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:

enter image description here


Fazit

Es ist offensichtlich, dass das Testen nur für eine Funktion nichts beweist. Man sollte auch bedenken, dass für das ausgewä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.

Aufgrund dieser Untersuchung und meiner bisherigen Erfahrung würde ich jedoch feststellen, dass Numba das einfachste Werkzeug mit der besten Leistung zu sein scheint.


Zeichnen von Laufzeiten mit 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)
_

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 anfällt.

Die meisten Funktionen, die Sie elementweise auf ein NumPy-Array anwenden möchten, funktionieren nur, obwohl für einige möglicherweise Änderungen erforderlich sind. Zum Beispiel funktioniert if nicht elementweise. Sie möchten diese konvertieren, um Konstrukte wie numpy.where zu verwenden:

_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)
_
22
user2357112

Ich glaube an eine neuere Version (ich verwende 1.13) von NumPy. Sie können die Funktion einfach aufrufen, indem Sie das NumPy-Array an die Funktion übergeben, die Sie für den Skalartyp geschrieben haben. Der Funktionsaufruf wird automatisch auf jedes Element über dem NumPy-Array angewendet und Sie werden zurückgegeben ein anderes 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])
9
Peiti Li

Es scheint, als hätte niemand eine eingebaute Fabrikmethode zur Herstellung von ufunc in einer numpy-Verpackung erwähnt: np.frompyfunc, die ich erneut getestet habe np.vectorize und um etwa 20-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 als 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

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

2
Wunderbar

Wie in dieser Beitrag erwähnt, benutze einfach Generator-Ausdrücke wie folgt:

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