Index Ai Academy
Phase 4 · Deep learning · Neural networks

Computation graphs

Every calculation as a graph of small operations.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. Every calculation is a graph
  2. Local derivatives
  3. Chaining them
  4. When a value is used twice, gradients add
  5. Worked example: a neuron with ReLU
  6. 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:

  • ww, the weight, is a number the model can adjust.
  • xx is the input.
  • bb, the bias, is a second number the model can adjust.
  • yy is the target: the answer you wanted.

The prediction is wx+bw x + b. The loss is the miss, squared: L=(wx+b−y)2L = (w x + b - y)^2.

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:

NodeOperationWith w = 2, x = 3, b = 1, y = 10
p=w⋅xp = w \cdot xmultiply6
q=p+bq = p + badd7
r=q−yr = q - ysubtract−3
L=r2L = r^2square9

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 ww and xx into pp. Then from pp and bb into qq, and so on, until you reach LL.

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.

Quick check
Here are the same four steps with new numbers. What does this print?
w, x, b, y = 1.0, 4.0, 2.0, 5.0
p = w * x
q = p + b
r = q - y
print(r ** 2)
  1. 36.0
  2. 1.0
  3. -1.0
  4. 9.0
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 p=w⋅xp = w \cdot x works the same way. With w=2w = 2 and x=3x = 3, pp is

  1. Raise ww by 1, to 3. Now p=3×3=9p = 3 \times 3 = 9, up by 3. So pp moves 3 for every 1

that ww moves. That 3 is xx, the other input.

This rate is written ∂p∂w\frac{\partial p}{\partial w}. The curly d, ∂\partial, is said "partial". You met it in the chain rule lesson. It means how fast pp changes as ww changes, with every other input held still. Here ∂p∂w=x=3\frac{\partial p}{\partial w} = x = 3.

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, aa is an operation's first input. Its second input is called bb here. This bb is any second input, not the bias.

OperationOutputDerivative with respect to each input
adda+ba + b1 and 1
subtracta−ba - b1 and −1
multiplya⋅ba \cdot bbb and aa (each input's slope is the other input)
squarea2a^22a2a
ReLUmax⁡(0,a)\max(0, a)1 if a>0a > 0, 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 a=−3a = -3 has a local derivative of 2×(−3)=−62 \times (-3) = -6. A ReLU node at a=−1a = -1 has a local derivative of 0.

Quick check
In the graph p = w·x, what is ∂p/∂w?
  1. w
  2. w·x
  3. 1
  4. x
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 2×3=$62 \times 3 = \$6. The graph is a chain too. ww feeds pp, which feeds qq, then rr, then LL. So the effect of ww on LL is the product of the local derivatives along that path:

∂L∂w=∂L∂r⋅∂r∂q⋅∂q∂p⋅∂p∂w=(2r)(1)(1)(x)\frac{\partial L}{\partial w} = \frac{\partial L}{\partial r} \cdot \frac{\partial r}{\partial q} \cdot \frac{\partial q}{\partial p} \cdot \frac{\partial p}{\partial w} = (2r)(1)(1)(x)

In plain words: start at the loss and walk back to ww. At each node, multiply by that node's local derivative. The square gives 2r2r. The subtract and the add each give 1. The multiply gives xx, the other input.

Now put in the numbers. Here r=−3r = -3, so 2r=−62r = -6. Then −6×1×1×3=−18-6 \times 1 \times 1 \times 3 = -18. For bb, the path is shorter: from LL to rr to qq, then to bb. That gives −6×1×1=−6-6 \times 1 \times 1 = -6.

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 LL 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}")
It runs right here in your browser, with nothing to install.

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 ww and bb a tiny amount and measures how far LL moves. It gets about −18.0000 and −6.0000, a match.

Both rates are negative. That means increasing ww or bb 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 hh to one millionth. The code works out the loss with w+hw + h and again with w−hw - h. It divides the difference by the gap between them, 2h2h. That gives the slope near ww. 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.

Try it now

Forward computes the values. Backward multiplies the local slopes, one step at a time.

Quick check
For this graph, ∂L/∂w = −18. To lower the loss, which way should training nudge w?
  1. Up: a negative rate means raising w lowers the loss
  2. Down: a negative rate means w is too big
  3. Set w to −18
  4. Leave it alone: the rate is negative, so the loss is already falling
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 f=a⋅a+3af = a \cdot a + 3a, with a=2a = 2. Split it into nodes: u=a⋅au = a \cdot a, then v=3av = 3a, then f=u+vf = u + v. The value aa reaches ff along three paths:

  1. As the first input to the multiply node uu. Its local slope is the other input, a=2a = 2.
  2. As the second input to uu. Again, the slope is the other input: 2.
  3. Through v=3av = 3a. Raising aa by 1 raises vv by 3, so the slope is 3.

The add node f=u+vf = u + v passes each path through with a slope of 1. Add the three paths: 2+2+3=72 + 2 + 3 = 7.

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 a2+3aa^2 + 3a is 2a+32a + 3, which is 7 when a=2a = 2.

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.

Quick check
A value feeds two paths. One contributes 4.0 to its gradient, and the other contributes −1.0. What does this print?
grad = 0.0
for contribution in [4.0, -1.0]:
    grad += contribution
print(grad)
  1. 4.0
  2. 3.0
  3. -4.0
  4. -1.0
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

Worked example: a neuron with ReLU

A tiny neuron works out z=wx+bz = w x + b, then passes zz through ReLU. Take w=0.5w = 0.5, x=4x = 4 and b=−1b = -1. Find the output. Then find how fast the output changes with ww and with bb.

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)
In the lesson you can change this and run it.

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

Create a free account

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?

  1. Add the gradients coming back along both paths
  2. Take the larger of the two paths' gradients
  3. Multiply the gradients from the two paths
  4. Use the gradient from the most recent path

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson