Publié par
Il y a 4 mois · 16 minutes · Data

TensorFlow & Deep Learning – Épisode 1 – Introduction

Tensorflow logoNous en entendons beaucoup parler ces derniers temps, et pour cause, TensorFlow est devenu en un temps record l’un des frameworks de référence pour le Deep Learning, utilisé aussi bien dans la recherche qu’en entreprise pour des applications en production. Au-delà de la hype présente autour de ce framework et des projets qui émergent grâce à ce dernier, il reste un gap non négligeable à atteindre afin de l’utiliser pleinement et efficacement.

Le but de cette série d’articles est d’aider le développeur à se familiariser avec TensorFlow via un cas d’application simple qui sera notre fil rouge. Nous montrerons quelles sont les briques essentielles pour créer, entraîner et utiliser différentes architectures de Réseaux de Neurones, et comment les optimiser pour se rendre compte qu’il n’y a pas besoin de tout ré-implémenter lorsque l’on veut modifier l’architecture du réseau de neurones.

Commençons dès maintenant par ce premier article introductif sur TensorFlow, son mode de fonctionnement ainsi que les principaux objets à manipuler. Nous rentrerons dans le vif du sujet dès le prochain article.

TensorFlow, mais qu’est-ce donc ?

TensorFlow est un framework de programmation pour le calcul numérique qui a été rendu Open Source par Google en Novembre 2015. Depuis sa release, TensorFlow n’a cessé de gagner en popularité, pour devenir très rapidement l’un des frameworks les plus utilisés pour le Deep Learning, comme le montrent les dernières comparaisons suivantes, faites par François Chollet (auteur de la librairie Keras).

Deep Learning

Quelles sont les raisons de cette popularité fracassante ? Elles sont nombreuses:

  • Multi-plateformes (Linux, Mac OS, et même Android et iOS !)
  • APIs en Python, C++, Java et Go (l’API Python est plus complète cependant, c’est sur celle-ci que nous allons travailler)
  • Temps de compilation très courts dû au backend en C/C++
  • Supporte les calculs sur CPU, GPU et même le calcul distribué sur cluster
  • Une documentation extrêmement bien fournie avec de nombreux exemples et tutoriels
  • Last but not least: Le fait que le framework vienne de Google et que ce dernier ait annoncé avoir migré la quasi totalité de ses projets liés au Deep Learning en TensorFlow est quelque peu rassurant

Bien qu’il ait initialement été développé pour optimiser les calculs numériques complexes, TensorFlow est aujourd’hui particulièrement utilisé pour le Deep Learning, et donc les réseaux de neurones. Son nom est notamment inspiré du fait que les opérations courantes sur des réseaux de neurones sont principalement faites via des tables de données multi-dimensionnelles, appelées Tenseurs (Tensor). Un Tensor à deux dimensions est l’équivalent d’une matrice.

Aujourd’hui, les principaux produits de Google sont basés sur TensorFlow: Gmail, Google Photos, Reconnaissance de voix, etc.

Comment ça marche ?

La particularité de TensorFlow est qu’il représente les calculs sous la forme d’un graphe d’exécution: chaque noeud représente une Operation à réaliser, et chaque lien représente un Tensor. Une Operation peut aller d’une simple addition à une fonction complexe de différenciation matricielle.

tensorboard graph simple

Chaque Operation prend en entrée zéro, un ou plusieurs Tensor, effectue un calcul, et retourne zéro, un ou plusieurs Tensor. Un exemple typique de Tensor est un batch d’images. Un batch d’images est représenté par un Tensor à 4 dimensions: taille du batch (nombre d’images dans le batch), hauteur, largeur et nombre de canaux de représentation (3 pour une image en couleurs représentée en RGB).

La création du graphe est automatiquement gérée par TensorFlow une fois les Tensor et Operation implémentés et instanciés. Cela permet une optimisation et parallélisation du code et de l’exécution lors du lancement.

TensorFlow possède de plus un support très vaste pour la création d’opérations spécifiques au Deep Learning, et il devient donc facile de construire un réseau de neurones et d’utiliser les opérations mathématiques couramment associées pour l’entraîner avec les bons optimiseurs.

Comment les calculs sont-ils effectués ?

La Session

Pour pouvoir exécuter quoi que ce soit, un graphe doit être lancé dans une Session. Une Session place les Operation du graphe dans des devices (CPU ou GPU) et met à disposition des méthodes pour les exécuter. Chaque Session peut avoir ses propres variables et readers, et il est possible d’instancier plusieurs Session afin d’entraîner plusieurs réseaux différents. Le lancement des opérations du graphe se fait via la méthode run() de la Session. Un graphe ne va exécuter les Operation qu’après la création d’une Session.

