Perché il modello keras prevede un rallentamento dopo la compilazione?


23

telecamere di velocità di previsione

In teoria, la previsione dovrebbe essere costante in quanto i pesi hanno una dimensione fissa. Come posso ripristinare la velocità dopo la compilazione (senza la necessità di rimuovere l'ottimizzatore)?

Vedi esperimento associato: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true


Penso che sia necessario adattare il modello dopo la compilazione, quindi utilizzare il modello addestrato per prevedere. Fare riferimento qui
ingenuo

@naive Fitting è irrilevante per il problema. Se sai come funziona effettivamente la rete, sarebbe curioso sapere perché la previsione è più lenta. Durante la previsione, solo i pesi vengono utilizzati per la moltiplicazione della matrice e i pesi devono essere fissati prima e dopo la compilazione, quindi il tempo di previsione dovrebbe rimanere costante.
off99555

So che è irrilevante per il problema . Inoltre, non è necessario sapere come funziona la rete per sottolineare che i compiti che sono stati elaborati e il confronto dell'accuratezza per i quali in realtà è insignificante. Senza adattare il modello ad alcuni dati che stai prevedendo e stai effettivamente confrontando il tempo impiegato. Non si tratta dei soliti o giusti casi d'uso per una rete neurale
ingenuo

3
@naive Il problema riguarda la comprensione delle prestazioni del modello compilate vs non compilate, non avendo nulla a che fare con l'accuratezza o la progettazione del modello. È un problema legittimo che può costare agli utenti TF: io per primo non ne avevo idea fino a quando non ci siamo imbattuti in questa domanda.
OverLordGoldDragon

1
@naive Non puoi fare a fitmeno compile; non esiste nemmeno l'ottimizzatore per aggiornare i pesi. predict può essere usato senza fito compilecome descritto nella mia risposta, ma la differenza di prestazioni non dovrebbe essere così drammatica - da qui il problema.
OverLordGoldDragon

Risposte:


22

AGGIORNAMENTO - 15/01/2020 : la migliore prassi corrente per piccoli lotti dovrebbe essere per alimentare ingressi al modello direttamente - cioè preds = model(x), se strati comportano in modo diverso a treno / inferenza, model(x, training=False). Per l'ultimo commit, questo è ora documentato .

Non li ho confrontati con questi, ma per il discussione su Git vale anche la pena provare predict_on_batch(), specialmente con miglioramenti in TF 2.1.


ULTIMA CULPRIT : self._experimental_run_tf_function = True. È sperimentale . Ma in realtà non è male.

A tutti gli sviluppatori di TensorFlow che leggono: ripulisci il tuo codice . È un casino. E viola importanti pratiche di codifica, come una funzione fa una cosa ; _process_inputsfa molto più di "input di processo", lo stesso per _standardize_user_data. "Io non sono pagato abbastanza" - ma faccio pagare, nel tempo in più speso comprendere la propria roba, e gli utenti che riempiono la tua pagina problemi con gli insetti più facile risolto con un codice più chiaro.


SOMMARIO : è solo un po ' più lento con compile().

compile()imposta un flag interno che assegna una diversa funzione di previsione a predict. Questa funzione costruisce un nuovo grafico ad ogni chiamata, rallentandolo rispetto al non compilato. Tuttavia, la differenza è pronunciata solo quando tempo del treno è molto più breve del tempo di elaborazione dei dati . Se aumentiamo le dimensioni del modello ad almeno medie, i due diventano uguali. Vedi il codice in fondo.

Questo leggero aumento del tempo di elaborazione dei dati è più che compensato dalla capacità del grafico amplificato. Poiché è più efficiente mantenere solo un grafico del modello in giro, quello di pre-compilazione viene scartato.Tuttavia : se il tuo modello è piccolo rispetto ai dati, stai meglio senza l' compile()inferenza del modello. Vedi la mia altra risposta per una soluzione alternativa.


COSA DOVREI FARE?

Confronta le prestazioni del modello compilate e non compilate come ho nel codice in fondo.

  • La compilazione è più veloce : eseguipredict compilazione su un modello compilato.
  • La compilazione è più lenta : viene eseguita predictsu un modello non compilato.

Sì, entrambi sono possibili e dipenderà dalla (1) dimensione dei dati; (2) dimensioni del modello; (3) hardware. Il codice in basso mostra in realtà un modello compilato più veloce, ma 10 iterazioni sono un piccolo esempio. Vedi "soluzioni alternative" nella mia altra risposta per il "come fare".


DETTAGLI :

Il debug ha richiesto del tempo, ma è stato divertente. Di seguito descrivo i principali colpevoli che ho scoperto, cito alcuni documenti pertinenti e mostrerò i risultati del profiler che hanno portato al collo di bottiglia finale.

( FLAG == self.experimental_run_tf_function, per brevità)

  1. Modeldi default crea un'istanza con FLAG=False. compile()lo imposta suTrue .
  2. predict() comporta l'acquisizione della funzione di previsione, func = self._select_training_loop(x)
  3. Senza alcun kwarg speciale passato a predicte compile, tutte le altre bandiere sono tali che:
    • (A) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. Dal docstring del codice sorgente , (A) è fortemente dipendente dal grafico, usa più strategia di distribuzione e le operazioni sono inclini a creare e distruggere elementi grafici, che "possono" (influire) sulle prestazioni.

Vero colpevole : _process_inputs()rappresenta l' 81% del tempo di esecuzione . Il suo componente principale? _create_graph_function(), 72% di runtime . Questo metodo non esiste nemmeno per (B) . L'uso di un modello di medie dimensioni, tuttavia, _process_inputscomprende meno dell'1% del tempo di esecuzione . Codice in basso e seguiranno i risultati della profilazione.


Responsabili del trattamento :

(A) :, <class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'>utilizzato in _process_inputs(). Codice sorgente pertinente

(B) :, numpy.ndarrayrestituito da convert_eager_tensors_to_numpy. Codice sorgente pertinente , e qui


FUNZIONE DI ESECUZIONE DEL MODELLO (ad es. Previsione)

(A) : funzione di distribuzione , e qui

(B) : funzione di distribuzione (diversa) , e qui


PROFILER : risultati per il codice nell'altra mia risposta, "modello piccolo", e in questa risposta, "modello medio":

Modello minuscolo : 1000 iterazioni,compile()

Modello minuscolo : 1000 iterazioni, n compile()

Modello medio : 10 iterazioni


DOCUMENTAZIONE (indirettamente) sugli effetti di compile(): fonte

A differenza di altre operazioni TensorFlow, non convertiamo input numerici python in tensori. Inoltre, viene generato un nuovo grafico per ciascun valore numerico python distinto , ad esempio chiamando g(2)e g(3)genererà due nuovi grafici

function crea un'istanza di un grafico separato per ogni set univoco di forme di input e tipi di dati . Ad esempio, il seguente frammento di codice determinerà la traccia di tre grafici distinti, poiché ciascun input ha una forma diversa

Potrebbe essere necessario mappare un singolo oggetto tf.function su più grafici di calcolo sotto il cofano. Questo dovrebbe essere visibile solo in quanto le prestazioni (i grafici di tracciamento hanno un costo di calcolo e di memoria diverso da zero ) ma non dovrebbero influire sulla correttezza del programma


ESEMPIO CONTRO :

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)

