Mathematics for Machine Learning

Gradient Descent and Optimization


You are on a mountainside in fog so thick you can see about a metre. You need to get to the bottom of the valley. You cannot see the valley, you cannot see the peaks, and you have no map. What you can do is feel the ground under your feet and work out which way is downhill from exactly where you stand.

So you take a step that way. Then you feel the ground again, because after moving, the downhill direction has changed. Step, re-measure, step, re-measure. If the slope is gentle you can afford a long stride; if it is steep and you overstride, you will overshoot the bottom and end up climbing the far side.

That is gradient descent in full. It is not an approximation to something cleverer — for a model with a million parameters, the fog is the honest situation. You cannot see the loss surface, you can only evaluate the loss and its slope at the point you currently occupy. Everything interesting in optimisation is about how to take good steps given only that local information.

The learning rate decides everythingToo large• Each step overshoots the minimum• Loss climbs, then prints nan• Divergence appears within a few epochs• Halving it is the first thing to tryToo small• Every step is correct and useless• Loss falls, then flattens far above zero• Thousands of epochs for no reason• Looks like underfitting, is not
The minus sign gets you pointed downhill; the learning rate is the only thing deciding whether you arrive.

The update rule, and why there is a minus sign

θt+1=θt−η∇θL(θt)\theta_{t+1} = \theta_t - \eta \nabla_\theta L(\theta_t)

Reading it piece by piece. θt\theta_t is the full set of parameters at step tt. L(θt)L(\theta_t) is the loss at those parameters — one number saying how badly the model is doing. ∇θL\nabla_\theta L is the gradient: a vector with one entry per parameter, each entry saying how much the loss would increase if you increased that parameter slightly. And η\eta (eta) is the learning rate, a small positive number controlling how far you move.

The minus sign is the whole point. The gradient points in the direction of steepest increase in the loss. You want the loss to go down. So you move in the opposite direction. Flip that sign and your model will reliably and confidently get worse, which is one of the more embarrassing bugs to find after an hour of watching a loss curve climb.

The gradient tells you where uphill is. Subtracting it is how you go downhill.

Watching it work, one number at a time

Minimise f(θ)=(θ−3)2f(\theta) = (\theta - 3)^2, whose minimum is obviously at θ=3\theta = 3, starting from θ0=0\theta_0 = 0 with η=0.1\eta = 0.1. The gradient is f′(θ)=2(θ−3)f'(\theta) = 2(\theta - 3), so the update becomes θ←θ−0.2(θ−3)\theta \leftarrow \theta - 0.2(\theta - 3).

Stepθ\thetaf′(θ)f'(\theta)f(θ)f(\theta)Distance from 3
00.000-6.0009.0003.000
10.600-4.8005.7602.400
21.080-3.8403.6861.920
31.464-3.0722.3591.536
41.771-2.4581.5101.229
52.017-1.9660.9660.983
62.214-1.5730.6180.786

Look at the last column. Each step multiplies the remaining distance by exactly 0.80.8. The gradient is negative throughout — the loss decreases as θ\theta increases — so subtracting it moves θ\theta up, towards 3. And as you get closer, the gradient shrinks, so the steps shrink automatically. Gradient descent slows down near the bottom without being told to.

The learning rate is the parameter that decides everything

That factor of 0.80.8 was not arbitrary. For this function, the error multiplier per step is ∣1−2η∣|1 - 2\eta|. Three regimes follow directly.

Too large: divergence

Set η=1.1\eta = 1.1. Now the multiplier is ∣1−2.2∣=1.2|1 - 2.2| = 1.2, which is greater than 1, so the error grows and flips sign each step:

Stepθ\thetaf(θ)f(\theta)
00.0009.00
16.60012.96
2-1.32018.66
38.18426.87
4-3.22138.70

The step overshoots the minimum, lands further away on the other side, and the next gradient is therefore even bigger. In a real model this reaches floating-point infinity within a handful of steps and the loss prints as nan.

There is a formula for when this happens. If the largest curvature of the loss surface is LL — the biggest eigenvalue of the Hessian, the matrix of second derivatives — then gradient descent is stable only when

η<2L\eta < \frac{2}{L}

Here f′′(θ)=2f''(\theta) = 2, so L=2L = 2 and the threshold is η<1\eta < 1. Our η=0.1\eta = 0.1 was comfortably inside it; η=1.1\eta = 1.1 was outside. This is worth internalising: divergence is not bad luck, it is a predictable consequence of the step size exceeding twice the reciprocal of the sharpest curvature.

