← All posts

Foundations

Post 1 of 15

Neurons, layers and backpropagation

Posts in this series · 15

Every model in this series, from the image classifiers of the 2010s to the speech models behind Pocket TTS, is built from one small part repeated millions of times. That part multiplies some numbers, adds them up, and bends the result. We call it a neuron.

This post builds a network from those parts, small enough that you can hold every number in your head: 2 inputs, 2 hidden neurons, 1 output, 9 adjustable numbers in total. We will push one example through it (the forward pass), measure how wrong it is (the loss), work out who is to blame for the error (backpropagation), and fix it a little (a weight update). The same numbers follow us the whole way, so you can check every step.

Here are the numbers we will use everywhere. Keep this box handy.

WhatValue
Inputsx1=1x_1 = 1, x2=2x_2 = 2
Target (the right answer)y=1y = 1
Hidden neuron 1weights w11=0.5w_{11} = 0.5, w12=0.25w_{12} = 0.25, bias b1=0b_1 = 0
Hidden neuron 2weights w21=−1w_{21} = -1, w22=0.5w_{22} = 0.5, bias b2=0.5b_2 = 0.5
Output neuronweights v1=1v_1 = 1, v2=−1v_2 = -1, bias c=0.25c = 0.25
ActivationReLU (rectified linear unit) in the hidden layer, none at the output
Learning rateη=0.1\eta = 0.1

Do not worry about any of those words yet. Each one gets its own section.

What a neuron computes

A neuron does two things. First it takes a weighted sum of its inputs: each input is multiplied by its own weight, the results are added up, and one extra number called the bias is added on top. Then it passes that sum through an activation function, a fixed rule that bends the number.

The everyday picture is a judge scoring a dish. The judge looks at a few things (taste, smell, look), cares about some more than others (the weights), has a personal mood that shifts every score up or down a little (the bias), and then turns the raw total into a final verdict (the activation).

The weighted sum

In one sentence: multiply each input by how much it matters, add them up, and add a constant.

z=w1x1+w2x2+bz = w_1 x_1 + w_2 x_2 + b

Worked example with hidden neuron 1 from our box: x1=1x_1 = 1, x2=2x_2 = 2, w1=0.5w_1 = 0.5, w2=0.25w_2 = 0.25, b=0b = 0.

What if a number changes? Raise w2w_2 from 0.250.25 to 0.50.5 and the second product doubles to 11, so zz becomes 1.51.5. Because x2=2x_2 = 2, every change to w2w_2 moves zz by twice as much. That fact (the input decides how sensitive zz is to its weight) will come back when we compute every gradient.

The activation

In one sentence: the activation takes zz and returns a new number a=f(z)a = f(z), using a fixed, usually curved, rule.

a=f(z)a = f(z)

For hidden neuron 1 we use ReLU, which keeps positive numbers and replaces negative numbers with zero. z=1.0z = 1.0 is positive, so a=1.0a = 1.0. The next section explains why we bother with ff at all.

A single neuron: two inputs, two weights, a bias and an activation
Activation
Figure 1. One neuron. Multiply, add, bend. Defaults are hidden neuron 1 of the tiny network: z = 1 and a = 1. Switch to sigmoid for 0.731, or set b = −2 to see ReLU output 0.

Why we need non-linear activations

Why bend the number at all? Why not just pass zz straight on? The short answer: without the bend, a hundred layers can do no more than one layer.

Three common activations

Each of these takes any number and returns a number. Here is each one in a plain sentence, then the formula.

Sigmoid squashes any number into the range between 0 and 1: big positive numbers come out near 1, big negative numbers near 0, and zero comes out as exactly one half.

σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}

Example: σ(0)=1/(1+1)=\sigma(0) = 1 / (1 + 1) = 0.5. σ(1)=1/(1+0.368)≈\sigma(1) = 1/(1 + 0.368) \approx 0.731. Raise zz to 1010 and σ\sigma is about 0.999950.99995; going further barely changes it. That flat region matters in the vanishing gradients post.

Tanh (hyperbolic tangent) is the same S shape stretched to run from −1 to 1, centred on zero.

tanh⁡(z)=ez−e−zez+e−z\tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}}

