Skip to main content
Backpropagation Concept

Backpropagation Deep Dive

The Central Question

You have a neural network with millions of parameters. The network makes a prediction. The prediction is wrong. How do you know which of those millions of parameters to adjust, and by how much? This is the problem backpropagation solves. It’s the algorithm that makes deep learning possible.
Historical Context: Backpropagation was independently discovered multiple times, but Rumelhart, Hinton, and Williams’ 1986 paper popularized it. It remained largely unused until 2012 because:
  1. Computers weren’t fast enough
  2. Data wasn’t available
  3. Techniques to train deep networks weren’t developed
Today, it runs trillions of times per second in data centers worldwide.

The Core Insight: Credit Assignment

Imagine a factory assembly line:
The final product is defective. Who’s at fault? You need to trace backward through the assembly line to find where things went wrong. That’s exactly what backpropagation does. But here is what makes it subtle: Worker 3 might have done their job perfectly given what Worker 2 handed them. The real mistake was Worker 1 using the wrong material. Backpropagation assigns proportional blame — each worker gets feedback on how much they specifically contributed to the defect, accounting for how their output was transformed by everyone downstream. This is the credit assignment problem, and it is the central challenge that backpropagation elegantly solves.
Credit Assignment Problem

Computational Graphs

To understand backpropagation, we first need to see neural networks as computational graphs.

A Simple Example

Let’s compute f(x,y,z)=(x+y)zf(x, y, z) = (x + y) \cdot z:

Backward Pass with Chain Rule

To compute fx\frac{\partial f}{\partial x}, we apply the chain rule: fx=faax\frac{\partial f}{\partial x} = \frac{\partial f}{\partial a} \cdot \frac{\partial a}{\partial x} Let’s trace through:
  1. f=azf = a \cdot z, so fa=z=4\frac{\partial f}{\partial a} = z = 4
  2. a=x+ya = x + y, so ax=1\frac{\partial a}{\partial x} = 1
  3. Therefore: fx=41=4\frac{\partial f}{\partial x} = 4 \cdot 1 = 4

The Chain Rule: The Key to Everything

The chain rule states: fx=fggx\frac{\partial f}{\partial x} = \frac{\partial f}{\partial g} \cdot \frac{\partial g}{\partial x} Or for a chain of functions f(g(h(x)))f(g(h(x))): fx=fgghhx\frac{\partial f}{\partial x} = \frac{\partial f}{\partial g} \cdot \frac{\partial g}{\partial h} \cdot \frac{\partial h}{\partial x} This is the ONLY math you need for backpropagation!

Intuition

If gg doubles when xx increases by 1, and ff triples when gg doubles:
  • Then ff should increase by 3×2=63 \times 2 = 6 when xx increases by 1
The chain rule is about multiplying sensitivities along a path. Here is a real-world analogy. Suppose you are converting currencies: USD to EUR to JPY. If 1 USD = 0.85 EUR, and 1 EUR = 130 JPY, then 1 USD = 0.85 x 130 = 110.5 JPY. You multiplied the conversion rates along the chain. The chain rule does exactly this — but for rates of change instead of currency rates. Each layer in a network is a “conversion” of its input, and backpropagation multiplies the “exchange rates” (derivatives) backward through every conversion to figure out how the original input affects the final output.
Chain Rule Visualization

Backpropagation in a Neural Network

Now let’s apply this to an actual neural network layer.

Single Neuron

A single neuron computes: y=σ(wx+b)y = \sigma(wx + b) where σ\sigma is the activation function (e.g., sigmoid). Forward pass:
Backward pass: We need Lw\frac{\partial L}{\partial w} and Lb\frac{\partial L}{\partial b}: Lw=Lyyzzw\frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial z} \cdot \frac{\partial z}{\partial w}

Full Backpropagation Algorithm

For a multi-layer network, backpropagation works as follows:

Algorithm

Implementation from Scratch


Gradient Checking

