Dlaczego model Keras przewiduje wolniejsze po kompilacji?

23

keras prędkości prognozy

Teoretycznie przewidywanie powinno być stałe, ponieważ odważniki mają ustalony rozmiar. Jak odzyskać prędkość po kompilacji (bez konieczności usuwania optymalizatora)?

Zobacz powiązany eksperyment: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true

off99555
źródło
Myślę, że musisz dopasować model po kompilacji, a następnie użyć wyszkolonego modelu do przewidywania. Zobacz tutaj
naiwny
@naive Fitting nie ma znaczenia dla problemu. Jeśli wiesz, jak naprawdę działa sieć, byłbyś ciekawy, dlaczego prognoza jest wolniejsza. Podczas prognozowania do mnożenia macierzy używane są tylko wagi, a wagi muszą być ustalone przed i po kompilacji, więc czas prognozowania powinien pozostać stały.
off99555,
Wiem, że to nie ma znaczenia dla tego problemu . I nie trzeba wiedzieć, jak działa sieć, aby wskazać, że zadania, które wymyśliłeś, i porównywanie dokładności są w rzeczywistości bez znaczenia. Bez dopasowania modelu do niektórych danych, które przewidujesz i faktycznie porównujesz czas. To nie są zwykłe lub właściwe przypadki użycia sieci neuronowej
naiwny
3
@naive Problem dotyczy zrozumienia wydajności modelu skompilowanego a nieskompilowanego, nie mającego nic wspólnego z dokładnością ani projektem modelu. Jest to uzasadniony problem, który może kosztować użytkowników TF - ja nie miałem pojęcia o tym, dopóki nie natknąłem się na to pytanie.
OverLordGoldDragon
1
@naive Nie możesz fitbez compile; Optymalizator nawet nie istnieje, aby zaktualizować wagi. predict można używać bez fitlub compilezgodnie z opisem w mojej odpowiedzi, ale różnica w wydajności nie powinna być tak dramatyczna - stąd problem.
OverLordGoldDragon

Odpowiedzi:

22

AKTUALIZACJA - 15.01.2020 : prąd najlepsze praktyki dla małych seriach powinno karmić wejść do modelu bezpośrednio - tj preds = model(x), a jeśli warstwy zachowują się inaczej w pociągu / wnioskowania model(x, training=False). Według ostatniego zatwierdzenia jest to teraz udokumentowane .

Nie przeprowadziłem testów porównawczych, ale na dyskusję na Git warto też spróbować predict_on_batch()- zwłaszcza z ulepszeniami w TF 2.1.


ULTIMATE winowajcą : self._experimental_run_tf_function = True. To jest eksperymentalne . Ale tak naprawdę nie jest źle.

Do każdego czytnika TensorFlow: wyczyść swój kod . To bałagan. I narusza ważne praktyki kodowania, takie jak jedna funkcja robi jedną rzecz ; _process_inputsrobi o wiele więcej niż „dane wejściowe procesu”, to samo dla _standardize_user_data. „Ja nie zapłacił za mało” - ale zrobić wynagrodzenia, w doliczonym czasie spędzonym zrozumienia własne rzeczy, aw użytkowników wypełniających swoją stronę problemy z błędami łatwiej rozwiązane z jaśniejszym kodu.


PODSUMOWANIE : jest tylko trochę wolniejszy compile().

compile()ustawia flagę wewnętrzną, która przypisuje inną funkcję predykcji predict. Ta funkcja tworzy nowy wykres przy każdym wywołaniu, spowalniając go względem nieskompilowanego. Różnica jest jednak wyraźna tylko wtedy, gdy czas pociągu jest znacznie krótszy niż czas przetwarzania danych . Jeśli zwiększymy rozmiar modelu do co najmniej średniej, oba staną się równe. Zobacz kod na dole.

Ten niewielki wzrost czasu przetwarzania danych jest z nadwyżką kompensowany przez zwiększoną wydajność wykresu. Ponieważ utrzymywanie tylko jednego wykresu modelu jest bardziej wydajne, jeden przed kompilacją jest odrzucany. Niemniej jednak : jeśli twój model jest mały w stosunku do danych, lepiej jest bez compile()wnioskowania o model. Zobacz moją drugą odpowiedź w celu obejścia tego problemu.


CO POWINIENEM ZROBIĆ?

Porównaj wydajność modelu skompilowaną z nieskompilowaną, jak mam w kodzie na dole.

  • Kompilacja jest szybsza : uruchom predictna skompilowanym modelu.
  • Kompilacja jest wolniejsza : uruchom predictna nieskompilowanym modelu.

Tak, oba są możliwe i będą zależeć od (1) wielkości danych; (2) rozmiar modelu; (3) sprzęt. Kod u dołu faktycznie pokazuje, że skompilowany model jest szybszy, ale 10 iteracji to mała próbka. Zobacz „obejścia” w mojej innej odpowiedzi na „instrukcje”.


SZCZEGÓŁY :

