Search Tech Journey

Find topics, journeys and posts

6-month learning plan99 / 130
back to blog
mladvanced 55m read

S099 · Optimizers — SGD, Momentum, Adam, RMSprop

The four optimisers you'll ever use in practice — plain SGD, SGD-with-momentum, RMSprop, and Adam. Learn the update equations, when each one shines, and why Adam is the default that quietly wins 90% of the time. Includes a numpy implementation of all four on the same loss surface so you can see them race.

🤖Machine LearningM12 · Deep Learning· Session 099 of 130 90 min

🎯 Implement SGD, momentum, RMSprop, and Adam on the same loss and reason about learning-rate schedules — so you can pick and tune an optimiser without cargo-culting.

Why this session exists

The gradient tells you which way is downhill. The optimiser decides how far and how confidently to step. It's the difference between a model that trains to state-of-the-art in 3 hours and one that plateaus at 90% of that in 30. Adam is the answer 90% of the time — but the other 10% (and every "why is my loss oscillating?" bug) requires knowing what's inside. This session opens the box.

You will be able to
  • Write the update rule for SGD, momentum, RMSprop, and Adam from memory.
  • Explain the two problems momentum fixes and the two problems RMSprop fixes.
  • Understand why Adam is the default and what its two hyperparameters (β1, β2) do.
  • Diagnose ‘loss oscillating’, ‘loss stuck’, and ‘loss NaN’ from optimiser first principles.
  • Choose a learning rate schedule (constant, step, cosine, warmup) for a given problem.

Prerequisites

  • S097 · MLP forward pass — you have a model to optimise.
  • S098 · Backpropagation — you have gradients to step with.
  • Basic notion of moving averages helps intuition (session S045 has this).


(a) Intuition · 5 min

A ball rolling down a lumpy hillside
🌍 Real world

You drop a ball on a hillside with hills, valleys, and ridges. The ball's next move depends on: the local slope (gradient), how fast it's already moving (momentum), and whether it's on smooth terrain or a rocky bit (adaptive step size).

Plain SGD ignores momentum entirely — it takes a step, stops, checks the slope, takes another step. In a long narrow valley, it zig-zags across the walls forever.

SGD with momentum keeps track of the ball's velocity: a fast-moving ball rolls smoothly through the zig-zags.

Adam adds one more trick: adapt the step size per parameter based on how bumpy that parameter's landscape has been recently.

💻 Code world

Formally: at each step we have the gradient g. SGD updates w ← w − lr·g. That's it. Momentum keeps a moving average v of past gradients and steps in the direction of v instead: v ← β·v + (1−β)·g; w ← w − lr·v. RMSprop divides the step by a moving average of squared gradients — parameters with large recent gradients get smaller steps. Adam combines both: momentum + RMSprop + bias correction.

The three problems optimisers solve

Why we've evolved from SGD to Adam
  • Slow ravines — long thin valleys where SGD zig-zags. Momentum fixes: velocity smooths across oscillations.
  • Different parameter scales — some weights need big steps, some need tiny. RMSprop / Adam adapt lr per parameter.
  • Noisy mini-batches — small batches make gradients noisy. Momentum's moving average averages out the noise.
  • Sparse gradients (embeddings, NLP) — some parameters only appear rarely. Adam's per-parameter learning rate keeps them alive.
  • Warmup / decay — the right learning rate at start is not the right learning rate at the end. Schedules handle this.

The evolution

  1. 1847
    Cauchy · gradient descent
    First formalisation. Still exactly the same math today.
  2. 1964
    Polyak · heavy-ball momentum
    First momentum-based optimiser. Predates neural networks by decades.
  3. 1983
    Nesterov accelerated gradient
    Lookahead momentum — provably faster than heavy-ball for convex problems.
  4. 2011
    AdaGrad · Duchi et al.
    Per-parameter learning rates for sparse features. Aggressive decay.
  5. 2012
    RMSprop · Hinton (Coursera lecture)
    Moving-average variant of AdaGrad that doesn't decay to zero.
  6. 2014
    Adam · Kingma & Ba
    RMSprop + momentum + bias correction. Becomes the default within 18 months.

(b) Visual walkthrough · 15 min

The update rules, side by side

The four optimisers on the same loss surface

1baseline
Plain SGD

Zig-zags in narrow valleys. Slow in flat regions. Fastest per step but many more steps needed.

