S020 · Calculus II — Gradients & Gradient Descent from Scratch
The one algorithm behind all of ML: gradients as the ‘uphill direction’ in n dimensions, SGD as the workhorse, and the learning-rate/momentum knobs that decide whether you converge or explode.
🎯 Implement gradient descent from scratch on linear regression, feel what learning rate does, and know when to reach for momentum, Adam, or a schedule.
Why this session exists
Every model in machine learning — linear regression, logistic regression, neural nets, transformers with 175 billion parameters — is trained by the same algorithm. Compute the gradient of the loss with respect to the parameters. Step in the opposite direction. Repeat. That's it. If you can implement gradient descent from scratch on a 2-parameter problem, you already understand how GPT is trained — the rest is engineering (parallelism, memory, tricks). This session gets you fluent in the mechanic that underlies everything.
- Compute the gradient of a multi-variable function by hand and interpret it as ‘direction of steepest ascent’.
- Implement vanilla gradient descent, SGD, and momentum in ≤ 20 lines each — and know when to use which.
- Choose a learning rate that neither diverges nor stalls (and diagnose which failure you're seeing).
- Explain why deep-learning defaults are Adam + cosine schedule + gradient clipping.
- Watch a loss curve and diagnose learning-rate too high / too low / just right in 5 seconds.
Prerequisites
- S019 · Derivatives & chain rule — a gradient is just a vector of partial derivatives.
- S017/S018 · Linear algebra — you'll do vector arithmetic and matrix updates.
(a) Intuition · 5 min
You're on a mountain in dense fog. You want to reach the valley but can only see the ground under your feet. Every few seconds you check which direction is most downhill and take a step that way. If the slope is steep, big step. If the ground is flat, tiny step. Eventually you're at the valley bottom — where the slope in every direction is zero.
That fog is what an ML model sees. It doesn't know the shape of the loss landscape globally. It only knows the local slope. Gradient descent is the ‘check underfoot, step downhill, repeat’ strategy — repeated a million times for a million-parameter landscape.
In code, the "slope in every direction" is the gradient — a vector where entry i is ∂loss/∂parameter_i. You step against it: params -= lr · gradient. The learning rate lr is your step size. Too big and you jump over the valley. Too small and you never arrive. Getting it right (or getting an adaptive optimiser to get it right for you) is the whole art of training.
Every optimiser you'll ever use — SGD, momentum, RMSprop, Adam, AdamW, Lion — is a variation on this one theme.
The gradient in one line
- For a scalar function f(x, y, z), the gradient ∇f = [∂f/∂x, ∂f/∂y, ∂f/∂z]. A vector of partial derivatives.
- Geometrically: ∇f points in the direction of steepest ASCENT. |∇f| is how steep. −∇f is steepest DESCENT.
- At a minimum, ∇f = 0. That's the fixed point gradient descent converges to.
- For a loss with 175 billion parameters, ∇f is a vector of 175 billion numbers. Every training step computes it.
A quick history
- 1847Cauchy · gradient descentAugustin-Louis Cauchy publishes the method for solving equations. 175 years old and still the winner.
- 1951SGD · Robbins & MonroStochastic approximation — descend using noisy per-sample gradient estimates. The seed of mini-batch SGD.
- 1964Momentum · PolyakAdd a fraction of the previous step to the current — dampens oscillations, accelerates flat regions.
- 2012AdaGrad · Duchi et al.Per-parameter adaptive learning rates. Then RMSprop, then Adam (2014). Deep learning explodes.
- 2018+AdamW · Loshchilov & HutterDecoupled weight decay. The default for training modern transformers (BERT, GPT, LLaMA).
- 2023+Lion, Sophia, ShampooNewer optimisers experimenting with sign-based updates and second-order approximations. Adam still dominates production.
(b) Visual walkthrough · 15 min
The update rule, forever
Batch · mini-batch · stochastic — three flavours
Full dataset per step
- True gradient — no noise
- One step per epoch
- Slow, memory-hungry
- OK for classical convex problems, bad for modern deep learning
One sample per step
- Very noisy gradient — but that noise helps escape local minima
- Cheap per step
- Requires small learning rate
- Rarely used pure — mini-batch dominates
The workhorse
- Batch of 32-1024 samples per step
- Compromise: gradient noise controlled but still cheap per step
- Utilises GPU parallelism efficiently
- This is what ‘SGD’ means in modern practice
The optimiser family tree
Optimisers, from simple to state-of-the-art
Learning-rate diagnostics from a loss curve
You're descending the loss surface. Consider slightly larger lr to see if you can converge faster.
You're overshooting the valley each step. Halve the lr and retry.
You're taking micro-steps. Double the lr. If still no movement, check gradient magnitudes — may be a vanishing-gradient problem.
Weights diverged; some intermediate value overflowed. Rewind to a checkpoint, clip gradient norms, use smaller lr.
Decay the learning rate (cosine, step, or exponential). Modern training almost always uses a schedule.
"Gradient descent finds the minimum. If training converged, I'm at the best solution the model can reach — and if it didn't converge, I should train longer."
Gradient descent finds a point where the gradient is approximately zero, reachable from where you started. For non-convex losses that is one of astronomically many stationary points, and which one you land in depends on initialisation, batch order, and learning rate schedule. "Converged" means "stopped moving", not "found the best".
Because for the convex problems used to teach it — linear regression, logistic regression — the myth is literally true: there is one minimum, and gradient descent finds it from anywhere. Everyone's first optimisation experience confirms it. Then the same intuition is carried into deep networks where the loss surface is wildly non-convex, and it produces two specific wrong conclusions. First, that a run that plateaued is done, when it may have stalled on a saddle point or in a flat region that momentum or a learning-rate change would escape. Second, that two runs of the same code should agree — they don't, and the spread between seeds is real information about your setup rather than noise to be hidden by fixing the seed and reporting one number.
Same function, same optimiser, different start — different answer:
import numpy as np
f = lambda x: x**4 - 3*x**3 + 2 # two basins
df = lambda x: 4*x**3 - 9*x**2
def descend(x, lr=0.01, steps=2000):
for _ in range(steps): x -= lr * df(x)
return x
print(descend(-1.0), descend(3.0)) # different minima
print(descend(0.0)) # stuck: gradient is exactly 0 hereWhy does stochastic gradient descent — using a noisy estimate from a small batch — work better than full-batch descent, not just faster? Using less data per step should be strictly worse information.
- 1The true gradient is the average of per-example gradients over the whole dataset, so a random batch's gradient is an unbiased estimator of it.forced by · the expectation of a uniform random sample's mean equals the population mean
- 2The standard error of that estimate shrinks like 1/√B in batch size B. So going from batch 100 to batch 10,000 costs 100× the compute to cut the noise by only 10×.forced by · estimator variance falls linearly in sample size, so the error falls with its square root
- 3Therefore per unit of compute, many noisy small steps make far more progress than one precise large step — the direction only has to be roughly right for the step to reduce the loss.forced by · descent needs a descent direction, not the optimal one, and any direction with positive inner product with the true gradient works
- 4Separately, the noise means the iterate never sits exactly at a stationary point. Random perturbation kicks it off saddle points, where the true gradient is exactly zero and full-batch descent would halt forever.forced by · a saddle is unstable in at least one direction, so any perturbation with a component along it escapes
- 5And the noise scale acts like a temperature: it cannot settle in a minimum whose basin is narrower than the typical step, so it preferentially ends up in wide, flat minima.forced by · escaping a basin requires a fluctuation larger than the basin's width, which is easy for narrow ones and hard for wide ones
Therefore the noise in SGD is not a tolerated cost of cheap computation — it is doing real work: escaping saddles and biasing the search toward flat regions, which tend to generalise better than sharp ones.
And note what this predicts, all observable: very large batches reduce the noise and are commonly reported to need explicit compensation — learning-rate scaling and warmup — to match small-batch generalisation. It predicts that decaying the learning rate late in training is what finally lets the iterate settle, since it shrinks the effective temperature. And it predicts why batch size and learning rate must be tuned together rather than independently: it is their ratio that sets the noise scale, so changing one alone changes the optimisation regime.
You are on a landscape in the dark. You cannot see the terrain — you can only feel which way is downhill under your feet, and take a step. That is the entire algorithm. The gradient is the local feel of the slope; the learning rate is your stride length; the loss curve is your altitude log.
Everything that goes wrong has an obvious physical reading: too big a stride and you leap across the valley and up the far side (loss diverges); too small and you take a week to descend (slow convergence); a flat plateau and you can't tell which way to go (vanishing gradient); a narrow ravine and you bounce between the walls while barely advancing along it (which is exactly what momentum fixes by accumulating the consistent direction and cancelling the oscillation).
- Learning rate is the single most important hyperparameter. Find it first, by sweeping orders of magnitude — 1e-1 to 1e-5 — not by tuning it in fine increments.
- Diverging loss or NaN means the step is too large for the local curvature. Lower the learning rate, add warmup, or clip gradients — in that order.
- Features on wildly different scales create ravines, because the loss curves far more steeply along one axis than another. Normalising inputs is not cosmetic; it reshapes the landscape into something a single global step size can handle.
- Always plot the loss curve. Diverging, plateaued, noisy-but-descending, and overfitting each have a distinct visual signature, and none of them is visible in a final metric.
Fire this model the moment you see: loss going to NaN · a training curve that is flat from step one · validation loss rising while training loss falls · results that swing between seeds · a "just train longer" suggestion · any hyperparameter search that didn't start with the learning rate.
Which optimiser: plain SGD, SGD with momentum, or an adaptive method like Adam?
Start with AdamW, because time-to-first-signal matters more than the last fraction of a point, and it removes learning-rate sensitivity as a confounder while you're still debugging the data and the model. Move to tuned SGD+momentum only if you have a long training budget and evidence the optimiser is what's limiting you.
And keep the sequence honest: optimiser choice is almost never the binding constraint. Data quality, feature scaling, and the learning rate schedule dominate it by a wide margin. Swapping optimisers to fix a bad training run is a way of avoiding the harder question of whether the loss, the labels, or the inputs are wrong.
(c) Hands-on · 25 min
You'll fit a straight line to noisy data three ways — analytic solution, vanilla GD, and SGD with momentum — and watch the learning-rate knob live. Save as gd_demo.py.
Run it:
uv run --with numpy python gd_demo.pyExpected output:
What each block does
Anatomy of the code
Visualise the loss landscape (requires matplotlib):
import matplotlib.pyplot as plt
ws = np.linspace(-1, 5, 50)
bs = np.linspace(-2, 4, 50)
W, B = np.meshgrid(ws, bs)
L = np.zeros_like(W)
for i in range(W.shape[0]):
for j in range(W.shape[1]):
L[i, j] = mse_loss(np.array([W[i, j], B[i, j]]), X, Y)
plt.figure(figsize=(7, 6))
plt.contour(W, B, L, levels=20)
plt.plot(2, 1, "r*", markersize=15, label="true (2, 1)")
plt.xlabel("slope w"); plt.ylabel("intercept b")
plt.title("MSE loss surface for y = wx + b")
plt.legend(); plt.savefig("loss_surface.png", dpi=100)Now overlay the trajectory of a GD run at three different lrs and see the paths side-by-side. This IS what "training curves" show, just in 2 dimensions instead of a million.
(d) Production reality · 15 min
The LLaMA paper reports several training runs going wildly divergent — loss spikes to 10× baseline and never recovers. Requires rewinding hundreds of billions of tokens of compute (millions of dollars).
Cause: rare gradient spikes on specific batches (numerical instability + fat-tailed input distribution) that momentarily exceed the ‘safe’ update magnitude.
clip_grad_norm_(params, max_norm=1.0)) before every optimiser step. If the gradient's total L2 norm exceeds 1.0, rescale it to length 1.0. Doesn't change direction; only magnitude. Also: skip the optimiser step entirely on any batch with NaN loss and rewind. Standard practice for all frontier LLM training.DataLoader(..., shuffle=True). In fit-loops: rng.permutation(n). The default is ‘on’ for good reason — if you're overriding, know why.λ·w to the gradient) interacts oddly with Adam's per-parameter scaling — the effective regularisation strength depends on gradient magnitudes.AdamW, not Adam. AdamW applies weight decay outside the adaptive-lr scaling: θ ← θ − lr · (Adam_update + λ · θ). Loshchilov & Hutter's 2018 paper. It's the default optimiser for modern transformers (BERT, GPT-family, ViT).Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
- What does gradient descent do, in one paragraph, to a total beginner?
- What is the learning rate and what happens if it's too big or too small?
- Why does every deep-learning framework compute gradients automatically?
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.