Debugowanie zajęło trochę czasu, ale było zabawne. Poniżej opisuję kluczowych sprawców, których odkryłem, przytaczam odpowiednią dokumentację i pokazuję wyniki profilowania, które doprowadziły do ​​ostatecznego wąskiego gardła.

( FLAG == self.experimental_run_tf_functiondla zwięzłości)

  1. Modeldomyślnie tworzy się z FLAG=False. compile()ustawia to True.
  2. predict() obejmuje nabycie funkcji przewidywania, func = self._select_training_loop(x)
  3. Bez żadnych specjalnych kwargów przekazywanych do predicti compile, wszystkie inne flagi są takie, że:
    • (A) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. Od kodu źródłowego docstring , (A) jest silnie zależny wykres, wykorzystuje bardziej strategii dystrybucyjnej i PO są podatne na tworzenie i niszczenie elementów wykresu, „może” (nie) udarność.

Prawdziwy winowajca : _process_inputs(), co stanowi 81% starcie . Jego główny składnik? _create_graph_function(), 72% czasu działania . Ta metoda nawet nie istnieje dla (B) . Jednak użycie modelu średniej wielkości _process_inputsstanowi mniej niż 1% czasu działania . Kod u dołu i wyniki profilowania.


PROCESORZY DANYCH :

(A) :, <class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'>używane w _process_inputs(). Odpowiedni kod źródłowy

(B) : numpy.ndarray, zwrócony przez convert_eager_tensors_to_numpy. Odpowiedni kod źródłowy i tutaj


FUNKCJA WYKONANIA MODELU (np. Przewidywanie)

(A) : funkcja dystrybucji i tutaj

(B) : funkcja dystrybucji (inna) i tutaj


PROFILER : wyniki dla kodu w mojej drugiej odpowiedzi „malutki model”, a w tej odpowiedzi „średni model”:

Mały model : 1000 iteracji,compile()

Mały model : 1000 iteracji, nie compile()

Model średni : 10 iteracji


DOKUMENTACJA (pośrednio) na temat wpływu compile(): źródła

W przeciwieństwie do innych operacji TensorFlow, nie przekształcamy danych liczbowych pytona na tensory. Ponadto generowany jest nowy wykres dla każdej odrębnej wartości liczbowej pytona , na przykład wywołania g(2)i g(3)wygeneruje dwa nowe wykresy

function tworzy osobny wykres dla każdego unikalnego zestawu kształtów wejściowych i typów danych . Na przykład następujący fragment kodu spowoduje prześledzenie trzech różnych wykresów, ponieważ każde wejście ma inny kształt

Pojedynczy obiekt tf.function może wymagać odwzorowania na wiele wykresów obliczeniowych pod maską. Powinno to być widoczne tylko jako wydajność (wykresy śledzenia mają niezerowe koszty obliczeniowe i pamięci ), ale nie powinny wpływać na poprawność programu


PRZYKŁAD :

from tensorflow.keras.layers import Input, Dense, LSTM, Bidirectional, Conv1D
from tensorflow.keras.layers import Flatten, Dropout
from tensorflow.keras.models import Model
import numpy as np
from time import time

def timeit(func, arg, iterations):
    t0 = time()
    for _ in range(iterations):
        func(arg)
    print("%.4f sec" % (time() - t0))

batch_size = 32
batch_shape = (batch_size, 400, 16)
ipt   = Input(batch_shape=batch_shape)
x     = Bidirectional(LSTM(512, activation='relu', return_sequences=True))(ipt)
x     = LSTM(512, activation='relu', return_sequences=True)(ipt)
x     = Conv1D(128, 400, 1, padding='same')(x)
x     = Flatten()(x)
x     = Dense(256, activation='relu')(x)
x     = Dropout(0.5)(x)
x     = Dense(128, activation='relu')(x)
x     = Dense(64,  activation='relu')(x)
out   = Dense(1,  activation='sigmoid')(x)
model = Model(ipt, out)

X = np.random.randn(*batch_shape)
timeit(model.predict, X, 10)
model.compile('adam', loss='binary_crossentropy')
timeit(model.predict, X, 10)

Wyjścia :

34.8542 sec
34.7435 sec
OverLordGoldDragon
źródło
1
Jaki jest wniosek na temat tego, co powinniśmy zrobić, aby uzyskać najszybszą prędkość prognozowania dla dowolnego rozmiaru modelu? Czy to po prostu nie robić compile()?
off99555,
3
@ off99555 „dla dowolnego rozmiaru modelu” - nie ma czegoś takiego. Przeczytaj całą odpowiedź - jeśli poświęcę kilka godzin na debugowanie, kilka minut od pytającego nie powinno być nierozsądne.
OverLordGoldDragon
Przeczytałem całość, ale trudno to zrozumieć, ponieważ to nie ja debugowałem kod. Musisz więc wyciągnąć wniosek, który nie obejmuje zmiennych pośrednich, które znajdziesz podczas fazy debugowania. Np. „Jeśli twój model jest mały, nie używaj kompilacji. Jeśli Twój model jest średniej wielkości, możesz użyć kompilacji.„ Coś w tym stylu.
off99555
1
@ off99555 Wystarczająco uczciwy; zaktualizowane. Nowa sekcja jest dość rozsądna, ale widzę, że nie jest od razu realizowana.
OverLordGoldDragon
1
@ off99555 Nie to, że testowałem, ale bardzo duże modele (ResNet itp.) mogą działać znacznie szybciej kompilowane, szczególnie. jeśli jest dystrybuowany na wielu urządzeniach - ponieważ (A) jest bardziej obciążony graficznie i dystrybucyjnie. Najpewniejszym testem jest test - jak w odpowiedzi. Nieznajomy z TF Lite, ale to osobne pytanie
OverLordGoldDragon
15