Too small: crawling

Set η=0.001\eta = 0.001. The multiplier is 0.9980.998, so after 100 steps the error has only fallen to 0.998100≈0.820.998^{100} \approx 0.82 of its original value — from a distance of 3 down to 2.46. You will get there eventually, but "eventually" may be a hundred thousand epochs. Worse, on a real non-convex surface a tiny learning rate lets the model settle into the first shallow dip it finds instead of passing through it.

Just right, and how you find it

The honest method is to run a few hundred steps at each of 10−1,10−2,10−3,10−410^{-1}, 10^{-2}, 10^{-3}, 10^{-4} and look at the loss curves. Too large diverges or oscillates wildly. Too small produces a nearly flat line. The right one drops fast and then bends. Pick the largest rate that still descends smoothly, since that gets you the most progress per step.

Schedules: change it as you go

Early in training you are far from anywhere good and want to cover ground. Late in training you are near a minimum and want to fine-tune. One fixed rate cannot do both, which is why almost every serious training run varies η\eta over time.

ScheduleRuleWhen it suits
Step decayMultiply η\eta by 0.1 every kk epochsSimple, predictable; the classic image-classification recipe
Exponential decayηt=η0e−kt\eta_t = \eta_0 e^{-kt}Smooth continuous shrinking, no sudden jumps
Cosine annealingηt=η02(1+cos⁡πtT)\eta_t = \tfrac{\eta_0}{2}\left(1 + \cos\frac{\pi t}{T}\right)Decays slowly at first, sharply at the end; strong default
WarmupRamp η\eta up from near zero over the first few hundred stepsLarge batches and transformers, where early steps are unstable
Reduce on plateauCut η\eta when validation loss stops improvingReactive; no schedule to tune in advance

How much data goes into each step

The gradient of the loss over the whole training set is the average of the per-example gradients. You do not have to use all of them.

Batch (full)Stochastic (one example)Mini-batch
Examples per updateAll nn132–512 typically
Updates per epoch1nnn/Bn / B
Gradient qualityExactVery noisyNoisy but usable
MemoryEntire datasetTrivialTunable to fit the device
Hardware useGood, but one step at a timeTerrible — no parallelismExcellent — fills a GPU
VerdictOnly for small dataRarely used literallyWhat everyone actually does

Mini-batching wins for two independent reasons. The practical one is hardware: a GPU computing 128 examples at once is barely slower than computing one, so you get 128 times the information for almost free. The subtler one is that the noise helps. An exact gradient will walk you straight into the nearest dip and stop. A noisy gradient rattles the parameters around enough to escape shallow dips and keep looking. Noise is not merely tolerated here — it is doing useful work.

Batch size is not just a memory setting. It controls how much noise is in every step, and that noise is part of how the model generalises.

Where plain gradient descent struggles

Consider a loss surface shaped like a long narrow valley — steep across, nearly flat along. Concretely, f(x,y)=x2+20y2f(x,y) = x^2 + 20y^2, where the yy direction is twenty times more curved than the xx direction.

The gradient at any point is dominated by the steep direction, so descent takes a big step across the valley, overshoots, comes back, overshoots again. It zig-zags violently while making almost no progress along the valley floor, which is where the minimum actually is. Your learning rate is capped by the steep direction (η<2/40=0.05\eta < 2/40 = 0.05 here) but the shallow direction needs a much larger step to move at all.

This is ill-conditioning, and it is the normal state of affairs for real models, not a pathological case. The ratio of largest to smallest curvature in a deep network is routinely thousands. Alongside it sit two more problems: plateaus, where the gradient is near zero over a wide region so progress stalls, and saddle points, where the gradient is exactly zero but the point is a minimum in some directions and a maximum in others. In high dimensions saddles hugely outnumber true local minima, because requiring all million directions to curve the same way is astronomically unlikely.

Momentum: remember where you were going

The zig-zag has a structure worth exploiting. The across-valley component of the gradient flips sign every step; the along-valley component keeps pointing the same way. If you accumulate a running average of recent gradients, the alternating components cancel and the consistent one builds up.

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

The vector vtv_t is a velocity. Each step it decays by a factor β\beta (usually 0.9) and has the current gradient added. The physical picture is a heavy ball rolling downhill rather than a hiker taking discrete steps: it carries inertia through flat patches and does not get thrown sideways by every local wobble.

If the gradient stays roughly constant at gg, the velocity converges to a geometric series:

v∞=g(1+β+β2+⋯ )=g1−βv_\infty = g\left(1 + \beta + \beta^2 + \cdots\right) = \frac{g}{1 - \beta}

