Gradients
The vector of all partial derivatives: which way is uphill.
Create a free account Try part of it here
Why it matters
Training a model means adjusting its weights, the numbers it learns, until its errors shrink. The gradient tells training which way to adjust them. It's a list with one number per weight. For a model with a billion weights, that's a billion numbers. Together they point in the direction that makes the loss (the model's error score) rise fastest. Step the opposite way and the loss falls.
You'll meet the word all over AI: "gradient descent", "vanishing gradients", "gradient clipping", "gradient accumulation". All of them are about this one list of numbers.
What you'll be able to do
- Collect a function's partial derivatives into its gradient vector
- Explain why the gradient points uphill and the negative gradient downhill
- Derive and vectorize the gradient of mean squared error for a linear model
- Check an analytic gradient against a numerical one
What it covers
- Lost on a foggy hill
- A bowl, worked by hand
- It points uphill
- The gradient of a loss
- One slope per weight
- Worked example: one step downhill
- The same thing with NumPy
Lost on a foggy hill
Picture yourself on a hillside in thick fog. You can't see the top or the bottom. You can only feel the ground under your feet.
Two questions tell you a lot. How steeply does the ground rise or fall if you step east? And if you step north? Each answer is a slope in one direction, with the other held still. You met these in the last lesson: they're partial derivatives.
Write the two answers as a list, east first, then north. That list is the gradient. Like any vector, you can draw it as an arrow. This arrow points up the hill.
The gradient as a formula
Take a function with two inputs, and . Its gradient is written . The upside-down triangle is called "nabla". Most people read as "grad f".
In plain words: the gradient is a list of slopes. The first is how fast changes when only moves. The second is how fast it changes when only moves. A function with ten inputs has a gradient of ten slopes.
A bowl, worked by hand
Here's a bowl-shaped surface: . Its lowest point is at , where is 0. The 4 makes it much steeper in the direction.
Find each slope with the derivative rules:
- Hold still. Then is a fixed number, with slope 0. The slope of is . So .
- Hold still. Now has slope 0, and has slope . So .
Put them in a list: . At the point the gradient is .
In plain words: at , a tiny step in the direction raises by about 2 times the step's size. A tiny step in the direction raises it by about 8 times the step's size. Here, the direction is four times steeper.
The code finds the gradient two ways. First with the rule. Then by nudging each input a little each side, the central difference from the last two lessons.
def f(x, y): return x ** 2 / 2 + 4 * y ** 2 def grad_f(x, y): return [x, 8 * y] print("by the rule:", grad_f(2, 1)) h = 0.001 slope_x = (f(2 + h, 1) - f(2 - h, 1)) / (2 * h) slope_y = (f(2, 1 + h) - f(2, 1 - h)) / (2 * h) print("by nudging:", round(slope_x, 6), round(slope_y, 6))
Both give 2 and 8. Nudging needs no rules, so it's a handy way to check your work.
x, y = 1.0, 2.0 grad = (2 * x, 2 * y) print(grad)
Show the answer
(2.0, 4.0)
2 × 1.0 is 2.0 and 2 × 2.0 is 4.0, so grad is (2.0, 4.0). For the bowl x² + y², each input's slope is twice that input. The gradient lists them.
Show the answer
[4, −8]
Put x = 4 and y = −1 into [x, 8y]: you get [4, 8 × (−1)] = [4, −8]. The negative entry means f falls as y grows here: growing y from −1 brings it closer to 0, the bottom of the bowl.
It points uphill
Two facts make the gradient useful.
- Direction: the gradient points the way of steepest ascent. Of all the directions you could take a small step, this one raises the most. So the opposite arrow, , is the steepest way down.
- Length: the arrow's length says how steep that steepest slope is. Near the bottom of a bowl the ground flattens out, so the gradient shrinks toward zero.
The length of a vector is written with double bars, as in . You met the recipe in the vector lessons:
In plain words: square each slope, add them up, and take the square root. The answer is how steep the hill is in its steepest direction.
Worked at : the gradient is . Then 2² + 8² = 4 + 64 = 68. The square root of 68 is about 8.25. The code repeats this at points closer and closer to the bottom:
import math def grad_f(x, y): return [x, 8 * y] for point in [(2, 1), (1, 0.5), (0.2, 0.1)]: g = grad_f(*point) length = math.sqrt(g[0] ** 2 + g[1] ** 2) print(point, g, "length", round(length, 2))
The length falls from 8.25 to 4.12 to 0.82. The closer you get to , the flatter the ground.
Across the contours, not at the center
On a hiking map, contour lines join points at the same height. The gradient always crosses them at a right angle. Rain on a hillside does the same: it runs straight down across the contours, never along them.
So the downhill arrow often doesn't point at the bottom. At the bottom of the bowl lies in the direction . But is . It dives mostly in , where the bowl is steepest.
Go deeper Why the gradient is the steepest direction
Pick a direction as an arrow of length 1. Take a tiny step of size that way. The change in , written (delta f), is about:
Read it as: "the change in f is roughly the step size times the gradient dotted with the direction." A dot product is biggest when the two arrows point the same way. So the best direction is the gradient's own, and then the dot product equals the gradient's length.
At , the dot product is 2 for east and 8 for north . For the gradient's own direction it is 8.25, and no other direction of length 1 beats it.
The purple arrow is −∇f, the steepest way down from the current point. Click to move the start and press Step: each step follows the arrow. Notice it doesn't point at the center. It points straight across the contours.
Show the answer
The surface is flat there: a minimum, maximum or saddle point
All the partial derivatives are zero, so a tiny step in any direction goes neither up nor down. That happens at the bottom of a valley, the top of a hill, or a saddle point: a spot that's lowest one way and highest another, like the middle of a horse's saddle.
The gradient of a loss
Now swap the hillside for a model. Say you predict each campaign's revenue from a few numbers about it, called its features, such as spend and clicks. A linear model multiplies each feature by a weight and adds up the results. Training means finding good weights.
Four symbols carry the story. Bold letters are vectors, as in the vector lessons.
- is the table of features: one row per campaign, one column per feature.
- is the list of weights, one per feature.
- holds the real revenue of each campaign.
- is the number of campaigns.
The predictions are , read "y-hat equals X times w". Each prediction is the dot product of one row with the weights.
A standard loss is the mean squared error (MSE):
In plain words: for each campaign, take the prediction minus the real value. Square it, so a miss in either direction counts as bad. Add these up and divide by to get the average.
Here is row of the table: one campaign. The sign (sigma) means "add up for every from 1 to ", like a for loop. The right-hand version says the same thing with the squared length of the error vector (the double bars again).
Worked with small numbers: two campaigns, two features each. The table is [[1, 2], [2, 1]], the real values are [5, 4] and the weights are [1, 1]. The predictions are 1×1 + 2×1 = 3 and 2×1 + 1×1 = 3. The errors are 3 − 5 = −2 and 3 − 4 = −1. Their squares are 4 and 1, so the loss is (4 + 1) ÷ 2 = 2.5.
Show the answer
So misses above and below can't cancel out, and big misses count for more
Without squaring, a miss of +5 and a miss of −5 would add up to a perfect score.
One slope per weight
Each weight gets its own slope: how fast the loss changes when only that weight moves. Those slopes, in a list, are the gradient of the loss. It has one entry per weight, so it has the same shape as . Worked out with the chain rule (the panel below shows how), it is:
The small under the nabla says the slopes are taken with respect to the weights. is "X transposed": the table flipped so its columns become rows.
In plain words: read it from the right. is the list of errors, one per campaign. Multiplying by gives, for each weight, "error times that weight's feature", added up over all campaigns. The scales the result: the 2 comes from the square, and dividing by takes the average.
A weight whose feature lines up with big errors gets a big gradient.
Back to the small example. The errors are [-2, -1]. Weight 1's feature column is [1, 2], so 1×(−2) + 2×(−1) = −4. Weight 2's column is [2, 1], so 2×(−2) + 1×(−1) = −5. Here , so , and the gradient is [−4, −5].
Both entries are negative, so raising either weight lowers the loss. That fits: both predictions were too low.
Go deeper Where the formula comes from
Call campaign 's error . Write for that campaign's value of feature . If only weight moves, the error changes at the rate .
By the chain rule from its own lesson, the square then changes at the rate . Averaging over the campaigns gives one entry of the gradient:
Read it as: "weight j's slope is 2 over n, times the sum of feature j times the error, over all campaigns." Doing that for every weight at once is exactly what times the error vector computes.
Show the answer
5
Feature times error, added up: 3 × 2 + 1 × (−1) = 5. Then 2/n = 2/2 = 1, so the entry is 5.
Worked example: one step downhill
Two campaigns, two features, weights [1, 1]. Compute the loss and its gradient with plain Python loops. Then take one small step against the gradient and measure the loss again.
X = [[1, 2], [2, 1]] # two campaigns, two features each y = [5, 4] # what really happened w = [1, 1] # the current weights n = len(y) preds = [row[0] * w[0] + row[1] * w[1] for row in X] errors = [p - t for p, t in zip(preds, y)] loss = sum(e ** 2 for e in errors) / n grad = [2 / n * sum(errors[i] * X[i][j] for i in range(n)) for j in range(2)] print("predictions:", preds, " errors:", errors) print("loss:", loss, " gradient:", grad) new_w = [w[j] - 0.1 * grad[j] for j in range(2)] new_preds = [row[0] * new_w[0] + row[1] * new_w[1] for row in X] new_loss = sum((p - t) ** 2 for p, t in zip(new_preds, y)) / n print("new weights:", [round(v, 2) for v in new_w], " new loss:", round(new_loss, 3))
The loss of 2.5 and the gradient of [−4.0, −5.0] match the hand calculation. Stepping against the gradient means subtracting 0.1 times it. The weights rise to [1.4, 1.5] and the loss drops from 2.5 to 0.225. The 0.1 sets the step size, called the learning rate.
The same thing with NumPy
Real datasets have thousands of rows, and loops get slow. In NumPy, X @ w makes every prediction at once. Then X.T @ (X @ w - y) does every "error times feature" sum. One matrix multiplication computes the whole gradient.
The cell makes 50 made-up campaigns with 3 features each. Their revenue comes from the true weights [2, -1, 0.5], plus a little random noise. The model's weights start at zero.
import numpy as np rng = np.random.default_rng(0) X = rng.normal(size=(50, 3)) true_w = np.array([2.0, -1.0, 0.5]) y = X @ true_w + rng.normal(0, 0.1, 50) def loss(w): return np.mean((X @ w - y) ** 2) def gradient(w): return 2 / len(y) * X.T @ (X @ w - y) w = np.zeros(3) print("loss at w = 0:", round(loss(w), 3)) print("gradient:", gradient(w).round(3)) print("loss after a small step against it:", round(loss(w - 0.1 * gradient(w)), 3))
One step against the gradient already lowers the loss. Keep stepping and you've invented gradient descent, the topic of the next lesson.
Always check gradients numerically
Gradients worked out by hand are easy to get subtly wrong. A 2 goes missing, or a table that should be transposed isn't. The standard test is to compare with central differences on a few weights: nudge one weight up and down and see how the loss changes. If the two methods agree to several decimal places, the formula is right. Here's the check for the small example:
X = [[1, 2], [2, 1]] y = [5, 4] def loss(w): preds = [row[0] * w[0] + row[1] * w[1] for row in X] return sum((p - t) ** 2 for p, t in zip(preds, y)) / len(y) h = 1e-6 for j in range(2): up = [1.0, 1.0] down = [1.0, 1.0] up[j] += h down[j] -= h print("weight", j + 1, "numeric slope:", round((loss(up) - loss(down)) / (2 * h), 4))
The numeric slopes are −4.0 and −5.0, the same as the formula. Nudging costs two runs of the loss for every weight. That's fine for checking a small model, but far too slow for training a big one. In the exercise you'll build this check for any function.
Show the answer
The formula is missing the 2 in 2/n
Every entry is off by exactly a factor of 2, which points to a dropped 2. The central difference makes no such slip, so trust it and fix the formula.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Which way does −∇L point for a loss L?