Weeks
Deep Learning and GenAIWeek 5 · Optimisation: SGD, Momentum, Adam and Learning-Rate Schedules
Week 5 Notes

Deep Learning,
Week 5

Introduction to DL and GenAI, Week 5: optimisation. Empirical versus true risk, mini-batch gradients, convexity, local minima and saddle points, the stability limit of gradient descent, momentum and Nesterov, Adagrad, RMSProp, Adam and AdamW, and learning-rate schedules. Every number was really run.

8 chapters13 worked examples~48 min read
Mini-batch SGDConvexitySaddle pointsLearning rateMomentumAdam / AdamWSchedulesInterview prep

Chapters

  1. 01What We Optimise and How Noisy It IsPart 1
  2. 02Convexity, Minima and Saddle PointsPart 2
  3. 03Gradient Descent, Learning Rate and NewtonPart 3
  4. 04Momentum and NesterovPart 4
  5. 05Adagrad, RMSProp, Adam and AdamWPart 5
  6. 06Schedules and Choosing an OptimiserPart 6
  7. 07Interview Question BankInterview
  8. 08Week 5 Summary SheetSummary
01
Part 1

What We Optimise and How Noisy It Is

Every network in this course is trained the same way: define a loss that measures how wrong the model is, then change the weights to make it smaller. That second step is optimisation, and this whole week is about doing it well. Before the algorithms, two ideas about what we are really minimising.

1.1True risk and empirical risk

What we want is a model that does well on all the data the world could throw at it: the true risk R(θ)=E(x,y)[ℓ(fθ(x),y)]R(\theta)=\mathbb{E}_{(x,y)}[\ell(f_\theta(x),y)]. We can never compute it, because we never see all the data. What we can compute is the average loss over the nn training examples we have: the empirical risk

R^(θ)=1n∑i=1nℓ(fθ(xi),yi)\hat R(\theta)=\frac1n\sum_{i=1}^{n}\ell\big(f_\theta(x_i),y_i\big)

Training minimises R^\hat R and hopes that RR comes down with it. Sometimes it does not. Here are three polynomials fitted to 10 noisy points of a sine wave; the empirical risk is measured on those 10 points, and the true risk is estimated on 20,000 fresh points:

main.py
import numpy as np
rng = np.random.default_rng(0)
f_true = np.sin                                       # the (unknown) truth we want to learn
x_tr = rng.uniform(0, 2 * np.pi, 10); y_tr = f_true(x_tr) + rng.normal(0, 0.2, 10)   # 10 noisy training points
x_new = rng.uniform(0, 2 * np.pi, 20000); y_new = f_true(x_new) + rng.normal(0, 0.2, 20000)   # fresh data stands in for the whole distribution

for degree in (1, 3, 9):
    coef = np.polyfit(x_tr, y_tr, degree)
    emp = np.mean((np.polyval(coef, x_tr) - y_tr) ** 2)          # empirical risk: the loss we actually minimise
    true = np.mean((np.polyval(coef, x_new) - y_new) ** 2)       # estimate of the true risk: the loss we care about
    print(f"degree {degree}: empirical risk {emp:10.4f}   true risk (estimated) {true:12.4f}   gap {true - emp:12.4f}")
Output
degree 1: empirical risk     0.2472   true risk (estimated)       0.4075   gap       0.1604
degree 3: empirical risk     0.0238   true risk (estimated)       0.0551   gap       0.0313
degree 9: empirical risk     0.0000   true risk (estimated)       0.3838   gap       0.3838

The degree-9 polynomial reaches an empirical risk of 0.0000, the best possible, and a true risk of 0.38, seven times worse than the modest degree-3 fit (0.055). Minimising the empirical risk to zero is therefore not the goal; it is a proxy, and the gap between the two is overfitting (Week 4). Optimisation tells us how to minimise; it does not tell us that we should minimise all the way.

1.2Batch, stochastic and mini-batch gradient descent

The gradient of R^\hat R is an average over all nn examples. Computing it exactly means a pass over the whole dataset for one small step, which is far too slow for millions of images. Three ways to estimate it:

MethodExamples per updateUpdates per epochBehaviour
Batch GDn1exact gradient, smooth, but one step per pass: slow and memory-hungry
Stochastic GD (SGD)1nvery noisy, very cheap per step
Mini-batch GDB (32-256)n/Bthe practical middle: GPUs process a batch in parallel, noise is moderate

An epoch is one pass over the whole training set; an iteration is one weight update (one mini-batch). With n=10,000n=10{,}000 and B=100B=100 there are 100 iterations per epoch. In practice "SGD" almost always means the mini-batch version.

1.3How noisy is a mini-batch gradient? Measured

18.40283.015641.0625120.38140960.14mini-batch size (log scale)noise in the gradient (std, log scale)every x8 in batch sizecuts the noise by about2.8 = sqrt(8)the average is the same;only the noise shrinks
Figure 1. Same average, shrinking noise
main.py
import numpy as np
rng = np.random.default_rng(0)
N = 20000
X = rng.normal(size=N); y = 3 * X + rng.normal(scale=1.0, size=N)
w = 0.0                                                        # gradient of the squared error at w = 0 is -2 x y
full = np.mean(-2 * X * (y - w * X))
print("full-batch gradient:", round(full, 3))
print("batch size   mean of mini-batch gradients   std of the mini-batch gradient   std x sqrt(B)")
for B in (1, 8, 64, 512, 4096):
    grads = []
    for _ in range(2000):
        idx = rng.integers(0, N, B)
        grads.append(np.mean(-2 * X[idx] * (y[idx] - w * X[idx])))
    grads = np.array(grads)
    print(f"{B:10d}   {grads.mean():28.3f}   {grads.std():30.3f}   {grads.std() * np.sqrt(B):12.2f}")
print("same mean (an unbiased estimate), but the noise falls like 1/sqrt(B): that is why a bigger batch tolerates a bigger learning rate")
Output
full-batch gradient: -5.959
batch size   mean of mini-batch gradients   std of the mini-batch gradient   std x sqrt(B)
         1                         -5.848                            8.402           8.40
         8                         -5.898                            3.015           8.53
        64                         -5.947                            1.062           8.49
       512                         -5.962                            0.381           8.61
      4096                         -5.960                            0.140           8.93
same mean (an unbiased estimate), but the noise falls like 1/sqrt(B): that is why a bigger batch tolerates a bigger learning rate

Two facts are visible. The mean of the mini-batch gradients matches the full-batch one at every batch size (about −5.96-5.96): the estimate is unbiased. The spread falls like 1/B1/\sqrt{B}: the last column, std ×B\times\sqrt{B}, stays near 8.5 instead of shrinking. So a batch 64 times larger has only 8 times less noise: bigger batches pay a lot of compute for a slowly shrinking benefit. A little noise is not even bad: it can shake the weights out of poor spots, which is one reason small-batch training often generalises well.

Say it precisely. Mini-batch gradients are unbiased (correct on average) but noisy (variance ∝1/B\propto 1/B). "SGD is faster because it is more accurate" is wrong; it is faster per update because it is cheaper, even though each update is less accurate.

1.4Chapter summary

  • We minimise the empirical risk R^\hat R as a proxy for the true risk RR; driving R^\hat R to 0 can make RR worse (0.0000 vs 0.38 measured).
  • Batch GD: exact, slow. SGD: noisy, cheap. Mini-batch: the practical compromise.
  • Mini-batch gradient: unbiased, variance ∝1/B\propto1/B (noise falls by B\sqrt{B}).
Next · Part 2Convexity, Minima and Saddle Points
02
Part 2

Convexity, Minima and Saddle Points

Gradient descent walks downhill. Whether that walk ends somewhere good depends on the shape of the landscape, and the vocabulary for shapes is convex, local minimum and saddle point.

2.1Convex functions

A function is convex if the straight line (chord) between any two points of its graph never goes below the graph:

f(λx+(1−λ)y)  ≤  λf(x)+(1−λ)f(y)for all x,y and λ∈[0,1]f\big(\lambda x+(1-\lambda)y\big)\;\le\;\lambda f(x)+(1-\lambda)f(y)\qquad\text{for all }x,y\text{ and }\lambda\in[0,1]

Test it by brute force: draw 100,000 random chords and count how often the chord dips below the curve.

main.py
import numpy as np
rng = np.random.default_rng(1)
def violations(f, trials=100000):
    x, y = rng.uniform(-3, 3, (2, trials)); lam = rng.uniform(0, 1, trials)
    return int(np.sum(f(lam * x + (1 - lam) * y) > lam * f(x) + (1 - lam) * f(y) + 1e-12))
print("chord above the curve fails for x^2   :", violations(lambda t: t ** 2), "of 100000 random chords (it is convex)")
print("chord above the curve fails for |x|   :", violations(np.abs), "(convex, not smooth)")
print("chord above the curve fails for sin(x):", violations(np.sin), "(NOT convex)")
print("chord above the curve fails for x^3   :", violations(lambda t: t ** 3), "(NOT convex on all of R)")
Output
chord above the curve fails for x^2   : 0 of 100000 random chords (it is convex)
chord above the curve fails for |x|   : 0 (convex, not smooth)
chord above the curve fails for sin(x): 50034 (NOT convex)
chord above the curve fails for x^3   : 50104 (NOT convex on all of R)

x2x^2 and ∣x∣|x| never break the rule (note ∣x∣|x| has a corner: convex does not need smooth). sin⁡x\sin x breaks it about half the time, and x3x^3 too (it bends the wrong way for x<0x<0). For a twice-differentiable function there is an easier test: convex   ⟺  \iff the Hessian is positive semi-definite everywhere (all eigenvalues ≥0\ge0, the curvature never points down).

