๐Ÿ“š Study Notes / Home / Neural Nets / Session 4
Session 04 ยท Backpropagation

How a neural network actually learns

This is the session where the magic stops being magic. You'll see exactly how a network turns a mistake into a tiny adjustment of every weight inside it. We start each topic with a simple story, then do the real arithmetic by hand โ€” no skipped steps. By the end you'll be able to backpropagate through a tiny network with pen and paper, and you'll know precisely what loss.backward() does under the hood.

โฑ 20 min read๐Ÿ“– 4 topics

1 How a network learns โ€” the big loop


Explain like I'm 5

Imagine you're blindfolded on a bumpy hill and you want to walk to the lowest point. You can't see, but you can feel which way the ground tilts under your feet. So you take a tiny step downhill, feel again, take another tiny step, and keep going. Bit by bit you slide to the bottom. A neural network learns the exact same way: it feels which direction makes its mistakes smaller, then takes a tiny step that way โ€” over and over.

Recall from Session 3 that a network is just a stack of layers, each holding a pile of numbers called weights (and biases). Learning means finding good values for those weights so the network's outputs match the right answers. Training is a loop that repeats this four-step cycle millions of times:

โžก๏ธ
1. Forward pass
Run input through the net to get a prediction
โ†’
๐Ÿ“
2. Loss
Measure how wrong the prediction is
โ†’
โฌ…๏ธ
3. Backward pass
Find how each weight affected the error
โ†’
๐Ÿ”ง
4. Update
Nudge every weight to shrink the error

Step 1 โ€” the forward pass

The forward pass is just running the network normally: feed in the input, let each layer compute its outputs, and read off the final prediction. We did this in Session 3. Nothing learns yet โ€” we're just guessing.

Step 2 โ€” the loss

The loss (also called the cost or error) is a single number that says how bad the guess was. Big loss = very wrong; loss of zero = perfect. A simple one is the squared error: take the difference between the prediction and the true answer, and square it. We'll explore many loss functions in Session 5; for now just think "one number measuring wrongness."

Step 3 โ€” the backward pass

This is the heart of this whole session. The backward pass (a.k.a. backpropagation) answers one question for every single weight in the network: "if I nudge you up a little, does the loss go up or down, and by how much?" That "how much" is called a gradient.

What is a gradient, really?

A gradient is just a slope. For one weight, the gradient โˆ‚Loss/โˆ‚w (read: "the rate of change of loss with respect to w") tells you the slope of the loss as you wiggle that weight. Positive slope โ†’ increasing the weight increases the loss (so you should decrease it). The gradient for the whole network is just the collection of every individual weight's slope.

Step 4 โ€” the update (gradient descent)

Now we take the downhill step. Gradient descent updates each weight by moving it a tiny bit in the direction that reduces the loss โ€” i.e. opposite to the gradient (because the gradient points uphill):

# the single most important formula in deep learning
w_new = w_old - learning_rate * (∂Loss / ∂w)

The learning rate is the size of your step. Too small and you crawl downhill forever; too big and you leap right over the valley and bounce around. (We tune it properly in Session 5.)

Concrete example โ€” one weight, one step

Suppose a weight is w = 0.80, the loss is currently 0.50, and backprop tells us the gradient is ∂Loss/∂w = +2.0 (loss rises steeply as w grows). With learning rate 0.1:

w_new = 0.80 - 0.1 × (+2.0)
      = 0.80 - 0.20
      = 0.60

We decreased w because its gradient was positive (uphill). After this nudge, the next forward pass should give a slightly smaller loss. Repeat a few thousand times and the network gets good.

Key takeaway

Learning = repeatedly roll downhill on the loss surface. The forward pass makes a guess, the loss scores it, backprop measures the slope for every weight, and gradient descent steps each weight a little downhill. The whole rest of this session is just how backprop computes those slopes efficiently.

Recap Training is a four-step loop: forward pass (guess) โ†’ loss (measure wrongness) โ†’ backward pass (gradient = slope for each weight) โ†’ update (w = w โˆ’ lrยทโˆ‚Loss/โˆ‚w, step downhill). Do this many times and the network descends to low loss.

2 The chain rule & computational graphs


Explain like I'm 5

Imagine a line of dominoes. You push the first one, it knocks the second, which knocks the third. If you wanted to know "how much harder do I push the first domino to make the last one fall 2 cm further?", you'd multiply the effects along the chain: push โ†’ domino 1 โ†’ domino 2 โ†’ domino 3. The chain rule is exactly that โ€” when one thing affects another through a chain of steps, you multiply the little effects together to get the total effect.

