Skip to content

Backpropagation

The algorithm that enables deep learning — efficiently computing gradients through neural networks


The Problem: Computing Gradients

To train a neural network with gradient descent, we need gradients of the loss with respect to every weight:

θnew=θoldαLθ

The challenge: A network with 1 million weights needs 1 million partial derivatives. How do we compute them efficiently?

Naive Approach: Numerical Gradients

Perturb each weight and measure the change in loss:

LwiL(wi+ϵ)L(wiϵ)2ϵ

Problem: Requires 2 forward passes per weight. For 1M weights, that's 2M forward passes per update — far too slow.

The Solution: Backpropagation

Backpropagation computes all gradients in one forward pass + one backward pass, regardless of network size.


Computation Graphs

Neural networks can be represented as directed acyclic graphs (DAGs) where:

  • Nodes = operations (add, multiply, activation)
  • Edges = data flow

Computation Graph

Example: Simple Network

For y=σ(w2σ(w1x+b1)+b2):

x ──┬──→ [*w₁] ──→ [+b₁] ──→ [σ] ──→ [*w₂] ──→ [+b₂] ──→ [σ] ──→ y
    │                         │
    └─── w₁                   h₁

Each node has:

  • Forward: compute output from inputs
  • Backward: compute gradient of loss w.r.t. inputs, given gradient w.r.t. output

The Chain Rule

Backpropagation is the chain rule applied systematically through the computation graph.

Single Variable Chain Rule

If y=f(g(x)):

dydx=dydgdgdx

Multivariate Chain Rule

If L=L(y1,y2) where y1=f1(x) and y2=f2(x):

Lx=Ly1y1x+Ly2y2x

Key insight: Gradients sum when paths merge, and multiply along paths.

Chain Rule Visualization


Backpropagation Algorithm

Forward Pass

Compute activations layer by layer, caching intermediate values:

python
def forward(x, weights, biases):
    cache = {'a0': x}
    a = x
    for l, (W, b) in enumerate(zip(weights, biases)):
        z = a @ W + b
        cache[f'z{l+1}'] = z
        a = relu(z)  # or other activation
        cache[f'a{l+1}'] = a
    return a, cache

Backward Pass

Propagate gradients from output to input using cached values:

python
def backward(y_pred, y_true, cache, weights):
    grads = {}
    L = len(weights)

    # Output layer gradient
    dL_da = y_pred - y_true  # for softmax + cross-entropy

    for l in reversed(range(1, L+1)):
        # Gradient through activation
        dL_dz = dL_da * relu_derivative(cache[f'z{l}'])

        # Gradient for weights and biases
        grads[f'dW{l}'] = cache[f'a{l-1}'].T @ dL_dz
        grads[f'db{l}'] = np.sum(dL_dz, axis=0)

        # Propagate to previous layer
        dL_da = dL_dz @ weights[l-1].T

    return grads

Backprop Flow Animation


Full Derivation: 2-Layer Network

Consider: Input x → Hidden h → Output y^

Forward Pass

z1=W1x+b1h=σ(z1)z2=W2h+b2y^=softmax(z2)

Loss

Cross-entropy: L=iyilogy^i

Backward Pass

Step 1: Output layer gradient (softmax + cross-entropy simplifies nicely)

Lz2=y^yδ2

Step 2: Output weight gradients

LW2=hTδ2Lb2=δ2

Step 3: Propagate to hidden layer

Lh=δ2W2T

Step 4: Through activation

Lz1=Lhσ(z1)δ1

Step 5: Hidden weight gradients

LW1=xTδ1Lb1=δ1

The Pattern

For any layer l:

δl=δl+1Wl+1Tg(zl)WlL=al1Tδl

Forward Mode vs Reverse Mode Autodiff

ModeDirectionEfficiency
Forward modeInput → OutputO(1) for one input, all outputs
Reverse modeOutput → InputO(1) for one output, all inputs

Why Reverse Mode for Neural Networks?

Neural networks have:

  • Many inputs (millions of weights)
  • One output (scalar loss)