main.py
import torch
H = torch.autograd.functional.hessian
cases = {
    "x^2 + y^2          ": lambda p: p[0] ** 2 + p[1] ** 2,
    "x^2 + 5 y^2        ": lambda p: p[0] ** 2 + 5 * p[1] ** 2,
    "x^2 - y^2 (saddle) ": lambda p: p[0] ** 2 - p[1] ** 2,
    "x^4 - 3 x^2 + y^2  ": lambda p: p[0] ** 4 - 3 * p[0] ** 2 + p[1] ** 2,
}
for name, f in cases.items():
    for pt in ([0.0, 0.0], [1.5, 0.0]):
        eig = torch.linalg.eigvalsh(H(f, torch.tensor(pt)))
        verdict = "locally convex here" if bool((eig >= -1e-6).all()) else "NOT convex here"
        print(f"f = {name} at {pt}: Hessian eigenvalues {[round(v, 2) for v in eig.tolist()]} -> {verdict}")
print("a function is convex iff its Hessian is positive semi-definite EVERYWHERE; x^4-3x^2+y^2 fails at the origin")
Output
f = x^2 + y^2           at [0.0, 0.0]: Hessian eigenvalues [2.0, 2.0] -> locally convex here
f = x^2 + y^2           at [1.5, 0.0]: Hessian eigenvalues [2.0, 2.0] -> locally convex here
f = x^2 + 5 y^2         at [0.0, 0.0]: Hessian eigenvalues [2.0, 10.0] -> locally convex here
f = x^2 + 5 y^2         at [1.5, 0.0]: Hessian eigenvalues [2.0, 10.0] -> locally convex here
f = x^2 - y^2 (saddle)  at [0.0, 0.0]: Hessian eigenvalues [-2.0, 2.0] -> NOT convex here
f = x^2 - y^2 (saddle)  at [1.5, 0.0]: Hessian eigenvalues [-2.0, 2.0] -> NOT convex here
f = x^4 - 3 x^2 + y^2   at [0.0, 0.0]: Hessian eigenvalues [-6.0, 2.0] -> NOT convex here
f = x^4 - 3 x^2 + y^2   at [1.5, 0.0]: Hessian eigenvalues [2.0, 21.0] -> locally convex here
a function is convex iff its Hessian is positive semi-definite EVERYWHERE; x^4-3x^2+y^2 fails at the origin

The last function x4−3x2+y2x^4-3x^2+y^2 is convex near x=1.5x=1.5 (eigenvalues 2 and 21) but not at the origin (an eigenvalue of −6-6), so it is not convex: a function must pass the test everywhere. The saddle x2−y2x^2-y^2 fails everywhere.

Why convexity is the good case

For a convex function every local minimum is a global minimum, and a zero gradient proves you are at one. So gradient descent with a small enough step cannot get trapped. Linear regression and logistic regression have convex losses; neural networks do not, which is why this week's algorithms matter.

Jensen's inequality

The chord definition, extended from two points to a whole distribution, gives Jensen's inequality: for convex ff, f(E[X])≤E[f(X)]f(\mathbb{E}[X])\le\mathbb{E}[f(X)], and the inequality flips for concave ff. It appears in later weeks (for example in the bound that GANs and diffusion models rest on), so see it once with numbers:

main.py
import numpy as np
rng = np.random.default_rng(0)
X = rng.normal(2.0, 1.5, 100000)                      # a random variable with mean 2, variance 2.25
f = lambda x: x ** 2                                  # convex
print("f(E[X]) =", round(f(X.mean()), 4), "   E[f(X)] =", round(f(X).mean(), 4))
print("f(E[X]) <= E[f(X)]:", f(X.mean()) <= f(X).mean(), "  (the gap is exactly Var(X) =", round(X.var(), 3), ")")
g = np.log                                            # concave: the inequality flips
Y = rng.uniform(1, 5, 100000)
print("log: f(E[Y]) =", round(g(Y.mean()), 4), ">= E[f(Y)] =", round(g(Y).mean(), 4))
Output
f(E[X]) = 3.9946    E[f(X)] = 6.2451
f(E[X]) <= E[f(X)]: True   (the gap is exactly Var(X) = 2.251 )
log: f(E[Y]) = 1.0977 >= E[f(Y)] = 1.0112

For f(x)=x2f(x)=x^2 the gap E[X2]−(EX)2\mathbb{E}[X^2]-(\mathbb{E}X)^2 is exactly the variance (2.251 here), which is also why variance is never negative.

2.2Local minima and the non-convex world

global minimumlocal minimum-3.514-1.070f(x) = x^4 - 3x^2 + xstart at -2.0 or -0.5:ends at the global minimumstart at +0.5 or +2.0:stuck in the local minimumgradient descent only ever goes downhill from where it starts
Figure 2. Two minima: where you start decides where you end
main.py
# f(x) = x^4 - 3x^2 + x has TWO local minima; gradient descent ends in the one nearest its start
f = lambda x: x ** 4 - 3 * x ** 2 + x
df = lambda x: 4 * x ** 3 - 6 * x + 1
for start in (-2.0, -0.5, 0.5, 2.0):
    x = start
    for _ in range(2000):
        x -= 0.01 * df(x)
    print(f"start {start:+.1f}  ->  ends at x = {x:+.4f}, f = {f(x):+.4f}")
print("two different answers from the same function: the left one (f = -3.514) is the global minimum, the right one (f = -1.070) is only a local minimum")
Output
start -2.0  ->  ends at x = -1.3008, f = -3.5139
start -0.5  ->  ends at x = -1.3008, f = -3.5139
start +0.5  ->  ends at x = +1.1309, f = -1.0702
start +2.0  ->  ends at x = +1.1309, f = -1.0702
two different answers from the same function: the left one (f = -3.514) is the global minimum, the right one (f = -1.070) is only a local minimum

The same algorithm, the same learning rate, two different answers, decided only by the starting point. In real networks there are astronomically many such valleys, and the surprising empirical finding of deep learning is that most reachable minima are about equally good, so getting stuck in a bad one is rarer than the picture suggests. The troublemaker in high dimensions is a different shape.

2.3Saddle points

along x: f = x^2 (a valley)along y: f = -y^2 (a hill)at the origin:gradient = (0, 0)Hessian eigenvalues+2 and -2a saddle, not a minimumf(x, y) = x^2 - y^2: a minimum in one direction, a maximum in the other
Figure 3. A saddle: a minimum along one axis, a maximum along the other

A saddle point has a zero gradient but is neither a minimum nor a maximum: it curves up in some directions and down in others (like a horse saddle or a mountain pass). The Hessian there has both positive and negative eigenvalues. Check it, then run descent from two starts:

main.py
import torch
f = lambda p: p[0] ** 2 - p[1] ** 2                      # a minimum along x, a maximum along y: a saddle
p0 = torch.zeros(2, requires_grad=True)
f(p0).backward()
print("gradient at the origin:", p0.grad.tolist(), " <- zero, yet the origin is NOT a minimum")
H = torch.autograd.functional.hessian(f, torch.zeros(2))
print("Hessian eigenvalues   :", torch.linalg.eigvalsh(H).tolist(), " <- one negative: some direction goes down")

def descend(start, steps=60, lr=0.1):
    p = torch.tensor(start)
    for _ in range(steps):
        g = torch.stack([2 * p[0], -2 * p[1]])
        p = p - lr * g
    return p.tolist()
print("start (1, 0.000) exactly on the ridge ->", [round(v, 6) for v in descend([1.0, 0.0])], " (walks into the saddle and stops)")
print("start (1, 0.001) a hair off the ridge ->", [round(v, 2) for v in descend([1.0, 0.001])], " (slides away, but slowly at first)")
Output
gradient at the origin: [0.0, 0.0]  <- zero, yet the origin is NOT a minimum
Hessian eigenvalues   : [-2.0, 2.0]  <- one negative: some direction goes down
start (1, 0.000) exactly on the ridge -> [2e-06, 0.0]  (walks into the saddle and stops)
start (1, 0.001) a hair off the ridge -> [0.0, 56.35]  (slides away, but slowly at first)

Two lessons. Zero gradient does not mean minimum: always say "critical point", and use the Hessian (all eigenvalues positive: minimum; mixed: saddle; all negative: maximum) to classify it. And descent does escape a saddle, but only if it is nudged off the ridge: from exactly y=0y=0 it stops dead at the saddle, from y=0.001y=0.001 the escape direction grows by 1.2 each step (0.001×1.260≈560.001\times1.2^{60}\approx56), slow at first and then fast. A long, flat plateau near a saddle is where training seems to stall for ages.

2.4Why saddles, not minima, dominate in high dimensions

At a critical point in dd dimensions there are dd curvature directions. For it to be a true minimum all dd must curve upward. If each is roughly a coin flip, that is a 2−d2^{-d} event. A quick simulation with random symmetric matrices as stand-in Hessians:

main.py
import numpy as np
rng = np.random.default_rng(0)
# Model the Hessian at a critical point as a random symmetric matrix.
# It is a local minimum only if ALL eigenvalues are positive.
print(" dim   P(all eigenvalues > 0)   (rest are saddles or maxima)")
for d in (1, 2, 3, 5, 8, 12, 20):
    ok = 0
    trials = 4000
    for _ in range(trials):
        A = rng.normal(size=(d, d)); H = (A + A.T) / 2
        ok += bool(np.all(np.linalg.eigvalsh(H) > 0))
    print(f"{d:4d}   {ok / trials:8.4f}")
Output
 dim   P(all eigenvalues > 0)   (rest are saddles or maxima)
   1     0.4868
   2     0.1547
   3     0.0240
   5     0.0000
   8     0.0000
  12     0.0000
  20     0.0000

The chance that all eigenvalues are positive collapses: 49% in one dimension, 2.4% in three, and below 1 in 4000 from five dimensions up. A network has millions of dimensions, so a random critical point is almost surely a saddle, not a minimum. Honest caveat: real Hessians are not random matrices, so this is an intuition for why saddles are common, not a measurement of any network. It is the standard argument for why saddles, not local minima, are the main obstacle in deep learning, and why algorithms that keep moving (momentum, adaptive methods) help.

2.5Chapter summary

  • Convex: chords stay above the graph; Hessian PSD everywhere. Then every local minimum is global.
  • Neural-network losses are non-convex: the end point depends on the start (two minima, -3.514 vs -1.070).
  • Zero gradient = critical point; classify with the Hessian. Saddles (mixed signs) dominate in high dimensions.
  • Jensen: convex f(EX)≤Ef(X)f(\mathbb{E}X)\le\mathbb{E}f(X); the gap for x2x^2 is the variance.
Next · Part 3Gradient Descent, Learning Rate and Newton
03
Part 3

Gradient Descent, Learning Rate and Newton

The basic update, repeated: move a little against the gradient, θ←θ−η ∇L(θ)\theta\leftarrow\theta-\eta\,\nabla L(\theta). The only knob is the learning rate η\eta. Too small is slow; too large is not just fast and sloppy, it is unstable. Here is exactly where the line is.

3.1The learning rate and its stability limit

Take the simplest bowl, L=12λw2L=\tfrac12\lambda w^2, whose gradient is λw\lambda w. One step gives w←(1−ηλ) ww\leftarrow(1-\eta\lambda)\,w. The weight shrinks to zero only if ∣1−ηλ∣<1|1-\eta\lambda|<1, that is η<2/λ\eta<2/\lambda. A steeper bowl (bigger curvature λ\lambda) tolerates a smaller step. If there are several directions with curvatures λmin⁡…λmax⁡\lambda_{\min}\ldots\lambda_{\max}, the steepest one sets the limit, η<2/λmax⁡\eta<2/\lambda_{\max}, while the flattest one sets the speed.

Test it on an elongated bowl with curvatures 1 and 50 (rotated 45 degrees, so no direction is special). The predicted limit is 2/50=0.042/50=0.04:

main.py
import numpy as np
# an elongated bowl: curvature 1 in one direction and 50 in the other, rotated 45 degrees
th = np.pi / 4; Q = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
A = Q @ np.diag([1.0, 50.0]) @ Q.T
f = lambda p: 0.5 * p @ A @ p
g = lambda p: A @ p

def steps_to_converge(lr, max_steps=20000):
    p = np.array([1.0, 0.3])
    for t in range(1, max_steps + 1):
        p = p - lr * g(p)
        if not np.isfinite(p).all() or np.linalg.norm(p) > 1e6:
            return None
        if np.linalg.norm(g(p)) < 1e-3:
            return t
    return None

print("curvatures 1 and 50, so a stable step needs lr < 2/50 = 0.04")
for lr in (0.001, 0.01, 0.02, 0.039, 0.0399, 0.041):
    n = steps_to_converge(lr)
    print(f"lr = {lr:<7}: " + (f"converged in {n} steps" if n else "did not converge (diverged)"))
Output
curvatures 1 and 50, so a stable step needs lr < 2/50 = 0.04
lr = 0.001  : converged in 6821 steps
lr = 0.01   : converged in 679 steps
lr = 0.02   : converged in 338 steps
lr = 0.039  : converged in 199 steps
lr = 0.0399 : converged in 2019 steps
lr = 0.041  : did not converge (diverged)

Exactly as predicted: below 0.04 it converges, at 0.041 it diverges. Two more details. Going from η=0.001\eta=0.001 to 0.01 to 0.02 to 0.039 cuts the steps from 6821 to 679 to 338 to 199, because the slow, flat direction needs a big step. But at 0.0399, just under the limit, it needs 2019 steps, ten times more: the steep direction is multiplied by 1−0.0399×50=−0.9951-0.0399\times50=-0.995 each step and so flips sign and almost never dies away. The best step is not "as large as stable" but η∗=2/(λmin⁡+λmax⁡)=2/51≈0.039\eta^*=2/(\lambda_{\min}+\lambda_{\max})=2/51\approx0.039.

3.2Conditioning: why elongated bowls are hard

The ratio κ=λmax⁡/λmin⁡\kappa=\lambda_{\max}/\lambda_{\min} is the condition number. At the best step, each iteration shrinks the error by the factor (κ−1)/(κ+1)(\kappa-1)/(\kappa+1). For κ=50\kappa=50 that is 49/51=0.96149/51=0.961, and 0.961199≈3.5×10−40.961^{199}\approx3.5\times10^{-4}: the 199 steps above. For κ=1\kappa=1 (a round bowl) one step lands on the minimum; for κ=104\kappa=10^4 you would need about 100 times more steps. Plain gradient descent is slow whenever the landscape is much steeper in one direction than in another, which is exactly the shape of real loss surfaces, and all of this week's improvements attack that.

3.3The second-order answer: Newton's method

If the problem is that the gradient ignores curvature, use the curvature: θ←θ−H−1∇L\theta\leftarrow\theta-H^{-1}\nabla L, where HH is the Hessian. On a quadratic bowl this lands on the exact minimum in one step, whatever κ\kappa. So why is it not used to train networks?

main.py
import numpy as np
th = np.pi / 4; Q = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
A = Q @ np.diag([1.0, 50.0]) @ Q.T                    # f(p) = 0.5 p^T A p, Hessian = A
p = np.array([1.0, 0.3])
p_new = p - np.linalg.solve(A, A @ p)                 # Newton: p - H^{-1} grad
print("one Newton step on an ill-conditioned quadratic:", p, "->", p_new.round(12), " (exact minimum in ONE step)")

# on the saddle f = x^2 - y^2 Newton is attracted to the saddle itself
H = np.diag([2.0, -2.0]); q = np.array([1.0, 1.0]); grad = np.array([2.0 * q[0], -2.0 * q[1]])
print("Newton on the saddle x^2 - y^2 from (1, 1):", (q - np.linalg.solve(H, grad)).tolist(), " <- lands ON the saddle, not a minimum")

for d in (1_000, 1_000_000, 100_000_000):
    gb = d * d * 4 / 1e9
    print(f"d = {d:>11,} parameters: Hessian has {d * d:.0e} entries = {gb:,.3f} GB in float32" if gb < 1 else f"d = {d:>11,} parameters: Hessian has {d * d:.0e} entries = {gb:,.0f} GB in float32")
Output
one Newton step on an ill-conditioned quadratic: [1.  0.3] -> [-0. -0.]  (exact minimum in ONE step)
Newton on the saddle x^2 - y^2 from (1, 1): [0.0, 0.0]  <- lands ON the saddle, not a minimum
d =       1,000 parameters: Hessian has 1e+06 entries = 0.004 GB in float32
d =   1,000,000 parameters: Hessian has 1e+12 entries = 4,000 GB in float32
d = 100,000,000 parameters: Hessian has 1e+16 entries = 40,000,000 GB in float32
  • Cost. The Hessian has d2d^2 entries: 4,000 GB for a million parameters, tens of millions of GB for a hundred million. Inverting it costs about d3d^3.
  • Saddles attract it. Newton looks for a point where the gradient is zero, so on x2−y2x^2-y^2 it jumps straight to the saddle (the output above) instead of leaving it.
  • Noise. With a noisy mini-batch gradient and a noisy Hessian the "exact" step is not exact.

So deep learning keeps the first-order machinery and borrows curvature information cheaply: momentum smooths the path, and adaptive methods rescale each weight by a running estimate of its gradient size. That is the next two chapters.

3.4Chapter summary

  • Update θ←θ−η∇L\theta\leftarrow\theta-\eta\nabla L; stable only if η<2/λmax⁡\eta<2/\lambda_{\max} (measured: diverges at 0.041, limit 0.04).
  • Best step 2/(λmin⁡+λmax⁡)2/(\lambda_{\min}+\lambda_{\max}); error shrinks by (κ−1)/(κ+1)(\kappa-1)/(\kappa+1) per step, so large κ\kappa means slow.
  • Newton fixes conditioning in one step but costs d2d^2 memory, d3d^3 time and is drawn to saddles.
Next · Part 4Momentum and Nesterov
04
Part 4

Momentum and Nesterov

On an elongated bowl, plain gradient descent bounces across the narrow direction and crawls along the long one. Look at the paths.

plain gradient descent, lr 0.035momentum 0.9, lr 0.01the bowl is 50x steeper across than along: GD bounces, momentum averages it out
Figure 4. The same bowl: plain GD zigzags, momentum glides

4.1Momentum: remember where you were going

Picture a heavy ball rolling down the bowl instead of a hiker who stops and re-plans at every step. The ball keeps a velocity vv, a running blend of past gradients:

vt=β vt−1−η ∇L(θt−1),θt=θt−1+vtv_{t}=\beta\,v_{t-1}-\eta\,\nabla L(\theta_{t-1}),\qquad \theta_{t}=\theta_{t-1}+v_{t}

With β=0.9\beta=0.9 the velocity remembers roughly the last 1/(1−β)=101/(1-\beta)=10 gradients. Two effects follow. In the narrow direction the gradient alternates in sign (left, right, left), so the contributions cancel in the average and the bouncing dies down. In the long direction the gradient keeps the same sign, so the contributions add up: the ball accelerates, and in steady motion its step is about η/(1−β)\eta/(1-\beta) =10η=10\eta, as if the learning rate had been multiplied by ten, but only where the gradient is consistent.

PyTorch writes it a little differently

torch.optim.SGD(momentum=0.9) keeps v←βv+gv\leftarrow\beta v+g and then θ←θ−ηv\theta\leftarrow\theta-\eta v, the learning rate multiplies after the buffer. For a constant η\eta it follows exactly the same path as the formula above; it only differs if you change η\eta mid-training. Do not be thrown when a textbook and the library look different.

