← Blog
Sphère en céramique verte posée dans un bol en spirale ivoire, sur un fond lavande.
Publié le
2026-11-11

PyTorch Adam : mises à jour, état et weight decay

Dans PyTorch, Adam met à jour les paramètres à partir d’estimations cumulées des gradients et de leurs carrés. Dans une boucle d’entraînement, créez l’optimiseur une seule fois, effacez les gradients au moment prévu, calculez la perte, appelez backward, puis step. L’historique de l’optimiseur influence la mise à jour suivante.

Ce guide présente cinq petits exemples et leurs sorties réelles. Nous avons exécuté 41 vérifications avec Python 3.12.14 et PyTorch 2.15.0+cpu, en comparant les mises à jour à des calculs indépendants. Les tests utilisent des tenseurs float64 sur CPU et désactivent les modes foreach et fused. Ils vérifient le comportement de l’optimiseur, sans mesurer les performances GPU ni la qualité d’un entraînement.

Commencer par une mise à jour Adam

Notre paramètre contient deux valeurs. La somme de leurs carrés donne une perte dont le gradient se calcule facilement : deux fois chaque valeur. Nous utilisons un taux d’apprentissage de 0.1 pour rendre le déplacement visible, avec les valeurs par défaut de betas et d’epsilon. Ce taux sert à l’explication ; il ne constitue pas une recommandation pour votre modèle.

import torch

p = torch.nn.Parameter(torch.tensor([1.0, -2.0], dtype=torch.float64))
opt = torch.optim.Adam([p], lr=0.1, foreach=False, fused=False)
opt.zero_grad(set_to_none=True)
loss = p.square().sum()
loss.backward()
gradient = p.grad.clone()
opt.step()
state = opt.state[p]
print("gradient:", gradient.tolist())
print("parameter:", [round(x, 6) for x in p.detach().tolist()])
print("first moment:", [round(x, 6) for x in state["exp_avg"].tolist()])
print("second moment:", [round(x, 6) for x in state["exp_avg_sq"].tolist()])
print("step:", int(state["step"].item()))
gradient: [2.0, -4.0]
parameter: [0.9, -1.9]
first moment: [0.2, -0.4]
second moment: [0.004, 0.016]
step: 1

Le gradient vaut [2, -4]. Le premier moment enregistré vaut [0.2, -0.4] et le second [0.004, 0.016]. À cette première étape, diviser ces estimations respectivement par 0.1 et 0.001 corrige leur biais initial. Les valeurs corrigées correspondent au gradient et à son carré.

Pour la première composante, le calcul est 1 - 0.1 * 2 / (2 + 1e-8). Pour la seconde, il est -2 + 0.1 * 4 / (4 + 1e-8). L’affichage est arrondi à six décimales ; les tests comparent les valeurs complètes avec une tolérance.

Les deux composantes se déplacent ici d’une quantité presque identique en valeur absolue, malgré des gradients d’amplitudes différentes. Cette observation concerne la première mise à jour, avec des moments initialement nuls. Elle ne signifie pas que chaque mise à jour ultérieure aura une amplitude égale au taux d’apprentissage.

Identifier les réglages utilisés

La référence Adam de PyTorch 2.15 indique les valeurs par défaut lr=0.001, betas=(0.9, 0.999), eps=1e-8 et weight_decay=0. Les coefficients beta contrôlent les estimations cumulées ; epsilon intervient dans le dénominateur de la mise à jour. Ce guide laisse AMSGrad et la maximisation désactivés.

Pour un modèle, transmettez les paramètres à entraîner, généralement via model.parameters(). Créez l’optimiseur hors de la boucle sur les lots afin de conserver son état entre les étapes. Pour modifier le taux pendant l’entraînement, consultez notre guide des ordonnanceurs de taux d’apprentissage PyTorch, en anglais.

Effacer les gradients au moment prévu

L’appel à step n’efface pas le gradient. L’exemple suivant effectue deux appels à backward sur des pertes recalculées, sans modifier le paramètre entre les deux.

import torch