Ce système d’exécution de graphe est une des propriétés fondamentales de TensorFlow. Cela permet d’éviter de retourner dans le monde Python à chaque étape (contrairement à ce qui est fait dans NumPy) et d’éxécuter toutes les opérations du graphe en une seule fois dans un même backend optimisé.

L’utilisation d’une Session n’est pas ce qu’il y a de plus intuitif et simple, en particulier lorsque l’on utilise des notebooks et que l’on ne souhaite pas lancer le run de la Session à chaque fois que l’on veut tester une étape. Pour s’affranchir de ces contraintes lors d’une utilisation via des notebooks, on peut utiliser à la place une InteractiveSessionqui remplit les mêmes fonctions.

Structure du code

Généralement, le code associé à TensorFlow se divise donc en deux étapes principales:

  • Une phase de construction durant laquelle on décrit et assemble toutes les variables et opérations du graphe
  • Une phase d’exécution qui utilise une Session afin d’exécuter les opérations du graphe

C’est cette séparation qui permet à TensorFlow d’optimiser l’enchaînement des étapes du graphe avant de les exécuter.

Concrètement, ça ressemble à quoi ?

Trêve de théorie, le mieux est de montrer les différents concepts exposés jusque là via un exemple simple représentant un produit entre deux matrices.

NB: Tous les exemples montrés par la suite font référence à la version 1.0 de TensorFlow.

Première étape: La création des variables et des Operation associées.

import tensorflow as tf

# Create a Constant op that produces a 1x2 matrix.
matrix1 = tf.constant([[3., 3.]])

# Create another Constant that produces a 2x1 matrix.
matrix2 = tf.constant([[2.], [2.]])

# Create a matmul op that performs the matrix multiplication of matrix1 by matrix2.
product = tf.matmul(matrix1, matrix2)

Comme on peut le voir, même l’initialisation des données d’entrée se fait via des Operation: des constantvariable ou placeholder. L’Operation tf.constant permet par exemple de définir la création d’une constante via les valeurs qui lui sont fournies. Le résultat de cette Operation est un Tensor de la dimension des données fournies en entrée.

Le produit matriciel se fait ensuite via l’Operation tf.matmul. Le résultat de l’opération, product, est lui aussi un Tensor de la dimension du résultat du produit matriciel.

Pour le moment, il ne s’est rien passé dans le code. Nous avons terminé la phase de construction du graphe, et il faut maintenant passer à la phase d’exécution via une Session.

# Launch the default graph.
sess = tf.Session()

# Call the session run() method to run the matmul op.
result = sess.run(product)
print(result)

# You can call multiple operations at the same time
res_product, res_matrix1 = sess.run([product, matrix1])
print(res_matrix1)

# Close the session
sess.close()

On instancie donc dans un premier temps une Session qui nous permettra d’exécuter le graphe. Cette exécution se fait via la méthode run de la Session, avec en paramètre le nom de l’Operation à exécuter. Si une Operation fait référence à d’autres Operation (devant être déclarées précédemment), celles-ci seront récursivement exécutées jusqu’à ce que tout le graphe des Operations qui en sont dépendantes soit exécuté. La fonction run peut prendre plusieurs Operation en paramètres, afin par exemple de pouvoir évaluer des étapes intermédiaires.

Une fois l’opération terminée et le résultat obtenu, il est important de fermer la Session afin de relâcher les ressources.

Afin d’éviter de fermer la Session, on peut aussi définir les opérations à effectuer dans un bloc « with ». La Session se fermera après l’exécution du bloc.

with tf.Session() as sess:
 result = sess.run(product)
 print(result)

Bien que ces exemples soient assez basiques, ils sont importants car permettent de spécifier les grandes étapes d’implémentation d’un projet avec TensorFlow: Définition des inputs, ajout des Operation nécessaires et exécution du graphe dans une Session.

Les opérations courantes pour gérer les inputs

Constant

Dans l’exemple précédent, nous avons vu l’utilisation de tf.constant pour créer un Tensor à partir d’une valeur que l’on souhaite garder fixe.

# Instanciation of a Constant
const = tf.constant([2., 1.])

Variable

Si on souhaite pouvoir modifier les valeurs d’un Tensor au cours de l’exécution d’un graphe, le mieux est d’utiliser tf.Variable(). Les Variable permettent de maintenir un état durant l’exécution du graphe, avant d’être modifiées pour de prochaines exécutions. On peut par exemple vouloir créer une variable de comptage, initialisée à 0, et qui sera incrémentée au cours de l’exécution du graphe.

# Instanciation of a Variable
counter = tf.Variable(0, name="counter")

On remarquera que l’on peut donner des noms spécifiques à chaque Operation. Cela sera très utile pour s’y retrouver par la suite, notamment lorsque nous utiliserons TensorBoard.

