Aller au contenu principal

Module 1 — Tenseurs, variables et graphes de calcul

Le cours 07 a expliqué ce qu'un réseau calcule. Celui-ci explique avec quoi on le construit. Et tout commence par deux objets qu'on confond volontiers, alors qu'ils ont des rôles opposés.

Un tenseur ne change jamais, une variable est faite pour ça

Un tf.Tensor est immuable. Toute opération sur un tenseur en produit un nouveau, elle ne modifie pas l'original.

import tensorflow as tf

x = tf.constant([[1.0, 2.0], [3.0, 4.0]])
y = x * 2 # nouveau tenseur, x est intact
print(x.shape) # (2, 2)
print(x.dtype) # <dtype: 'float32'>

Un tf.Variable est mutable, et c'est précisément ce qui en fait le support des poids d'un modèle : l'entraînement doit pouvoir les écraser des milliers de fois.

w = tf.Variable([[0.5, -0.2]])
w.assign([[0.7, -0.1]]) # remplace le contenu
w.assign_sub([[0.01, 0.01]]) # soustrait sur place

La règle à retenir : les données sont des tenseurs, les paramètres appris sont des variables. Un w = w - 0.01 * gradient naïf sur une variable la remplace par un tenseur ordinaire et la retire silencieusement de la liste des paramètres entraînables du modèle. L'entraînement continue sans erreur, mais ce poids ne bouge plus.

Trois attributs déterminent tout le reste

Un tenseur se décrit par sa forme, son type et son dispositif.

AttributCe qu'il porteErreur typique
shapedimensions, None pour une taille libreoublier la dimension de lot en tête
dtypefloat32 par défaut, float16 en précision mixtemélanger int32 et float32 dans une opération
deviceprocesseur ou accélérateurcopies implicites qui dominent le temps de calcul

La dimension de tête est presque toujours celle du lot. Un lot de 32 images en 224 par 224 pixels et trois canaux a la forme (32, 224, 224, 3). Quand Keras affiche (None, 224, 224, 3), ce None signifie « n'importe quelle taille de lot », ce qui permet d'entraîner par lots de 32 et de prédire sur une seule image.

La conversion de type ne se fait pas toute seule

TensorFlow refuse d'additionner un int32 et un float32. Contrairement à NumPy, il ne promeut pas les types en silence : il lève une exception. C'est volontaire, car une promotion implicite sur un accélérateur coûte cher et se remarque rarement. Utilisez tf.cast(x, tf.float32) explicitement.

Exécution immédiate contre mode graphe

Par défaut, TensorFlow s'exécute ligne par ligne, comme n'importe quel code Python. C'est l'exécution immédiate : on peut afficher un tenseur, poser un point d'arrêt, inspecter une valeur intermédiaire. Le confort de débogage est total, la vitesse est ordinaire.

Le mode graphe est l'autre régime. TensorFlow analyse la fonction, en construit une représentation sous forme de graphe d'opérations, puis l'optimise : fusion d'opérations adjacentes, élimination des calculs inutiles, parallélisation. Le résultat est plus rapide, et surtout exportable — un graphe se sérialise et se sert en production sans interpréteur Python, ce qui est l'objet du module 10.

Le passage de l'un à l'autre tient à un décorateur :

@tf.function
def etape(x, w):
return tf.matmul(x, w)

Ce que tf.function fait réellement

Voici le point que la plupart des tutoriels passent sous silence, et qui explique la majorité des comportements déroutants.

À la première invocation, tf.function exécute le corps Python pour observer les opérations TensorFlow réalisées, et en déduit le graphe. C'est le traçage. Aux appels suivants, le corps Python n'est plus exécuté : seul le graphe tourne.

Trois conséquences directes :

@tf.function
def bavarde(x):
print("trace Python") # une seule fois, au traçage
tf.print("execution graphe") # a chaque appel
return x * 2

bavarde(tf.constant(1.0)) # affiche les deux lignes
bavarde(tf.constant(2.0)) # affiche seulement la seconde

D'abord, un print Python n'apparaît qu'au traçage, alors que tf.print devient une opération du graphe et s'exécute toujours. Ensuite, un compteur Python incrémenté dans le corps reste figé à la valeur du traçage. Enfin, une boucle for Python sur un nombre fixe d'itérations est dépliée dans le graphe, ce qui peut produire un graphe énorme ; tf.while_loop reste une boucle.

Le second piège est le retraçage. TensorFlow trace un graphe par signature d'entrée. Appeler la fonction avec des formes qui changent à chaque fois déclenche un traçage à chaque appel, et le code devient plus lent qu'en exécution immédiate.

@tf.function(input_signature=[tf.TensorSpec([None, 10], tf.float32)])
def stable(x):
return tf.reduce_sum(x, axis=1)

Fixer une signature avec None sur la dimension de lot force un graphe unique, valable pour toutes les tailles de lot.

GradientTape enregistre pour pouvoir dériver

Le mode graphe ne suffit pas à calculer des dérivées : il faut savoir quelles opérations ont été appliquées et dans quel ordre. C'est le rôle de tf.GradientTape, qui enregistre les opérations impliquant des variables surveillées.

w = tf.Variable(3.0)

with tf.GradientTape() as ruban:
perte = w ** 2

gradient = ruban.gradient(perte, w) # 2 * w = 6.0

Le ruban est consommé par le premier appel à gradient, pour libérer la mémoire. Deux dérivées depuis un même ruban exigent persistent=True, et sa suppression explicite ensuite. Les tf.Variable sont surveillées automatiquement ; un tenseur ordinaire demande un ruban.watch(x) explicite.

C'est cette mécanique que model.fit utilise sous le capot, et qu'on reprend à la main au module 4 quand la boucle standard ne suffit plus.

En résumé

  • Un tenseur est immuable, une variable est mutable : les données sont des tenseurs, les poids appris sont des variables, et écraser une variable par une affectation Python la retire des paramètres entraînables.
  • Forme, type et dispositif décrivent un tenseur ; la dimension de tête est celle du lot, et None y signifie « taille de lot libre ».
  • tf.function trace le corps Python une fois pour en déduire un graphe, puis ne l'exécute plus : les effets de bord Python n'ont lieu qu'au traçage, et une signature d'entrée instable provoque un retraçage coûteux.
  • tf.GradientTape enregistre les opérations pour permettre la dérivation ; il est consommé au premier appel sauf s'il est déclaré persistant.

Module suivant : l'API séquentielle, qui construit un modèle complet en quelques lignes sans jamais manipuler un ruban.