Aller au contenu principal

Module 4 — Dataset et DataLoader : alimenter l'entraînement

Un modèle bien écrit qui reçoit ses données par un pipeline mal pensé finit sous-entraîné, ou pire, entraîné sur des exemples contaminés par la validation. Ce module met en place l'infrastructure de données qui nourrira la boucle du module 5, avec un accent sur les erreurs qui ne lèvent pas d'exception mais faussent l'évaluation.

Deux abstractions, deux rôles

PyTorch sépare le stockage d'un jeu de données (comment obtenir l'élément numéro i) et son alimentation en lots (mélange, regroupement, parallélisme). Cette séparation permet de réutiliser un même chargeur avec n'importe quel jeu, et de tester chaque brique séparément.

  • Un Dataset répond à deux questions : combien d'éléments (__len__) et quel est l'élément numéro i (__getitem__).
  • Un DataLoader enveloppe un Dataset et gère le tirage (mélange, échantillonneur), le regroupement (batch_size, collate_fn), et le parallélisme (num_workers, pin_memory).

Un Dataset personnalisé, en trente lignes

Pour Fashion-MNIST livré par torchvision, la classe existe déjà (on l'a utilisée au module 1). Pour un jeu maison — un dossier d'images étiquetées par le nom du sous-dossier, un fichier CSV pointant vers des fichiers — on écrit sa propre classe.

from pathlib import Path
from torch.utils.data import Dataset
from PIL import Image
import torch

class MesImages(Dataset):
def __init__(self, racine: str, transform=None):
self.racine = Path(racine)
self.fichiers = sorted(self.racine.glob("*/*.png"))
classes = sorted({f.parent.name for f in self.fichiers})
self.classe_vers_id = {c: i for i, c in enumerate(classes)}
self.transform = transform

def __len__(self) -> int:
return len(self.fichiers)

def __getitem__(self, i: int):
chemin = self.fichiers[i]
image = Image.open(chemin).convert("L") # niveaux de gris
etiquette = self.classe_vers_id[chemin.parent.name]
if self.transform is not None:
image = self.transform(image)
return image, etiquette

__getitem__ renvoie un tuple (entrée, cible) ; DataLoader sait empiler les entrées en un tenseur de forme (N, ...) et les cibles en un long de forme (N,).

Transformations : composables, appliquées par exemple

torchvision.transforms fournit un langage bref pour normaliser, redimensionner, augmenter les images. La composition se lit de haut en bas.

from torchvision import transforms

transform_entrainement = transforms.Compose([
transforms.RandomHorizontalFlip(), # augmentation, entraînement seulement
transforms.ToTensor(), # PIL -> tenseur [0, 1]
transforms.Normalize((0.2860,), (0.3530,)), # moyenne et écart-type de Fashion-MNIST
])

transform_validation = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.2860,), (0.3530,)),
])

Deux transformations distinctes, l'entraînement avec augmentation, la validation sans. C'est un point crucial : appliquer un retournement horizontal aléatoire au calcul de la métrique de validation la rend non reproductible et incomparable d'une exécution à l'autre.

La fuite de normalisation entre entraînement et validation

La moyenne et l'écart-type utilisés dans Normalize doivent être calculés sur le jeu d'entraînement uniquement, puis appliqués aux trois découpages (train, val, test). Calculer ces statistiques sur « toutes les données » avant de découper laisse la validation influencer la préparation, et gonfle discrètement la métrique rapportée.

DataLoader : le vrai chevalier des données

Le DataLoader transforme un Dataset en un itérable de lots.

from torch.utils.data import DataLoader
from torchvision import datasets

train_ds = datasets.FashionMNIST(
"./donnees", train=True, download=True, transform=transform_entrainement
)
test_ds = datasets.FashionMNIST(
"./donnees", train=False, download=True, transform=transform_validation
)

train_loader = DataLoader(
train_ds,
batch_size=64,
shuffle=True,
num_workers=4,
pin_memory=True,
drop_last=True,
)
test_loader = DataLoader(
test_ds, batch_size=256, shuffle=False, num_workers=2, pin_memory=True
)

