youssefsabri
← index · · 3 min read #ml#python

Backprop is just the chain rule with bookkeeping

One idea, one data structure. The whole engine in forty lines.

Backprop is not a special algorithm. It is the chain rule, applied in an order that avoids recomputing anything. The only reason it needs code is that something has to remember the order.

The one idea

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

Nothing deeper happens in a hundred-layer network. When a value feeds three places, its gradient is the sum of what comes back from each.

The one data structure

Build the graph as a side effect of the forward pass. Every operation records its parents and a closure holding the local derivative.

class Value:
    def __init__(self, data, parents=(), backward=lambda: None):
        self.data = data
        self.grad = 0.0
        self._parents = parents
        self._backward = backward

    def __mul__(self, other):
        out = Value(self.data * other.data, (self, other))

        def backward():
            self.grad  += other.data * out.grad
            other.grad += self.data  * out.grad

        out._backward = backward
        return out

Note the +=. That is the fan-out rule, and the most common bug in a hand-rolled engine.

graph LR
x[x] --> m((*))
w[w] --> m
m --> a((+))
b[b] --> a
a --> t[tanh]
t --> L[loss]
fig.01 Forward builds the graph; backward walks it in reverse topological order.

Ordering is the algorithm

A gradient is final only once every consumer has contributed. Sort topologically, reverse, call each closure once.

def backward(self):
    order, seen = [], set()

    def build(v):
        if v in seen:
            return
        seen.add(v)
        for p in v._parents:
            build(p)
        order.append(v)

    build(self)
    self.grad = 1.0
    for node in reversed(order):
        node._backward()

That is the entire engine. PyTorch is this plus tensors, kernels, and thirty thousand lines of making it fast.

x, w*+LFORWARDBACKWARD
fig.02 Values move right; gradients move left along the same edges.

Check it

∂L∂w≈L(w+ε)−L(w−ε)2ε\frac{\partial L}{\partial w} \approx \frac{L(w + \varepsilon) - L(w - \varepsilon)}{2\varepsilon}
checkanalyticnumericrel. error
dL/dw0.419974340.419974364.8e-08
dL/db0.500000000.499999984.0e-08
end 2026.10.22
← back to index
normal ~/posts/backprop-is-bookkeeping --:--:--