2vision
SGD + Momentum (β=0.9)

Smooths oscillations. Rolls through narrow valleys. Standard for image classification (ResNet, EfficientNet all use SGD-M).

3legacy
RMSprop

Adapts step per parameter. Great for RNNs and non-stationary problems. Nearly obsolete today — Adam does the same and more.

4default
Adam (β1=0.9, β2=0.999)

The safe default. Momentum + adaptive step + bias correction. Almost always converges reasonably.

5transformer
AdamW

Adam with decoupled weight decay. Fixes a subtle bug in Adam's L2 regularisation. The transformer world's default since 2019.

Choosing an optimiser

Adam / AdamW

The default for everything

  • Best out-of-the-box performance
  • Robust to learning-rate mis-tuning
  • Standard for transformers, NLP, LLMs
  • AdamW is the transformer standard (fixed weight decay)
SGD + Momentum + LR schedule

Best for CNN vision (ResNet, EfficientNet)

  • Often reaches lower final loss than Adam on vision tasks
  • Requires careful LR schedule (step decay or cosine)
  • Standard for ImageNet-scale training
  • The recipe: lr=0.1, momentum=0.9, decay lr by 10× at plateau
RMSprop

Historical, mostly deprecated

  • Adam does the same thing plus momentum
  • Occasionally used in RL for policy gradients
  • Skip unless you have a specific reason
Plain SGD

Baselines and pedagogy only

  • Great for teaching
  • Sometimes wins on very small models with lots of tuning
  • Not competitive on most real problems
  • Use only if you want to eliminate optimiser as a variable

Learning rate schedules

LR schedules — pick one, always

Constant lr
Only for very short runs or debugging. Real training always uses a schedule.
toy
Step decay
lr /= 10 every N epochs (or when loss plateaus). Simple, good baseline. Standard for CNNs.
vision
Cosine annealing
lr decays smoothly from lr_max to lr_min following a cosine curve. Better than step decay in most cases.
modern
Warmup + cosine
First N steps ramp lr from 0 to lr_max (warmup); then cosine decay. Standard for transformers.
transformer
1cycle / OneCycle · Leslie Smith
lr ramps up to a max then back down within one cycle. Trains faster than plain step decay. Standard in fast.ai.
fastai

Common misconception
✗ What most people think

"Adam is adaptive, so it tunes the learning rate for me. That's the whole point — I can leave the learning rate at the default and let the optimiser handle it."

✓ What is actually true

Adam adapts the relative step size per parameter by dividing by a running estimate of gradient magnitude. It does not know the correct global scale, and the base learning rate still multiplies everything. Adam makes the learning rate less catastrophic to get wrong, not unnecessary to set — the difference between 3e-4 and 3e-2 is still the difference between convergence and divergence, and learning-rate schedule remains the single highest-impact hyperparameter in deep learning.

Why the myth is so sticky

The myth is sticky because Adam's default genuinely works across a remarkable range of problems, so most people never observe a failure and conclude the parameter is inert. The name reinforces it: "adaptive moment estimation" gets shortened to "adaptive", which is heard as "self-tuning". And the per-parameter normalisation does something real and visible — it rescues problems where SGD would need careful tuning — so the partial truth is repeatedly confirmed and never contradicted until you hit a problem where the default scale is wrong.

Prove it to yourself

Hold everything constant and sweep only the base learning rate under Adam:

for lr in [1e-5, 1e-4, 3e-4, 1e-3, 1e-2, 1e-1]:
    model = make_model()
    opt = torch.optim.Adam(model.parameters(), lr=lr)
    losses = train_n_steps(model, opt, n=300)
    print(lr, losses[0], losses[-1])

# 1e-5  -> barely moves (underfits within budget)
# 3e-4  -> converges cleanly
# 1e-1  -> diverges or NaN
# 'adaptive' did not save you at either extreme
From first principles
Start with the question