How do you know your gradients are correct? Numerical gradient checking: fθf(θ+ϵ)f(θϵ)2ϵ\frac{\partial f}{\partial \theta} \approx \frac{f(\theta + \epsilon) - f(\theta - \epsilon)}{2\epsilon}

Visualizing Gradient Flow

Gradient Flow Through Network

The Vanishing Gradient Problem

With sigmoid activation:
  • Derivative: σ(z)=σ(z)(1σ(z))0.25\sigma'(z) = \sigma(z)(1 - \sigma(z)) \leq 0.25
  • After 10 layers: 0.25100.0000010.25^{10} \approx 0.000001
Gradients shrink exponentially as they flow backward!
Solutions (each attacks the problem from a different angle):
  1. ReLU activation: Gradient is 1 for positive inputs — no multiplicative shrinking
  2. Batch normalization: Keeps activations centered and scaled, preventing them from drifting into saturation zones
  3. Residual connections: Skip connections add gradients from a direct path, giving gradients a highway that bypasses the multiplicative chain
  4. Better initialization: He initialization sets weights so that variance is preserved across layers, preventing gradients from shrinking or exploding from the very first step
Practical diagnostic: If your loss plateaus early and your network is deep, check gradient magnitudes layer by layer. In PyTorch, you can do this with [p.grad.norm() for p in model.parameters()] after a backward pass. If early layers have gradients orders of magnitude smaller than later layers, you have vanishing gradients.

PyTorch Autograd

PyTorch handles all of this automatically:

How Autograd Works

PyTorch builds a computational graph during the forward pass:

Visualizing the Computation Graph


Common Backprop Patterns

Pattern 1: Add Gate

f=x+y    fx=1,fy=1f = x + y \implies \frac{\partial f}{\partial x} = 1, \quad \frac{\partial f}{\partial y} = 1 Gradient distributor: Passes gradient unchanged to both inputs.

Pattern 2: Multiply Gate

f=xy    fx=y,fy=xf = x \cdot y \implies \frac{\partial f}{\partial x} = y, \quad \frac{\partial f}{\partial y} = x Gradient switcher: Gradient to xx is scaled by yy and vice versa.

Pattern 3: Max Gate

f=max(x,y)    fx=1x>y,fy=1y>xf = \max(x, y) \implies \frac{\partial f}{\partial x} = \mathbf{1}_{x>y}, \quad \frac{\partial f}{\partial y} = \mathbf{1}_{y>x} Gradient router: Gradient flows only to the larger input.

Pattern 4: ReLU

f=max(0,x)    fx=1x>0f = \max(0, x) \implies \frac{\partial f}{\partial x} = \mathbf{1}_{x>0} Gradient gate: Passes gradient if input was positive, blocks if negative. This is why “dying ReLU” is a problem — if a neuron’s input is always negative, it permanently blocks gradient flow and can never recover. The neuron is effectively dead.
Debugging hint: If your network stops learning, check the fraction of neurons with zero activations. If it is above 50%, you likely have a dying ReLU problem. Fix it by lowering the learning rate, using He initialization, or switching to Leaky ReLU.
Backpropagation Patterns

Exercises

Compute the gradients by hand for this simple network on the input x=1x=1:f=σ(σ(xw1+b1)w2+b2)f = \sigma(\sigma(xw_1 + b_1)w_2 + b_2)With w1=0.5w_1 = 0.5, b1=0.1b_1 = 0.1, w2=0.3w_2 = -0.3, b2=0.2b_2 = 0.2.Verify your answer with PyTorch.
Implement a custom activation function with PyTorch autograd:
Test that it gives the same results as nn.ReLU().
Create a 50-layer network with:
  1. Sigmoid activations
  2. ReLU activations
  3. Tanh activations
Measure the gradient magnitude at each layer. Plot and compare.
Implement the backward pass for batch normalization:Forward: x^=xμσ2+ϵ\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}The backward pass involves computing Lx\frac{\partial L}{\partial x}, Lγ\frac{\partial L}{\partial \gamma}, Lβ\frac{\partial L}{\partial \beta}.

Key Takeaways


What’s Next

Module 4: Activation Functions

