Python >> Tutoriel Python >  >> Python Tag >> Keras

Pourquoi le modèle Keras prédit-il plus lentement après la compilation ?

MISE À JOUR - 15/01/2020  :la meilleure pratique actuelle pour les petites tailles de lots devrait être d'alimenter directement le modèle en entrées - c'est-à-dire preds = model(x) , et si les couches se comportent différemment lors de l'apprentissage/de l'inférence, model(x, training=False) . Par dernier commit, ceci est maintenant documenté.

Je ne les ai pas comparés, mais selon la discussion Git, cela vaut également la peine d'essayer predict_on_batch() - en particulier avec les améliorations de TF 2.1.

CULPRET ULTIME :self._experimental_run_tf_function = True . C'est expérimental . Mais ce n'est pas vraiment mauvais.

À tous les développeurs TensorFlow lisant :nettoyez votre code . C'est le bordel. Et cela enfreint des pratiques de codage importantes, telles que une fonction fait une chose; _process_inputs fait beaucoup plus que "traiter les entrées", idem pour _standardize_user_data . "Je ne suis pas assez payé" - mais vous faites payer, en temps supplémentaire passé à comprendre vos propres trucs et en utilisateurs remplissant votre page Problèmes avec des bogues plus faciles à résoudre avec un code plus clair.

RÉSUMÉ :ce n'est qu'un peu plus lent avec compile() .

compile() définit un indicateur interne qui attribue une fonction de prédiction différente à predict . Cette fonction construit un nouveau graphe à chaque appel, le ralentissant par rapport à non compilé. Cependant, la différence n'est prononcée que lorsque le temps de train est beaucoup plus court que le temps de traitement des données . Si nous augmentons la taille du modèle à au moins la taille moyenne, les deux deviennent égaux. Voir le code en bas.

Cette légère augmentation du temps de traitement des données est plus que compensée par une capacité graphique amplifiée. Puisqu'il est plus efficace de ne conserver qu'un seul graphe de modèle, la précompilation est ignorée. Néanmoins  :si votre modèle est petit par rapport aux données, vous êtes mieux sans compile() pour l'inférence de modèle. Voir mon autre réponse pour une solution de contournement.

QUE DOIS-JE FAIRE ?

Comparez les performances du modèle compilé vs non compilé comme je l'ai dans le code en bas.

  • La compilation est plus rapide :exécutez predict sur un modèle compilé.
  • La compilation est plus lente :exécutez predict sur un modèle non compilé.

Oui, les deux sont possibles, et cela dépendra (1) de la taille des données ; (2) taille du modèle ; (3) matériel. Le code en bas montre en fait compilé modèle étant plus rapide, mais 10 itérations est un petit échantillon. Voir "solutions de contournement" dans mon autre réponse pour le "comment faire".

DÉTAILS :

Cela a pris du temps à déboguer, mais c'était amusant. Ci-dessous, je décris les principaux coupables que j'ai découverts, cite des documents pertinents et montre les résultats du profileur qui ont conduit au goulot d'étranglement ultime.

(FLAG == self.experimental_run_tf_function , pour plus de concision)

  1. Model par défaut instancie avec FLAG=False . compile() le définit sur True .
  2. predict() implique l'acquisition de la fonction de prédiction, func = self._select_training_loop(x)
  3. Sans aucun kwarg spécial passé à predict et compile , tous les autres drapeaux sont tels que :
    • (A) FLAG==True --> func = training_v2.Loop()
    • (B) FLAG==False --> func = training_arrays.ArrayLikeTrainingLoop()
  4. À partir de la chaîne de documentation du code source, (A) est fortement dépendant des graphiques, utilise davantage de stratégie de distribution et les opérations sont enclines à créer et à détruire des éléments de graphique, ce qui "peut" avoir un impact sur les performances.

Véritable coupable :_process_inputs() , représentant 81 % du temps d'exécution . Son composant majeur ? _create_graph_function() , 72 % du temps d'exécution . Cette méthode n'existe même pas pour (B) . En utilisant un modèle de taille moyenne, cependant, _process_inputs représente moins de 1 % du temps d'exécution . Code en bas, et les résultats du profilage suivent.

