ml-models / Mathematical foundations / Derivatives and gradients intro

Derivatives and gradients

Training a model means nudging its numbers so a loss goes down, and the gradient is what says which way to nudge. This page builds it from the slope of a curve, through partial derivatives, to the gradient of a function of many inputs and the Jacobian of a function with many outputs. It then runs gradient descent by hand and shows how PyTorch's requires_grad and .grad compute the same numbers, at v2.15.0.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The derivative
  5. Partial derivatives
  6. The gradient
  7. Gradient descent
  8. The Jacobian
  9. How PyTorch exposes gradients
  10. Worked example
  11. Implementation
  12. Tradeoffs
  13. Check yourself
  14. Connections
  15. Read first
  16. Read next
  17. References

Problem

A model's loss is a function of millions of parameters. We want the parameters that make it small, but we can't try every combination, and we can't solve for the minimum with algebra. What we can do is stand at the current parameters and ask: if I change each one a little, how does the loss change? Answer that for every parameter at once and you know which direction lowers the loss fastest. Take a small step that way and repeat.

That answer is the gradient, and "step against it and repeat" is gradient descent. Every optimizer on this site, from plain SGD to AdamW, is a variation on that loop.

Intuition

For a function of one number, the derivative is the slope of its graph: how much the output rises per unit the input moves, measured at a single point. Zoom far enough into any smooth curve and it looks like a straight line, and the derivative is that line's slope.

For a function of many numbers, picture a landscape whose height is the loss. At any spot you can measure the slope in the east–west direction and the north–south direction separately; those are partial derivatives. Put them together as a vector and it points uphill, the steepest way up. Its length says how steep. To go down, walk the opposite way.

The derivative as the slope of a tangent line The parabola f(x) = x squared, with the point (3, 9) marked. A solid tangent line touches it there with slope 6. A dashed secant line through (3, 9) and (4, 16) has slope 7; as the second point slides toward the first, the secant's slope approaches the tangent's. 0 1 2 3 4 (3, 9) (4, 16) f(x) = x² tangent at 3: slope 6 secant, h = 1: slope 7 h = 0.1: slope 6.1000 h = 0.01: slope 6.0100 h = 0.001: slope 6.0010 the secant → the tangent as h → 0

The derivative is the limit of "rise over run" as the run shrinks to zero. For x² the secant's slope is exactly 6 + h, so the error shrinks in step with h.

Mechanics

The derivative

f′(x) = limh→0 (f(x + h) − f(x)) / h

A handful of rules cover almost everything: the derivative of xⁿ is n·xⁿ⁻¹, of eˣ is eˣ, of ln x is 1/x, constants factor out, and the derivative of a sum is the sum of the derivatives. So for f(x) = x², f′(x) = 2x and f′(3) = 6, matching the tangent above. Functions built from other functions need the chain rule, the next page.

The sign says which way is downhill: if f′(x) > 0, decreasing x decreases f. The size says how sensitive f is. Where f′(x) = 0 the curve is flat: a minimum, a maximum or a saddle.

Partial derivatives

For a function of several inputs, a partial derivative ∂f/∂xᵢ varies one input and holds the others fixed. Take the bowl used for the rest of this page,

f(x, y) = x² + 3y²,   ∂f/∂x = 2x,   ∂f/∂y = 6y

At (3, 2): ∂f/∂x = 6 and ∂f/∂y = 12. The bowl is three times steeper in y, so the same move in y changes the loss more.

The gradient

∇f = (∂f/∂x₁, …, ∂f/∂xₙ),   ∇f(3, 2) = (6, 12)

The gradient has one entry per input, so it has the same shape as the parameters. Moving a small step d changes f by about ∇f · d, a dot product. Among all steps of the same length, that is largest when d points along ∇f (cosine 1) and most negative when d points against it. So the gradient is the direction of steepest ascent, −∇f the direction of steepest descent, and any step at right angles to ∇f leaves f unchanged to first order: the gradient is perpendicular to the contour lines.

Gradient descent

θ ← θ − η ∇f(θ)

η is the learning rate. On the bowl with η = 0.1, x moves by −0.1·2x, so it keeps 0.8 of its value each step, and y keeps 0.4. The steep direction converges faster, but it also sets the speed limit: once η exceeds 0.333, y's factor 1 − 6η falls below −1 and every step overshoots further than the last.

