ML Learning Hub
Deep Learningintermédiaire

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.

40 min
10 diagrammes
8 Concepts Couverts

Prérequis

Neural Networks

Concepts Couverts

AdamAdamWMomentumBatch NormalizationDropoutLR WarmupCosine ScheduleGradient Clipping

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

Loading visualization…
🎯

Un Bon Modèle avec une Mauvaise Optimisation Ne Vaut Rien

motivation

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

intuition

**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

algorithm
1

Initialiser les poids : He init pour les couches ReLU (σ=√(2/fan_in)), Xavier pour tanh/sigmoid (σ=√(2/(fan_in+fan_out))).

2

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.

3

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.

4

Cosine annealing (ou ReduceLROnPlateau) : décroître lr en douceur jusqu'à 1e-6. OneCycleLR est une forte alternative.

5

É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_().

6

Régularisation : Weight decay (L2, λ=1e-4 à 1e-2). Dropout (p=0,1–0,5). Label smoothing (ε=0,1) pour la classification.

7

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

code
python101 lines
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

pitfall

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.