The lesson in one minute
What you'll be able to explain
- Gradient descent: step against the slope, scaled by the learning rate.
- The learning rate is the most important knob: too high diverges, too low stalls.
- Momentum accumulates a velocity so consistent directions speed up and zig-zags cancel.
- Adam gives every weight its own step size (average gradient ÷ its typical size); AdamW decouples weight decay from that rescaling and is the transformer default.
- Transformers use warmup then cosine decay, and clip gradients by global norm.
Level 1
The practitioner's guide
In one sentence
An optimizer is the rule that turns "the slope of the loss here" into "the step every weight takes", and the handful of settings that come with it (learning rate, momentum, the two Adam betas, weight decay, warmup, clipping) are most of what a training recipe consists of.
When you need it
You need this the moment a training or fine-tuning
job asks you for a learning rate, and again the first time a loss curve
spikes, turns NaN, or flattens long before the model is any good. The tell:
a config with lr, betas, weight_decay, warmup_steps and
max_grad_norm copied from someone else's run, and a paper's training
section that reads as Greek (β₁ = 0.9, β₂ = 0.95, ε = 10⁻⁵) rather than as
choices. You don't need it to call a trained model, and a hosted
fine-tuning API chooses the optimizer for you; what you set there is the
learning rate and how long to train, which is still the most important
choice on this page. One number from this lesson shows how much the rule
matters: in a narrow valley, 100 steps of momentum reach a loss more than
10,000 times lower than 100 steps of plain gradient descent at the same
learning rate.
Your options
The rules, plus two add-ons that ride on any of them, from the simplest to the one transformers use:
| Option | What it does | What it guarantees | What it costs | Where it lives |
|---|---|---|---|---|
| Plain SGD | Steps against the current batch's slope, scaled by the learning rate | The simplest rule, nothing to tune but η | The steepest direction caps η, so it creeps along gentle valley floors: loss 1.56 after 100 steps here | Your training script, no extra memory |
| SGD with momentum | Keeps a velocity: 90% of the last one plus the new slope | Consistent directions speed up and zig-zags cancel: loss 6.5 × 10⁻⁵ after the same 100 steps | One extra number per weight, and overshoot past the minimum before it settles | The default for CNNs |
| Adam | Gives every weight its own step: its average gradient divided by its typical gradient size; the first step is exactly η whatever the gradient | One learning rate works across weights whose gradients differ by orders of magnitude | Two extra numbers per weight; unreliable averages in the first steps; an L2 penalty added to its gradient loses its meaning | Most training scripts |
| AdamW | Adam, with weight decay applied straight to the weights instead of through the gradient | λ means what it says: shrink by η × λ per step | The same memory as Adam | The transformer default; PyTorch's AdamW ships with lr 0.001, betas (0.9, 0.999), eps 10⁻⁸, weight_decay 0.01 |
| Warmup then decay (a schedule on top) | Ramps η from 0 to its peak, then lowers it along a cosine or a straight line to a floor | No full-size steps while the weights are random and Adam's averages unsettled; a gentle finish into a good minimum | Two more settings, warmup steps and total steps, so the run's length must be known up front | The scheduler |
| Gradient clipping (on top) | Rescales the whole gradient when its combined length exceeds a limit, direction unchanged | A rare spike becomes a blip instead of a wrecked run | One norm per step; it can hide a network that explodes every step | One line before the optimizer step |
How to choose
Start from the architecture, then from the size.
- A transformer, pretraining or fine-tuning: AdamW, warmup, a cosine or
linear decay, clipping at 1.0. That is the Llama 2 recipe (AdamW with
β₁ = 0.9, β₂ = 0.95, ε = 10⁻⁵, 2000 warmup steps, cosine decay to 10% of
the peak, weight decay 0.1, clipping 1.0) and the shape of Hugging Face's
TrainingArgumentsdefaults (AdamW, a linear schedule, max_grad_norm 1.0). - A convolutional network: SGD with momentum 0.9, still the default there.
- The learning rate itself: the fastest one that doesn't blow up, found by trying a few. Bigger models take smaller rates (Llama 2 uses 3 × 10⁻⁴ for its 7B and 13B models and 1.5 × 10⁻⁴ for 34B and 70B), and fine-tuning takes smaller rates than pretraining (Hugging Face defaults to 5 × 10⁻⁵).
- A published recipe you are reproducing: copy every setting, betas included. The transformer paper's β₂ = 0.98 and Llama 2's 0.95 are both deliberate departures from Adam's 0.999.
- Whatever you pick, the learning rate is the single most important hyperparameter, and a schedule is part of it: the peak, the warmup and the total steps are one decision.
What it costs
Memory: momentum keeps one extra number per weight and Adam or AdamW keep two, so the optimizer state of a large model is twice the size of the weights it trains, which is a large part of why training needs more memory than serving. Compute: a few operations per weight per step, small beside the forward and backward passes. Time: warmup and decay need the total step count, so the budget is fixed before the run starts, and a run stopped halfway never reached its low learning rate. Quality: on this lesson's narrow valley, after 300 steps plain SGD sits at a loss of 3.7 × 10⁻³, Adam at 1.1 × 10⁻¹¹ and momentum at 1.8 × 10⁻¹²; the rule decides whether a fixed budget of steps gets there at all.
What breaks
- Loss spikes, then NaN. The learning rate is too high: on a bowl with slope 2w, any η above 1 makes every step overshoot further than the last. Lower it, add warmup, clip.
- Loss barely moves. Too low: at η = 0.001 the bowl's weight is still at 0.98 after ten steps. Raise it until training becomes unstable, then back off.
- Weight decay that does nothing, or too much. An L2 penalty inside Adam gets divided by the typical gradient size, so λ = 0.1 and λ = 0.001 shrink a weight by the same amount. Use AdamW, where λ = 0.1 means 0.99 per step and 0.001 means 0.9999.
- Divergence in the first hundred steps with Adam. No warmup: its averages are built from a handful of steps and the weights are random.
- Overshoot. Momentum swings hard, across the valley and past the minimum, before it settles; the trajectory figure in Level 2 shows it. A smaller β or a smaller η calms it.
- Clipping on every step. The gradient is exploding, not spiking; the
fix is initialization or normalization (
primer.ml.deep_nets), and clipping is hiding it. - A schedule cut short. Cosine decay reaches its floor only at the planned last step; stop early and the model never settled.
In the wild
Attention Is All You Need trained with Adam at β₁ = 0.9,
β₂ = 0.98, ε = 10⁻⁹, a learning rate that rises linearly for 4000 warmup
steps and then falls with the inverse square root of the step. Llama 2
used AdamW with the recipe above. PyTorch ships torch.optim.SGD
(momentum as an argument), Adam and AdamW, and clips with
torch.nn.utils.clip_grad_norm_, which measures the norm over all
parameters "as if the norms of the individual gradients were concatenated
into a single vector". Hugging Face's TrainingArguments defaults to
AdamW (adamw_torch_fused on recent PyTorch), learning_rate 5e-5,
weight_decay 0.0, betas 0.9 and 0.999, epsilon 1e-8, a linear schedule
with no warmup and max_grad_norm 1.0. The rules come from Kingma and Ba
(Adam, 2014), Loshchilov and Hutter (AdamW, 2017, and cosine annealing,
2016), Sutskever et al. (momentum, 2013) and Pascanu et al. (clipping,
2012), all linked at the end of the lesson.
Go deeper
Level 2 walks each rule down the same bowl with two-row tables you can check by hand, races SGD, momentum and Adam across a narrow valley, shows why an L2 penalty inside Adam stops meaning anything, draws the warmup-and-cosine curve, and clips (3, 4) down to (0.6, 0.8). If you only needed to fill in a config, you are done.
Level 2
How it works, from scratch
Every optimizer runs the same loop: feel the slope, turn it into a step, take it, repeat. This level builds each rule on a one-number valley you can check by hand, then lets them race across a valley that is not round.
Chapter 1
The idea: walking downhill in fog
You're on a hillside in thick fog and want to reach the lowest point in the
valley. You can't see the valley; you can only feel the slope under your
feet. So you feel which way is downhill, take a step that way, and repeat.
That is gradient descent. The hillside is the loss (how wrong the model
is, for every possible setting of its weights), your position is the
current weights, and the slope under your feet is the gradient: the list
of slopes of the loss, one per weight. (See primer.ml.neural_net for how
backprop measures it, and primer.notation for the symbols.) An
optimizer is your rule for turning "the slope here" into "the step I
take".
Worked example on the simplest possible valley, the bowl f(w) = w², whose slope at w is 2w. Start at w = 1 with step size 0.1:
| step | w | slope 2w | step taken 0.1 × slope | new w |
|---|---|---|---|---|
| 0 | 1.0 | 2.0 | 0.2 | 0.8 |
| 1 | 0.8 | 1.6 | 0.16 | 0.64 |
Each step keeps 80% of w, sliding smoothly toward the bottom at 0.
Figure 1 · Diagram
flowchart LR
W[Current weights] --> G[Feel the slope<br/>compute gradient]
G --> R{Optimizer rule}
R --> S[Step]
S --> W2[New weights]
W2 -->|repeat| G
R -.uses.-> H[Its own memory:<br/>velocity, averages]
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the weights at step | ||
| the step counter | 0, 1, 2, … | |
| "eta", the learning rate (step size) | 0.1 | |
| the loss | ||
| "nabla L", the gradient: the slope of the loss at the current weights, one number per weight |
In words: "the next weights are the current weights minus the learning rate times the slope of the loss where we stand."
With the numbers: ; .
Level 3: in Python
w, eta = 1.0, 0.1
# ∇L for the bowl L = w²
def grad_L(w): return 2 * w
for t in range(2):
# w_(t+1) = w_t - η ∇L(w_t)
w = w - eta * grad_L(w)
print(round(w, 2)) # → 0.8 0.64
"Stochastic" gradient descent (SGD) means the slope is estimated from a
small random batch of examples instead of the whole dataset: noisier, but
thousands of times cheaper per step. descend_bowl runs the table above;
SGD is the general version.
Why it matters every model you've heard of was trained by a descendant of this one line. The variants below exist because real loss landscapes are not round bowls.
Chapter 2
The learning rate: how long a stride?
In the fog, stride length is everything. Tiny shuffling steps are safe but you'll be walking all night. Giant leaps overshoot the valley floor and land you higher up the opposite slope; keep leaping and you climb out of the valley altogether.
Worked example on the bowl w², where each step multiplies w by (1 − 2η):
| learning rate η | multiplier 1 − 2η | w after 1, 2, 3 steps | what happens |
|---|---|---|---|
| 0.001 | 0.998 | 0.998, 0.996, 0.994 | stalls: 10 steps only reach 0.980 |
| 0.1 | 0.8 | 0.8, 0.64, 0.512 | smooth progress |
| 0.5 | 0 | 0, 0, 0 | lands on the bottom in one step |
| 1.1 | −1.2 | −1.2, 1.44, −1.728 | overshoots further each time: diverges |
Figure 2 · Drawn from the lesson's code
Over 30 steps, rate 0.001 barely lowers the loss, 0.1 falls steadily, 0.45 plunges below the chart within six steps, and 1.1 climbs as every step overshoots
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the slope of at | 2.0 at | |
| the factor each step multiplies by | −1.2 when |
In words: "on this bowl, one step multiplies the weight by one minus twice the learning rate; if that factor's size is above 1, the weight grows instead of shrinking."
With the numbers: : , so .
Level 3: in Python
def three_steps(eta, w=1.0):
out = []
for t in range(3):
# w - η·2w = (1 - 2η) w
w = (1 - 2 * eta) * w
out.append(round(w, 3))
return out
three_steps(1.1) # → [-1.2, 1.44, -1.728]
three_steps(0.1) # → [0.8, 0.64, 0.512]
# η = 0.001 after 10 steps: barely moved
round(0.998 ** 10, 3) # → 0.98
In code: descend_bowl is the same loop with the learning rate as an argument; call it with each rate in the table to reproduce every row.
Why it matters the learning rate is the single most important hyperparameter. Too high and training diverges or bounces (loss spikes, NaNs); too low and it takes forever or settles somewhere poor. The steepest direction of the landscape sets the ceiling: on a bowl with slope 2w, any η above 1 diverges.
Chapter 3
Momentum: a heavy ball instead of a cautious hiker
Now imagine a long, narrow valley: steep walls on both sides, a gentle slope along the floor. A cautious hiker who only reads the local slope zig-zags from wall to wall and barely moves along the floor. A heavy ball rolling down the same valley behaves differently: its sideways bouncing cancels out, while the gentle downhill pull along the floor keeps adding up, so it builds speed exactly where you want it. That accumulated speed is momentum.
Worked example on the bowl w², learning rate 0.1, momentum β = 0.9:
| step | slope g = 2w | velocity v = 0.9·v + g | new w = w − 0.1·v |
|---|---|---|---|
| 1 | 2.0 | 2.0 | 1.0 − 0.2 = 0.8 |
| 2 | 1.6 | 0.9 × 2.0 + 1.6 = 3.4 | 0.8 − 0.34 = 0.46 |
After two steps plain descent is at 0.64; momentum is already at 0.46.
Figure 4 · Diagram
flowchart LR G[Slope now g_t] --> V["Velocity v_t = β·v_(t−1) + g_t<br/>(remember 90% of the old speed)"] VO["Old velocity v_(t−1)"] --> V V --> S["Step: w − η·v_t"] S --> VO
Figure 3 · Drawn from the lesson's code
After 100 steps from (-8, 1), plain SGD has only crept to x = -1.8 along the valley floor, while momentum and Adam reach the minimum after some overshoot
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the gradient (slope) at step | 2.0, then 1.6 | |
| the velocity: a running, fading sum of past gradients | 2.0, then 3.4 | |
| "beta", how much old velocity is kept each step (0 to 1) | 0.9 | |
| learning rate | 0.1 |
In words: "the velocity is 90% of the previous velocity plus the new slope, and the weights move by the learning rate times the velocity."
With the numbers: ; .
Level 3: in Python
w, v, beta, eta = 1.0, 0.0, 0.9, 0.1
for t in range(2):
# the slope of w² here
g = 2 * w
# v_t = β v_(t-1) + g_t
v = beta * v + g
# w_(t+1) = w_t - η v_t
w = w - eta * v
print(round(v, 2), round(w, 2)) # → 2.0 0.8 3.4 0.46
In code: momentum_on_bowl runs the two-row table; SGD with a nonzero momentum keeps its velocity between calls to SGD.step. narrow_valley is the valley in the figure (rosenbrock is a harder, banana-shaped one), and run walks any optimizer across a landscape and records its path.
Why it matters real loss surfaces are full of narrow valleys. On the valley above, 100 steps of momentum reach a loss over 10,000× lower than 100 steps of plain SGD at the same learning rate. Momentum is still the default for training CNNs.
Chapter 4
Adam: a separate stride for every direction
Back in the narrow valley, what you'd really like is short steps across the steep walls and long strides along the gentle floor. Adam does exactly that: it keeps, for every single weight, a running average of the slope (the direction, like momentum) and a running average of the squared slope (how big that weight's slopes typically are), then divides the first by the square root of the second. Every weight ends up taking steps of roughly the same size, the learning rate, whatever the scale of its gradient.
Worked example: on its very first step, Adam moves a weight by exactly the learning rate (0.01 here), no matter whether that weight's gradient is 1000, 1 or 0.001. (With bias correction, m̂ = g and v̂ = g², so the step is 0.01 × g / |g| = 0.01.)
Figure 5 · Diagram
flowchart LR G[Gradient g] --> M["m: average of g<br/>(direction)"] G --> V["v: average of g²<br/>(typical size)"] M --> MC["m̂ = m / (1 − β1^t)"] V --> VC["v̂ = v / (1 − β2^t)"] MC --> D["step = η · m̂ / (√v̂ + ε)"] VC --> D D --> W[w − step]
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example (first step, g = 1000) |
|---|---|---|
| this weight's gradient at step | 1000 | |
| running average of the gradient ("first moment") | ||
| running average of the squared gradient ("second moment") | ||
| how much of the old averages to keep | 0.9, 0.999 | |
| raised to the step number | ||
| "m-hat, v-hat": the bias-corrected averages | 1000, | |
| square root | ||
| "epsilon", a tiny number to avoid dividing by zero | ||
| learning rate | 0.01 |
In words: "keep a running average of the gradient and of its square, correct both for starting at zero, then step by the learning rate times the average gradient divided by its typical size."
With the numbers: , , step .
Level 3: in Python
import math
beta_1, beta_2, eta, eps, t = 0.9, 0.999, 0.01, 1e-8, 1
def first_step(g):
# m_1, starting from m_0 = 0
m = beta_1 * 0 + (1 - beta_1) * g
# v_1, starting from v_0 = 0
v = beta_2 * 0 + (1 - beta_2) * g ** 2
# undo the pull toward zero
m_hat = m / (1 - beta_1 ** t)
v_hat = v / (1 - beta_2 ** t)
return eta * m_hat / (math.sqrt(v_hat) + eps)
# m_1, v_1
round((1 - beta_1) * 1000, 6), round((1 - beta_2) * 1000 ** 2, 6) # → (100.0, 1000.0)
# the same step every time
[round(first_step(g), 6) for g in (1000, 1, 0.001)] # → [0.01, 0.01, 0.01]
In code: Adam keeps the two running averages and the step count for every weight and applies the five formulas in Adam.step; adam_first_step shows the first step is always the learning rate.
Why it matters Adam is forgiving: one learning rate works across weights whose gradients differ by orders of magnitude, which is the norm in transformers (embeddings, attention, layer norms all behave differently). On the valley above it reaches a loss below 10⁻¹⁰ in 300 steps while plain SGD is still around 10⁻³.
Chapter 5
Weight decay, and why AdamW exists
Weight decay is a gentle leash that pulls every weight a little toward zero
each step, so the model prefers small, smooth weights over large, spiky ones
(a form of regularization; see primer.ml.regularization). The classic way
to add it was an L2 penalty: add λw to the gradient. With plain SGD that
is the same as shrinking the weight. With Adam it isn't, because Adam
divides the whole gradient, penalty included, by its typical size. The
leash's strength gets rescaled away.
Worked example: one step with zero loss-gradient, so only the decay acts. w = 1, learning rate 0.1.
| method | decay λ = 0.1 | decay λ = 0.001 |
|---|---|---|
| AdamW (decoupled) | 1 − 0.1 × 0.1 = 0.99 | 1 − 0.1 × 0.001 = 0.9999 |
| Adam with L2 in the gradient | 0.9 | 0.9 (the same!) |
With L2 inside Adam, a 100× weaker penalty shrinks the weight exactly as much: λ has stopped meaning what it says.
Figure 6 · Diagram
flowchart TB
subgraph L2["Adam + L2 penalty"]
g1[loss gradient] --> add["+ λ·w"] --> ad1[Adam rescaling<br/>÷ √v̂] --> s1[step]
end
subgraph AW["AdamW (decoupled)"]
g2[loss gradient] --> ad2[Adam rescaling<br/>÷ √v̂] --> s2[step]
w2[weights] --> dec["shrink: w − η·λ·w"] --> s2
end
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| "lambda", the weight-decay strength | 0.1 | |
| how much the leash pulls this step | ||
| Adam's usual step direction | 0 here (no loss gradient) |
In words: "take Adam's normal step, then separately shrink every weight by learning rate times decay times the weight."
With the numbers: .
Level 3: in Python
# no loss gradient: Adam's step is 0
w, eta, adam_step = 1.0, 0.1, 0.0
for lam in (0.1, 0.001):
# AdamW: shrink by η λ w
print(round(w - eta * adam_step - eta * lam * w, 4)) # → 0.99 0.9999
for lam in (0.1, 0.001):
g = lam * w
print(round(w - eta * g / abs(g), 4)) # → 0.9 0.9
In code: Adam implements both recipes: with decoupled decay it is AdamW, otherwise it adds the L2 penalty to the gradient. one_decay_step runs the table.
Why it matters AdamW is the default optimizer for transformers. The fix was a one-line change that made weight decay behave predictably and improved generalization.
Chapter 6
Warmup and cosine decay: easing on and off the gas
Think of driving an unfamiliar car. You ease onto the accelerator at first, because you don't yet know how it responds. You cruise at speed for most of the trip. Near the destination you slow gradually and glide into the parking spot. Transformers are trained the same way: the learning rate warms up linearly from 0 to its peak, then decays along a cosine curve toward a small floor.
Worked example with peak 0.001, 100 warmup steps, 1,000 steps total:
| step | phase | learning rate |
|---|---|---|
| 50 | halfway through warmup | 0.0005 |
| 100 | end of warmup | 0.001 (peak) |
| 550 | halfway through decay | 0.0005 |
| 1000 | end | the floor (e.g. 0.00001) |
Figure 7 · Drawn from the lesson's code
The learning rate ramps straight up from 0 to 0.001 over 100 steps, then falls along a half cosine, passing 0.0005 at step 550 and ending near 0.00001
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the learning rate at step | 0.0005 at | |
| the peak learning rate | 0.001 | |
| the floor | 0 (or 0.00001) | |
| number of warmup steps | 100 | |
| total steps | 1000 | |
| progress through the decay, from 0 to 1 | ||
| cosine: 1 at 0, 0 at , −1 at | ||
| pi, ≈ 3.1416 (half a turn, in radians) |
In words: "during warmup the rate climbs in a straight line to the peak; after that it follows half a cosine wave from the peak down to the floor."
With the numbers: step 550: progress 0.5, , so .
Level 3: in Python
import math
eta_max, eta_min, T_w, T = 0.001, 0.0, 100, 1000
def eta(t):
if t < T_w:
# the straight ramp
return eta_max * t / T_w
# 0 to 1 through the decay
progress = (t - T_w) / (T - T_w)
return eta_min + (eta_max - eta_min) * (1 + math.cos(math.pi * progress)) / 2
[round(eta(t), 6) for t in (50, 100, 550, 1000)] # → [0.0005, 0.001, 0.0005, 0.0]
In code: warmup_cosine returns the learning rate for any step: the straight ramp during warmup, then the half cosine down to the floor.
Why it matters at the very start, Adam's averages are unreliable and the weights are random, so a full-size step can wreck them; warmup avoids early divergence. The slow finish lets the model settle into a good minimum instead of bouncing around it.
Chapter 7
Gradient clipping: a speed limiter
Occasionally one bad batch produces an enormous gradient, a sudden cliff in the fog. Taking a full step along it could throw the weights far from anywhere useful. Clipping is a speed limiter: if the step would be longer than a set limit, shorten it to the limit, keeping its direction.
Worked example: the gradient (3, 4) has length √(3² + 4²) = 5. With a limit of 1, scale it by 1/5 to get (0.6, 0.8), length 1, same direction. A gradient of (0.3, 0.4) has length 0.5, under the limit, so it's left alone.
Figure 8 · Diagram
flowchart LR
G[All gradients] --> N["Global length<br/>‖g‖ = √(sum of every squared entry)"]
N --> C{"‖g‖ > limit?"}
C -->|no| K[Use as is]
C -->|yes| S["Multiply every gradient<br/>by limit / ‖g‖"]
S --> K2[Same direction,<br/>length = limit]
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| every gradient, treated as one long list | (3, 4) | |
| one entry of that list | 3, 4 | |
| the norm (length) of : square every entry, add, take the square root | 5 | |
| the clipping limit | 1 | |
| the smaller of the two |
In words: "if the gradient's length exceeds the limit, scale it down so its length equals the limit; otherwise leave it alone."
With the numbers: .
Level 3: in Python
import math
def clip(g, c):
# ‖g‖ = √(Σ g_i²)
norm = math.sqrt(sum(g_i ** 2 for g_i in g))
# shrink only if too long
scale = min(1, c / norm)
return [round(g_i * scale, 2) for g_i in g]
clip([3, 4], c=1) # → [0.6, 0.8]
# length 0.5: under the limit, left alone
clip([0.3, 0.4], c=1) # → [0.3, 0.4]
In code: clip_by_global_norm measures the length of all gradients together and scales every one by the same factor when it exceeds the limit.
Why it matters large-model training runs almost always clip (a limit of
1.0 is common). It turns rare loss spikes from run-ending disasters into
harmless blips. It's measured globally, across all layers together, so the
update's direction is preserved (see primer.ml.deep_nets for exploding
gradients).
Test yourself
6 questions
Answer each one out loud or on paper before you open it. If you can explain it, you know it.
Question 1What's the single most important hyperparameter, and what happens at each extreme?Think it through, then reveal
The learning rate. Too high: steps overshoot and training diverges or oscillates (loss spikes, NaNs). Too low: progress is so slow that training stalls or settles in a poor spot.
Question 2Why does momentum help in a narrow valley?Think it through, then reveal
Gradients across the valley flip sign every step and cancel in the velocity, while the small, consistent gradient along the valley accumulates, so the optimizer speeds up in the useful direction.
Question 3What does Adam's division by √v̂ achieve?Think it through, then reveal
It normalizes each weight's step by the typical size of its gradient, so every weight moves roughly η per step regardless of gradient scale. One learning rate then works for all parameters.
Question 4What's the difference between Adam with L2 and AdamW?Think it through, then reveal
With L2, the decay term is added to the gradient and then rescaled by Adam, so its effective strength varies per weight and λ loses its meaning. AdamW applies decay directly to the weights, outside the rescaling.
Question 5Why warm up the learning rate?Think it through, then reveal
At the start, weights are random and Adam's moment estimates are based on a handful of steps, so full-size updates can be wildly wrong and destabilize training. Ramping up gives the statistics time to settle.
Question 6What does gradient clipping protect against, and why clip the global norm?Think it through, then reveal
Rare huge gradients (exploding gradients, bad batches) that would throw the weights far off. Clipping the combined norm scales every tensor by the same factor, which preserves the update's direction.
Primary sources
The papers behind this lesson
Kingma & Ba, Adam: A Method for Stochastic Optimization (2014): Combined momentum with per-weight step sizes and bias correction into the optimizer most networks are trained with.
Read the annotated companion →The paper ↗Loshchilov & Hutter, Decoupled Weight Decay Regularization (AdamW, 2017): Showed that L2 regularization and weight decay differ under Adam, and fixed it by decoupling the decay.
The paper ↗Sutskever, Martens, Dahl & Hinton, On the importance of initialization and momentum in deep learning (ICML 2013): Demonstrated that well-tuned momentum makes plain SGD competitive on hard deep-network problems.
The paper ↗Loshchilov & Hutter, SGDR: Stochastic Gradient Descent with Warm Restarts (2016): Introduced cosine learning-rate annealing, now the standard decay shape.
The paper ↗Pascanu, Mikolov & Bengio, On the difficulty of training recurrent neural networks (2012): Analysed exploding gradients and proposed clipping the gradient norm.
The paper ↗Researcher's shelf
Further reading
- Sebastian Ruder, An overview of gradient descent optimization algorithms: https://arxiv.org/abs/1609.04747
- Gabriel Goh, Why Momentum Really Works (Distill): https://distill.pub/2017/momentum/
- Kingma & Ba, Adam: A Method for Stochastic Optimization (2014): https://arxiv.org/abs/1412.6980
- Loshchilov & Hutter, Decoupled Weight Decay Regularization (AdamW, 2017): https://arxiv.org/abs/1711.05101
- Loshchilov & Hutter, SGDR: Stochastic Gradient Descent with Warm Restarts (cosine schedules, 2016): https://arxiv.org/abs/1608.03983
- Pascanu, Mikolov & Bengio, On the difficulty of training recurrent neural networks (gradient clipping, 2012): https://arxiv.org/abs/1211.5063
- CS231n notes, Neural Networks Part 3 (parameter updates): https://cs231n.github.io/neural-networks-3/
- PyTorch
torch.optimdocs: https://pytorch.org/docs/stable/optim.html
About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit c8d5c21, so the two always agree: the explanation, the code that builds it and the tests that prove it.