With β=0.9\beta = 0.9 that is 10g10g. So in a consistent direction, momentum eventually takes steps ten times larger than plain descent would. That is where the speed-up comes from — and also the warning: momentum can overshoot a sharp minimum and need several steps to settle, which is why raising β\beta towards 0.99 sometimes destabilises training that was fine at 0.9.

Adaptive rates: one learning rate per parameter

Momentum fixes the direction. It does not fix the fact that different parameters need wildly different step sizes. RMSprop addresses that by tracking a running average of each parameter's squared gradient and dividing by its square root:

st=βst−1+(1−β) gt2,θt+1=θt−ηst+ϵ gts_t = \beta s_{t-1} + (1-\beta)\,g_t^2, \qquad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_t} + \epsilon}\,g_t

Everything here is element-wise, so each parameter gets its own scaling. A parameter that has been receiving large gradients gets a large ss, so its effective step is divided down. A parameter with consistently small gradients gets a small ss and its step is boosted. The tiny ϵ\epsilon (about 10−810^{-8}) exists only to stop division by zero.

The effect on the narrow valley is direct: the steep direction gets damped, the shallow direction gets amplified, and the zig-zag largely disappears.

Adam combines both

mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t \qquad v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2

m^t=mt1−β1tv^t=vt1−β2tθt+1=θt−η m^tv^t+ϵ\hat m_t = \frac{m_t}{1 - \beta_1^t} \qquad \hat v_t = \frac{v_t}{1 - \beta_2^t} \qquad \theta_{t+1} = \theta_t - \frac{\eta\,\hat m_t}{\sqrt{\hat v_t} + \epsilon}

mtm_t is momentum (an average of gradients) and vtv_t is the RMSprop term (an average of squared gradients). The interesting part is the bias correction in the middle line, which people often skip over.

Both averages start at zero. At the very first step with β1=0.9\beta_1 = 0.9, you get m1=0.1 g1m_1 = 0.1\,g_1 — a tenth of the true gradient, purely because the average has not had time to fill up. Dividing by 1−β11=0.11 - \beta_1^1 = 0.1 recovers exactly g1g_1. The second moment is worse: with β2=0.999\beta_2 = 0.999, v1=0.001 g12v_1 = 0.001\,g_1^2, and dividing by 1−0.999=0.0011 - 0.999 = 0.001 recovers g12g_1^2. Without the correction, v1\sqrt{v_1} would be about 32 times too small while m1m_1 is only 10 times too small, so the first step would come out about three times larger than intended — at exactly the moment the parameters are furthest from anywhere sensible. The correction factors shrink towards 1 as tt grows and become irrelevant after a few hundred steps, but they matter enormously at the start.

OptimiserTypical η\etaStrengthsWeaknesses
SGD0.01–0.1Simple; often best final accuracy with a good scheduleSlow; very sensitive to η\eta
SGD + momentum0.01–0.1, β=0.9\beta=0.9Much faster; the standard for vision modelsTwo things to tune; can overshoot
RMSprop0.001Handles varying scales; good for recurrent modelsNo momentum on its own
Adam0.001Works out of the box on almost anythingCan generalise slightly worse than tuned SGD
AdamW0.001Adam with weight decay applied correctly, not folded into the gradientAdds a decay coefficient to tune

Practical advice: start with Adam at η=10−3\eta = 10^{-3}. It is forgiving and gets you a working baseline fast. If you are chasing the last fraction of a percent on a well-understood task, switch to SGD with momentum and a cosine schedule and tune it properly.

Knowing when to stop

For a convex surface — a single bowl, one minimum — convergence means the gradient norm has gone to zero and you are provably at the global best. Linear and logistic regression are like this.

Deep networks are not. Their surfaces have vast numbers of critical points, and no optimiser gives you a guarantee about which one you reached. "Converged" honestly means "the loss stopped improving and I stopped paying for compute". Reasonable stopping criteria:

  • Small gradient: ∥∇L∥<10−6\|\nabla L\| < 10^{-6}. Sound in theory, but on a plateau or saddle it fires early.
  • Loss plateau: the change in loss stays under a threshold for several consecutive epochs.
  • Iteration budget: a hard cap, so a bad run cannot burn the cluster.
  • Early stopping: track loss on a held-out validation set, keep a copy of the best weights, and stop after pp epochs with no improvement.

