← All tutorials

Backpropagation and Gradient Checking

How a network works out every weight’s share of the error, and how to check it.

On this page

1 One training step, four moves 2 Nudging a weight measures its slope 3 Slopes multiply along a path and add where paths meet 4 One backward pass through a small network 5 Autograd records the graph and walks it back 6 Long chains shrink or blow up the gradient 7 Checking backprop against the nudge 8 Common backprop bugs and their symptoms 9 References

Gradient descent moves every weight against the slope of the loss, so it needs one slope per weight. Backpropagation gets all of them in one backward pass. It applies the chain rule from the loss back to the input and reuses the values the forward pass stored. Gradient checking compares that answer with a slow nudge-and-measure estimate.

Everything here runs on the small network of Multilayer Perceptrons, on its blue point x = (−2, 2) with label y = 1. The input x goes through the first layer’s weights W1 and biases b1 to the hidden sums z. ReLU turns them into the hidden values h. The second layer’s weights W2 and bias b2 give the output score s, and the sigmoid turns s into the probability ŷ.

1 One training step, four moves

Every training loop repeats the same four moves.

forward stores z, h, s, ŷ 1 loss L from ŷ and y 2 backward every weight’s slope 3 update gradient descent 4
Figure 1. Forward 1, loss 2, backward 3, update 4, then again.

The backward pass runs once per training step, after the forward pass and the loss and before the optimizer step. It costs about twice the forward pass. For a language model, Scaling Laws counts about 6 FLOPs (floating-point operations) per parameter for each training token. Two are in the forward pass and four in the backward pass. Gradient checking runs once, when you write a new layer or loss, never inside the loop.

2 Nudging a weight measures its slope

A weight’s slope can be measured directly: nudge the weight and watch the loss. The second layer has weights W2 = (0.5, 0.5); nudge its first entry.

W2 entry 1loss Lslope, change over 0.001
0.50.38687−0.641
0.5010.38623
backprop, Section 4.1−0.642
Nudge one weight, watch the loss, divide by the nudge.
∂L∂w ≈ L(w + ε) − L(w)ε
The slope of the loss along one weight. w is any one weight and ε (epsilon) a small nudge to it. The top is how much the loss L changed; dividing by ε gives the change per unit of w. ∂ (a partial derivative) means every other weight stays fixed, and ≈ means the nudge only approximates the slope, closer as ε shrinks. The list of these slopes, one per weight, is the gradient, as in Linear Regression. Section 7 makes the nudge symmetric.

This network has 9 weights and biases: 4 in W1, 2 in b1, 2 in W2 and 1 in b2. Nudging needs 9 extra forward passes, or 18 for the two-sided check of Section 7. Backpropagation gets all 9 slopes in one backward pass that costs about two forward passes. A model with billions of weights could never be trained by nudging.

3 Slopes multiply along a path and add where paths meet

The network’s last two steps turn the score s into the probability ŷ with the sigmoid σ, then ŷ into the loss, −log ŷ for label 1.

stepat s = 0.75
the sigmoid gives ŷ0.679
slope of the loss along ŷ, which is −1 over ŷ−1.472
slope of the sigmoid along s, which is ŷ times 1 minus ŷ0.218
their product−0.321, exactly the probability minus the label

A value that feeds the loss along several paths gets the sum of their gradients.

one value, several pathsits gradient
shared layers under two task outputs (Multitask Learning)(2, 0) and (−1, 0.5) add to (1, 0.5), illustrative
a residual step, which adds a block F’s output to the block’s own input, with F′ the slope of F (The Transformer Block)1 plus F′, so 1.2 when F′ is 0.2, illustrative
an RNN’s weight U, shared by every time step (RNN Basics)one term per step; 0.3, 0.1 and 0.05 add to 0.45, illustrative
Through two steps, multiply the local slopes; along two paths, add.
∂L∂s = ∂L∂ŷ ∂ŷ∂s
The loss reacts to the score s through the probability ŷ. The first factor is how the loss reacts to ŷ, the second how the sigmoid reacts to s. Each is the local slope of one step, and their product is the slope of the two steps together, −0.321 in the table. Multiply it by h1 = 2, the slope of s along W2 entry 1, and you get backprop’s −0.642, which the nudge of Section 2 measured as −0.641.
∂L∂w = g1 + g2
When w reaches the loss along two paths, its gradient is the sum of what comes back along each. g1 and g2 are the gradients back along path 1 and path 2; w can be a whole vector of shared weights. Forgetting to clear old gradients makes PyTorch add across steps the same way (Section 8).

This is not the chain rule of Probability and Bayes Rule, which multiplies probabilities, not slopes.

4 One backward pass through a small network

For every value the forward pass stored, the backward pass computes an error, named δ (delta). δs is the slope of the loss L along s.

forward, stored backward x = (−2, 2) δx (0.160, −0.160) z = (2, −6) δz (−0.160, 0) h = (2, 0) δh (−0.160, −0.160) s = 0.75 δs −0.321 ŷ = 0.679 δŷ −1.472 L = 0.387 W1 x + b1 ReLU W2 h + b2 sigmoid −log ŷ start here
Figure 2. Left, the stored forward pass. Right, each value’s δ.