Uscite :

34.8542 sec
34.7435 sec

1
Qual è la conclusione su cosa dovremmo fare per ottenere la massima velocità di previsione per qualsiasi dimensione del modello? Non è proprio per non farlo compile()?
off99555

3
@ off99555 "per qualsiasi dimensione di modello" - non esiste nulla del genere. Leggi l'intera risposta: se impiegassi ore per eseguire il debug, a pochi minuti dall'assistente non dovrebbe essere irragionevole.
OverLordGoldDragon

Ho letto tutto ma è difficile da capire perché non sono quello che ha eseguito il debug del codice. Quindi devi dare una conclusione che non coinvolga le variabili intermedie che trovi durante la fase di debug. Ad esempio "Se il tuo modello è piccolo, non utilizzare la compilazione. Se il tuo modello è di medie dimensioni, puoi utilizzare la compilazione." Qualcosa del genere.
off99555

1
@ off99555 Abbastanza giusto; aggiornato. La nuova sezione ha un buon senso, ma vedo che non si realizza immediatamente.
OverLordGoldDragon

1
@ off99555 Non che ho provato, ma modelli molto grandi (ResNet, ecc.) possono essere compilati in modo notevolmente più veloce, esp. se distribuito su molti dispositivi, poiché (A) è più pesante per grafici e distribuzione. Il test più sicuro è, beh, un test, come nella risposta. Non familiare con TF Lite, ma questa è una domanda separata
OverLordGoldDragon