4.2Nesterov: look before you leap

Momentum computes the gradient where the ball is. Nesterov momentum computes it where the velocity is about to take the ball: ∇L(θ+βv)\nabla L(\theta+\beta v). If the ball is about to overshoot the valley floor, the look-ahead gradient already points back, so it brakes earlier.

vt=βvt−1−η ∇L(θt−1+βvt−1),θt=θt−1+vtv_t=\beta v_{t-1}-\eta\,\nabla L(\theta_{t-1}+\beta v_{t-1}),\qquad \theta_t=\theta_{t-1}+v_t

4.3Measured: how much faster?

The same elongated bowl as Chapter 3, the same starting point, the same learning rate of 0.01 for all three. Steps until the gradient norm drops below 0.001:

main.py
import numpy as np
th = np.pi / 4; Q = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
A = Q @ np.diag([1.0, 50.0]) @ Q.T
g = lambda p: A @ p
START = np.array([1.0, 0.3])

def run(step, init, max_steps=20000):
    p, s = START.copy(), init()
    for t in range(1, max_steps + 1):
        p = step(p, s, t)
        if np.linalg.norm(g(p)) < 1e-3:
            return t
    return None

def gd(lr):       return (lambda p, s, t: p - lr * g(p)), (lambda: None)
def momentum(lr, b):
    def step(p, s, t): s["v"] = b * s["v"] - lr * g(p); return p + s["v"]
    return step, lambda: {"v": np.zeros(2)}
def nesterov(lr, b):
    def step(p, s, t): s["v"] = b * s["v"] - lr * g(p + b * s["v"]); return p + s["v"]
    return step, lambda: {"v": np.zeros(2)}
def adagrad(lr):
    def step(p, s, t): gg = g(p); s["s"] += gg ** 2; return p - lr * gg / (np.sqrt(s["s"]) + 1e-8)
    return step, lambda: {"s": np.zeros(2)}
def rmsprop(lr, b=0.9):
    def step(p, s, t): gg = g(p); s["s"] = b * s["s"] + (1 - b) * gg ** 2; return p - lr * gg / (np.sqrt(s["s"]) + 1e-8)
    return step, lambda: {"s": np.zeros(2)}
def adam(lr, b1=0.9, b2=0.999):
    def step(p, s, t):
        gg = g(p); s["m"] = b1 * s["m"] + (1 - b1) * gg; s["v"] = b2 * s["v"] + (1 - b2) * gg ** 2
        return p - lr * (s["m"] / (1 - b1 ** t)) / (np.sqrt(s["v"] / (1 - b2 ** t)) + 1e-8)
    return step, lambda: {"m": np.zeros(2), "v": np.zeros(2)}

rows = [("plain GD, best stable lr 0.039", gd(0.039)), ("plain GD, lr 0.01", gd(0.01)),
        ("momentum 0.9, lr 0.01", momentum(0.01, 0.9)), ("Nesterov 0.9, lr 0.01", nesterov(0.01, 0.9)),
        ("Adagrad lr 0.5", adagrad(0.5)), ("RMSProp lr 0.05", rmsprop(0.05)), ("Adam lr 0.1", adam(0.1)), ("Adam lr 0.05", adam(0.05))]
print("steps until the gradient norm is below 1e-3 (condition number 50):")
for name, (step, init) in rows:
    n = run(step, init)
    print(f"  {name:32s}", n if n else "not reached (keeps jittering at the step size)")
Output
steps until the gradient norm is below 1e-3 (condition number 50):
  plain GD, best stable lr 0.039   199
  plain GD, lr 0.01                679
  momentum 0.9, lr 0.01            95
  Nesterov 0.9, lr 0.01            62
  Adagrad lr 0.5                   263
  RMSProp lr 0.05                  not reached (keeps jittering at the step size)
  Adam lr 0.1                      147
  Adam lr 0.05                     303

At the same η=0.01\eta=0.01: plain GD needs 679 steps, momentum 95 (7 times fewer), Nesterov 62 (11 times fewer). Momentum even beats plain GD at its very best step (199). Momentum also widens the stable range of learning rates, from η<2/λmax⁡\eta<2/\lambda_{\max} to η<2(1+β)/λmax⁡\eta<2(1+\beta)/\lambda_{\max}:

main.py
import numpy as np
th = np.pi / 4; Q = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])
A = Q @ np.diag([1.0, 50.0]) @ Q.T                       # curvatures 1 and 50, as before
beta = 0.9
print("plain GD is stable for lr < 2/50 = 0.04; heavy-ball momentum for lr < 2(1+beta)/50 =", round(2 * (1 + beta) / 50, 3))
for lr in (0.05, 0.07, 0.075, 0.078):
    p = np.array([1.0, 0.3]); v = np.zeros(2); result = "not converged in 20000 steps"
    for t in range(1, 20001):
        v = beta * v - lr * A @ p; p = p + v
        if not np.isfinite(p).all() or np.linalg.norm(p) > 1e6:
            result = "diverged"; break
        if np.linalg.norm(A @ p) < 1e-3:
            result = f"converged in {t} steps"; break
    print(f"momentum 0.9, lr = {lr}: {result}")
Output
plain GD is stable for lr < 2/50 = 0.04; heavy-ball momentum for lr < 2(1+beta)/50 = 0.076
momentum 0.9, lr = 0.05: converged in 158 steps
momentum 0.9, lr = 0.07: converged in 149 steps
momentum 0.9, lr = 0.075: converged in 182 steps
momentum 0.9, lr = 0.078: diverged

With β=0.9\beta=0.9 the limit moves from 0.04 to 0.076, and the run confirms it (0.075 converges, 0.078 diverges). On non-convex problems the benefit is also that the ball carries enough speed to roll through shallow dips and flat plateaus near saddles, and a large β\beta trades this for overshoot: the ball swings past the bottom and oscillates before it settles.

About the other rows. The output above also lists Adagrad, RMSProp and Adam. They are explained in the next chapter; the comparison figure is in Chapter 6, where all of them sit together.

4.4Chapter summary

  • Momentum: v←βv−ηg, θ←θ+vv\leftarrow\beta v-\eta g,\ \theta\leftarrow\theta+v. Cancels zigzag across the steep direction, accumulates along the flat one.
  • Nesterov takes the gradient at the look-ahead point θ+βv\theta+\beta v.
  • Measured at the same lr: 679 (GD), 95 (momentum), 62 (Nesterov) steps.
Next · Part 5Adagrad, RMSProp, Adam and AdamW
05
Part 5

Adagrad, RMSProp, Adam and AdamW

Momentum fixes the direction problem. A second problem is that one learning rate is shared by every weight, though weights differ wildly: a weight on a rare feature gets a gradient only now and then, a weight on a common feature gets one every step. Adaptive methods give each weight its own step size, scaled by how large its gradients have been.

5.1Adagrad: divide by the history

st=st−1+gt2,θt=θt−1−ηst+ϵ gt(all operations per weight)s_t=s_{t-1}+g_t^2,\qquad \theta_t=\theta_{t-1}-\frac{\eta}{\sqrt{s_t}+\epsilon}\,g_t\quad(\text{all operations per weight})

sts_t is the running sum of squared gradients for that weight. A weight that has seen big gradients gets a small effective step; one that has seen tiny gradients keeps a large step. It suits sparse data (a rare word's embedding keeps a large step for the day it finally appears) and needs almost no tuning of η\eta. Its flaw is that sts_t only ever grows:

main.py
import numpy as np
lr = 0.1
print("a constant gradient g = 1 at every step")
print("   step t    accumulated sum    effective lr = lr / sqrt(sum)")
for t in (1, 10, 100, 1000, 10000):
    s = t * 1.0 ** 2                                     # sum of squared gradients grows linearly
    print(f"{t:9d}    {s:13.0f}       {lr / np.sqrt(s):.6f}")
print("the learning rate shrinks like 1/sqrt(t) and never recovers: Adagrad can stall before it finishes")

beta = 0.9                                               # RMSProp: an exponential moving average instead of a running sum
s = 0.0
for t in range(1, 10001):
    s = beta * s + (1 - beta) * 1.0
print("RMSProp after 10000 steps of the same gradient: denominator", round(float(np.sqrt(s)), 4), "-> effective lr", round(lr / float(np.sqrt(s)), 4), "(stays near lr)")
Output
a constant gradient g = 1 at every step
   step t    accumulated sum    effective lr = lr / sqrt(sum)
        1                1       0.100000
       10               10       0.031623
      100              100       0.010000
     1000             1000       0.003162
    10000            10000       0.001000
the learning rate shrinks like 1/sqrt(t) and never recovers: Adagrad can stall before it finishes
RMSProp after 10000 steps of the same gradient: denominator 1.0 -> effective lr 0.1 (stays near lr)

With a constant gradient the effective rate falls like 1/t1/\sqrt{t}: from 0.1 to 0.001 after 10,000 steps, and it never recovers. On a long deep-learning run Adagrad can slow to a crawl before it has finished learning.

5.2RMSProp and AdaDelta: forget the distant past

st=ρ st−1+(1−ρ) gt2,θt=θt−1−ηst+ϵ gt(ρ≈0.9)s_t=\rho\,s_{t-1}+(1-\rho)\,g_t^2,\qquad \theta_t=\theta_{t-1}-\frac{\eta}{\sqrt{s_t}+\epsilon}\,g_t\qquad(\rho\approx0.9)

The fix is a one-word change: replace the sum with an exponential moving average, so old gradients fade out. In the output above, after 10,000 steps of the same gradient RMSProp's denominator is 1.0 and its effective rate is still 0.1, where Adagrad's had fallen to 0.001. AdaDelta uses the same moving average and additionally replaces η\eta by a running average of the size of past updates, so it needs no learning rate at all (it is rarely the first choice today).

5.3Adam: momentum plus RMSProp

gradient gthe raw slopem: average of gdirection (momentum)v: average of g^2scale (RMSProp)the steplr * m / sqrt(v)Adam = momentum (direction) + RMSProp (scale) + bias correctionfirst step for gradients 1e-4, 0.5 and 5000: 0.009998, 0.010000, 0.010000
Figure 5. Adam keeps two moving averages and combines them
mt=β1mt−1+(1−β1)gt,vt=β2vt−1+(1−β2)gt2,m^t=mt1−β1 t, v^t=vt1−β2 t,θt=θt−1−ηm^tv^t+ϵm_t=\beta_1 m_{t-1}+(1-\beta_1)g_t,\quad v_t=\beta_2 v_{t-1}+(1-\beta_2)g_t^2,\quad \hat m_t=\frac{m_t}{1-\beta_1^{\,t}},\ \hat v_t=\frac{v_t}{1-\beta_2^{\,t}},\quad \theta_t=\theta_{t-1}-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}

