Skip to content
learn
  1. Learn
  2. Machine Learning
  3. Neural Networks and Deep Learning Foundations

Machine Learning ML-08.06

Backpropagation

How a network gets the loss gradient for every weight in one backward sweep: computational graphs, local gradients and the chain rule, with real numbers.

Intermediate 20 min Last reviewed

The big idea

Gradient descent needs one number per weight: if I nudge this weight up a little, does the loss go up or down, and how fast? A modern network has millions or billions of weights. Nudging each one separately and re-running the network would take millions of forward passes per training step.

BackpropagationThe algorithm that computes the gradient of the loss with respect to every parameter of a network in one backward sweep, by applying the chain rule from the output back to the inputs. gets all of them in one backward sweep that costs about as much as the forward pass. The trick is bookkeeping. Every operation in the network only needs to know its own small, local derivative. Multiply those local derivatives along the path from the loss back to a weight, and you have that weight's gradient: the chain rule, applied systematically.

Think of a company that missed its target. The CEO tells each division head how much of the miss is on them. Each head splits their share among their managers in proportion to each manager's influence, and so on down. Nobody needs to understand the whole company; each person just passes blame back to the people who fed them, weighted by how much they relied on each. When the message reaches the bottom, everyone knows exactly how their own decisions moved the final number.

How it works

Step 1: The network as a computational graph

Write the forward pass as a computational graphA graph whose nodes are simple operations (add, multiply, activation) and whose edges carry values. Frameworks record it during the forward pass so they can run it backwards.: each node is one simple operation, each edge carries a number. Take the 2-2-1 network from Forward pass by hand with input (1, 2), target t = 1 and cross-entropy loss. Follow the path through hidden neuron 2:

Forward values and backward gradients along one path A chain of nodes left to right: input x2 = 2 with weight w22 = 0.8, then z2 = 1.7, then ReLU giving h2 = 1.7, then z_out = -1.14, then sigmoid giving y = 0.242, then the loss L = 1.417. Cyan arrows on top carry forward values left to right. Glowing pink arrows below carry gradients right to left: dL/dy = -4.127, dL/dz_out = -0.758, dL/dh2 = 0.530, dL/dz2 = 0.530, and finally dL/dw22 = 1.061. -4.127 -0.758 0.530 0.530 1.061 w22 = 0.8 x2 = 2 z2 = 1.7 sum h2 = 1.7 ReLU z = -1.14 x v2 = -0.7 y = 0.242 sigmoid L = 1.417 cross-entropy pink: dL/d(value), computed right to left Figure 1. Forward values (cyan) flow left to right; gradients (pink) flow right to left. Each pink number is the one to its right times one local derivative: -4.127 x 0.184 = -0.758, then x (-0.7) = 0.530, then x 1 (ReLU is active), then x 2 (the input) = 1.061. Press play to watch the two sweeps.

Step 2: Local gradients

Each node knows how its output changes when its input changes, using only its own inputs:

  • A sum z = w x + b: dz/dw = x, dz/dx = w, dz/db = 1.
  • ReLU: slope 1 if z > 0, else 0.
  • Sigmoid: dy/dz = y (1 - y).
  • Cross-entropy with target 1, L = -log y: dL/dy = -1 / y.

These are local gradientsThe derivative of one node's output with respect to its own input, computed from values available at that node alone.. None of them needs to know anything about the rest of the network.

Step 3: The chain rule, backwards

The chain ruleThe calculus rule that the derivative of a composition is the product of the derivatives of its parts: dL/dx = dL/dy times dy/dx. says that if L depends on y and y depends on z, then dL/dz = dL/dy x dy/dz. Start at the loss with dL/dL = 1 and walk backwards, multiplying by one local gradient per step:

Positive: increasing w22 would increase the loss, so gradient descent will decrease it. Check the sign by intuition: the target is 1 but y is only 0.242; h2 pushes the output down through the negative weight v2; so making h2 smaller (by lowering w22) helps.

A useful shortcut: sigmoid followed by cross-entropy simplifies to dL/dz_out = y - t = 0.242 - 1 = -0.758, the same number as -4.127 x 0.184. Libraries use this fused form because it is faster and numerically safer.

Step 4: Reuse is what makes it fast

