Aller au contenu principal

Module 5 — tf.data : lecture, transformation et préchargement

Un accélérateur inoccupé coûte le même prix qu'un accélérateur qui calcule. Or dans un entraînement mal alimenté, il passe l'essentiel de son temps à attendre les données. Ce module traite de ce goulot, qui est le plus fréquent et le plus rentable à corriger.

Le problème : la famine de l'accélérateur

Sans pipeline, le déroulement est strictement séquentiel. Le processeur lit un lot, le transforme, le transmet ; pendant ce temps l'accélérateur attend. L'accélérateur calcule ; pendant ce temps le processeur attend. Les deux ne travaillent jamais ensemble, et le temps total est la somme des deux.

tf.data construit un pipeline où lecture et calcul se chevauchent. Le temps total devient le maximum des deux au lieu de leur somme. Sur un entraînement où la préparation des données prend 40 % du temps, le gain est immédiat et considérable.

Composer un pipeline

import tensorflow as tf

AUTO = tf.data.AUTOTUNE

jeu = (
tf.data.Dataset.from_tensor_slices((chemins, etiquettes))
.shuffle(10_000)
.map(charger_et_redimensionner, num_parallel_calls=AUTO)
.batch(32)
.prefetch(AUTO)
)

modele.fit(jeu, epochs=20)

Chaque maillon a un rôle précis :

OpérationRôleRemarque
from_tensor_slicesdécoupe en élémentsgarder les chemins, pas les images
shuffle(n)mélange dans un tampon de nn trop petit ne mélange rien
map(f)applique une transformationparalléliser avec num_parallel_calls
batch(k)regroupe en lots de kaprès le mélange, jamais avant
prefetchprépare le lot suivanttoujours en dernier

AUTOTUNE laisse TensorFlow choisir le degré de parallélisme et la profondeur de tampon en mesurant à l'exécution. C'est presque toujours meilleur qu'une valeur fixée à la main, et cela s'adapte à la machine.

L'ordre des opérations change le résultat

Ce n'est pas une question de style : plusieurs permutations donnent des résultats faux ou lents.

shuffle avant batch. Mélanger après le regroupement ne réordonne que les lots, dont le contenu reste identique d'une époque à l'autre. Le modèle voit toujours les mêmes voisinages, et l'effet régularisant du mélange disparaît.

batch avant map quand la transformation est vectorisable. Appliquer une normalisation élément par élément coûte un appel de fonction par exemple. Sur un lot, c'est un seul appel sur un tenseur. Pour une opération purement arithmétique, inverser l'ordre accélère nettement. En revanche un décodage d'image doit rester par élément, chaque fichier étant distinct.

prefetch en dernier, toujours. Placé au milieu, il ne recouvre que la fin du pipeline et laisse l'accélérateur attendre les étapes en amont.

Le tampon de mélange n'est pas le jeu de données

shuffle(1000) remplit un tampon de mille éléments et tire dedans. Sur un jeu de cent mille exemples triés par classe, ce tampon ne contient qu'une seule classe à la fois : le mélange est illusoire et chaque lot est monoclasse. Le modèle diverge sans raison apparente. Prenez un tampon de l'ordre de la taille du jeu, ou mélangez les chemins de fichiers en amont avec shuffle sur une liste Python, ce qui ne coûte rien en mémoire.

Où placer le cache

cache mémorise le résultat des étapes qui le précèdent, et sa position détermine ce qui est réutilisé.

jeu = (
tf.data.Dataset.from_tensor_slices((chemins, etiquettes))
.map(decoder_image, num_parallel_calls=AUTO)
.cache() # apres le decodage, avant l'augmentation
.shuffle(10_000)
.map(augmenter, num_parallel_calls=AUTO)
.batch(32)
.prefetch(AUTO)
)

Le décodage donne toujours le même résultat : le mettre en cache économise un travail identique à chaque époque. L'augmentation, elle, doit produire une variation nouvelle à chaque passage ; la placer avant le cache figerait une seule version augmentée par image et annulerait tout son intérêt.

Le cache en mémoire n'est viable que si le jeu décodé y tient. Sinon, cache("/chemin/fichier") écrit sur disque, ce qui reste bien plus rapide qu'un décodage répété.

Lire depuis des fichiers

Pour des images sur disque, image_dataset_from_directory couvre le cas courant en une ligne, en déduisant les classes des noms de dossiers :

jeu = keras.utils.image_dataset_from_directory(
"donnees/entrainement",
image_size=(224, 224),
batch_size=32,
label_mode="int",
)

Pour un volume important, le format TFRecord devient préférable. Il regroupe les exemples dans peu de gros fichiers séquentiels, ce qui supprime le coût d'ouverture de millions de petits fichiers — souvent le facteur dominant sur un stockage réseau.

jeu = (
tf.data.TFRecordDataset(fichiers, num_parallel_reads=AUTO)
.map(analyser_exemple, num_parallel_calls=AUTO)
.batch(64)
.prefetch(AUTO)
)

Le piège des fonctions Python

Une fonction passée à map est tracée, comme au module 1. Elle doit donc s'exprimer en opérations TensorFlow. Une bibliothèque Python arbitraire n'y a pas sa place.

# ne fonctionne pas : PIL n'est pas une operation TensorFlow
def charger(chemin):
return numpy.array(PIL.Image.open(chemin.numpy()))

Deux issues. La bonne consiste à utiliser les opérations natives, tf.io.read_file puis tf.image.decode_jpeg. L'autre enveloppe le code Python dans tf.py_function, ce qui fonctionne mais sérialise l'exécution sur le verrou global de l'interpréteur : la parallélisation disparaît, et avec elle l'essentiel du bénéfice.

Mesurer avant d'optimiser

Comparez le temps d'une époque avec votre pipeline complet, puis avec jeu.take(1).repeat() qui rejoue un lot déjà en mémoire. Si le second est nettement plus rapide, les données sont le goulot et ce module s'applique. Si les deux durées sont proches, le calcul domine et il faut chercher ailleurs — taille du modèle, précision mixte, ou distribution au module 9.

En résumé

  • Sans pipeline, le temps total est la somme de la préparation et du calcul ; avec prefetch, il devient leur maximum.
  • L'ordre est contraignant : shuffle avant batch, batch avant map pour les transformations vectorisables, et prefetch toujours en dernier.
  • Le tampon de mélange doit être comparable à la taille du jeu, sinon un jeu trié produit des lots monoclasses et un entraînement qui diverge sans raison visible.
  • cache se place après les transformations déterministes et avant l'augmentation, faute de quoi l'augmentation se figerait sur une seule version par exemple.

Module suivant : les rappels, qui permettent d'agir pendant l'entraînement sans le réécrire.