Problem
The previous page differentiated functions you could write in one line. A network's loss is nothing like that. It is loss(softmax(linear(relu(linear(x))))), dozens or hundreds of layers deep, with every weight buried somewhere in the middle. Writing out ∂loss/∂w for each weight as one formula is hopeless, and estimating it with finite differences would take a forward pass per weight.
We need a rule that builds the derivative of a composition out of the derivatives of its parts, and a way to apply it that costs about as much as computing the loss once.
Intuition
Rates of change multiply. If u changes 3 times as fast as x, and f changes 8 times as fast as u, then f changes 3 × 8 = 24 times as fast as x. A chain of functions is a chain of gears, and the overall ratio is the product of the ratios.
The order you multiply in doesn't change the answer, but it changes the work. Starting from the loss and walking back toward the inputs, you carry a single vector, "how much does the loss care about this value", and every layer updates it with one cheap product. That backward walk is reverse mode, and backpropagation is reverse mode applied to a network.
Forward (green) computes values; backward (amber) starts from ∂f/∂f = 1 and multiplies by each operation's local derivative on the way back. Each local derivative only needs values the forward pass already computed.
Mechanics
The scalar chain rule
f(x) = g(h(x)) ⇒ f′(x) = g′(h(x)) · h′(x), or df/dx = df/du · du/dx
For f(x) = (3x + 1)², let u = 3x + 1. Then du/dx = 3 and df/du = 2u, so df/dx = 2(3x + 1) · 3. At x = 1, u = 4 and df/dx = 8 · 3 = 24. Expanding (3x + 1)² = 9x² + 6x + 1 and differentiating directly gives 18x + 6 = 24 too; the chain rule just never needs the expansion.
When a value feeds into several places, its derivative is the sum over
the paths: if f depends on x through both u and v, then
df/dx = ∂f/∂u · du/dx + ∂f/∂v · dv/dx. This is why .grad accumulates:
autograd adds up the contribution of each use.
The vector form: multiplying Jacobians
For vector-valued steps the derivatives are Jacobians and the chain rule becomes matrix multiplication. If x → u = h(x) → y = g(u), then
Jy←x = Jy←u · Ju←x [m, n] = [m, k] · [k, n]
A network with L layers has L Jacobians to multiply. When the final output is a scalar loss, its Jacobian is a row vector, the gradient, and
∇xL = ∇yL · JL · JL−1 ⋯ J1
Forward mode versus reverse mode
Matrix products are associative, so that chain can be evaluated from either end.
- Forward mode goes right to left in the formula, in the same order as the computation: push a tangent vector v through as J₁v, then J₂(J₁v), and so on. Each pass is a Jacobian-vector product (JVP) and gives the derivative of every output along one input direction. A full gradient needs one pass per input.
- Reverse mode starts from the output and goes backwards: ∇yL · JL, then that times JL−1, and so on. Each pass is a vector-Jacobian product (VJP) and gives the derivative of one output with respect to every input. A full gradient needs one pass per output.
Training has millions of inputs (the parameters) and one output (the loss), so reverse
mode wins by a factor of the parameter count. The price is memory: the backward pass
needs the forward pass's intermediate values, so they are all kept until
backward runs.
Forward mode pays once per input, reverse mode once per output. With one loss and a billion parameters the gap is a billion to one, which is why every training framework is built on reverse mode.
Local gradients per operation
Reverse mode never forms a Jacobian. Each operation only needs a rule that turns the gradient of its output into gradients of its inputs. For the operations in a two-layer network, with ḡ meaning ∂L/∂(that value):
| Operation | Forward | Backward (given ȳ) |
|---|---|---|
| Linear | y = Wx + b | W̄ = ȳ xᵀ, b̄ = ȳ, x̄ = Wᵀ ȳ |
| ReLU | y = max(0, z) | z̄ = ȳ where z > 0, else 0 |
| Dot product | y = w · h + b | w̄ = ȳ h, h̄ = ȳ w, b̄ = ȳ |
| Squared error | L = ½(y − t)² | ȳ = y − t |
Notice the transpose in x̄ = Wᵀȳ. Multiplying by a Jacobian's transpose is exactly a VJP, and for a linear layer the Jacobian is W itself.
How PyTorch packages a local rule
Every differentiable operation in PyTorch pairs a forward with a backward, the VJP rule.
You can write your own by subclassing
torch.autograd.Function:
forward computes the output and stashes what backward needs with
ctx.save_for_backward;
backward
receives the output's gradient and returns one gradient per input. The source notes that
vjp = backward: they are the same thing. An optional
jvp supplies
the forward-mode rule. Calling loss.backward() runs
torch.autograd.backward,
which walks the recorded graph from the loss back to the leaves, calling each node's VJP.
| Term | Shape/Params | Meaning | Source |
|---|---|---|---|
| Chain rule | df/dx = df/du · du/dx | The derivative of a composition is the product of local derivatives. | — |
| Upstream gradient ȳ | same shape as y | ∂L/∂y, arriving at an operation during the backward pass. | — |
| VJP | v: [outputs] → [inputs] |
vᵀJ without building J; one reverse-mode step. | autograd.functional.vjp |
| JVP | v: [inputs] → [outputs] |
Jv without building J; one forward-mode step. | autograd.functional.jvp, forward_ad.make_dual |
Function |
static forward(ctx, …), backward(ctx, *grads) |
A differentiable operation with a hand-written VJP; call it through .apply. |
autograd.Function |
ctx.save_for_backward |
tensors | Keep forward values the VJP will need; read back as ctx.saved_tensors. |
FunctionCtx.save_for_backward |
torch.autograd.backward |
tensors, grad_tensors |
Run reverse mode from the given outputs and accumulate into leaves' .grad. |
autograd.backward |
Worked example
A two-layer network, by hand. Input x = [2, 1], first layer W₁ = [[1, -1], [-1, 1]] with bias b₁ = [1, 0], ReLU, then an output y = w₂ · h + b₂ with w₂ = [2, 3], b₂ = 0, and loss L = ½(y − t)² with target t = 3.
| Forward | Value | Backward | Value |
|---|---|---|---|
| z₁ = W₁x + b₁ | [2, -1] | ȳ = y − t | 1 |
| h = ReLU(z₁) | [2, 0] | w̄₂ = ȳ h, b̄₂ = ȳ | [2, 0], 1 |
| y = w₂ · h + b₂ | 4 | h̄ = ȳ w₂ | [2, 3] |
| L = ½(y − t)² | 0.5 | z̄₁ = h̄ where z₁ > 0 | [2, 0] |
| W̄₁ = z̄₁ xᵀ, b̄₁ = z̄₁ | [[4, 2], [0, 0]], [2, 0] | ||
| x̄ = W₁ᵀ z̄₁ | [2, -2] |
Read the backward column top to bottom. The output is 1 too high, so ȳ = 1. The output weight w̄₂ = ȳ h says to shrink the weight on the hidden unit that fired (2) and leave the one that didn't alone. The hidden unit with z₁ = -1 was switched off by ReLU, so the gradient through it is 0, and so is the whole second row of W̄₁: a dead ReLU passes no gradient.
Check it. Nudge W₁[0][0] from 1 to 1.001. Then z₁[0] becomes 2.002, y becomes 4.004 and L becomes ½(1.004)² ≈ 0.504, a change of about 0.004, which is 0.001 × W̄₁[0][0] = 0.001 × 4. The tests behind this page check every gradient in the table this way, by finite differences.
Implementation
The same network in PyTorch; autograd reproduces every number in the table:
import torch
x = torch.tensor([2.0, 1.0], requires_grad=True)
W1 = torch.tensor([[1.0, -1.0], [-1.0, 1.0]], requires_grad=True)
b1 = torch.tensor([1.0, 0.0], requires_grad=True)
w2 = torch.tensor([2.0, 3.0], requires_grad=True)
b2 = torch.tensor(0.0, requires_grad=True)
h = torch.relu(W1 @ x + b1) # [2, 0]
y = w2 @ h + b2 # 4
loss = 0.5 * (y - 3.0) ** 2 # 0.5
loss.backward()
W1.grad # tensor([[4., 2.], [0., 0.]])
w2.grad # tensor([2., 0.])
x.grad # tensor([ 2., -2.])
A custom operation with a hand-written VJP, the way PyTorch's own operations are built:
class Square(torch.autograd.Function):
@staticmethod
def forward(ctx, u):
ctx.save_for_backward(u)
return u * u
@staticmethod
def backward(ctx, grad_output): # grad_output = ∂L/∂(u²)
(u,) = ctx.saved_tensors
return grad_output * 2 * u # ∂L/∂u, by the chain rule
x = torch.tensor(1.0, requires_grad=True)
Square.apply(3 * x + 1).backward()
x.grad # tensor(24.)
torch.autograd.gradcheck(Square.apply, torch.randn(3, dtype=torch.double, requires_grad=True))
Tradeoffs
- Reverse mode trades memory for speed. One backward pass costs about two forward passes, but every intermediate the VJPs need stays in memory until it runs. Activation memory often limits batch size more than the weights do. Gradient checkpointing recomputes some activations during backward to save memory.
- Forward mode still has uses. With few inputs and many outputs, such as the sensitivity of a whole output to one hyperparameter, or Jacobian-vector products inside second-order methods, forward mode is cheaper and needs no stored graph.
- Long products shrink or blow up. A gradient that passes through 100 layers is a product of 100 Jacobians. If their typical scale is below 1 it vanishes; above 1 it explodes. Initialization, normalization and residual connections exist largely to keep that product near 1; the backpropagation page covers it.
-
Hand-written backward functions are a bug source. A wrong VJP trains
silently worse instead of crashing. Always check a custom
backwardwithgradcheck, which compares it to finite differences in double precision.
Check yourself
What is d/dx of e2x at x = 0?
Let u = 2x. d(eᵘ)/du = eᵘ and du/dx = 2, so the derivative is 2e2x, which is 2 at x = 0.
A function maps 10 inputs to 10,000 outputs. Forward or reverse mode for the full Jacobian?
Forward mode: it needs one pass per input (10), against one per output (10,000) for reverse mode.
In the worked example, why is the second row of W̄₁ all zeros?
The second hidden unit had z₁ = −1, so ReLU output 0 and its local derivative is 0. Nothing upstream of it receives gradient through that unit, so its weights don't move this step.
For y = Wx with W of shape [3, 5], what shape is x̄, and how is it computed?
x̄ has x's shape, [5]. It is Wᵀȳ: a [5, 3] matrix times the [3] upstream gradient.
Connections
Read first
Read next
- Backpropagation core
References
- Goodfellow, Bengio and Courville. Deep Learning, section 6.5 (back-propagation and other differentiation algorithms). MIT Press, 2016.
- Rumelhart, Hinton and Williams. Learning representations by back-propagating errors. Nature 323, 1986.
- Baydin et al. Automatic Differentiation in Machine Learning: a Survey. JMLR 2018. arXiv:1502.05767
- Griewank and Walther. Evaluating Derivatives: Principles and Techniques of Algorithmic Differentiation, 2nd ed. SIAM, 2008.
- PyTorch v2.15.0:
torch.autograd.Functionandtorch.autograd.backward.