mm is momentum (direction, the average of gg), vv is RMSProp's scale (the average of g2g^2). Defaults: β1=0.9, β2=0.999, ϵ=10−8\beta_1=0.9,\ \beta_2=0.999,\ \epsilon=10^{-8}, and a learning rate around 10−310^{-3}. The bias correction divides by 1−βt1-\beta^t because both averages start at zero and are therefore biased low for the first steps. Run three steps by hand:

main.py
import numpy as np
lr, b1, b2, eps = 0.01, 0.9, 0.999, 1e-8
m = v = 0.0
for t, g in enumerate([0.5, 0.5, 0.5], start=1):
    m = b1 * m + (1 - b1) * g                  # first moment: a moving average of the gradient
    v = b2 * v + (1 - b2) * g ** 2             # second moment: a moving average of the squared gradient
    m_hat = m / (1 - b1 ** t)                  # bias correction
    v_hat = v / (1 - b2 ** t)
    update = lr * m_hat / (np.sqrt(v_hat) + eps)
    print(f"t={t}: m={m:.5f}  v={v:.8f}  m_hat={m_hat:.3f}  v_hat={v_hat:.4f}  update={update:.6f}")
print()
print("without bias correction at t=1 the step would be", round(lr * (0.05) / (np.sqrt(0.00025) + eps), 5), "(3.16x too LARGE: v starts at 0 and is badly underestimated, more than m is)")
print("with correction, the very first step is lr * sign(g) =", lr, ": Adam's step size is about lr, whatever the gradient scale")
for scale in (1e-4, 1.0, 1e4):
    g = 0.5 * scale
    mh, vh = g, g ** 2
    print(f"gradient {g:10.4f}: first Adam step {lr * mh / (np.sqrt(vh) + eps):.6f}")
Output
t=1: m=0.05000  v=0.00025000  m_hat=0.500  v_hat=0.2500  update=0.010000
t=2: m=0.09500  v=0.00049975  m_hat=0.500  v_hat=0.2500  update=0.010000
t=3: m=0.13550  v=0.00074925  m_hat=0.500  v_hat=0.2500  update=0.010000

without bias correction at t=1 the step would be 0.03162 (3.16x too LARGE: v starts at 0 and is badly underestimated, more than m is)
with correction, the very first step is lr * sign(g) = 0.01 : Adam's step size is about lr, whatever the gradient scale
gradient     0.0001: first Adam step 0.009998
gradient     0.5000: first Adam step 0.010000
gradient  5000.0000: first Adam step 0.010000

Look at what the correction does. Without it the very first step would be 0.0316, 3.16 times too large, because vv (weight 0.001 on g2g^2) is underestimated far more than mm (weight 0.1 on gg). With it m^=0.5\hat m=0.5 and v^=0.25\hat v=0.25 are exact and the step is exactly η\eta. And the last three lines show Adam's signature property: for gradients of 10−410^{-4}, 0.5 and 5000 the first step is the same, about η\eta. Adam's step size is set by the learning rate, not by the size of the gradient, which is why one value like 10−310^{-3} works across so many problems.

5.4AdamW: decoupled weight decay

Weight decay (Week 4) shrinks weights towards zero. In plain SGD, adding an L2 penalty λ2∥w∥2\tfrac{\lambda}{2}\|w\|^2 to the loss and shrinking the weights directly are the same thing. In Adam they are not: the L2 gradient λw\lambda w is added to gg, and then divided by v^\sqrt{\hat v} like everything else, so the strength of the decay now depends on the gradient history. AdamW removes the decay from the gradient and applies it separately: θ←θ−η(m^/(v^+ϵ)+λθ)\theta\leftarrow\theta-\eta\big(\hat m/(\sqrt{\hat v}+\epsilon)+\lambda\theta\big). Isolate the effect by giving the loss no gradient at all:

main.py
import torch
# Isolate the weight decay: the loss gradient is exactly zero, so the only force on the weight is the penalty.
# isolate the decay: no loss gradient, only decay
def decay_only(opt_cls):
    w = torch.nn.Parameter(torch.tensor([1.0, 1.0]))
    opt = opt_cls([w], lr=0.1, weight_decay=0.1)
    for _ in range(100):
        opt.zero_grad(); (0 * w).sum().backward(); opt.step()
    return [round(v, 3) for v in w.tolist()]
print("weights start at [1.0, 1.0], no loss gradient, weight_decay = 0.1, lr = 0.1, 100 steps")
print("decay only, Adam :", decay_only(torch.optim.Adam), " <- L2 sits inside the gradient, so Adam rescales it like any gradient: the weight is driven to ~0")
print("decay only, AdamW:", decay_only(torch.optim.AdamW), " <- plain shrinkage factor (1 - lr*wd)^100 =", round((1 - 0.1 * 0.1) ** 100, 3))
Output
weights start at [1.0, 1.0], no loss gradient, weight_decay = 0.1, lr = 0.1, 100 steps
decay only, Adam : [0.003, 0.003]  <- L2 sits inside the gradient, so Adam rescales it like any gradient: the weight is driven to ~0
decay only, AdamW: [0.366, 0.366]  <- plain shrinkage factor (1 - lr*wd)^100 = 0.366

Same learning rate, same weight_decay=0.1, same 100 steps: Adam with L2 drives the weight to 0.003 (Adam rescaled the tiny penalty gradient up to a full step, every step), AdamW shrinks it by exactly the predictable factor (1−ηλ)100=0.366(1-\eta\lambda)^{100}=0.366. AdamW's decay does what the hyper-parameter says, which is why it is the default for Transformers (Weeks 9-10).

OptimiserPer-weight stateIdeaWeak point
SGD + momentumvsmooth the directionone global lr; sensitive to scale
Adagrads (sum)small steps for frequent weightsrate decays forever
RMSProps (EMA)Adagrad with forgettingno bias correction, no momentum
Adamm, vmomentum + RMSProp + bias correctionL2 mixes with the rescaling
AdamWm, vAdam with decoupled decaythe usual default

5.5Chapter summary

  • Adagrad: divide by sum of g2\sqrt{\text{sum of }g^2}, the rate only shrinks. RMSProp: moving average, so it forgets. AdaDelta: no learning rate.
  • Adam = momentum mm + RMSProp vv + bias correction; defaults 0.9, 0.999, 10−810^{-8}; step ≈η\approx\eta whatever the gradient size.
  • AdamW decouples weight decay (0.366 vs 0.003 measured); use it rather than Adam with L2.
Next · Part 6Schedules and Choosing an Optimiser
06
Part 6

Schedules and Choosing an Optimiser

Two practical questions remain. How should the learning rate change during training? And which optimiser should I actually pick?

6.1Learning-rate schedules

A good learning rate early (large, to make progress and cross flat regions) is a bad one late (small is needed to settle into the valley instead of bouncing around it). A schedule lowers it over time:

00.050.10255075100step decay (x0.1 every 30)exponential decaycosine annealingepochlearning ratestart large to move fast,end small to settle
Figure 6. Three common schedules, starting from the same 0.1
  • Step decay: multiply by a factor (here 0.1) every fixed number of epochs. Simple, a classic for image models.
  • Exponential decay: ηt=η0e−kt\eta_t=\eta_0e^{-kt}, a smooth version of the same idea.
  • Cosine annealing: ηt=12η0(1+cos⁡(πt/T))\eta_t=\tfrac12\eta_0\big(1+\cos(\pi t/T)\big), a gentle S-shaped fall to zero. Very popular today.
  • Warm-up: start tiny and ramp up for the first few hundred steps, because the very first Adam steps use badly estimated statistics (Transformers almost always use it).
main.py
import math, torch
lr0, T = 0.1, 100
step_decay = lambda t: lr0 * 0.1 ** (t // 30)                          # x0.1 every 30 epochs
exp_decay = lambda t: lr0 * math.exp(-0.03 * t)
cosine = lambda t: 0.5 * lr0 * (1 + math.cos(math.pi * t / T))
print("epoch   step    exponential   cosine")
for t in (0, 15, 30, 60, 90, 100):
    print(f"{t:5d}   {step_decay(t):.4f}   {exp_decay(t):.4f}        {cosine(t):.4f}")

# the PyTorch schedulers produce the same cosine numbers
w = torch.nn.Parameter(torch.zeros(1))
opt = torch.optim.SGD([w], lr=lr0)
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=T)
got = []
for t in range(T + 1):
    got.append(opt.param_groups[0]["lr"]); opt.step(); sched.step()
print("scheduler matches the formula:", all(abs(got[t] - cosine(t)) < 1e-9 for t in (0, 15, 30, 60, 90, 100)))
Output
epoch   step    exponential   cosine
    0   0.1000   0.1000        0.1000
   15   0.1000   0.0638        0.0946
   30   0.0100   0.0407        0.0794
   60   0.0010   0.0165        0.0345
   90   0.0001   0.0067        0.0024
  100   0.0001   0.0050        0.0000