Example: tanh⁡(0)=0\tanh(0) = 0, tanh⁡(1)≈\tanh(1) \approx 0.762, tanh⁡(−1)≈−0.762\tanh(-1) \approx -0.762. It is symmetric: flip the sign of the input, and the output flips too.

ReLU (rectified linear unit) keeps positive numbers and turns negative numbers into zero.

ReLU(z)=max⁡(0,z)\text{ReLU}(z) = \max(0, z)

Example: ReLU(1)=1\text{ReLU}(1) = 1, ReLU(−3)=0\text{ReLU}(-3) = 0, ReLU(0.5)=0.5\text{ReLU}(0.5) = 0.5. The everyday picture is a one-way valve: water flows through in one direction, and nothing at all comes back the other way. ReLU became the default for deep networks after Nair and Hinton (2010) and Glorot, Bordes and Bengio (2011) showed it trains faster than sigmoid and tanh in deep stacks, partly because its slope for positive inputs is always 1 and never shrinks.

Three activation functions, their values and slopes at a chosen input
Bold curve
Figure 2. Three ways to bend. The gold line is the slope. The steps visit z = 0, 1, 4 and −2. At z = 4 the S curves are flat (slopes 0.018 and 0.001) while ReLU’s slope is still 1.

The slopes in that figure are not decoration. When we pass blame backwards, every neuron multiplies the blame by its slope. A slope near zero means almost no blame gets through.

Stacking linear layers stays linear

Suppose we skip the activation, so each layer just multiplies and adds. Take a one-number toy. Layer A doubles its input and adds 1. Layer B triples its input and subtracts 1.

Put A’s output into B:

y=3(2x+1)−1=6x+3−1=6x+2y = 3(2x + 1) - 1 = 6x + 3 - 1 = 6x + 2

Two layers, and the result is one multiply (66) and one add (22): exactly what a single layer could have done. Check with x=1x = 1: A gives u=3u = 3, B gives y=8y = 8, and the single layer gives 6×1+2=86 \times 1 + 2 = 8. Same answer. This is true however many layers you stack and however many inputs each has: a chain of multiply-and-add steps is always one multiply-and-add step. The everyday picture is stacking magnifying glasses. Two lenses give a stronger zoom, but they still only zoom; they can never bend a straight line into a curve.

Now put a ReLU between them: u=ReLU(2x+1)u = \text{ReLU}(2x + 1). For x=1x = 1, nothing changes (u=3u = 3, y=8y = 8). But for x=−1x = -1, 2x+1=−12x + 1 = -1, ReLU turns it into 00, and y=3×0−1=−1y = 3 \times 0 - 1 = -1, whereas the straight line 6x+26x + 2 would give −4-4. The graph now has a corner at x=−0.5x = -0.5. Corners, added up across many neurons, can build any shape we like. This is why every network in this series puts a non-linear activation between its layers.

Two stacked layers without an activation equal one layer; with ReLU between them the line bends
Between the layers
Figure 3. Why we bend. Without an activation, two layers are one layer: 3(2x + 1) − 1 = 6x + 2. With a ReLU between them the line gets a corner at x = −0.5, and x = −1 gives −1 instead of −4.

Layers and the matrix form

A layer is a group of neurons that all read the same inputs. Our hidden layer has two neurons, both reading x1x_1 and x2x_2. Writing each neuron out separately gets long, so we use two pieces of shorthand: vectors and matrices.

A vector is just a list of numbers written in a column. Our inputs are the vector

x=[12]\mathbf{x} = \begin{bmatrix} 1 \\ 2 \end{bmatrix}

A matrix is a table of numbers. We put each neuron’s weights in its own row:

W=[w11w12w21w22]=[0.50.25−10.5],b=[00.5]W = \begin{bmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \end{bmatrix} = \begin{bmatrix} 0.5 & 0.25 \\ -1 & 0.5 \end{bmatrix}, \qquad \mathbf{b} = \begin{bmatrix} 0 \\ 0.5 \end{bmatrix}

In one sentence: multiplying a matrix by a vector means doing one weighted sum per row.

z=Wx+b\mathbf{z} = W\mathbf{x} + \mathbf{b}

Row 1 times x\mathbf{x}: 0.5×1+0.25×2=10.5 \times 1 + 0.25 \times 2 = 1; plus b1=0b_1 = 0 gives z1=z_1 = 1. Row 2 times x\mathbf{x}: −1×1+0.5×2=0-1 \times 1 + 0.5 \times 2 = 0; plus b2=0.5b_2 = 0.5 gives z2=z_2 = 0.5.

So z=[1,0.5]\mathbf{z} = [1, 0.5]. Then the activation is applied to each entry separately: h=ReLU(z)=[1,0.5]\mathbf{h} = \text{ReLU}(\mathbf{z}) = [1, 0.5]. The everyday picture is a row of judges at a contest, each holding their own scorecard (a row of WW) and all tasting the same dish (x\mathbf{x}).

The matrix form is not a new idea, just a shorter way of writing the same sums. It matters in practice because computers, and especially graphics cards, are built to do these row-times-column sums for thousands of neurons at once.

A forward pass through the tiny network

The forward pass means running the inputs through the network, layer by layer, to get an answer. Our network has two layers of weights.

Hidden layer (done above): z1=1z_1 = 1, z2=0.5z_2 = 0.5, h1=ReLU(1)=1h_1 = \text{ReLU}(1) = 1, h2=ReLU(0.5)=0.5h_2 = \text{ReLU}(0.5) = 0.5.

Output layer: one neuron reading h1h_1 and h2h_2 with weights v1=1v_1 = 1, v2=−1v_2 = -1 and bias c=0.25c = 0.25, and no activation (we want any number out, not just positive ones).

y^=v1h1+v2h2+c\hat{y} = v_1 h_1 + v_2 h_2 + c

The target was y=1y = 1, so the network is short by 0.250.25.

The tiny network computing its prediction one step at a time
Figure 4. The forward pass, one operation at a time. Seven steps from inputs to loss: z = (1, 0.5), h = (1, 0.5), ŷ = 0.75, L = 0.03125. Set x₁ = 2 and neuron 2 switches off, giving ŷ = 1.75.

Measuring the error with a loss

To improve, the network needs one number that says how wrong it was. That number is the loss. In one sentence: square the gap between guess and target, and halve it.

L=12(y^−y)2L = \tfrac{1}{2}(\hat{y} - y)^2

Our loss: (−0.25)2=0.0625(-0.25)^2 = 0.0625, halved: L=L = 0.03125.

If the guess had been 0.50.5 instead, the gap would double to −0.5-0.5 and the loss would quadruple to 0.1250.125. This is called squared error, and the next post on loss functions compares it with other choices and explains when each one fits.

The chain rule from scratch

Our goal is to find, for each of the nine weights and biases, how much the loss changes if we nudge that number a tiny bit. That “how much” is called the gradient (or slope, or derivative) of the loss with respect to that number, written ∂L/∂w\partial L / \partial w and read “the change in LL per change in ww”.

The trouble is that a weight like w11w_{11} does not touch the loss directly. It changes z1z_1, which changes h1h_1, which changes y^\hat{y}, which changes LL. We need a way to combine effects along a chain. That is the chain rule.

An everyday picture: gears

Picture three gears in a row. Turn gear A once and gear B turns 3 times. Turn gear B once and gear C turns 2 times. How many times does C turn when A turns once?

3×2=3 \times 2 = 6. You multiply the ratios along the chain.

That is the whole chain rule. In one sentence: when one thing affects another through a middle step, the overall rate of change is the product of the rates of each step.

∂C∂A=∂C∂B×∂B∂A\frac{\partial C}{\partial A} = \frac{\partial C}{\partial B} \times \frac{\partial B}{\partial A}

Check with the numbers from the linear collapse example: u=2x+1u = 2x + 1 changes by 22 per unit of xx; y=3u−1y = 3u - 1 changes by 33 per unit of uu; so yy should change by 2×3=62 \times 3 = 6 per unit of xx. And indeed y=6x+2y = 6x + 2. The chain rule just told us the 66 without expanding anything.

If one gear in the chain is jammed (its ratio is 00), the whole product is 00 and nothing downstream moves. A ReLU with a negative input is exactly such a jammed gear.

The local slopes we need

Every step in our network is one of three simple operations, and each has an easy slope.

Three linked gears: the turns multiply along the chain
Turn by hand
Figure 5. The chain rule is gear ratios. Turn A once: B turns 3 times and C turns 3 × 2 = 6 times. Jam one link (ratio 0) and C stops, just as a ReLU with a negative input stops the blame.

Backpropagation: passing blame backwards

Backpropagation is a way of computing every gradient in the network in one sweep from the output back to the inputs. In one sentence: start with the blame at the loss, and at every step backwards multiply by that step’s local slope.

The everyday picture is a relay team that lost a race by 0.25 seconds. The coach starts at the finish line and walks back down the track. Each runner gets handed the blame from the runner after them, and passes on to the runner before them a share scaled by how much their own leg mattered. Nobody has to re-run the race; one walk back down the track is enough.

Why walk backwards rather than forwards? Because the blame on h1h_1 is needed by every weight feeding into h1h_1. Computing it once and reusing it is what makes backpropagation fast. The same reuse is why a network with a billion weights can get all billion gradients for roughly the cost of two or three forward passes.

The method was popularised for neural networks by Rumelhart, Hinton and Williams (1986). The underlying idea, reverse-mode differentiation, is older: Linnainmaa described it in 1970, and Werbos proposed applying it to neural networks in his 1974 thesis.

We write δ\delta (delta) for “blame on this quantity”, meaning ∂L/∂(that quantity)\partial L / \partial(\text{that quantity}).

Every gradient of the tiny network

We walk back from the loss. Each line multiplies the blame arriving from the right by one local slope.

At the output. The blame on the prediction is the slope of the half square:

δy^=y^−y=0.75−1=−0.25\delta_{\hat{y}} = \hat{y} - y = 0.75 - 1 = -0.25

Negative blame means “if y^\hat{y} went up, the loss would go down”. That matches what we know: the guess was too low.

The output weights and bias. y^=v1h1+v2h2+c\hat{y} = v_1 h_1 + v_2 h_2 + c. The slope of y^\hat{y} with respect to v1v_1 is h1h_1 (each factor’s slope is the other factor), with respect to cc is 11.

Passing blame to the hidden activations. The slope of y^\hat{y} with respect to h1h_1 is v1v_1.

Notice δh2\delta_{h_2} is positive. h2h_2 reaches the output through a negative weight, so raising h2h_2 would lower y^\hat{y}, which is the wrong direction. The blame says “push h2h_2 down”.

Through the ReLUs. Both z1=1z_1 = 1 and z2=0.5z_2 = 0.5 are positive, so both ReLU slopes are 11 and the blame passes through unchanged.

The hidden weights and biases. z1=w11x1+w12x2+b1z_1 = w_{11} x_1 + w_{12} x_2 + b_1, so the slope with respect to w11w_{11} is x1=1x_1 = 1, with respect to w12w_{12} is x2=2x_2 = 2, with respect to b1b_1 is 11.

Read one end to end, say w12w_{12}: it is the product of every local slope on its path, (y^−y)×v1×ReLU′(z1)×x2=(−0.25)×1×1×2=−0.5(\hat{y} - y) \times v_1 \times \text{ReLU}'(z_1) \times x_2 = (-0.25) \times 1 \times 1 \times 2 = -0.5. That is the gear chain from before, four gears long.

What would change? If z2z_2 had been negative, the ReLU slope would be 00, and w21w_{21}, w22w_{22} and b2b_2 would all get gradient 00: that neuron learns nothing from this example. A neuron stuck in that state for every example is called a “dead ReLU”. And because x2=2x_2 = 2 is twice x1x_1, the weights on x2x_2 always get twice the gradient of the weights on x1x_1: larger inputs mean larger blame.

Blame flowing backwards from the loss to every weight of the tiny network
Figure 6. Backpropagation, one multiplication at a time. Eleven steps, each multiplying the blame arriving from the right by one local slope. Defaults end with all nine gradients from the text. With x₁ = 2, neuron 2’s ReLU is off and its three gradients are 0.

One weight update

Now we know which way to push each number. In one sentence: move every weight a small step against its gradient.

wnew=wold−η ∂L∂ww_{\text{new}} = w_{\text{old}} - \eta \, \frac{\partial L}{\partial w}

The everyday picture is walking down a foggy hill: you cannot see the bottom, but you can feel which way the ground slopes under your feet, so you take a small step downhill.

Applying it to all nine numbers:

NumberOldGradientStep =−0.1×= -0.1 \times gradientNew
w11w_{11}0.5−0.25+0.0250.525
w12w_{12}0.25−0.5+0.050.3
b1b_10−0.25+0.0250.025
w21w_{21}−10.25−0.025−1.025
w22w_{22}0.50.5−0.050.45
b2b_20.50.25−0.0250.475
v1v_11−0.25+0.0251.025
v2v_2−1−0.125+0.0125−0.9875
cc0.25−0.25+0.0250.275

Run the forward pass again with the new numbers:

The loss fell from 0.03125 to about 0.00585, more than five times smaller, after one step.

Notice that the prediction jumped from 0.750.75 past the target to about 1.111.11. The step was a little too big: all nine numbers moved at once, and their effects added up. With η=0.05\eta = 0.05 the steps would be half as large and the overshoot smaller. With η=1\eta = 1 the first step flies far past: the loss jumps to 3.45, then 4.88 after the second step, both hidden neurons end up with negative zz, and the ReLUs shut them off for good. Choosing the step size well is the subject of the optimisers post.

Every weight moving a small step against its gradient, and the loss after the update
Loss after each of 20 updates at the chosen learning rate
Figure 7. One step downhill. Each number moves by minus the learning rate times its gradient. At η = 0.1 the loss falls from 0.03125 to 0.00585. At η = 1 the loss jumps to 3.45, then 4.88, and both hidden neurons die; only the output bias still learns.

The computational graph view

Everything we did can be drawn as a computational graph: a diagram where each box is one simple operation (multiply, add, ReLU, half square) and arrows show which results feed which.

Reading the graph left to right is the forward pass: each box computes its output from its inputs and remembers them. Reading it right to left is the backward pass: each box receives a blame from its output side, multiplies it by its local slope, and sends the result to each of its inputs. No box needs to know anything about the rest of the network. A multiply box only knows “my slope for one input is the other input”; a ReLU box only knows “pass the blame if my input was positive, otherwise send zero”.

Two rules cover every case:

This is how deep learning libraries work. When you write the forward pass in PyTorch, it quietly records the graph; calling loss.backward() walks it in reverse with these two rules. Every network later in this series, from convolutions to attention, is just a bigger graph made of different boxes, and the same walk computes its gradients.

The last figure ties it together on a problem a single layer cannot solve. XOR (exclusive or) outputs 1 when exactly one of its two inputs is 1. Minsky and Papert (1969) showed that a single-layer perceptron cannot compute it; a hidden layer with a non-linear activation can.

A network learning XOR: the shaded output, the decision boundary and the loss over training
Training
Random start (seed)
Hidden neurons
Activation
Figure 8. Learning XOR. The full training loop on four points. With the defaults (seed 7, η = 0.5, 4 tanh neurons) all four points are right from epoch 530, and the loss is 0.0012 at epoch 2000. Choose “None (linear)” and the loss stalls at 0.125. Seed 2 gets stuck: a reminder that the random start matters.

Counting the parameters

The weights and biases are the network’s parameters: the numbers training is allowed to change. A model’s size, as in “124 million parameters”, is just their count.

A dense layer (every input connected to every output) with ninn_{\text{in}} inputs and noutn_{\text{out}} outputs has

nin×nout+noutn_{\text{in}} \times n_{\text{out}} + n_{\text{out}}

parameters.

Our tiny network has two such layers. The hidden layer, 2 inputs to 2 neurons: 2×2+2=62 \times 2 + 2 = 6. The output layer, 2 inputs to 1 neuron: 2×1+1=32 \times 1 + 1 = 3. In total 9, exactly the nine numbers the weight update changed. The activation (ReLU) has no parameters: it is a fixed rule, not something learned.

In PyTorch every layer keeps its learnable numbers in .parameters(), and p.numel() is how many numbers a tensor holds, so one line adds them up.

import torch.nn as nn

net = nn.Sequential(nn.Linear(2, 2), nn.ReLU(), nn.Linear(2, 1))
for name, p in net.named_parameters():
    print(name, tuple(p.shape), p.numel())
print("total", sum(p.numel() for p in net.parameters()))

It prints:

0.weight (2, 2) 4
0.bias (2,) 2
2.weight (1, 2) 2
2.bias (1,) 1
total 9

PyTorch stores a layer’s weight matrix as (outputs, inputs), the transpose of how we wrote WW above; the count is the same.

What to remember

Try it yourself

Sources