Reverse accumulation
Apply the chain rule through a graph and implement the core of a scalar reverse-mode autodiff engine.
THE IDEA
Start at the output and walk backwards
The loss changes one-for-one with itself, so the backward pass starts with ∂L/∂L = 1. Going backwards, each node multiplies the gradient it received by its local derivatives and passes the results on. Multiplying along a path is the chain rule.
A value that affects the loss along more than one path receives a contribution from each path, and the contributions are added. This is the most common source of mistakes when doing backpropagation by hand.
Predict first. If x is used twice in x + x, what is the derivative of the output with respect to x?
Reading the numbers
A gradient of zero does not mean the input is unimportant in general. It means that at this point a small change does nothing, for instance because a multiplying weight is zero or because the upstream gradient is zero.
Chapter context
Backpropagation organizes repeated applications of the chain rule. The graph stores how values depend on earlier values. Reverse traversal accumulates how the final loss changes with each intermediate quantity.
TRY IT YOURSELF
Part 1. Do it hands-on
Move the inputs and watch the gradients (orange) change. Set them so the gradients take the values the tasks describe.
- To do: Make the loss exactly 0. What happens to all four gradients?
- To do: Set the inputs so that ∂L/∂b is exactly −6
- To do: Make ∂L/∂x equal to 0 while ∂L/∂w is not 0
- To do: Make ∂L/∂w equal to −2 times ∂L/∂b, with ∂L/∂b not 0
- To do: On the second graph, L = (a·b + a)², make ∂L/∂a equal to 8
- To do: Make ∂L/∂a equal to 0 although a is not 0
Part 2. Do it in code
- evaluate and local_grads are given. Write backward(graph, values, out): return a dict with the gradient d(out)/d(name) for EVERY name in values (leaves and nodes). The gradient of out is 1.
- Walk the graph from the last node to the first. For each node send upstream * local to each input and ADD it to what the input already holds (an input can be used more than once).
- A leaf that is not used by the output must get 0.0. Calling backward twice must give the same answer twice (build a fresh dict each time).
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
If x is used twice in x+x, what is the total derivative?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
If and , then
At a shared node x, add all downstream contributions: .
Worked example
Take a=2,b=3,c=1. Then u=7 and L=49. The upstream gradient into u is 14, giving dL/da=14×3=42, dL/db=14×2=28 and dL/dc=14. These are sensitivities, not new parameter values; an optimizer still needs a step size.
Reference code
The following example uses the Python standard library and does not call a remote service.
from math import isclose
a,b,c = 2.,3.,1.
u = a*b+c
assert u*u == 49
assert (2*u*b,2*u*a,2*u) == (42,28,14)
h = 1e-5
f = lambda x: (x*b+c)**2
assert isclose((f(a+h)-f(a-h))/(2*h), 42, rel_tol=1e-7)
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
Forward values and gradients are different objects. Shared branches require addition of gradient contributions. Backpropagation computes derivatives; the optimizer changes parameters.