scheduler matches the formula: True

In PyTorch the same curves come from torch.optim.lr_scheduler (StepLR, ExponentialLR, CosineAnnealingLR); the last line confirms the library reproduces the formula. Call scheduler.step() once per epoch (or per iteration, depending on how you set it up), after optimizer.step().

6.2All the optimisers on one problem

steps to reach gradient norm 1e-3 on the elongated bowl (condition number 50)plain GD (lr 0.039)199plain GD (lr 0.01)679momentum 0.995Nesterov 0.962Adagrad263Adam lr 0.1147Adam lr 0.05303RMSProp (lr 0.05) never reached it: it keeps jittering at about the size of its step
Figure 7. Steps to converge on the elongated bowl

On the elongated bowl (Chapter 3): momentum and Nesterov win clearly; Adam at lr 0.1 beats plain GD at its best (147 against 199) but at lr 0.05 it needs 303, so it is not tuning-free; and RMSProp at lr 0.05 never reaches the target because its step is about η\eta per weight regardless of the gradient, so it keeps jittering around the minimum at that size. That last point is a real property: sign-like methods need a decaying learning rate to settle, which is why Adam is usually paired with a schedule.

And on a real small network (two interleaved half-moons, a 2-32-32-1 ReLU net, full batch, the same initial weights): epochs until the training loss falls below 0.15.

main.py
import torch, torch.nn as nn, math
def make_moons(n, noise, seed):
    g = torch.Generator().manual_seed(seed)
    t = torch.rand(n, generator=g) * math.pi
    X = torch.cat([torch.stack([torch.cos(t), torch.sin(t)], 1), torch.stack([1 - torch.cos(t), 0.5 - torch.sin(t)], 1)]) + noise * torch.randn(2 * n, 2, generator=g)
    y = torch.cat([torch.zeros(n), torch.ones(n)]); p = torch.randperm(2 * n, generator=g)
    return X[p], y[p]
X, y = make_moons(400, 0.15, 0)

def epochs_to(make_opt, target=0.15, max_epochs=600):
    torch.manual_seed(0)
    model = nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 1))
    opt = make_opt(model.parameters())
    for epoch in range(1, max_epochs + 1):
        opt.zero_grad()
        loss = nn.functional.binary_cross_entropy_with_logits(model(X).squeeze(1), y)
        loss.backward(); opt.step()
        if loss.item() < target:
            return epoch
    return None

opts = {
    "SGD lr 0.1":                 lambda p: torch.optim.SGD(p, lr=0.1),
    "SGD lr 0.1, momentum 0.9":   lambda p: torch.optim.SGD(p, lr=0.1, momentum=0.9),
    "SGD lr 0.1, Nesterov 0.9":   lambda p: torch.optim.SGD(p, lr=0.1, momentum=0.9, nesterov=True),
    "Adagrad lr 0.1":             lambda p: torch.optim.Adagrad(p, lr=0.1),
    "RMSprop lr 0.01":            lambda p: torch.optim.RMSprop(p, lr=0.01),
    "Adam lr 0.01":               lambda p: torch.optim.Adam(p, lr=0.01),
    "AdamW lr 0.01":              lambda p: torch.optim.AdamW(p, lr=0.01),
}
print("full-batch epochs until training loss < 0.15 (same network, same initial weights):")
for name, make in opts.items():
    n = epochs_to(make)
    print(f"  {name:28s}", n if n else ">600")
Output
full-batch epochs until training loss < 0.15 (same network, same initial weights):
  SGD lr 0.1                   459
  SGD lr 0.1, momentum 0.9     65
  SGD lr 0.1, Nesterov 0.9     62
  Adagrad lr 0.1               17
  RMSprop lr 0.01              16
  Adam lr 0.01                 36
  AdamW lr 0.01                36

Plain SGD needs 459 epochs; momentum 65; Nesterov 62; Adam and AdamW 36; RMSprop 16; Adagrad 17. Read this carefully rather than as a league table: each optimiser got one hand-picked learning rate, the problem is tiny and easy, and "epochs to reach 0.15" says nothing about how well the network generalises. What it does show is the large, consistent gap between plain SGD and everything that has momentum or per-weight scaling.

6.3Which one should I use?

SituationA sensible default
Transformers, language models, most new workAdamW, lr around 10−410^{-4} to 10−310^{-3}, warm-up then cosine decay
CNNs for image classificationSGD + momentum 0.9 with step or cosine decay is the long-standing choice; Adam also works and needs less tuning
Quick experiment, unknown problemAdam, lr 10−310^{-3}: the most forgiving start
GANs (Week 6)Adam with β1\beta_1 lowered to about 0.5 (momentum can destabilise the two-player game)
Honest caveat. The table is common practice, not a theorem. The remark that SGD+momentum "generalises better" than Adam on image models is widely reported but depends on tuning and is debated. In an exam, state the trade-off (Adam: fast and forgiving; SGD+momentum: more tuning, often a slightly better final result), not a verdict.

6.4Chapter summary

  • Decay the learning rate: step, exponential, cosine; warm-up first for Adam-style training.
  • Measured: plain SGD 459 epochs, momentum 65, Adam 36 on the moons network; Adam is not tuning-free (303 vs 147 steps on the bowl).
  • Defaults: AdamW + warm-up + cosine for Transformers; SGD+momentum for classic CNNs; Adam 10−310^{-3} to start.
Next · InterviewInterview Question Bank
07
Interview

Interview Question Bank

What is likely to be asked from Week 5, with 30-second answers, deeper explanations, a lecture-or-extra tag on each question, and code you can run.

Week 5 questions are about why each optimiser exists: what problem it fixes, the formula, and the number that shows it. Each answer starts with a 30-second version. Questions are tagged From the notes or Extra.

1Definition

One clear sentence: what it is.

2Intuition or tiny example

Why it works, in one picture or three lines.

3The failure it prevents

What goes wrong without it.

7.1Interview heat-map: what is most likely

Very likely6 topics
Gradient descent and the learning rate"What happens if the learning rate is too large or too small?"Notes 3.1-3.2
Momentum"How does momentum speed up training?"Notes 4.1
Adam"Write out Adam and explain every term."Notes 5.3
Adagrad versus RMSProp"What is wrong with Adagrad and how does RMSProp fix it?"Notes 5.1-5.2
Convex versus non-convex"Why do we care whether the loss is convex?"Notes 2.1-2.2
Saddle points"What is a saddle point? Why are they a problem in deep learning?"Notes 2.3-2.4
Likely5 topics
Mini-batch, epoch, iteration"Batch vs stochastic vs mini-batch GD. Epoch vs iteration?"Notes 1.2
True versus empirical risk"What is the difference between empirical risk and true risk?"Notes 1.1
AdamW versus Adam with L2"Why AdamW? What is decoupled weight decay?"Notes 5.4
Learning-rate schedules and warm-up"Name some LR schedules. What is warm-up?"Notes 6.1
Newton's method"Why not use Newton's method for deep learning?"Notes 3.3
Sometimes2 topics
Jensen's inequality"State Jensen's inequality."Notes 2.1
Condition number"What is the condition number and why does it matter?"Notes 3.2

7.2The landscape

Interview 1What is empirical risk minimisation, and why is minimising it to zero not the goal?
Week 5LikelyNotes 1.1Extra

Proxy versus target.

Prints
degree 1: empirical risk     0.2472   true risk (estimated)       0.4075   gap       0.1604
degree 3: empirical risk     0.0238   true risk (estimated)       0.0551   gap       0.0313
degree 9: empirical risk     0.0000   true risk (estimated)       0.3838   gap       0.3838
Answer
Say this first (30 seconds)

The true risk is the expected loss over the whole data distribution (unknowable); the empirical risk is the average loss on the training set, the thing we can minimise. A flexible model can drive it to 0 by memorising noise while the true risk is large: a degree-9 polynomial on 10 points had empirical risk 0.0000 and true risk 0.38, against 0.055 for degree 3. That gap is overfitting.

Go deeper
  • Validation data stands in for the true risk during training.
Follow-up they may askHow do you estimate the true risk in practice?
Interview 2Compare batch, stochastic and mini-batch gradient descent. What is an epoch and an iteration?
Week 5LikelyNotes 1.2From the lecture

How many examples per update?

Prints
batch size   mean of mini-batch gradients   std of the mini-batch gradient   std x sqrt(B)
         1                         -5.848                            8.402           8.40
         8                         -5.898                            3.015           8.53
        64                         -5.947                            1.062           8.49
       512                         -5.962                            0.381           8.61
      4096                         -5.960                            0.140           8.93
same mean (an unbiased estimate), but the noise falls like 1/sqrt(B): that is why a bigger batch tolerates a bigger learning rate
Answer
Say this first (30 seconds)

Batch: all nn examples per update, exact but slow. SGD: one example, noisy but cheap. Mini-batch: BB examples (32-256), the practical compromise that also suits GPUs. An epoch is one pass over the training set; an iteration is one update. With n=10,000, B=100n=10{,}000,\ B=100: 100 iterations per epoch.

Go deeper
  • Mini-batch gradients are unbiased with variance ∝1/B\propto1/B: 64x the batch gives 8x less noise.
Follow-up they may askWhy do very large batches often need a larger learning rate?
Interview 3What is a convex function, and why does it matter for optimisation? Are neural-network losses convex?
Week 5Very likelyNotes 2.1-2.2From the lecture

Chords above the graph; every local minimum is global.