4.1 Each layer sends the error back with two products

Each layer sends the error back through its weights and its local slopes.
δz = (W2T δs) ⊙ 1[z > 0]
δz is the error at the hidden sums z. W2 is a row of two weights; the superscript T is the transpose, which turns it into a column, so W2T δs sends the one output error back to both hidden units, each scaled by its own weight. This is the δh of Figure 2, −0.160 in both entries. 1[z > 0] is ReLU’s slope: 1 where a unit fired and 0 where it did not. On the blue point it is (1, 0), and this pair is called the mask. ⊙ multiplies entry by entry, so the mask zeroes the error of every unit that did not fire. This is the chain rule of Section 3 along every path at once.
∂L∂W1 = δz xT
xT is the input written as a row, so δzxT is an outer product: a column times a row gives a grid, with row j equal to unit j’s error times the input. That is one entry per weight of W1, and its top-left entry is the MLP page’s one-weight formula. The same kind of product with W1T gives the input’s error δx. Each layer does two products backward, the error for the layer below and its own weight gradient, each as big as its forward multiply, so the backward pass costs about twice the forward (Section 1).
gradientfrom the errors of Figure 2on the blue point
W2δs times h(−0.642, 0)
b2δs itself−0.321
W1δz times x as a rowrow 1 (0.321, −0.321), row 2 zeros
b1δz itself(−0.160, 0)
Drag the point, then step the error back.

Figure 3. Line width is a weight’s gradient, ring size a value’s error δ.

5 Autograd records the graph and walks it back

PyTorch’s autograd records every operation on a tensor marked requires_grad during the forward pass, then walks that record backward. On the blue point it prints the gradients of Section 4.1.

import torch
x = torch.tensor([-2.0, 2.0])  # the blue point, label 1
W1 = torch.tensor([[-1., 1.], [1., -1.]], requires_grad=True)
b1 = torch.tensor([-2.0, -2.0], requires_grad=True)
W2 = torch.tensor([0.5, 0.5], requires_grad=True)
b2 = torch.tensor(-0.25, requires_grad=True)
s = W2 @ torch.relu(W1 @ x + b1) + b2  # the score
loss = torch.nn.functional.softplus(-s)  # BCE for label 1, 0.3869
loss.backward()
print(W1.grad, b1.grad, W2.grad, b2.grad)
# W1.grad [[0.3208, -0.3208], [0, 0]]  b1.grad [-0.1604, 0]
# W2.grad [-0.6416, 0]  b2.grad -0.3208
  1. 1The network’s weights and biases, with requires_grad=True so autograd records every operation on them.
  2. 2The forward pass stores h and s. softplus is a smooth version of ReLU, the log of 1 plus e to its input. softplus(-s) is the same loss as −log ŷ, worked out from the score so it never takes the log of 0 (Section 8). backward walks the record back. This runs once every training step.
  3. 3The four gradients, the numbers of the table in Section 4.1.

Two controls turn the record off. This snippet reuses the tensors of the autograd snippet.

target = loss.detach()
with torch.no_grad():  # nothing recorded
    y_val = torch.sigmoid(W2 @ torch.relu(W1 @ x + b1) + b2)
print(loss.requires_grad, target.requires_grad, y_val.requires_grad)
# True False False
  1. 1detach gives the same value with no record behind it, so no gradient flows back through it: a fixed target, as in GradNorm.
  2. 2no_grad records nothing inside its block, for evaluation.
  3. 3Only the original loss can still be walked back.

The values the forward pass stores are the activation memory. Checkpointing (Chen et al., 2016) stores fewer and recomputes the rest during the backward pass; CLIP uses it to fit its huge batches.

6 Long chains shrink or blow up the gradient

The walk of Section 4.1 multiplies one local slope per step, so a long chain multiplies many.

slope per step aafter n = 27 steps back
0.90, the default of RNN Basics, section 2.1, whose 28 time steps are 27 steps back to step 10.058, about 6%
1.1, an illustrative slope (Figure 4 can set it)13.1
Through many steps, the error is multiplied by one slope per step.
δin = a1 a2 ⋯ an δout
δout is the error at the loss end of a chain of n steps (layers, or time steps in an RNN), and δin what reaches the first step. ai is step i’s local slope. For a layer with several units it is a matrix, its weights times its slopes, and the product works the same way. This is the walk of Section 4.1 along a long path.
δin ≈ an δout
If every step’s slope is about the same a, the product is a multiplied by itself n times. Slightly below 1 it shrinks toward 0 (vanishing); slightly above 1 it grows without limit (exploding).

Figure 4. The error left after each step back, on a log scale.

Each fix keeps the factors near 1 or gives the gradient a path around them.

