Roadmap / Theory

Backpropagation by Hand

Derive and code gradients yourself, then prove them right with gradient checking.

A network learns by adjusting its parameters: the numbers inside it, such as the entries of its weight matrices. Training repeats one step millions of times: measure how wrong the network is (the loss LL, a single number), then nudge every parameter a little in the direction that makes LL smaller. To know that direction, we need a derivative for every parameter. This page explains what those derivatives are and how backpropagation computes all of them at once.

Derivatives and gradients

The derivative of the loss with respect to one parameter θ\theta (theta) answers one question: if I change θ\theta by a tiny amount, how much does LL change, and in which direction? It is written ∂L/∂θ\partial L / \partial \theta.

Example. Take L=θ2L = \theta^2 at θ=3\theta = 3, so L=9L = 9. Move θ\theta to 3.0013.001: now L=9.006001L = 9.006001. The loss grew by about 0.0060.006 for a step of 0.0010.001, a ratio of about 66. That ratio is the derivative: ∂L/∂θ=2θ=6\partial L / \partial \theta = 2\theta = 6.

A positive derivative means "increasing θ\theta increases the loss", so training decreases θ\theta. With a step size of 0.10.1: θ←3−0.1⋅6=2.4\theta \leftarrow 3 - 0.1 \cdot 6 = 2.4, and the loss drops from 99 to 5.765.76.

The gradient is the list of these derivatives, one per parameter. A small LLM (large language model) has millions of parameters, so we need millions of derivatives, at every training step, cheaply. Frameworks such as PyTorch compute them automatically, and every one of them uses the same idea, which you will code in this section.

The chain rule

A network is a long chain of simple operations: add, multiply, matrix product, tanh, and so on. Each operation takes the result of earlier ones. Drawn as a diagram, with one box per operation and arrows from inputs to outputs, this is the computational graph. The loss LL is the last box.

Suppose LL depends on xx only through an intermediate value y=f(x)y = f(x). The chain rule says:

∂L∂x=∂L∂y⋅∂y∂x\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial x}

where:

  • ∂L/∂y\partial L / \partial y is the upstream gradient: how the loss reacts to yy. It is computed first, coming back from the end of the graph;
  • ∂y/∂x\partial y / \partial x is the local gradient: how yy reacts to xx. It depends only on the operation ff itself.

In words: how xx affects the loss = how xx affects yy, times how yy affects the loss.

Example. Take y=3xy = 3x and L=y2L = y^2, at x=1x = 1 (so y=3y = 3 and L=9L = 9).

  1. Upstream: L=y2L = y^2, so ∂L/∂y=2y=6\partial L / \partial y = 2y = 6.
  2. Local: y=3xy = 3x, so ∂y/∂x=3\partial y / \partial x = 3.
  3. Chain rule: ∂L/∂x=6⋅3=18\partial L / \partial x = 6 \cdot 3 = 18.

Check directly: L=(3x)2=9x2L = (3x)^2 = 9x^2, whose derivative 18x18x is 1818 at x=1x = 1.

So each operation needs just one rule: "given the upstream gradient, produce the gradient for my inputs". It never needs to know what the rest of the network looks like. A few rules cover most of a network. In the table, gg is the upstream gradient ∂L/∂y\partial L / \partial y:

Forward Backward
y=a+by = a + b ∂L/∂a=g\partial L / \partial a = g, ∂L/∂b=g\partial L / \partial b = g
y=a⋅by = a \cdot b ∂L/∂a=g b\partial L / \partial a = g\, b, ∂L/∂b=g a\partial L / \partial b = g\, a
y=tanh⁡(x)y = \tanh(x) ∂L/∂x=g (1−y2)\partial L / \partial x = g\, (1 - y^2)
y=max⁡(0,x)y = \max(0, x) ∂L/∂x=g\partial L / \partial x = g where x>0x > 0, else 00

How to read the rows:

  • addition passes the gradient unchanged to both inputs;
  • multiplication sends each input the gradient times the other input;
  • tanh multiplies the gradient by 1−y21 - y^2, using its own output yy;
  • max⁡(0,x)\max(0, x), called ReLU (rectified linear unit), lets the gradient through only where the input was positive.

Backpropagation, step by step

Backpropagation applies these rules from the end of the graph to the start: first the forward pass computes every value, then the backward pass computes every gradient, in the opposite order.

Example. c=a⋅b+ac = a \cdot b + a with a=2a = 2 and b=−3b = -3.

Forward. d=a⋅b=−6d = a \cdot b = -6, then c=d+a=−4c = d + a = -4.

Backward, starting from ∂c/∂c=1\partial c / \partial c = 1:

  1. c=d+ac = d + a is an addition: it passes 11 to dd and 11 to aa. So far ∂c/∂d=1\partial c / \partial d = 1 and aa has received 11.
  2. d=a⋅bd = a \cdot b is a multiplication with upstream gradient 11: aa receives 1⋅b=−31 \cdot b = -3, and bb receives 1⋅a=21 \cdot a = 2.
  3. aa was used twice, so its two contributions add up: 1+(−3)=−21 + (-3) = -2.

