Warum sagt das Keras-Modell nach dem Kompilieren langsamer voraus?

23

Vorhersage Geschwindigkeit Keras

Theoretisch sollte die Vorhersage konstant sein, da die Gewichte eine feste Größe haben. Wie bekomme ich meine Geschwindigkeit nach dem Kompilieren zurück (ohne das Optimierungsprogramm entfernen zu müssen)?

Siehe zugehöriges Experiment: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true

off99555
quelle
Ich denke, Sie müssen das Modell nach der Kompilierung anpassen und dann das trainierte Modell zur Vorhersage verwenden. Siehe hier
naiv
@naive Fitting ist für das Problem irrelevant. Wenn Sie wissen, wie das Netzwerk tatsächlich funktioniert, sind Sie gespannt, warum die Vorhersage langsamer ist. Bei der Vorhersage werden nur die Gewichte für die Matrixmultiplikation verwendet, und die Gewichte müssen vor und nach dem Kompilieren festgelegt werden, damit die Vorhersagezeit konstant bleibt.
off99555
Ich weiß, dass das für das Thema irrelevant ist . Und man muss nicht wissen, wie das Netzwerk funktioniert, um darauf hinzuweisen, dass die Aufgaben, die Sie sich gestellt haben und für die Sie die Genauigkeit vergleichen, tatsächlich bedeutungslos sind. Ohne das Modell über einige Daten anzupassen, die Sie vorhersagen, und Sie vergleichen tatsächlich die benötigte Zeit. Dies ist nicht der übliche oder richtige Anwendungsfall für ein neuronales Netzwerk
naiv
3
@naive Das Problem betrifft das Verständnis der kompilierten und nicht kompilierten Modellleistung, was nichts mit Genauigkeit oder Modelldesign zu tun hat. Es ist ein legitimes Problem, das TF-Benutzer kosten kann - ich jedenfalls hatte keine Ahnung davon, bis ich über diese Frage stolperte.
OverLordGoldDragon
1
@naive Du kannst nicht fitohne compile; Der Optimierer existiert nicht einmal, um Gewichte zu aktualisieren. predict kann ohne fitoder compilewie in meiner Antwort beschrieben verwendet werden, aber der Leistungsunterschied sollte nicht so dramatisch sein - daher das Problem.
OverLordGoldDragon

Antworten:

22

UPDATE - 15.01.2020 : Die derzeitige bewährte Methode für kleine Losgrößen sollte darin bestehen, Eingaben direkt in das Modell einzuspeisen - dh preds = model(x)und wenn sich Ebenen beim Zug / bei der Inferenz unterschiedlich verhalten model(x, training=False). Nach dem letzten Commit ist dies jetzt dokumentiert .

Ich habe diese nicht bewertet, aber laut Git-Diskussion lohnt es sich auch, es zu versuchen predict_on_batch()- insbesondere mit Verbesserungen in TF 2.1.


ULTIMATIVER SCHULD : self._experimental_run_tf_function = True. Es ist experimentell . Aber es ist nicht wirklich schlecht.

Für alle TensorFlow-Entwickler: Bereinigen Sie Ihren Code . Es ist ein Chaos. Und es verstößt gegen wichtige Codierungspraktiken, z. B. wenn eine Funktion eine Sache tut ; _process_inputsmacht viel mehr als "Prozesseingaben", das gleiche gilt für _standardize_user_data. "Ich werde nicht genug bezahlt" - aber Sie zahlen, wenn Sie mehr Zeit damit verbringen, Ihre eigenen Sachen zu verstehen, und wenn Benutzer Ihre Issues-Seite mit Fehlern füllen, die mit einem klareren Code leichter behoben werden können.


ZUSAMMENFASSUNG : Es ist nur ein bisschen langsamer mit compile().

compile()setzt ein internes Flag, das eine andere Vorhersagefunktion zuweist predict. Diese Funktion erstellt bei jedem Aufruf ein neues Diagramm und verlangsamt es im Vergleich zu nicht kompilierten. Der Unterschied ist jedoch nur dann ausgeprägt, wenn die Zugzeit viel kürzer als die Datenverarbeitungszeit ist . Wenn wir die Modellgröße auf mindestens mittelgroß erhöhen , werden beide gleich. Siehe Code unten.

Diese leichte Verlängerung der Datenverarbeitungszeit wird durch die verstärkte Grafikfähigkeit mehr als kompensiert. Da es effizienter ist, nur ein Modelldiagramm beizubehalten, wird das eine Vorkompilierungsprogramm verworfen. Nichtsdestotrotz : Wenn Ihr Modell im Verhältnis zu Daten klein ist, sind Sie ohne compile()Modellschluss besser dran . Siehe meine andere Antwort für eine Problemumgehung.


WAS SOLLTE ICH TUN?