fixwhat it does to the product
ReLU (MLP, section 2)slope exactly 1 where a unit fires, while the sigmoid’s slope is at most 0.25
variance-keeping init (Glorot and Bengio, 2010; He et al., 2015)sets each layer’s starting weights so its outputs keep their variance, and in the 2010 scheme its gradients too, so the factors start near 1
residual connections (ResNet)the skip adds 1 to each layer’s slope, so one path skips every layer’s factor
gating (LSTM)the cell state is updated by adding; the factor along it is the forget gate, a learned number between 0 and 1 for how much of the old cell state to keep, which can stay near 1
normalization (Normalization Layers)keeps each layer’s values on one scale, so the factors stay near 1
clipping (Optimizers in Practice)caps the gradient’s size, for the exploding end only

7 Checking backprop against the nudge

Section 2’s one-sided nudge read −0.641 where backprop gives −0.642. Nudging both ways closes most of that gap, and a relative score says when two slopes agree.

Nudge both ways, then compare by relative size.
gnum = L(w + ε) − L(w − ε)2ε
gnum is the numerical slope from the central difference: nudge w up and down by ε and divide the change by the 2ε between them. The one-sided nudge of Section 2 errs in proportion to ε; here the two errors cancel, and what is left shrinks like ε2, far smaller. Run it in float64, double-precision numbers with about 16 significant digits, as PyTorch’s gradcheck expects; float32’s rounding swamps a small ε.
r = |gbp − gnum|max(|gbp|, |gnum|)
gbp is backprop’s slope and r the relative error. The bars mean size, ignoring sign, and max picks the larger of the two. Dividing by the larger size makes r relative to the gradient itself, so a tiny gradient and a huge one are judged alike.

The table runs both nudges on W1 entry (1, 1), whose backprop slope is 0.320821.

W1 entry (1, 1), float64one-sided rcentral r
ε = 10−23.4 × 10−34.1 × 10−6
ε = 10−43.4 × 10−54.0 × 10−10

PyTorch’s torch.autograd.gradcheck nudges by ε = 10−6 in float64 and passes an entry when the gap between the two slopes is small. On W1 entry (1, 1), whose slope is 0.32, a gap up to about 0.00033 passes.

A gap passes when it is under a small floor plus a fixed share of the slope.
|gbp − gnum| ≤ atol + rtol |gnum|
The left side is the gap between backprop’s slope and the nudge’s. atol, the absolute tolerance, is a floor of 10−5 that forgives rounding when the slope is near 0. rtol, the relative tolerance, is 10−3: the gap may also grow by a thousandth of the slope’s size |gnum|. At a slope of 0.32 the two add to about 0.00033.
|gbp − gnum||gnum| ≤ rtol + atol|gnum|
Dividing both sides by the slope’s size turns the left side into nearly the relative error r of the card above, which divides by the larger of the two sizes. So r must stay under rtol plus atol over the slope, about 1.03 × 10−3 at 0.32. For slopes much bigger than 0.01 the atol part fades and the limit is about 10−3.

Both central rows of the table pass with room. A bug looks very different: a dropped factor of 2 gives r = 0.5, and a wrong sign gives r = 2.

Turn off randomness first, such as dropout, which zeroes random units during training (Regularization, section 1.1). Stay away from ReLU’s kink at 0, where the nudge sees a slope backprop does not; Figure 3 shows the kink. The same check runs in code on the same weight.

import torch
f64 = dict(dtype=torch.float64)
x, b1 = torch.tensor([-2., 2.], **f64), torch.tensor([-2., -2.], **f64)
W2, b2 = torch.tensor([0.5, 0.5], **f64), torch.tensor(-0.25, **f64)
def L(W1):  # the loss as a function of W1 alone
    return torch.nn.functional.softplus(-(W2 @ torch.relu(W1 @ x + b1) + b2))
W1 = torch.tensor([[-1., 1.], [1., -1.]], **f64, requires_grad=True)
L(W1).backward()
eps, E = 1e-4, torch.tensor([[1., 0.], [0., 0.]], **f64)  # E picks entry (1, 1)
g_num = (L(W1.detach() + eps * E) - L(W1.detach() - eps * E)) / (2 * eps)
print(f'{W1.grad[0, 0]:.6f} {g_num:.6f}', torch.autograd.gradcheck(L, (W1,)))
# 0.320821 0.320821 True
  1. 1The same network and stable loss in float64, with the loss written as a function of W1 only.
  2. 2Backprop’s slope for W1 entry (1, 1), then the central difference at ε = 10−4. Run once when you write a new layer or loss, never inside the training loop.
  3. 3The two slopes agree to six decimals. PyTorch’s own check nudges every entry and prints True when all pass.

8 Common backprop bugs and their symptoms

bugwhat you seefix
no zero_gradgradients of several steps add up, the way two paths add in Section 3call zero_grad() at the top of each step
an in-place change, such as adding 1 to h by overwriting it, on a value autograd storedan error at backward: a needed value was modifiedadd into a new value instead of overwriting h
.item() or .numpy() inside the lossan error, or a cut graph: the weights before it get no gradientkeep the loss in tensors
a dead ReLUa unit whose sum stays below 0 never gets a gradient againMLP training failures
log of 0the loss is inf, then the gradients and every weight turn NaN (not a number)compute the loss from the score s, not from ŷ

The next page, CTC Loss, builds a loss for speech and handwriting, where the labels are not lined up with the input frames, and backpropagation trains through it the same way.

9 References