๐Ÿ”— Deep Learning ยท Lecture 6 of 38

Backpropagation Derived Step by Step

Backpropagation computes every gradient in a network at about the cost of one forward pass. We derive it for a two-layer network by hand, generalise to any depth, implement it in NumPy and verify it numerically.

Backpropagation is the algorithm that made deep learning possible. It was discovered several times โ€” Linnainmaa's reverse-mode differentiation in 1970, Werbos' application to neural networks in 1974 โ€” and brought to prominence by Rumelhart, Hinton and Williams in 1986. Every time you call loss.backward(), this algorithm runs. Today we derive it completely, by hand, so that it never feels like magic again.

The setting#

Consider a two-layer network for classification with input $\mathbf{x} \in \mathbb{R}^d$, a hidden layer of width $h$ and $K$ output classes:

$$ \begin{aligned} \mathbf{z}_1 &= \mathbf{W}_1\mathbf{x} + \mathbf{b}_1 && (h) \\ \mathbf{a}_1 &= \text{ReLU}(\mathbf{z}_1) && (h) \\ \mathbf{z}_2 &= \mathbf{W}_2\mathbf{a}_1 + \mathbf{b}_2 && (K) \\ \mathbf{p} &= \text{softmax}(\mathbf{z}_2) && (K) \\ L &= -\log p_y \end{aligned} $$

We want $\frac{\partial L}{\partial\mathbf{W}_1}, \frac{\partial L}{\partial\mathbf{b}_1}, \frac{\partial L}{\partial\mathbf{W}_2}, \frac{\partial L}{\partial\mathbf{b}_2}$.

The key idea#

Define the error signal at each layer, $\boldsymbol{\delta} = \partial L/\partial\mathbf{z}$. Compute it at the output, then propagate it backwards layer by layer using the chain rule. Once you know $\boldsymbol{\delta}$ for a layer, the weight gradients of that layer follow immediately.

Step 1: output layer#

For softmax with cross-entropy, we showed earlier that

$$ \boldsymbol{\delta}_2 = \frac{\partial L}{\partial\mathbf{z}_2} = \mathbf{p} - \mathbf{y} $$

where $\mathbf{y}$ is the one-hot label.

Step 2: gradients of the output weights#

Since $\mathbf{z}_2 = \mathbf{W}_2\mathbf{a}_1 + \mathbf{b}_2$, each entry $z_{2,k} = \sum_j W_{2,kj}a_{1,j} + b_{2,k}$, so $\partial z_{2,k}/\partial W_{2,kj} = a_{1,j}$. Therefore

$$ \frac{\partial L}{\partial\mathbf{W}_2} = \boldsymbol{\delta}_2\,\mathbf{a}_1^\top \quad (K \times h), \qquad \frac{\partial L}{\partial\mathbf{b}_2} = \boldsymbol{\delta}_2 $$