p = torch.nn.Parameter(torch.tensor(2.0, dtype=torch.float64))
opt = torch.optim.Adam([p], lr=0.1, foreach=False, fused=False)
p.square().backward()
first = p.grad.item()
p.square().backward()
accumulated = p.grad.item()
opt.zero_grad(set_to_none=True)
cleared = p.grad is None
p.square().backward()
fresh = p.grad.item()
opt.step()
after_step = p.grad.item()
print("first, accumulated:", first, accumulated)
print("cleared to None:", cleared)
print("fresh, after step:", fresh, after_step)
first, accumulated: 4.0 8.0
cleared to None: True
fresh, after step: 4.0 4.0

Le premier appel produit 4. Le second ajoute encore 4, pour un total de 8. Après l’effacement vers None, un nouvel appel produit à nouveau 4. Cette valeur reste disponible après l’étape de l’optimiseur.

Si chaque lot doit produire sa propre mise à jour, effacez les gradients précédents avant d’accumuler la contribution du lot suivant. Si plusieurs lots doivent contribuer à une seule mise à jour, choisissez cette limite explicitement. Notre guide de l’accumulation des gradients, en anglais, présente la boucle complète. Un gradient résiduel accidentel ne prouve pas que l’accumulation est correctement configurée.

Conserver l’historique d’Adam entre les étapes

Pour isoler l’optimiseur du modèle, les exemples suivants attribuent directement des gradients fixes. Chaque attribution remplace le tenseur précédent. Il s’agit de données de test contrôlées, pas d’un remplacement de backward dans un entraînement ordinaire.

import torch

def run(reset_each_step):
    p = torch.nn.Parameter(torch.tensor(1.0, dtype=torch.float64))
    opt = torch.optim.Adam([p], lr=0.1, foreach=False, fused=False)
    trajectory = []
    for g in [1.0, -1.0, 0.5]:
        if reset_each_step:
            opt = torch.optim.Adam([p], lr=0.1, foreach=False, fused=False)
        p.grad = torch.tensor(g, dtype=torch.float64)
        opt.step()
        trajectory.append(p.item())
    return trajectory, opt.state[p]

persistent, state = run(False)
reset, reset_state = run(True)
print("persistent:", [round(x, 6) for x in persistent])
print("reset:", [round(x, 6) for x in reset])
print("final step counts:", int(state["step"].item()), int(reset_state["step"].item()))
persistent: [0.9, 0.905263, 0.887791]
reset: [0.9, 1.0, 0.9]
final step counts: 3 1

Les deux exécutions reçoivent les gradients 1, -1 et 0.5, dans cet ordre. Elles partent du même paramètre et effectuent la même première mise à jour. À la deuxième étape, l’optimiseur conservé atteint environ 0.905263, tandis que l’optimiseur recréé revient à environ 1.0.

L’optimiseur recréé traite chaque gradient comme sa première observation. Son compteur final vaut un, contre trois pour l’optimiseur conservé. Nos vérifications par récurrence reproduisent les deux trajectoires ainsi que les premier et second moments de l’optimiseur conservé.

Enregistrer uniquement les poids du modèle ne conserve pas cet historique. Pour reprendre un entraînement, consultez notre guide des checkpoints PyTorch, en anglais, qui traite de l’enregistrement et du chargement de l’état de l’optimiseur avec celui du modèle. Recréer Adam avec le même taux ne restitue pas ses anciennes estimations de moments.

Distinguer un gradient nul d’une absence de gradient

Après une première mise à jour avec un gradient de 1, nous effectuons une seconde étape sans nouvel appel à backward. Une branche remplace le gradient existant par None ; l’autre le transforme en un tenseur contenant zéro.

import torch

def second_step(use_none):
    p = torch.nn.Parameter(torch.tensor(1.0, dtype=torch.float64))
    opt = torch.optim.Adam([p], lr=0.1, foreach=False, fused=False)
    p.grad = torch.tensor(1.0, dtype=torch.float64)
    opt.step()
    before = p.item()
    opt.zero_grad(set_to_none=use_none)
    opt.step()
    return before, p.item(), int(opt.state[p]["step"].item())