A computation is a graph

Any calculation can be drawn as a computational graph: a diagram of nodes (operations like + or ร—) connected by arrows that carry values. Inputs flow in on the left; the final result comes out on the right. A neural network is just a very big computational graph.

Let's build a tiny one. Consider the function:

f = (a + b) × c

We break it into two simple steps, giving the intermediate a name:

d = a + b      # node 1: an addition
f = d × c      # node 2: a multiplication
๐Ÿ…ฐ๏ธ
a
input
โ†’
โž•
d = a+b
add node
โ†’
โœ–๏ธ
f = dร—c
multiply node
โ†’
๐ŸŽฏ
f
output

Local gradients โ€” each node minds its own business

The beautiful trick: each node only needs to know how its own output changes when its own inputs wiggle. These are called local gradients, and they're always simple, even when the whole function is huge. The standard derivative rules are all you need:

NodeOperationLocal gradients
Addd = a + bโˆ‚d/โˆ‚a = 1, โˆ‚d/โˆ‚b = 1 (an add just passes gradient through)
Multiplyf = d ร— cโˆ‚f/โˆ‚d = c, โˆ‚f/โˆ‚c = d (a multiply swaps the other input in)

The chain rule composes them

To find how the final output f changes when an early input a changes, we can't read it off directly โ€” a doesn't touch f directly, it goes through d. The chain rule says: multiply the local gradients along the path.

∂f/∂a = (∂f/∂d) × (∂d/∂a)

Read it as: "how f reacts to d" times "how d reacts to a." The intermediate d cancels conceptually, leaving the total effect of a on f. This is the entire mathematical engine of backpropagation.

Worked example with numbers

Let a = 2, b = 3, c = 4.

Forward pass (left to right):

d = a + b = 2 + 3 = 5
f = d × c = 5 × 4 = 20

Local gradients at these values:

∂f/∂d = c = 4
∂f/∂c = d = 5
∂d/∂a = 1
∂d/∂b = 1

Chain them to get the gradient of f with respect to every input:

∂f/∂a = (∂f/∂d)(∂d/∂a) = 4 × 1 = 4
∂f/∂b = (∂f/∂d)(∂d/∂b) = 4 × 1 = 4
∂f/∂c = 5                              # direct, one node away

Sanity check: the gradient โˆ‚f/โˆ‚a = 4 claims that bumping a by a tiny amount multiplies into f four-fold. Test it: set a = 2.01. Then d = 5.01 and f = 5.01 ร— 4 = 20.04. The output rose by 0.04 for an input bump of 0.01 โ€” exactly 4ร— . The gradient was right. ๐ŸŽ‰

Why graphs make this easy

The graph view lets us solve a terrifyingly complex derivative by only ever doing tiny local derivatives at each node and multiplying along edges. You never have to differentiate the whole monster function at once. A network with millions of weights is just a huge graph where this same trick is applied edge by edge.

Recap A computation is a graph of simple nodes. Each node has easy local gradients. The chain rule gets the effect of an early input on the final output by multiplying local gradients along the path. That's the whole math behind backprop โ€” verified above by a tiny numeric perturbation.

3 Backpropagation, step by step


Explain like I'm 5

Picture a relay race run in reverse. The error finishes at the end, then it's handed back down the line โ€” each runner takes the baton, keeps a share of the blame for the mistake, and passes the rest backward to the runner before them. By the time the baton reaches the start, everyone knows exactly how much they contributed to the error. Backprop hands the error backward through the network so every weight learns its share of the blame.

Now we do the real thing: a complete forward and backward pass on a tiny 2-layer network, with every number worked out. Take it slowly โ€” this is the most important worked example in the whole course.

The network

One input x, one hidden neuron, one output neuron, and we'll use the sigmoid activation ฯƒ(z) = 1/(1+eโปแถป) on each (recall sigmoids from Session 3). Squared-error loss against a target y.

# Layer 1 (hidden)
z1 = w1 · x  + b1
h  = σ(z1)

# Layer 2 (output)
z2 = w2 · h  + b2
o  = σ(z2)

# Loss (ยฝ makes the derivative clean)
L  = ½ (o - y)²

A handy fact we'll lean on: the derivative of sigmoid is ฯƒ'(z) = ฯƒ(z)ยท(1 โˆ’ ฯƒ(z)). So if a neuron's output is h, then โˆ‚h/โˆ‚z1 = hยท(1โˆ’h). Neat โ€” no exponentials needed once you already have the output.