Why does momentum accelerate convergence, and why specifically in ravines — loss surfaces that are steep in one direction and nearly flat in another? Averaging past gradients sounds like it should merely smooth things, not speed them up.

  1. 1
    Consider a quadratic bowl that is far steeper along one axis than another — the condition number of its curvature is large. This is not pathological; it is the normal shape of neural loss surfaces, especially with unscaled inputs.
    forced by · different parameters influence the loss at wildly different scales, which is exactly what an ill-conditioned Hessian means
  2. 2
    Plain gradient descent takes a step proportional to the gradient. The steep direction has a large gradient, so it takes large steps there; the flat direction has a small gradient and takes tiny steps.
    forced by · the step is the same scalar learning rate times the gradient, with no per-direction adjustment
  3. 3
    Now the learning rate is trapped. It must be small enough to avoid overshooting and oscillating across the steep direction, and that same small value makes progress along the flat direction — which is where the minimum actually lies — glacially slow.
    forced by · one scalar must simultaneously satisfy a stability constraint from the steepest curvature and a speed requirement from the flattest
  4. 4
    Observe the structure of the resulting path: in the steep direction the gradient alternates sign as it oscillates back and forth, while in the flat direction it points consistently the same way.
    forced by · oscillation means overshooting past the valley floor and coming back
  5. 5
    So take an exponentially weighted average of past gradients. Alternating-sign components largely cancel; consistent-sign components accumulate and grow toward roughly 1/(1−β) times a single gradient. Momentum damps exactly the oscillation and amplifies exactly the progress.
    forced by · averaging is destructive interference for sign-flipping terms and constructive for consistent ones
⇒ Therefore

Therefore momentum is not smoothing for its own sake — it is a sign-based filter that exploits the geometric difference between oscillation and consistent descent. With β = 0.9 the effective step along a consistent direction is roughly 10× a single gradient step.

And note what this predicts: any mechanism that equalises the scale across directions should reduce the need for momentum and permit larger learning rates. That is precisely what input normalisation, BatchNorm, and LayerNorm do, and precisely why they let you train at learning rates that would diverge without them. It also predicts Adam's second component — dividing by the RMS of recent gradients directly rescales each direction, attacking the conditioning problem rather than merely coping with it.

Mental modelA ball with mass rolling on a per-axis rescaled surface

SGD is a massless particle that instantly moves in the direction of steepest local descent — so it follows every wiggle and stalls in ravines. Momentum gives it mass: it accumulates velocity, so it coasts through small bumps and builds speed along consistent slopes.

Adam adds a second idea: divide each parameter's step by the RMS of its own recent gradients. Parameters with consistently large gradients get proportionally smaller steps and vice versa, so every axis moves at a comparable rate. Momentum fixes where you are heading; the RMS normalisation fixes how far each axis moves. Adam is simply both at once.

  • Adam = momentum (first moment) + per-parameter RMS normalisation (second moment) + bias correction, since both running averages start at zero and would otherwise be biased low early on.
  • Learning-rate schedule beats optimiser choice. Warmup then cosine or step decay is worth more than swapping SGD for Adam on most problems.
  • Adam stores two extra state tensors per parameter, so optimiser state costs roughly 2× the model size in memory — often the reason a model trains with SGD but OOMs with Adam.
  • Adam's normalisation breaks L2 weight decay, because the decay term gets divided by the same RMS. AdamW applies decay directly to the weights instead, which is why it is the default in modern training.
🔔 Fires when you see

Fire this the moment you see: loss going NaN in early steps · training loss oscillating without descending · a flat loss curve that never moves · Adam used with weight_decay instead of AdamW · a fixed learning rate with no schedule · a transformer trained without warmup · optimiser state doubling your memory footprint unexpectedly.

The tradeoff

Which optimiser do you train with: SGD with momentum, Adam/AdamW, or SGD after an Adam warmup?

SGD with momentum
+ you gain frequently generalises better on vision tasks — the noisier, less-adapted updates appear to favour flatter minima; uses only one state tensor per parameter, so it costs half the optimiser memory of Adam; and it has fewer moving parts to misconfigure
− you pay much more sensitive to the learning rate and to a good schedule, so tuning cost is real; converges slowly on ill-conditioned or sparse problems; and it essentially requires careful input normalisation to behave
pick when vision models with established recipes, where the schedule is known and squeezing out final accuracy matters more than time-to-first-result
AdamW
+ you gain robust across an enormous range of architectures with minimal tuning, converges fast in early training, and handles sparse gradients and wildly varying per-parameter scales — which is why it is effectively mandatory for transformers and any model with embedding layers
− you pay doubles optimiser memory, which at large model scale is a serious constraint; can converge to slightly worse-generalising solutions than well-tuned SGD on some vision benchmarks; and its adaptivity can mask a badly conditioned model that you would otherwise have noticed and fixed
pick when transformers, embeddings, sparse features, anything new where you do not yet have a tuned recipe, and any situation where iteration speed matters more than the last fraction of accuracy
Adam first, then switch to SGD
+ you gain Adam's fast, forgiving early progress gets you out of the difficult initial region, and switching to SGD for the later phase can recover its generalisation advantage — attempts to get both properties
− you pay the switch point becomes another hyperparameter with no principled way to choose it, and the transition itself can destabilise training since the two optimisers have very different effective step sizes; more code and more ways to be subtly wrong
pick when you are chasing a benchmark number and have the budget to tune the switch point empirically — rarely worth it in production
What a senior engineer actually does