Pattern: weight gradient = (error at the layer's output) ร— (input to the layer)แต€.

Step 3: propagate to the hidden layer#

The hidden activation $\mathbf{a}_1$ influences every output through $\mathbf{W}_2$. Summing over those paths (the multivariable chain rule):

$$ \frac{\partial L}{\partial\mathbf{a}_1} = \mathbf{W}_2^\top\boldsymbol{\delta}_2 $$

Then pass through the ReLU, whose derivative is 1 where $z_1 > 0$ and 0 elsewhere:

$$ \boldsymbol{\delta}_1 = \frac{\partial L}{\partial\mathbf{z}_1} = \left(\mathbf{W}_2^\top\boldsymbol{\delta}_2\right) \odot \mathbb{1}[\mathbf{z}_1 > 0] $$

Step 4: gradients of the first layer#

Exactly the same pattern as Step 2:

$$ \frac{\partial L}{\partial\mathbf{W}_1} = \boldsymbol{\delta}_1\,\mathbf{x}^\top, \qquad \frac{\partial L}{\partial\mathbf{b}_1} = \boldsymbol{\delta}_1 $$

The general algorithm#

For a network with layers $\mathbf{z}_l = \mathbf{W}_l\mathbf{a}_{l-1} + \mathbf{b}_l$, $\mathbf{a}_l = \phi(\mathbf{z}_l)$:

  1. Forward pass โ€” compute and store all $\mathbf{z}_l$ and $\mathbf{a}_l$.
  2. Output error โ€” $\boldsymbol{\delta}_L = \partial L/\partial\mathbf{z}_L$.
  3. Backward pass โ€” for $l = L, \dots, 1$:
$$ \frac{\partial L}{\partial\mathbf{W}_l} = \boldsymbol{\delta}_l\mathbf{a}_{l-1}^\top, \qquad \frac{\partial L}{\partial\mathbf{b}_l} = \boldsymbol{\delta}_l, \qquad \boldsymbol{\delta}_{l-1} = \left(\mathbf{W}_l^\top\boldsymbol{\delta}_l\right) \odot \phi'(\mathbf{z}_{l-1}) $$

For a mini-batch stored as rows of $\mathbf{X}$, the products become $\mathbf{A}_{l-1}^\top\boldsymbol{\Delta}_l$ summed (or averaged) over the batch.

Why it is efficient#

A naive approach would compute each of the $P$ parameter derivatives separately by perturbation, costing $O(P)$ forward passes. Backpropagation reuses the shared error signals $\boldsymbol{\delta}_l$, computing all gradients in one backward pass whose cost is about two to three times the forward pass. For a model with a billion parameters, that is the difference between impossible and routine. The price is memory: stored activations from the forward pass.

Implementation in NumPy#

python
import numpy as np

rng = np.random.default_rng(0)
N, d, h, K = 64, 10, 32, 3
X = rng.normal(size=(N, d)); y = rng.integers(0, K, N); Y = np.eye(K)[y]
W1 = rng.normal(0, np.sqrt(2 / d), (d, h)); b1 = np.zeros(h)
W2 = rng.normal(0, np.sqrt(2 / h), (h, K)); b2 = np.zeros(K)

def forward(W1, b1, W2, b2):
    Z1 = X @ W1 + b1; A1 = np.maximum(0, Z1)
    Z2 = A1 @ W2 + b2
    Z2s = Z2 - Z2.max(1, keepdims=True)
    P = np.exp(Z2s) / np.exp(Z2s).sum(1, keepdims=True)
    loss = -np.log(P[np.arange(N), y]).mean()
    return loss, (Z1, A1, P)

def backward(cache):
    Z1, A1, P = cache
    D2 = (P - Y) / N                        # dL/dZ2, averaged over the batch
    dW2, db2 = A1.T @ D2, D2.sum(0)
    D1 = (D2 @ W2.T) * (Z1 > 0)             # dL/dZ1
    dW1, db1 = X.T @ D1, D1.sum(0)
    return dW1, db1, dW2, db2

loss, cache = forward(W1, b1, W2, b2)
grads = backward(cache)

# Gradient check on W1 with central differences
num = np.zeros_like(W1); eps = 1e-5
for i in range(d):
    for j in range(h):
        W1[i, j] += eps; lp, _ = forward(W1, b1, W2, b2)
        W1[i, j] -= 2 * eps; lm, _ = forward(W1, b1, W2, b2)
        W1[i, j] += eps; num[i, j] = (lp - lm) / (2 * eps)
print("relative error:", np.linalg.norm(num - grads[0]) / np.linalg.norm(num + grads[0]))

# Train with plain gradient descent
params = [W1, b1, W2, b2]
for step in range(500):
    loss, cache = forward(*params)
    for p, g in zip(params, backward(cache)):
        p -= 0.5 * g
print("final training loss:", round(loss, 4))

The gradient check should report a relative error around $10^{-8}$ or smaller โ€” confirmation that our derivation and code agree.

JA
Written by

Janin A Apurba

B.Sc. in CSE, AUST ยท Advanced ICT Officer, CNRS-UNHCR. Teaching AI, ML and Deep Learning to the next generation of engineers and researchers.

Keep learning

Related lectures

๐Ÿ”— Deep Learning

Optimisers I: SGD, Momentum and Nesterov Acceleration

Plain SGD zig-zags through ravines and crawls across plateaus. Momentum accumulates velocity to fix both. We derive heavy-ball and Nesterov momentum, analyse their effect on ill-conditioned problems, and give tuning advice.

Intermediateโฑ 5 min#104
๐Ÿ”— Deep Learning

From Biological Neurons to Artificial Neural Networks

We open the Deep Learning track by tracing the path from biological neurons to artificial ones, defining a neural network precisely, and explaining why depth and learned representations changed AI.

Beginnerโฑ 4 min#097
๐Ÿ”— Deep Learning

Vanishing and Exploding Gradients โ€” Causes and Cures

Gradients are products of many Jacobians, so they can shrink or grow exponentially with depth. We analyse why, how to diagnose it, and the arsenal of fixes from ReLU and initialisation to residuals, normalisation, clipping and gating.

Intermediateโฑ 4 min#108