Forward pass (pick concrete numbers)

Forward arithmetic

Inputs & parameters: x = 1.0, y = 0.0 (target), w1 = 0.5, b1 = 0.0, w2 = 0.5, b2 = 0.0.

z1 = w1·x + b1 = 0.5·1.0 + 0 = 0.500
h  = σ(0.500)        = 0.6225        # 1/(1+e^-0.5)

z2 = w2·h + b2 = 0.5·0.6225 + 0 = 0.31124
o  = σ(0.31124)     = 0.5772        # the prediction

L  = ½(o - y)² = ½(0.5772 - 0)² = ½(0.33316) = 0.16658

The network predicted 0.5772 but the target was 0, so we have a loss of 0.1666. Now we assign blame.

Backward pass โ€” propagate the gradient right-to-left

We walk backward through the graph, applying the chain rule at each node. At every step we keep a running gradient "of the loss with respect to this value," then multiply by the next local gradient as we move left.

Backward arithmetic โ€” every step shown

Step A โ€” loss w.r.t. output o. With L = ยฝ(oโˆ’y)ยฒ:

∂L/∂o = (o - y) = 0.5772 - 0 = 0.5772

Step B โ€” through the output sigmoid (o = ฯƒ(z2), local gradient o(1โˆ’o)):

∂o/∂z2 = o(1-o) = 0.5772 × (1 - 0.5772) = 0.5772 × 0.4228 = 0.2440
∂L/∂z2 = ∂L/∂o × ∂o/∂z2 = 0.5772 × 0.2440 = 0.14086

Call this running value ฮด2 = โˆ‚L/โˆ‚z2 = 0.14086 โ€” the "blame at the output neuron's pre-activation."

Step C โ€” output-layer weights & bias. Since z2 = w2ยทh + b2, the local gradients are โˆ‚z2/โˆ‚w2 = h and โˆ‚z2/โˆ‚b2 = 1:

∂L/∂w2 = δ2 × h = 0.14086 × 0.6225 = 0.08769
∂L/∂b2 = δ2 × 1 = 0.14086

Step D โ€” push blame back into the hidden output h. Also from z2 = w2ยทh + b2, we have โˆ‚z2/โˆ‚h = w2:

∂L/∂h = δ2 × w2 = 0.14086 × 0.5 = 0.07043

Step E โ€” through the hidden sigmoid (h = ฯƒ(z1), local gradient h(1โˆ’h)):

∂h/∂z1 = h(1-h) = 0.6225 × (1 - 0.6225) = 0.6225 × 0.3775 = 0.23499
∂L/∂z1 = ∂L/∂h × ∂h/∂z1 = 0.07043 × 0.23499 = 0.01655

Call this ฮด1 = โˆ‚L/โˆ‚z1 = 0.01655 โ€” the blame at the hidden neuron.

Step F โ€” hidden-layer weight & bias. From z1 = w1ยทx + b1: โˆ‚z1/โˆ‚w1 = x, โˆ‚z1/โˆ‚b1 = 1:

∂L/∂w1 = δ1 × x = 0.01655 × 1.0 = 0.01655
∂L/∂b1 = δ1 × 1 = 0.01655

We now have a gradient for every parameter. Notice the pattern: at each layer the blame ฮด is "current running gradient ร— local sigmoid slope," then we spread it onto that layer's weights (ร— the input feeding the weight) and pass a slimmer version further back (ร— the weight). That repeating shape is why it's called back-propagation.

The update step

One gradient-descent step (learning rate = 1.0)
w2 ← 0.5 - 1.0 × 0.08769 = 0.41231
b2 ← 0.0 - 1.0 × 0.14086 = -0.14086
w1 ← 0.5 - 1.0 × 0.01655 = 0.48345
b1 ← 0.0 - 1.0 × 0.01655 = -0.01655

Did it help? Re-run the forward pass with the new weights: z1 = 0.48345 โ†’ h = 0.6186 โ†’ z2 = 0.41231ยท0.6186 โˆ’ 0.14086 = 0.1143 โ†’ o = ฯƒ(0.1143) = 0.5285 โ†’ L = ยฝ(0.5285)ยฒ = 0.1397. The loss dropped from 0.1666 to 0.1397. The network just learned. โœ…

Why notice that the deeper weight got a tiny gradient?

