Pourquoi le modèle des kéros prévoit-il un ralentissement après la compilation?

23

keras de vitesse de prédiction

En théorie, la prédiction devrait être constante car les poids ont une taille fixe. Comment récupérer ma vitesse après la compilation (sans avoir besoin de supprimer l'optimiseur)?

Voir l'expérience associée: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true

off99555
la source
Je pense que vous devez adapter le modèle après la compilation, puis utiliser le modèle formé pour prédire. Référez-vous ici
naïf
@naive Fitting est sans rapport avec le problème. Si vous savez comment fonctionne réellement le réseau, vous seriez curieux de savoir pourquoi la prédiction est plus lente. Lors de la prédiction, seuls les poids sont utilisés pour la multiplication matricielle et les poids doivent être fixés avant et après la compilation, de sorte que le temps de prédiction doit rester constant.
off99555
Je sais que cela n'a rien à voir avec le problème . Et, il n'est pas nécessaire de savoir comment fonctionne le réseau pour souligner que les tâches pour lesquelles vous avez élaboré et comparer la précision n'ont en fait aucun sens. Sans ajuster le modèle à certaines données que vous prédisez et que vous comparez réellement le temps pris. Ce ne sont pas les cas d'utilisation habituels ou appropriés pour un réseau de neurones
naïf
3
@naive Le problème concerne la compréhension des performances du modèle compilé vs non compilé, n'ayant rien à voir avec la précision ou la conception du modèle. C'est un problème légitime qui peut coûter aux utilisateurs de TF - pour ma part, je n'en avais aucune idée avant de tomber sur cette question.
OverLordGoldDragon
1
@naive Vous ne pouvez pas fitsans compile; L'optimiseur n'existe même pas pour mettre à jour les poids. predict peut être utilisé sans fitou compilecomme décrit dans ma réponse, mais la différence de performances ne devrait pas être aussi dramatique - d'où le problème.
OverLordGoldDragon

Réponses:

22

MISE À JOUR - 15/01/2020 : les meilleures pratiques actuelles pour les petites tailles de lots devrait être d'alimenter les entrées au modèle directement - par exemple preds = model(x), et si les couches se comportent différemment en train / inférence model(x, training=False). Par dernier commit, cela est désormais documenté .

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


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

Pour tous les développeurs TensorFlow qui lisent: nettoyez votre code . C'est le bordel. Et il viole d'importantes pratiques de codage, telles qu'une fonction fait une chose ; _process_inputsfait beaucoup plus que les "entrées de processus", pareil pour _standardize_user_data. « Je ne suis pas assez payé » - mais vous ne payer, dans le temps supplémentaire passé à comprendre vos propres trucs, et les utilisateurs de remplir votre page Problèmes avec des bugs plus facile résolus avec un code plus clair.


RÉSUMÉ : c'est seulement 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 graphique à 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 de 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 la capacité de graphique amplifiée. Puisqu'il est plus efficace de ne conserver qu'un seul graphique de modèle, celui qui précompile est supprimé. Néanmoins : si votre modèle est petit par rapport aux données, il vaut mieux sans compile()pour l'inférence du modèle. Voir mon autre réponse pour une solution de contournement.


QUE DEVRAIS-JE FAIRE?

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

  • Compilé est plus rapide : exécuté predictsur un modèle compilé.
  • Compilé est plus lent : exécuté predictsur un modèle non compilé.

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


DÉTAILS :

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

( FLAG == self.experimental_run_tf_function, par souci de concision)

  1. Modelpar défaut instancie avec FLAG=False. compile()le définit True.
  2. predict() consiste à acquérir la fonction de prédiction, func = self._select_training_loop(x)
  3. Sans aucun kwarg spécial transmis à predictet compile, tous les autres indicateurs sont tels que:
    • (A) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. À partir du code source docstring , (A) est fortement tributaire des graphiques, utilise plus de stratégie de distribution et les opérations sont sujettes à la création et à la destruction d'éléments graphiques, ce qui "peut" (avoir) un impact sur les performances.

Véritable coupable : il _process_inputs()représente 81% du temps d'exécution . Sa composante majeure? _create_graph_function(), 72% du temps d'exécution . Cette méthode n'existe même pas pour (B) . Cependant, l'utilisation d'un modèle de taille moyenne _process_inputsreprésente moins de 1% du temps d'exécution . Code en bas, et les résultats du profilage suivent.


PROCESSEURS DE DONNÉES :

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

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


FONCTION D'EXÉCUTION DU MODÈLE (par ex. 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, "modèle moyen":

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

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

Modèle moyen : 10 itérations


DOCUMENTATION (indirecte) sur les effets de compile(): source

Contrairement à d'autres opérations TensorFlow, nous ne convertissons pas les entrées numériques python 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 séparé 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 traçage de trois graphiques distincts, car chaque entrée a une forme différente

Un seul objet tf.function peut devoir être mappé à plusieurs graphiques de calcul sous le capot. Cela ne devrait être visible qu'en tant que performances (le traçage des graphiques a un coût de calcul et de mémoire différent de zéro ) 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
OverLordGoldDragon
la source
1
Quelle est la conclusion sur ce que nous devons faire pour obtenir la vitesse de prédiction la plus rapide pour n'importe quelle taille de modèle? Est-ce simplement pour ne pas faire compile()?
off99555
3
@ off99555 "pour n'importe quelle taille de modèle" - il n'y a rien de tel. Lisez la réponse entière - si j'ai mis des heures à la déboguer, quelques minutes du demandeur ne devraient pas être déraisonnables.
OverLordGoldDragon
J'ai lu le tout mais c'est difficile à comprendre car ce n'est pas moi qui ai débogué le code. Vous devez donc donner une conclusion qui n'implique pas les variables intermédiaires que vous trouvez pendant la phase de débogage. Par exemple "Si votre modèle est petit, n'utilisez pas la compilation. Si votre modèle est de taille moyenne, vous pouvez utiliser la compilation.` Quelque chose comme ça.
off99555
1
@ off99555 Assez bien; mise à jour. La nouvelle section est assez logique, mais je peux voir qu'elle ne se réalise pas immédiatement.
OverLordGoldDragon
1
@ off99555 Ce n'est pas que j'ai testé, mais les très gros modèles (ResNet, etc.) peuvent s'exécuter de manière beaucoup plus rapide, en particulier. si distribué sur de nombreux appareils - car (A) est plus lourd en termes de graphiques et de distribution Le test le plus sûr est, eh bien, un test - comme dans la réponse. Pas familier avec TF lite, mais c'est une question distincte
OverLordGoldDragon
15

MISE À JOUR : voir la réponse réelle publiée comme une réponse distincte; cet article contient des informations supplémentaires


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

Ce qui est inattendu, c'est l'ampleur du ralentissement - décuplé sur ma propre expérience, et pour predict(), qui ne met à jour aucun poids. En examinant le code source de TF2, les éléments du graphique semblent étroitement liés, les ressources n'étant pas nécessairement allouées "équitablement".

Les développeurs predictpeuvent ignorer les performances d'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 que ce soit 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ère d'ouvrir un problème Github sur TensorFlow. (OP a; ici )


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

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


UPDATE : clarifier, optimiseur est pas entièrement instancié avec compile, y compris ses weightset updatestenseurs - cela se fait lorsque le premier appel d'une fonction d' ajustement est effectuée ( fit, train_on_batch, etc.), par l' intermédiaire model._make_train_function().

Le comportement observé est donc encore plus étrange. Pire encore, la construction de l'optimiseur ne pas obtenir d'autres ralentissements (voir ci - dessous) - suggérant « la taille du graphique » n'est pas la principale explication ici.


EDIT : 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
OverLordGoldDragon
la source
1
C'est intéressant. Cela fait un moment que je veux tester la formation avec un graphique statique model.fit()par rapport à une boucle dynamique avec une exécution impatiente pour voir si la perte de performance est trop importante ...
Daniel Möller
1
Dans le passé, j'ai pu remarquer une différence de vitesse significative entre Keras et PyTorch (étant PyTorch beaucoup plus rapide).
Daniel Möller
1
J'ai ouvert un problème ici: github.com/tensorflow/tensorflow/issues/33340
off99555
2
Oui. C'est un mauvais choix de conception que vous mettiez du code lié à la formation dans la prédiction. Parce que les utilisateurs utiliseront cette fonction de prédiction de manière séquentielle plusieurs fois en production. Il devrait fonctionner le plus rapidement pour provoquer la moindre surprise. Par rapport à l'implémentation numpy, il vous suffit de multiplier une matrice, d'ajouter un biais, d'activer, et c'est tout pour une couche dense. Il n'est pas nécessaire de concerner une fonction de perte.
off99555
1
Astuce, vous pouvez utiliser load_model(name, compile=False), c'est plus simple que de sauvegarder / charger des poids et de recréer le modèle.
Daniel Möller