Result: ∂c/∂a=−2\partial c / \partial a = -2 and ∂c/∂b=2\partial c / \partial b = 2. Check directly: c=ab+ac = ab + a, so ∂c/∂a=b+1=−2\partial c / \partial a = b + 1 = -2 and ∂c/∂b=a=2\partial c / \partial b = a = 2.

The backward pass visits the operations in reverse topological order: every operation comes after all the operations that use its result. In the example, cc comes before dd, because cc uses dd. This guarantees that when the pass reaches a value, every path from that value to LL has already sent its contribution, so its gradient is complete.

The total cost is a small constant times the cost of the forward pass, whatever the number of parameters. This is reverse-mode automatic differentiation. Linnainmaa's 1970 master's thesis is usually cited as its first published description (the idea was found independently several times), and Rumelhart, Hinton and Williams popularized it for training neural networks in 1986.

Two rules that prevent most bugs

Gradients add up. When a value is used in more than one place, each use sends back its own contribution, and these must be summed: in code grad += ..., never grad = .... In the example above, aa received 11 and −3-3. Another case: y=a⋅ay = a \cdot a sends back g ag\,a twice, once per factor, for a total of 2ga2ga, the derivative of a2a^2.

Broadcasting is the same rule in disguise. A bias bb added to every row of a batch is used once per row, so its gradient is the sum of the gradients of all the rows.

Shapes match. The gradient with respect to an array always has the shape of that array. For a matrix product Y=XWY = XW, with upstream gradient G=∂L/∂YG = \partial L / \partial Y, this alone almost forces the answer:

∂L∂W=X⊤G∂L∂X=G W⊤\frac{\partial L}{\partial W} = X^\top G \qquad \frac{\partial L}{\partial X} = G\, W^\top

where X⊤X^\top is XX transposed: rows become columns. These are the only two products whose shapes work.

Example. XX has shape (2,3)(2, 3) and WW has shape (3,4)(3, 4), so YY and GG have shape (2,4)(2, 4).

  • X⊤GX^\top G is (3,2)×(2,4)=(3,4)(3, 2) \times (2, 4) = (3, 4): the shape of WW.
  • GW⊤G W^\top is (2,4)×(4,3)=(2,3)(2, 4) \times (4, 3) = (2, 3): the shape of XX.

Any other combination, such as GXG X, does not even have shapes that can be multiplied.

Gradient checking

A wrong backward pass can still make the loss go down a little, so you cannot trust it just because training "works". Test it against the definition of the derivative instead: nudge a parameter a tiny amount and measure how the loss moves. The central difference nudges it both ways:

∂L∂θ≈L(θ+ε)−L(θ−ε)2ε\frac{\partial L}{\partial \theta} \approx \frac{L(\theta + \varepsilon) - L(\theta - \varepsilon)}{2\varepsilon}

where:

  • ε\varepsilon (epsilon) is the small nudge, such as 10−510^{-5};
  • L(θ+ε)L(\theta + \varepsilon) and L(θ−ε)L(\theta - \varepsilon) are the loss with θ\theta nudged up and down;
  • dividing by 2ε2\varepsilon, the distance between the two points, gives the slope.

Its error shrinks like ε2\varepsilon^2, while the one-sided version, (L(θ+ε)−L(θ))/ε(L(\theta+\varepsilon) - L(\theta))/\varepsilon, only shrinks like ε\varepsilon.

Example. L(θ)=θ3L(\theta) = \theta^3 at θ=1\theta = 1, where the true derivative is 33. With ε=0.01\varepsilon = 0.01:

  • central: (1.013−0.993)/0.02=3.0001(1.01^3 - 0.99^3) / 0.02 = 3.0001;
  • one-sided: (1.013−13)/0.01=3.0301(1.01^3 - 1^3) / 0.01 = 3.0301, a hundred times further off.

To compare your gradient aa with the numerical one bb, use a relative error, because gradients can be tiny or huge, and a difference of 0.0010.001 means nothing until you know the size of the numbers:

rel_error(a,b)=max⁡i∣ai−bi∣max⁡i(∣ai∣+∣bi∣)\text{rel\_error}(a, b) = \frac{\max_i |a_i - b_i|}{\max_i \left(|a_i| + |b_i|\right)}

where aia_i and bib_i are entry ii of the two gradients. In words: the largest difference, divided by the size of the numbers being compared.

How to read it: around 10−710^{-7} or below is a pass; 10−210^{-2} means a bug. Use float64 and ε\varepsilon between about 10−610^{-6} and 10−410^{-4}. A check costs two forward passes per parameter, so run it on small inputs only, never during real training.

References

Practice

0/4