Deep dive into ReLU, GELU, Swish, and when to use which activation.

Interview Deep-Dive

Strong Answer:
  • The vanishing gradient problem occurs when gradients shrink exponentially as they propagate backward through many layers. In a network with sigmoid activations, the maximum derivative is 0.25 (at z=0z=0). After nn layers, gradients are scaled by roughly 0.25n0.25^n. After 10 layers: 0.25101060.25^{10} \approx 10^{-6}. Early layers receive gradients so small that their weights barely change — they effectively stop learning.
  • The root cause is repeated multiplication by factors less than 1 during backpropagation. The chain rule multiplies local gradients along the path from loss to parameter, and if each local gradient is less than 1, the product approaches zero.
  • Most effective solutions, ranked by impact:
    • ReLU activation: gradient is exactly 1 for positive inputs, eliminating the multiplicative shrinking. This alone enabled training networks from 5 to 20+ layers.
    • Residual connections (skip connections): the gradient of x+F(x)x + F(x) with respect to xx always includes a term of 1, providing a “gradient highway” that bypasses the vanishing chain. This enabled 100-1000+ layer networks.
    • Normalization (BatchNorm, LayerNorm): keeps activations centered and scaled, preventing them from drifting into saturation regions where gradients vanish.
    • Careful initialization (He for ReLU, Xavier for sigmoid/tanh): ensures the variance of activations and gradients is preserved across layers at the start of training.
  • These solutions are complementary, not alternatives. Modern architectures use all four simultaneously.
Follow-up: Can gradients also explode? What is the relationship between vanishing and exploding gradients?Exploding gradients are the mirror problem: repeated multiplication by factors greater than 1 causes gradients to grow exponentially, leading to NaN values and training divergence. Vanishing and exploding are two sides of the same coin — they both stem from the eigenvalues of the weight matrices. If the largest singular value of WhhW_{hh} is less than 1, gradients vanish; if greater than 1, they explode. In RNNs, this is particularly severe because the same weight matrix is applied at every time step. Gradient clipping (capping the gradient norm) is the standard fix for exploding gradients, while LSTM/GRU gating mechanisms address vanishing gradients in the sequential setting.
Strong Answer:
  • Backpropagation requires roughly 2x the compute of the forward pass. The forward pass computes activations; the backward pass computes gradients for both weights AND activations at each layer, plus multiplies by the cached activations. The total training step (forward + backward) is roughly 3x a single forward pass.
  • We cache activations because the gradient computation at each layer requires the activations from the forward pass. Specifically, L/Wl=δlal1T\partial L / \partial W_l = \delta_l \cdot a_{l-1}^T, where al1a_{l-1} is the activation from the previous layer (computed during the forward pass) and δl\delta_l is the error signal propagated backward.
  • This creates a fundamental memory-compute trade-off: storing all activations requires O(L×B×D)O(L \times B \times D) memory (L layers, B batch size, D layer width). For large models like GPT-3, this is the primary memory bottleneck during training.
  • Gradient checkpointing addresses this by discarding intermediate activations and recomputing them during the backward pass. This reduces memory from O(L)O(L) to O(L)O(\sqrt{L}) (with checkpoints every L\sqrt{L} layers) at the cost of one additional forward pass, trading roughly 30% more compute for 60-70% less memory.
