Learn ML

Chapter 14 of 25

Backpropagation

How a network learns from its own mistakes

22 min read

Motivation

Suppose a group project gets a failing grade. The professor doesn't just shrug — they need to figure out exactly which teammate's section pulled the score down, and by how much, so each person knows precisely what to fix next time. Do it fairly and efficiently, and the whole group improves fast. Do it by guessing, and you're stuck.

A multi-layer perceptron faces the exact same problem after every single prediction. The network gets a loss — a single number saying "you were this wrong" — and it needs to know how much every individual weight, in every layer contributed to that wrongness. With potentially millions of weights spread across many layers, this is called the credit assignment problem, and backpropagation is the algorithm that solves it, efficiently and exactly, using nothing more exotic than the chain rule from calculus.

A Real-Life Scenario

Think of a factory assembly line producing a final product, where the final inspector rejects it for being 2cm too long. That error didn't originate at the final station — it could have started at station 1, been slightly amplified at station 2, and slightly corrected at station 3. To fix the process, you need to trace the error back through every station and figure out each one's share of the blame.

Backpropagation does exactly this inside a neural network. It starts with the error at the output (the loss), and walks backward through every layer, computing exactly how much each layer — and each weight within that layer — is responsible for the final mistake.

Building the Intuition

Here's the picture to hold in your head before any formulas:

  • Forward pass: information flows left to right — input → layer 1 → layer 2 → ... → output → loss.
  • Backward pass: blame flows right to left — loss → output layer → ... → layer 2 → layer 1.
  • At every layer, backprop computes one central quantity: how sensitive is the final loss to a tiny change in this layer's raw output? That sensitivity is called the layer's error signal, written δ[]\boldsymbol{\delta}^{[\ell]}.
  • Once you know a layer's error signal, computing that layer's weight gradients is simple arithmetic — no calculus left to do. All the hard work is in propagating δ\boldsymbol{\delta} backward correctly.

1.Forward pass

Input flows through every layer, caching z and a at each step, ending in a prediction and a loss.

2.Output error

Compute δ at the last layer: how the loss reacts to the network's raw output.

3.Backward recursion

Propagate δ backward, layer by layer, reusing the weights and cached activations from the forward pass.

4.Parameter gradients

At every layer, turn δ into concrete weight and bias gradients — ready for gradient descent.

The Math Behind It

We reuse the forward-propagation notation: z[]=W[]a[1]+b[]\mathbf{z}^{[\ell]} = \mathbf{W}^{[\ell]}\mathbf{a}^{[\ell-1]}+\mathbf{b}^{[\ell]}, a[]=φ[](z[])\mathbf{a}^{[\ell]} = \varphi^{[\ell]}(\mathbf{z}^{[\ell]}), and a per-example loss L=(y^,y)L = \ell(\hat{\mathbf{y}}, \mathbf{y}) that can be any differentiable loss function.

The Error Signal
δ[]Lz[]\boldsymbol{\delta}^{[\ell]} \triangleq \frac{\partial L}{\partial \mathbf{z}^{[\ell]}}

This is the one quantity backpropagation computes at every layer, working from the output back to the input. Everything else follows from it.

Step 1 — The Output Layer Error

By the chain rule, the error at the very last layer LL splits into two ingredients:

Output Layer Error Signal
δ[L]=a[L]L  φ[L](z[L])\boldsymbol{\delta}^{[L]} = \nabla_{\mathbf{a}^{[L]}}L \ \odot\ \varphi'^{[L]}\big(\mathbf{z}^{[L]}\big)

Where: a[L]L\nabla_{\mathbf{a}^{[L]}}L is the gradient of the loss with respect to the network's raw output, and \odot is the element-wise (Hadamard) product, since the activation is applied element-wise.

Intuition: the error combines how wrong the prediction is with how sensitive the activation function is right now. A big loss gradient paired with a steep activation slope produces a big error signal — and either one being near zero shrinks it.

A famous simplification

For two very common pairings — sigmoid activation with binary cross-entropy, and softmax with categorical cross-entropy — this entire expression collapses to δ[L]=y^y\boldsymbol{\delta}^{[L]} = \hat{\mathbf{y}} - \mathbf{y}. Chapter 16 derives this explicitly; it's one of the most frequently tested results in deep learning, precisely because it's so clean: the gradient is just prediction minus target.

Step 2 — Propagating the Error Backward

This is the heart of the algorithm: relating layer \ell's error to the error one layer ahead of it.

Backward Recursion
δ[]=((W[+1])δ[+1])φ[](z[])\boldsymbol{\delta}^{[\ell]} = \Big(\big(\mathbf{W}^{[\ell+1]}\big)^\top \boldsymbol{\delta}^{[\ell+1]}\Big) \odot \varphi'^{[\ell]}\big(\mathbf{z}^{[\ell]}\big)

Where it comes from: layer \ell's pre-activation only affects the loss through layer +1\ell+1. Summing over every neuron in layer +1\ell+1 that a given neuron feeds into (the multivariate chain rule) gives:

δj[]=k=1n+1δk[+1]Wkj[+1]φ[](zj[])\delta^{[\ell]}_j = \sum_{k=1}^{n_{\ell+1}} \delta^{[\ell+1]}_k\, W^{[\ell+1]}_{kj}\, \varphi'^{[\ell]}(z^{[\ell]}_j)

which, written across every neuron jj at once, is exactly the matrix recursion above.

Why 'back'-propagation?

To compute δ[]\boldsymbol{\delta}^{[\ell]} you need δ[+1]\boldsymbol{\delta}^{[\ell+1]}, which needs δ[+2]\boldsymbol{\delta}^{[\ell+2]}, and so on — all the way from the output. That dependency chain is why error signals are computed starting at the output and working backward, the exact mirror image of the forward pass.

Step 3 — Turning Error Signals Into Gradients

Once every δ[]\boldsymbol{\delta}^{[\ell]} is known, the actual trainable parameters fall out directly — no more chain rule required.

Weight and Bias Gradients
LW[]=δ[](a[1]),Lb[]=δ[]\frac{\partial L}{\partial \mathbf{W}^{[\ell]}} = \boldsymbol{\delta}^{[\ell]}\big(\mathbf{a}^{[\ell-1]}\big)^\top, \qquad \frac{\partial L}{\partial \mathbf{b}^{[\ell]}} = \boldsymbol{\delta}^{[\ell]}

Derivation sketch: since zj[]=iWji[]ai[1]+bj[]z^{[\ell]}_j = \sum_i W^{[\ell]}_{ji}a^{[\ell-1]}_i + b^{[\ell]}_j, we get zj[]/Wji[]=ai[1]\partial z^{[\ell]}_j/\partial W^{[\ell]}_{ji} = a^{[\ell-1]}_i and zj[]/bj[]=1\partial z^{[\ell]}_j/\partial b^{[\ell]}_j = 1. Applying the chain rule and writing it in matrix form gives the outer product above.

Forward and Backward Pass Through a Multi-Layer Perceptron
Forward propagation (green) computes activations left to right; backpropagation (red) computes error signals right to left, reusing values cached during the forward pass.

Worked Example: Backpropagation Through a 2-Layer Network

We continue the network from Chapter 13's worked example: input x=(1.0,0.5)\mathbf{x}=(1.0, 0.5), sigmoid activations throughout, with the forward pass already computed as z[1]=(0.30,0.45)\mathbf{z}^{[1]}=(0.30,0.45), a[1]=(0.5744,0.6106)\mathbf{a}^{[1]}=(0.5744,0.6106), z[2]=0.2931z^{[2]}=0.2931, and y^=0.5728\hat{y} = 0.5728. The true label is y=1y = 1, and we use binary cross-entropy with a sigmoid output — so, using the simplification above:

δ[2]=y^y=0.57281=0.4272\delta^{[2]} = \hat y - y = 0.5728 - 1 = -0.4272

Layer 2 gradients:

LW[2]=δ[2](a[1])(0.2454, 0.2609),Lb[2]=0.4272\frac{\partial L}{\partial \mathbf{W}^{[2]}} = \delta^{[2]}(\mathbf{a}^{[1]})^\top \approx (-0.2454,\ -0.2609), \qquad \frac{\partial L}{\partial b^{[2]}} = -0.4272

Backward recursion into layer 1, using σ(z)=σ(z)(1σ(z))(0.2445, 0.2378)\sigma'(z)=\sigma(z)(1-\sigma(z)) \approx (0.2445,\ 0.2378):

(W[2])δ[2]=[0.80.6](0.4272)=[0.34180.2563]      δ[1]=[0.08360.0610](\mathbf{W}^{[2]})^\top\delta^{[2]} = \begin{bmatrix}0.8\\-0.6\end{bmatrix}(-0.4272) = \begin{bmatrix}-0.3418\\0.2563\end{bmatrix} \ \implies\ \boldsymbol{\delta}^{[1]} = \begin{bmatrix}-0.0836\\0.0610\end{bmatrix}

Layer 1 gradients:

LW[1]=δ[1]x=[0.08360.04180.06100.0305],Lb[1]=[0.08360.0610]\frac{\partial L}{\partial \mathbf{W}^{[1]}} = \boldsymbol{\delta}^{[1]}\mathbf{x}^\top = \begin{bmatrix}-0.0836 & -0.0418\\0.0610 & 0.0305\end{bmatrix}, \qquad \frac{\partial L}{\partial \mathbf{b}^{[1]}} = \begin{bmatrix}-0.0836\\0.0610\end{bmatrix}