See how โˆ‚L/โˆ‚w1 = 0.0166 is much smaller than โˆ‚L/โˆ‚w2 = 0.0877? Each sigmoid multiplied the gradient by a factor less than 1 (here ~0.24, ~0.23) as it travelled backward. Stack many sigmoid layers and the gradient can shrink toward zero โ€” the famous vanishing gradient problem. It's exactly why modern nets prefer ReLU activations and careful initialisation. We'll meet it again in later sessions.

The same thing in code

# Backprop by hand in plain Python โ€” the example above
import math

def sigmoid(z): return 1 / (1 + math.exp(-z))

x, y = 1.0, 0.0
w1, b1, w2, b2 = 0.5, 0.0, 0.5, 0.0

# --- forward ---
z1 = w1 * x + b1
h  = sigmoid(z1)
z2 = w2 * h + b2
o  = sigmoid(z2)
L  = 0.5 * (o - y) ** 2

# --- backward (chain rule, right to left) ---
dL_do  = (o - y)                 # 0.5772
dL_dz2 = dL_do * o * (1 - o)     # 0.14086  (= delta2)
dL_dw2 = dL_dz2 * h              # 0.08769
dL_db2 = dL_dz2                  # 0.14086
dL_dh  = dL_dz2 * w2             # 0.07043
dL_dz1 = dL_dh * h * (1 - h)      # 0.01655  (= delta1)
dL_dw1 = dL_dz1 * x              # 0.01655
dL_db1 = dL_dz1                  # 0.01655