15

AGGIORNAMENTO : vedere la risposta effettiva pubblicata come risposta separata; questo post contiene informazioni supplementari


.compile() imposta la maggior parte del grafico TF / Keras, inclusi perdite, metriche, gradienti e in parte l'ottimizzatore e i suoi pesi - che garantisce un notevole rallentamento.

Ciò che è inaspettato è l'entità del rallentamento - 10 volte sul mio esperimento, e per predict(), che non aggiorna alcun peso. Esaminando il codice sorgente di TF2, gli elementi del grafico appaiono strettamente intrecciati, con risorse non necessariamente allocate "in modo equo".

Possibile trascuratezza da parte degli sviluppatori sulle predictprestazioni di un modello non compilato, poiché i modelli vengono generalmente utilizzati compilati, ma in pratica questa è una differenza inaccettabile. È anche possibile che sia un "male necessario", in quanto esiste una semplice soluzione (vedi sotto).

Questa non è una risposta completa e spero che qualcuno possa fornirla qui - in caso contrario, suggerirei di aprire un problema Github su TensorFlow. (OP ha; qui )


Soluzione alternativa : addestrare un modello, salvare i suoi pesi , ricostruire il modello senza compilare, caricare i pesi. Evitare Non salvare l'intero modello (ad esempio model.save()), in quanto sarà carica la compilazione - invece usare model.save_weights()e model.load_weights().

Soluzione 2 : sopra, ma usare load_model(path, compile=False); credito di suggerimento: D. Möller


UPDATE : per chiarire, ottimizzatore è non completamente istanziato con compile, compresi i suoi weightse updatestensori - questo è fatto quando la prima chiamata a una funzione raccordo è fatto ( fit, train_on_batch, ecc), via model._make_train_function().

Il comportamento osservato è quindi ancora più strano. Peggio ancora, la costruzione del ottimizzatore non senza suscitare ulteriori rallentamenti (vedi sotto) - suggerendo "formato grafico" non è la spiegazione principale qui.


EDIT : su alcuni modelli, un rallentamento di 30x . TensorFlow, cosa hai fatto. Esempio sotto:

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)

Uscite :

0.9891 sec
29.785 sec
29.521 sec

1
Interessante. È da un po 'che voglio testare l'allenamento con un grafico statico model.fit()rispetto a un loop dinamico con un'esecuzione entusiasta per vedere se la perdita di prestazioni è troppo grande ...
Daniel Möller

1
In passato ho notato una differenza di velocità significativa tra Keras e PyTorch (essendo PyTorch molto più veloce).
Daniel Möller,


2
Sì. È una cattiva scelta progettuale inserire il codice relativo alla formazione all'interno della previsione. Poiché gli utenti useranno questa funzione di previsione più volte in sequenza nella produzione. Dovrebbe funzionare più velocemente per causare la minima sorpresa. Rispetto all'implementazione numpy, devi solo moltiplicare una matrice, aggiungere un bias, attivare e il gioco è fatto per un livello denso. Non è necessario riguardare alcuna funzione di perdita.
off99555

1
Suggerimento, è possibile utilizzare load_model(name, compile=False), è più semplice che salvare / caricare pesi e ricreare il modello.
Daniel Möller,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.