Gradient descent on the bowl x² + 3y² Elliptical contour lines of f(x, y) = x² + 3y² at levels 1, 4, 9 and 21, wider in x than in y. Descent starts at (3, 2) on the level-21 contour. An amber arrow shows the gradient (6, 12) pointing uphill, perpendicular to the contour. The blue path takes six steps with learning rate 0.1, cutting quickly toward the x-axis and then crawling along it toward the minimum at the origin. 1 4 9 21 ∇f = (6, 12) start (3, 2) minimum η = 0.1: each step is −η∇f; f falls from 21 to 0.62 in 6 steps

The path drops quickly in the steep y direction, then spends most of its steps crawling along the shallow x direction. Raise η to speed up x and y starts to overshoot. That unequal curvature is what momentum and Adam, on the optimizers page, are built to fix.

The Jacobian

A function with m outputs and n inputs has one gradient per output. Stacked as rows they form the m × n Jacobian, Jij = ∂fᵢ/∂xⱼ, the best linear approximation of the function near a point: f(x + d) ≈ f(x) + J d. For f(x, y) = (xy, x + y²),

J = [[y, x], [1, 2y]],   J(2, 3) = [[3, 2], [1, 6]]

A loss is a scalar, so its Jacobian is a single row, the gradient. Inside the network the Jacobians of layers are huge, a [4096, 4096] block for one linear layer on one token, and autograd never builds them. It only multiplies a vector by them, which the chain rule page explains.

How PyTorch exposes gradients

Mark a tensor with requires_grad=True and PyTorch records every operation on it. Calling .backward() on a scalar result fills the .grad of each leaf tensor with ∂result/∂leaf (Tensor.backward, which calls torch.autograd.backward). Three details catch everyone:

  • .grad accumulates. A second backward adds to it, which is why training loops call zero_grad (Tensor.grad).
  • Only leaf tensors, the ones you created, get .grad; intermediates need retain_grad() (is_leaf).
  • backward on a non-scalar needs a gradient argument, the vector in a vector-Jacobian product. torch.autograd.grad returns gradients instead of storing them (autograd.grad), and torch.no_grad() turns recording off for inference (no_grad).
TermShape/ParamsMeaningSource
Derivative f′(x) scalar in, scalar out Slope of f at x: output change per unit of input change. —
Partial derivative ∂f/∂xᵢ one input varied, the rest fixed Slope along one coordinate axis. —
Gradient ∇f same shape as the inputs All partials together; points in the direction of steepest ascent. Tensor.grad
Jacobian J [outputs, inputs] One gradient row per output; the local linear map. autograd.functional.jacobian
requires_grad bool on a tensor Record operations on this tensor so gradients can flow to it. requires_grad, requires_grad_
backward gradient for non-scalars, retain_graph, create_graph Accumulate ∂output/∂leaf into each leaf's .grad. Tensor.backward
autograd.grad outputs, inputs Return the gradients instead of accumulating them. torch.autograd.grad
Learning rate η positive scalar Step size in θ ← θ − η∇f. —

Worked example

Gradient descent on f(x, y) = x² + 3y², from (3, 2), with η = 0.1. Each step computes the gradient (2x, 6y), multiplies it by η and subtracts.

Step(x, y)f(x, y)∇f = (2x, 6y)Next = (x, y) − η∇f
0 (3, 2) 21 (6, 12) (2.4, 0.8)
1 (2.4, 0.8) 7.68 (4.8, 4.8) (1.92, 0.32)
2 (1.92, 0.32) 3.994 (3.84, 1.92) (1.536, 0.128)
3 (1.536, 0.128) 2.408 (3.072, 0.768) (1.2288, 0.0512)
4 (1.2288, 0.0512) 1.518 (2.4576, 0.3072) (0.98304, 0.02048)
5 (0.98304, 0.02048) 0.9676 (1.96608, 0.12288) (0.786432, 0.008192)
6 (0.786432, 0.008192) 0.6187 (1.57286, 0.049152) …

Step 1 by hand: ∇f(3, 2) = (2·3, 6·2) = (6, 12); η∇f = (0.6, 1.2); (3, 2) − (0.6, 1.2) = (2.4, 0.8), and f drops from 21 to 2.4² + 3·0.8² = 7.68.

