Czasami musisz napisać nieidiomatyczny kod numpy, jeśli naprawdę chcesz przyspieszyć swoje obliczenia, czego nie możesz zrobić z natywną numpy.
numba
kompiluje kod Pythona do niskiego poziomu C. Ponieważ sama liczba numpy jest zwykle tak szybka jak C, zwykle jest to przydatne, jeśli twój problem nie nadaje się do natywnej wektoryzacji za pomocą numpy. To jest jeden przykład (gdzie założyłem, że indeksy są ciągłe i posortowane, co znajduje również odzwierciedlenie w przykładowych danych):
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
A oto niektóre czasy z wykorzystaniem %timeit
magii IPython :
>>> %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)
Wykorzystując zaktualizowane przykładowe dane w pytaniu, te liczby (tj. Środowisko wykonawcze funkcji python vs. środowisko uruchomieniowe funkcji przyspieszonej przez JIT) są
>>> %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)
Odpowiada to przyspieszeniu 65-krotnemu w mniejszym przypadku i 26-krotnym przyspieszeniu w większym przypadku (oczywiście w porównaniu z kodem wolnej pętli) przy użyciu kodu przyspieszonego. Kolejną zaletą jest to, że (w przeciwieństwie do typowej wektoryzacji z natywną numpy) nie potrzebowaliśmy dodatkowej pamięci, aby osiągnąć tę prędkość, chodzi o zoptymalizowany i skompilowany kod niskiego poziomu, który ostatecznie jest uruchamiany.
Powyższa funkcja zakłada, że tablice int numpy są int64
domyślnie, co w rzeczywistości nie ma miejsca w systemie Windows. Alternatywą jest więc usunięcie podpisu z wywołania do numba.njit
, uruchamiając odpowiednią kompilację just-in-time. Oznacza to jednak, że funkcja zostanie skompilowana podczas pierwszego wykonania, co może wtrącać się w wyniki pomiaru czasu (możemy wykonać funkcję raz ręcznie, używając reprezentatywnych typów danych, lub po prostu zaakceptować, że pierwsze wykonanie pomiaru czasu będzie znacznie wolniejsze, co powinno być zignorowanym). Właśnie tego próbowałem zapobiec, określając podpis, który uruchamia kompilację z wyprzedzeniem.
W każdym razie, w przypadku właściwego JIT, dekorator, którego potrzebujemy, jest po prostu
@numba.njit
def diffmedian_jit(...):
Zauważ, że powyższe czasy, które pokazałem dla funkcji skompilowanej z użyciem Jit, mają zastosowanie tylko po skompilowaniu funkcji. Dzieje się tak w momencie definicji (przy gorącej kompilacji, gdy przekazywany jest wyraźny podpis numba.njit
) lub podczas pierwszego wywołania funkcji (przy leniwej kompilacji, gdy nie jest przekazywany żaden podpis numba.njit
). Jeśli funkcja ma zostać wykonana tylko raz, należy również wziąć pod uwagę czas kompilacji dla szybkości tej metody. Zazwyczaj warto kompilować funkcje tylko wtedy, gdy całkowity czas kompilacji + wykonania jest krótszy niż nieskompilowany środowisko wykonawcze (co w rzeczywistości jest prawdą w powyższym przypadku, gdy natywna funkcja python jest bardzo wolna). Dzieje się tak głównie wtedy, gdy wywołujesz skompilowaną funkcję wiele razy.
Jak zauważono w komentarzu max9111 , jedną ważną cechą numba
jest cache
słowo kluczowe to jit
. Przekazanie cache=True
do numba.jit
spowoduje zapisanie skompilowanej funkcji na dysku, dzięki czemu podczas następnego wykonania danego modułu Pythona funkcja zostanie stamtąd załadowana, a nie ponownie skompilowana, co w dłuższej perspektywie może zaoszczędzić ci czasu działania.
scipy.ndimage.median
sugestię w połączonej odpowiedzi? Nie wydaje mi się, że potrzebuje takiej samej liczby elementów na etykietę. A może coś przeoczyłem?