Forward mode: O(n) passes for n weights Reverse mode (backprop): O(1) passes regardless of weight count

Forward vs Reverse Mode


Implementation Details

Gradient Checking

Verify backprop implementation by comparing to numerical gradients:

python
def gradient_check(f, x, epsilon=1e-7):
    """Check analytical gradient against numerical gradient."""
    analytical_grad = f.backward(x)
    numerical_grad = np.zeros_like(x)

    for i in range(x.size):
        x_plus = x.copy()
        x_plus.flat[i] += epsilon
        x_minus = x.copy()
        x_minus.flat[i] -= epsilon

        numerical_grad.flat[i] = (f(x_plus) - f(x_minus)) / (2 * epsilon)

    error = np.linalg.norm(analytical_grad - numerical_grad)
    error /= np.linalg.norm(analytical_grad) + np.linalg.norm(numerical_grad)

    return error < 1e-5  # Should be very small

Numerical Stability

Problem: Softmax can overflow for large inputs

Solution: Subtract max before exponentiating

python
def stable_softmax(z):
    z_shifted = z - np.max(z, axis=-1, keepdims=True)
    exp_z = np.exp(z_shifted)
    return exp_z / np.sum(exp_z, axis=-1, keepdims=True)

Vectorized Implementation

Always work with batches for efficiency:

python
# Bad: loop over samples
for i in range(batch_size):
    grad_W += x[i:i+1].T @ delta[i:i+1]

# Good: vectorized
grad_W = x.T @ delta  # (input_dim, batch) @ (batch, output_dim)

Why Cache Activations?

To compute WlL=al1Tδl, we need al1.

Options:

  1. Recompute: Run forward pass again (slow)
  2. Cache: Store during forward pass (memory-intensive)

Trade-off: Training uses ~2x memory of inference because we cache activations.

Gradient checkpointing: Recompute some activations to trade compute for memory — useful for very large models.


Interview Questions

Q1: "Walk through backpropagation for a 2-layer network."

Forward pass:

  1. z1=W1x+b1 (linear)
  2. a1=σ(z1) (activation)
  3. z2=W2a1+b2 (linear)
  4. y^=softmax(z2) (output)

Loss: L=yilogy^i

Backward pass:

  1. δ2=y^y (output gradient)
  2. W2=a1Tδ2 (output weight gradient)
  3. a1=δ2W2T (propagate back)
  4. δ1=a1σ(z1) (through activation)
  5. W1=xTδ1 (hidden weight gradient)

Update: WWαW

Q2: "Why do we cache activations during forward pass?"

We cache activations because we need them for gradient computation.

The gradient w.r.t. weights at layer l is:

WlL=al1Tδl

We need al1 (the previous layer's activation) to compute this.

Trade-offs:

  • Cache everything: Training uses ~2x inference memory
  • Recompute: Slower, but saves memory
  • Gradient checkpointing: Hybrid — cache some, recompute others

This is why training large models requires much more memory than inference.

Q3: "What's the difference between forward and reverse mode autodiff?"

Forward mode propagates derivatives input → output:

  • Efficient for: Few inputs, many outputs
  • Complexity: O(n) passes for n inputs

Reverse mode (backpropagation) propagates derivatives output → input:

  • Efficient for: Many inputs, few outputs
  • Complexity: O(m) passes for m outputs

Neural networks: Millions of weights (inputs to loss), one scalar loss (output). Reverse mode computes all gradients in one backward pass — that's why we use backpropagation.

Forward mode would require millions of passes. The efficiency difference is the reason deep learning is computationally feasible.


Key Takeaways

  1. Backpropagation is the chain rule applied systematically through a computation graph.

  2. Reverse mode autodiff computes all gradients in O(1) backward passes — essential for efficiency.

  3. Cache activations during forward pass for use in backward pass (trade memory for speed).

  4. Gradient checking validates implementations by comparing to numerical gradients.

  5. The pattern: δl=δl+1Wl+1Tg(zl) — gradients flow backward, multiplying by weights and activation derivatives.