Before diving into backpropagation's mechanics, it's worth answering the question directly: why does this specific algorithm exist at all, instead of just computing gradients some simpler, more obvious way?
The Naive Alternative: Finite Differences
You could, in principle, estimate a gradient without calculus at all โ nudge one parameter by a tiny amount \(h\), rerun the forward pass, see how much the loss changed, and divide:
This works, and it's a genuinely useful sanity check (a "gradient check") for verifying an analytical implementation is correct. But look at the cost: this requires one full forward pass per parameter. A network with a million parameters would need a million extra forward passes just to estimate the gradient for a single training step โ computationally hopeless at any real scale.
What Backpropagation Achieves Instead
| Approach | Cost to Get Every Parameter's Gradient |
|---|---|
| Finite differences (naive) | Roughly \(P\) additional forward passes, for \(P\) parameters |
| Backpropagation | Roughly the cost of one additional backward pass โ regardless of \(P\) |
Backpropagation achieves this through the chain rule's structure (see Chain Rule): rather than treating each parameter's gradient as an independent computation, it computes shared intermediate quantities once, working backward from the loss, and reuses them across every parameter that depends on them. This single algorithmic insight โ computing exactly what's needed, exactly once, in the right order โ is what makes training networks with millions or billions of parameters computationally feasible at all.
Code โ Seeing the Cost Difference Directly
import torch
# Finite-difference gradient check for ONE parameter -- illustrative only,
# never used for actual training due to its cost at scale
def numerical_gradient(f, w, h=1e-5):
return (f(w + h) - f(w - h)) / (2 * h)
f = lambda w: (w ** 2).sum()
w = torch.tensor([2.0, 3.0])
print(numerical_gradient(f, w[0])) # requires 2 extra forward evaluations for just ONE parameter
# Backpropagation gets gradients for ALL parameters in a single backward call
w2 = torch.tensor([2.0, 3.0], requires_grad=True)
loss = (w2 ** 2).sum()
loss.backward()
print(w2.grad) # both gradients, computed together, in one backward pass
A Brief Historical Note
The backpropagation algorithm (as applied to training neural networks) was popularized by Rumelhart, Hinton and Williams in 1986, though the underlying reverse-mode differentiation idea has roots stretching back further in the broader field of automatic differentiation. Its popularization is widely credited as a key turning point that made training multi-layer networks practical โ directly connecting to the "why deep learning became successful" story covered in Why Deep Learning Became Successful.
Common Mistakes
- Assuming backpropagation "invents" new gradient information beyond what the chain rule already implies โ it computes the mathematically exact same gradients finite differences would eventually approximate; the innovation is entirely about efficiency, not a different mathematical result.
- Using finite differences for actual training instead of only as an occasional debugging/verification tool for a custom gradient implementation.
Interview Relevance
Q: "Why can't you just use finite differences to train a neural network instead of backpropagation?" Finite differences require a separate forward pass per parameter to estimate its gradient, which costs roughly \(P\) forward passes for \(P\) parameters โ computationally infeasible for networks with millions or billions of parameters. Backpropagation computes gradients for every parameter simultaneously in roughly the cost of one additional pass, by reusing shared intermediate computations via the chain rule.
Practice Question
A network has 10 million parameters. Roughly how many forward passes would a finite-difference approach need to estimate every parameter's gradient once? How does this compare to backpropagation's cost for the same task?