Prints
start -2.0  ->  ends at x = -1.3008, f = -3.5139
start -0.5  ->  ends at x = -1.3008, f = -3.5139
start +0.5  ->  ends at x = +1.1309, f = -1.0702
start +2.0  ->  ends at x = +1.1309, f = -1.0702
Prints
chord above the curve fails for x^2   : 0 of 100000 random chords (it is convex)
chord above the curve fails for |x|   : 0 (convex, not smooth)
chord above the curve fails for sin(x): 50034 (NOT convex)
chord above the curve fails for x^3   : 50104 (NOT convex on all of R)
Answer
Say this first (30 seconds)

f(λx+(1−λ)y)≤λf(x)+(1−λ)f(y)f(\lambda x+(1-\lambda)y)\le\lambda f(x)+(1-\lambda)f(y): chords never dip below the graph; equivalently the Hessian is positive semi-definite everywhere. For a convex loss every local minimum is global and a zero gradient proves it, so gradient descent with a small step cannot get trapped. Neural-network losses are non-convex: the answer depends on the start (two minima, -3.514 and -1.070, in the example), and saddles abound.

Go deeper
  • Linear and logistic regression are convex. Convex does not need smooth: ∣x∣|x| is convex.
Follow-up they may askIs x3x^3 convex?
Interview 4What is a saddle point and why is it a bigger obstacle than local minima in deep learning?
Week 5Very likelyNotes 2.3-2.4From the lecture

Zero gradient, mixed curvature.

Prints
gradient at the origin: [0.0, 0.0]  <- zero, yet the origin is NOT a minimum
Hessian eigenvalues   : [-2.0, 2.0]  <- one negative: some direction goes down
start (1, 0.000) exactly on the ridge -> [2e-06, 0.0]  (walks into the saddle and stops)
start (1, 0.001) a hair off the ridge -> [0.0, 56.35]  (slides away, but slowly at first)
Prints
 dim   P(all eigenvalues > 0)   (rest are saddles or maxima)
   1     0.4868
   2     0.1547
   3     0.0240
   5     0.0000
   8     0.0000
  12     0.0000
  20     0.0000
Answer
Say this first (30 seconds)

A critical point (zero gradient) where the Hessian has both positive and negative eigenvalues: a minimum along some directions, a maximum along others. For a true minimum all dd directions must curve up, which becomes vanishingly rare as dd grows, so in a network with millions of parameters most critical points are saddles. Gradients are tiny near them, so plain descent slows to a crawl; momentum and adaptive methods help to escape.

Go deeper
  • Measured: random matrices of dimension 1, 3 and 5 were all-positive 49%, 2.4% and under 0.03% of the time (an intuition, not a measurement of a real network).
  • From exactly the ridge descent stops at the saddle; a tiny offset escapes exponentially.
Follow-up they may askHow do you tell a saddle from a minimum?
Interview 5State Jensen's inequality and give an example.
Week 5SometimesNotes 2.1From the lecture

Which side is bigger for convex f?

Prints
f(E[X]) = 3.9946    E[f(X)] = 6.2451
f(E[X]) <= E[f(X)]: True   (the gap is exactly Var(X) = 2.251 )
log: f(E[Y]) = 1.0977 >= E[f(Y)] = 1.0112
Answer
Say this first (30 seconds)

For convex ff: f(E[X])≤E[f(X)]f(\mathbb{E}[X])\le\mathbb{E}[f(X)]; for concave ff the inequality flips. Example: f=x2f=x^2 gives E[X2]≥(EX)2\mathbb{E}[X^2]\ge(\mathbb{E}X)^2, and the gap is exactly the variance.

Go deeper
  • Measured: 3.995 ≤\le 6.245, gap 2.251 = the variance.
Follow-up they may askWhich way does it go for log⁡\log?

7.3Gradient descent and momentum

Interview 6What happens if the learning rate is too large or too small? Is there a precise limit?
Week 5Very likelyNotes 3.1-3.2From the lecture

Slow, good, or diverging, with an exact threshold.

Prints
curvatures 1 and 50, so a stable step needs lr < 2/50 = 0.04
lr = 0.001  : converged in 6821 steps
lr = 0.01   : converged in 679 steps
lr = 0.02   : converged in 338 steps
lr = 0.039  : converged in 199 steps
lr = 0.0399 : converged in 2019 steps
lr = 0.041  : did not converge (diverged)
Answer
Say this first (30 seconds)

Too small: convergence is very slow (it may look stuck). Too large: the loss oscillates or diverges. For a quadratic with largest curvature λmax⁡\lambda_{\max} gradient descent is stable exactly when η<2/λmax⁡\eta<2/\lambda_{\max}. The best step balances the steepest and flattest directions: η∗=2/(λmin⁡+λmax⁡)\eta^*=2/(\lambda_{\min}+\lambda_{\max}).

Go deeper
  • Measured with curvatures 1 and 50: lr 0.039 converged in 199 steps, 0.0399 in 2019, 0.041 diverged.
Follow-up they may askWhy is 0.0399 so much slower than 0.039 though both are stable?
Interview 7What is the condition number of the loss and why does it slow gradient descent?
Week 5SometimesNotes 3.2Extra

Steepest curvature over flattest.

Prints
curvatures 1 and 50, so a stable step needs lr < 2/50 = 0.04
lr = 0.001  : converged in 6821 steps
lr = 0.01   : converged in 679 steps
lr = 0.02   : converged in 338 steps
Answer
Say this first (30 seconds)

κ=λmax⁡/λmin⁡\kappa=\lambda_{\max}/\lambda_{\min}, the ratio of the steepest to the flattest curvature. The stable step is limited by λmax⁡\lambda_{\max} but progress along the flat direction is set by λmin⁡\lambda_{\min}, so at the best step the error shrinks by (κ−1)/(κ+1)(\kappa-1)/(\kappa+1) per iteration. κ=50\kappa=50 gives 0.961, i.e. 199 steps.

Go deeper
  • A round bowl (κ=1\kappa=1) is solved in one step.
Follow-up they may askWhat does normalising the inputs do to κ\kappa?
Interview 8What is Newton's method and why is it not used to train neural networks?
Week 5LikelyNotes 3.3From the lecture

Uses the Hessian.

Prints
one Newton step on an ill-conditioned quadratic: [1.  0.3] -> [-0. -0.]  (exact minimum in ONE step)
Newton on the saddle x^2 - y^2 from (1, 1): [0.0, 0.0]  <- lands ON the saddle, not a minimum
d =       1,000 parameters: Hessian has 1e+06 entries = 0.004 GB in float32
d =   1,000,000 parameters: Hessian has 1e+12 entries = 4,000 GB in float32
d = 100,000,000 parameters: Hessian has 1e+16 entries = 40,000,000 GB in float32
Answer
Say this first (30 seconds)

