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ération | Rôle | Remarque |
|---|---|---|
from_tensor_slices | découpe en éléments | garder les chemins, pas les images |
shuffle(n) | mélange dans un tampon de n | n trop petit ne mélange rien |
map(f) | applique une transformation | paralléliser avec num_parallel_calls |
batch(k) | regroupe en lots de k | après le mélange, jamais avant |
prefetch | prépare le lot suivant | toujours 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.
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.
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 :
shuffleavantbatch,batchavantmappour les transformations vectorisables, etprefetchtoujours 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.
cachese 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.