# --- update (gradient descent, lr = 1.0) ---
lr = 1.0
w2 -= lr * dL_dw2;  b2 -= lr * dL_db2
w1 -= lr * dL_dw1;  b1 -= lr * dL_db1
Recap Backprop = forward to get every value, then backward applying the chain rule node by node. At each layer: blame ฮด = incoming gradient ร— activation slope; spread it onto weights (ร— the weight's input) and pass it further back (ร— the weight). We computed all four gradients by hand, took one step, and watched the loss fall from 0.1666 to 0.1397.

4 Automatic differentiation


Explain like I'm 5

Doing that backward arithmetic by hand was fine for one tiny network โ€” but real networks have millions of weights. Nobody wants to compute a million derivatives with pen and paper. So the computer keeps a little notebook: every time it does a math step on the way forward, it jots down what it did. Then to find all the gradients, it just reads its notebook backward and applies the chain rule automatically. That clever notebook trick is called automatic differentiation.

Automatic differentiation ("autodiff" or "autograd") is how frameworks like PyTorch and TensorFlow compute exact gradients with no hand-derived formulas from you. It is not the same as the two things people confuse it with:

ApproachHow it worksProblem
NumericalNudge a weight by a tiny ฮต, re-run forward, see how loss changed.Slow (one re-run per weight) and imprecise (rounding error). Fine for spot-checks, useless for training.
SymbolicDerive one giant algebra formula for the whole gradient (like a calculus class).The formula explodes in size ("expression swell") for big nets.
Automatic (autodiff)Record each elementary op in a graph; apply the chain rule numerically along it.None of the above โ€” exact and fast. This is what frameworks use.

It records the graph as you compute

When you run a forward pass in PyTorch, it secretly builds the same computational graph from Topic 2, node by node, remembering each operation and the local gradient it will need later. This recorded graph is sometimes called the tape (TensorFlow even names its tool GradientTape). When you ask for gradients, it replays the tape backward โ€” exactly the right-to-left walk we did by hand in Topic 3.

Forward-mode vs reverse-mode

There are two directions you can apply the chain rule along the graph:

Forward-modeReverse-mode
DirectionInputs โ†’ output (left to right)Output โ†’ inputs (right to left)
One pass gives youDerivative of all outputs w.r.t. one inputDerivative of one output w.r.t. all inputs
Cheap whenFew inputs, many outputsMany inputs, few outputs
Cost to get all gradientsOne pass per inputOne pass per output
Why deep learning uses reverse-mode

A neural network has millions of inputs (the weights) but the loss is a single output number. Forward-mode would need one pass per weight โ€” millions of passes. Reverse-mode gets the gradient for every weight in just one backward pass. That's the whole game. Backpropagation is simply reverse-mode autodiff applied to a neural network.

So when is forward-mode better?

When you have few inputs and many outputs โ€” the mirror image. That's rarer in deep learning, but it shows up in some physics/sensitivity problems. For training a net (many weights, one loss), reverse-mode wins every time.

PyTorch autograd โ€” our example, automatically

Here's the very same 2-layer computation from Topic 3, but now PyTorch computes all the gradients for us. We just mark which numbers need gradients with requires_grad=True, run the forward pass, and call .backward().

import torch

# parameters we want gradients for (requires_grad=True)
w1 = torch.tensor(0.5, requires_grad=True)
b1 = torch.tensor(0.0, requires_grad=True)
w2 = torch.tensor(0.5, requires_grad=True)
b2 = torch.tensor(0.0, requires_grad=True)
x  = torch.tensor(1.0)
y  = torch.tensor(0.0)

# forward pass โ€” autograd records the graph as this runs
h = torch.sigmoid(w1 * x + b1)
o = torch.sigmoid(w2 * h + b2)
L = 0.5 * (o - y) ** 2

# one call walks the tape backward and fills every .grad
L.backward()

print(w1.grad, b1.grad, w2.grad, b2.grad)
# tensor(0.0166) tensor(0.0166) tensor(0.0877) tensor(0.1409)
# — identical to the numbers we derived by hand!  ๐ŸŽ‰

The printed gradients match our hand calculation exactly: โˆ‚L/โˆ‚w1 = 0.0166, โˆ‚L/โˆ‚w2 = 0.0877, โˆ‚L/โˆ‚b2 = 0.1409. Autograd did in one line what took us six steps โ€” and it would do the same for a network with a billion weights.

TensorFlow does the same with a tape

TensorFlow is explicit about the notebook: you wrap the forward pass in with tf.GradientTape() as tape:, then call tape.gradient(L, [w1, w2, ...]). Same reverse-mode autodiff, just a different name. In practice you rarely write either by hand โ€” an optimiser (Session 5) calls .backward() and the update step for you inside the training loop.

Key takeaway

You will almost never compute gradients by hand again โ€” but knowing what .backward() actually does (record a graph, replay it backward with the chain rule, reverse-mode because there are many weights and one loss) is what separates someone who can debug a model from someone who just hopes it works.

Recap Autodiff records each operation in a graph (the "tape") during the forward pass, then replays it backward applying the chain rule โ€” exact and fast, unlike numerical or symbolic differentiation. Deep learning uses reverse-mode (one backward pass yields every weight's gradient) because there are many weights but one loss. PyTorch's .backward() and TF's GradientTape are just this โ€” and they reproduced our hand-computed gradients exactly.

โ˜… Putting it all together


You now understand the single most important algorithm in deep learning. Here's the one-paragraph story that ties all four topics together:

A network learns by a loop: forward pass to make a prediction, a loss to score how wrong it is, a backward pass to get the gradient (slope) of the loss for every weight, and an update that steps each weight downhill (w = w โˆ’ lrยทโˆ‚L/โˆ‚w). The backward pass works because any computation is a graph of simple nodes with easy local gradients, and the chain rule multiplies them along the path. We did this fully by hand on a 2-layer net โ€” pushing blame right-to-left to get every gradient and watching the loss fall. Frameworks automate exactly this with reverse-mode automatic differentiation: record the graph on the way forward, replay it backward in one pass with .backward(). Same math, a million times faster.

Quick self-check

What are the four steps of the training loop, in order?

Forward pass (predict) โ†’ loss (measure error) โ†’ backward pass (compute gradients) โ†’ update (step each weight downhill with gradient descent). Repeat.

Why do we subtract the gradient in the update, instead of adding it?

The gradient points in the direction that increases the loss (uphill). We want to decrease the loss, so we move the opposite way: w = w โˆ’ lrยทโˆ‚L/โˆ‚w.

What is the chain rule's role in backpropagation?

It lets us find how an early value affects the final loss by multiplying the simple local gradients of each node along the path between them โ€” so we never have to differentiate the whole network at once.

In our worked example, why was โˆ‚L/โˆ‚w1 smaller than โˆ‚L/โˆ‚w2?

The gradient passed back through an extra sigmoid, which multiplied it by a factor less than 1 (its slope โ‰ˆ0.24). Stacking many such layers shrinks gradients toward zero โ€” the vanishing-gradient problem.

Why does deep learning use reverse-mode autodiff rather than forward-mode?

A net has many inputs (weights) but one output (the loss). Reverse-mode gets the gradient for all weights in a single backward pass; forward-mode would need one pass per weight โ€” millions of them.

What does PyTorch's L.backward() actually do?

It walks the computational graph that was recorded during the forward pass backward, applying the chain rule at each node, and stores the resulting gradient for every tensor that had requires_grad=True in its .grad attribute.

๐Ÿ“š References & Further Reading


Class material

  • SST Deep Learning handout (Session 4) โ€” your course handout for this session.

Papers, docs & deep dives