TRAITEURS DE DONNÉES :

(A) :<class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'> , utilisé dans _process_inputs() . Code source pertinent

(B) :numpy.ndarray , renvoyé par convert_eager_tensors_to_numpy . Code source pertinent, et ici

FONCTION D'EXÉCUTION DU MODÈLE (par exemple, prédire)

(A) :fonction de distribution, et ici

(B) :fonction de distribution (différente), et ici

PROFILER :résultats pour le code dans mon autre réponse, "petit modèle", et dans cette réponse, "moyen modèle":

Petit modèle :1000 itérations, compile()

Petit modèle :1000 itérations, non compile()

Modèle moyen :10 itérations

DOCUMENTATION (indirectement) sur les effets de compile() :sources

Contrairement aux autres opérations TensorFlow, nous ne convertissons pas les entrées pythonnumerical en tenseurs. De plus, un nouveau graphique est généré pour chaque valeur numérique python distincte , par exemple en appelant g(2) et g(3) générera deux nouveaux graphiques

function instancie un graphique distinct pour chaque ensemble unique de formes d'entrée et de types de données . Par exemple, l'extrait de code suivant entraînera le tracé de trois graphiques distincts, car chaque entrée a une forme différente

Un seul objet tf.function peut avoir besoin d'être mappé sur plusieurs graphiques de calcul sous le capot. Cela devrait être visible uniquement en tant que performances (le traçage des graphiques a un coût de calcul et de mémoire non nul ) mais ne devrait pas affecter l'exactitude du programme

CONTRE-EXEMPLE :

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)

Sorties :

34.8542 sec
34.7435 sec

MISE À JOUR :voir la réponse réelle publiée en tant que réponse séparée ; ce message contient des informations supplémentaires

.compile() met en place la majorité du graphique TF/Keras, y compris les pertes, les métriques, les gradients, et en partie l'optimiseur et ses pondérations - ce qui garantit un ralentissement notable.

Qu'est-ce qu'est inattendu est l'étendue du ralentissement - 10 fois sur ma propre expérience, et pour predict() , qui ne met pas à jour les pondérations. En regardant dans le code source de TF2, les éléments graphiques apparaissent étroitement imbriqués, les ressources n'étant pas nécessairement allouées "équitablement".

Oubli possible par les développeurs sur predict pour un modèle non compilé, car les modèles sont généralement utilisés compilés - mais en pratique , c'est une différence inacceptable. Il est également possible qu'il s'agisse d'un "mal nécessaire", car il existe une solution de contournement simple (voir ci-dessous).

Ce n'est pas une réponse complète, et j'espère que quelqu'un pourra la fournir ici - sinon, je suggérerais d'ouvrir un problème Github sur TensorFlow. (OP a ; ici)

Solution  :entraîner un modèle, enregistrer ses poids , reconstruire le modèle sans compiler, charger les poids. Ne pas enregistrer le modèle entier (par exemple model.save() ), car il se chargera compilé - utilisez plutôt model.save_weights() et model.load_weights() .

Solution 2 :ci-dessus, mais utilisez load_model(path, compile=False); crédit suggestion :D. Möller

MISE À JOUR  :pour clarifier, l'optimiseur n'est pas entièrement instancié avec compile , y compris son weights et updates tenseurs - cela se fait lors du premier appel à une fonction d'ajustement (fit , train_on_batch , etc.), via model._make_train_function() .

Le comportement observé est donc encore plus étrange. Pire encore, la construction de l'optimiseur ne le fait pas susciter d'autres ralentissements (voir ci-dessous) - ce qui suggère que la "taille du graphique" n'est pas la principale explication ici.

MODIFIER :sur certains modèles, un ralentissement de 30x . TensorFlow, qu'avez-vous fait ? Exemple ci-dessous :

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)

Sorties :

0.9891 sec
29.785 sec
29.521 sec