Backpropagation
How does backpropagation compute the loss gradient for a weight two layers back?
The idea
Every gradient is a product of local derivatives along a path from the loss back to that number, summed over every path that reaches it. Backpropagation computes those products once, from the loss backwards, so all of them together cost one backward pass: a small constant multiple of the forward pass, not one pass per weight.
Builds on Reverse-Mode Automatic Differentiation.
The picture
The network in the figure reads two inputs, , passes them through three hidden units, and predicts . The target is . The loss sees only that one output, yet every weight, even one two layers back, has to learn which way to move. The figure works it out in three moves.
- Forward. Values flow from the inputs towards the loss. Each hidden unit adds up its weighted inputs and a bias to get , then squashes it with ; the readout mixes the three into , and the loss compares with the target. Every value is kept, because the way back needs it.
- Local derivatives. Each edge carries one number: how much its end moves when its start moves a little. On a weight's edge it is the weight. Across a tanh it is , near 1 while the unit sits near zero and near 0 once it saturates towards . On the last edge, from to the loss, it is the error .
- Backward. Gradients flow the other way, starting from 1 at the loss. Crossing an edge multiplies the gradient by that edge's number. Where a node feeds several others, as each input feeds all three hidden units, the gradients coming back along the separate paths add.
So a weight's gradient is a product: the error, times each factor on the path back to the weight, times the input the weight multiplied on the way forward. A hidden unit learns only when two things hold at once: something downstream depends on it, and its own tanh still responds.
Nothing is computed twice. Each node's gradient is found once and then reused by everything behind it, which is why one backward pass delivers the gradient of every weight at once.
The mathematics
For a simple feed-forward network, write activations as
for layers . For one training example, use column-vector shapes:
Let the scalar loss be
Backpropagation tracks sensitivities flowing backward from the loss. Define
For the final layer,
If the output layer is the identity, then and . For squared error , this gives .
For earlier layers, the chain rule gives
Read the two factors separately. is the signal layer receives from downstream: how the loss changes with its activations. is the local derivative: how much those activations change with their pre-activations, computed from layer alone. The product is taken unit by unit.
Once is known, the parameter gradients are local:
This is the core efficiency: the same downstream sensitivity is reused to compute gradients for both parameters and earlier activations. For a scalar loss with many parameters, one backward pass computes the full gradient needed by gradient descent.
For a batch, the same local rules apply per example, then weight and bias gradients are summed or averaged across the batch dimension.
Worked pass: the notebook network
This is the network in the figure and in the Code below. It has inputs, tanh hidden units and one identity output, so , and the loss is . Units and inputs are counted from 1: is the weight from input into hidden unit , called H. The givens, with their shapes:
The forward pass keeps every value the backward pass will reuse:
The output is the identity, so . It is negative: raising would lower the loss. Each hidden unit then gets its two factors and their product:
Hidden unit 1, step by step. H1 reaches the loss by one route, , through the readout weight . So the signal it receives is : raising would lift toward the target and lower the loss. Its local derivative ignores everything downstream: , because tanh is still fairly steep at . The delta is the product, .
Now the weights. H1's pre-activation is , so : each weight's gradient is the delta times the input that weight multiplied. Row 1 of the gradient is
The signs carry meaning. The first entry is negative because is positive, so gradient descent increases . The second is positive because flips it, so descent decreases . For this input both moves raise , which lifts toward the target through the positive readout weight.
H2 and H3. H2's readout weight is negative, so its received signal is positive, : raising would push further below the target. Its local derivative equals H1's because and squaring removes the sign. H3 follows the same two steps with its own numbers: received , local derivative at , delta . Its gradient row is left for you to finish in the practice below.
The same deltas give , the readout gradient and the signal passed to the input, . You can check any entry by nudging one parameter: the central difference with matches it.
A weight used in two places
In this network each weight is used once. When a value is used by several later operations, as a weight tied across positions or an input feeding several units is, each use contributes its own term, and the terms add; Baydin et al. (2018, Section 3.2) show the same accumulation for an input that reaches the output through two intermediate values. A two-branch graph makes it concrete:
with , so , and . Take , , and . Forward, , , and ; backward, , and . Then and ; keeping only one path would give the derivative of a different graph. With and instead, , and , so while : the two paths to now partly cancel. Both the branch and the target changed between the two cases, so they do not compare one variable; the rule of adding every path's contribution is the same in both. The practice below works both cases.
Try it
First a worked example, then one to finish, then problems on your own. Check your answer before you look at the solution. Answers typed here are not kept when you leave.
1 · Worked example
One weight row, by hand
The figure's network: , target , prediction . Hidden unit H1 has activation and readout weight . Find the gradients of the two weights coming into H1, .
Error. .
Say the next step to yourself first.2 · Finish it
Finish H3's weight
Same network. H3 has readout weight and activation . The first two steps are done. What is , the gradient of the weight from into H3? Give four decimal places.
Across the readout edge. .
The local derivative. .
- Your turn.
3 · On your own
Three paths into one input
Input feeds all three hidden units. The hidden units' gradients are , and the weights leaving are . Find to six decimal places.
5 · On your own
Change the branch, and the paths partly cancel
Same and , but now the second branch is and the target is . What is ?
6 · On your own
A louder signal that gets through less
Change one weight: goes from to , so and H3 is deep in tanh's flat tail. The prediction falls further from the target, and the gradient arriving at almost doubles, from to . What happens to , H3's own learning signal?
The code
import numpy as np
# The notebook network: separate numbers from the interactive Cases A/B/C.
# Shapes (column vectors):
# x: (2, 1), W1: (3, 2), b1: (3, 1)
# W2: (1, 3), b2: (1, 1), y: (1, 1)
x = np.array([[0.5], [-1.0]])
y = np.array([[0.25]])
W1 = np.array([[0.6, -0.2],
[-0.3, 0.4],
[0.1, 0.5]])
b1 = np.array([[0.05], [0.0], [-0.05]])
W2 = np.array([[0.25, -0.15, 0.2]])
b2 = np.array([[0.02]])
# Forward pass: store what the backward pass reuses
z1 = W1 @ x + b1 # (3, 1) pre-activations
h1 = np.tanh(z1) # (3, 1) hidden activations
z2 = W2 @ h1 + b2 # (1, 1)
pred = z2 # identity output, y-hat
loss = 0.5 * float(np.sum((pred - y) ** 2))
# Backward pass
delta2 = pred - y # dJ/dz2, (1, 1)
dW2 = delta2 @ h1.T # (1, 3)
db2 = delta2
dh1 = W2.T @ delta2 # received signal dJ/dh1, (3, 1)
phi_prime = 1 - h1 ** 2 # local derivative tanh'(z1), (3, 1)
delta1 = dh1 * phi_prime # dJ/dz1 = received * local, (3, 1)
dW1 = delta1 @ x.T # (3, 1) @ (1, 2) -> (3, 2)
db1 = delta1
dx = W1.T @ delta1 # input cotangent, (2, 1)
def show(name, value):
print(f"{name:<10}", np.round(np.ravel(value), 6))
print("loss:", round(loss, 6))
for name, value in [("z1", z1), ("h1", h1), ("pred", pred), ("delta2", delta2),
("dh1", dh1), ("phi_prime", phi_prime), ("delta1", delta1)]:
show(name, value)
show("dW1 row 1", dW1[0])
# Row 3 (Python index 2): hidden unit 3's delta times the input row x^T
row3 = delta1[2, 0] * x.T # (1, 2)
show("dW1 row 3", row3)
assert np.allclose(row3, dW1[2])
show("dW2", dW2)
show("dx", dx)
print("hidden signal shape:", delta1.shape)
print("first-layer gradient shape:", dW1.shape)
print("input cotangent shape:", dx.shape)
It prints the same named values as the worked pass, in this order: loss, z1, h1, pred for , delta2, dh1 for the received signal , phi_prime for the local derivative , delta1, row 1 of dW1, H3's row delta1[2, 0] * x.T checked against dW1[2], then dW2, dx and the three shapes. Python counts from 0, so H3 is index 2. That row answers the H3 exercise above, so try it first.
This tiny network has one hidden layer, but it shows the recurrence: delta2 moves through W2.T, becomes delta1 after the tanh derivative, and then produces both dW1 and dx. A deeper network repeats those lines once per layer, from the loss back to the input.
Keep it
How do you get the gradient of the loss with respect to one weight deep inside a network?
Answer from memory, then check
Multiply the local derivatives on the path from the loss back to that weight: the error, then each edge's factor (a weight, or an activation's slope), ending with the input the weight multiplied.
A node feeds several later nodes. How is its gradient formed?
Answer from memory, then check
Add the contributions from every path back to the loss, one product per path. Paths with opposite signs can cancel, so a small total does not mean nothing flows through.
One weight is used in two places in the forward pass. How is its gradient formed?
Answer from memory, then check
One contribution per use, each the gradient arriving at that use times the local derivative there, then add them. Tied weights get their gradient this way.
Why does a saturated tanh unit barely learn, even when a large gradient reaches it?
Answer from memory, then check
Its local derivative is nearly zero when is near . Everything passing back through the unit, including its incoming weights' gradients, is multiplied by that tiny factor.
Why does computing every gradient take one backward pass, not one pass per parameter?
Answer from memory, then check
Going backwards from a single loss, each node's gradient is computed once and reused by everything behind it. Each edge is crossed once on the way back, with a fixed amount of arithmetic, so the backward pass costs a small constant multiple of the forward pass however many parameters there are.
The prediction lands exactly on the target. What are the gradients?
Answer from memory, then check
All zero. Every path back from the loss starts with the factor .
Recalling an answer from memory makes it last longer than rereading it (Roediger and Karpicke, 2006). Add these 6 cards and they come back tomorrow, then after gaps of 3, 7, 16, 35 and 75 days while you still remember them. They stay in this browser only.
The edge
As an algorithm, backpropagation is settled: it is the chain rule, evaluated from the loss backwards so that each intermediate result is computed once. Two questions around it are not.
Brains learn, but a neuron has no obvious way to send an exact error backwards through the same synapses it used going forward. Whether the brain approximates backpropagation, and how, is argued over (Lillicrap et al., 2020, “Backpropagation and the brain”). One surprise from that line of work: fixed random backward weights, in place of the transposed forward ones, still let a network learn (Lillicrap et al., 2016).
The other question is why following these gradients works so well at all. The loss of a deep network is far from a simple bowl, yet small steps downhill reliably find good solutions, and a full explanation is still missing.