Vergleichen Sie die kompilierte und die nicht kompilierte Modellleistung wie im Code unten.

  • Kompiliert ist schneller : predictAuf einem kompilierten Modell ausführen .
  • Kompiliert ist langsamer : predictAuf einem nicht kompilierten Modell ausführen .

Ja, beides ist möglich und hängt von (1) der Datengröße ab. (2) Modellgröße; (3) Hardware. Der Code unten zeigt tatsächlich, dass das kompilierte Modell schneller ist, aber 10 Iterationen sind ein kleines Beispiel. Siehe "Problemumgehungen" in meiner anderen Antwort für die "Anleitung".


DETAILS :

Das Debuggen dauerte eine Weile, hat aber Spaß gemacht. Im Folgenden beschreibe ich die Haupttäter, die ich entdeckt habe, zitiere einige relevante Dokumentationen und zeige Profiler-Ergebnisse, die zum endgültigen Engpass geführt haben.

(der FLAG == self.experimental_run_tf_functionKürze halber)

  1. ModelStandardmäßig instanziiert mit FLAG=False. compile()setzt es auf True.
  2. predict() beinhaltet den Erwerb der Vorhersagefunktion, func = self._select_training_loop(x)
  3. Ohne spezielle kwargs, die an predictund übergeben werden compile, sind alle anderen Flags so, dass:
    • (A) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. Ausgehend von der Quellcode-Dokumentzeichenfolge ist (A) stark grafisch abhängig, verwendet eine stärkere Verteilungsstrategie und Ops neigen dazu, Diagrammelemente zu erstellen und zu zerstören, was die Leistung "beeinträchtigen" kann.

Wahr Schuldige : _process_inputs()für Buchhaltung 81% der Laufzeit . Seine Hauptkomponente? _create_graph_function(), 72% der Laufzeit . Diese Methode existiert nicht einmal für (B) . Verwendung eines mittelgroßen Modell jedoch _process_inputsweist weniger als 1% der Laufzeit . Code unten und Profilerstellungsergebnisse folgen.


DATENVERARBEITER :

(A) : <class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'>verwendet in _process_inputs(). Relevanter Quellcode

(B) : numpy.ndarray, zurückgegeben von convert_eager_tensors_to_numpy. Relevanter Quellcode und hier


MODEL EXECUTION FUNCTION (zB vorhersagen)

(A) : Verteilungsfunktion und hier

(B) : Verteilungsfunktion (unterschiedlich) und hier


PROFILER : Ergebnisse für Code in meiner anderen Antwort "winziges Modell" und in dieser Antwort "mittleres Modell":

Winziges Modell : 1000 Iterationen,compile()

Winziges Modell : 1000 Iterationen, Nr compile()

Mittleres Modell : 10 Iterationen


DOKUMENTATION (indirekt) über die Auswirkungen von compile(): Quelle

Im Gegensatz zu anderen TensorFlow-Operationen konvertieren wir keine numerischen Python-Eingaben in Tensoren. Außerdem ein neues Diagramm wird für jeden einzelnen Python Zahlenwert erzeugt , zum Beispiel rufenden g(2)und g(3)zwei neue Graphen erzeugen

function Instanziiert ein separates Diagramm für jeden eindeutigen Satz von Eingabeformen und Datentypen . Das folgende Codefragment führt beispielsweise dazu, dass drei verschiedene Diagramme verfolgt werden, da jede Eingabe eine andere Form hat

Ein einzelnes tf.function-Objekt muss möglicherweise mehreren Berechnungsgraphen unter der Haube zugeordnet werden. Dies sollte nur als Leistung sichtbar sein (das Verfolgen von Diagrammen hat einen Rechenaufwand und Speicherkosten ungleich Null ), sollte jedoch die Richtigkeit des Programms nicht beeinträchtigen


Gegenbeispiel :

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)

Ausgänge :

34.8542 sec
34.7435 sec
OverLordGoldDragon
quelle
1
Was ist die Schlussfolgerung darüber, was wir tun sollten, um die schnellste Vorhersagegeschwindigkeit für jede Modellgröße zu erhalten? Ist es einfach nicht zu tun compile()?
off99555
3
@ off99555 "für jede Modellgröße" - so etwas gibt es nicht. Lesen Sie die gesamte Antwort - wenn ich Stunden gebraucht habe, um sie zu debuggen, sollten ein paar Minuten vom Fragesteller nicht unangemessen sein.
OverLordGoldDragon
Ich habe die ganze Sache gelesen, aber es ist schwer zu verstehen, weil ich nicht derjenige bin, der den Code debuggt hat. Sie müssen also eine Schlussfolgerung ziehen, die nicht die Zwischenvariablen enthält, die Sie während der Debugging-Phase finden. Beispiel: "Wenn Ihr Modell klein ist, verwenden Sie keine Kompilierung. Wenn Ihr Modell mittelgroß ist, können Sie die Kompilierung verwenden." So ähnlich.
off99555
1
@ off99555 Fair genug; Aktualisiert. Der neue Abschnitt ist ziemlich vernünftig, aber ich kann sehen, dass er nicht sofort realisiert wird.
OverLordGoldDragon
1
@ off99555 Nicht dass ich getestet hätte, aber sehr große Modelle (ResNet usw.) laufen möglicherweise deutlich schneller kompiliert, insb. wenn verteilt auf vielen Geräten - wie (A) mehr Graph und verteilungs schwer ist. Der sicherste Test ist ein Test - wie in der Antwort.
Nicht vertraut
15