Les Variable doivent être initialisées avant qu’un graphe puisse les utiliser. On utilise pour cela la fonction tf.global_variables_initializer(). L’exemple suivant montre comment incrémenter la valeur d’une Variable dans une boucle au sein d’une Session, la mise à jour de la variable se faisant via l’opération tf.assign().

# Counter Variable definition
counter = tf.Variable(0, name="counter")

# Creation of a constant
one = tf.constant(1)

# Operations to perform in order to increment the variable value
new_value = tf.add(counter, one)
update = tf.assign(counter, new_value)

# Initialize all variables
init_op = tf.global_variables_initializer()

# Increment the value of the variable in a session
with tf.Session() as sess:
 sess.run(init_op)
 for _ in range(5):
  sess.run(update)
  print(sess.run(counter))

Encore une fois, cela semble être beaucoup de travail pour une simple boucle d’incrémentation, mais nous comprendrons vite que la mécanique mise en place ici sera toujours la même, même pour des opérations plus complexes.

Typiquement, une Variable peut être utilisée pour contenir les poids d’un réseau de neurones. Celle-ci peut être instanciée et mise à jour. Nous allons initialiser le Tensor de poids grâce à une Variable avec des valeurs initiales à 0 ou à de petites valeurs aléatoires. Au cours de la phase d’apprentissage, ces poids seront mis à jour à chaque itération de l’optimisation, lors de l’execution du graphe. Au fur et à mesure des itérations, les poids de l’algorithme convergeront vers des poids (localement) optimaux.

# Initialization of a Variable as a Tensor full of zeros
weights = tf.Variable(tf.zeros([image_pixels, num_classes]))

# Initialization of a Variable as a Tensor with small random values
weights = tf.Variable(tf.truncated_normal(shape=[num_pixels, num_classes], stddev=0.1))

La Variable a pour propriété de n’être modifiable que lors de l’exécution d’un graphe, donc dans une Session. Si au contraire, je souhaite alimenter un Tensor avec de nouvelles données à chaque étape, indépendamment d’une mise à jour de mon modèle, il faut utiliser des Placeholder.

Placeholder

Un Placeholder est un Tensor sans valeur spécifique. Sa valeur sera fixée lors du run d’un calcul, potentiellement indépendamment du run en lui-même. Il peut être vu comme une Variable qui ne recevra ses données que plus tard dans l’exécution.

L’utilisation la plus classique d’un Placeholder intervient lorsque l’on souhaite fournir un nouveau batch d’images lors d’une itération de l’entraînement d’un modèle. A chaque itération, on doit avoir de nouvelles images afin de mettre à jour les poids du réseau de neurones pour converger vers de meilleures classifications. Le choix de ces images doit se faire de manière indépendante de l’itération en tant que telle. On utilise donc un Placeholder afin d’alimenter le graphe avec un nouveau batch d’images à chaque itération.

Pour créer un Placeholder, il suffit d’utiliser la méthode tf.placeholder(datatype). Il faut fournir à la méthode le type de données que le Tensor devra recevoir.

# Instanciation of a Placeholder
simple_placeholder = tf.placeholder(tf.float32)

Puisque le Placeholder n’a pas de valeur initiale, on ne peut pas lancer le run d’une Session de la même manière qu’avec une Variable. Il faut alors utiliser un argument supplémentaire à la méthode run de la Session afin d’alimenter le Placeholder avec des données. Cela se fait via l’argument feed_dict auquel on passe un dictionnaire avec les valeurs de tous les Placeholder à alimenter.

L’exemple suivant montre comment réaliser la multiplication entre deux Tensor initialisés avec des Placeholder.

# Instanciation of two Placeholders
input1 = tf.placeholder(tf.float32)
input2 = tf.placeholder(tf.float32)

# Multiplication operation
output = tf.multiply(input1, input2)

# Graph execution, we need to feed the placeholders
with tf.Session() as sess:
 result = sess.run(output, feed_dict={input1: [7.], input2: [2.]})
 print(result)

Dans l’exemple ci-dessus, on voit que les valeurs des Placeholder peuvent être changées comme bon nous semble durant l’exécution du graphe via l’argument feed_dict.

Nous avons maintenant à notre disposition une bonne partie des opérations les plus courantes en TensorFlow. Une fois ces concepts maîtrisés, les prochaines étapes pour la création et l’entraînement d’un réseau de neurones ne seront qu’une succession d’Operation qui seront par la suite exécutées dans une Session.

TensorBoard