skipped = second_step(True)
zero = second_step(False)
print("None: before, after, step:", tuple(round(x, 6) for x in skipped))
print("zero: before, after, step:", tuple(round(x, 6) for x in zero))
None: before, after, step: (0.9, 0.9, 1)
zero: before, after, step: (0.9, 0.832994, 2)

Avec None, le paramètre reste à environ 0.9 et son compteur reste à un. Avec un gradient nul, l’historique des moments entraîne une autre mise à jour, jusqu’à environ 0.832994, et le compteur passe à deux. Le weight decay vaut zéro dans les deux branches : il n’explique donc pas le déplacement.

La référence de zero_grad documente cette distinction entre une étape ignorée et une étape exécutée. Ici, set_to_none=False met à zéro un gradient déjà présent. N’en déduisez pas que cet appel alloue des gradients pour les paramètres qui n’en ont jamais reçu.

Comparer Adam et AdamW avec le même weight decay

Utilisez le même taux et le même coefficient de weight decay pour examiner la différence. Dans notre environnement, Adam utilise un coefficient nul par défaut, tandis que AdamW utilise 0.01. Comparer les réglages par défaut modifierait deux éléments à la fois.

import torch

def decay_run(kind):
    p = torch.nn.Parameter(torch.tensor(2.0, dtype=torch.float64))
    options = dict(lr=0.1, weight_decay=0.1, foreach=False, fused=False)
    if kind == "AdamW":
        opt = torch.optim.AdamW([p], **options)
    else:
        opt = torch.optim.Adam(
            [p], decoupled_weight_decay=(kind == "decoupled Adam"), **options
        )
    values = []
    for g in [0.25, -0.5, 0.0]:
        p.grad = torch.tensor(g, dtype=torch.float64)
        opt.step()
        values.append(p.item())
    return values, opt.state[p]

results = {kind: decay_run(kind) for kind in ["Adam", "AdamW", "decoupled Adam"]}
for kind, (values, _) in results.items():
    print(kind + ":", [round(x, 6) for x in values])
Adam: [1.9, 1.887059, 1.856736]
AdamW: [1.88, 1.89781, 1.907132]
decoupled Adam: [1.88, 1.89781, 1.907132]

Nous fixons explicitement weight_decay à 0.1 et fournissons les mêmes trois gradients aux différentes branches. Pour la première mise à jour d’Adam avec pénalisation couplée, le gradient utilisé dans les moments est 0.25 + 0.1 * 2 = 0.45. Le paramètre obtenu vaut environ 1.9.

AdamW applique d’abord une réduction séparée : 2 * (1 - 0.1 * 0.1) = 1.98. Son calcul des moments utilise 0.25 sans ajouter cette pénalisation, puis la mise à jour adaptative ramène le paramètre à environ 1.88. La différence persiste aux deux étapes suivantes.

Avec decoupled_weight_decay=True, Adam reproduit les résultats d’AdamW sur les trois étapes testées, y compris les deux tenseurs de moments. Cette option figure dans l’API PyTorch 2.15 utilisée ici ; vérifiez votre version avant de reprendre le même appel. Nous avons aussi testé un nouveau paramètre AdamW sans gradient : il reste inchangé et aucun état d’optimiseur n’est créé. En lui fournissant un gradient nul, la pénalisation isolée fait passer sa valeur de 2 à 1.98.

Examiner la mise à jour avant de régler un entraînement complet

Si un entraînement avec Adam se comporte de manière inattendue, examinez le gradient avant step, le paramètre après, puis le compteur et les estimations de moments de l’optimiseur. Confirmez que l’effacement intervient au moment prévu et que vous conservez la même instance. Notez explicitement le mode de weight decay pour comparer les exécutions.

Ces exemples établissent le comportement exact de petits jeux de valeurs fixes sur CPU. Ils ne désignent pas le meilleur optimiseur ou taux pour un jeu de données, ne mesurent pas la convergence et ne comparent pas les modes d’exécution CUDA. Pour ces choix, utilisez une mesure de validation adaptée à votre tâche, après avoir vérifié que la séquence de mises à jour correspond à votre intention.

Votre prochaine charge de travail sur Hivenet.

Choisissez une charge de travail d’IA, de calcul ou de stockage à tester sur Hivenet, ou échangez avec notre équipe sur sa mise en production.