Gradient descent
Fit a forecasting line by gradient descent in NumPy, trace updates by hand, and diagnose learning rates that crawl, bounce or explode.
Words in this lessonLoss function · Gradient · Gradient descent · Learning rate · Mini-batch · Parameters
- Loss function
- A formula that turns “how wrong is the model?” into a single number to minimise. Like a scorecard that gives one total penalty for every mistake made on a tray of pastries.
- Gradient
- The list of slopes of the loss, one for every parameter, pointing in the direction where the loss rises fastest. Like a compass that always points uphill, so you walk the other way to get down.
- Gradient descent
- The method training uses to decide which way, and how far, to turn each parameter to reduce the loss. Like walking down a hillside blindfolded, feeling which way the ground slopes and taking a small step that way.
- Learning rate
- The number that sets how big a step training takes on each update. Like your stride length walking downhill: tiny steps take forever, giant leaps overshoot the valley.
- Mini-batch
- A small random group of training examples used for one update step. Like tasting one tray of croissants instead of every croissant baked that day.
- Parameters
- The adjustable numbers inside a model, set during training, that decide what it outputs. Like the knobs on a sound mixing desk: the same desk sounds completely different depending on where the knobs sit.
On this page
- The data, and what "a good line" means
- Arjun's brute-force idea
- A walk in the fog
- Following one parameter, step by step
- Now take giant strides
- Opening the hood: the update rule
- Gradient descent in code
- Why the standardising mattered
- Tasting by the tray: mini-batches
- Where gradient descent goes wrong
- Try it yourself
- Common mistakes
- Recap
- Go deeper
Crumb sells cold coffee, and cold coffee has a problem: the cold brew has to be made the night before. Last Tuesday, a 31-degree day, the Koregaon Park store prepped 90 cups and sold 70. Twenty cups went down the sink at ₹45 each. On Saturday, at 37 degrees, they prepped 60 and ran out by 2pm. The shift lead counted 28 customers who left without buying anything.
Meera wanted a rule for the evening shift. “The weather app tells us tomorrow’s temperature. Give me a number of cups.”
Arjun had five days of data and a spreadsheet. He typed a formula, cups = 2 × temperature − 10, and compared it to reality. Too low. He tried 3 × temperature − 20. Closer. He nudged the 3 to 2.9, then the 20 to 22, then back. Twenty minutes later he had a decent line and a headache.
Priya leaned over his shoulder. “You’re doing gradient descent. Slowly, by hand, by feel. Let’s give the computer a compass instead.”
Important
The idea in one sentence: gradient descent finds good parameter values by repeatedly measuring how wrong the model is, computing which direction makes it less wrong (the gradient), and taking a small step that way, with the learning rate setting the step size.
The data, and what “a good line” means
Here are Arjun’s five days at Koregaon Park:
| Temperature (°C) | 25 | 28 | 31 | 34 | 37 |
|---|---|---|---|---|---|
| Cold coffees sold | 52 | 61 | 70 | 80 | 88 |
The model is a straight line with two parameters (the adjustable numbers a model learns): a slope and an intercept .
is the temperature and (“y-hat”) is the predicted number of cups. Arjun’s guess of predicts 55 cups at 25 degrees. Reality was 52, an error of 3.
To judge a line, you need one number for “how wrong is it overall?”. Take each day’s error, square it (so a miss of −3 and a miss of +3 count the same, and big misses count a lot more), then average. That is mean squared error, or MSE, the most common loss function (a formula that boils “how wrong” down to one number) for predicting quantities:
A perfect line has loss 0. The goal is to find the and that make as small as possible.
Arjun’s brute-force idea
Arjun had a better plan than nudging by hand. “Computers are fast. Let me try every combination.”
import numpy as np
temps = np.array([25, 28, 31, 34, 37], dtype=float)
sales = np.array([52, 61, 70, 80, 88], dtype=float)
best = (None, None, float("inf"))
for w in np.arange(0, 20, 0.01): # 2,000 slopes
for b in np.arange(-100, 100, 0.1): # 2,000 intercepts
loss = ((w * temps + b - sales) ** 2).mean()
if loss < best[2]:
best = (w, b, loss)
print(best)
Four million combinations. It ran for about fifteen seconds on his laptop and found , , with a loss of 0.14. He looked pleased.
“Good,” said Priya. “Now do the late-delivery network you’re building this week. It’ll have 241 parameters.”
“Same idea, more loops.”
“Suppose you only try 10 values for each parameter. That’s 10 times 10 times 10, 241 times. combinations. There are about atoms in the observable universe.”
Arjun did not reply. Brute force works for two knobs and is hopeless for anything you would call a neural network. An LLM has billions of knobs. There has to be a way to improve the knobs without trying them all.
A walk in the fog
“Have you been up Sinhagad at dawn in the monsoon?” Priya asked. “Fog so thick you can’t see your own feet. Say you want to get down to the car park. What do you do?”
“Walk downhill?”
“Which way is downhill? You can’t see.”
Arjun thought. “I’d feel it. Whichever way the ground slopes down under my feet, I’d step that way. Then feel again.”
“That’s the whole algorithm.”
- Where you stand is the current values of the parameters, and .
- Your height is the loss. Lower is better.
- The slope under your feet is the gradient (the list of slopes of the loss, one per parameter, pointing in the direction where the loss rises fastest).
- Your stride is the learning rate (a small number that sets how far you move each step).
You never see the whole mountain. You only feel the local slope, step, and feel again. With two parameters, the “mountain” is a surface you could draw. With 241, or a billion, nobody can picture it, but the procedure is identical, because computing a slope does not care how many directions there are.
“What if I take giant strides to get down faster?” Arjun asked.
“Try it and see,” Priya said. “On paper first.”
Following one parameter, step by step
To see every number, shrink the problem. One parameter, one data point. The model is (no intercept). The single data point is , . The right answer is obviously , but the algorithm does not know that.
The loss for this one point is:
Its derivative (the slope of the loss as you change , from lesson 1) is:
The update rule says: new = old − learning rate × slope. Start at with a learning rate of 0.05.
| Step | Slope | Change | New | Loss before | |
|---|---|---|---|---|---|
| 1 | 0.00 | −24.00 | +1.20 | 1.20 | 36.0 |
| 2 | 1.20 | −14.40 | +0.72 | 1.92 | 12.96 |
| 3 | 1.92 | −8.64 | +0.43 | 2.35 | 4.67 |
| 4 | 2.35 | −5.18 | +0.26 | 2.61 | 1.68 |
| 5 | 2.61 | −3.11 | +0.16 | 2.77 | 0.60 |
The slope is negative, which means “increasing decreases the loss”, so goes up. It creeps towards 3. Nobody told it the answer; it felt its way there.
Note
Pause and think. The learning rate stayed at 0.05 the whole time, yet the steps shrank: +1.20, +0.72, +0.43, +0.26. Why?
Show the answer
The step is learning rate × slope, and the slope shrinks as you approach the bottom of the valley. At the slope is exactly zero, so the step is zero and the parameter stops moving. Gradient descent slows down automatically as it arrives, like a ball settling in a bowl. That is also why it can be painfully slow near the end: tiny slopes mean tiny steps.
Now take giant strides
Same start, learning rate 0.3:
| Step | Slope | Change | New | |
|---|---|---|---|---|
| 1 | 0.00 | −24.0 | +7.20 | 7.20 |
| 2 | 7.20 | +33.6 | −10.08 | −2.88 |
| 3 | −2.88 | −47.0 | +14.11 | 11.23 |
It leaps right over the answer to 7.2, then overcorrects to −2.88, then flies off to 11.23, further away each time. This is divergence. In real training it looks like a loss that climbs and then turns into nan (not a number, which is what a computer prints when values overflow).
There is a neat way to see exactly where the cliff edge is. For this problem, every step multiplies your distance from the answer by , where (“eta”) is the learning rate:
| Learning rate | Multiplier | What happens |
|---|---|---|
| 0.01 | 0.92 | Shrinks the gap by 8% a step: slow but steady |
| 0.05 | 0.60 | Shrinks by 40% a step: good |
| 0.125 | 0.00 | Lands on 3 in one step: perfect (and lucky) |
| 0.25 | −1.00 | Bounces between 0 and 6 forever |
| 0.3 | −1.40 | Each bounce 40% bigger: explodes |
The best learning rate depends on how steep the valley is. Steeper valleys need smaller strides. A real network has a different steepness in every direction, which is why choosing the learning rate is part science, part trying a few values.
Opening the hood: the update rule
Back to two parameters and the full dataset. For MSE on a straight line, calculus gives the slope for each parameter:
The curly just means “slope with respect to this one parameter, holding the others still”. You do not need to derive these; read them. The slope for is twice the average error. If the line predicts too high on average, the slope is positive, and stepping against it lowers . The slope for is the same thing, but each error is weighted by its temperature, so hot days have more say over the slope.
Put both slopes in a list, (“nabla L”, the gradient), put both parameters in a list, (“theta”), and the entire algorithm is one line:
New parameters equal old parameters minus learning rate times gradient. The minus sign is “go downhill”. Repeat until the loss stops falling.
Gradient descent in code
Here it is on the cold coffee data. One extra step at the top: the temperatures are standardised (rescaled to mean 0 and standard deviation 1, as in lesson 1). You will see why in a moment.
import numpy as np
temps = np.array([25, 28, 31, 34, 37], dtype=float)
sales = np.array([52, 61, 70, 80, 88], dtype=float)
# Standardise the input so one learning rate suits both parameters
mu, sigma = temps.mean(), temps.std() # 31.0 and about 4.24
x = (temps - mu) / sigma
w, b = 0.0, 0.0 # start anywhere
lr = 0.1 # the learning rate
for step in range(101):
pred = w * x + b # 1. predict
error = pred - sales
loss = (error ** 2).mean() # 2. measure (MSE)
dw = 2 * (error * x).mean() # 3. slope for w
db = 2 * error.mean() # slope for b
w -= lr * dw # 4. step downhill
b -= lr * db
if step % 20 == 0:
print(f"step {step:3d} loss {loss:8.2f} w {w:6.2f} b {b:6.2f}")
t = 30
print(f"Forecast at {t}°C: {w * (t - mu) / sigma + b:.1f} cold coffees")
Line by line, the new parts: error is how far off each day’s prediction is. dw and db are the two slope formulas from above, in NumPy. w -= lr * dw is the update rule. The last line converts 30 degrees into the same standardised units before predicting.
step 0 loss 5093.80 w 2.57 b 14.04
step 20 loss 0.82 w 12.75 b 69.55
step 40 loss 0.14 w 12.87 b 70.19
step 60 loss 0.14 w 12.87 b 70.20
step 80 loss 0.14 w 12.87 b 70.20
step 100 loss 0.14 w 12.87 b 70.20
Forecast at 30°C: 67.2 cold coffees
The loss falls from 5,094 to 0.14 in about forty steps and then stops, because it has reached the bottom. That 0.14 is exactly what Arjun’s four-million-combination search found, in 101 cheap steps instead of four million.
(Why is 12.87 and not 3.03? Because it is the slope per standard deviation of temperature, not per degree. One standard deviation is 4.24 degrees, and . Same line, different units.)
Why the standardising mattered
Arjun tried removing the standardisation, using raw temperatures with the same learning rate of 0.1. The loss went to infinity within a few dozen steps.
Look at the slope formula for : every error is multiplied by . With raw temperatures around 30, the slope for is roughly 30 times bigger than the slope for . A step size that is sensible for is wildly too big for , which overshoots and explodes, exactly like the learning rate 0.3 table. Shrink the learning rate until behaves, and now crawls.
In the fog analogy: the valley is a long, narrow ravine, steep across and nearly flat along. You keep bouncing between the walls while barely moving down the length. Standardising reshapes the ravine into a round bowl, where every direction is about equally steep and one stride length fits all. This is the main reason almost every deep learning pipeline standardises its inputs.
Tasting by the tray: mini-batches
The code above looks at all five days before taking each step. That is called batch gradient descent. Fine for five rows. Hopeless for Crumb’s 50,000 product photos, where computing one exact slope would mean processing every photo before moving once.
Priya’s bakery version: you want to know whether today’s croissant batch needs more butter. You could taste all 400 croissants (accurate, and nobody wants to), taste one random croissant (fast, but it might be the odd one), or taste a tray of 32 (a good estimate, quickly).
- Stochastic gradient descent (SGD) uses one example per step. Very cheap, very noisy.
- Mini-batch gradient descent uses a small random group of examples, a mini-batch (usually 32 to 256), per step. This is what everyone actually does. Confusingly, libraries still call it “SGD”.
Each mini-batch gives an estimate of the true slope: right on average, wrong by a bit each time. So the path down the mountain wobbles. That is mostly fine. You get hundreds of slightly wobbly steps in the time one perfect step would take.
Note
Pause and think. If the full dataset gives the exact slope, why not always use it, even if each step is slower? Wouldn’t a perfect path be worth it?
Show the answer
Two reasons. First, cost: with 50,000 photos, one exact step costs as much as about 1,500 mini-batch steps of 32, and those 1,500 noisy steps get you much further downhill than one perfect step. Second, the noise is useful. A slightly jittery path can bounce out of small dips and flat regions that a perfectly smooth path would settle into and get stuck. Many practitioners believe this noise also helps models generalise better to new data, though exactly why is still researched.
Where gradient descent goes wrong
The cold coffee problem is a perfect bowl: one bottom, and every path leads there. Neural networks are not so kind. Here is what goes wrong in practice, in rough order of how often it bites.
The learning rate is wrong. By far the most common problem. Too high and the loss jumps around or becomes nan. Too low and training crawls, and you conclude the model “can’t learn” when it simply has not had time. When anything looks off, try the learning rate 10 times smaller and 3 times larger before changing anything else.
Features are on different scales. As you saw, raw inputs create ravines. Standardise them.
The landscape is bumpy. A neural network’s loss surface is a mountain range of ridges, plateaus and valleys inside valleys. Researchers spent years worrying about local minima (dips that are not the lowest point). It turns out that with millions of parameters, most dips are roughly as good as each other. The bigger practical problems are plateaus (flat regions where the slope is nearly zero and progress stalls) and saddle points (downhill in some directions, uphill in others). The upgrades to gradient descent in two lessons’ time, momentum and Adam, exist largely to push through those.
It finds the best answer for the data you gave it, not for the world. Arjun’s line was trained on days between 25 and 37 degrees. In July, on a 23-degree monsoon day, it predicted 46 cups and the store sold 18, because rainy days have nothing to do with the hot days in the data. Gradient descent did its job perfectly. The data was the problem. You will see this again, and properly, in lesson 5 and in Module 29’s forecasting lesson.
Try it yourself
Run the code above with these learning rates, keeping 100 steps: 0.01, 0.1, 0.5, 1.0, 1.1. Before you run each, write down your prediction: converge, crawl or explode?
Show the answer
0.01: crawls. The loss is still about 93 after 100 steps.0.1and0.5: both reach 0.14, the best a straight line can do on this data.1.0: stuck. The loss stays at 5,093.8 forever, bouncing between two points, like the 0.25 row in the one-parameter table.1.1: explodes, reaching around .
Notice the narrow gap between “works well” (0.5) and “broken” (1.0). On real models you find the right learning rate by trying a few values a factor of 3 to 10 apart.
Common mistakes
- Getting the sign wrong.
w += lr * dwclimbs instead of descending. If the loss rises steadily from the very first step, check the sign. - Treating the learning rate as a one-time choice. It is the most important setting in training. Revisit it whenever anything changes: new data, new model size, new batch size.
- Skipping input scaling. Unscaled features make one learning rate wrong for every parameter at once.
- Judging progress by a single step. With mini-batches, the loss jumps around. Look at the trend over many steps.
- Thinking zero loss is the goal. Zero loss on the training data usually means the model has memorised it, which is a problem you will meet in lesson 5.
Recap
- A loss function, such as MSE, turns “how wrong is the model?” into one number.
- The gradient is the slope of the loss for every parameter; stepping against it lowers the loss.
- Gradient descent repeats: predict, measure, compute the gradient, update .
- Too small a learning rate crawls; too big overshoots and diverges. Standardising inputs makes one learning rate fit all parameters.
- Real training uses mini-batches: noisy but cheap estimates of the slope, hundreds of steps for the cost of one exact step.
That evening Arjun explained the cold coffee rule to Meera. “The computer starts with a random line and checks how wrong it is against the last five days. Then it works out which way to tilt and slide the line to be a bit less wrong, and moves it a little. It does that about forty times and the line stops improving. Thirty degrees tomorrow, prep 67 cups.”
The store sold 66. Meera was impressed for about three seconds. “And the photo sorter? That’s going to be millions of these little numbers, isn’t it?”
“Which is why nobody writes the slope formulas by hand,” Priya said. “Arjun, install PyTorch. Tomorrow the computer does the calculus too.”
Go deeper
- PyTorch tutorials: the “Learn the Basics” series, which you start next lesson.
- NumPy documentation for the array maths used here.