Nous nous en rendrons compte très rapidement lorsque nous allons créer des réseaux de neurones, l’enchaînement des opérations peut vite devenir complexe. Nous allons très vite ressentir le besoin de visualiser le graphe créé, ainsi que de contrôler l’évolution de nos phases d’apprentissage (évolution du taux de prédiction, activités des neurones, etc.). Heureusement pour nous, TensorFlow met à disposition un outil, TensorBoard, qui répond à ces besoins. TensorBoard est une réelle force et constitue un vrai élément différenciant de TensorFlow par rapport aux autres frameworks de Deep Learning.

Comment utiliser TensorBoard ?

Pour utiliser TensorBoard, il faut spécifier dans le code quelles sont les opérations dont on souhaite résumer l’activité. Une fois que c’est fait, il reste alors à créer un Summarizer qui va merger toutes les informations calculées. Une fois que le programme est lancé, il suffit de lancer la commande adéquate avec le chemin d’accès vers les logs pour que l’interface graphique associée se lance.

tensorboard --logdir="path/to/logs"

Les modifications à apporter

Afin de pouvoir visualiser l’évolution de certains nœuds du graphe, il faut annoter l’opération correspondante via des Summary Operations. On trouve par exemple tf.summary.scalar (ex: visualisation d’un fonction de coût) ou des tf.summary.histogram (ex: visualisation de la distribution de l’activation des neurones d’une couche).

Reprenons l’exemple de notre compteur, en le modifiant pour ajouter des résumés à afficher dans TensorBoard.

# Counter Variable definition
with tf.name_scope('counter'):
    counter = tf.Variable(1, name="counter")
    tf.summary.scalar('counter', counter)

# Creation of a constant
two_op = tf.constant(2, name="const")

# Operations to perform in order to increment the variable value
new_value = tf.multiply(counter, two_op)
update = tf.assign(counter, new_value)

merged = tf.summary.merge_all()

# Initialize all variables
init_op = tf.global_variables_initializer()

with tf.Session() as sess:
    # Increment the value of the variable in a session
    sess.run(init_op)

    summary_writer = tf.summary.FileWriter("/tmp/nn_test", sess.graph)

    for i in range(5):
        summary, _ = sess.run([merged, update])
        summary_writer.add_summary(summary, i)
        print(sess.run(counter))

Comme on peut le voir dans l’exemple, le résumé des valeurs prises par la variable counter est géré par l’opération tf.summary.scalar(‘counter’, counter). La création de la Variable ainsi que le résumé associé sont encapsulés dans un bloc with tf.name_scope. Cela permet de générer des grandes zones dans TensorBoard, ce qui nous sera très utile lorsque nous aurons beaucoup d’opérations enchainées.

Une fois les summarizers créés, on ajoute une dernière opération chargée de tous les merger: tf.summary.merge_all(). 

L’étape suivante consiste à créer un summary_writer spécifiant où seront écrits les logs qui seront lus par TensorBoardtf.summary.FileWriter(). Lors de l’éxécution du graphe, il suffit alors de rajouter à l’étape de run l’opération de merge, puis d’ajouter le summary au writer créé: summary_writer.add_summary(summary, i).

Ces quelques lignes de code supplémentaires nous permettent de faire appel à TensorBoard en ligne de commande pour lancer l’interface graphique associée. On observe alors plusieurs onglets, notamment « Scalars » et « Graph ».

TensorBoard Tensorflow

 

Conclusion & Next steps

Dans ce premier article, nous avons fait un rapide aperçu du fonctionnement de TensorFlow ainsi que des principales opérations et structures à connaître. Comme nous allons le voir dans les prochains articles, TensorFlow est un outil puissant pour créer et entraîner des réseaux de neurones complexes.

Il faut admettre que ce framework demande une phase initiale d’apprentissage relativement conséquente. Cependant, une fois les principales abstractions ainsi que le concept de graphe et de Session maîtrisés, tout s’enchaîne beaucoup plus simplement.

Dans les prochains articles, nous allons passer à la création d’un réseau de neurones pour la classification d’images. Nous allons commencer par une architecture simple et voir toutes les grandes étapes associées, pour petit à petit la complexifier et voir la souplesse de TensorFlow pour créer de nouvelles architectures.

Rendez-vous au prochain épisode !

NB: Avant de publier le prochain épisode, nous allons vous proposer un résumé du TensorFlow Dev Summit qui a eu lieu le 15 février dernier. Nous reprendrons avec l’épisode 2 de cette série par la suite.

Quelques sources utiles

Yoann Benoit
Yoann est Data Scientist chez Xebia depuis près de deux ans. Il intervient sur de nombreux sujets autour de la Data Science et du Big Data, allant de la collecte, du traitement et de l'analyse des données jusqu'à la mise en production de pipelines complets de Machine Learning. Speaker et rédacteur à la fois sur les concepts et les technologies liées à la Data Science, il travaille principalement avec Python, Scala et Spark. Il intervient de plus en tant que formateur sur l'Analyse de Données et le Machine Learning sur Spark.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *