Manchmal müssen Sie nicht-idiomatischen Numpy-Code schreiben, wenn Sie Ihre Berechnung wirklich beschleunigen möchten, was mit nativem Numpy nicht möglich ist.
numba
Kompiliert Ihren Python-Code auf Low-Level C. Da viele Numpys selbst normalerweise so schnell wie C sind, ist dies meistens nützlich, wenn sich Ihr Problem nicht für eine native Vektorisierung mit Numpys eignet. Dies ist ein Beispiel (bei dem ich angenommen habe, dass die Indizes zusammenhängend und sortiert sind, was sich auch in den Beispieldaten widerspiegelt):
import numpy as np
import numba
# use the inflated example of roganjosh https://stackoverflow.com/a/58788534
data = [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0, 0, 1, 1, 1, 1, 2, 3, 3]
data = np.array(data * 500) # using arrays is important for numba!
index = np.sort(np.random.randint(0, 30, 4500))
# jit-decorate; original is available as .py_func attribute
@numba.njit('f8[:](f8[:], i8[:])') # explicit signature implies ahead-of-time compile
def diffmedian_jit(data, index):
res = np.empty_like(data)
i_start = 0
for i in range(1, index.size):
if index[i] == index[i_start]:
continue
# here: i is the first _next_ index
inds = slice(i_start, i) # i_start:i slice
res[inds] = data[inds] - np.median(data[inds])
i_start = i
# also fix last label
res[i_start:] = data[i_start:] - np.median(data[i_start:])
return res
Und hier sind einige Timings mit IPythons %timeit
Magie:
>>> %timeit diffmedian_jit.py_func(data, index) # non-jitted function
... %timeit diffmedian_jit(data, index) # jitted function
...
4.27 ms ± 109 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
65.2 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Unter Verwendung der aktualisierten Beispieldaten in der Frage sind diese Zahlen (dh die Laufzeit der Python-Funktion im Vergleich zur Laufzeit der JIT-beschleunigten Funktion)
>>> %timeit diffmedian_jit.py_func(data, groups)
... %timeit diffmedian_jit(data, groups)
2.45 s ± 34.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
93.6 ms ± 518 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Dies entspricht einer 65-fachen Beschleunigung im kleineren Fall und einer 26-fachen Beschleunigung im größeren Fall (natürlich im Vergleich zu langsamem Schleifencode) unter Verwendung des beschleunigten Codes. Ein weiterer Vorteil ist, dass wir (im Gegensatz zur typischen Vektorisierung mit nativem Numpy) keinen zusätzlichen Speicher benötigten, um diese Geschwindigkeit zu erreichen. Es geht um optimierten und kompilierten Low-Level-Code, der letztendlich ausgeführt wird.
Bei der obigen Funktion wird davon ausgegangen, dass numpy int-Arrays int64
standardmäßig verwendet werden, was unter Windows nicht der Fall ist. Eine Alternative besteht darin, die Signatur aus dem Aufruf von zu entfernen numba.njit
und eine ordnungsgemäße Just-in-Time-Kompilierung auszulösen. Dies bedeutet jedoch, dass die Funktion während der ersten Ausführung kompiliert wird, was sich in Timing-Ergebnisse einmischen kann (wir können die Funktion entweder einmal manuell unter Verwendung repräsentativer Datentypen ausführen oder einfach akzeptieren, dass die erste Timing-Ausführung viel langsamer sein wird, was sollte ignoriert werden). Dies ist genau das, was ich versucht habe, indem ich eine Signatur angegeben habe, die eine vorzeitige Kompilierung auslöst.
Wie auch immer, im richtigen JIT-Fall ist der Dekorateur, den wir brauchen, einfach
@numba.njit
def diffmedian_jit(...):
Beachten Sie, dass die obigen Timings, die ich für die jit-kompilierte Funktion gezeigt habe, erst gelten, wenn die Funktion kompiliert wurde. Dies geschieht entweder bei der Definition (bei eifriger Kompilierung, wenn eine explizite Signatur übergeben wird numba.njit
) oder beim ersten Funktionsaufruf (bei verzögerter Kompilierung, wenn keine Signatur übergeben wird numba.njit
). Wenn die Funktion nur einmal ausgeführt werden soll, sollte die Kompilierungszeit auch für die Geschwindigkeit dieser Methode berücksichtigt werden. Das Kompilieren von Funktionen lohnt sich normalerweise nur, wenn die Gesamtzeit für Kompilierung und Ausführung kürzer ist als die nicht kompilierte Laufzeit (was im obigen Fall tatsächlich der Fall ist, wenn die native Python-Funktion sehr langsam ist). Dies geschieht meistens, wenn Sie Ihre kompilierte Funktion häufig aufrufen.
Wie max9111 in einem Kommentar feststellte, numba
ist das cache
Schlüsselwort to ein wichtiges Merkmal von jit
. Wenn Sie cache=True
an übergeben, numba.jit
wird die kompilierte Funktion auf der Festplatte gespeichert, sodass die Funktion bei der nächsten Ausführung des angegebenen Python-Moduls von dort geladen und nicht neu kompiliert wird, was Ihnen auf lange Sicht wiederum Laufzeit ersparen kann.
scipy.ndimage.median
Vorschlag in der verknüpften Antwort zeitlich festgelegt ? Es scheint mir nicht, dass es eine gleiche Anzahl von Elementen pro Etikett benötigt. Oder habe ich etwas verpasst?