Index Ai Academy
Phase 2 · Math, when you need it · Calculus

Gradients

The vector of all partial derivatives: which way is uphill.

About 25 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. Lost on a foggy hill
  2. A bowl, worked by hand
  3. It points uphill
  4. The gradient of a loss
  5. One slope per weight
  6. Worked example: one step downhill
  7. 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 ff with two inputs, xx and yy. Its gradient is written ∇f\nabla f. The upside-down triangle is called "nabla". Most people read ∇f\nabla f as "grad f".

∇f(x,y)=[∂f∂x, ∂f∂y]\nabla f(x, y) = \left[\frac{\partial f}{\partial x},\ \frac{\partial f}{\partial y}\right]

In plain words: the gradient is a list of slopes. The first is how fast ff changes when only xx moves. The second is how fast it changes when only yy moves. A function with ten inputs has a gradient of ten slopes.

A bowl, worked by hand

Here's a bowl-shaped surface: f(x,y)=x22+4y2f(x, y) = \frac{x^2}{2} + 4y^2. Its lowest point is at (0,0)(0, 0), where ff is 0. The 4 makes it much steeper in the yy direction.

Find each slope with the derivative rules:

  • Hold yy still. Then 4y24y^2 is a fixed number, with slope 0. The slope of x22\frac{x^2}{2} is xx. So ∂f∂x=x\frac{\partial f}{\partial x} = x.
  • Hold xx still. Now x22\frac{x^2}{2} has slope 0, and 4y24y^2 has slope 8y8y. So ∂f∂y=8y\frac{\partial f}{\partial y} = 8y.

Put them in a list: ∇f=[x, 8y]\nabla f = [x,\ 8y]. At the point (2,1)(2, 1) the gradient is [2,8][2, 8].

In plain words: at (2,1)(2, 1), a tiny step in the xx direction raises ff by about 2 times the step's size. A tiny step in the yy direction raises it by about 8 times the step's size. Here, the yy 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))
It runs right here in your browser, with nothing to install.

Both give 2 and 8. Nudging needs no rules, so it's a handy way to check your work.

Quick check
What does this print?
x, y = 1.0, 2.0
grad = (2 * x, 2 * y)
print(grad)
  1. (2.0, 4.0)
  2. (3.0, 3.0)
  3. (1.0, 2.0)
  4. (4.0, 2.0)
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.

Quick check
For the bowl f(x, y) = x²/2 + 4y², the gradient is ∇f = [x, 8y]. What is the gradient at the point (4, −1)?
  1. [2, −8]
  2. [4, −8]
  3. [8, −8]
  4. [4, 8]
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.

  1. Direction: the gradient points the way of steepest ascent. Of all the directions you could take a small step, this one raises ff the most. So the opposite arrow, −∇f-\nabla f, is the steepest way down.
  2. 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 ∥∇f∥\|\nabla f\|. You met the recipe in the vector lessons:

∥∇f∥=(∂f∂x)2+(∂f∂y)2\|\nabla f\| = \sqrt{\left(\frac{\partial f}{\partial x}\right)^2 + \left(\frac{\partial f}{\partial y}\right)^2}

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 (2,1)(2, 1): the gradient is [2,8][2, 8]. 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 (0,0)(0, 0), 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 (2,1)(2, 1) the bottom of the bowl lies in the direction [−2,−1][-2, -1]. But −∇f-\nabla f is [−2,−8][-2, -8]. It dives mostly in yy, where the bowl is steepest.

Go deeper Why the gradient is the steepest direction

Pick a direction as an arrow u\mathbf{u} of length 1. Take a tiny step of size ss that way. The change in ff, written Δf\Delta f (delta f), is about:

Δf≈s (∇f⋅u)\Delta f \approx s \, (\nabla f \cdot \mathbf{u})

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 (2,1)(2, 1), the dot product is 2 for east [1,0][1, 0] and 8 for north [0,1][0, 1]. For the gradient's own direction it is 8.25, and no other direction of length 1 beats it.

Try it now

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.

Quick check
At some point, ∇f = [0, 0]. What does that mean?
  1. The function has no gradient
  2. The surface is flat there: a minimum, maximum or saddle point
  3. f equals zero there
  4. You're at the steepest point
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.

  • XX is the table of features: one row per campaign, one column per feature.
  • w\mathbf{w} is the list of weights, one per feature.
  • y\mathbf{y} holds the real revenue of each campaign.
  • nn is the number of campaigns.

The predictions are y^=Xw\hat{y} = X\mathbf{w}, 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):

L(w)=1n∑i=1n(xi⋅w−yi)2=1n∥Xw−y∥2L(\mathbf{w}) = \frac{1}{n} \sum_{i=1}^{n} (\mathbf{x}_i \cdot \mathbf{w} - y_i)^2 = \frac{1}{n}\|X\mathbf{w} - \mathbf{y}\|^2

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 nn to get the average.

Here xi\mathbf{x}_i is row ii of the table: one campaign. The ∑\sum sign (sigma) means "add up for every ii from 1 to nn", 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.

Quick check
Why does the loss square each miss?
  1. So misses above and below can't cancel out, and big misses count for more
  2. To make the numbers smaller
  3. To make the code run faster
  4. Because prices are always positive
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 w\mathbf{w}. Worked out with the chain rule (the panel below shows how), it is:

∇wL=2nX⊤(Xw−y)\nabla_{\mathbf{w}} L = \frac{2}{n} X^\top (X\mathbf{w} - \mathbf{y})

The small w\mathbf{w} under the nabla says the slopes are taken with respect to the weights. X⊤X^\top is "X transposed": the table flipped so its columns become rows.

In plain words: read it from the right. Xw−yX\mathbf{w} - \mathbf{y} is the list of errors, one per campaign. Multiplying by X⊤X^\top gives, for each weight, "error times that weight's feature", added up over all campaigns. The 2n\frac{2}{n} scales the result: the 2 comes from the square, and dividing by nn 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 n=2n = 2, so 2n=1\frac{2}{n} = 1, 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 ii's error ei=xi⋅w−yie_i = \mathbf{x}_i \cdot \mathbf{w} - y_i. Write xijx_{ij} for that campaign's value of feature jj. If only weight wjw_j moves, the error changes at the rate xijx_{ij}.

By the chain rule from its own lesson, the square ei2e_i^2 then changes at the rate 2eixij2 e_i x_{ij}. Averaging over the campaigns gives one entry of the gradient:

∂L∂wj=2n∑i=1nxij ei\frac{\partial L}{\partial w_j} = \frac{2}{n} \sum_{i=1}^{n} x_{ij}\, e_i

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 X⊤X^\top times the error vector computes.

Quick check
The errors are [2, −1]. One weight's feature column is [3, 1]. There are n = 2 campaigns. What is that weight's entry in the gradient (2/n) Xᵀ(Xw − y)?
  1. 7
  2. 5
  3. 2.5
  4. 10
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

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))
In the lesson you can change this and run it.

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.

Quick check
Your hand-derived formula gives the gradient [−2, −2.5]. The numeric check gives [−4, −5]. What is the most likely bug?
  1. The numeric check always gives double the true value
  2. The weights are too big
  3. The formula is missing the 2 in 2/n
  4. The loss shouldn't be squared
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

Create a free account

Try a quiz question

Which way does −∇L point for a loss L?

  1. The direction of steepest decrease of the loss
  2. Toward the origin, where all weights are 0
  3. Along a contour line, where the loss stays level
  4. The direction of steepest increase of the loss

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson