Vanishing and exploding gradients
Why deep networks were hard to train.
Create a free account Try part of it here
Why it matters
For decades, networks deeper than a few layers wouldn't train, and nobody could fully say why. The answer is in the chain rule. Backpropagation multiplies the gradient by one factor per layer. Multiply many numbers a bit below 1, and the result heads to zero. Multiply many a bit above 1, and it heads to infinity. Every major trick in this phase (ReLU, careful starting weights, normalization, residual connections) exists to keep that product near 1. The same problem shapes how large language models are built today.
What you'll be able to do
- Explain why gradients shrink or grow exponentially with depth
- Measure gradient size layer by layer in a real network
- Connect the fixes: activations, initialization, normalization, residual connections
- Use gradient clipping against exploding gradients
What it covers
- A whisper down a long line
- Where each factor comes from
- Measuring it in a real network
- Five fixes, one goal
- Exploding gradients and clipping
- Worked example: clipping a gradient by hand
- Recurrent networks: the same problem over time
A whisper down a long line
Picture a long line of people passing a message along. Each person repeats it to the next, but at half the volume they heard. After 20 repeats, the message is about a millionth as loud as it started. Nobody can hear it.
Now flip it. Each person repeats the message 1.5 times louder. After 20 repeats it's over 3,000 times as loud. That's a scream, and the words are lost again. Only a factor of exactly 1 keeps the message at the volume it started.
Backpropagation has the same problem. You saw how it works in the last lessons. The gradient starts at the loss and travels backward, one layer at a time. Each layer multiplies it by its own factor on the way through. That factor is the layer's local slope, from the chain rule.
Here it is written out for a network with layers. is the loss. is the output of layer 1, and is the output of the top layer. A symbol like is a gradient. It says how much the loss changes when you nudge .
In plain words: the gradient that reaches layer 1 is the gradient at the top, times one factor for every layer in between. Multiply many factors together, and it matters a lot whether they sit below 1 or above 1.
Here's a small case. Three layers each multiply the gradient by 0.5. The gradient at layer 1 is 0.5 × 0.5 × 0.5 = 0.125 times the gradient at the top. That's already eight times smaller.
With 20 layers it's , about one millionth. This is called a vanishing gradient. With a factor of 1.5 instead, 20 layers give . This is called an exploding gradient. Here is the same calculation in Python, for three different factors:
for factor in [0.5, 1.0, 1.5]: grad = 1.0 # gradient at the top layer for layer in range(20): grad = grad * factor # one factor per layer print(factor, f"{grad:.1e}")
The numbers print in short form. 9.5e-07 means 9.5 with the decimal point moved 7 places to the left: 0.00000095. 3.3e+03 means 3.3 with the point moved 3 places to the right: 3,300. A factor of exactly 1 leaves the gradient at 1.0e+00, which is plain 1.
Sigmoid's slope is at most 0.25, so with 16 layers the blame shrinks to almost nothing. Try 1, then 1.2.
Where each factor comes from
Each layer's factor mixes two things:
- The activation's slope. Sigmoid's slope is never more than 0.25. For large inputs, it's far less.
- The size of the weights. Big weights scale the gradient up. Small weights scale it down.
So unless the weights make up for it, each sigmoid layer cuts the gradient to a quarter or less. Stack ten of them and almost nothing gets through.
Note: This picture is a simplification. In a real network, each factor is a whole table of numbers (a matrix), not one number. The single-number version still shows the main effect.
Go deeper Why sigmoid's slope tops out at 0.25
The sigmoid squashes any input into a number between 0 and 1. Write its output as , said "sigma of x". Its slope at is .
In plain words: take the output, and multiply it by one minus the output.
That product is largest when the output is 0.5, which happens at . Then the slope is 0.5 × 0.5 = 0.25. Far from zero, the output gets close to 0 or 1, and the slope gets close to 0. At , for example, the output is about 0.982 and the slope is about 0.018.
import math for x in [0, 2, 4]: s = 1 / (1 + math.exp(-x)) print(x, round(s * (1 - s), 3))
This prints a slope of 0.25 at 0, 0.105 at 2, and 0.018 at 4.
print(f"{0.25 ** 10:.1e}")
Show the answer
9.5e-07
0.25 multiplied by itself 10 times is about 0.00000095, under one millionth, which prints as 9.5e-07. It's the same as 0.5 multiplied 20 times, because 0.25 = 0.5 × 0.5.
Measuring it in a real network
Now watch it happen in PyTorch. The code below builds four networks, each 20 layers deep. They differ only in the activation and in how the starting weights are chosen:
- sigmoid
- tanh
- ReLU with PyTorch's default starting weights
- ReLU with He initialization, which sizes the starting weights to suit ReLU (the weight initialization lesson shows where that size comes from)
Each network looks at the same 256 small images of handwritten digits. The code runs the backward pass once. Then it measures the size of each layer's weight gradient.
"Size" here means the gradient's norm. It's one number for how big a whole list of numbers is. Square every number, add them up, and take the square root. Think of it as the length of an arrow.
The chart uses a log scale. Each step up the side means 10 times bigger. A flat line means every layer gets a gradient of about the same size.
import numpy as np import torch import torch.nn as nn import matplotlib.pyplot as plt from sklearn.datasets import load_digits digits = load_digits() X = torch.tensor(digits.data[:256] / 16.0, dtype=torch.float32) y = torch.tensor(digits.target[:256]) def deep_net(act, he_init=False, depth=20): layers = [] for _ in range(depth): lin = nn.Linear(64, 64) if he_init: nn.init.kaiming_normal_(lin.weight, nonlinearity="relu") nn.init.zeros_(lin.bias) layers += [lin, act()] return nn.Sequential(*layers, nn.Linear(64, 10)) for name, act, he in [("sigmoid", nn.Sigmoid, False), ("tanh", nn.Tanh, False), ("ReLU, default init", nn.ReLU, False), ("ReLU, He init", nn.ReLU, True)]: torch.manual_seed(0) model = deep_net(act, he) nn.CrossEntropyLoss()(model(X), y).backward() norms = [m.weight.grad.norm().item() for m in model if isinstance(m, nn.Linear)] print(f"{name:<19} layer 1: {norms[0]:.1e} layer 10: {norms[9]:.1e} last: {norms[-1]:.1e}") plt.semilogy(range(1, len(norms) + 1), norms, marker="o", markersize=3, label=name) plt.xlabel("layer") plt.ylabel("weight gradient size (log scale)") plt.legend()
Read the printed numbers the same way as before. For example, 1.0e-05 means 0.00001.
With sigmoid, the first layer's gradient is about times smaller than the last layer's. That's a 1 followed by 17 zeros. The early layers barely learn at all. Tanh, and ReLU with PyTorch's default starting weights, do better. They still lose several factors of 10 on the way down. ReLU with He initialization keeps every layer's gradient on about the same scale. (The torch here is the course's in-browser PyTorch subset.)
Show the answer
About 800× smaller (0.8³⁰ ≈ 0.0012)
0.8 multiplied by itself 30 times is about 0.0012. One divided by 0.0012 is roughly 800. A factor only a little below 1 still adds up over 30 layers.
Five fixes, one goal
Every fix in this table works on the same product of factors. Each one keeps the factors close to 1.
| Fix | How it keeps the factors near 1 |
|---|---|
| ReLU (and GELU) | slope exactly 1 for active units, instead of sigmoid's 0.25 or less |
| Careful initialization | starting weights sized so signals neither grow nor shrink per layer |
| Normalization layers | re-scale activations at every layer, whatever happened before |
| Residual connections | add a shortcut path, so each factor is "1 + something" (next lesson) |
| Gradient clipping | caps the total size of a step when gradients do explode |
A few of these need a word of explanation:
- An active unit is a ReLU neuron whose input is above zero. Its output equals its input, so its slope is 1. It passes the gradient back unchanged.
- A normalization layer works like a volume knob that resets the level at every layer. It has its own lesson.
- A residual connection adds a layer's input straight onto its output. The gradient then has two routes back: through the layer, and around it. The route around it has a factor of exactly 1. So even if the layer's own part is small, say 0.1, the total factor is 1.1, close to 1. Without the shortcut it would be 0.1. (That's a simplified picture. The next lesson covers it properly.)
Modern large language models use all five at once. They have GELU or SwiGLU activations (close relatives of ReLU), scaled initialization, and a normalization layer in every block. They also have residual connections around every block, and they clip gradients during training.
Show the answer
1
For inputs above zero, ReLU's output equals its input, so its slope is 1. The gradient passes back unchanged. (For inputs below zero, the slope is 0, and that unit passes no gradient back.)
Exploding gradients and clipping
Exploding gradients show up as sudden spikes in the loss. They can also show up as nan, short for "not a number". That's what you get when a calculation breaks down, often after numbers grow too large to store.
Fixing the network's design helps. The standard safety net on top is gradient clipping. Think of a speed limiter on a car. You can still steer anywhere, but you can't go faster than the limit.
Here's how it works. Measure the combined size (the norm) of all the gradients together. If it's bigger than a threshold you choose, called max_norm, scale every gradient down by the same amount. Afterward, the combined size equals the threshold. The direction stays the same. Only the length of the step is capped.
The combined size uses the norm from earlier. Write the gradient numbers as , , and so on. The last one is , where is how many numbers there are.
In plain words: square every gradient number, add them all up, and take the square root. If that total is above
max_norm, multiply every gradient bymax_norm÷ total.
It's the same rule as measuring a walk on a map. Go 6 blocks east and 8 blocks north, and you end up 10 blocks away in a straight line. The worked example below clips exactly that gradient.
In PyTorch, clipping is one line, between the backward pass and the optimizer step:
loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # typical LLM setting optimizer.step()
Worked example: clipping a gradient by hand
A tiny model has two weights. Their gradients are 6 and 8. The clipping threshold, max_norm, is 2. Find the combined size of the gradients, then the clipped gradients.
import math grads = [6.0, 8.0] max_norm = 2.0 total = math.sqrt(sum(g * g for g in grads)) print("total:", total) if total > max_norm: grads = [g * max_norm / total for g in grads] print("clipped:", grads) print("new total:", math.sqrt(sum(g * g for g in grads)))
The squares are 6 × 6 = 36 and 8 × 8 = 64. They add up to 100, and the square root of 100 is
- That's above the threshold of 2. So every gradient is multiplied by 2 ÷ 10 = 0.2, and the
gradients become 1.2 and 1.6. Their combined size is now exactly 2.
Notice that 1.2 to 1.6 is the same ratio as 6 to 8. Both are 3 to 4. So the step still points the same way. It's only shorter.
grads = [9.0, 12.0] max_norm = 5.0 total = (9.0**2 + 12.0**2) ** 0.5 print(total, [g * max_norm / total for g in grads])
Show the answer
15.0 [3.0, 4.0]
81 + 144 = 225, and the square root of 225 is 15.0. That's above 5, so each gradient is multiplied by 5 ÷ 15. So 9 becomes 3.0, and 12 becomes 4.0.
Recurrent networks: the same problem over time
A recurrent network reads a sequence one step at a time, such as one word at a time. It applies the same layer at every step. So a sequence of 100 words acts like a 100-layer network, where every layer shares the same weights.
The gradient from the end of a long sequence has to pass back through all those steps. It vanishes before it reaches the beginning. That's why plain recurrent networks (RNNs) struggled to remember anything far back. It's also why gated versions (LSTMs) were invented, and later attention. The recurrent networks lesson covers them.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Why did deep networks with sigmoid activations train so poorly?