Le TensorFlow Dev Summit 2019 a eu lieu le 6 mars dernier, avec comme annonce majeure la release en version alpha de TensorFlow 2.0. Cet évènement a permis de faire un tour d’horizon de toutes les nouveautés apportées au framework sur cette dernière année et de donner quelques projections sur les prochains développements.
Cette série d’articles a pour but de vous résumer les principaux enseignements que nous en avons tiré. Dans ce premier article, nous parlerons de TensorFlow 2.0 et de ses APIs haut niveau, ainsi que de tf.function et autograph.
Introducing TensorFlow 2.0 and its high-level APIs
C’est officiel, TensorFlow 2.0 est maintenant disponible en version alpha !
Le focus principal pour cette nouvelle version de TensorFlow est mis sur l’utilisabilité, avec notamment deux changements majeurs :
- tf.keras est maintenant l’API haut niveau par défaut pour construire des modèles. Elle a même été étendue pour fonctionner avec toutes les fonctionnalités avancées de TensorFlow.
- Le mode eager execution est activé par défaut afin de pouvoir travailler de manière plus intuitive et évaluer des opérations immédiatement sans avoir à passer par la notion de graphes.
Qui dit nouvelle version majeure, dit beaucoup de nettoyage et changements. Beaucoup de code dupliqué a été supprimé, et un gros travail sur la consistance du framework a été fait. À la demande de la communauté, des améliorations ont également été apportées afin de clarifier la documentation et fournir plus d’exemples de code.
Ces avancées pour simplifier l’utilisation de TensorFlow et ses APIs haut niveau n’enlèvent en rien la possibilité d’utiliser du code plus bas niveau, avec maintenant une API qui expose toutes les opérations internes.
Les chercheurs et Kagglers ne sont pas oubliés puisque, couplé à l’eager execution, il est possible de faire de l’héritage de classe et des boucles d’entraînement personnalisées afin d‘avoir plus de flexibilité dans la modélisation d’un problème.
class Encoder(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz): super(Encoder, self).__init__() self.batch_sz = batch_sz self.enc_units = enc_units self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim) self.gru = tf.keras.layers.GRU(self.enc_units, return_sequences=True, return_state=True, recurrent_initializer="glorot_uniform") def call(self, x, hidden): x = self.embedding(x) output, state = self.gru(x, initial_state=hidden) return output, state def initialize_hidden_state(self): return tf.zeros((self.batch_sz, self.enc_units)) |
Exemple de boucle personnalisée permettant d’avoir un contrôle fin sur le gradient et le processus d’optimisation:
def train_step(inp, targ, enc_hidden): loss = 0 with tf.GradientTape() as tape: enc_output, enc_hidden = encoder(inp, enc_hidden) dec_hidden = enc_hidden dec_input = tf.expand_dims([targ_lang.word_index["<start>"]] * BATCH_SIZE, 1) for t in range(1, targ.shape[1]): # passing enc_ouput to the decoder predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output) loss += loss_function(targ[:, t], predictions) # using teacher forcing dec_input = tf.expand_dims(targ[:, t], 1) batch_loss = (loss / int(targ.shape[1])) variables = encoder.trainable_variables + decoder.trainable_variables gradients = tape.gradient(loss, variables) optimizer.apply_gradients(zip(gradients, variables)) return batch_loss |
Des guides de migration vers 2.0 sont déjà présents et seront enrichis au fur et à mesure, ainsi qu’un script de conversion (tf_upgrade_v2) et une couche de compatibilité pour permettre à TensorFlow 2.0 d’utiliser des API TensorFlow 1.x.
Il va falloir encore patienter un petit peu pour la version définitive : les versions 2.0 RC et finale seront mises à disposition au cours du printemps.
TensorFlow 2.0 et ses APIs haut niveau
Keras a été intégré à TensorFlow sous la forme de tf.keras, qui est une version optimisée de Keras pour TensorFlow, s’interfaçant directement avec les autres modules.
Il est très simple avec tf.keras de définir un modèle et une boucle d’entraînement, mais aussi de créer des sous-ensembles d’architectures via des héritages et classes spécifiques.
model = tf.keras.models.Sequential([ tf.keras.layers.Flatten(), tf.keras.layers.Dense(512, activation="relu"), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(10, activation="softmax") ]) model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"]) model.fit(x_train, y_train, epochs=5) model.evaluate(x_test, y_test) |
tf.keras a notamment été construit pour du prototypage rapide et des modèles relativement petits. Or il y a souvent le besoin de créer des modèles à plus grande échelle, sur beaucoup de données, et en mode distribué pour accélérer les entraînements.
C’est justement la raison d’être des estimators, qui ont été conçus avec un objectif de distribution et de passage à l’échelle sur plusieurs centaines de machines de manière intuitive.
wide_columns = [ tf.feature_column.categorical_column_with_identity("user_id", num_buckets=10000) ] deep_columns = [ tf.feature_column.numeric_column("visits"), tf.feature_column.numeric_column("clicks"), ] tf.estimator.DNNLinearCombinedClassifier( linear_feature_columns=wide_columns, dnn_feature_columns=deep_columns, dnn_hidden_units=[100, 75, 50, 25] ) |
Mais le but n’est pas d’avoir à choisir entre une API simple et une API scalable. TensorFlow 2.0 permet donc de combiner l’API tf.keras (pour construire simplement des modèles) avec les estimators (pour bénéficier de toute leur puissance). L’objectif est donc de pouvoir passer d’un code de prototypage à un entraînement distribué, jusqu’à la mise en production, et ce de manière fluide.
Concrètement, le même code tf.keras va pouvoir être utilisé de la version 1.13 à la version 2.0, sauf que pour la dernière version, le mode eager sera utilisé par défaut. Cela veut dire que l’on peut facilement déboguer, et contrôler l’entraînement étape par étape en compilant le modèle avec le flag run_eagerly=True.
tf.keras est aussi intégré à d’autres outils de l’écosystème TensorFlow, comme par exemple le parsing de données structurées grâce aux feature_column. Il est donc possible de parser ses données avec des feature_column et de les intégrer directement dans un layer keras (tf.keras.layers.DenseFeatures).
user_id = tf.feature_column.categorical_column_with_identity("user_id", num_buckets=10000) uid_embedding = tf.feature_column.embedding_column(user_id, 10) columns = [ uid_embedding, tf.feature_column.numeric_column("visits"), tf.feature_column.numeric_column("clicls") ] feature_layer = tf.keras.layers.DenseFeatures(columns) model = tf.keras.models.Sequential([feature_layer, ...]) |
Pour la visualisation des avancements des entraînements, TensorBoard est votre meilleur ami. Il peut être directement intégré à un entraînement de modèle tf.keras grâce à un callback.
TensorFlow 2.0 amène aussi une nouvelle API : tf.distribute.strategy. Cette API fournit un ensemble de stratégies intégrées pour l’entraînement distribué de modèles, fonctionnant nativement avec tf.keras. Cette API a pour but d’être simple à utiliser et propose de très bonnes performances avec très peu d’efforts. Elle est aussi suffisamment versatile pour gérer plusieurs types de distributions et infrastructures (de la machine multi-GPU au cluster entier de machines). Tout le travail de distribution des données et des entraînements est fait pour nous.
strategy = tf.distribute.MirroredStrategy() with strategy.scope(): model = tf.keras.models.Sequential([ tf.keras.layers.Dense(64, input_shape=[10]), tf.keras.layers.Dense(64, activation="relu"), tf.keras.layers.Dense(10, activation="softmax") ]) model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"]) |
Cette API permet de passer à un entraînement distribué sans avoir à changer de code.
Enfin, en termes d’export de modèles, SavedModel devient la référence dans tout l’écoystème TensorFlow. Les modèles entraînés via tf.keras peuvent maintenant facilement être sérialisés au format SavedModel), mais la fonctionnalité est encore expérimentale. Les modèles peuvent alors être exportés pour être utilisés via TensorFlow Serving, TensorFlow Lite ou autres systèmes tierces.
saved_model_path = tf.keras.experimental.export_saved_model(model, "/path/to/model") new_model = tf.keras.experimental.load_from_saved_model(saved_model_path) new_model.summary() |
Quelle est la suite pour TensorFlow 2.0 ?
Durant les prochains mois, de nombreuses améliorations et nouveautés vont apparaître, avec notamment des stratégies de distribution pour du multiple noeuds sur plusieurs machines (MultiWorkerMirroredStrategy), ainsi que pour les TPUs (TPUStrategy), et une exposition de Canned Estimators (estimators pré-implémentés) dans l’API tf.keras.
Pour voir la vidéo au complet :
tf.function and autograph
A partir de la version 2.0 de TensorFlow, le mode eager execution est celui par défaut. Mais cela n’empêche pas de vouloir utiliser les graphes !
Certains types de hardware, comme les TPUs, bénéficient énormément des optimisations à l’échelle du programme dans son ensemble, chose rendue possible par les graphes. De plus, avec les graphes, il est simple de prendre un modèle et de le déployer sur des servers ou des devices mobile par exemple.
Bonne nouvelle, nous allons maintenant pouvoir bénéficier des avantages du mode graphe de TensorFlow, sans avoir à écrire le code qui était nécessaire dans les versions précédentes (ajout de nœuds à un graphe puis utilisation de session.run pour un calcul optimisé). Tout ceci est simplifié grâce à tf.function.
# A function is like an op @tf.function def add(a, b): return a + b add(tf.ones([2, 2]), tf.ones([2, 2])) # [[2., 2.], [2., 2.]] |
tf.function agit exactement comme une opération, qu’il faut créer de ses propres mains, en composant des opérations de TensorFlow. Cette fonction peut alors être utilisée à votre guise. L’idée est donc de définir sa fonction facilement en Python, afin de pouvoir conserver un style de programmation plus proche de celui que l’on utilise en Python habituellement.
En termes de performances, le réel gain par rapport au mode eager execution intervient lorsque le modèle imbrique plusieurs petites briques fonctionnelles.
Un autre avantage de tf.function est la possibilité de contrôler les dépendances dans le modèle créé. C’était une des difficultés dans les versions précédentes de TensorFlow car les optimisations à l’échelle du graphe complet faisait parfois perdre des dépendances et amenaient à des erreurs. Les opérations sont maintenant exécutées les unes à la suite des autres, dans l’ordre où elles sont écrites.
A partir de maintenant, il n’y a plus besoin d’initialiser soi-même les Variables TensorFlow (c’est-à-dire utiliser tf.global_variables_initializer).
Une autre feature très intéressante de tf.function est son intégration avec Autograph, qui est un compilateur Python qui réécrit les expressions control flow permettant de se séparer des commandes tf.cond ou tf.while_loop.
@tf.function def f(x): while tf.reduce_sum(x) > 1: x = tf.tanh(x) return x f(tf.random.uniform([10])) |
En résumé, beaucoup de choses sont simplifiées avec l’incorporation de tf.functions :
- Plus besoin de session.run
- Plus besoin de tf.control_dependencies
- Plus besoin de tf.global_variables_initializer
- Plus de tf.cond, tf.while_loop
- Utilisation de tf.function
Pour voir la vidéo au complet :
What else ?
Ce premier article reprend les principales modifications apportées au coeur de TensorFlow 2.0. Elles ont pour objectif de rendre plus simple et fluide l’utilisation du framework dans son ensemble, et de proposer des optimisations pour toujours bénéficier des avantages des versions précédentes.
Dans le prochain article, nous parlerons des nouveautés qui concernant d’autres briques autour de TensorFlow, notamment TensorFlow Datasets, TensorFlow Hub et TensorBoard.