Backpropagation — How Neural Networks Learn
The chain rule applied to a network of layers. Gradients flow backward, weights update, the network gets better. Understood once, never forgotten.
A network makes a prediction. It is wrong. Backpropagation answers one question: which weights caused the error, and by exactly how much should each one change?
Module 41 showed the forward pass — data flows left to right through the network, layer by layer, until a prediction emerges. The prediction is compared to the true label. The difference is the loss. Now what? The network has thousands of weights. Which ones made the prediction wrong? How wrong did each one make it? How much should each one move?
This is the credit assignment problem — the hardest problem in training neural networks. If the network predicts 32 minutes for a delivery that actually took 41 minutes, which of the 5,000 weights is responsible for the 9-minute underestimate? All of them contributed — but in different amounts, through different paths.
Backpropagation solves credit assignment using the chain rule from calculus. It starts at the loss and works backwards — computing how much the loss would change if each weight changed by a tiny amount. That quantity is the gradient. Once you have the gradient for every weight, gradient descent subtracts a small fraction of it from each weight. Repeat this millions of times and the network learns.
A manager wants to know why a project was delivered late. They start at the final delay (the loss) and trace backwards. The deployment was late because testing was late. Testing was late because development was late. Development was late because requirements were unclear. At each step they answer: how much did this step contribute to the final delay? That is the chain rule — each step's contribution multiplied together to reach the root cause.
Backpropagation traces the prediction error backwards through the network. At each layer it asks: how much did this layer's weights contribute to the final error? The answer — the gradient — tells each weight exactly how to change to reduce the error next time.
The chain rule — the only piece of calculus backprop needs
The chain rule says: if y depends on z which depends on x, then how y changes with x equals how y changes with z multiplied by how z changes with x. Written as: ∂y/∂x = (∂y/∂z) × (∂z/∂x).
A neural network is a chain of functions. The loss depends on the output, which depends on layer 3, which depends on layer 2, which depends on layer 1, which depends on the weights. Backpropagation applies the chain rule at each link in this chain — starting from the loss and multiplying derivatives backwards through every layer until reaching the weights.
Backprop in matrix form — the same chain rule, but for every weight simultaneously
The chain rule example above worked on one weight. A real network has thousands. The key insight: in matrix form, the chain rule applies to entire layers simultaneously — the same equations work regardless of how wide or deep the network is. Each layer produces two outputs during backprop: the gradient for its own weights (used to update them) and the gradient to pass further backward (used by the previous layer).
Vanishing and exploding gradients — why deep networks were hard before 2015
The chain rule multiplies gradients across layers. In a 10-layer network, the gradient for layer 1's weights is the product of 10 terms — one per layer. If each term is slightly less than 1 (like sigmoid derivatives, which top out at 0.25), the product shrinks exponentially. By layer 1, the gradient is essentially zero — weights never update. This is vanishing gradients.
The opposite happens if each term is greater than 1. The product grows exponentially — gradients explode to billions, weights update by enormous amounts, and training diverges. This is exploding gradients.
Gradient checking — the numerical test that proves backprop is correct
When you implement backprop manually, bugs are easy to introduce — a transposed matrix, a missing factor, a wrong sign. Gradient checking is the gold standard test: compare every analytical gradient (from backprop) to its numerical approximation (computed by slightly perturbing each weight and measuring the loss change). If they match to within 1e-5, backprop is correct.
Two-sided difference is more accurate than one-sided: error is O(h²) vs O(h)
Do this for every weight element — expensive but definitive
Autograd — PyTorch builds the computational graph and runs backprop automatically
Everything you coded by hand above — caching intermediate values, applying the chain rule at each layer, computing dW and passing dA backwards — PyTorch does automatically. When you call loss.backward(), PyTorch traces the computational graph it built during the forward pass and applies the chain rule to every operation automatically. Every tensor that had requires_grad=Truegets its .grad populated.
Every common backprop mistake — explained and fixed
Nobody hand-codes backprop in production. Here is where understanding it earns its keep anyway.
Every production model calls loss.backward() once and lets autograd handle the chain rule end to end. That does not make this module optional — it means the moments where backprop understanding actually matters are concentrated into a few specific, recurring situations instead of spread across every line of code.
Loss plateaus at a suspiciously round number, decreases then suddenly spikes to NaN, or early layers barely move while the last layer overfits fast. Each pattern maps to a specific point in the backward pass — dead activations, exploding gradients, or a vanishing signal that never reaches the early layers.
A network trains fine at 10 layers but stalls at 40. The fix — residual connections, normalisation, better initialisation — only makes sense once you can picture the chain rule multiplying a derivative at every one of those 40 layers.
Quantisation-aware training, custom losses with numerically tricky gradients, or research code implementing a method from a paper all require a hand-written backward pass. Autograd cannot infer one for you.
1. Print the gradient norm of every layer's weights right after one backward() call — iterate model.named_parameters() and print each parameter's grad norm.
2. Compare the first layer's norm to the last layer's. A ratio smaller than roughly 1e-3 points to vanishing gradients. A ratio larger than roughly 1e3, or any norm in the thousands, points to exploding gradients.
3. If gradients look healthy at every layer but the loss still will not move, the problem is almost never backprop itself — check the learning rate, the loss function choice, and whether labels are shaped and typed correctly.
The custom autograd.Function case is worth seeing in code, because it is the one situation where "PyTorch handles it automatically" stops being true. Quantisation-aware training rounds activations to a fixed set of levels during the forward pass — but rounding has zero derivative almost everywhere, so plain autograd would report that every upstream weight had exactly zero effect on the loss. Teams shipping quantised models write a custom backward pass specifically to work around this, using exactly the chain-rule mechanics this module covers.
Five things people get wrong about backpropagation
They solve two different problems that happen to run back to back. Backprop answers "how much does each weight contribute to the error?" — it computes gradients, nothing more. Gradient descent answers "given these gradients, how should I update the weights?" You could compute gradients with backprop and then update weights with a completely different optimiser (Adam, RMSprop, SGD with momentum) — backprop does not change. It is the gradient-computation half of training, not the whole thing.
Mathematically backprop works at any depth — the chain rule does not care how many layers there are. Practically, depth is exactly what breaks it: each layer's derivative gets multiplied into the gradient, and with sigmoid/tanh those derivatives are consistently below 1, so a 20-layer network can vanish the gradient to numerical zero before it reaches the early layers. Every major deep learning breakthrough since 2015 — ResNets' skip connections, batch normalisation, careful initialisation — exists specifically to keep very deep networks trainable despite this, not because backprop "handles" depth for free.
Backprop only supplies a direction (the gradient); gradient descent then takes a step in that direction. Neural network loss surfaces are highly non-convex, so this process is only guaranteed to reach *a* local minimum, never provably the global one. In practice, modern research suggests that in large, overparameterised networks most local minima found this way perform comparably — but that is an empirical observation about the loss landscape, not a guarantee backprop itself provides.
Autograd computes every gradient for you — you will rarely, if ever, write dW = A_prev.T @ dZ by hand in production code. But "rarely write it" is not "never need to understand it": every one of the errors in this module (dead ReLUs, NaN losses, gradient explosions) is invisible to autograd itself — it faithfully computes whatever gradient the math produces, including a completely broken one. Debugging those requires knowing what backprop is actually doing under the hood, which is exactly why this module exists even though nobody hand-codes it day to day.
It is the identical multivariable chain rule from a calculus course — nothing about it is neural-network-specific. What backprop contributes is an efficient *algorithm* for applying that chain rule to a deeply nested composition of functions: instead of re-deriving the derivative of the whole network with respect to every weight from scratch (which would recompute the same intermediate terms over and over), it caches intermediate gradients during a single backward pass — the same idea as dynamic programming applied to calculus.
Backpropagation — 5 questions interviewers actually ask
A good answer avoids equations entirely: "The network makes a prediction, we measure how wrong it was, and backpropagation is the process of tracing that error backward through the network to figure out exactly how much each individual connection contributed to the mistake — like assigning blame after a failed project by tracing the decision chain back to its source. Once we know each connection's share of the blame, we nudge it slightly to make that mistake less likely next time."
They happen because the chain rule multiplies each layer's local derivative into the gradient flowing backward, and sigmoid/tanh derivatives are bounded well below 1 (sigmoid tops out at 0.25) — across many layers that product shrinks toward zero, so early layers effectively stop learning. Fixes: switch to ReLU-family activations (derivative is exactly 1 for positive inputs, so nothing shrinks), add residual/skip connections so gradients have a direct path around the multiplication, and use batch or layer normalisation to keep activations in a well-behaved range at every layer.
Backprop is a gradient-computation algorithm: given the network and the loss, it produces ∂L/∂W for every weight, using the chain rule and a single backward pass. Gradient descent is an optimisation algorithm: given those gradients, it decides how to update the weights (W ← W − learning_rate × gradient), and there are many variants (SGD, Adam, RMSprop) that all consume the same backprop-computed gradients differently. Conflating them is the single most common mix-up on this topic.
PyTorch accumulates gradients into .grad by default instead of overwriting them — useful on purpose for cases like gradient accumulation across several mini-batches (simulating a larger batch size than fits in memory) or RNNs where you sometimes want gradients to add up across time steps. But for standard training, forgetting zero_grad() means each step's gradient gets added on top of the previous step's leftover gradient, silently corrupting every update after the first — one of the most common real bugs in PyTorch code, and worth mentioning unprompted in an interview.
Gradient checking: compute the gradient two independent ways and compare them. The analytical gradient comes from your backprop implementation. The numerical gradient comes from the definition of a derivative directly — nudge one parameter by a tiny epsilon (e.g. 1e-7), rerun the forward pass, and measure how much the loss changed: (loss(w+ε) − loss(w−ε)) / (2ε). Compute the relative error between the two gradients; below roughly 1e-5–1e-7 means the implementation is correct. This should be done once per new architecture, on a small toy network, then removed — it is far too slow to run during actual training.
You understand how networks learn. Next: what they learn through.
Backpropagation is the learning algorithm. But the network's ability to learn depends critically on two other choices: the activation function (what non-linearity to apply at each neuron) and the loss function (what the network is trying to minimise). Module 43 covers every major activation and loss function — what each one does, when to use it, and the numerical stability pitfalls that trip up every practitioner at least once.
ReLU, GELU, Swish, sigmoid, softmax — and cross-entropy, MSE, Huber, focal loss. When to use each and why numerical stability matters more than you think.
🎯 Key Takeaways
- ✓Backpropagation solves credit assignment — given a prediction error, how much is each weight responsible? It applies the chain rule backwards through the network: start at the loss, multiply derivatives layer by layer back to the weights.
- ✓Each layer in the backward pass produces two things: the gradient for its own weights (∂L/∂W = A_prev.T @ dZ) and the gradient to pass further backward (∂L/∂A_prev = dZ @ W.T). This pattern repeats identically for every layer.
- ✓Vanishing gradients happen when derivatives multiply to near-zero across many layers — sigmoid derivatives top out at 0.25, so 10 layers gives 0.25^10 ≈ 10^-7. The fix is ReLU activation, which has derivative 1 for positive inputs and preserves gradient magnitude.
- ✓Gradient checking is the definitive test for correct backprop: compare analytical gradients (from backprop) to numerical gradients (finite difference). Relative error below 1e-5 means your implementation is correct. Always gradient-check before training a new network architecture.
- ✓PyTorch autograd builds a computational graph during the forward pass and runs backprop automatically on loss.backward(). Every tensor with requires_grad=True gets its .grad populated. Call optimizer.zero_grad() before every backward() — PyTorch accumulates gradients by default.
- ✓Three practical rules: use BCEWithLogitsLoss instead of Sigmoid+BCELoss for numerical stability, always clip sigmoid inputs to avoid overflow, and call optimizer.zero_grad() at the start of every training step without exception.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.