Chapitre 7 · 15 min
La descente de gradient en direct
Ouvre les optimiseurs, compare SGD, momentum et Adam, puis ajoute l’état de l’optimiseur à ton projet local.
Au chapitre 5, tu as soustrait lr * gradient de chaque et appelé ça . Ça marchait parce que la était gentille et convexe. Au chapitre 6, la même idée entraînait un , mais avec plateaux, oscillations ou divergences.
La raison : la est une façon particulière de naviguer dans un paysage de . Ce n’est pas toujours la meilleure. Il existe toute une famille d’optimiseurs. , l’optimiseur le plus courant en deep learning moderne, est surtout “ avec de la comptabilité en plus”. Cette comptabilité compte.
On utilise une fonction volontairement pénible pour : f(x, y) = ½(x² + 10y²), un bol étiré. La courbure est 10× plus forte en y qu’en x, donc oscille verticalement et avance lentement horizontalement.
1. Le gradient
Pour f(x, y) = ½(x² + 10y²) :
Écris la fonction. Le chapitre la vérifie sur trois points.
Code · JavaScript
Si tout correspond, on peut descendre.
2. Descente de gradient vanilla
Le pas le plus simple : soustraire lr * <Lexikey term="gradient">gradient</Lexikey> à la position courante.
Code · JavaScript
Regarde la trajectoire. Le minimum est (0, 0). L’optimiseur essaie d’y aller, mais la direction y, trop raide, le fait dépasser, revenir, redépasser. La direction x avance à peine.
C’est la raison classique pour laquelle un seul est difficile à régler.
3. Momentum
La correction : garder une vitesse courante. Chaque pas ajoute une version amortie de la vitesse précédente. Les mouvements qui vont dans le même sens se renforcent ; les oscillations se compensent.
Code · JavaScript
Avec à 0.9, la trajectoire doit être plus lisse. À 0.99, elle peut dépasser le minimum et rebondir : trop de vitesse. À 0, tu retrouves vanilla.
4. Adam
lisse la trajectoire, mais garde un même pas pour chaque dimension. maintient une estimation de la moyenne du et de sa variance, dimension par dimension, puis divise le pas par la racine de cette variance.
Defaults : β₁ = 0.9, β₂ = 0.999, ε = 1e-8. Tape la formule.
Code · JavaScript
doit produire la trajectoire la plus directe vers le minimum. C’est pour ça qu’il est le choix par défaut de beaucoup de modèles modernes : il s’adapte aux bizarreries de surface de .
Pourquoi c’est important
Chaque chapitre à partir d’ici utilise un optimiseur. Le du chapitre 6 a été entraîné en vanilla ; si tu as vu la courbe de hachée, c’est pour ça. Les modernes utilisent (ou AdamW) presque exclusivement. Ce n’est pas un dogme — c’est l’observation empirique que la plupart des paysages de profonds ont la pathologie illustrée par notre bol jouet : des directions de courbure très différentes, dans le même réseau, à la même itération.
5. Ajouter l’état d’optimiseur localement
Crée llm/optim.py :
"""Tiny optimizer updates before we delegate tensors to PyTorch."""
from __future__ import annotations
import math
Vector = list[float]
# [1]
def sgd(params: Vector, grads: Vector, lr: float) -> Vector:
return [p - lr * g for p, g in zip(params, grads)]
def momentum(
params: Vector,
grads: Vector,
velocity: Vector,
lr: float,
beta: float = 0.9,
) -> tuple[Vector, Vector]:
# [2]
next_velocity = [beta * v - lr * g for v, g in zip(velocity, grads)]
return [p + v for p, v in zip(params, next_velocity)], next_velocity
# [3]
def adam(
params: Vector,
grads: Vector,
m: Vector,
v: Vector,
step: int,
lr: float = 3e-4,
beta1: float = 0.9,
beta2: float = 0.999,
eps: float = 1e-8,
) -> tuple[Vector, Vector, Vector]:
next_m = [beta1 * mi + (1 - beta1) * g for mi, g in zip(m, grads)]
next_v = [beta2 * vi + (1 - beta2) * g * g for vi, g in zip(v, grads)]
# [4]
m_hat = [mi / (1 - beta1**step) for mi in next_m]
v_hat = [vi / (1 - beta2**step) for vi in next_v]
# [5]
next_params = [
p - lr * mh / (math.sqrt(vh) + eps)
for p, mh, vh in zip(params, m_hat, v_hat)
]
return next_params, next_m, next_v- [1]
sgdn’a aucune mémoire. - [2]
momentumtransporte une vitesse. - [3]
adamtransporte deux mémoires : direction moyennem, taille quadratique moyennev. - [4] corrige le biais des premiers pas.
- [5] divise par
sqrt(v_hat)pour réduire les pas dans les dimensions aux énormes.
PyTorch gérera bientôt cet état. L’écrire une fois rend la boîte noire moins noire.
Recap
- vanilla est l’optimiseur le plus simple. Il galère quand les dimensions ont des courbures très différentes. -
lisse la trajectoire en accumulant une vitesse. Même formule de pas plus un terme de ; un de plus. - divise le pas de chaque dimension par la racine de la variance de son . adaptatif par dimension. La correction de biais compte au démarrage. -
Pourquoi gagne par défaut : dans les vrais réseaux, les dimensions de ont des magnitudes de très différentes. Un
lrglobal est un compromis ; supprime le compromis. - Ton projet local a maintenantllm/optim.py, le même contrat d’optimiseur que celui utilisé partout ensuite.
Pour aller plus loin
- Kingma & Ba, « Adam: A Method for Stochastic Optimization » (2014). Le papier . Court.
- Ruder, « An overview of gradient descent optimization algorithms » — un panorama qui couvre vanilla, , Nesterov, Adagrad, RMSprop, , AdamW.
- Distill, « Why Momentum Really Works » — magnifique tour interactif de la géométrie du .
Prochaine étape : fin de la partie II. La partie III commence avec une tête d’ — le mécanisme qui permet à un de réellement regarder les autres , au lieu d’être limité à un de taille fixe.