Follow-up: Why is the backward pass roughly 2x the forward pass, not 1x?In the forward pass, each layer computes one matrix multiplication: z=Wa+bz = Wa + b. In the backward pass, each layer computes two: the gradient with respect to the weights (L/W=δaT\partial L / \partial W = \delta \cdot a^T) and the gradient with respect to the input (L/a=WTδ\partial L / \partial a = W^T \cdot \delta), which is needed to propagate the error signal to the previous layer. So the backward pass does approximately twice the matrix multiplications of the forward pass. In practice, the ratio varies because the backward pass also involves element-wise operations (activation derivatives) and memory access patterns differ.
Strong Answer:
  • Numerical gradient checking is the gold standard. For each parameter θ\theta, compute the numerical gradient using the centered finite difference: f(θ+ϵ)f(θϵ)2ϵ\frac{f(\theta + \epsilon) - f(\theta - \epsilon)}{2\epsilon} with ϵ107\epsilon \approx 10^{-7}. Compare this to the analytical gradient from backpropagation.
  • The comparison metric should be the relative error: ganalyticalgnumericalganalytical+gnumerical+ϵ\frac{||g_{analytical} - g_{numerical}||}{||g_{analytical}|| + ||g_{numerical}|| + \epsilon}. Relative error below 10710^{-7} is excellent, below 10510^{-5} is acceptable, above 10310^{-3} indicates a bug.
  • Practical considerations: (1) Check gradients on a small network with small inputs to keep cost manageable — numerical gradient checking is O(n)O(n) per parameter, so it is prohibitively expensive for full-sized networks. (2) Use double precision (float64) for gradient checking to avoid floating-point artifacts. (3) Check with and without regularization separately. (4) Be careful with non-differentiable points (e.g., ReLU at 0) — the numerical and analytical gradients may legitimately disagree at these points.
  • In PyTorch, torch.autograd.gradcheck() automates this process and handles the numerical stability details for you. Always run it on custom autograd functions before trusting them.
Follow-up: Your gradient check passes but your model still does not train. What else could be wrong?Correct gradients are necessary but not sufficient. Other common issues: (1) The learning rate is wrong — too high causes divergence, too low causes imperceptible progress. (2) The loss function does not match the task — using MSE for classification, or forgetting to use logits-based loss for numerical stability. (3) Data preprocessing is incorrect — labels are shuffled, images are not normalized, or the data loader has a bug. (4) The architecture has a bottleneck — an information-destroying layer (like a 2-neuron bottleneck in the middle of a 512-neuron network). The first debugging step should always be overfitting a single batch: if the model cannot drive loss to near-zero on one batch, the problem is in the model or loss, not in generalization.
Strong Answer:
  • Add gate (f=x+yf = x + y): distributes the upstream gradient equally to both inputs (gradient to both is 1). This is why skip connections (residual connections) preserve gradient flow — the addition operation passes gradients through without attenuation.
  • Multiply gate (f=xyf = x \cdot y): swaps and scales gradients. The gradient to xx is yy and vice versa. This means if one input is small, the gradient to the other is small. This is why weight matrices can cause vanishing gradients (weights multiply activations) and why attention mechanisms (which multiply queries by keys) need the 1/dk1/\sqrt{d_k} scaling factor to prevent gradients from becoming too large or too small.
  • Max gate (f=max(x,y)f = \max(x, y)): routes the entire gradient to the larger input and gives zero gradient to the other. This is exactly how ReLU works — gradients flow through active (positive) neurons and are blocked by inactive (negative) ones. This creates the “dying ReLU” problem: if a neuron’s input is always negative, it receives zero gradient and can never recover.
  • These patterns are the building blocks of all neural network architectures. Understanding them lets you predict gradient flow properties of novel architectures without running experiments. For example, when you see a gating mechanism like LSTM’s forget gate (ftct1f_t \odot c_{t-1}), you immediately recognize a multiply gate and know that gradient flow to ct1c_{t-1} will be scaled by ftf_t — which is why the forget gate value must stay close to 1 for long-range dependencies.
Follow-up: Given these patterns, explain why the LSTM cell state update is specifically designed as Ct=ftCt1+itC~tC_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t.The cell state update combines an add gate and two multiply gates. The add gate ensures that some gradient always flows to Ct1C_{t-1} — this is the “gradient highway.” The multiply by ftf_t (forget gate) allows the network to selectively reduce old information, but when ft1f_t \approx 1, gradients pass through nearly unattenuated across hundreds of time steps. This is fundamentally different from vanilla RNNs, where the gradient must pass through a tanh and a weight matrix at every step (multiply gates with typical factors less than 1). The LSTM’s design directly encodes the gradient-preserving add gate as a structural prior, rather than hoping the network will learn to preserve gradients on its own.