Optimisation en Apprentissage Profond
“De SGD à Adam — les astuces qui permettent aux réseaux profonds de vraiment s'entraîner”
SGD vs Momentum vs Adam vs AdamW, warmup et scheduling cosinus, batch normalization, dropout, gradient clipping, entraînement précision mixte.
Prérequis
Concepts Couverts
∑Formules Clés
SGD + Momentum
Accumuler la vitesse dans la direction du gradient — échappe aux minima peu profonds et amortit les oscillations
Adam
Taux d'apprentissage adaptatif par paramètre : m̂_t = moyenne du gradient corrigée, v̂_t = variance du gradient
Normalisation par Lot
Normalise les activations par mini-lot ; γ,β apprenables restaurent le pouvoir représentationnel
Planning LR en Cosinus
Décroissance douce du taux d'apprentissage de ηmax à ηmin sur T étapes — meilleure que la décroissance par palier
▶Simulation Interactive
Un Bon Modèle avec une Mauvaise Optimisation Ne Vaut Rien
Le même réseau avec SGD vanille, une bonne initialisation et un planning adapté peut surpasser un réseau plus grand entraîné négligemment. Les astuces d'optimisation font la différence entre « ça marche dans l'article » et « ça marche sur ton GPU en production ». L'histoire : les premiers DNN échouaient à cause des gradients évanescents et de la mauvaise initialisation. La percée ImageNet 2012 (AlexNet) utilisait ReLU + dropout + weight decay. Les ResNets (2015) ont ajouté des connexions sautées pour résoudre le flux de gradients à profondeur 100+. Les transformeurs modernes s'entraînent de façon stable à profondeur 1000+ avec une normalisation soignée, un warmup du taux d'apprentissage et un écrêtage du gradient. Chaque astuce a résolu un mode d'échec concret.
Le taux d'apprentissage est le paramètre le plus important. Un taux 10× mal choisi fait souvent la différence entre un modèle qui s'entraîne et un qui diverge — avant même d'essayer autre chose.
Pourquoi Chaque Astuce Existe
**Momentum (μ=0,9) :** La descente de gradient sur un bassin de perte allongé zigzague selon la dimension étroite. Le momentum amortit ces oscillations en moyennant les gradients dans le temps — transformant un zigzag lent en courbe douce vers le minimum. **Adam :** Différents paramètres ont des magnitudes de gradient très différentes. Adam normalise chaque paramètre par sa magnitude historique de gradient — les caractéristiques rares obtiennent des taux d'apprentissage effectifs plus grands. **Batch Normalization :** Le décalage de covariance interne force les couches suivantes à se réajuster à chaque mise à jour des poids. BatchNorm recentre les activations à chaque couche, stabilisant l'entraînement et permettant des taux d'apprentissage 10× plus élevés. **Dropout (p=0,5) :** Met aléatoirement à zéro la moitié des activations pendant l'entraînement — force le réseau à apprendre des représentations redondantes, empêchant la co-adaptation des neurones.
La taille du batch et le taux d'apprentissage sont couplés : doubler la taille du batch a un effet similaire à diviser le taux d'apprentissage par deux. Règle de mise à l'échelle linéaire (Goyal et al. 2017) : mettre à l'échelle lr proportionnellement à la taille du batch, ajouter 5 époques de warmup.
Recette d'Entraînement Moderne pour le Deep Learning
Initialiser les poids : He init pour les couches ReLU (σ=√(2/fan_in)), Xavier pour tanh/sigmoid (σ=√(2/(fan_in+fan_out))).
Choisir l'optimiseur : Adam (β₁=0,9, β₂=0,999, ε=1e-8, lr=3e-4) pour la plupart des tâches. SGD+momentum pour le fine-tuning ImageNet.
Ajouter un warmup du taux d'apprentissage : monter linéairement de 0 à lr cible sur 5% des étapes totales — évite les grandes étapes de gradient avant la stabilisation du modèle.
Cosine annealing (ou ReduceLROnPlateau) : décroître lr en douceur jusqu'à 1e-6. OneCycleLR est une forte alternative.
Écrêtage du gradient (max_norm=1,0) : limiter la norme du gradient avant la mise à jour — essentiel pour RNN, Transformeurs. torch.nn.utils.clip_grad_norm_().
Régularisation : Weight decay (L2, λ=1e-4 à 1e-2). Dropout (p=0,1–0,5). Label smoothing (ε=0,1) pour la classification.
Précision mixte (torch.cuda.amp) : float16 pour le passage avant, float32 pour la perte — 2× de vitesse, 2× d'efficacité mémoire sur les GPU modernes.
Boucle d'Entraînement PyTorch Moderne
import torch import torch.nn as nn import torch.optim as optim from torch.cuda.amp import GradScaler, autocast from torch.utils.data import TensorDataset, DataLoader class="tok-comment"># ── Modèle minimal + chargeur pour la démo ──────────────────────────── class MonModele(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential(nn.Linear(class="tok-num">16, class="tok-num">64), nn.ReLU(), nn.Linear(class="tok-num">64, class="tok-num">10)) def forward(self, x): return self.net(x) X_donnees = torch.randn(class="tok-num">512, class="tok-num">16) y_donnees = torch.randint(class="tok-num">0, class="tok-num">10, (class="tok-num">512,)) chargeur_donnees = DataLoader(TensorDataset(X_donnees, y_donnees), batch_size=class="tok-num">32, shuffle=True) def entrainer_une_epoque(modele, chargeur, optimiseur, reechelonneur, planificateur, peripherique, norme_max=class="tok-num">1.0): modele.train() perte_totale = class="tok-num">0.0 for idx_lot, (X, y) in enumerate(chargeur): X, y = X.to(peripherique), y.to(peripherique) optimiseur.zero_grad(set_to_none=True) class="tok-comment"># plus rapide que zero_grad() class="tok-comment"># ── Passage avant en précision mixte ───────────────────────────────── with autocast(): logits = modele(X) perte = nn.functional.cross_entropy(logits, y, label_smoothing=class="tok-num">0.1) class="tok-comment"># ── Rétropropagation avec écrêtage du gradient ─────────────────────── reechelonneur.scale(perte).backward() reechelonneur.unscale_(optimiseur) torch.nn.utils.clip_grad_norm_(modele.parameters(), norme_max) reechelonneur.step(optimiseur) reechelonneur.update() planificateur.step() class="tok-comment"># par lot pour OneCycleLR perte_totale += perte.item() return perte_totale / len(chargeur) class="tok-comment"># ── Configuration ───────────────────────────────────────────────────────────── peripherique = torch.device(class="tok-str">"cuda" if torch.cuda.is_available() else class="tok-str">"cpu") modele = MonModele().to(peripherique) class="tok-comment"># AdamW (Adam avec weight decay découplé — mieux quclass="tok-str">'Adam+L2) optimiseur = optim.AdamW(modele.parameters(), lr=class="tok-num">3e-4, weight_decay=class="tok-num">1e-2) class="tok-comment"># OneCycle LR : warmup + cosine anneal en un seul planning planificateur = optim.lr_scheduler.OneCycleLR( optimiseur, max_lr=class="tok-num">3e-4, total_steps=class="tok-num">100 * len(chargeur_donnees), class="tok-comment"># époques × étapes_par_époque pct_start=class="tok-num">0.05, class="tok-comment"># class="tok-num">5% de warmup anneal_strategy="cos", ) reechelonneur = GradScaler() class="tok-comment"># pour la précision mixte class="tok-comment"># ── Comparaison des optimiseurs ─────────────────────────────────────────────── class="tok-comment"># SGD + Momentum (fort pour le fine-tuning de CNN pré-entraînés) sgd = optim.SGD(modele.parameters(), lr=class="tok-num">0.01, momentum=class="tok-num">0.9, weight_decay=class="tok-num">1e-4, nesterov=True) class="tok-comment"># Adam (par défaut pour la plupart des tâches) adam = optim.Adam(modele.parameters(), lr=class="tok-num">3e-4, betas=(class="tok-num">0.9, class="tok-num">0.999)) class="tok-comment"># AdamW (weight decay correctement découplé — recommandé par les meilleures pratiques) adamw = optim.AdamW(modele.parameters(), lr=class="tok-num">3e-4, weight_decay=class="tok-num">0.01) class="tok-comment"># ── Exemple de Batch Normalization ──────────────────────────────────────────── class BlocConv(nn.Module): def __init__(self, can_entree, can_sortie): super().__init__() self.bloc = nn.Sequential( nn.Conv2d(can_entree, can_sortie, class="tok-num">3, padding=class="tok-num">1, bias=False), nn.BatchNorm2d(can_sortie), class="tok-comment"># bias=False car BN a son propre β nn.ReLU(inplace=True), ) def forward(self, x): return self.bloc(x) class="tok-comment"># ── Layer Normalization (les Transformeurs préfèrent LayerNorm) ─────────────── class BlocTransformateur(nn.Module): def __init__(self, d_modele, n_tetes): super().__init__() self.attn = nn.MultiheadAttention(d_modele, n_tetes, batch_first=True) self.ff = nn.Sequential(nn.Linear(d_modele, d_modele*class="tok-num">4), nn.GELU(), nn.Linear(d_modele*class="tok-num">4, d_modele)) self.norme1 = nn.LayerNorm(d_modele) class="tok-comment"># pré-norme (plus stable que post-norme) self.norme2 = nn.LayerNorm(d_modele) self.abandon = nn.Dropout(class="tok-num">0.1) def forward(self, x): class="tok-comment"># Architecture pré-norme (utilisée dans GPT-class="tok-num">2+, meilleur flux de gradient) x = x + self.abandon(self.attn(self.norme1(x), self.norme1(x), self.norme1(x))[class="tok-num">0]) x = x + self.abandon(self.ff(self.norme2(x))) return x class="tok-comment"># ── Chercheur de taux d'apprentissage ───────────────────────────────────────── from torch_lr_finder import LRFinder chercheur = LRFinder(modele, optimiseur, nn.CrossEntropyLoss(), device=peripherique) chercheur.range_test(chargeur_donnees, end_lr=class="tok-num">10, num_iter=class="tok-num">100) chercheur.plot() class="tok-comment"># chercher la descente la plus raide — c'est votre max_lr
Modes d'Échec Cachés de BatchNorm
BatchNorm est puissant mais présente plusieurs modes d'échec subtils : (1) **Petites tailles de batch :** BatchNorm calcule des statistiques sur le batch — avec batch_size < 8, les estimations sont trop bruitées. Utiliser GroupNorm (group_size=32) ou LayerNorm à la place. (2) **model.eval() est critique :** En mode éval, BatchNorm utilise les statistiques courantes calculées pendant l'entraînement. Oublier d'appeler model.eval() avant l'inférence cause des prédictions très différentes. (3) **Fine-tuning sur des distributions différentes :** Si vous affinez un modèle pré-entraîné, la moyenne/variance courante peut ne pas correspondre à vos données. Envisager de définir track_running_stats=False ou utiliser un faible taux d'apprentissage pour les couches BN. (4) **RNN :** BatchNorm ne fonctionne pas avec des séquences de longueur variable — utiliser LayerNorm à la place.
La deuxième erreur la plus fréquente en PyTorch (après les mauvaises dimensions de tenseur) est d'oublier model.eval() — BatchNorm et Dropout se comportent différemment en mode train et eval.
?Vérification des Connaissances
La progression est sauvegardée dans votre navigateur — aucun compte requis.
Besoin d'un ingénieur IA ou data scientist ?
Je conçois des modèles ML sur mesure, des agents IA, de la vision par ordinateur et de l'automatisation — de l'idée à la production.