Notice that dL/dz_out = -0.758 is needed for every parameter upstream: v1, v2, c, and everything in the first layer. Backprop computes it once and passes it back. Each node receives the gradient of the loss with respect to its output (the upstream gradientDuring backpropagation, the gradient of the loss with respect to a node's output, passed back from the nodes that consumed it.), multiplies by its local gradient and sends the result to its inputs. When a value feeds several nodes, the gradients coming back from each are added.

That is why the whole backward pass costs roughly the same as the forward pass (in practice about two times), whatever the number of weights. The naive alternative, nudging each weight and re-running, costs one forward pass per weight.

Step 5: Gradient flow and blocked paths

Gradients can be stopped or shrunk on the way back. Hidden neuron 1 had z = -0.2, so ReLU's local gradient is 0: no gradient reaches w11, w12 or b1 for this input, even though dL/dh1 = -0.909 arrived at it. Long chains of local gradients smaller than 1 shrink the signal (vanishing gradients); larger than 1 grow it (exploding gradients). This is why the choice of activation ( Activation functions ) and the residual connections of Vanishing and exploding gradients, residual connections matter so much.

Step 6: The full update

Collect all nine gradients and take one gradient descent step with learning rate 0.5. In the code the output rises from 0.242 to 0.604 and the loss falls from 1.417 to 0.504. One forward pass, one backward pass, one update: that is a training step, repeated millions of times in real training.

Try it

This interactive needs JavaScript. The diagrams and text above cover the same idea.

Try this:

  1. Press Next through all six stages with the defaults. Which first-layer weights get a gradient of 0, and which stage explains why?
  2. At stage 6, press Next to apply the update. Run the six stages again. How much did the loss drop per update over three or four updates?
  3. Reset and set the target to 0. Which way do the gradients point now? Before stepping, predict the sign of dL/dz_out.

Going deeper

Code: backprop by hand, checked numerically

backprop.pyruns with python3 + numpy
import numpy as np

# The 2-2-1 network from the forward-pass lesson, with target t = 1 and cross-entropy loss
x, t = np.array([1.0, 2.0]), 1.0
W1, b1 = np.array([[0.5, -0.4], [0.3, 0.8]]), np.array([0.1, -0.2])
w2, b2 = np.array([1.2, -0.7]), 0.05

def forward(W1, b1, w2, b2):
    z1 = W1 @ x + b1
    h = np.maximum(0, z1)
    z2 = w2 @ h + b2
    y = 1 / (1 + np.exp(-z2))
    loss = -(t * np.log(y) + (1 - t) * np.log(1 - y))
    return z1, h, z2, y, loss

z1, h, z2, y, loss = forward(W1, b1, w2, b2)
print(f"forward:  z1 {z1}  h {h}  z2 {z2:.3f}  y {y:.4f}  loss {loss:.4f}")

# Backward pass: chain rule, from the loss back to every parameter
dz2 = y - t                       # sigmoid + cross-entropy simplify to this
dw2, db2 = dz2 * h, dz2           # z2 = w2 . h + b2
dh = dz2 * w2                     # how the loss changes with each hidden output
dz1 = dh * (z1 > 0)               # ReLU passes the gradient only where z1 > 0
dW1, db1 = np.outer(dz1, x), dz1  # z1 = W1 x + b1
print(f"dL/dz2 {dz2:.4f}")
print("dL/dw2", np.round(dw2, 4) + 0.0, " dL/db2", round(dz2, 4))
print("dL/dh ", np.round(dh, 4), " dL/dz1", np.round(dz1, 4) + 0.0)
print("dL/dW1\n", np.round(dW1, 4) + 0.0, "\ndL/db1", np.round(db1, 4) + 0.0)

# Gradient check: nudge one weight and measure the loss change numerically
eps = 1e-6
W1p = W1.copy(); W1p[1, 1] += eps
print(f"\nnumerical dL/dW1[1,1] {(forward(W1p, b1, w2, b2)[4] - loss) / eps:.4f}"
      f"   backprop {dW1[1, 1]:.4f}")

# One gradient descent step with learning rate 0.5
lr = 0.5
new = forward(W1 - lr * dW1, b1 - lr * db1, w2 - lr * dw2, b2 - lr * dz2)
print(f"after one step: y {y:.4f} -> {new[3]:.4f}, loss {loss:.4f} -> {new[4]:.4f}")

Output

forward:  z1 [-0.2  1.7]  h [0.  1.7]  z2 -1.140  y 0.2423  loss 1.4175
dL/dz2 -0.7577
dL/dw2 [ 0.     -1.2881]  dL/db2 -0.7577
dL/dh  [-0.9092  0.5304]  dL/dz1 [0.     0.5304]
dL/dW1
 [[0.     0.    ]
 [0.5304 1.0608]] 
dL/db1 [0.     0.5304]

numerical dL/dW1[1,1] 1.0608   backprop 1.0608
after one step: y 0.2423 -> 0.6041, loss 1.4175 -> 0.5039

The gradient check at the end is a standard debugging tool: nudge one weight by a tiny epsilon, measure how much the loss moves, and divide. The numerical estimate (1.0608) matches backprop (1.0608). If you ever implement a layer's backward pass yourself, check it this way before trusting it.

Matrix form

For a layer z = W x + b with upstream gradient g = dL/dz, the three gradients are compact matrix expressions:

In the code, dL/dW1 is the outer product of dL/dz1 = (0, 0.530) and x = (1, 2): the first row is all zero (neuron 1 was inactive) and the second row is (0.530, 1.061). For a batch, the per-example gradients are summed or averaged, which turns these into matrix-matrix products, just like the forward pass.

Automatic differentiation

Nobody writes backward passes by hand for real networks. Automatic differentiationSoftware that records the operations of a computation and applies the chain rule automatically to produce exact gradients. Reverse mode, used for neural networks, is backpropagation. frameworks record the computational graph as your forward code runs, then apply each operation's known local gradient in reverse. That is exactly what loss.backward() does in PyTorch ( Autograd and your first PyTorch training loop ). The price is memory: every intermediate value from the forward pass must be kept until the backward pass uses it, which is why training needs far more memory than inference.

Trade-offs

OptionWins whenLoses when
Backpropagation (reverse mode) Many parameters, one scalar loss: all gradients for about 2x a forward pass Must store forward activations; memory grows with depth and batch
Numerical gradients (finite differences) Checking a gradient implementation on a few weights One extra forward pass per parameter; approximate
Forward-mode differentiation Few inputs, many outputs One pass per input parameter; impractical for networks
Activation checkpointing Very deep or large models that do not fit in memory Recomputes parts of the forward pass, adding compute time

Common mistakes

  • Forgetting to add gradients when one value feeds several nodes (the gradients from each use must be summed).
  • Dropping the ReLU mask, so gradient flows through units that were inactive.
  • Mixing up shapes in the matrix form; dL/dW must have the same shape as W.
  • Not resetting accumulated gradients between steps in frameworks that add into them by default.
  • Trusting a hand-written backward pass without a numerical gradient check.

In the wild

3Blue1Brown

Neural networks series (opens in a new tab)

A series of visual lessons that builds neural networks from the ground up: what a network computes, how gradient descent uses the cost, and what backpropagation is doing, both intuitively and with the calculus written out. It is a good companion to this lesson for seeing how each weight's nudge is decided by the chain of influences from the output back.

Andrej Karpathy

micrograd (opens in a new tab)

A tiny scalar-valued autograd engine, a few hundred lines of Python, that records a dynamic graph of operations and runs backpropagation over it, with a small neural network library and a PyTorch-like API on top. Reading it shows exactly the mechanism described here: each operation stores its inputs and a local backward rule, and calling backward walks the graph in reverse applying the chain rule.

Check yourself

  1. A node computes z = w x with x = 3. The upstream gradient dL/dz is 0.5. What is dL/dw?

    Show answer

    B. 1.5 Local gradient dz/dw = x = 3. Chain rule: 0.5 x 3 = 1.5.

  2. A ReLU unit had input z = -1.3 in the forward pass. Its upstream gradient is 2.0. What gradient does it pass back?

    Show answer

    B. 0 ReLU's slope is 0 for negative inputs, so the gradient is blocked: 2.0 x 0 = 0.

  3. Why is backpropagation so much cheaper than nudging each weight and re-running the network?

    Show answer

    B. It reuses each intermediate gradient for all parameters upstream, so one backward pass gives every gradient Shared gradients like dL/dz_out are computed once and passed back. The total cost is a small multiple of one forward pass, not one pass per weight.

  4. A hidden value h feeds two later nodes, which send back gradients 0.4 and -0.1. What is dL/dh?

    Show answer

    C. 0.3 When a value is used in several places, its gradient is the sum of the gradients from each use: 0.4 + (-0.1) = 0.3.

  5. Why does training use much more memory than inference?

    Show answer

    B. Forward activations must be stored for the backward pass Local gradients need the forward values (like x and z), so they are kept until the backward pass has used them.

Recap

  • Backpropagation computes the loss gradient for every parameter in one backward sweep.
  • Write the network as a computational graph; each node only needs its local gradient.
  • The chain rule multiplies local gradients from the loss back to each parameter; shared paths are computed once.
  • ReLUs with negative input block gradient; long chains can vanish or explode.
  • Autograd frameworks do this automatically, at the cost of storing forward activations.

Get new lessons by email

Get new posts on backend and AI internals. No spam, unsubscribe anytime.

Your email is only used to send new posts. Privacy