AKTUALIZACJA : zobacz rzeczywistą odpowiedź opublikowaną jako osobną odpowiedź; ten post zawiera dodatkowe informacje


.compile() ustawia większość wykresów TF / Keras, w tym straty, metryki, gradienty, a częściowo optymalizator i jego wagi - co gwarantuje zauważalne spowolnienie.

Co jest nieoczekiwana jest skala spowolnienia - 10-krotnie na moim własnym doświadczeniu, a predict(), który nie aktualizuje żadnych ciężarów. Patrząc na kod źródłowy TF2, elementy wykresu wydają się ściśle ze sobą powiązane, a zasoby niekoniecznie są przydzielane „sprawiedliwie”.

Możliwe przeoczenie przez programistów predictwydajności nieskompilowanego modelu, ponieważ modele są zwykle kompilowane - ale w praktyce jest to niedopuszczalna różnica. Możliwe też, że jest to „zło konieczne”, ponieważ istnieje proste obejście tego problemu (patrz poniżej).

To nie jest pełna odpowiedź i mam nadzieję, że ktoś może ją tutaj podać - jeśli nie, sugeruję otwarcie problemu z Github na TensorFlow. (OP ma; tutaj )


Obejście : wytrenuj model, zapisz jego wagi , odbuduj model bez kompilacji, załaduj wagi. Czy nie zapisać cały model (np model.save()), jak to będzie ładować skompilowany - zamiast używać model.save_weights()i model.load_weights().

Obejście 2 : powyżej, ale użyj load_model(path, compile=False); sugestia: D. Möller


UPDATE : wyjaśnienie, optymalizator jest nie w pełni tworzony z compiletym jego weightsand updatestensorów - odbywa się to po pierwsze wywołanie funkcji łącznika jest wykonana ( fit, train_on_batchitp), poprzez model._make_train_function().

Obserwowane zachowanie jest zatem jeszcze bardziej dziwne. Co gorsza, budowa optymalizator ma nie wywoływać jakiekolwiek dalsze spowolnienie (patrz niżej) - sugeruje „rozmiar Wykres” nie jest głównym wyjaśnienie tutaj.


EDYCJA : w niektórych modelach 30- krotne spowolnienie . TensorFlow, co zrobiłeś. Przykład poniżej:

from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model
import numpy as np
from time import time

def timeit(func, arg, iterations):
    t0 = time()
    for _ in range(iterations):
        func(arg)
    print("%.4f sec" % (time() - t0))

ipt   = Input(shape=(4,))
x     = Dense(2, activation='relu')(ipt)
out   = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)

X = np.random.randn(32,4)

timeit(model.predict, X, 1000)
model.compile('adam', loss='binary_crossentropy')
timeit(model.predict, X, 1000)
model._make_train_function()  # build optimizer
timeit(model.predict, X, 1000)

Wyjścia :

0.9891 sec
29.785 sec
29.521 sec
OverLordGoldDragon
źródło
1
To interesujące. Od dłuższego czasu chcę przetestować trening za pomocą statycznego wykresu w model.fit()porównaniu z dynamiczną pętlą z chętnym wykonywaniem, aby zobaczyć, czy utrata wydajności jest zbyt duża ...
Daniel Möller
1
W przeszłości mogłem zauważyć znaczną różnicę prędkości między Keras a PyTorch (jest PyTorch znacznie szybszy).
Daniel Möller,
1
Otworzyłem problem tutaj: github.com/tensorflow/tensorflow/issues/33340
off99555
2
Tak. Jest to zły wybór projektu, jeśli umieścisz kod związany ze szkoleniem w przewidywaniu. Ponieważ użytkownicy będą używać tej funkcji przewidywania sekwencyjnie wiele razy podczas produkcji. Powinien działać najszybciej, aby wywołać najmniejszą niespodziankę. W porównaniu do implementacji numpy wystarczy pomnożyć macierz, dodać odchylenie, aktywować, i to wszystko w przypadku gęstej warstwy. Nie ma potrzeby dotyczyć żadnej funkcji straty.
off99555,
1
Wskazówka, możesz użyć load_model(name, compile=False), jest to prostsze niż zapisywanie / ładowanie ciężarów i odtwarzanie modelu.
Daniel Möller,