Every gradient needed for one step of gradient descent is now available — computed with only local quantities and a single backward pass.

The most-asked derivation question in the field

"Derive backpropagation" is arguably the single most common deep learning interview question. Be ready to reproduce, from memory: (1) the definition of δ[]\delta^{[\ell]}, (2) the output layer formula, (3) the backward recursion and its chain-rule derivation, and (4) the weight/bias gradient formulas. Working through the numeric example above without looking is excellent practice.

Implementing It

import numpy as np
 
def sigmoid(z):
    return 1 / (1 + np.exp(-z))
 
def sigmoid_deriv(a):
    return a * (1 - a)   # takes the ACTIVATION a = sigmoid(z), not z
 
x = np.array([1.0, 0.5]); y = 1.0
W1 = np.array([[0.3, -0.2], [0.5, 0.1]]); b1 = np.array([0.1, -0.1])
W2 = np.array([[0.8, -0.6]]);             b2 = np.array([0.2])
 
# --- Forward pass ---
z1 = W1 @ x + b1
a1 = sigmoid(z1)
z2 = W2 @ a1 + b2
a2 = sigmoid(z2)          # y_hat
 
# --- Backward pass ---
delta2 = a2 - y                                   # BCE + sigmoid simplification
dW2 = np.outer(delta2, a1)
db2 = delta2
 
delta1 = (W2.T @ delta2) * sigmoid_deriv(a1)
dW1 = np.outer(delta1, x)
db1 = delta1

The comment on sigmoid_deriv matters more than it looks: that function takes the activation a=σ(z)a=\sigma(z), not the pre-activation zz, because σ(z)=σ(z)(1σ(z))\sigma'(z) = \sigma(z)(1-\sigma(z)) can be written directly in terms of the already-computed aa. This is exactly why the forward pass caches every z[]\mathbf{z}^{[\ell]} and a[]\mathbf{a}^{[\ell]} — backprop reuses them instead of recomputing anything.

Backpropagation is nearly as cheap as the forward pass

It's easy to assume that computing gradients for millions of parameters must be far more expensive than a single forward pass. In reality, backpropagation costs only a small constant factor more — typically 2-3x — because it reuses cached forward-pass values instead of recomputing anything from scratch. The alternative, estimating each parameter's gradient independently via finite differences, would scale with the number of parameters, which is computationally hopeless at modern scale.

Key Takeaways

Key Takeaways
  • Backpropagation solves the credit assignment problem: computing L/W[]\partial L/\partial\mathbf{W}^{[\ell]} for every layer, efficiently.
  • The error signal δ[]=L/z[]\boldsymbol{\delta}^{[\ell]} = \partial L/\partial\mathbf{z}^{[\ell]} is computed recursively, from the output layer backward.
  • The backward recursion follows directly from the multivariate chain rule: δ[]=((W[+1])δ[+1])φ[](z[])\boldsymbol{\delta}^{[\ell]}=\big((\mathbf{W}^{[\ell+1]})^\top\boldsymbol{\delta}^{[\ell+1]}\big)\odot\varphi'^{[\ell]}(\mathbf{z}^{[\ell]}).
  • Weight and bias gradients are simple outer products / direct copies of the error signal once δ[]\boldsymbol{\delta}^{[\ell]} is known.
  • Backpropagation costs roughly the same order of computation as one forward pass — the reason training deep networks is feasible at all.

Common Mistakes

Confusing δ with the weight gradient

δ[]\boldsymbol{\delta}^{[\ell]} is a gradient with respect to the pre-activation, not the weight. It's an intermediate quantity — you still need one more step (the outer product with a[1]\mathbf{a}^{[\ell-1]}) to get L/W[]\partial L/\partial\mathbf{W}^{[\ell]}.

Using the wrong derivative input

σ(z)=σ(z)(1σ(z))\sigma'(z) = \sigma(z)(1-\sigma(z)) is written in terms of the activation a=σ(z)a=\sigma(z). Plugging in the raw pre-activation zz instead of the cached activation aa is a common off-by-one-step bug when implementing this from scratch.

Interview Corner

Quick Check
1. What does the error signal δ^[ℓ] represent?
2. Why is backpropagation computationally efficient compared to finite differences?

Where This Shows Up in Practice

Every deep learning framework — PyTorch, TensorFlow, JAX — is built around an automatic differentiation engine that is, at its core, backpropagation generalized to arbitrary computation graphs. Understanding the derivation in this chapter by hand is what makes concepts like vanishing gradients (Chapter 15), gradient clipping, and modern optimizers (Chapter 17) make sense — they're all strategies for keeping this exact backward flow of error signals well-behaved.