Use AdamW with a warmup-then-cosine schedule as the default. It is robust, it is what modern architectures were developed against, and it removes an entire class of tuning problems so you can spend your attention on data and features — which almost always move the metric more than the optimiser does.

The genuinely important discipline is not which optimiser but the schedule and the diagnosis. Plot the loss curve from step one: a flat curve means the learning rate is too low or gradients are not flowing, oscillation means it is too high, and an early NaN usually means a bad initialisation, a missing normalisation, or a numerically unstable loss. Those readings tell you far more than any optimiser comparison, and they cost nothing.


(c) Hands-on · 25 min

Implement all four optimisers in numpy and race them on a synthetic anisotropic quadratic (the classic "narrow valley" problem) plus a small MLP on a toy classification task. Save as optimizer_lab.py, uv run optimizer_lab.py.

"""optimizer_lab.py — implement SGD, Momentum, RMSprop, Adam side-by-side."""
from __future__ import annotations
import numpy as np
 
RNG = np.random.default_rng(0)
 
 
# ---------------- optimizers ----------------
class SGD:
    def __init__(self, params: dict, lr: float = 0.1) -> None:
        self.params, self.lr = params, lr
    def step(self, grads: dict) -> None:
        for k in self.params:
            self.params[k] -= self.lr * grads[k]
 
 
class Momentum:
    def __init__(self, params: dict, lr: float = 0.1, beta: float = 0.9) -> None:
        self.params, self.lr, self.beta = params, lr, beta
        self.v = {k: np.zeros_like(v) for k, v in params.items()}
    def step(self, grads: dict) -> None:
        for k in self.params:
            self.v[k] = self.beta * self.v[k] + grads[k]
            self.params[k] -= self.lr * self.v[k]
 
 
class RMSprop:
    def __init__(self, params: dict, lr: float = 0.01, beta: float = 0.9, eps: float = 1e-8) -> None:
        self.params, self.lr, self.beta, self.eps = params, lr, beta, eps
        self.s = {k: np.zeros_like(v) for k, v in params.items()}
    def step(self, grads: dict) -> None:
        for k in self.params:
            self.s[k] = self.beta * self.s[k] + (1 - self.beta) * grads[k] ** 2
            self.params[k] -= self.lr * grads[k] / (np.sqrt(self.s[k]) + self.eps)
 
 
class Adam:
    def __init__(self, params: dict, lr: float = 1e-3, b1: float = 0.9, b2: float = 0.999,
                 eps: float = 1e-8) -> None:
        self.params, self.lr = params, lr
        self.b1, self.b2, self.eps = b1, b2, eps
        self.m = {k: np.zeros_like(v) for k, v in params.items()}
        self.v = {k: np.zeros_like(v) for k, v in params.items()}
        self.t = 0
    def step(self, grads: dict) -> None:
        self.t += 1
        for k in self.params:
            self.m[k] = self.b1 * self.m[k] + (1 - self.b1) * grads[k]
            self.v[k] = self.b2 * self.v[k] + (1 - self.b2) * grads[k] ** 2
            m_hat = self.m[k] / (1 - self.b1 ** self.t)   # bias correction
            v_hat = self.v[k] / (1 - self.b2 ** self.t)
            self.params[k] -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
 
 
