The Vanishing/Exploding Gradient Problem
A network that can theoretically remember arbitrarily far back — and, in practice, forgets almost everything after a handful of timesteps. Here's the arithmetic behind why.
Lecture 15's recurrence equation looks elegant on paper. This lecture asks the question a working engineer always has to ask next: does backpropagation actually succeed at training it? For long sequences, the answer is no — and understanding precisely why it fails is what makes Lecture 17's fix make sense, instead of looking like an arbitrary pile of extra gates.
- Express the gradient of a late-timestep loss w.r.t. an early hidden state as a product of Jacobians.
- Explain why that product typically shrinks toward zero as the gap between timesteps grows.
- Reproduce, by hand, a numeric chain-multiplication example that vanishes toward zero within a handful of steps.
- Contrast this with the exploding-gradient case and identify the shared root cause.
- List the standard mitigations, and name the architectural fix this course covers next (LSTM, Lecture 17).
1. Recap: What BPTT Actually Computes
Lecture 15 established that an RNN, once unrolled across \(T\) timesteps, is trained by Backpropagation Through Time (BPTT) — Lecture 8's chain rule, applied to the unrolled graph, with gradients for the shared weights \(W_h, W_x, W_y\) accumulated across every timestep. To see why that accumulation is dangerous, look at just one piece of it: the gradient of the loss at the final timestep \(T\) with respect to a hidden state far in the past, \(h_1\).
Because \(h_t\) depends on \(h_{t-1}\), which depends on \(h_{t-2}\), and so on, the chain rule forces this gradient to be written as a product of many intermediate Jacobian terms — one factor per timestep in between:
$$\frac{\partial L_T}{\partial h_1} = \frac{\partial L_T}{\partial h_T}\,\prod_{t=2}^{T}\frac{\partial h_t}{\partial h_{t-1}} \qquad\text{where}\qquad \frac{\partial h_t}{\partial h_{t-1}} \approx \text{diag}\big(\tanh'(z_t)\big)\,W_h^\top,\quad z_t = W_h h_{t-1}+W_x x_t$$Every one of the \(T-1\) factors in that product is itself a combination of two things: the local derivative of the \(\tanh\) activation, and the weight matrix \(W_h\). Both turn out to matter.
- \(|\tanh'(x)| \le 1\) for every \(x\), with equality only at \(x=0\) — and it is typically much less than 1 once the network has learned anything (saturated tanh units have derivatives near 0).
- Weight matrices are often initialized, and frequently end up trained, with spectral norm (largest singular value) less than 1 — so multiplying by \(W_h^\top\) tends to shrink a vector's norm rather than grow it.
Recall Lecture 15's scalar RNN worked example: \(W_h=0.5\), and the hand-computed hidden states \(h_1=\tanh(0.8)=0.6640\), \(h_2=\tanh(0.732)=0.6244\), using pre-activations \(z_1=0.8\) and \(z_2=0.732\). In the scalar case the Jacobian factor \(\partial h_t/\partial h_{t-1}\) from the formula above collapses to a single number, \(\tanh'(z_t)\cdot W_h\), and the derivative of \(\tanh\) has the closed form \(\tanh'(z)=1-\tanh^2(z)\) (a standard identity — differentiate \(\tanh\) and substitute). So the actual per-timestep factor at \(t=1\) is:
$$\frac{\partial h_1}{\partial h_0}=\tanh'(z_1)\cdot W_h=\big(1-\tanh^2(0.8)\big)\times0.5=(1-0.6640^2)\times0.5=(1-0.4409)\times0.5=0.5591\times0.5\approx\mathbf{0.2796}$$
and at \(t=2\):
$$\frac{\partial h_2}{\partial h_1}=(1-\tanh^2(0.732))\times0.5=(1-0.6244^2)\times0.5=(1-0.3899)\times0.5=0.6101\times0.5\approx\mathbf{0.3051}$$
These are exactly the same kind of "ordinary sub-1 number" — around 0.28-0.31 — as the illustrative factors (0.3, 0.2, 0.5, 0.8, 0.02, 0.1) used in the chain-multiplication example below. They are not a made-up device for the illustration; they are what \(\tanh'(z_t)\cdot W_h\) actually evaluates to for a completely ordinary RNN with completely ordinary weights. That is the mechanistic link: every factor in the vanishing-gradient product below is literally one timestep's \(\tanh'(z_t)\cdot W_h\), computed from the exact same forward-pass equations Lecture 15 introduced.
2. The Vanishing Gradient, Numerically
Multiply many numbers that are each less than 1, and the product shrinks geometrically — not linearly — toward zero. This is exactly what happens to \(\partial L_T/\partial h_1\) as the gap \(T-1\) grows: it isn't that one factor kills the gradient, it's that a chain of ordinary-looking, individually-plausible sub-1 factors compounds into an almost-zero product astonishingly fast.
Here is the instructor's own worked illustration: each number below stands in for one timestep's combined local-gradient magnitude (the product of a \(\tanh'\) term and a weight factor, exactly like the 0.2796 and 0.3051 just computed above) — a perfectly ordinary value, less than 1. Watch what happens after just five multiplications, worked out in full below:
$$0.3 \times 0.2 = \mathbf{0.06}$$
Two ordinary local-gradient factors, both well under 1 — nothing alarming yet.
$$0.06 \times 0.5 = \mathbf{0.03}$$
Already down by roughly 17× from the original factors, after just two multiplications.
$$0.03 \times 0.8 = \mathbf{0.024}$$
Three timesteps back from the loss, and the signal is already faint — even though 0.8 itself is not a small number.
$$0.024 \times 0.02 = \mathbf{0.00048}$$
One small factor (0.02 — think of a saturated \(\tanh\) unit whose derivative is close to zero) and the product collapses by two orders of magnitude in a single step.
$$0.00048 \times 0.1 = \mathbf{0.000048} \approx 0$$
Five multiplications — five perfectly ordinary sub-1 numbers, nothing pathological about any single one of them — and the gradient reaching back to timestep 1 is numerically indistinguishable from zero.
You can replay the same five steps interactively below — useful for testing yourself before moving on:
By the fifth multiplication the running product is \(0.000048 \approx 0\) — for all practical purposes, the gradient signal from timestep 6 back to timestep 1 has vanished. In a real RNN, this means the update to \(W_h\) and \(W_x\) carries essentially no information about how the early part of the sequence should change — the network cannot learn dependencies that span more than a handful of timesteps. It "forgets" the beginning of a long sequence, not because it wants to, but because the gradient that would teach it to remember never arrives.
3. The Exploding Gradient — Same Root Cause, Opposite Symptom
The vanishing case assumed every factor was comfortably below 1. Flip that assumption — let even one factor in the chain be large — and the same repeated-multiplication mechanism produces the opposite failure: the product grows without bound, or flips sign wildly, instead of decaying to zero. Here is the same style of step-by-step running product, this time exploding:
$$0.3 \times 0.2 \times 0.5 \times 0.8 = \mathbf{0.024}$$
Identical to the first three multiplications of the vanishing chain above — nothing different has happened yet.
$$0.024 \times 1.8 = \mathbf{0.0432}$$
Replace the tiny 0.02 factor from before with a single factor greater than 1 (e.g. an under-trained or poorly-initialized weight direction where \(W_h\)'s spectral norm exceeds 1), and the running product jumps back up instead of continuing to shrink.
$$0.0432 \times 1.8 \times 1.8 \times 1.8 = \mathbf{0.252}$$
Left unchecked across more timesteps, a repeated >1 factor compounds geometrically the same way the vanishing chain shrank — just in the opposite direction. Over dozens of timesteps this reaches astronomically large values, producing NaN losses and huge, destabilizing weight updates.
$$0.06 \times (-6.4) = \mathbf{-0.384}$$
A large-magnitude negative factor (starting fresh from the first two factors 0.3 × 0.2 = 0.06) is even more disruptive: after just two multiplications the running product has both exploded in magnitude and flipped sign — the gradient now points the weight update in essentially a random direction rather than one that reduces the loss.
Same idea, click-through form:
Vanishing and exploding gradients are not two unrelated bugs — they are the same root cause (repeated multiplication by a shared weight matrix, once per timestep, across a long chain) manifesting in opposite directions depending on whether the typical factor magnitude is below or above 1. A network can even suffer from both at different points in training, or in different weight directions simultaneously.
4. Consequences for Learning
Practically, vanishing gradients mean training stalls on long-range structure: the loss keeps improving on short-range, local patterns (which get healthy gradient signal) while dependencies spanning dozens of timesteps simply never get learned, because their gradient contribution is numerically indistinguishable from zero. Exploding gradients are more dramatic and easier to notice: weight updates suddenly become enormous, loss spikes to NaN or diverges, and training visibly breaks — which, in a strange way, makes exploding gradients the "easier" of the two problems, since at least you can tell something has gone wrong.
5. Standard Mitigations
None of these fixes are specific to the local course slides — they are standard, widely-used engineering responses to this well-known problem:
Before applying a weight update, rescale the gradient vector if its norm exceeds a threshold: \(g \leftarrow g \cdot \min(1, \tau/\|g\|)\). This directly caps how large an update can be, preventing the exploding case from derailing training — but it does nothing for vanishing gradients, since it only ever shrinks, never amplifies.
Initialization schemes (e.g. orthogonal initialization for recurrent weight matrices) aim to start \(W_h\) with singular values near 1, so that early in training the chain of Jacobians neither shrinks nor grows too aggressively before learning has a chance to correct it.
Since \(\tanh'(x)\) is what caps each factor at 1 and pushes it toward 0 when saturated, some architectures (e.g. IRNN) replace \(\tanh\) with ReLU, whose derivative is exactly 1 for all positive pre-activations — removing one of the two shrinking forces, at the cost of new stability concerns of its own.
The most effective and widely-used solution isn't a training trick at all — it's an architectural change. Lecture 17 introduces the LSTM, which routes the memory signal through a (near-)additive path (the cell state) instead of repeatedly multiplying it by a weight matrix at every single timestep. That one design change is what lets gradients flow across long sequences largely intact.
6. Summary
- BPTT expresses \(\partial L_T/\partial h_1\) as a product of \(T-1\) Jacobian terms, each roughly \(\text{diag}(\tanh'(\cdot))\,W_h^\top\).
- Since \(|\tanh'(\cdot)|\le1\) and weight spectral norms are often <1, this product shrinks geometrically — the vanishing gradient problem — killing long-range learning within just a handful of timesteps (five multiplications of typical sub-1 factors is already enough).
- If instead even one factor is large in magnitude (or large and negative), the product can grow explosively or flip sign wildly instead — the exploding gradient problem, same root cause, opposite symptom.
- Mitigations: gradient clipping (exploding only), careful initialization, ReLU-family activations, and — the architectural fix covered next — gated units (LSTM) with a (near-)additive memory path.
7. Code: Chain Multiplication, Vanishing vs. Exploding
The script below reproduces the vanishing chain from Section 2 numerically, then contrasts it with an exploding chain and a sign-flipping chain.
import numpy as np
def running_products(factors):
"""Print the running product after each multiplication."""
product = 1.0
trail = []
for f in factors:
product *= f
trail.append(product)
print(f" x {f:>6} -> running product = {product:.6f}")
return trail
print("VANISHING chain (typical sub-1 local gradients each step):")
vanishing_factors = [0.3, 0.2, 0.5, 0.8, 0.02, 0.1]
vanish_trail = running_products(vanishing_factors)
print(f"-> after {len(vanishing_factors)} steps, gradient ~ {vanish_trail[-1]:.6f} (essentially 0)\n")
print("EXPLODING chain (same first four factors, then a >1 factor repeats):")
explode_factors = [0.3, 0.2, 0.5, 0.8, 1.8, 1.8, 1.8, 1.8]
explode_trail = running_products(explode_factors)
print(f"-> after {len(explode_factors)} steps, gradient ~ {explode_trail[-1]:.6f} and still growing\n")
print("SIGN-FLIP / explosive chain (one large negative factor early):")
signflip_factors = [0.3, 0.2, -6.4]
signflip_trail = running_products(signflip_factors)
print(f"-> after just {len(signflip_factors)} steps: {signflip_trail[-1]:.6f}"
" -- large magnitude AND flipped sign\n")
# Same root cause every time: repeated multiplication by a shared weight
# matrix (here, scalar factors standing in for it) across many timesteps.
# Vanishing: |factor| < 1 typically. Exploding: some |factor| > 1.
⬇ Download lecture-16-vanishing-gradient.py More resources for this lecture →