Cinq réglages méritent d'être compris et non pas recopiés.

  • batch_size définit le nombre d'exemples par lot. Sur portable, 64 est un compromis raisonnable pour Fashion-MNIST ; augmenter au-delà peut dépasser la mémoire GPU sur des images plus grandes.
  • shuffle=True mélange les indices à chaque époque, indispensable à l'entraînement. On le laisse à False en validation, pour un ordre reproductible.
  • num_workers est le nombre de processus fils qui préparent les lots en parallèle. Zéro fait le travail dans le processus principal, ce qui limite le débit ; 4 à 8 est un point de départ courant sur un processeur récent. Sous Windows, le lancement de processus fils exige que le code soit protégé par if __name__ == "__main__":.
  • pin_memory=True épingle la mémoire hôte, ce qui accélère le transfert vers un GPU au module 7. Sans GPU, ça ne coûte quasiment rien.
  • drop_last=True ignore le dernier lot incomplet, utile pour garder une taille de lot constante (indispensable pour certaines formes de BatchNorm où le petit lot fausse les statistiques).

collate_fn : personnaliser l'empilement

Par défaut, DataLoader empile les entrées avec torch.stack. Cela suppose que tous les exemples ont la même forme. Pour des séquences de longueurs différentes — textes, spectrogrammes, séries — il faut fournir sa propre collate_fn, qui reçoit une liste d'exemples et renvoie le lot.

def collate_padder(exemples):
# exemples : liste de (tenseur 1D de longueur variable, étiquette)
longueurs = torch.tensor([e[0].numel() for e in exemples])
max_L = int(longueurs.max())
lot = torch.zeros(len(exemples), max_L)
for i, (t, _) in enumerate(exemples):
lot[i, : t.numel()] = t
cibles = torch.tensor([e[1] for e in exemples])
return lot, cibles, longueurs

Pour Fashion-MNIST toutes les images font 28 par 28, on garde le collate_fn par défaut.

Découpage train, validation, test — dans cet ordre

Un jeu de données réel se découpe en trois sous-jeux avant tout, et le test ne se regarde qu'à la fin. torch.utils.data.random_split fait le travail proprement.

from torch.utils.data import random_split

train_val = datasets.FashionMNIST(
"./donnees", train=True, download=True, transform=transform_entrainement
)
n_val = 6000
n_train = len(train_val) - n_val
train_ds, val_ds = random_split(
train_val, [n_train, n_val], generator=torch.Generator().manual_seed(42)
)

Le generator avec une graine fixe rend le découpage reproductible ; un même code donnera le même train, la même validation, à chaque exécution.

La validation doit imiter le déploiement

Toute transformation qui ne serait pas appliquée en production ne devrait pas l'être en validation. C'est pourquoi le retournement horizontal aléatoire, l'ajout de bruit, la découpe aléatoire (RandomCrop) restent à l'entraînement. Si la mesure ne ressemble pas à la réalité, c'est qu'elle mesure autre chose.

Vérifier un lot avant d'entraîner

Encore une inspection de trente secondes qui rattrape bien des surprises.

lot_images, lot_etiquettes = next(iter(train_loader))
print(lot_images.shape, lot_images.dtype) # torch.Size([64, 1, 28, 28]) torch.float32
print(lot_etiquettes.shape, lot_etiquettes.dtype) # torch.Size([64]) torch.int64
print(lot_images.min().item(), lot_images.max().item()) # bornes après normalisation

Un lot où toutes les étiquettes valent la même valeur trahit un mélange mal fait — souvent un shuffle=False oublié, parfois un tampon mémoire trop petit. Une valeur maximale au-dessus de 5 signale une normalisation manquante.

En résumé

  • Dataset répond à __len__ et __getitem__ ; DataLoader gère lots, mélange et parallélisme. Cette séparation permet de tester chaque brique isolément.
  • Deux transform distinctes : augmentation à l'entraînement, sans augmentation en validation. La statistique de Normalize se calcule sur l'entraînement uniquement.
  • num_workers > 0 accélère le pipeline mais exige if __name__ == "__main__": sous Windows ; pin_memory=True accélère le transfert vers GPU.
  • Découper train, val, test avant tout avec random_split et une graine fixe ; le test ne se regarde qu'à la fin, jamais pour choisir un hyperparamètre.

Le module suivant assemble tenseurs, nn.Module et DataLoader dans une boucle d'entraînement complète, la première version end-to-end du fil rouge Fashion-MNIST.