# ---------------- race on narrow ellipsoid ----------------
def ellipsoid_race():
    """Minimise f(x, y) = 0.5·(a·x² + b·y²). Very stretched → SGD zig-zags."""
    a, b = 1.0, 20.0
    def loss(p):  return 0.5 * (a * p["x"] ** 2 + b * p["y"] ** 2)
    def grad(p):  return {"x": a * p["x"], "y": b * p["y"]}
 
    print("\n=== narrow-valley race (a=1, b=20) — steps to loss < 1e-3 ===")
    for name, opt_cls, lr in [
        ("SGD",      SGD,      0.05),
        ("Momentum", Momentum, 0.05),
        ("RMSprop",  RMSprop,  0.5),
        ("Adam",     Adam,     0.3),
    ]:
        p = {"x": np.array(-5.0), "y": np.array(-1.0)}
        opt = opt_cls(p, lr=lr) if opt_cls in (SGD,) else opt_cls(p, lr=lr)
        for step in range(1, 2001):
            opt.step(grad(p))
            if loss(p) < 1e-3:
                print(f"  {name:<10s}: converged in {step:>4d} steps (final={loss(p):.2e})")
                break
        else:
            print(f"  {name:<10s}: did NOT converge in 2000 (final={loss(p):.2e})")
 
 
# ---------------- MLP race on classification ----------------
def relu(z):      return np.maximum(0, z)
def relu_grad(z): return (z > 0).astype(z.dtype)
def softmax(z):
    z = z - z.max(axis=1, keepdims=True)
    e = np.exp(z); return e / e.sum(axis=1, keepdims=True)
 
 
def init_mlp(sizes):
    p = {}
    for i in range(len(sizes) - 1):
        fin, fout = sizes[i], sizes[i + 1]
        p[f"W{i+1}"] = RNG.normal(0, np.sqrt(2.0 / fin), (fin, fout))
        p[f"b{i+1}"] = np.zeros(fout)
    return p
 
 
def forward_backward(p, X, y_oh):
    z1 = X @ p["W1"] + p["b1"]; a1 = relu(z1)
    z2 = a1 @ p["W2"] + p["b2"]; probs = softmax(z2)
    n = X.shape[0]
    loss = -float(np.mean(np.sum(y_oh * np.log(probs + 1e-12), axis=1)))
    dz2 = (probs - y_oh) / n
    grads = {
        "W2": a1.T @ dz2, "b2": dz2.sum(0),
    }
    da1 = dz2 @ p["W2"].T
    dz1 = da1 * relu_grad(z1)
    grads["W1"] = X.T @ dz1
    grads["b1"] = dz1.sum(0)
    return loss, grads, probs
 
 
def mlp_race():
    from sklearn.datasets import make_classification
    X, y = make_classification(n_samples=1500, n_features=10, n_informative=6,
                               n_classes=3, random_state=0)
    y_oh = np.zeros((y.shape[0], 3), dtype=np.float32); y_oh[np.arange(y.shape[0]), y] = 1.0
 
    print("\n=== MLP race — final loss after 50 epochs, batch=64 ===")
    for name, opt_cls, lr in [
        ("SGD",      SGD,      0.1),
        ("Momentum", Momentum, 0.1),
        ("RMSprop",  RMSprop,  0.005),
        ("Adam",     Adam,     0.003),
    ]:
        p = init_mlp([10, 32, 3])
        opt = opt_cls(p, lr=lr)
        for _ in range(50):
            idx = RNG.permutation(len(y))
            for start in range(0, len(y), 64):
                b = idx[start:start + 64]
                _, grads, _ = forward_backward(p, X[b], y_oh[b])
                opt.step(grads)
        loss, _, probs = forward_backward(p, X, y_oh)
        acc = float((probs.argmax(1) == y).mean())
        print(f"  {name:<10s}: loss={loss:.4f}  acc={acc:.4f}  (lr={lr})")
 
 
if __name__ == "__main__":
    ellipsoid_race()
    mlp_race()

Anatomy of the script

Anatomy of the script

Line 26 · Momentum: v = β·v + g
Simple momentum (no lr in the update to v). Update: w -= lr·v. Equivalent formulation to torch's SGD(momentum=0.9). β=0.9 is universal.
momentum
Line 33 · RMSprop: s = β·s + (1−β)·g²
Squared-gradient running average. Update divides gradient by sqrt(s), giving effective per-parameter lr. eps=1e-8 prevents /0.
rmsprop
Line 47-49 · Adam m and v
Two moving averages: m for momentum (β1=0.9), v for second moment (β2=0.999). Note β2 is much larger — squared-gradient average changes more slowly.
adam
Line 50-51 · bias correction
m and v start at zero; without correction they're biased toward zero early in training. m_hat = m / (1 − β1^t) undoes this. Critical for the first ~100 steps.
correction
Line 62 · ellipsoid a=1, b=20
The classic anisotropic bowl. The narrow direction (b=20) forces SGD to take tiny steps or it will overshoot. Momentum + Adam navigate it easily.
toy
Line 95 · lr defaults are DIFFERENT per optimiser
SGD: 0.1. Momentum: 0.1. RMSprop: 0.005. Adam: 0.003. Never use the same lr across optimisers — Adam's adaptive step is already ‘large’.
lr
Try itFeel the bias correction of Adam

