1 How a network learns โ the big loop
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:
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.
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.)
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.
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.
w = w โ lrยทโLoss/โw, step downhill). Do this
many times and the network descends to low loss.
2 The chain rule & computational graphs
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
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:
| Node | Operation | Local gradients |
|---|---|---|
| Add | d = a + b | โd/โa = 1, โd/โb = 1 (an add just passes gradient through) |
| Multiply | f = 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.
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. ๐
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.
3 Backpropagation, step by step
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)
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.
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
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. โ
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
ฮด = 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
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:
| Approach | How it works | Problem |
|---|---|---|
| Numerical | Nudge 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. |
| Symbolic | Derive 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-mode | Reverse-mode | |
|---|---|---|
| Direction | Inputs โ output (left to right) | Output โ inputs (right to left) |
| One pass gives you | Derivative of all outputs w.r.t. one input | Derivative of one output w.r.t. all inputs |
| Cheap when | Few inputs, many outputs | Many inputs, few outputs |
| Cost to get all gradients | One pass per input | One pass per output |
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.
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 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.
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.
.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
- Rumelhart, Hinton & Williams (1986) โ "Learning representations by back-propagating errors" โ the landmark paper that put backpropagation on the map; remarkably readable.
- Stanford CS231n โ Backpropagation notes โ the clearest gateway explanation, built entirely around computational graphs and local gradients.
- Andrej Karpathy โ "Yes you should understand backprop" โ why treating
.backward()as a black box will burn you, with vivid failure cases. - PyTorch โ Autograd basics tutorial โ the official intro to
requires_gradand.backward()used above. - Karpathy โ "The spelled-out intro to neural networks and backpropagation" (micrograd) โ builds a tiny autodiff engine from scratch; the perfect companion to Topic 4.