Gradient descent says which way. Optimizers say how far.
On a long, narrow ravine, the gradient points across the valley, not along it. Momentum remembers which way you were going; RMSProp rescales each parameter by its own recent gradients; Adam does both and corrects its cold start; AdamW keeps the weight decay out of the adaptive machinery. Five rules, one job: turn a gradient into a step.
The gradient is a compass. The step size is the terrain.
w ← w − lr·gradient is the whole of vanilla gradient descent, and it fails in three predictable ways: it oscillates across ravines, it gives every parameter the same step size, and it crawls through flat regions. On the lesson's ravine the two parameter gradients at one point are 0.1 and 10 — the same lr is 100× wrong for at least one of them.
∂L/∂x = 0.1 · ∂L/∂y = 10 at the same point02 / MEMORY AND SCALE
Momentum keeps a velocity v ← βv + g, so flipping gradients cancel and consistent ones stack. RMSProp keeps a memory of squared gradients s ← βs + (1−β)g², so each parameter's step is lr × (g/RMS(g)) — a ratio that ignores the gradient's size. Adam does both, then divides the cold start out with bias correction. AdamW applies the weight decay outside the whole arrangement.
w ← w − lr·m̂/(√v̂+ε) − lr·λ·w03 / DECOUPLE, SCHEDULE, CHOOSE
Keep the decay out, put the lr on a schedule, match the model.
In Adam, L2 regularization and weight decay stop being the same algorithm — the adaptive denominator scales the penalty per parameter. AdamW decouples it: shrink every weight by the same fraction lr·λ. Then put the learning rate on warmup + cosine decay, and start from the field-tested recipe for your architecture: AdamW for transformers, SGD + momentum for CNNs, β₁ = 0.5 for GANs.
w ← w·(1 − lr·λ) then the adaptive step
MENTAL MODEL IN ONE SENTENCE
An optimizer is a rule for turning a gradient into a step: SGD multiplies it by lr; momentum smooths it with history; RMSProp divides it by its own recent size; Adam does both and corrects the cold start; AdamW does all of that with the weight decay kept safely outside.
By the end you will be able to take one SGD step and one momentum step by hand on a quadratic; explain why flipping gradients cancel in a running velocity; compute AdaGrad’s 1/√t stall and RMSProp’s equalized step size for two parameters 100× apart; walk two Adam steps with bias correction and say why the uncorrected first step is 3.16× too large; explain what “decoupled” weight decay buys; and pick a defensible optimizer, learning rate and schedule for transformers, CNNs, GANs and fine-tuning.
01
DIRECTION IS NOT ENOUGH
Gradient descent is a compass. It is not a GPS.
You computed the gradients. You know weight #4,721 should decrease by 0.003 to reduce the loss. But 0.003 in what units? Scaled by what? And should step 1,000 move as much as step 1? The gradient gives a direction; only the optimizer decides how far to walk.
The whole idea fits in one line: w ← w − lr · gradient. The gradient is the direction of steepest ascent, so subtract it and you walk downhill. The learning ratelr is the step size, and in vanilla gradient descent it is the only knob there is. That one line works — the source builds its first optimizer from it in a handful of Python lines — but it hides three problems that make real neural networks painful.
Start with the source’s own demo, because it shows what “it works” looks like when the landscape is friendly. Minimise f(w) = (w − 3)² from w = 10 with lr = 0.1. The gradient is 2(w − 3), so every step shrinks the distance to the target by exactly 20%:
step w gradient loss
0 10.0000 14.0000 49.0000
1 8.6000 11.2000 31.3600
2 7.4800 8.9600 20.0704
3 6.5840 7.1680 12.8451
5 5.2938 4.5875 5.2613
10 3.7516 1.5032 0.5649
20 3.0807 0.1614 0.0065
closed form: w(t) − 3 = 7 × 0.8ᵗ
loss(t) = 49 × 0.64ᵗ
one step is lr × gradient = 0.1 × 14 = 1.4 units of w
Nothing oscillates, nothing stalls, and 20 steps take the loss from 49 to 0.0065. This is the optimistic case: a well-conditioned bowl where one step size fits every direction. Neural networks are not bowls. Their loss landscapes are ravines — long, narrow valleys where the gradient points across the valley (steep) rather than along it (shallow). Use the lesson’s standard two-parameter ravine as a stand-in:
L(x, y) = ½(0.1·x² + 10·y²) a ravine, condition number 100
at the starting point (1, 1):
∂L/∂x = 0.1 × 1 = 0.1 shallow direction
∂L/∂y = 10 × 1 = 10 steep direction — 100× larger
the same lr is therefore ~100× too small for y and ~100× too large for x.
step factors per step: steep 1 − lr·10 · shallow 1 − lr·0.1
The learning-rate sweep on a ravine
One knob, four fates. Drag the learning rate and watch where vanilla gradient descent walks: a crawl along the valley floor, a steady descent, a ringing zig-zag, or a blow-up.
lr 0.0800 · 200 steps · verdict: steady descent
loss now 0.002012
best loss 0.002012
|x| now 0.2006
|y| now 0.0000
steep-axis factor 1 − lr·b = 0.2000
shallow-axis factor 1 − lr·a = 0.9920
stability limit for SGD: lr < 2/b = 0.200
no oscillation, but progress along the shallow axis is only a few percent per step.
The source’s three-way diagram, made draggable: too high diverges, too low wastes compute, and in between the pace is set by the shallow direction — the one the gradient barely describes.
Problem 1 — oscillation. In the ravine, gradient descent bounces back and forth across the narrow dimension while making tiny progress along the useful one. The loss drops fast, then plateaus — not because the model converged, but because most of every step is being spent on a direction that keeps flipping sign. The lab above shows it at lr = 0.16: the path crosses the valley over fifty times.
Problem 2 — one learning rate for all parameters is wrong. Some weights need large updates; others are already near their optimal value and need tiny ones. At the start of the ravine the two gradients differ by 100×, so any single lr is wrong for at least one of them. A learning rate that works for the steep direction destroys the shallow one, and vice versa.
Problem 3 — saddle points and flat regions. In high dimensions the landscape has vast near-flat regions where the gradient is almost zero. Vanilla SGD crawls through them at the speed of the gradient — effectively zero — and the run looks stuck when it is merely in a flat neighborhood with useful descent on the other side. Nothing in the vanilla update remembers momentum from before the plateau.
Worked arithmetic — how the factors compound
Both examples have closed forms, which makes the failure modes easy to count rather than describe.
1-D bowl, lr = 0.1, curvature f″ = 2:
factor per step = 1 − lr·f″ = 1 − 0.2 = 0.8
after 10 steps: 0.8¹⁰ = 0.1074 loss multiplier 0.1074² = 0.0115
after 20 steps: 0.8²⁰ = 0.0115 loss multiplier 1.33×10⁻⁴
loss(20) = 49 × 1.33×10⁻⁴ = 0.0065 ✓ matches the table
ravine, lr = 0.16:
steep factor 1 − 0.16×10 = −0.6 → sign flips, |factor| < 1: rings
shallow factor 1 − 0.16×0.1 = 0.984 → 1.6% progress per step
after 200 steps: 0.984²⁰⁰ = 0.0397 → x ≈ 0.04, loss ≈ 8×10⁻⁵ — if the
ringing dies. At lr = 0.21 the steep factor is 1 − 2.1 = −1.1: each
crossing grows 10%, and after 200 steps y ≈ 1.1²⁰⁰ ≈ 1.9×10⁸.
The steep factor 1 − lr·b is the whole stability story: positive means it decays, negative with magnitude below 1 means it oscillates while decaying, and magnitude above 1 means the run diverges. SGD’s stability limit on this surface is lr < 2/b = 0.2.
Quick check
Your loss drops quickly for 30 steps, then flattens while the parameters keep moving every step. On the ravine surface, what is the most likely story?
02
MEMORY IN THE UPDATE
The ball remembers where it was going.
Instead of stepping by the gradient alone, momentum keeps a running velocity and steps by that. Gradients that point the same way accumulate; gradients that flip sign cancel. The zig-zag dies and the useful direction accelerates — the ball-rolling-downhill analogy is overused, but it is accurate.
The update gains one extra line and one extra variable:
v ← β · v + gradient accumulate a velocity
w ← w − lr · v step by the velocity, not the gradient
β = 0.9 → memory horizon 1/(1 − β) = 10 gradients
β = 0.99 → horizon 100 gradients
v starts at 0, so the very first step is exactly the SGD step.
Why it fixes oscillation: in the ravine, the across component of the gradient flips sign on every crossing, so those contributions arrive with alternating signs and cancel in the running sum. The along component keeps the same sign, so its contributions stack. The result is the same smoothing that a low-pass filter gives a noisy signal, applied one coordinate at a time. And it is the reason momentum can tolerate a larger learning rate than SGD: its stability limit is lr < 2(1 + β)/b instead of 2/b.
steady state of the velocity on a constant gradient g:
v → g / (1 − β) = 10g for β = 0.9 — the velocity is ~10× the
gradient, which is why momentum usually
wants its own, smaller learning rate
steady state on an alternating +g, −g, +g, … sequence:
v → ± g / (1 + β) = ±0.526g
first five velocities, constant g = 0.1:
t=1 0.1000 t=2 0.1900 t=3 0.2710
t=4 0.3439 t=5 0.4095 … → 1.0000
first five velocities, alternating +10, −10:
10.0000 → −1.0000 → 9.1000 → −1.8100 → 8.3710 … → ±5.2632
the useful direction ends up (1.0/0.1) ÷ (5.26/10) ≈ 19× stronger
relative to the flipping one — that is the entire trick.
Worked example A — one SGD step vs one momentum step
Same quadratic as chapter 01: f(w) = (w − 3)², w₀ = 10, lr = 0.1, β = 0.9.
STEP 1 (v starts at 0)
gradient 2(10 − 3) = 14
SGD w = 10 − 0.1×14 = 8.6000
momentum v = 0.9×0 + 14 = 14 → w = 10 − 0.1×14 = 8.6000
identical: a cold velocity has no history yet
STEP 2
SGD gradient 2(8.6 − 3) = 11.2 → w = 8.6 − 1.12 = 7.4800
momentum v = 0.9×14 + 11.2 = 23.8 → w = 8.6 − 2.38 = 6.2200
STEP 3
SGD gradient 2(7.48 − 3) = 8.96 → w = 7.48 − 0.896 = 6.5840
momentum v = 0.9×23.8 + 6.44 = 27.86 → w = 6.22 − 2.786 = 3.4340
STEP 4 (the honest part)
momentum v = 0.9×27.86 + 0.868 = 25.942 → w = 3.434 − 2.594 = 0.8398
it sailed past the target w = 3: on a symmetric bowl, an un-tuned
velocity overshoots. Momentum is not "SGD but faster"; it is a different
dynamical system, and the ravine in the lab below is where it pays.
That is exactly why the lab below defaults to a learning rate above SGD’s stability limit. At lr = 0.22 on the ravine, plain SGD’s steep factor is 1 − 2.2 = −1.2 and its loss after 80 steps is 2.3×10¹³; momentum with β = 0.8 stays inside its own limit (0.22 < 2(1.8)/10 = 0.36) and finishes at 2.0×10⁻⁷. Tuning matters: momentum is not automatically faster at any step size.
Zig-zag versus velocity
Both paths use the same learning rate. The steep direction flips sign every crossing; momentum’s velocity adds those flips together and cancels them, so the across-valley ringing dies while the along-valley direction accumulates.
lr 0.220 · β 0.80 · horizon 1/(1−β) = 5.0 gradients
stability limits (b = 10)
SGD lr < 2/b = 0.200 ✗ unstable
momentum lr < 2(1+β)/b = 0.360 ✓ stable
at step 80
SGD loss 2.33×10^13 never below 10⁻³ in 80 steps
momentum loss 2.04×10^-7 loss < 10⁻³ at step 30
steep-axis sign flips
SGD 79 momentum 45
max |y| reached: SGD 2160228.462 · momentum 1.200
Momentum does not win at every learning rate — on a symmetric bowl it can overshoot too. What it changes is the stability limit and the fate of flipping gradients. Tune β and the shared lr, and watch the red path die while the blue one keeps going.
Nesterov momentum — look before you leap
Standard momentum evaluates the gradient where you are. Nesterov momentum evaluates it at the lookahead position, where the velocity is about to carry you:
v ← β · v + gradient(w − lr·β·v)
w ← w − lr · v
on the quadratic, w₀ = 10, lr = 0.1, β = 0.9:
t=1 lookahead 10.0000, g = 14.0000 w = 8.6000 v = 14.0000
t=2 lookahead 7.3400, g = 8.6800 w = 6.4720 v = 21.2800
t=3 lookahead 4.5568, g = 3.1136 w = 4.2454 v = 22.2656
t=4 lookahead 2.2415, g = −1.5170 w = 2.3932 v = 18.5221
standard momentum at the same settings: w = 8.60 → 6.22 → 3.43 → 0.84
Nesterov sees the overshoot coming and brakes in advance: 6.47 → 4.25 → 2.39.
The source's exercise asks you to implement it and compare on the circle
dataset — on smooth problems it is usually the better-behaved of the two.
Quick check
Why does momentum dampen the across-valley oscillation instead of amplifying it?
03
ONE STEP SIZE PER PARAMETER
Divide every gradient by its own recent size.
The ravine gave one parameter gradients 100× larger than the other. Adaptive methods fix that by keeping a per-parameter memory of how big that parameter’s gradients have been lately, and dividing by it. A weight that always gets huge updates slows down; a weight that rarely moves speeds up.
The first attempt was AdaGrad (2011): keep a running sum of every squared gradient a parameter has ever seen, and divide the update by its square root.
s ← s + g² accumulate all history, forever
w ← w − lr · g / (√s + ε) scale the step by 1/√(history)
ε = 1e-8 keeps a parameter that has never moved from dividing by zero.
Plain English: s measures how loud this parameter’s gradients have been. If they have been loud, √s is large, and the same gradient produces a smaller step. If they have been quiet, the step is amplified. The problem is the word forever: with a constant gradient of size g, s grows like t·g², so the effective step decays like 1/√t and training eventually stalls. AdaGrad worked well on sparse problems — exactly where each parameter gets rare, large updates — but on dense networks it grinds to a halt.
RMSProp fixes the stalling by making the memory decay. Instead of a lifetime sum, s is an exponential moving average of recent squared gradients:
s ← β · s + (1 − β) · g² decaying average of recent g²s
w ← w − lr · g / (√s + ε) divide by the recent RMS
β = 0.9 → a memory of about the last 1/(1−β) = 10 gradients
this is the same moving-average machinery as momentum, applied to g²
instead of g. Hinton proposed it in a Coursera lecture and never formally
published it — the most useful unpublished optimizer in the field.
AdaGrad accumulates, RMSProp forgets
Two parameters with gradients that differ 100×: one sees 10, the other 0.1. Adaptive optimizers divide each gradient by its recent root mean square, so both parameters end up stepping at roughly the same rate.
effective step per parameter, t = 1 / 10 / 100 / 1000
AdaGrad 0.1000 0.0316 0.0100 0.0032
RMSProp 0.3162 0.1239 0.1000 0.1000
current |g| = 10 (lr 0.100, β 0.90)
parameter A (|g| = 10): step at t=1 0.1000 (AdaGrad) · 0.3162 (RMSProp)
parameter B (|g| = 0.1): step at t=1 0.1000 (AdaGrad) · 0.3162 (RMSProp)
AdaGrad step at t: lr / √t · (|g|/|g|) — it keeps every square forever and stalls.
RMSProp step: lr / √(1−βᵗ) → lr — it forgets, so it can keep moving.
The curves are identical for g = 10 and g = 0.1: the scale cancels.
RMSProp's first step is 3.16× lr — a cold start Adam fixes with bias correction.
The source calls RMSProp “the first per-parameter adaptive learning rate method that actually worked.” The number 1e-8 hiding in the denominator is epsilon — it keeps a parameter that has never seen a gradient from dividing by zero.
Two numbers make the mechanism concrete. First, the effective step size with a constant gradient:
lr = 0.1, constant gradient
step at t t=1 t=10 t=100 t=1000
AdaGrad 0.1000 0.0316 0.0100 0.00316 ← 1/√t, stalling
RMSProp 0.3162 0.1239 0.1000 0.10000 ← settles at lr
AdaGrad's step is lr·g/(√(t·g²)) = lr/√t.
RMSProp's step is lr·g/√(g²(1−βᵗ)) = lr/√(1−βᵗ), which → lr.
Second, the point of the whole chapter — the gradient's scale cancels.
Parameter A sees |g| = 10, parameter B sees |g| = 0.1, 100× apart:
step at t t=1 t=5 t=10 t=20
A (|g|=10) 0.31623 0.15627 0.12391 0.10670
B (|g|=0.1) 0.31623 0.15627 0.12391 0.10670 identical
Both parameters move at the same rate because each is divided by its own
recent RMS: the step becomes lr × (g / RMS(g)) — a ratio, not a magnitude.
Notice the cold start in that first row: RMSProp’s very first step is 0.3162, not 0.1 — more than three times too big — because s began at zero and the denominator has not warmed up. That is not a detail to shrug at; it is precisely the bias the next chapter’s optimizer corrects explicitly. The number 3.16 is 1/√(1 − 0.9), and it is why Adam divides by (1 − βᵗ).
Simplified teaching model: both panels use a constant gradient per parameter, so the numbers are closed-form and checkable. Real gradients fluctuate; the qualitative behavior — AdaGrad decaying, RMSProp settling, the scale cancelling — is what survives, and the ε term matters most exactly when a parameter goes quiet.
04
BOTH IDEAS AT ONCE
Adam keeps two averages. Then corrects their cold start.
Momentum smooths the gradient; RMSProp rescales it. Adam does both, with one running average per parameter for the mean gradient and one for the mean squared gradient — plus a small correction that most explanations skip, and which is why its first step is not wildly oversized.
Adam (Kingma & Ba, 2014) maintains two exponential moving averages for every parameter. The first moment m is momentum’s job — “which way have I been drifting?” The second moment v is RMSProp’s job — “how big have my gradients been lately?”
m ← β₁·m + (1 − β₁)·g first moment: mean gradient
v ← β₂·v + (1 − β₂)·g² second moment: mean squared gradient
m̂ = m / (1 − β₁ᵗ) bias-corrected mean
v̂ = v / (1 − β₂ᵗ) bias-corrected variance
w ← w − lr · m̂ / (√v̂ + ε)
defaults: lr = 0.001, β₁ = 0.9, β₂ = 0.999, ε = 1e-8
1/(1−β₁) = 10 → m remembers ~10 gradients
1/(1−β₂) = 1000 → v remembers ~1000 gradients
Why bias correction exists. Both averages start at zero, so early values are dragged toward zero by their own initial condition. At step 1, m = (1 − β₁)·g = 0.1g — ten times too small. Dividing by (1 − β₁¹) = 0.1 recovers exactly g. The same happens to v, except its cold-start bias is far larger: v = (1 − β₂)g² = 0.001g² is a thousand times too small, and dividing by (1 − β₂¹) = 0.001 recovers g².
numeric check inside the derivation — step 1, g = 1:
raw m = 0.1 v = 0.001
correct m̂ = 0.1/0.1 = 1.000000 v̂ = 0.001/0.001 = 1.000000
√v̂ = 1.000000 (not √0.001 = 0.031623)
uncorrected step lr·0.1/√0.001 = lr·3.162278
corrected step lr·1.0/√1.0 = lr·1.000000
the cold start would overshoot the first step by 3.16× — in the wrong
direction from the "too small" intuition, because v is biased far more
than m is.
Worked example B — two Adam steps with every number
Defaults, lr = 0.001, and a shrinking gradient: g₁ = 1.0, g₂ = 0.5. Every value below is reproduced live by the stepper lab.
That last line is the property to internalise: for a consistent gradient direction, Adam’s step size is approximately lr no matter the gradient’s magnitude. Feed it g = 100 instead of g = 1 and step 1 is still 0.001: m̂ = 100, √v̂ = 100, the ratio is 1. This is what makes Adam robust to badly scaled weights — and what makes it unable to fine-tune itself without a schedule, because a fixed lr keeps it orbiting the minimum at radius ≈ lr.
Adam’s bookkeeping, step by step
The moving averages start at zero, so early values are biased toward zero. Walk the table and watch the corrections divide that cold start out: at step 1, m is 10× too small and v is 1000× too small.
A STEADY GRADIENT — THE CASE KINGMA & BA'S DEMO PRINTS
t
g
m
m̂ = m/(1−β₁ᵗ)
v
v̂ = v/(1−β₂ᵗ)
√v̂
step = lr·m̂/(√v̂+ε)
no correction
1
1
0.10000
1.00000
0.001000
1.00000
1.00000
10.00×10^-4
0.003162
2
1
0.19000
1.00000
0.001999
1.00000
1.00000
10.00×10^-4
0.004250
3
1
0.27100
1.00000
0.002997
1.00000
1.00000
10.00×10^-4
0.004950
4
1
0.34390
1.00000
0.003994
1.00000
1.00000
10.00×10^-4
0.005442
5
1
0.40951
1.00000
0.004990
1.00000
1.00000
10.00×10^-4
0.005797
6
1
0.46856
1.00000
0.005985
1.00000
1.00000
10.00×10^-4
0.006057
7
1
0.52170
1.00000
0.006979
1.00000
1.00000
10.00×10^-4
0.006245
8
1
0.56953
1.00000
0.007972
1.00000
1.00000
10.00×10^-4
0.006379
9
1
0.61258
1.00000
0.008964
1.00000
1.00000
10.00×10^-4
0.006470
10
1
0.65132
1.00000
0.009955
1.00000
1.00000
10.00×10^-4
0.006528
raw m
corrected m̂
raw v
0.001999
divide by (1 − β₂ᵗ)
0.001999
corrected v̂
1.000000
÷ (1 − β₁ᵗ) = ÷0.19000
÷ (1 − β₂ᵗ) = ÷0.00200
w after step t: -0.002000
step 2 · g = 1
m 0.190000 / 0.190000 = m̂ 1.000000
v 0.001999 / 0.001999 = v̂ 1.000
√v̂ 1.000000
step = lr · m̂/(√v̂+ε) = 10.00×10^-4
without correction: 0.004250 (0.235× the corrected step)
scale check: same gradient multiplied by 100 gives the same step —
Adam divides the gradient's size out, leaving its direction.
Bias correction is a warm-up for the averages, not a hack: dividing by (1 − βᵗ) turns “the mean of a history that began at zero” into “the mean of the gradients seen so far.” The correction fades as t grows; by t = 50 the raw m is within 1% of m̂.
A note on the name: “Adam” is not an acronym to memorise; it is adaptive moment estimation, and the two moments are exactly the two averages above. Once you can say that sentence, the paper’s first page reads itself.
Quick check
At step 1 with the defaults, the raw second moment is v = 0.001·g². What does the bias correction (1 − β₂ᵗ) = 0.001 do to it?
05
WEIGHT DECAY, DECOUPLED
In Adam, L2 and weight decay are not the same algorithm.
In plain gradient descent, adding λ·w to the gradient and shrinking w by λ each step are the same arithmetic. Put an adaptive denominator between them and the equivalence shatters: suddenly the regularization strength depends on each parameter’s gradient statistics. AdamW moves the decay out of the gradient entirely.
L2 regularization adds a penalty for large weights to the loss. Its gradient is proportional to the weight itself, so it pushes every weight toward zero a little on every step:
L2: loss ← loss + (λ/2)·w²
gradient ← gradient + λ·w
w ← w − lr·(gradient + λ·w) ← the same term as weight decay
weight decay (the old trick):
w ← w − lr·λ·w identical arithmetic under plain SGD
Under vanilla SGD those two lines are literally the same update, so the names got used interchangeably for years. Then Adam arrives and puts an adaptive divisor around the whole gradient:
Adam + L2:
w ← w − lr · (m̂ of (g + λw)) / (√(v̂ of (g + λw)) + ε)
the regularization term rides inside the denominator, so its effective
size depends on the gradients — parameters with large gradients get
relatively less decay, quiet parameters get relatively more.
AdamW (Loshchilov & Hutter, 2017):
w ← w − lr · m̂ / (√v̂ + ε) − lr · λ · w
= w·(1 − lr·λ) − lr · m̂ / (√v̂ + ε)
the decay is applied straight to the weight, after the adaptive step.
Every parameter shrinks by exactly the same fraction per step.
Worked check — the same λ, two very different decays
Put numbers on the entanglement. Suppose two parameters have the same weight w = 10 and the same λ = 0.1, but different gradient histories: parameter A has recent gradient RMS 10, parameter B has RMS 0.01. The decay’s contribution to the Adam numerator is λ·w = 1.0 for both; the denominator differs by 1000×:
decay contribution to the update = lr·λ·w / (√v̂ + ε)
lr = 0.001, λ = 0.1, w = 10:
A (√v̂ = 10): 0.001 × 1.0 / 10 = 1.0×10⁻⁴
B (√v̂ = 0.01): 0.001 × 1.0 / 0.01 = 1.0×10⁻¹
same λ, 1000× different regularization pressure.
(B's linearised 0.1 cannot actually be taken: Adam's update magnitude is
bounded by ≈lr = 0.001. The point stands — for B the decay dominates the
step, for A it is a rounding error.)
AdamW, same weights and λ:
every step: w ← w × (1 − lr·λ) = w × 0.9999
after 100 steps: ×0.9999¹⁰⁰ = 0.990049
after 1000 steps: ×0.9999¹⁰⁰⁰ = 0.904800
identical for A and B, no matter what their gradients did.
That is what “decoupled” means: the decay is not a term in the loss being filtered through the optimizer, it is a separate, predictable shrink applied by the optimizer itself.
Decoupled decay, watched on the weights
Same ten weights, same random gradient stream, three update rules. AdamW shrinks every weight by exactly (1 − lr·λ) per step — the dashed envelope. Adam + L2 adds λ·w to the gradient and lets Adam normalize it, so how much a weight shrinks depends on its gradients.
‖w₀‖ = 6.8827 (10 weights, seed 42)
per-step shrink (1 − lr·λ) = 0.9999000
after 200 steps = 0.980198
at step 200
Adam (no decay) 6.8912
Adam + L2 6.4369
AdamW 6.7547
pure decay 6.7464
AdamW tracks the envelope: uniform, proportional, gradient-independent.
Adam + L2 ends 0.3178 lower — the decay rode Adam's normalized step and bit harder.
Zero λ turns all three lines into one.
This is the Loshchilov & Hutter point in one picture: with Adam + L2 the decay is inside the adaptive denominator, so regularization strength depends on gradient statistics. AdamW moves the decay out of the gradient entirely.
06
THE ONE KNOB THAT MATTERS MOST
If you tune one thing, tune the learning rate.
A 10× change in learning rate matters more than any architectural decision you will make. Too high and the loss explodes; too low and it crawls into a suboptimal corner; in between, the ideal value drifts as training proceeds — which is why the step size is usually put on a schedule.
All three fates are visible on the same ravine from chapter 01, after 200 steps of plain gradient descent:
lr = 0.02 loss 2.2×10⁻² x still at 0.67 — crawling, not failing
lr = 0.08 loss 2.0×10⁻³ steady descent, no oscillation
lr = 0.16 loss 7.9×10⁻⁵ zig-zagging, but the ringing decays
lr = 0.21 loss 1.8×10¹⁷ diverged: the steep factor is −1.1
The failure modes are asymmetric. Too low looks like patience; you only discover it after burning the compute. Too high looks like success for the first few steps, then produces NaN weights. That is why production training rarely uses a constant learning rate — and when it does, it is because someone has already measured the right value.
The modern default is warmup + cosine decay: ramp the learning rate linearly from zero over the first 1–10% of steps, then decay it smoothly to zero. Warmup exists because the very first updates are the least trustworthy — Adam’s moments are cold-starting, the gradients are measured on the least-informative parameters, and a full-size step can wreck a pretrained model before the averages warm up. Decay exists because a fixed step that is right early is too big late: Adam orbits its minimum at radius ≈ lr, so shrinking lr is how it actually settles.
warmup: lr(t) = (t / T_w) · peak t < T_w
cosine: lr(t) = peak/2 · (1 + cos(π·(t−T_w)/(T−T_w)))
peak = 3e-4, total = 1000 steps, warmup = 5% = 50 steps:
step 25 lr = 1.500×10⁻⁴ half-way up the ramp
step 50 lr = 3.000×10⁻⁴ the peak
step 525 lr = 1.500×10⁻⁴ half-way down the cosine
step 1000 lr = 0 fully settled
linear decay swaps the cosine for a straight line; in both cases the
slope at the end flattens, which is where the fine-tuning happens.
The schedule around the step
The learning rate rarely stays constant. Warmup ramps it up from zero so the fragile first updates stay small; decay brings it down so the model can settle into a minimum instead of orbiting it.
warmup + cosine · peak 3.00e-4 · 1000 steps
learning rate at:
step 0 0.000e+0
step 10 6.000e-5
step 50 3.000e-4
step 100 2.980e-4
step 250 2.684e-4
step 500 1.624e-4
step 1000 0.000e+0
typical peak values
SGD + momentum 0.01 – 0.1 (needs a schedule most of all)
Adam / AdamW 1e-4 – 3e-4 (the modern default)
fine-tuning a model 1e-5 – 5e-5 (protect the pretrained weights)
warmup arithmetic at 5% of 1000 steps: lr ramps 0 → 3.00e-4 over 50 steps, ≈ 6.00e-6 per step.
If you tune exactly one hyperparameter, tune this one. The source puts it plainly: a 10× change in learning rate matters more than any architectural decision you will make.
Where to start — the field's default bands
SGD + momentum lr = 0.01 – 0.1 needs a schedule most of all
Adam / AdamW lr = 1e-4 – 3e-4 the modern default
fine-tuning pretrained lr = 1e-5 – 5e-5 ~10–15× below the fresh-training band
warmup first 1 – 10% of steps
When AdamW underperforms, the tuning order is:
1. learning rate (arguably the only one that reliably matters)
2. β₂ (0.99 or 0.98 when gradients are noisy or sparse)
3. almost never β₁ or ε — the defaults are robust across problems.
The claim to remember is the source’s: these defaults work for about 80% of problems, and when they do not, change the learning rate first. Everything else in the optimizer is a second-order knob.
The training loop, in the order PyTorch expectspython
optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
for epoch in range(100):
optimizer.zero_grad() # 1. clear old gradients
output = model(torch.randn(32, 784))
loss = F.cross_entropy(output, labels) # 2. forward + loss
loss.backward() # 3. backward: gradients
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 4. clip
optimizer.step() # 5. update the weights
scheduler.step() # 6. adjust the learning rate
Memorise the order: zero, forward, loss, backward, clip, step, schedule. Calling scheduler.step() before optimizer.step() is a classic source of subtle bugs — the schedule reads a step count the optimizer has not taken.
Gradient clipping is the seatbelt that pairs with an ambitious learning rate. Clip by global norm: if the gradient vector’s length exceeds a threshold, scale the whole vector down so its length equals the threshold, preserving direction. With max_norm = 1.0, a gradient of [3, 4] has length 5 and becomes [0.6, 0.8]. It cannot turn a bad learning rate into a good one, but it converts “loss went to NaN five minutes in” into “loss wobbled and recovered,” which is the difference between a completed run and a lost afternoon.
Quick check
Training is unstable in the first hundred steps, then fine once it gets going. What does warmup actually change?
07
WHEN EACH OPTIMIZER WINS
Match the optimizer to the task.
On our little ravine momentum wins. On a transformer AdamW wins. On a GAN you deliberately break Adam’s defaults. There is no single best optimizer — only defaults that work for 80% of problems and a short list of well-understood exceptions.
The race below runs all three families on the chapter 01 ravine, each with its own learning rate because their step scales mean different things. Press play, then tune one knob at a time and re-run it. The ordering you will find — momentum, then Adam, then SGD — is real for this geometry, and it is not the ordering that decides production training. What decides it is the model you are training and what you need from the final weights.
Three optimizers, one ravine
Each optimizer gets its own learning rate, because their step scales mean different things. Press play and watch how the red, violet and green paths trade places — then tune the knobs and re-run the race.
at step 80 (full race)
SGD final 0.004454 best 0.004454
momentum final 1.61×10^-7 best 2.43×10^-9
Adam final 8.04×10^-5 best 3.09×10^-6
leader: momentum
steps to loss < 10⁻³
SGD never
momentum 21
Adam 24
Adam's step is ≈ lr regardless of the gradient's size — so unlike the other
two it cannot fine-tune itself without a learning-rate schedule.
On this quadratic ravine the ordering is momentum → Adam → SGD. On a transformer the ordering flips, and the next chapters explain why.
The short history is worth knowing, because it explains why the defaults look the way they do. Stochastic gradient descent is the oldest; Polyak’s momentum dates to 1964; AdaGrad (2011) made step sizes per-parameter; Hinton’s RMSProp (2012) fixed its stalling. Adam (2014) combined the last two and became the default because it converges fast with almost no tuning. Then AdamW (2017) fixed the weight-decay bug you saw in chapter 05, and it became the default for everything with attention in it. Meanwhile vision practitioners noticed something awkward: adaptive methods converge faster but sometimes generalize slightly worse, while plain SGD with momentum lands in flatter minima. The result is the fork in the road below — not a settled scientific question, but a working consensus.
So the decision is not “which optimizer is best” but “which recipe has been field-tested for this architecture”:
Transformer / LLM AdamW lr = 1e-4, wd = 0.01 – 0.1
CNN / ResNet SGD+momentum lr = 0.1, momentum = 0.9
GAN Adam lr = 2e-4, β₁ = 0.5
fine-tuning pretrained AdamW lr = 2e-5, wd = 0.01
don't know yet AdamW lr = 3e-4, wd = 0.01
Why the GAN exception: two networks pull in opposite directions every step.
β₁ = 0.9 is a memory of where the *other* player used to be — with β₁ = 0.5
each network stays responsive to the opponent's latest move.
The optimizer decision table
“Don’t fight the consensus without a measured reason.” Pick what you are training and read the field-tested recipe — including the one deliberate exception to Adam’s defaults.
WHAT ARE YOU TRAINING?
AdamWlr = 3e-4weight decay 0.01
These defaults work on roughly 80% of problems. Start here, measure, then specialize.
In the wild: The PyTorch-era default for nearly every new architecture.
Watch out: If it underperforms, tune the learning rate before anything else — then β2, and almost never β1 or ε.
task Not sure yet
optimizer AdamW
lr 3e-4
settings weight decay 0.01
why
These defaults work on roughly 80% of problems. Start here, measure, then specialize.
the train-loop order (memorize it)
1. optimizer.zero_grad()
2. forward + loss
3. loss.backward()
4. clip gradients (optional, max_norm = 1.0)
5. optimizer.step()
6. scheduler.step()
The source's warning: step 6 before step 5 is a classic source of
subtle bugs — the schedule reads a step count the optimizer has not
taken yet.
one knob to rule them all
A 10× change in learning rate matters more than any architectural decision you will make. Warmup protects the first 1–10% of steps; cosine decay lets the last steps settle.
This table is the source’s flowchart, made clickable. The “not sure” row is not a cop-out: AdamW at 3e-4 with weight decay 0.01 is a strong prior for a brand-new architecture.
Scientific honesty: the “SGD generalizes better” claim is a real but contested empirical result, not a theorem — see Wilson et al.’s The Marginal Value of Adaptive Gradient Methods in the sources. What is not contested is that matching the field’s standard recipe for a known architecture is the rational baseline, and that curiosity should be funded by a measured reason.
08
CHECK YOURSELF
Five questions. Then the terms worth keeping.
Answer before you look. The bias-correction question and the AdamW question are the two that expose whether the optimizers are a mechanism you can debug or a list of magic names.
0 / 5 answered · 0 correct
01What problem does momentum solve in gradient descent?
02What is the key difference between Adam and AdamW?
03Why does Adam use bias correction in early training steps?
04What are the standard default hyperparameters for Adam?
05Which optimizer is the modern default for training transformers and LLMs?
Key terms, demystified
Click a card to swap the lazy description for what it actually means.
Exercises from the lesson
Four problems with exact numbers — implement Nesterov momentum and check it by hand, compute a warmup-plus-cosine schedule, audit Adam’s effective step, and measure what gradient clipping does to a diverging run. Try first; a worked answer is one click away.
Implement Nesterov momentum — evaluate the gradient at the lookahead position w − lr·β·v — and compare it to standard momentum on f(w) = (w − 3)² from w = 10 with lr = 0.1, β = 0.9. Trace four steps of each and explain what the lookahead changes.Show one worked answer
Standard momentum at w = 10: g = 2(10 − 3) = 14, v = 14, w = 8.6. Nesterov: the lookahead is w − lr·β·v = 10 − 0.1·0.9·0 = 10 (the cold velocity makes step 1 identical); g = 14, v = 14, w = 8.6. Step 2: momentum v = 0.9·14 + 2(8.6 − 3) = 23.8 → w = 6.22; Nesterov looks at 8.6 − 1.26 = 7.34, sees g = 8.68, so v = 0.9·14 + 8.68 = 21.28 → w = 6.472. Step 3: momentum v = 27.86 → w = 3.434; Nesterov looks at 4.5568, g = 3.1136, v = 22.2656 → w = 4.2454. Step 4: momentum w = 0.8398 (overshoot); Nesterov looks at 2.2415, sees g = −1.5170 (negative!), v = 18.5221 → w = 2.3932, braking before the target. The lookahead is a one-step forecast: when the velocity is about to carry the parameter past the minimum, the gradient at the predicted position already points back, so Nesterov damps the overshoot before it happens. The trade is the same stability bound and slightly better smoothness; on the circle dataset the source's exercise measures it reaching high accuracy sooner than plain momentum.
Write the warmup + cosine schedule for a peak learning rate of 3e-4 over 10,000 steps with 5% warmup, and compute lr at steps 1, 250, 500, 5000 and 10,000. How much smaller is the total 'step budget' spent during warmup than 500 steps at the flat peak, and why is that cost worth paying?Show one worked answer
Warmup: lr(t) = (t/500)·3e-4 for t < 500. lr(1) = 6e-7; lr(250) = 1.5e-4; lr(500) = 3e-4. Cosine decay after warmup: lr(t) = 1.5e-4·(1 + cos(π(t − 500)/9500)). At t = 5000 the cosine argument is π·4500/9500 = 0.4737π, so cos ≈ 0.0827 and lr ≈ 1.624e-4; at t = 10,000 the argument is π, so lr = 0. The warmup phase spends ½·500·3e-4 = 0.075 of cumulative (lr × step), exactly half of the 0.15 that 500 flat-peak steps would spend — warmup is a one-time cost of about 250 peak-equivalent steps. It buys protection when the step is least trustworthy: Adam's moments are cold-starting, the gradients of a freshly initialized (or freshly re-purposed) network are noisiest, and a full 3e-4 step can move every weight before any evidence has accumulated. Run the source's experiment to measure the epoch cost on your setup; the schedule arithmetic above is the part that is exact, and the mechanism predicts a noisier, occasionally divergent start without warmup versus a reliable arrival that spends a few extra epochs early.
The effective learning rate for one parameter under Adam is lr·|m̂|/(√v̂+ε). Compute it at steps 1, 10 and 100 for two parameters with constant gradients 10 and 0.1, then switch one to an alternating +1, −1, +1, −1 sequence and compute steps 1–4. Are all parameters updated at the same speed?Show one worked answer
Constant gradients: for both g = 10 and g = 0.1, m̂ = g and √v̂ = |g| at every step, so the effective rate is 0.001·g/|g| = 1.0e-3 at t = 1, 10 and 100 — identical despite a 100× difference in gradient size. The scale cancels; only the direction and consistency matter. Alternating ±1: v̂ ≈ 1 throughout (|g| = 1 never changes), but m̂ collapses when signs disagree. t = 1: m̂ = 1.0 → step 1.00e-3; t = 2: m = 0.9·0.1 − 0.1 = −0.01, m̂ = −0.01/0.19 = −0.0526 → step −5.26e-5; t = 3: m = 0.091, m̂ = 0.336 → step 3.36e-4; t = 4: m̂ = −0.0526 again → −5.26e-5. The effective rate alternates between ~0.34·lr and ~0.05·lr instead of a steady lr. So no — parameters move at the same speed only when their gradients are consistent; oscillating gradients get a much smaller effective rate, because m̂ measures net direction while √v̂ measures gross magnitude. That is the mechanism that lets Adam keep quiet parameters moving and noisy ones steady, and it is also why Adam cannot settle without a decaying lr.
Clip by global norm with max_norm = 1.0. (a) What does g = [3, 4, −12] become? (b) The ravine run diverges at lr = 0.21 because the steep factor is 1 − 2.1 = −1.1. If every gradient step is clipped to norm 1.0, does the run converge? Explain with the arithmetic of the update.Show one worked answer
(a) ‖g‖ = √(9 + 16 + 144) = √169 = 13 > 1, so the whole vector is scaled by 1/13: [0.2308, 0.3077, −0.9231], with norm exactly 1. (b) No. Clipping bounds the size of each update to lr·‖g_clipped‖ = 0.21 per step, so a NaN blow-up becomes a bounded oscillation — but the direction is still wrong: the steep coordinate keeps flipping sign every step (factor −1.1 before clipping became a bounded flip after), so the parameter rings inside a window of width ≈ 2·lr = 0.42 instead of approaching zero. The learning rate is the cause, not the step magnitude. On this surface you must drop below the SGD stability limit lr < 2/b = 0.2; clipping merely keeps the run alive long enough to notice. The source's Exercise 4 is exactly this measurement — ten seeds, Adam at lr = 0.01, with and without clipping — and the expectation is that clipping drives the divergence count toward zero for moderately oversized rates, at the cost of slowing the steps where large gradients were legitimate. It is stability insurance, not a fix for a badly chosen lr.
Terms this lesson borrows from later lessons (or outside)
You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.
gradient descent — The loop that nudges parameters downhill on the loss surface. This lesson takes the loop from Phase 1 and replaces the step rule inside it — same gradients, different optimizers. (Phase 1, Lesson 08)
learning rate — The step-size multiplier that appears in the very first update rule. Here it becomes a schedule and a per-parameter quantity. (Phase 1, Lesson 08)
backpropagation — The algorithm that produces the gradients the optimizer consumes. Optimizers are downstream of backprop: backprop says which way is downhill; the optimizer decides how far to move. (Phase 3, Lesson 03)
loss function — The function whose landscape is being descended. Its shape — the ravine, the saddle, the flat region — is why different optimizers exist. (Phase 3, Lesson 05)
mini-batch — A random subset of the data used to estimate the gradient. It is the source of SGD's useful noise, and the reason the same step rule behaves differently at batch sizes 32 and 3,200. (Phase 1, Lesson 08)
KEEP GOING
A picture is a start. Practice is the rest.
This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.
Lesson text adapted from AI Engineering from Scratch (Phase 03, Lesson 06) and the Math Foundations Notebook reference build. The eight labs (ravine learning-rate sweep, momentum-versus-zig-zag with stability limits, AdaGrad/RMSProp effective-step chart, Adam bias-correction stepper, the decoupled-decay experiment, learning-rate schedule explorer, three-way animated race, and the optimizer decision table), the ravine surface ½(0.1x² + 10y²), worked example A for SGD/momentum/Nesterov on (w − 3)², worked example B for two Adam steps, the alternating-versus-constant velocity arithmetic, AdaGrad's 1/√t stall table, the v-correction's 1000-step horizon, the same-λ 1000×-pressure check for AdamW, and the warmup/cosine arithmetic are original to this page. Every number shown is computed live by the labs or verified by hand in the prose.