UPDATE : Die tatsächliche Antwort wird als separate Antwort angezeigt. Dieser Beitrag enthält zusätzliche Informationen


.compile() Richtet den Großteil des TF / Keras-Diagramms ein, einschließlich Verluste, Metriken, Gradienten und teilweise des Optimierers und seiner Gewichte - was eine bemerkenswerte Verlangsamung garantiert.

Was ist unerwartet ist das Ausmaß der Verlangsamung - 10-fach auf meinem eigenen Experiment, und für predict(), die keine Gewichte nicht aktualisiert. Wenn man sich den Quellcode von TF2 ansieht, scheinen die Diagrammelemente eng miteinander verflochten zu sein, wobei die Ressourcen nicht unbedingt "fair" zugewiesen werden.

Möglicherweise übersehen Entwickler predictdie Leistung eines nicht kompilierten Modells, da Modelle normalerweise kompiliert verwendet werden. In der Praxis ist dies jedoch ein inakzeptabler Unterschied. Es ist auch möglich, dass es ein "notwendiges Übel" ist, da es eine einfache Problemumgehung gibt (siehe unten).

Dies ist keine vollständige Antwort, und ich hoffe, dass jemand sie hier bereitstellen kann. Wenn nicht, würde ich vorschlagen, ein Github-Problem auf TensorFlow zu eröffnen. (OP hat; hier )


Problemumgehung : Trainieren Sie ein Modell, speichern Sie seine Gewichte , erstellen Sie das Modell neu, ohne es zu kompilieren, und laden Sie die Gewichte. Speichern Sie nicht das gesamte Modell (z. B. model.save()), da es kompiliert geladen wird. Verwenden Sie stattdessen model.save_weights()und model.load_weights().

Problemumgehung 2 : oben, aber verwenden load_model(path, compile=False); Vorschlagsgutschrift: D. Möller


UPDATE : Um zu klären, wird Optimierer nicht vollständig mit instanziiert compile, einschließlich seiner weightsund updatesTensoren - dies erfolgt ist , wenn der erste Anruf zu einer Anpassungsfunktion vorgenommen wird ( fit, train_on_batch, usw.), über model._make_train_function().

Das beobachtete Verhalten ist daher noch seltsamer. Schlimmer noch, hat das Optimierungsprogramm den Bau nicht weitere Verlangsamungen (siehe unten) entlocken - was darauf hindeutet , „Diagrammgröße“ ist hier nicht die Haupterklärung.


EDIT : bei einigen Modellen eine 30-fache Verlangsamung . TensorFlow, was hast du getan? Beispiel unten:

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)

Ausgänge :

0.9891 sec
29.785 sec
29.521 sec
OverLordGoldDragon
quelle
1
Das ist interessant. Es ist eine Weile her, dass ich das Training mit einem statischen Diagramm model.fit()gegen eine dynamische Schleife mit eifriger Ausführung testen möchte, um festzustellen, ob der Leistungsverlust zu groß ist ...
Daniel Möller
1
In der Vergangenheit konnte ich einen signifikanten Geschwindigkeitsunterschied zwischen Keras und PyTorch feststellen (PyTorch ist viel schneller).
Daniel Möller
1
Ich habe hier ein Problem eröffnet: github.com/tensorflow/tensorflow/issues/33340
off99555
2
Ja. Es ist eine schlechte Designentscheidung, dass Sie trainingsbezogenen Code in die Vorhersage einfügen. Weil Benutzer diese Vorhersagefunktion in der Produktion mehrmals nacheinander verwenden. Es sollte am schnellsten funktionieren, um die geringste Überraschung zu verursachen. Im Vergleich zur Numpy-Implementierung müssen Sie nur eine Matrix multiplizieren, eine Verzerrung hinzufügen, aktivieren und das wars für eine dichte Ebene. Es besteht keine Notwendigkeit, eine Verlustfunktion zu betreffen.
off99555
1
Hinweis, Sie können verwenden load_model(name, compile=False), es ist einfacher als das Speichern / Laden von Gewichten und das Neuerstellen des Modells.
Daniel Möller