Modify Adam to skip bias correction (comment out the m_hat, v_hat lines and use m, v directly). Re-run the ellipsoid race. You'll see convergence takes 2-3× longer because the first ~200 steps are effectively at 10× smaller lr. This is why Kingma & Ba added bias correction — it makes Adam trainable from step 1 without a warmup.

💡 Hint · Without bias correction, the first few Adam steps are much smaller than they should be — the moving averages haven't warmed up yet.

(d) Production reality · 15 min

War story Every deep learning tutorial · common failure modethousands of confused practitioners
🔥 What broke

New DL practitioner copies an ImageNet training recipe (SGD + momentum, lr=0.1) for their NLP task. Loss doesn't move. They increase lr — loss NaN. They panic and switch to Adam with the same lr=0.1. Loss still NaN.

🧯 The fix
Different optimisers have different sensible LR ranges. Adam's default is 1e-3 (or 3e-4, ‘Karpathy's constant’); SGD's default is 0.01-0.1. Use the optimiser's default, then tune within 10× either direction — not from scratch.
🎓 Lesson to steal
Learning rate is not portable across optimisers. Copying an SGD recipe's lr into Adam (or vice versa) will fail dramatically. Always start from the optimiser's default lr.
Post-mortem
War story Google · Attention Is All You Need· 2017the paper that launched transformers
🔥 What broke
Early transformer training with plain Adam was unstable — models diverged in the first 1000 steps. Root cause: attention softmax + Adam produced huge gradient variance in the first steps before the running averages stabilised.
🧯 The fix

Standard transformer recipe includes LR warmup: for the first N steps (~4000 for original transformer, ~2000-10000 today), linearly ramp lr from 0 to lr_max. This lets Adam's running averages stabilise before big steps.

Then decay via inverse-square-root, cosine, or step schedule. Every major LLM does this.

🎓 Lesson to steal
Adam's bias correction handles small-time-scale instability; LR warmup handles the larger issue of gradient variance in early training. Both are effectively mandatory for training transformers past a few million parameters.
Post-mortem
War story Facebook AI Research · training ResNet from scratchImageNet at scale
🔥 What broke
Team trains ResNet-50 with Adam (‘Adam is the default, right?’). Reaches 75% top-1 accuracy. Same architecture with SGD-momentum + step LR schedule reaches 76.5%. Turns out on well-tuned vision problems, SGD often beats Adam by 0.5-2%.
🧯 The fix

SGD + momentum + cosine LR schedule is the standard recipe for ImageNet-scale vision training. Adam is often 1-2 points worse. NLP and RL usually go the other way — Adam wins.

🎓 Lesson to steal
Adam is the best default but not the best answer for every problem. Vision → SGD-momentum. NLP / LLMs → AdamW. RL → often Adam or its variants. If you're squeezing the last 1% out of a benchmark, always try both optimisers.
Post-mortem

Where this shows up in the rest of the plan

Optimiser choice ripples through every training pipeline downstream
S100 · PyTorch fundamentals
torch.optim.Adam(model.parameters(), lr=1e-3) — the one-liner version of what we built.
S101 · Regularisation
AdamW = Adam with decoupled weight decay. Understand Adam's update to understand why decoupling matters.
S105 · Transformers
LR warmup + cosine schedule + AdamW — the transformer recipe.
S117 · Fine-tuning LLMs
LoRA typically uses AdamW too — but at 100× smaller lr.
S123 · RLHF
Policy optimisation uses Adam/AdamW; value networks sometimes use SGD.
S128 · MLOps monitoring
Track gradient norm, param norm, and lr each step — alerts on optimiser pathologies.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

If you can't teach these three to a friend without notes, redo the session:

  1. What is momentum and what does it fix?
  2. What is Adam doing that plain SGD is not?
  3. When would you not use Adam, and what would you use instead?

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.