Index Ai Academy
Phase 4 · Deep learning · Training

Vanishing and exploding gradients

Why deep networks were hard to train.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. A whisper down a long line
  2. Where each factor comes from
  3. Measuring it in a real network
  4. Five fixes, one goal
  5. Exploding gradients and clipping
  6. Worked example: clipping a gradient by hand
  7. 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 nn layers. LL is the loss. h1h_1 is the output of layer 1, and hnh_n is the output of the top layer. A symbol like ∂L∂h1\frac{\partial L}{\partial h_1} is a gradient. It says how much the loss changes when you nudge h1h_1.

∂L∂h1=∂L∂hn⋅∂hn∂hn−1⋯∂h2∂h1\frac{\partial L}{\partial h_1} = \frac{\partial L}{\partial h_{n}} \cdot \frac{\partial h_{n}}{\partial h_{n-1}} \cdots \frac{\partial h_2}{\partial h_1}

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 0.520≈10−60.5^{20} \approx 10^{-6}, about one millionth. This is called a vanishing gradient. With a factor of 1.5 instead, 20 layers give 1.520≈3,3001.5^{20} \approx 3{,}300. 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}")
It runs right here in your browser, with nothing to install.

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.

Try it now

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 xx into a number between 0 and 1. Write its output as σ(x)\sigma(x), said "sigma of x". Its slope at xx is σ(x)(1−σ(x))\sigma(x)(1 - \sigma(x)).

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 x=0x = 0. 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 x=4x = 4, 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.

Quick check
Ten sigmoid layers each multiply the gradient by 0.25, sigmoid's largest slope. Predict what this prints.
print(f"{0.25 ** 10:.1e}")
  1. 1.0e-10
  2. 9.5e-07
  3. 2.5e+00
  4. 2.5e-01
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 101710^{17} 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.)

Quick check
Each of 30 layers multiplies the backward gradient by about 0.8. Roughly how much smaller is the gradient at layer 1 than at the top?
  1. About 0.8× smaller
  2. About 800× smaller (0.8³⁰ ≈ 0.0012)
  3. About 24× smaller
  4. The same size
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.

FixHow it keeps the factors near 1
ReLU (and GELU)slope exactly 1 for active units, instead of sigmoid's 0.25 or less
Careful initializationstarting weights sized so signals neither grow nor shrink per layer
Normalization layersre-scale activations at every layer, whatever happened before
Residual connectionsadd a shortcut path, so each factor is "1 + something" (next lesson)
Gradient clippingcaps 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.

Quick check
A ReLU unit receives an input of 2.3. By what factor does it multiply the gradient passing back through it?
  1. 0.25
  2. 2.3
  3. 1
  4. 0
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 g1g_1, g2g_2, and so on. The last one is gkg_k, where kk is how many numbers there are.

total=g12+g22+⋯+gk2\text{total} = \sqrt{g_1^2 + g_2^2 + \dots + g_k^2}

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 by max_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

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)))
In the lesson you can change this and run it.

The squares are 6 × 6 = 36 and 8 × 8 = 64. They add up to 100, and the square root of 100 is

  1. 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.

Quick check
Predict what this prints.
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])
  1. 15.0 [9.0, 12.0]
  2. 21.0 [9.0, 12.0]
  3. 15.0 [5.0, 5.0]
  4. 15.0 [3.0, 4.0]
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

Create a free account

Try a quiz question

Why did deep networks with sigmoid activations train so poorly?

  1. The sigmoid's slope is at most 0.25, so gradients shrink by a large factor at every layer
  2. Sigmoids can't output negative numbers, so half of the weights could never be pushed down at all
  3. Sigmoids are too slow to compute, which limited how many training steps anyone could afford to run
  4. Sigmoids cause overfitting, because their smooth curve lets a network fit every point in its data

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson