Backpropagation
Suppose you build a ten-layer network with a million weights. You need to know, for each one, how a tiny nudge changes the loss. Computing this naively — perturb each weight, rerun the forward pass, measure the change — costs one full forward pass per parameter. For modern networks, that computation would take longer than the age of the universe. Backpropagation solves this by computing all million gradients in roughly the same time as a single forward pass. It is the single algorithmic fact that makes deep learning computationally feasible. The idea predates deep learning by decades. It is reverse-mode automatic differentiation applied to the computational graph of a neural network. It uses the The Chain Rule for Neural Networks to propagate derivatives backward from the loss to every parameter. Every modern deep learning framework — PyTorch's `autograd`, TensorFlow's `GradientTape`, JAX's `grad` — is an industrial-strength implementation of this exact algorithm. Each generalizes it to arbitrary computational graphs. Without backpropagation, gradient-based training of deep networks would be a different discipline entirely. It would be restricted to shallow models or hand-derived gradients for each new architecture. With it, an engineer can define an arbitrary differentiable computation. Exact gradients follow essentially for free. This article gives the precise algorithm, a worked numerical trace, a correctness argument grounded in the chain rule, and the complexity analysis explaining why it is efficient. By the end you should be able to derive backprop for a small network by hand. You should be able to implement the forward/backward pseudocode. You should also recognize the failure modes (vanishing gradients, memory blowup) that motivate later refinements. These include automatic differentiation engines and gradient-stabilization techniques.
- Backpropagation computes gradients of a scalar loss with respect to every parameter in one backward pass.
- It is reverse-mode automatic differentiation applied to a neural network's computational graph.
- Each node caches its forward output; the backward pass multiplies local Jacobians by upstream gradients.
- Total cost is O(1) forward passes' worth of work — roughly 2–3× the forward pass, independent of parameter count.
- Correctness follows directly from the multivariable The Chain Rule for Neural Networks applied recursively.
- It underlies every framework's `.backward()` / `grad()` call and is the training engine of essentially all deep learning.
Backpropagation reuses the chain rule's shared subexpressions by sweeping backward once through the computational graph, turning a naive exponential-in-depth gradient computation into one that costs the same order as a single forward pass.
The idea
A neural network computes a composite function: input passes through layers , each parameterized by weights , producing a scalar loss after comparison with a target via a loss function. Training requires for every layer , so that gradient descent can update each weight. The computational graph is the directed acyclic graph (DAG) of all intermediate values: each node is a tensor, each edge a differentiable operation, and the loss is the unique sink.
Forward-mode differentiation propagates derivatives of intermediate variables with respect to one input forward through the graph — efficient when there are few inputs and many outputs. Reverse-mode differentiation, which is what backpropagation is, propagates derivatives of the loss with respect to many intermediate variables backward from the single output. Because a neural network has one scalar output (the loss) and potentially millions of parameters (playing the role of "inputs" whose sensitivities we want), reverse mode is the correct choice: it computes gradients with respect to all parameters in a single backward traversal, regardless of how many there are. The key mathematical object is the adjoint, written for any node in the graph. Backpropagation computes adjoints for every node in one pass, from the loss backward to the inputs, using the multivariate chain rule: if feeds into nodes , then
This single recurrence, applied node by node in reverse topological order, is the entirety of the algorithm. Everything else — layer-specific derivative formulas, matrix shapes, caching strategies — is implementation detail around this recurrence.
How it works
Training a feedforward network proceeds in two passes over the same computational graph.
Forward pass. Compute and cache every intermediate activation, since the backward pass will need them to compute local derivatives. For a layer with pre-activation and activation , cache both and .
Backward pass. Starting from , walk the graph in reverse topological order. At each node, use the local derivative (a Jacobian or its transpose-vector product) to convert the adjoint of the node's outputs into the adjoint of its inputs. For a dense layer this yields the classical equations:
where is elementwise multiplication and is the traditional "error signal" at layer . Note is exactly what the previous layer needs as its output-adjoint — this is the chaining that makes the pass a single linear sweep. ```
# Forward + backward pass for an L-layer MLP.
# Weights W[1..L], biases b[1..L]; activation function sigma with derivative sigma_prime.
# x: input vector, y: target, loss L(a_L, y) with known dL/da_L. function FORWARD(x, W, b): a[0] = x for l = 1 to L: z[l] = W[l] @ a[l-1] + b[l] # cache z[l] a[l] = sigma(z[l]) # cache a[l] return a[L], {z, a} # a[L] used to compute the loss function BACKWARD(y, a_L, cache, W):
# 1. Seed: gradient of loss wrt the network's final output grad_a[L] = dLoss_dOutput(a_L, y) # e.g. (a_L - y) for MSE for l = L downto 1:
# 2. Push through the activation function (local Jacobian is diagonal) delta[l] = grad_a[l] * sigma_prime(cache.z[l]) # elementwise
# 3. Parameter gradients (accumulate — outer product) grad_W[l] = delta[l] @ transpose(cache.a[l-1]) grad_b[l] = delta[l]
# 4. Propagate to the previous layer's activation (matrix-vector) if l > 1: grad_a[l-1] = transpose(W[l]) @ delta[l] return grad_W, grad_b function TRAIN_STEP(x, y, W, b, learning_rate): a_L, cache = FORWARD(x, W, b) grad_W, grad_b = BACKWARD(y, a_L, cache, W) for l = 1 to L: W[l] = W[l] - learning_rate grad_W[l] b[l] = b[l] - learning_rate grad_b[l] return LOSS(a_L, y) ``` Two invariants are worth naming explicitly. First, the caching invariant: any value used to compute a local derivative during the backward pass must have been stored during the forward pass (this is why memory, not just compute, scales with depth — see below). Second, the adjoint-accumulation invariant: if a node's output feeds into multiple downstream nodes (e.g., a weight shared across time steps, or a skip connection), its adjoint is the sum of the contributions from each consumer, exactly as the chain-rule sum above dictates. Forgetting to accumulate — instead of overwrite — at fan-out nodes is one of the most common implementation bugs.
Complexity & correctness
Time complexity. Let be the cost of the forward pass. Each forward operation — a matrix multiply, an elementwise nonlinearity — has a corresponding backward operation whose cost is the same asymptotic order as its forward counterpart: the backward of is two matrix-vector (or matrix-matrix, for batches) products of identical shape, and , each costing . Summing over all operations in the graph, the total backward cost is , with a constant factor typically cited as 2–3 (one multiply forward, roughly two equivalent multiplies backward, per layer). This is the crucial efficiency claim: gradient computation costs a constant multiple of a single forward pass, independent of the number of parameters — a stark contrast to numerical finite-differencing, which costs for parameters, since it needs one perturbed forward pass per parameter.
Space complexity. The forward pass must cache every intermediate activation needed by the backward pass, so memory scales as — i.e., proportional to the total number of intermediate activations, not just the number of parameters. This is the well-known memory bottleneck of deep networks and the motivation for gradient checkpointing (recomputing selected activations during the backward pass instead of storing all of them, trading time for memory).
Correctness argument. The algorithm is a direct, mechanical unfolding of the multivariate chain rule, and its correctness is provable by induction over the reverse topological order of the DAG. Claim: after processing node in the backward traversal, the computed value equals the true adjoint . Base case: the loss node is initialized with , trivially correct. Inductive step: suppose every node downstream of (in forward-graph terms, every consumer of ) has already had its adjoint correctly computed, because the traversal visits nodes in reverse topological order — a node is only processed after all its consumers have been. Let be the direct consumers of in the graph, so each for some differentiable . By the multivariate chain rule, since depends on only through the 's,
Each is available by the inductive hypothesis, and is a local derivative computable from cached forward values without reference to the rest of the graph. The algorithm computes exactly this sum, so 's adjoint is correct. Since the DAG is finite and reverse-topological order always exists (the graph is acyclic by construction — no operation can depend on its own output), induction terminates with every adjoint, including all parameter adjoints, correctly computed.
Honest tradeoffs. Backpropagation gives exact gradients (up to floating-point error) at optimal asymptotic cost, but it is not free of pathology: repeated multiplication of Jacobians across many layers can make gradients shrink or grow geometrically with depth (foreshadowing The Vanishing and Exploding Gradient Problem), and the memory cost of caching every activation can dominate GPU memory budgets for very deep or very wide networks, forcing engineering tradeoffs like checkpointing or mixed precision.
Worked example
Consider a tiny two-layer network with scalar-friendly shapes: input , one hidden unit with sigmoid activation, one linear output unit, and squared-error loss against target .
Take concrete numbers: , , , , , target .
Forward pass:
| quantity | computation | value |
|---|---|---|
Backward pass, seeding :
| quantity | formula | computation | value |
|---|---|---|---|
A gradient-descent step with learning rate updates, e.g., , nudging the output closer to — consistent with the fact that (output too high) correctly produces gradient updates that increase and the hidden-layer weights that feed into it. Notice how every downstream adjoint () was computed exactly once and reused to derive both the parameter gradients at that layer and the adjoint passed to the previous layer — the essence of the algorithm's efficiency: no quantity is recomputed.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Overwriting instead of accumulating adjoints at fan-out nodes | Forgetting a parameter or activation is reused (shared weights, skip connections, multiple loss terms) | Silently wrong gradients, often too small |
| Forgetting to cache forward activations | Trying to save memory by discarding intermediates before backward pass runs | Backward pass has nothing to multiply against; crashes or uses stale values |
| Using the wrong local derivative for the loss/activation pair | E.g., pairing sigmoid output with cross-entropy without using the simplified combined gradient | Numerically unstable or simply incorrect gradient formula |
| In-place operations that corrupt cached tensors | Autograd needs the original forward values; in-place ops silently overwrite them | Gradient computed against wrong (mutated) value, causing subtle training bugs |
| Ignoring the vanishing/exploding gradient effect of depth | Chain rule multiplies many Jacobians; if their norms are consistently or , products shrink/grow exponentially | Early layers stop learning, or weights diverge |
| Mixing up transpose conventions in matrix backward rules | vs. look similar but are not interchangeable | Shape mismatches or transposed gradients that "work" only by accident on square matrices |
Pioneers & history
Backpropagation as applied to neural networks was popularized by the 1986 paper of Rumelhart, Hinton, and Williams, though the mathematical technique — reverse-mode accumulation of derivatives through a chain of computations — was independently discovered multiple times in control theory and automatic differentiation research going back to the 1960s and 70s (notably by Seppo Linnainmaa's 1970 thesis on reverse-mode AD, and earlier control-theoretic work by Henry J. Kelley and Arthur Bryson).
Origins · 1 minGeoffrey Hinton and how this was discovered
Hinton, alongside David Rumelhart and Ronald Williams, showed empirically that backpropagation could train networks with hidden layers to learn internal representations — such as learning distributed representations of words and family-tree relations — reigniting interest in connectionist models after the setback of Minsky and Papert's 1969 critique of single-layer perceptrons.
Where this matters
Every deep learning framework's automatic differentiation engine — PyTorch's `autograd`, TensorFlow's `tf.GradientTape`, JAX's `grad`/`vjp` — is a general-purpose implementation of reverse-mode backpropagation over an arbitrary computational graph, not just fixed feedforward layers. Training convolutional networks for vision, transformers for language, graph neural networks, diffusion models, and reinforcement learning policies all reduce, at the innermost loop, to one backward pass computed by this algorithm. Hardware accelerators (GPUs, TPUs) are designed around the matrix-multiply-heavy structure of both forward and backward passes, and systems like gradient checkpointing, mixed-precision training, and distributed data-parallel training all exist specifically to manage the compute/memory tradeoffs backpropagation introduces. Understanding backpropagation is the prerequisite for understanding Automatic Differentiation engines in full generality and for diagnosing training pathologies like The Vanishing and Exploding Gradient Problem.
Ask the tutor: How would the backward-pass equations change if two layers share the same weight matrix?
Is backpropagation the same thing as gradient descent?
No. Backpropagation is an algorithm for computing gradients efficiently; gradient descent (or its variants like SGD, Adam) is an algorithm for using those gradients to update parameters. Backpropagation answers "what is for every weight?" Gradient descent answers "given that gradient, how should I change to reduce ?" They are complementary: you run backpropagation once per training step to get gradients, then apply an optimizer's update rule using those gradients. Confusing the two is common, but keeping them separate clarifies that you can swap in different optimizers (SGD, Adam, RMSProp) without touching how gradients themselves are computed.
Does backpropagation work for networks with branches, skip connections, or shared weights?
Yes, and this generality is exactly why it's framed in terms of computational graphs rather than fixed layer stacks. As long as the computation is a directed acyclic graph of differentiable operations, reverse-mode differentiation applies unchanged: the only subtlety is that nodes with multiple consumers (branches, skip connections, weight sharing across time steps in an RNN) require summing the adjoint contributions from every consumer, as dictated by the multivariate chain rule. Modern autodiff frameworks handle this automatically by building the graph dynamically and accumulating gradients at each node during the backward traversal.
Why does backpropagation need the network to be differentiable?
The algorithm is a mechanical application of the chain rule, which requires each operation in the graph to have a well-defined local derivative (Jacobian) at the point where it is evaluated. If an operation is non-differentiable at a point (like a hard threshold or the `argmax` function), backpropagation cannot compute an exact local derivative there. In practice, this is handled with subgradients (ReLU's kink at zero is assigned a derivative of 0 or 1 by convention) or with surrogate gradients for genuinely discrete operations, but the theoretical guarantee of exactness technically applies only where the function is differentiable.
How is backpropagation related to automatic differentiation?
Backpropagation is precisely reverse-mode automatic differentiation (AD), specialized to the computational graphs neural networks define. Automatic differentiation is the broader framework covering both forward-mode (propagating derivatives with respect to a single input forward through the graph) and reverse-mode (propagating adjoints backward from a single output). Because neural networks have one scalar loss and many parameters, reverse mode is the efficient choice — this is exactly what "backpropagation" names in the deep learning context. Modern frameworks implement general-purpose reverse-mode AD engines, of which the classic layer-by-layer backprop equations are a special case.
Why do gradients vanish or explode in very deep networks?
Because the chain rule multiplies many Jacobians together as the adjoint propagates backward through dozens or hundreds of layers. If each layer's local Jacobian has singular values consistently below 1 (common with saturating activations like sigmoid, whose derivative is at most 0.25), the product shrinks geometrically with depth, so early-layer gradients become vanishingly small and those layers stop learning. If singular values are consistently above 1, the product grows unboundedly, causing unstable updates. This is a direct mathematical consequence of the backward recurrence, not a bug in the algorithm itself, and it motivates architectural and normalization techniques covered separately.
Can backpropagation get stuck in a bad local minimum?
Backpropagation itself doesn't get stuck — it just computes gradients correctly at whatever point you evaluate it. Whether training converges to a good solution depends on the optimizer, the loss landscape, initialization, and learning rate, not on backpropagation's correctness. In practice, for large overparameterized networks, empirical and theoretical evidence suggests the loss landscape has far fewer problematic local minima than intuition from low-dimensional optimization would suggest, and saddle points and plateaus are the more common obstacles gradient-based training encounters.
Is backpropagation specific to feedforward networks, or does it generalize?
It generalizes completely. Recurrent networks use "backpropagation through time," which unrolls the recurrence into a DAG over time steps and applies the same reverse-mode recurrence, summing gradient contributions to shared weights across time steps. Convolutional layers, attention layers, normalization layers, and even control-flow-heavy architectures all reduce to the same underlying reverse-mode traversal over whatever computational graph the forward pass builds. This universality is precisely why frameworks implement backpropagation once, as a generic graph-traversal engine, rather than special-casing it per architecture.