Deep Learning 4 — Réseaux convolutifs (2/3)
:::tip Notebook Kaggle Le code complet et exécutable de ce chapitre est sur Kaggle : Ouvrir →
Versions anglaise et chinoise disponibles depuis la page d'accueil. :::
Au chapitre précédent, nous avons entraîné des CNN sur des datasets stockés en CSV (MNIST, CIFAR-10). Pratique pour démarrer, mais en pratique les images viennent presque toujours de fichiers PNG/JPG. Nous découvrons aussi deux techniques essentielles pour stabiliser et régulariser les CNN profonds : Batch Normalization et Dropout.
Pourquoi ce chapitre ?
Vous y apprenez :
- à charger des images depuis le disque avec
ImageFolder; - à accélérer le pipeline de données (
num_workers,pin_memory) ; - la Batch Normalization : pourquoi et où la placer ;
- le Dropout pour réduire le surapprentissage.
ImageFolder : organisation par dossiers
PyTorch attend une organisation très simple : un dossier par classe.
dataset/
├── train/
│ ├── 0/
│ │ ├── img_001.png
│ │ └── img_002.png
│ ├── 1/
│ └── ...
└── test/
├── 0/
└── ...
Le nom du sous-dossier sert de label. torchvision.datasets.ImageFolder fait tout le travail.
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
transform = transforms.Compose([
transforms.Resize((28, 28)),
transforms.ToTensor() # PIL [0,255] → tensor [0,1] (C, H, W)
])
train_dataset = datasets.ImageFolder(root='dataset/train', transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
La magie de ToTensor()
ToTensor() fait trois choses en une :
- convertit la PIL Image en tenseur PyTorch ;
- divise par 255 → valeurs dans ;
- réorganise en CHW depuis HWC.
Conséquence pratique : avec ImageFolder + ToTensor(), on n'a plus jamais besoin de reshape ni de transpose. Les batchs arrivent directement au format que PyTorch attend.
| Source | Reshape nécessaire ? |
|---|---|
| CSV (chapitre 3) | oui |
ImageFolder (ici) | non, déjà BCHW grâce à ToTensor() |
Accélérer le chargement
Quand un entraînement « semble lent » sur GPU, la cause n'est presque jamais le modèle. C'est typiquement le pipeline de données qui rame. Quatre leviers :
num_workers
Par défaut, num_workers=0 : tout est fait dans le processus principal. Avec num_workers > 0, des processus séparés préparent les batchs en parallèle.
DataLoader(train_dataset, batch_size=256, shuffle=True, num_workers=4)
Valeur usuelle : 2-4 sur Kaggle, 8 sur une machine costaud.
pin_memory
Avec pin_memory=True, les tenseurs sont alloués en mémoire pinnée (page-locked), ce qui accélère le transfert vers GPU.
DataLoader(..., pin_memory=True)
for Xb, yb in train_loader:
Xb = Xb.to(device, non_blocking=True)
persistent_workers
Évite de recréer les workers à chaque epoch. Utile quand les epochs sont courtes.
DataLoader(..., num_workers=4, persistent_workers=True)
Récapitulatif
DataLoader(
train_dataset,
batch_size=128,
shuffle=True,
num_workers=4,
persistent_workers=True,
pin_memory=True,
)
L'objectif : que le GPU n'attende jamais un batch.
Batch Normalization
Le problème : à chaque pas de gradient, les poids changent, donc les distributions d'activations en entrée des couches suivantes changent aussi. Le modèle doit constamment se réajuster — entraînement lent et instable.
La solution : normaliser les activations à chaque batch. Pour chaque canal, on recentre (moyenne ≈ 0) et on remet à l'échelle (variance ≈ 1), puis on applique une transformation affine apprise (deux paramètres et par canal).
Bénéfices
- entraînement beaucoup plus stable ;
- convergence plus rapide (on peut souvent doubler ou tripler le
lr) ; - moins de sensibilité à l'initialisation ;
- léger effet régularisant.
Où placer la BN
Le placement standard : après la convolution, avant l'activation.
Conv2d → BatchNorm2d → ReLU
self.conv = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.bn = nn.BatchNorm2d(64) # 64 canaux en sortie de conv
self.relu = nn.ReLU()
PyTorch gère automatiquement le double mode :
- en
model.train(): statistiques calculées sur le batch courant ; - en
model.eval(): statistiques moyennes accumulées pendant l'entraînement.
:::warning Toujours appeler eval() à l'évaluation
Sans model.eval(), le modèle continuerait à utiliser les stats du batch courant — ce qui change ses prédictions. Réflexe à prendre.
:::
Dropout
Le problème : un réseau profond peut surapprendre en s'appuyant trop fort sur certaines combinaisons de neurones, créant des chemins fragiles.
La solution : pendant l'entraînement, désactiver aléatoirement une fraction des neurones à chaque pas (typiquement pour les couches denses, - pour le conv). Le réseau apprend à ne dépendre d'aucun neurone en particulier.
À l'évaluation, le dropout est désactivé automatiquement par model.eval().
Où placer le Dropout
Principalement dans les couches denses (Linear) en fin de réseau.
Linear → ReLU → Dropout
self.fc = nn.Linear(512, 256)
self.dropout = nn.Dropout(p=0.5)
Plus rarement après les couches conv (la BN joue déjà un rôle régularisant à ce niveau).
BN ou Dropout, ou les deux ?
| Technique | Quand l'utiliser |
|---|---|
| BatchNorm seul | Standard moderne dans les CNN. Suffisant dans beaucoup de cas. |
| Dropout seul | Quand BatchNorm pose problème (petits batchs, RNN, certains transformers). |
| Les deux | Compatible. BN dans la partie conv, Dropout dans la partie dense. |
Ces deux techniques sont aussi importantes que ReLU dans la trousse à outils du deep learning.
Architecture CNN moderne typique
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(128 * 4 * 4, 256), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(256, 10),
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
C'est le squelette qu'on retrouve dans la plupart des architectures « maison » modernes.