θ←θ−H−1∇L\theta\leftarrow\theta-H^{-1}\nabla L: it rescales the gradient by the curvature, landing on the minimum of a quadratic in one step. But the Hessian has d2d^2 entries (4,000 GB for a million parameters in float32) and inverting it costs ∼d3\sim d^3; it is attracted to saddles (it jumps onto x2−y2x^2-y^2's saddle); and the Hessian is noisy under mini-batches. Momentum and Adam are cheap approximations to using curvature.

Go deeper
  • The "learning rate" of Newton is automatically 1/curvature in each direction.
Follow-up they may askName a quasi-Newton method that avoids storing the Hessian.
Interview 9Explain momentum. Why does it help on an elongated valley?
Week 5Very likelyNotes 4.1From the lecture

A moving average of gradients.

Prints
  plain GD, best stable lr 0.039   199
  plain GD, lr 0.01                679
  momentum 0.9, lr 0.01            95
  Nesterov 0.9, lr 0.01            62
Prints
plain GD is stable for lr < 2/50 = 0.04; heavy-ball momentum for lr < 2(1+beta)/50 = 0.076
momentum 0.9, lr = 0.05: converged in 158 steps
momentum 0.9, lr = 0.07: converged in 149 steps
momentum 0.9, lr = 0.075: converged in 182 steps
momentum 0.9, lr = 0.078: diverged
Answer
Say this first (30 seconds)

Momentum keeps a velocity v←βv−ηg, θ←θ+vv\leftarrow\beta v-\eta g,\ \theta\leftarrow\theta+v, a running blend of past gradients (about 1/(1−β)1/(1-\beta) of them). In a valley the gradient alternates sign across the narrow direction, so it cancels in the average and the zigzag dies, while along the valley the gradient keeps its sign and accumulates, so the step grows (up to about η/(1−β)\eta/(1-\beta)).

Go deeper
  • Measured at the same lr 0.01: 679 steps (plain) against 95 (momentum).
  • It also widens the stable range to η<2(1+β)/λmax⁡\eta<2(1+\beta)/\lambda_{\max}: 0.04 becomes 0.076.
Follow-up they may askHow does Nesterov momentum differ?

7.4Adaptive methods and schedules

Interview 10What is the weakness of Adagrad and how does RMSProp fix it?
Week 5Very likelyNotes 5.1-5.2From the lecture

A sum that only grows.

Prints
a constant gradient g = 1 at every step
   step t    accumulated sum    effective lr = lr / sqrt(sum)
        1                1       0.100000
       10               10       0.031623
      100              100       0.010000
     1000             1000       0.003162
    10000            10000       0.001000
the learning rate shrinks like 1/sqrt(t) and never recovers: Adagrad can stall before it finishes
RMSProp after 10000 steps of the same gradient: denominator 1.0 -> effective lr 0.1 (stays near lr)
Answer
Say this first (30 seconds)

Adagrad divides by st\sqrt{s_t} with st=∑g2s_t=\sum g^2, a sum that only grows, so the effective learning rate falls like 1/t1/\sqrt t and never recovers: learning can stall early. RMSProp uses an exponential moving average instead, so old gradients are forgotten and the rate stays usable. Both give frequent weights smaller steps and rare ones larger steps.

Go deeper
  • Measured with a constant gradient: Adagrad's rate 0.1 to 0.001 over 10,000 steps; RMSProp stayed at 0.1.
Follow-up they may askFor which kind of data is Adagrad still attractive?
Interview 11Write the Adam update and explain each term, including the bias correction.
Week 5Very likelyNotes 5.3From the lecture

Two moving averages, two corrections.

Prints
t=1: m=0.05000  v=0.00025000  m_hat=0.500  v_hat=0.2500  update=0.010000
t=2: m=0.09500  v=0.00049975  m_hat=0.500  v_hat=0.2500  update=0.010000
t=3: m=0.13550  v=0.00074925  m_hat=0.500  v_hat=0.2500  update=0.010000

without bias correction at t=1 the step would be 0.03162 (3.16x too LARGE: v starts at 0 and is badly underestimated, more than m is)
with correction, the very first step is lr * sign(g) = 0.01 : Adam's step size is about lr, whatever the gradient scale
gradient     0.0001: first Adam step 0.009998
gradient     0.5000: first Adam step 0.010000
gradient  5000.0000: first Adam step 0.010000
Answer
Say this first (30 seconds)

mt=β1mt−1+(1−β1)gtm_t=\beta_1m_{t-1}+(1-\beta_1)g_t (average gradient: momentum); vt=β2vt−1+(1−β2)gt2v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2 (average squared gradient: RMSProp scale); m^=m/(1−β1t), v^=v/(1−β2t)\hat m=m/(1-\beta_1^t),\ \hat v=v/(1-\beta_2^t) (bias correction, because both start at 0); θ←θ−ηm^/(v^+ϵ)\theta\leftarrow\theta-\eta\hat m/(\sqrt{\hat v}+\epsilon). Defaults 0.9, 0.999, 10−80.9,\ 0.999,\ 10^{-8}, lr about 10−310^{-3}.

Go deeper
  • Without the correction the first step would be 3.16x too large (0.0316 instead of 0.01).
  • The step is about η\eta whatever the gradient size (1e-4, 0.5, 5000 all gave 0.01).
Follow-up they may askWhy does v^\hat v sit under a square root?
Interview 12Why is AdamW preferred over Adam with an L2 penalty?
Week 5LikelyNotes 5.4Extra

L2 goes through the rescaling.

Prints
weights start at [1.0, 1.0], no loss gradient, weight_decay = 0.1, lr = 0.1, 100 steps
decay only, Adam : [0.003, 0.003]  <- L2 sits inside the gradient, so Adam rescales it like any gradient: the weight is driven to ~0
decay only, AdamW: [0.366, 0.366]  <- plain shrinkage factor (1 - lr*wd)^100 = 0.366
Answer
Say this first (30 seconds)

In SGD an L2 penalty equals shrinking the weights. In Adam the penalty gradient λw\lambda w is added to gg and then divided by v^\sqrt{\hat v}, so the actual decay depends on each weight's gradient history. AdamW applies the decay separately, θ←θ−η(m^/v^+λθ)\theta\leftarrow\theta-\eta(\hat m/\sqrt{\hat v}+\lambda\theta), so it shrinks weights by a predictable factor.

Go deeper
  • Measured with no loss gradient: Adam 0.003, AdamW 0.366 (the predicted factor).
Follow-up they may askWhat is the shrink factor after TT steps in AdamW?
Interview 13Name three learning-rate schedules and explain warm-up.
Week 5LikelyNotes 6.1Extra

Large early, small late; and tiny at the very start.

main.py
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
warm = torch.optim.lr_scheduler.LinearLR(opt, start_factor=0.01, total_iters=500)   # ramp up
cos = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=10000)                  # then decay
sched = torch.optim.lr_scheduler.SequentialLR(opt, [warm, cos], milestones=[500])
# loop: loss.backward(); opt.step(); sched.step()
Prints
epoch   step    exponential   cosine
    0   0.1000   0.1000        0.1000
   15   0.1000   0.0638        0.0946
   30   0.0100   0.0407        0.0794
   60   0.0010   0.0165        0.0345
   90   0.0001   0.0067        0.0024
  100   0.0001   0.0050        0.0000
Answer
Say this first (30 seconds)

Step decay (multiply by 0.1 every k epochs), exponential decay (η0e−kt\eta_0e^{-kt}), cosine annealing (half-cosine to zero). Large early rates make progress, small late ones let the weights settle. Warm-up ramps the rate up from nearly 0 over the first few hundred steps, because early Adam statistics are poorly estimated and big steps can destabilise training; it is standard for Transformers.

Go deeper
  • PyTorch: scheduler.step() after optimizer.step().
Follow-up they may askWhy does a schedule help a sign-like optimiser such as RMSProp settle?
Next · SummaryWeek 5 Summary Sheet
08
Summary

Week 5 Summary Sheet

Week 5 on one page: chapter glance, every optimiser's update rule, the pairs to tell apart, a "which optimiser" guide, a checklist and corrections.

8.1Week 5 at a glance

C1The objective

Minimise empirical risk as a proxy for true risk (degree 9: 0.0000 vs 0.38). Mini-batch gradient: unbiased, noise ∝1/B\propto1/\sqrt B.

C2Landscape

Convex: every local min is global. Networks are non-convex; saddles (mixed Hessian signs) dominate in high dimensions.

C3Gradient descent

Stable iff η<2/λmax⁡\eta<2/\lambda_{\max}; error factor (κ−1)/(κ+1)(\kappa-1)/(\kappa+1). Newton: one step but d2d^2 memory.

C4Momentum

v←βv−ηgv\leftarrow\beta v-\eta g. 679 steps to 95 (62 Nesterov). Stable up to 2(1+β)/λmax⁡2(1+\beta)/\lambda_{\max}.

C5Adaptive

Adagrad (sum, decays), RMSProp (EMA), Adam (m + v + bias correction), AdamW (decoupled decay).

C6Practice

Schedules: step, exponential, cosine, warm-up. Moons: SGD 459 epochs, momentum 65, Adam 36.

8.2Every update rule

OptimiserUpdateNotes
SGDθ−ηg\theta-\eta gstable if η<2/λmax⁡\eta<2/\lambda_{\max}
Momentumv=βv−ηg; θ+vv=\beta v-\eta g;\ \theta+vβ≈0.9\beta\approx0.9
Nesterovgg at θ+βv\theta+\beta vlook-ahead gradient
Adagradθ−ηg/∑g2\theta-\eta g/\sqrt{\textstyle\sum g^2}rate only shrinks
RMSPropθ−ηg/EMA(g2)\theta-\eta g/\sqrt{\mathrm{EMA}(g^2)}ρ≈0.9\rho\approx0.9
Adamθ−ηm^/(v^+ϵ)\theta-\eta\hat m/(\sqrt{\hat v}+\epsilon)0.9, 0.999, 1e-8
AdamWAdam −ηλθ-\eta\lambda\thetadecay outside the gradient

8.3Key terms

C1-C2
empirical risk
average training loss.
mini-batch
B examples per update.
convex
chords above the graph.
saddle
zero gradient, mixed curvature.
C3-C4
condition number
λmax⁡/λmin⁡\lambda_{\max}/\lambda_{\min}.
Newton step
H−1gH^{-1}g.
momentum
velocity, running gradient average.
Nesterov
gradient at look-ahead.
C5-C6
bias correction
divide by 1−βt1-\beta^t.
AdamW
decoupled weight decay.
warm-up
ramp lr up first.
cosine
half-cosine decay.

8.4The pairs you must tell apart

Adagradsum of g2g^2 (rate decays)
VS
RMSPropmoving average (forgets)
Adam + L2decay is rescaled
VS
AdamWdecay is decoupled
momentumgradient at θ\theta
VS
Nesterovgradient at θ+βv\theta+\beta v
local minimumall curvatures up
VS
saddlemixed curvatures
SGD1 example
VS
mini-batchB examples
epocha pass over the data
VS
iterationone update
first-ordergradient only
VS
second-orderuses the Hessian

8.5Last-minute checklist

C1-C2
  • mini-batch gradient: unbiased, noisy
  • convex ⇒\Rightarrow local = global
  • zero gradient ≠\ne minimum
  • Hessian classifies critical points
C3-C4
  • η<2/λmax⁡\eta<2/\lambda_{\max}
  • κ\kappa large = slow
  • momentum cancels zigzag
  • Newton: d2d^2 memory, saddles
C5-C6
  • Adagrad rate only decays
  • Adam bias correction at start
  • AdamW for Transformers
  • warm-up then cosine

8.6Corrections and additions to the source notes

WhereThe source saysBetter
Momentum"momentum makes training faster" onlyit also widens the stable learning-rate range from 2/λmax⁡2/\lambda_{\max} to 2(1+β)/λmax⁡2(1+\beta)/\lambda_{\max} (measured 0.04 to 0.076)
Adam"Adam needs no tuning"the learning rate still matters: lr 0.05 took 303 steps against 147 at 0.1 on the bowl
Adamweight decay as L2 penaltywith Adam use AdamW; L2 gets rescaled (0.003 vs 0.366 measured)
Local minima"local minima are the main obstacle"in high dimensions saddles dominate; the random-matrix demo is an intuition, not a measurement of a real network
Newton"second-order is strictly better"one step on a quadratic, but d2d^2 memory and attracted to saddles
Optimiser league tables"X converges faster, so X is better"speed on training loss says nothing about generalisation, and each optimiser got one hand-picked lr
Revision overview
Objectiveempirical vs true risk
mini-batch
noise 1/sqrt(B)
Landscapeconvex
local minima
saddles
Gradient descentlr < 2/lambda_max
condition number
Newton
Week 5Optimisation
Momentumvelocity
Nesterov
wider stable range
AdaptiveAdagrad, RMSProp
Adam
AdamW
Practiceschedules
warm-up
which optimiser
Revision: all of Week 5 on one page
Back to top