Adaptive methods: AdaGrad, RMSProp, Adam and AdamW
Before this lesson: momentum as a running sum of gradients (lesson 04), minibatch noise (lesson 05), and diagonal preconditioning by feature scaling (lesson 02). Time: about 70 minutes, plus the notebook.
Why this matters #
Adam (Kingma and Ba 2015) and AdamW (Loshchilov and Hutter 2019) train almost every large neural network: language models (GPT-3 and LLaMA used AdamW with ), vision transformers, diffusion models and weather models such as GraphCast. Small details of the update decide whether a run is stable: the value of , the bias correction, and whether weight decay goes through the gradient. Interviewers ask exactly these details, and you need them to debug an unstable run or to resume one correctly. Lesson 07 adds the learning-rate schedules, warmup and clipping that complete the recipe.
Notation. In this lesson and the next one we count steps with , as in the Adam
paper and the PyTorch documentation. are the parameters, the (minibatch) gradient at
step , the learning rate (lr), the weight-decay coefficient. Squares, square roots
and divisions of vectors act per coordinate.
1. Predict first #
2. The picture #
Moving averages remember the recent past #
Momentum (lesson 04) and Adam both use an exponential moving average (EMA):
Unroll it: . The weight of a gradient decays by the factor per step. The weights add up to almost 1, and most of the weight sits on the last steps: about 10 steps for , 1000 steps for . Components that keep their sign add up; components that flip sign cancel.
The zero start is a problem. At , : for this is only a tenth of the gradient. Dividing by fixes this: is the bias-corrected average, and .
Slide and watch the two averages of the noisy gradients (grey dots). The orange EMA without correction starts near 0 and needs about steps to catch up; the aqua corrected average is right from the first step. With jump at t = 50, a large reacts slowly: long memory means smooth but late. The bars show the weight of each past gradient in the last average; the dashed line marks steps back.
, and the first gradient is . What are and the bias-corrected ?
: the zero start pulls the average towards 0. The correction divides by , so . The option swaps the weights and .
One step size per coordinate #
Take . The gradient is . Gradient descent is stable only for (lesson 01), so moves at most 2% of its distance per step. Now divide each coordinate of the step by the size of its own gradient, as Adam does. The factor 100 cancels, and both coordinates move at the same speed. In matrix form the step is with a diagonal matrix : a diagonal preconditioner, like feature scaling in lesson 02, but learned from the gradients during training.
A diagonal preconditioner can only rescale the coordinate axes. Rotate the same valley by : the steep and the flat direction are now diagonal. Both coordinates of the gradient mix the two directions and have similar sizes, so is close to a multiple of the identity, and nothing cancels.
Race four optimizers. Gradient descent (blue), momentum (orange), RMSProp (aqua, dashed) and Adam (aqua, solid) start from the same point (drag it). On the axis-aligned quadratic, Adam and RMSProp go almost straight to the minimum. Switch to rotated 45°: with the same learning rates they lose their advantage and stall, while gradient descent is unchanged. Then try the saddle (Adam escapes in a few steps, because it rescales the tiny gradient along ), Rosenbrock and the double well, and turn on gradient noise. Each optimizer has its own learning-rate slider; Default learning rates resets them.
Adam, with a well-tuned learning rate, quickly solves . You rotate the problem by . What happens?
Adam multiplies the gradient by a diagonal matrix . On the axis-aligned problem cancels the curvature. After a rotation both gradient coordinates have similar sizes, is close to a multiple of , and the scaled Hessian still has condition number about 100. Full-matrix preconditioners (Newton, Shampoo, lessons 10 and 12) can undo rotations.
3. Definitions and main results #
The exponential moving average #
Definition. For , the EMA of a sequence is with . Unrolled:
The weights sum to . Their "average age" is , so the memory is about steps.
Heavy-ball momentum in PyTorch form, (lesson 04), is the same average without the factor : . With the momentum buffer is 10 times the EMA, which is why a constant gradient gives the effective step .
AdaGrad, RMSProp and Adam #
All state starts at 0. Here are the PyTorch rules (without weight decay) and their defaults.
AdaGrad (Duchi, Hazan and Singer 2011; , ):
only grows. With gradients of steady size , , so the step decays like . This is good for convex problems with sparse features (rare features keep large steps), but too fast a decay for long nonconvex training.
RMSProp (Hinton's lecture notes, 2012; , , ):
The sum of AdaGrad becomes an EMA, so old gradients are forgotten and the step does not decay to zero. RMSProp has no bias correction: , so and the first step is in every coordinate.
Adam (Kingma and Ba 2015; , , , ):
Adam is RMSProp plus momentum () plus bias correction. estimates the mean of the gradient, the mean of its square (the "uncentered variance"), so the ratio is like a signal-to-noise ratio per coordinate.
| Method | State per parameter | Bias correction | Effective step for a steady gradient |
|---|---|---|---|
| SGD | none | – | |
| AdaGrad | – | ||
| RMSProp | no | (first step ) | |
| Adam | , | yes |
Proposition 1 (bias correction) #
If the gradients are stationary, and for all , then and . So and are unbiased.
Proof.
- Unroll from : .
- Take expectations: .
- Geometric sum: .
- The same steps with and give . Dividing by removes the bias that the zero start creates.
(Kingma and Ba add a small error term when drifts slowly.)
Why the correction matters most for . With , for small , so is far too small for hundreds of steps. Without any correction, at : and , so the step is about . With the correction, and , so the first step is
size in every coordinate, whatever the scale of the gradient.
Two consequences (Kingma and Ba §2.1).
- Scale invariance. Multiply all gradients by : and both grow by , and the step does not change (as long as is negligible).
- Bounded step. By Jensen's inequality, , so typically . When the gradient sign is noisy, is much smaller than , and the step shrinks automatically.
Type a gradient (or pick an example) and read the first three steps of each method, in units of . SGD copies the gradient: steps of and . RMSProp (light aqua) starts with ; Adam without correction (grey) starts with about ; Adam (aqua) moves by in both coordinates. With (100, 0.01), then flip, the coordinate whose sign flips moves back by only 5% of . Set to see Adam lose its adaptivity.
At the first step . Adam with and the default : what is the step ?
With bias correction and , so the step is : and . is the step without bias correction; is RMSProp's first step.
The roles of and #
. remembers about steps: 1000 for . A long memory gives a smooth estimate, but a slow one: after a sudden rise of the gradient scale, lags behind and the steps exceed for a while. This is one reason for loss spikes in large runs. Many LLM and weather-model recipes use (GPT-3, LLaMA, GraphCast), with a memory of about 20 steps.
. If , it does nothing. If , the update is about : momentum SGD with step . So a large makes Adam less adaptive, and coordinates with gradients below are not rescaled.
You train with Adam and set , while the gradients have size about . What does the optimizer behave like?
Here , so and the update is : an EMA of gradients times a fixed step. The adaptivity is gone, and the steps are about times smaller than with the default .
Weight decay and L2 regularization #
Weight decay shrinks the weights by a fixed factor at every step: , where is the step direction that the optimizer computes from the loss. L2 regularization adds to the loss, so its gradient is added to .
Proposition 2 (L2 equals weight decay for SGD, not for Adam) #
- Plain SGD. The L2 step is : exactly weight decay with the same (PyTorch form). In Loshchilov and Hutter's form, , the two agree for (their Proposition 1).
- Adam. The L2 gradient enters and and is divided by . When is small next to the gradient noise, is about the RMS of the gradient, and the per-step shrinkage is about . Weights with large gradients are decayed less. For any preconditioner that is not a multiple of , no L2 coefficient reproduces weight decay (their Proposition 2).
(With momentum, SGD + L2 and SGD + weight decay are also no longer identical, because PyTorch's L2 term passes through the momentum buffer. The difference is small for small .)
AdamW takes the decay out of the normalization (PyTorch form):
where and are computed from the raw . Every weight shrinks by the same factor
per step. PyTorch's default is . The paper multiplies its decay by the schedule
multiplier, not by the base learning rate; PyTorch multiplies it by lr. In both, the decay follows
the learning-rate schedule.
Two groups of weights, no reason to move. All weights start at 1 and see zero-mean gradient noise: group A with RMS 10 (solid lines), group B with a smaller RMS (dashed). Only the decay moves them on average. With AdamW (aqua) both groups shrink alike, like . With Adam + L2 (magenta), group A barely shrinks and group B shrinks fast, like . Change the RMS of group B and the decay .
For which optimizer is adding to the loss exactly the same as decaying the weights by per step?
For plain SGD the L2 gradient is multiplied by only, which gives . Adaptive methods divide it by , a different factor for every coordinate, so weights with large gradients are decayed less. AdamW applies the decay outside the division.
Adam as sign descent, and its memory cost #
With and , Adam's step is : sign descent. With the usual 's Adam is a smoothed version of it: each coordinate moves by about in the direction of its recent average sign. This view explains its robustness to gradient scale and connects it to Lion (Chen et al. 2023), which uses the sign of a momentum average directly, and to the matrix optimizers of lesson 12.
Adam stores two extra numbers per parameter ( and ). With mixed precision a parameter costs about 16 bytes: fp16 weight and gradient (2 + 2), and fp32 master weight, and (4 + 4 + 4) (Rajbhandari et al. 2020). This optimizer state is what ZeRO and FSDP shard across GPUs.
Adam does not always converge
Reddi, Kale and Kumar (2018, Theorem 3) show that for any constant there is a stochastic convex problem on which Adam does not converge to the optimum. In their example a rare, large gradient points the right way, but it also raises and so is damped; the frequent small gradients point the wrong way and get large steps once has forgotten the large one. Their fix, AMSGrad, uses . In practice, a large and a decaying learning rate avoid the problem, and Adam with these settings is the default. Zhang et al. (2022) show that Adam converges when is large enough for the problem.
Answers to section 1. (1) Both move by about : Adam's first step is in every coordinate ( and with ). (2) The weight with small gradients. Inside Adam the L2 gradient is divided by , which is large for the weight with large gradients, so that weight is barely regularized. AdamW decays both by the same factor.
4. Worked example #
(a) Two Adam steps by hand (defaults, negligible). Gradients , then .
- : and . Corrected: and . Step . Without correction: .
- : and . With and : and . Step .
Gradients that differ by a factor give the same step. The coordinate whose sign flips moves back by only 5% of : the momentum still remembers the first gradient.
(b) RMSProp's first step. , so and the step is . With (the default) this is per coordinate, whatever the gradient.
(c) L2 against decoupled decay, one step. , , , .
- SGD + L2: .
- SGD + weight decay: . Same.
- Adam + L2: the gradient becomes ; the first Adam step is , so . The decay disappeared in the normalization.
- AdamW: .
(d) Many steps. , , zero-mean gradient noise, 20,000 steps.
- AdamW shrinks every weight by per step: .
- Adam + L2 shrinks by about per step: for noise RMS 10, so ; and for RMS 0.1, so .
A simulation with 500 weights per group (notebook, exercise 4) gives mean weights of about and for Adam + L2, and about for both groups with AdamW.
5. In code #
The NumPy function below follows the PyTorch formulas exactly. It reproduces the hand computations of section 4.
import numpy as np
def adam_steps(grads, lr=1e-3, b1=0.9, b2=0.999, eps=1e-8,
wd=0.0, decoupled=False, theta0=0.0, correct=True):
"""Adam / AdamW exactly as in torch.optim; returns the parameters after each step."""
theta = np.full(len(grads[0]), theta0, dtype=float)
m, v, out = np.zeros_like(theta), np.zeros_like(theta), []
for t, g in enumerate(grads, start=1):
g = np.asarray(g, dtype=float)
if decoupled:
theta = theta * (1 - lr * wd) # AdamW: decay outside the normalization
else:
g = g + wd * theta # Adam(weight_decay=...): L2 via the gradient
m = b1 * m + (1 - b1) * g
v = b2 * v + (1 - b2) * g ** 2
m_hat = m / (1 - b1 ** t) if correct else m
v_hat = v / (1 - b2 ** t) if correct else v
theta = theta - lr * m_hat / (np.sqrt(v_hat) + eps)
out.append(theta.copy())
return out
# (a) two Adam steps, in units of lr
th = adam_steps([[100.0, 0.01], [-100.0, 0.01]])
print(np.round(-th[0] / 1e-3, 6), np.round(-(th[1] - th[0]) / 1e-3, 6))
# [1. 0.999999] [-0.052632 0.999999] (0.999999: the effect of eps = 1e-8)
print(np.round(-adam_steps([[100.0, 0.01]], correct=False)[0] / 1e-3, 4))
# [3.1623 3.1622] without bias correction: about 3.16 lr
# (b) RMSProp's first step: v_1 = 0.01 g^2
g = np.array([100.0, 0.01]); v1 = 0.01 * g ** 2
print(g / np.sqrt(v1)) # [10. 10.]
# (c) one step from theta = 2 with g = 1, lr = 0.1, weight decay 0.1
print(2 - 0.1 * (1 + 0.1 * 2)) # SGD + L2: 1.88
print(adam_steps([[1.0]], lr=0.1, wd=0.1, theta0=2.0)[0]) # Adam + L2: [1.9]
print(adam_steps([[1.0]], lr=0.1, wd=0.1, decoupled=True, theta0=2.0)[0]) # AdamW: [1.88]
The same check with PyTorch, where these optimizers are used in practice:
import torch
for name, opt_class in [("SGD + L2", torch.optim.SGD), ("Adam + L2", torch.optim.Adam),
("AdamW", torch.optim.AdamW)]:
p = torch.tensor([2.0], requires_grad=True)
opt = opt_class([p], lr=0.1, weight_decay=0.1)
p.grad = torch.tensor([1.0])
opt.step()
print(f"{name:9s} theta_1 = {p.item():.4f}") # 1.8800, 1.9000, 1.8800
6. Where it shows up #
- Large language models. AdamW with a linear warmup and a cosine or warmup–stable–decay schedule (lesson 07) is the standard pretraining recipe. GPT-3 used , ; LLaMA used AdamW with and weight decay 0.1. Weight decay is usually applied to weight matrices only, not to biases and normalization gains.
- ML for science. GraphCast trained with AdamW, and weight decay 0.1 (Lam et al. 2023). Diffusion models usually train with AdamW; DiT used a constant learning rate of , no weight decay, and an EMA of the weights for sampling. Physics-informed networks often run Adam first and then switch to L-BFGS (lesson 10).
- Distributed training. Adam's state (, ) is twice the size of the model. ZeRO and FSDP shard it across GPUs; 8-bit optimizers store it in low precision.
- Sparse features. AdaGrad's per-feature steps suit problems with rare features, such as embeddings and linear models for click prediction.
- Beyond diagonal. Adam fixes only axis-aligned scaling. Shampoo, SOAP and Muon (lesson 12) precondition whole weight matrices.
7. Common mistakes #
torch.optim.Adam(weight_decay=...)is not AdamW. It adds an L2 term to the gradient. Usetorch.optim.AdamW(recent PyTorch versions also acceptdecoupled_weight_decay=Truein Adam).- Forgetting that PyTorch's AdamW decay is
lr * weight_decayper step. Halving the learning rate also halves the decay. - Bias correction with the wrong . If and restart at 0 but keeps counting (a global step counter), the correction is about 1 and the first steps are several times too large: at the first step, and up to about near step 12 for a constant gradient. Counting from divides by .
- Resuming a run without the optimizer state. , and restart, again comes from a few gradients, and the first steps are about in every coordinate, as at a start without warmup.
- in a different place. The paper's "efficient" form puts next to the uncorrected , so the same number means a different in libraries that use it.
- Expecting Adam to fix any ill-conditioning. A rotated valley stays ill-conditioned for a diagonal preconditioner.
- Comparing optimizers at one learning rate. Each method has its own good range (Adam around to , SGD often 10 to 100 times larger); compare each at its tuned value.
8. Check yourself #
- What is the size of RMSProp's first step (, )? And of AdaGrad's first step?
Answer
RMSProp: , so and the step is . AdaGrad: , so the step is . - Adam sees the same gradient at every step. What is its step at step ?
Answer
and exactly, so , , and the step is at every (up to ). - With constant gradients of size , how does AdaGrad's step change with ? Why is this a
problem for long training runs?
Answer
, so the step is . It keeps shrinking even when the problem would allow large steps, so long nonconvex runs slow down. RMSProp's EMA forgets old gradients and keeps the step near . - Plain SGD with and an L2 coefficient . Which weight decay gives the same
updates, in PyTorch form and in Loshchilov–Hutter form?
Answer
The per-step factor is . PyTorch form : . Loshchilov–Hutter form : . - On from , Adam (exact gradients) keeps at
every step. Why?
Answer
Scale invariance per coordinate. While , the gradient of coordinate 2 is 100 times that of coordinate 1 at every step, so its and its are both 100 times larger, and the two steps are equal (up to ). By induction this holds at every step. - A colleague restarts training from a checkpoint but forgets to load the optimizer state. What
happens in the first steps?
Answer
, and restart at 0. After bias correction the first step is about in every coordinate, even for coordinates that had tiny steps before. This is like a start without warmup and can cause a loss spike. Load the state, or warm up again.
How many steps does an EMA with coefficient remember?
About steps: 10 for , 1000 for .
Write the Adam update with bias correction.
, , , , .
Why does Adam need bias correction?
and start at 0, so and . Dividing by removes this bias; it matters most for with .
What is the size of Adam's first step in each coordinate?
About : the step is , whatever the gradient scale.
What is the size of RMSProp's first step with ?
per coordinate, because and there is no bias correction.
Why does Adam not fix a valley rotated by ?
Adam rescales coordinates with a diagonal matrix. After a rotation both gradient coordinates have similar sizes, so the rescaling is almost uniform and the curvature ratio stays.
What does a large do to Adam?
When the update is about : momentum SGD with step , without per-coordinate adaptivity.
Why is L2 regularization not weight decay inside Adam?
The L2 gradient is divided by , so the effective decay is about : weights with large gradients are decayed less.
Write the AdamW update (PyTorch form).
, with , computed from the raw gradient.
9. Interview angle #
- "Adam or AdamW?" — Adam's
weight_decayis an L2 term through the gradient and is divided by ; AdamW decays every weight by , outside the normalization. Use AdamW. - "Why bias correction?" — and start at 0 and underestimate by the factor ; without correction the first step is about with the default betas.
- "What does Adam actually adapt to?" — The per-coordinate scale of the gradient: it is a diagonal preconditioner , invariant to rescaling each coordinate. It does not adapt to rotations or correlations.
- "Why for LLMs?" — A shorter memory (about 20 steps) lets follow sudden changes of the gradient scale, so the steps do not exceed for long; this reduces instabilities.
- "What is Adam's memory cost?" — Two extra tensors per parameter; about 16 bytes per parameter with mixed precision, which is what ZeRO and FSDP shard.
10. Practice #
The notebook for this lesson runs in Colab. It has five exercises, each with a check cell and a folded solution:
- Implement AdaGrad, RMSProp, Adam and AdamW steps in NumPy, exactly as the PyTorch formulas, and test them against the hand computations of section 4.
- Bias correction on a constant gradient: show that for every , and measure the step without correction.
- The diagonal preconditioner on an axis-aligned and on a rotated quadratic: compute the condition number after rescaling, and run Adam on both.
- Adam + L2 against AdamW: simulate two groups of weights and compare their mean weights after 20,000 steps.
- Train a softmax regression on the scikit-learn digits with SGD, momentum and Adam for the same number of steps, and compare them at several learning rates.
11. Watch and read #
- Kingma and Ba, Adam: A Method for Stochastic Optimization (ICLR 2015): §2 (the algorithm and the step size) and §3 (bias correction).
- Loshchilov and Hutter, Decoupled Weight Decay Regularization (ICLR 2019): §2 (Propositions 1 and 2, AdamW).
- Duchi, Hazan and Singer, Adaptive Subgradient Methods for Online Learning and Stochastic Optimization (JMLR 2011): AdaGrad.
- Reddi, Kale and Kumar, On the Convergence of Adam and Beyond (ICLR 2018): the non-convergence example and AMSGrad.
- PyTorch documentation,
torch.optim: the algorithm box of each optimizer (Adam, AdamW, RMSprop, Adagrad).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.