Unit 8: Backpropagation
The network finds its own numbers.
Unit 8 of 13 in AI and machine learning for kids. Its 4 lessons are Who Is to Blame?, The Slope of the Squash, It Teaches Itself XOR and When Training Goes Wrong — below is everything each one explains, and a question or two from it to try.
Every sample on this page is plain Python with no libraries, run before it shipped, and prints exactly what it says it prints.
This unit opens with a fortnight’s trial of everything — no card needed — or with a family plan, bought in the iPhone app. The first two units of every track are free for ever. Try it in the app.
🔍 Who Is to Blame?
One mistake, nine culprits
The network answered 0.8 when it should have said 0. The mistake is easy: out - y, so 0.8 too high.
The hard part is that nine numbers helped produce that 0.8. Which of them should move, and by how much? Gradient descent needs a slope for every single weight.
Blame goes where the push came from
Think about one weight in the output neuron. It multiplied the hidden value that arrived along it.
- If that hidden neuron sent a big number, this weight did a lot of the pushing — much of the blame is its.
- If the hidden neuron sent nearly 0, the weight changed nothing whatever it was — no blame.
Which is why the share of the blame is the mistake multiplied by the value that came in. You saw the same rule in unit 3, where each weight change was multiplied by its own input.
And the blame keeps travelling
The hidden neurons were not innocent either — they chose what to send. A hidden neuron's share of the blame is the output's blame, carried back along the weight that connects them: a fat weight passes back a lot, a weight near zero passes back almost none.
So the mistake starts at the answer and walks backwards through the network, splitting up as it goes. That walk is backpropagation.
Try it yourself
A hidden neuron sent out 0.0. How much does its weight into the output move?
- A lot — it caused the mistake
- Not at all — whatever that weight was, it contributed nothing
- The same as every other weight
- It moves in the opposite direction
Why is it called back propagation?
- The network runs backwards
- The mistake is worked out at the end and passed back through the layers
- The weights go negative
- It undoes the last training step
Answer them in the app
📈 The Slope of the Squash
One more thing is needed
The blame arrives at a neuron as "your total should have been lower". But the neuron does not hand out its total — it hands out the *squashed* total. So how much does moving the total actually move the answer?
That is the slope of the sigmoid at the point the neuron is sitting on, and it has a remarkably tidy shortcut:
slope = s * (1 - s)
where s is the value the sigmoid already gave.
Do not take our word for it
You already own a way to measure a slope: nudge the input and see how much the output moves. Here the shortcut and the nudge are put side by side.
Python
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
h = 0.0001
for x in [-2, 0, 1]:
nudged = (sigmoid(x + h) - sigmoid(x)) / h
s = sigmoid(x)
print(x, round(nudged, 4), round(s * (1 - s), 4))
It prints
-2 0.105 0.105 0 0.25 0.25 1 0.1966 0.1966
A confident neuron learns slowly
The slope is biggest in the middle — 0.25 at a sigmoid of 0.5 — and shrinks towards nothing at both ends. A neuron answering 0.99 has a slope of about 0.01, so all the blame arriving at it gets multiplied by almost zero and it barely moves.
That is a real problem with real networks, and it has a name: vanishing gradients. It is why very deep networks were hard to train for years.
Python
for s in [0.5, 0.75, 0.9, 0.99, 0.999]:
print(s, round(s * (1 - s), 5))
It prints
0.5 0.25 0.75 0.1875 0.9 0.09 0.99 0.0099 0.999 0.001
Try it yourself
A neuron currently answers 0.5. Its slope is:
- 0
- 0.25
- 0.5
- 1
Answer it in the app
🧠 It Teaches Itself XOR
Putting the three pieces together
For every example, run it forwards, then work out three blames and move nine numbers:
d_out = (out - y) * out * (1 - out) — the mistake, times the output neuron's slope.
d_h1 = d_out * w5 * h1 * (1 - h1) — the blame carried back along w5, times that neuron's own slope.
Then every weight moves by rate * blame * whatever came in along it, exactly as in unit 3.
Why the loss has a half in it
We score with (out - y) ** 2 / 2 rather than plain squaring. Halving cannot change which weights are best — it shrinks every score by the same amount — but it makes the slope come out as exactly out - y, with no stray 2 to carry around.
It is a tidiness trick, and everybody uses it.
Nine numbers, chosen badly on purpose
Training starts from small mixed-up numbers. They must not all be the same: identical neurons receive identical blame, change identically, and stay identical forever. A hidden layer of clones is one neuron wearing a hat.
Python
w1, w2, b1 = 0.5, -0.4, 0.1
w3, w4, b2 = -0.3, 0.6, -0.2
w5, w6, b3 = 0.7, -0.5, 0.3
print(w1, w2, b1)
print(w3, w4, b2)
print(w5, w6, b3)
It prints
0.5 -0.4 0.1 -0.3 0.6 -0.2 0.7 -0.5 0.3
Twenty thousand times round
Watch the loss. It sits almost still for a thousand passes — the network is drifting about learning nothing much — and then falls off a cliff. That plateau is normal, and it is why people leave training running.
Python
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
data = [[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0]]
w1, w2, b1 = 0.5, -0.4, 0.1
w3, w4, b2 = -0.3, 0.6, -0.2
w5, w6, b3 = 0.7, -0.5, 0.3
rate = 0.5
def forward(a, b):
h1 = sigmoid(a * w1 + b * w2 + b1)
h2 = sigmoid(a * w3 + b * w4 + b2)
return h1, h2, sigmoid(h1 * w5 + h2 * w6 + b3)
def loss():
total = 0
for a, b, y in data:
total = total + (forward(a, b)[2] - y) ** 2 / 2
return total / len(data)
for step in range(1, 20001):
for a, b, y in data:
h1, h2, out = forward(a, b)
d_out = (out - y) * out * (1 - out)
d_h1 = d_out * w5 * h1 * (1 - h1)
d_h2 = d_out * w6 * h2 * (1 - h2)
w5 = w5 - rate * d_out * h1
w6 = w6 - rate * d_out * h2
b3 = b3 - rate * d_out
w1 = w1 - rate * d_h1 * a
w2 = w2 - rate * d_h1 * b
b1 = b1 - rate * d_h1
w3 = w3 - rate * d_h2 * a
w4 = w4 - rate * d_h2 * b
b2 = b2 - rate * d_h2
if step in [500, 1000, 2000, 5000, 20000]:
print(step, round(loss(), 4))
for a, b, y in data:
print(a, b, round(forward(a, b)[2], 2))
It prints
500 0.1247 1000 0.1023 2000 0.0026 5000 0.0004 20000 0.0001 0 0 0.01 0 1 0.99 1 0 0.99 1 1 0.01
Stop and look at what just happened
Unit 3 proved that one neuron can never learn XOR. This network started from nine nearly-useless numbers, was told only which answers were right, and found nine numbers that work — including hidden neurons that do the jobs Or and Nand do, which nobody asked it for.
That is a neural network training itself, and it is the same procedure, unchanged, that trains models with a billion weights.
Try it yourself
Why must the starting weights be different from each other?
- Big numbers train faster
- Identical neurons get identical blame and stay identical forever
- Python needs unique variable names
- It makes the loss smaller at the start
Answer it in the app
🏆 When Training Goes Wrong
The same code, stuck
Nothing changed here except the learning rate — 1.0 instead of 0.5. Two of the four answers are 0.48, which is the network shrugging, and no amount of extra training moves them.
It has rolled into a dip that is not the bottom of the valley. Every direction out of a dip is uphill, so gradient descent stays. This is called a local minimum.
Python
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
data = [[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0]]
w1, w2, b1 = 0.5, -0.4, 0.1
w3, w4, b2 = -0.3, 0.6, -0.2
w5, w6, b3 = 0.7, -0.5, 0.3
rate = 1.0
def forward(a, b):
h1 = sigmoid(a * w1 + b * w2 + b1)
h2 = sigmoid(a * w3 + b * w4 + b2)
return h1, h2, sigmoid(h1 * w5 + h2 * w6 + b3)
for step in range(3000):
for a, b, y in data:
h1, h2, out = forward(a, b)
d_out = (out - y) * out * (1 - out)
d_h1 = d_out * w5 * h1 * (1 - h1)
d_h2 = d_out * w6 * h2 * (1 - h2)
w5 = w5 - rate * d_out * h1
w6 = w6 - rate * d_out * h2
b3 = b3 - rate * d_out
w1 = w1 - rate * d_h1 * a
w2 = w2 - rate * d_h1 * b
b1 = b1 - rate * d_h1
w3 = w3 - rate * d_h2 * a
w4 = w4 - rate * d_h2 * b
b2 = b2 - rate * d_h2
for a, b, y in data:
print(a, b, round(forward(a, b)[2], 2))
It prints
0 0 0.03 0 1 0.48 1 0 0.98 1 1 0.48
What people actually do about it
They start again from different numbers, or change the learning rate, or add more hidden neurons so there are more ways down.
There is no guarantee anywhere in this. Training a network is not a calculation with a right answer at the end — it is an attempt, and part of the job is noticing when it has failed.
The other way to fail: learn the wrong thing perfectly
Now hide one row. The network trains on three of XOR's four cases and never sees 1 1.
Python
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
train = [[0, 0, 0], [0, 1, 1], [1, 0, 1]]
w1, w2, b1 = 0.5, -0.4, 0.1
w3, w4, b2 = -0.3, 0.6, -0.2
w5, w6, b3 = 0.7, -0.5, 0.3
rate = 0.5
def forward(a, b):
h1 = sigmoid(a * w1 + b * w2 + b1)
h2 = sigmoid(a * w3 + b * w4 + b2)
return h1, h2, sigmoid(h1 * w5 + h2 * w6 + b3)
for step in range(5000):
for a, b, y in train:
h1, h2, out = forward(a, b)
d_out = (out - y) * out * (1 - out)
d_h1 = d_out * w5 * h1 * (1 - h1)
d_h2 = d_out * w6 * h2 * (1 - h2)
w5 = w5 - rate * d_out * h1
w6 = w6 - rate * d_out * h2
b3 = b3 - rate * d_out
w1 = w1 - rate * d_h1 * a
w2 = w2 - rate * d_h1 * b
b1 = b1 - rate * d_h1
w3 = w3 - rate * d_h2 * a
w4 = w4 - rate * d_h2 * b
b2 = b2 - rate * d_h2
for a, b, y in train:
print("seen", a, b, round(forward(a, b)[2], 2))
print("never seen 1 1 ->", round(forward(1, 1)[2], 2))
It prints
seen 0 0 0.02 seen 0 1 0.99 seen 1 0 0.99 never seen 1 1 -> 1.0
It learned "or" and is certain about it
Perfect on all three examples it was given. Completely wrong on the one it was not — and not hesitantly wrong, 1.0 wrong.
This is overfitting: the model fitted what it was shown instead of the pattern behind it. Nothing in its training could possibly have told it otherwise, and nothing in its confidence tells you it happened. Only a test set does.
Try it yourself
The training loss is tiny but the test accuracy is poor. What is happening?
- The learning rate is too small
- The model has overfitted — it learned the examples, not the pattern
- There is a bug in the loss
- It needs more training
Which of these does a confident answer from a model prove?
- That it is right
- That it saw a lot of examples like this
- Nothing — it can be confidently wrong about anything it was never shown
- That the training worked
Answer them in the app