Computation graphs
Every calculation as a graph of small operations.
Create a free account Try part of it here
Why it matters
To train a network, you need to know how each weight affects the loss. The loss is the score of how wrong the model was. A large model has billions of weights, and nobody works out their rates by hand. Instead, tools like PyTorch record the calculation as a graph of tiny steps. Each step has a rate of change that's easy to know. The tool then combines those rates by a fixed rule, with no human help. This lesson builds that graph. The next lesson runs it backward.
What you'll be able to do
- Break a calculation into a graph of small operations
- Evaluate a graph in topological order (the forward pass)
- Give every operation its local derivative
- Combine local derivatives with the chain rule, adding up contributions from branches
What it covers
- Every calculation is a graph
- Local derivatives
- Chaining them
- When a value is used twice, gradients add
- Worked example: a neuron with ReLU
- Why frameworks think in graphs
Every calculation is a graph
Think of a recipe. You can't ice a cake before you bake it. You can't bake it before you mix the batter. Each step uses results from earlier steps. A calculation works the same way.
Take one neuron's squared error. It uses four numbers:
- , the weight, is a number the model can adjust.
- is the input.
- , the bias, is a second number the model can adjust.
- is the target: the answer you wanted.
The prediction is . The loss is the miss, squared: .
In plain words: multiply the input by the weight and add the bias. Subtract the target. Then square what's left.
Now break that into single steps. Each step does one operation and gives its result a name:
| Node | Operation | With w = 2, x = 3, b = 1, y = 10 |
|---|---|---|
| multiply | 6 | |
| add | 7 | |
| subtract | −3 | |
| square | 9 |
These steps form a computation graph. Each named value is a node. Draw an arrow from each input to the node that uses it. Arrows go from and into . Then from and into , and so on, until you reach .
Each node depends only on earlier ones. So you can work them out in an order where every input is ready before it's needed, like the recipe. That order is called a topological order. Working through the nodes in that order is the forward pass. You met the forward pass for whole networks in the last lesson. This is the same idea, one small step at a time.
w, x, b, y = 1.0, 4.0, 2.0, 5.0 p = w * x q = p + b r = q - y print(r ** 2)
Show the answer
1.0
p = 1 × 4 = 4, q = 4 + 2 = 6, r = 6 − 5 = 1, and 1 squared is 1.0. Each line uses only values worked out above it. That's a topological order.
Local derivatives
Now ask each node one question. If you nudge one of its inputs up a little, how much does its output move? The answer is that node's local derivative for that input. "Local" means it only looks at its own inputs, not the whole graph.
Start with shopping. Apples cost $3 each. Buy one more apple and the bill goes up by $3. The multiply node works the same way. With and , is
- Raise by 1, to 3. Now , up by 3. So moves 3 for every 1
that moves. That 3 is , the other input.
This rate is written . The curly d, , is said "partial". You met it in the chain rule lesson. It means how fast changes as changes, with every other input held still. Here .
Every operation has its own small, fixed rule. ReLU, from the activation functions lesson, keeps positive numbers and turns negative ones into 0. In the table, is an operation's first input. Its second input is called here. This is any second input, not the bias.
| Operation | Output | Derivative with respect to each input |
|---|---|---|
| add | 1 and 1 | |
| subtract | 1 and −1 | |
| multiply | and (each input's slope is the other input) | |
| square | ||
| ReLU | 1 if , else 0 |
In plain words: adding passes a nudge straight through. Subtracting passes it through too, but flips the sign for the number taken away. Multiplying scales a nudge by the other input. Squaring scales it by twice the input. ReLU passes a nudge through when its input is positive. Otherwise it blocks it.
For example, a square node at has a local derivative of . A ReLU node at has a local derivative of 0.
Show the answer
x
For a multiply node, each input's slope is the other input.
Chaining them
The chain rule from Phase 2 says rates along a chain multiply. Each dollar buys 2 clicks, and each click is worth $3, so each dollar is worth . The graph is a chain too. feeds , which feeds , then , then . So the effect of on is the product of the local derivatives along that path:
In plain words: start at the loss and walk back to . At each node, multiply by that node's local derivative. The square gives . The subtract and the add each give 1. The multiply gives , the other input.
Now put in the numbers. Here , so . Then . For , the path is shorter: from to to , then to . That gives .
The code below does both passes. The forward lines fill in the nodes. The backward lines multiply local derivatives, from the loss back toward the inputs. A name like dL_dq holds the rate of with respect to that node.
w, x, b, y = 2.0, 3.0, 1.0, 10.0 # forward: evaluate the graph node by node p = w * x q = p + b r = q - y L = r ** 2 # backward: multiply local derivatives along each path dL_dr = 2 * r dL_dq = dL_dr * 1 dL_dp = dL_dq * 1 dL_dw = dL_dp * x # multiply node: the slope for w is the other input, x dL_db = dL_dq * 1 print(f"L = {L} dL/dw = {dL_dw} dL/db = {dL_db}") # check against finite differences: nudge w and b a tiny bit h = 1e-6 f = lambda w, b: (w * x + b - y) ** 2 print(f"numerical dL/dw ≈ {(f(w + h, b) - f(w - h, b)) / (2 * h):.4f} dL/db ≈ {(f(w, b + h) - f(w, b - h)) / (2 * h):.4f}")
The first line prints L = 9.0 dL/dw = -18.0 dL/db = -6.0. The second line checks those answers another way. It nudges and a tiny amount and measures how far moves. It gets about −18.0000 and −6.0000, a match.
Both rates are negative. That means increasing or would lower the loss. This makes sense. The prediction is 7 and the target is 10, so the model should aim higher.
Go deeper How the nudge check works
The line h = 1e-6 sets to one millionth. The code works out the loss with and again with . It divides the difference by the gap between them, . That gives the slope near . This is the nudge method from the derivatives lesson, often called finite differences.
It's slow, because it reruns the whole calculation for every input. The graph gets every rate in one backward walk. But the nudge is a good way to test that a hand-written backward pass is right.
Forward computes the values. Backward multiplies the local slopes, one step at a time.
Show the answer
Up: a negative rate means raising w lowers the loss
A negative rate means the loss falls as w rises. So raising w a little lowers the loss, and the prediction (7) moves toward the target (10).
When a value is used twice, gradients add
Picture a café. The price of milk goes into both the latte and the cappuccino. If milk costs 10 cents more, your costs go up through both drinks. The total change is the two changes added together.
A graph works the same way. If a value feeds into several later nodes, it affects the output along several paths. Its total rate is the sum of what each path contributes. These rates are often called gradients.
Take , with . Split it into nodes: , then , then . The value reaches along three paths:
- As the first input to the multiply node . Its local slope is the other input, .
- As the second input to . Again, the slope is the other input: 2.
- Through . Raising by 1 raises by 3, so the slope is 3.
The add node passes each path through with a slope of 1. Add the three paths: .
In plain words: follow every road from the input to the output. Work out each road's rate. Then add the roads together.
a = 2.0 u = a * a # a is used twice here... v = 3 * a # ...and once more here f = u + v df_du, df_dv = 1.0, 1.0 df_da = df_du * a + df_du * a + df_dv * 3 # three paths from a to f print(f"f = {f}, df/da = {df_da} (calculus says 2a + 3 = {2 * a + 3})")
It prints f = 10.0, df/da = 7.0. Calculus agrees. The slope of is , which is 7 when .
Watch out: when you write the backward pass by hand, add each new path to the running total (
+=). Replacing the total (=) keeps only the last path. Forgetting to add is the classic bug in hand-written backpropagation.
grad = 0.0 for contribution in [4.0, -1.0]: grad += contribution print(grad)
Show the answer
3.0
The += adds each path: 4.0 + (−1.0) = 3.0. With = instead of +=, the second path would overwrite the first and you'd get −1.0. That's the overwrite bug.
Worked example: a neuron with ReLU
A tiny neuron works out , then passes through ReLU. Take , and . Find the output. Then find how fast the output changes with and with .
w, x, b = 0.5, 4.0, -1.0 # forward p = w * x # multiply z = p + b # add out = max(0.0, z) # ReLU # backward: start at 1 and multiply local slopes d_out = 1.0 d_z = d_out * (1.0 if z > 0 else 0.0) # ReLU slope d_p = d_z * 1.0 # add: slope 1 d_b = d_z * 1.0 d_w = d_p * x # multiply: the other input print(out, d_w, d_b)
It prints 1.0 4.0 1.0. Forward: p = 0.5 × 4 = 2, then z = 2 − 1 = 1. ReLU keeps the 1 because it's positive. Backward: start with 1 at the output, since the output moves 1 for 1 with itself. ReLU's slope is 1 because z is above 0. The add node passes that 1 to both p and b, so d_b is 1. The multiply node scales it by the other input, x = 4, so d_w is 4. Now try x = 1. Then z is −0.5, ReLU outputs 0, and both slopes become 0. A ReLU that outputs 0 blocks every gradient behind it.
Why frameworks think in graphs
- PyTorch records the graph as your code runs. Each operation on a tensor that needs gradients adds a node. (A tensor is PyTorch's name for an array of numbers.) When you call
loss.backward(), it walks the graph backward and fills in every rate. It never writes out a formula for the whole thing, the way you might on paper. It only knows each small step's rule. - Memory. The backward pass needs values from the forward pass. The multiply rule, for example, needs the other input. So the framework stores those in-between values, called activations. It's like keeping your receipts on a trip, because you'll need them on the way back. Storing activations is why training uses far more memory than running a model. Some setups save memory by throwing some activations away and working them out again when needed. That trick is called activation checkpointing.
- Scale. One forward pass of a large language model (LLM) is a graph of millions of these small operations on large arrays. Yet every one of them has a local derivative as simple as the ones in the table above.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
A value x feeds into two different nodes. How do you get the gradient of the output with respect to x?