Early stopping is the one that matters most, because it targets the right quantity. Training loss goes down forever — that is what you are optimising. Validation loss goes down, bottoms out, and then rises as the model starts memorising. The bottom of that curve is the model you want, and you will only find it if you were watching.

Implementing it

Python
import numpy as np# An ill-conditioned quadratic: 20x more curved in y than in x.def f(p):     return p[0]**2 + 20 * p[1]**2def grad(p):  return np.array([2 * p[0], 40 * p[1]])def sgd(grad, theta, lr=0.04, steps=60, beta=0.0):    v = np.zeros_like(theta)    path = [theta.copy()]    for _ in range(steps):        g = grad(theta)        v = beta * v + g              # beta = 0 gives plain gradient descent        theta = theta - lr * v        path.append(theta.copy())    return np.array(path)def adam(grad, theta, lr=0.1, steps=60, b1=0.9, b2=0.999, eps=1e-8):    m = np.zeros_like(theta)    v = np.zeros_like(theta)    path = [theta.copy()]    for t in range(1, steps + 1):        g = grad(theta)        m = b1 * m + (1 - b1) * g        v = b2 * v + (1 - b2) * g**2        m_hat = m / (1 - b1**t)       # bias correction: vital at small t        v_hat = v / (1 - b2**t)        theta = theta - lr * m_hat / (np.sqrt(v_hat) + eps)        path.append(theta.copy())    return np.array(path)start = np.array([5.0, 1.0])for name, path in [("plain   ", sgd(grad, start)),                   ("momentum", sgd(grad, start, beta=0.9)),                   ("adam    ", adam(grad, start))]:    print(name, "final f =", round(float(f(path[-1])), 6))

Run it and the ordering is probably not the one you expected: plain descent finishes lowest (about 0.0011), momentum second (about 0.042) and Adam last (about 0.26). Each result is a caveat from this lesson made visible. The valley is only mildly stretched, with a curvature ratio of 20, and plain descent at a learning rate close to its 2/402/40 limit shrinks the xx distance by a steady factor of 0.92 per step. Momentum at β=0.9\beta = 0.9 overshoots: by step 10 it has swung past the minimum to x≈−2.8x \approx -2.8, and it is still spiralling in at step 60. Change it to beta=0.5 and it finishes near 5×10−125 \times 10^{-12}, far ahead of the others. Adam does what the RMSprop section promised — yy settles without a zig-zag — but each of its steps is roughly η=0.1\eta = 0.1 long whatever the gradient, so covering the five units along xx takes it most of the sixty steps.

The lesson is not that one optimiser is best. Each has settings, and the right settings depend on the shape of the surface.

Reading a training run when it goes wrong

Almost every training failure has a signature in the loss curve, and almost every signature points at the optimiser.

What you seeMost likely causeWhat to change
Loss becomes nan or infStep size above the 2/L2/L stability bound, or exploding gradientsCut η\eta by 10x; add gradient clipping; check for log⁡(0)\log(0) or division by zero in the loss
Loss oscillates without settlingη\eta near the stability limitHalve η\eta, or add a decay schedule
Loss almost flat from step 1η\eta far too small, or dead units, or saturated activationsRaise η\eta by 10x; check the initialisation; print gradient norms per layer
Loss drops then freezes at a poor valuePlateau or saddleAdd momentum; use a smaller batch for more noise; restart from a different initialisation
Training loss falls, validation loss risesOverfitting — nothing to do with the optimiserEarly stopping, weight decay, more data, a smaller model
Loss identical every epochGradients are not reaching the parameters at allCheck the optimiser is stepping, that gradients are being cleared between steps, and that nothing is detached

What to do on a real training run

Before anything else, overfit a tiny subset. Take twenty examples and train until the loss is essentially zero. If a model cannot memorise twenty examples, the problem is a bug — a wrong loss, a broken gradient, a label mismatch — and no amount of learning-rate tuning will help. This one check catches most real failures in minutes.

Then set a baseline: Adam at 10−310^{-3}, a mini-batch that fills your device, a cosine schedule, and early stopping on a validation split. Log the loss every few hundred steps and log the gradient norm alongside it, because the gradient norm tells you why the loss is doing what it is doing. A norm that climbs towards infinity and a norm that collapses to zero produce identically flat loss curves but need opposite fixes.

Finally, when a step size feels arbitrary, remember it is not. The bound η<2/L\eta < 2/L says the largest usable step is set by the sharpest curvature in the loss surface. Every technique in this area — normalisation layers, careful initialisation, momentum, per-parameter scaling — is ultimately a way of reshaping that surface so a single learning rate can work for every parameter at once.