Backpropagation: the full derivation
35 min read
Everything backpropagation does, it does with the chain rule, and the chain rule is already differentiated. The previous lesson left it in its vector form (Jacobians multiplying together, a gradient coming in at one end and leaving converted into that of the layer before) and, with it, the reason why, as it stands, it can't actually be run: applying it parameter by parameter costs a complete run over the network for every single number there is to adjust. What's missing isn't new mathematics. It's an order of operations.
And that order is easier to see on a small network than a large one. Take the smallest network that still has a hidden layer: two inputs, two hidden neurons, one output. It has nine parameters (four input weights, two hidden biases, two output weights and one bias), so nine derivatives need computing. Behind those nine sit three numbers, one per pre-activation in the network. Once those three are known, each of the nine derivatives is a product of two factors, and neither factor has to be fetched from far away.
One number per pre-activation, and the derivatives that hang off it
Fix a layer and a single example. Differentiating with respect to a weight is already settled; what needs deciding is with respect to what it pays off to differentiate first, and the answer is the pre-activation. Call the error of layer the gradient of that example's loss with respect to , and write for it:
one coordinate per neuron in the layer, and a column, the transpose of the one-row Jacobian the previous lesson used to write the gradient of a scalar. It's and not : the loss of one example, as everywhere in this block since the lesson on loss functions.
With that vector in hand, the parameters of layer come out without any walking at all. Its pre-activation is
or, coordinate by coordinate,
Look at where any particular weight shows up: appears in exactly one of the coordinates, the -th, so only one summand of the previous lesson's sum over paths survives:
because equals when and zero in the other cases, and the bias derivative is the same computation with a in place of an activation. Stacked, the entries form an outer product:
A column of by a row of , exactly the shape of , and that match is the cheapest check there is: if an outer product doesn't come out with the shape of the matrix it's differentiating, something along the way got transposed.
Now count what happened here, because it's the whole argument. Layer has parameters, and every one of its derivatives comes out of only numbers: the coordinates of and those of , which the forward pass has already computed. In the first layer of the previous lesson's network, that's thirty-six derivatives read off twelve numbers. One question remains, and it takes up the rest of this lesson: where does actually come from?
Where that number comes from: the backward recurrence
The value of comes from the layer next door. A layer's error is computed out of the error of the layer after it, which is exactly why the journey runs from the end back to the beginning.
Start at the end, where there's nothing left to do. The last layer delivers , softmax turns it into , and cross-entropy compares that with the label; the lesson on loss functions already differentiated that entire pair, and out of the two together came
Nothing that follows depends on it being that particular vector. Change the loss or the output activation and that vector changes; the rest of the algorithm never even finds out, since it only ever receives it. The small network from the opening is the running example, and this is the place to say what hasn't been said about it yet: it carries a sigmoid in both layers and the loss (the is the convention the lesson on loss functions chose not to adopt, kept here only so the numbers come out short), so its output layer enters with rather than : the extra factor is the sigmoid, which here has nothing to cancel against. Everything else stays identical.
Now for the backward step. Let . The pre-activation of the next layer is built out of this one's in two moves:
and the previous lesson already has the Jacobian of both pieces: an affine map's Jacobian is its own matrix, and a applied coordinate by coordinate has a diagonal one. Compose them,
and that same lesson said exactly what to do with a Jacobian when what's being propagated is the gradient of a scalar: transpose it, and multiply the gradient in from the right. Since is symmetric and transposing a product reverses the order of its factors,
which, rewritten with the Hadamard product so the diagonal is never actually built, is the form that reaches real code:
Read it as two separate moves, because they do two different jobs. The first, , transports: it shares the error from the neurons above out among the below, each according to the weight it used to feed them on the forward pass. Same weights, no new parameter anywhere. The second masks: it multiplies whatever reaches each neuron by that neuron's own slope. With ReLU (rectified linear unit), that slope is or , so a neuron with a negative pre-activation receives a zero and cuts off everything that was coming down from above.
The same recurrence, index by index
With no matrices in the way this time. Coordinate of is the derivative of with respect to , and reaches the loss through the pre-activations of the next layer: it's the previous lesson's sum over paths, with paths this time.
where the second equality only names the first factor. The second factor comes from writing out the next layer with its indices:
Differentiate with respect to , and a single summand of that sum survives, the one with , with the activation contributing its own derivative:
That last factor doesn't depend on , so it moves outside the sum:
Which is exactly the formula written above. The sum runs down column of , which is row of its transpose, that is, coordinate of the transport; the factor sitting outside is coordinate of the masking. Both forms say the same thing, and all the matrix version adds is that the computer runs it without a single index ever being written by hand.
The whole algorithm, written out for two hidden layers
With it fits whole, with no ellipses needed. Forwards, which is the lesson on the forward pass:
keeping the and the along the way. Backwards, from the end back to the beginning:
and out of those three vectors come all six gradients:
That's backpropagation, complete. Three errors, six gradients, and not one step more than that.
What it costs can be read straight off the shapes. The backward pass does one matrix-by-vector product at each layer, using the same matrices as the forward one: the same order of operations, done once, no matter what is. Probing the parameters one at a time costs two forward passes per parameter, and nothing bounds how many parameters there might be. In the network from the code cell below that's parameters, passes against a single one; with the twenty-thousand-entry vocabulary from the lesson on the forward pass, the ratio never changes shape, only size.
There is a price, and it's worth naming, because it isn't time. The backward pass needs the and the of every layer, so all of them have to stay in memory while the forward computation runs. A network that only predicts can throw each layer away the moment it's used; one that's about to train cannot, and that memory grows with both depth and batch size.
From the explorable to a network with two hidden layers
Every symbol is now defined, so here's the whole algorithm walked through slowly on the small network from the opening: the sixteen steps that run from the loss to the nine derivatives, one by one, with their actual numbers.
The first cell is that same network with those same weights, so you can compare the two figure by figure. Run it and read the output against the explorable's steps: the three errors first, then the nine gradients. The last line probes all nine parameters one at a time, the previous lesson's check applied here.
# The explorable's network: two inputs, two hidden neurons and one output, with a
# sigmoid in both layers and the loss ½(ŷ − y)². The weights are its own.
x, y = np.array([1.0, 0.5]), 1.0
W1 = np.array([[0.5, -0.3], [0.8, 0.2]])
b1 = np.array([0.1, -0.2])
W2 = np.array([[0.7, -0.6]]) # one row: the output layer has one neuron
b2 = np.array([0.15])
sigmoid = lambda z: 1.0 / (1.0 + np.exp(-z))
def forward():
h1 = sigmoid(W1 @ x + b1)
y_hat = sigmoid(W2 @ h1 + b2)[0]
return h1, y_hat, 0.5 * (y_hat - y) ** 2
h1, y_hat, loss = forward()
print("h(1) =", np.round(h1, 3), " ŷ = %.3f ℓ = %.3f" % (y_hat, loss))
# Backwards. One δ per pre-activation: three numbers, and there the chain ends.
d2 = np.array([(y_hat - y) * y_hat * (1.0 - y_hat)])
d1 = (W2.T @ d2) * h1 * (1.0 - h1) # transport with the transpose, then mask
print("∂ℓ/∂ŷ = %+.3f ∂ŷ/∂z(2)₁ = %.3f δ(2)₁ = %+.3f"
% (y_hat - y, y_hat * (1.0 - y_hat), d2[0]))
print("∂ℓ/∂h(1) =", np.round(W2.T @ d2, 3),
" φ'(z(1)) =", np.round(h1 * (1.0 - h1), 3),
" δ(1) =", np.round(d1, 3))
# The nine gradients, read off those three numbers.
grad = {"W1": np.outer(d1, x), "b1": d1, "W2": np.outer(d2, h1), "b2": d2}
print("∇W(1) =", np.round(grad["W1"], 3).tolist())
print("∇W(2) =", np.round(grad["W2"], 3).tolist(), " ∂ℓ/∂b(2)₁ = %+.3f" % d2[0])
# And the check: probe the nine parameters one at a time.
worst = 0.0
for name, P in [("W1", W1), ("b1", b1), ("W2", W2), ("b2", b2)]:
for k in np.ndindex(P.shape):
old = P[k]
P[k] = old + 1e-6
plus = forward()[2]
P[k] = old - 1e-6
minus = forward()[2]
P[k] = old
worst = max(worst, abs((plus - minus) / 2e-6 - grad[name][k]))
print("nine parameters, largest difference against the probe: %.1e" % worst)
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
The numbers match the explorable's to three figures, and so do the names, so you can read the two side by side. The third line shows the two moves separately. The transport leaves : a single error, , shared between the two hidden neurons according to their weights and , which is why the second coordinate flips sign. The masking is the right beside it, the derivative of the sigmoid at each pre-activation.
Now look at , which reads on top and below. Its first column is untouched, since ; the second is halved, since . That's an outer product seen up close, and it's the whole reason nine derivatives collapse into three numbers and two inputs.
The second cell is the reviews network with one extra hidden layer, where the recurrence applies twice instead of once: eight inputs, four hidden neurons, three more, and three outputs with softmax. I chose the weights so the pre-activations would come out round. Run it and watch for two things: where zeros appear, and the last two lines.
# The previous lesson's network with one more hidden layer: 8 -> 4 -> 3 -> 3.
x = np.array([0., 0., 1., 0., 1., 1., 0., 1.]) # "the movie is fun and i recommend it"
y = np.array([1., 0., 0.]) # label: positive, out of three classes
W1 = np.array([[-1., -1., 1., 1., 0., 1., -1., 0.], # I set the weights by hand:
[0., 0., -1., 0., 1., 0., 0., 0.5], # there is not one random number
[0., 0., 0., 0.5, 0.5, 0.5, 0., 0.5],
[1., 1., 0., -1., 0.5, -1., 1., 0.5]])
W2 = np.array([[0.5, 0., 0., -1.], [-1., 0.5, 0., 1.], [0., 1., 1., 0.5]])
W3 = np.array([[-0.5, 1., 0.5], [1., -1., -0.5], [0.5, 0.5, -0.5]])
Ws = [W1, W2, W3]
bs = [np.array([0., 0., -0.5, -1.5]), np.array([0., 0.5, -0.5]), np.zeros(3)]
calls = 0
def forward():
global calls
calls += 1
hs, zs = [x], [] # h(0) = x
for l in range(3):
zs.append(Ws[l] @ hs[-1] + bs[l])
hs.append(np.maximum(zs[-1], 0.)) # ReLU; layer 3's is not used
e = np.exp(zs[-1] - zs[-1].max())
return hs, zs, e / e.sum()
hs, zs, y_hat = forward()
print("z(1) =", zs[0], " h(1) =", hs[1], " <- one neuron switched off")
print("z(2) =", zs[1], " h(2) =", hs[2], " <- another")
print("z(3) =", zs[2], " ŷ =", np.round(y_hat, 4), " ℓ = %.6f" % -np.log(y_hat @ y))
# Backwards: one δ per layer, from the end to the beginning, and there is nothing else.
deltas = [None, None, y_hat - y] # softmax + cross-entropy
for l in (1, 0):
deltas[l] = np.where(zs[l] > 0., Ws[l + 1].T @ deltas[l + 1], 0.)
for l in (2, 1, 0): # in the order they come out
print("δ(%d) =" % (l + 1), np.round(deltas[l], 6))
grad_W = [np.outer(deltas[l], hs[l]) for l in range(3)]
print("∇W(2) =", (np.round(grad_W[1], 4) + 0.).tolist()) # the + 0. drops signed zeros
# The 63 gradients against the probe, which is what the other route costs.
worst, calls = 0., 0
for P, G in [(Ws[l], grad_W[l]) for l in range(3)] + [(bs[l], deltas[l]) for l in range(3)]:
for k in np.ndindex(P.shape):
v = P[k]
P[k] = v + 1e-6
d = -np.log(forward()[2] @ y)
P[k] = v - 1e-6
d -= -np.log(forward()[2] @ y)
P[k] = v
worst = max(worst, abs(d / 2e-6 - G[k]))
print("63 parameters, largest difference against the probe: %.1e" % worst)
print("forward passes spent on the probe: %d. By backpropagation: 1." % calls)
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
The zeros are ReLU at work, and there are two of them. The fourth neuron of the first hidden layer has , and the second neuron of the second layer has : both are switched off for this review, and both assign a zero to their . That zero shows up twice in , and for two different reasons. Its second row is null because , meaning none of that neuron's four weights gets corrected; its fourth column is null because , meaning the input those weights read contributes nothing at all. Row and column are the two halves of the outer product, and each vanishes for its own reason.
The last two lines are really the whole lesson. The gradients agree with the probe to , which is the probe's own error and not the algorithm's. And getting them by probing cost forward passes against the single one backpropagation needs. That's in a toy network, with three layers and eight inputs: the ratio is the number of parameters times two, so it only grows as weights get added, and it never stops growing.
Test your intuition
Four questions (a gradient read off an error, what exactly gets reused, the shapes of the backward pass, and what happens if the forward one is thrown away), plus a challenge that is the whole algorithm.
In the network of the second cell, the backward pass leaves , and that review's input is . What is , the derivative of the weight the second hidden neuron gives the fourth vocabulary entry?
A margin of ±0.001 is accepted.
Backpropagation uses no rule of differentiation the previous lesson did not already have. What is it, then, that it saves?
Mark everything that is true of the backward pass through a layer , with .
Select every correct option. This is graded all-or-nothing: there is no partial credit.
A network runs out of memory while training, and someone proposes freeing each as soon as layer has consumed it. What happens?
Write the backward pass of an -layer network, out of what the forward one left stored. The hidden activation is ReLU and the output layer carries softmax with cross-entropy.
errors(Ws, zs, y_hat, y)receives the list of weight matrices, the list of pre-activations (one per layer, in forward order) and the prediction with its label, and returns the list of the vectors , also in forward order.gradients(deltas, hs)receives those and the lisths, wherehs[l]is what goes into layer andhs[0]is . It returns the list of the , one per layer.
Neither function may write into the lists or the arrays it receives.
The first run downloads the Python interpreter (~15 MB); after that it stays in the browser cache. This challenge is much easier to solve on a physical keyboard: on a phone, read it and come back later.
The algorithm is complete, and it hasn't trained anything yet. The two cells compute the gradient of one example with weights I supplied, print it, and stop there: nobody has subtracted anything, and nobody has started over. What's missing to close the circle has already been written down, back in the lesson on gradient descent (), waiting for exactly the gradient this lesson has now produced.
Putting the two halves together calls for three decisions neither lesson has made yet. Examples arrive at a time, so the errors stop being columns and goes back to being the mean of the . The weights have to start somewhere, and the obvious choice, all of them at zero, fails for a reason you can read straight off the formula for derived above. And when to stop has to be decided too. Making those three calls, mounting them on a multilayer perceptron (MLP) written from scratch, and watching the loss actually fall, is the next lesson, on implementing an MLP.
Further reading2 sources · 1 paper, 1 book
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- Learning representations by back-propagating errors
The four pages that put backpropagation on the map. They carry the same recurrence with the same error per neuron, and the point this block keeps circling back to: hidden neurons end up representing features nobody set by hand.
- Deep Learning, ch. 6: Deep Feedforward Networks
Its §6.5 lifts backpropagation out of the fixed-layer network and onto any computation graph at all, which is what a library's automatic differentiation actually does: the same chain rule, walked over the graph instead of by hand.