Too large a step. With η = 0.4, above the 0.333 limit, y flips sign and grows by 1.4× each step: f goes 21 → 23.88 → 46.11 → 90.36 → 177.1. This is what a loss that shoots to nan after a learning-rate change looks like in miniature.

Checking a Jacobian numerically. For f(x, y) = (xy, x + y²) at (2, 3), nudge each input by ±10⁻⁶ and divide the change in outputs by 2·10⁻⁶. The rows come out as (3, 2) and (1, 6), matching the exact [[3, 2], [1, 6]]. This central-difference check is how torch.autograd.gradcheck tests hand-written backward functions.

Implementation

import torch

theta = torch.tensor([3.0, 2.0], requires_grad=True)
scale = torch.tensor([1.0, 3.0])
lr = 0.1

for step in range(6):
    loss = (scale * theta ** 2).sum()   # x² + 3y²
    loss.backward()                     # theta.grad = (2x, 6y)
    with torch.no_grad():               # the update itself isn't recorded
        theta -= lr * theta.grad
    theta.grad = None                   # .grad accumulates; clear it
    print(step, loss.item(), theta.tolist())
# 0 21.0 [2.4, 0.8]   1 7.68 [1.92, 0.32]   ...

from torch.autograd.functional import jacobian
f = lambda p: torch.stack([p[0] * p[1], p[0] + p[1] ** 2])
jacobian(f, torch.tensor([2.0, 3.0]))   # tensor([[3., 2.], [1., 6.]])

Without autograd, a numerical gradient by central differences:

def numerical_gradient(f, point, h=1e-6):
    grad = []
    for i in range(len(point)):
        up, down = list(point), list(point)
        up[i] += h
        down[i] -= h
        grad.append((f(up) - f(down)) / (2 * h))
    return grad

numerical_gradient(lambda p: p[0] ** 2 + 3 * p[1] ** 2, [3, 2])   # ≈ [6.0, 12.0]

Tradeoffs

  • Numerical versus automatic differentiation. Finite differences need two function evaluations per parameter, so a million-parameter model would take two million forward passes per gradient, and they lose precision as h shrinks. Autograd gets the exact gradient for about the cost of one extra forward pass. Finite differences remain the standard way to test autograd.
  • The learning rate. Too small and training crawls; too large and it diverges. The safe size depends on the steepest curvature, which differs across parameters and changes during training. That is why schedules and adaptive optimizers exist.
  • Local information only. The gradient describes an infinitesimal neighbourhood. On non-convex losses descent finds a nearby low point, not necessarily the lowest. In large networks most such points turn out to be good enough, and saddle points, where the gradient is also zero, are the more common nuisance.
  • Non-differentiable points. ReLU and absolute value have kinks. Frameworks pick a subgradient there (PyTorch uses 0 for ReLU at 0), which works in practice because exact ties are rare.
  • Memory. Recording the graph keeps intermediate values alive until backward. Wrap inference in torch.no_grad() or torch.inference_mode() to skip it.

Check yourself

What is the gradient of f(x, y) = x² + 3y² at (1, −1), and which way does descent move?

∇f = (2·1, 6·(−1)) = (2, -6). Descent moves along −∇f = (−2, 6): x decreases and y increases, both toward 0.

Why does a step perpendicular to the gradient leave the function (almost) unchanged?

The first-order change is ∇f · d, and the dot product of perpendicular vectors is 0. That is also why the gradient is perpendicular to contour lines.

You call loss.backward() twice without zeroing. What is in .grad?

The sum of both gradients, twice the true gradient if nothing changed in between. (The second call also needs retain_graph=True, since the graph is freed after the first.)

What shape is the Jacobian of nn.Linear(3, 2)'s output with respect to its input, for one example?

[2, 3]: one row per output and one column per input. It equals the weight matrix W, since y = Wx + b.

Connections

References

  • Goodfellow, Bengio and Courville. Deep Learning, chapter 4.3 (gradient-based optimization). MIT Press, 2016.
  • Deisenroth, Faisal and Ong. Mathematics for Machine Learning, chapter 5 (vector calculus). Cambridge University Press, 2020.
  • Baydin et al. Automatic Differentiation in Machine Learning: a Survey. JMLR 2018. arXiv:1502.05767
  • PyTorch v2.15.0: torch.autograd.backward and Tensor.backward.