Module 6 — Optimiseurs et planificateurs de taux d'apprentissage
La boucle du module 5 utilise Adam(lr=1e-3) sans réfléchir. Ce choix
suffit à obtenir un premier résultat, il ne permet pas d'aller plus
loin. Ce module fait deux choses : comparer les optimiseurs disponibles
dans torch.optim, et montrer comment faire varier le taux
d'apprentissage au fil des époques pour gagner plusieurs points
d'exactitude sans changer le modèle.
Qu'est-ce qu'un optimiseur, précisément
Un optimiseur PyTorch est un objet qui détient les paramètres à
mettre à jour et applique une règle à partir des gradients
calculés par backward(). Trois éléments définissent la règle : le
taux d'apprentissage lr, l'inertie accumulée sur les gradients
passés, et la régularisation appliquée aux poids.
import torch
from torch import optim
optimiseur = optim.SGD(modele.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)
optim.SGD(momentum=0) est la descente pure :
Avec momentum, une moyenne mobile du gradient est maintenue, ce qui
amortit les oscillations dans les vallées étroites.
SGD avec momentum : simple, robuste, encore compétitif
Contrairement à ce que suggère la mode, SGD avec momentum reste l'optimiseur de référence pour la vision par ordinateur classique. Correctement réglé, il donne souvent la meilleure généralisation.
optimiseur = optim.SGD(
modele.parameters(),
lr=0.1,
momentum=0.9,
weight_decay=5e-4,
nesterov=True,
)
Le taux d'apprentissage de 0.1 semble énorme comparé à Adam ; il l'est,
et c'est précisément ce qui permet à SGD d'échapper à des minima
étroits. La difficulté est d'accepter la sensibilité au réglage : un
lr mal choisi et l'entraînement diverge.
Adam et AdamW : l'adaptatif qui pardonne beaucoup
Adam maintient une moyenne mobile du gradient et de son carré, et divise la mise à jour par la racine du second. Chaque paramètre a donc son propre taux effectif, ce qui rend le réglage bien plus tolérant.
optimiseur = optim.Adam(modele.parameters(), lr=1e-3)
Le piège d'Adam concerne la régularisation. Dans optim.Adam, le
weight_decay est en réalité ajouté au gradient avant la
normalisation, ce qui couple la régularisation au taux
d'apprentissage adaptatif — et l'affaiblit sur les paramètres à
gradient variable. AdamW corrige ce défaut en appliquant la
diminution des poids séparément, comme une vraie régularisation.
optimiseur = optim.AdamW(modele.parameters(), lr=1e-3, weight_decay=1e-2)
Règle pratique : commencer avec AdamW sur un modèle inconnu, et
n'utiliser SGD que quand on sait ce qu'on fait.
| Situation | Optimiseur | lr de départ |
|---|---|---|
| Prototype rapide, modèle nouveau | AdamW | 1e-3 |
| Vision, ResNet ou similaire, budget d'exploration | SGD momentum 0.9 | 0.1 |
| Transformer, NLP, embeddings | AdamW | 3e-4 |
| Peu de données, régularisation critique | AdamW avec weight_decay=1e-2 | 5e-4 |
Le taux d'apprentissage : ce que fait vraiment un planificateur
Un lr fixe est un compromis unique entre convergence rapide au début
et convergence fine à la fin. Un planificateur — appelé
« scheduler » — change lr au fil des époques ou des pas pour obtenir
les deux.
Le principe est toujours le même : après l'étape de l'optimiseur, on
appelle planificateur.step(). Certains planificateurs se pilotent
par époque, d'autres par pas ; c'est écrit dans leur documentation.
Les quatre planificateurs à connaître
StepLR divise lr par un facteur toutes les step_size époques.
Le plus simple, adapté à un entraînement dont on connaît approximativement
la courbe.
planificateur = optim.lr_scheduler.StepLR(optimiseur, step_size=10, gamma=0.1)
CosineAnnealingLR fait décroître lr selon une demi-cosinusoïde,
de sa valeur initiale à eta_min sur T_max époques. Zéro
hyperparamètre à part la durée. C'est un excellent défaut moderne.
planificateur = optim.lr_scheduler.CosineAnnealingLR(optimiseur, T_max=30)
OneCycleLR implémente la méthode « super-convergence » de Leslie
Smith : lr augmente d'abord jusqu'à un pic, puis décroît en
descente cosinusoïdale. Piloté au pas (pas à l'époque), il permet
souvent d'atteindre la meilleure exactitude en beaucoup moins d'époques.
planificateur = optim.lr_scheduler.OneCycleLR(
optimiseur,
max_lr=0.1,
total_steps=nb_epoques * len(train_loader),
)
ReduceLROnPlateau réagit à une métrique qui stagne. On l'appelle
avec la valeur observée (planificateur.step(perte_val)) et il baisse
lr quand la métrique n'a pas progressé depuis patience époques.
planificateur = optim.lr_scheduler.ReduceLROnPlateau(
optimiseur, mode="min", factor=0.5, patience=3
)
Intégrer le planificateur à la boucle
modele = ReseauFashionMNIST()
optimiseur = optim.AdamW(modele.parameters(), lr=1e-3, weight_decay=1e-4)
planificateur = optim.lr_scheduler.CosineAnnealingLR(optimiseur, T_max=15)
for epoque in range(15):
entrainer_une_epoque(modele, train_loader, criterion, optimiseur)
perte_val, exactitude = evaluer(modele, val_loader, criterion)
planificateur.step() # après l'époque
lr_actuel = optimiseur.param_groups[0]["lr"]
print(f"époque {epoque:2d} | val {perte_val:.4f} | exact {exactitude:.3f} | lr {lr_actuel:.2e}")
Pour OneCycleLR, le step() se met à l'intérieur de la boucle sur
les lots, immédiatement après optimiseur.step(). Se tromper de niveau
transforme un cycle en une constante ; les motifs de perte deviennent
alors plats et incompréhensibles.
planificateur.step() après optimiseur.step()Depuis PyTorch 1.1, l'ordre attendu est : optimiseur.step() puis
planificateur.step(). L'inverser provoque un avertissement et saute
le premier taux prévu. OneCycleLR est pilotée au pas, StepLR,
CosineAnnealingLR et ReduceLROnPlateau à l'époque : lire la
documentation avant.
Trouver un taux d'apprentissage sans deviner
La méthode du balayage de lr proposée par Smith consiste à
augmenter lr de façon exponentielle sur quelques centaines de pas et à
tracer la perte. Le meilleur lr est le point où la courbe descend le
plus fortement, pas le minimum de la courbe.
import math
def balayer_lr(modele, loader, criterion, lr_min=1e-6, lr_max=1.0, nb_pas=100):
optimiseur = optim.SGD(modele.parameters(), lr=lr_min)
facteur = (lr_max / lr_min) ** (1 / nb_pas)
pertes, taux = [], []
for i, (x, y) in enumerate(loader):
if i >= nb_pas:
break
lr = lr_min * (facteur ** i)
for g in optimiseur.param_groups:
g["lr"] = lr
optimiseur.zero_grad()
perte = criterion(modele(x), y)
perte.backward()
optimiseur.step()
pertes.append(perte.item())
taux.append(lr)
return taux, pertes
On lance cette fonction sur un modèle fraîchement initialisé, on
regarde la courbe, et on choisit le lr immédiatement avant que la
perte ne remonte violemment. Cet exercice remplace trois heures d'essais
au hasard.
Groupes de paramètres : traiter différemment plusieurs blocs
Un optimiseur peut recevoir plusieurs groupes de paramètres avec des
lr distincts, indispensable pour l'affinage progressif du module 9.
optimiseur = optim.AdamW([
{"params": modele.reseau[:4].parameters(), "lr": 1e-5}, # blocs figés/affinés
{"params": modele.reseau[4:].parameters(), "lr": 1e-3}, # tête
], weight_decay=1e-4)
Le premier groupe apprend cent fois plus lentement que le second. On retrouve exactement ce mécanisme au module 9 pour affiner le préentraîné en préservant les premières couches.
En résumé
AdamWest un défaut moderne solide ;SGDavec momentum reste gagnant en vision quand on accepte de réglerlrfinement.weight_decaydeAdamest trompeur : sur un modèle sérieux, préférerAdamWqui applique la vraie régularisation par diminution des poids.- Un planificateur fait plus qu'un réglage de
lr:CosineAnnealingLRetOneCycleLRgagnent souvent plusieurs points sans autre changement, à condition d'appelerstep()au bon niveau. - Le balayage de
lrtrouve un ordre de grandeur solide en quelques minutes ; le point choisi est là où la perte descend le plus, pas là où elle est la plus basse.
Le module suivant déplace l'entraînement sur GPU et introduit la précision mixte, deux leviers qui accélèrent l'entraînement d'un facteur deux à cinq sans dégrader la qualité.