Backpropagation: the full derivation

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 ll 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 ll the gradient of that example's loss with respect to z(l)\mathbf{z}^{(l)}, and write δ(l)\boldsymbol{\delta}^{(l)} for it:

δ(l)=∇z(l) ℓ∈Rdl,\boldsymbol{\delta}^{(l)} = \nabla_{\mathbf{z}^{(l)}}\,\ell \in \mathbb{R}^{d_l},

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 ℓ\ell and not L\mathcal{L}: 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 ll come out without any walking at all. Its pre-activation is

z(l)=W(l)h(l−1)+b(l),\mathbf{z}^{(l)} = \mathbf{W}^{(l)}\mathbf{h}^{(l-1)} + \mathbf{b}^{(l)},

or, coordinate by coordinate,

zi(l)=∑kWik(l)hk(l−1)+bi(l).z^{(l)}_i = \sum_k \mathbf{W}^{(l)}_{ik} h^{(l-1)}_k + b^{(l)}_i.

Look at where any particular weight shows up: Wjk(l)\mathbf{W}^{(l)}_{jk} appears in exactly one of the dld_l coordinates, the jj-th, so only one summand of the previous lesson's sum over paths survives:

∂ℓ∂Wjk(l)=∑i=1dl∂ℓ∂zi(l) ∂zi(l)∂Wjk(l)=δj(l) hk(l−1),∂ℓ∂bj(l)=δj(l),\frac{\partial \ell}{\partial \mathbf{W}^{(l)}_{jk}} = \sum_{i=1}^{d_l} \frac{\partial \ell}{\partial z^{(l)}_i}\,\frac{\partial z^{(l)}_i}{\partial \mathbf{W}^{(l)}_{jk}} = \delta^{(l)}_j\, h^{(l-1)}_k, \qquad \frac{\partial \ell}{\partial b^{(l)}_j} = \delta^{(l)}_j,

because ∂zi(l)/∂Wjk(l)\partial z^{(l)}_i / \partial \mathbf{W}^{(l)}_{jk} equals hk(l−1)h^{(l-1)}_k when i=ji = j and zero in the other dl−1d_l - 1 cases, and the bias derivative is the same computation with a 11 in place of an activation. Stacked, the dl×dl−1d_l \times d_{l-1} entries form an outer product:

∇W(l) ℓ=δ(l)(h(l−1))⊤∈Rdl×dl−1,∇b(l) ℓ=δ(l).\nabla_{\mathbf{W}^{(l)}}\,\ell = \boldsymbol{\delta}^{(l)}\left(\mathbf{h}^{(l-1)}\right)^{\top} \in \mathbb{R}^{d_l \times d_{l-1}}, \qquad \nabla_{\mathbf{b}^{(l)}}\,\ell = \boldsymbol{\delta}^{(l)}.

A column of dld_l by a row of dl−1d_{l-1}, exactly the shape of W(l)\mathbf{W}^{(l)}, 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 ll has dl⋅dl−1+dld_l \cdot d_{l-1} + d_l parameters, and every one of its derivatives comes out of only dl+dl−1d_l + d_{l-1} numbers: the coordinates of δ(l)\boldsymbol{\delta}^{(l)} and those of h(l−1)\mathbf{h}^{(l-1)}, 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 δ(l)\boldsymbol{\delta}^{(l)} actually come from?

Where that number comes from: the backward recurrence

The value of δ(l)\boldsymbol{\delta}^{(l)} 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 z(L)\mathbf{z}^{(L)}, softmax turns it into y^\hat{\mathbf{y}}, 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

δ(L)=y^−y.\boldsymbol{\delta}^{(L)} = \hat{\mathbf{y}} - \mathbf{y}.

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 12(y^−y)2\tfrac{1}{2}\left(\hat{y} - y\right)^{2} (the 12\tfrac{1}{2} 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 δ1(2)=(y^−y)y^(1−y^)\delta^{(2)}_1 = \left(\hat{y} - y\right)\hat{y}\left(1 - \hat{y}\right) rather than y^−y\hat{y} - y: the extra factor is the sigmoid, which here has nothing to cancel against. Everything else stays identical.

Now for the backward step. Let l<Ll < L. The pre-activation of the next layer is built out of this one's in two moves:

z(l+1)=W(l+1)φ(z(l))+b(l+1),\mathbf{z}^{(l+1)} = \mathbf{W}^{(l+1)}\varphi\left(\mathbf{z}^{(l)}\right) + \mathbf{b}^{(l+1)},

and the previous lesson already has the Jacobian of both pieces: an affine map's Jacobian is its own matrix, and a φ\varphi applied coordinate by coordinate has a diagonal one. Compose them,

∂z(l+1)∂z(l)=W(l+1) diag(φ′(z(l)))∈Rdl+1×dl,\frac{\partial \mathbf{z}^{(l+1)}}{\partial \mathbf{z}^{(l)}} = \mathbf{W}^{(l+1)}\,\text{diag}\left(\varphi^{\prime}\left(\mathbf{z}^{(l)}\right)\right) \in \mathbb{R}^{d_{l+1} \times d_l},

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 diag\text{diag} is symmetric and transposing a product reverses the order of its factors,

δ(l)=(∂z(l+1)∂z(l))⊤δ(l+1)=diag(φ′(z(l)))(W(l+1))⊤δ(l+1),\boldsymbol{\delta}^{(l)} = \left(\frac{\partial \mathbf{z}^{(l+1)}}{\partial \mathbf{z}^{(l)}}\right)^{\top}\boldsymbol{\delta}^{(l+1)} = \text{diag}\left(\varphi^{\prime}\left(\mathbf{z}^{(l)}\right)\right)\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)},

which, rewritten with the Hadamard product so the diagonal is never actually built, is the form that reaches real code:

δ(l)=((W(l+1))⊤δ(l+1))⊙φ′(z(l)).\boldsymbol{\delta}^{(l)} = \left(\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)}\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(l)}\right).

Read it as two separate moves, because they do two different jobs. The first, (W(l+1))⊤δ(l+1)\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)}, transports: it shares the error from the dl+1d_{l+1} neurons above out among the dld_l 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 00 or 11, 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 jj of δ(l)\boldsymbol{\delta}^{(l)} is the derivative of ℓ\ell with respect to zj(l)z^{(l)}_j, and zj(l)z^{(l)}_j reaches the loss through the dl+1d_{l+1} pre-activations of the next layer: it's the previous lesson's sum over paths, with dl+1d_{l+1} paths this time.

δj(l)=∑i=1dl+1∂ℓ∂zi(l+1) ∂zi(l+1)∂zj(l)=∑i=1dl+1δi(l+1) ∂zi(l+1)∂zj(l),\delta^{(l)}_j = \sum_{i=1}^{d_{l+1}} \frac{\partial \ell}{\partial z^{(l+1)}_i}\,\frac{\partial z^{(l+1)}_i}{\partial z^{(l)}_j} = \sum_{i=1}^{d_{l+1}} \delta^{(l+1)}_i\,\frac{\partial z^{(l+1)}_i}{\partial z^{(l)}_j},

where the second equality only names the first factor. The second factor comes from writing out the next layer with its indices:

zi(l+1)=∑k=1dlWik(l+1) φ(zk(l))+bi(l+1).z^{(l+1)}_i = \sum_{k=1}^{d_l} \mathbf{W}^{(l+1)}_{ik}\,\varphi\left(z^{(l)}_k\right) + b^{(l+1)}_i.

Differentiate with respect to zj(l)z^{(l)}_j, and a single summand of that sum survives, the one with k=jk = j, with the activation contributing its own derivative:

∂zi(l+1)∂zj(l)=Wij(l+1) φ′(zj(l)).\frac{\partial z^{(l+1)}_i}{\partial z^{(l)}_j} = \mathbf{W}^{(l+1)}_{ij}\,\varphi^{\prime}\left(z^{(l)}_j\right).

That last factor doesn't depend on ii, so it moves outside the sum:

δj(l)=(∑i=1dl+1Wij(l+1) δi(l+1))φ′(zj(l)).\delta^{(l)}_j = \left(\sum_{i=1}^{d_{l+1}} \mathbf{W}^{(l+1)}_{ij}\,\delta^{(l+1)}_i\right)\varphi^{\prime}\left(z^{(l)}_j\right).

Which is exactly the formula written above. The sum runs down column jj of W(l+1)\mathbf{W}^{(l+1)}, which is row jj of its transpose, that is, coordinate jj of the transport; the factor sitting outside is coordinate jj 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 L=3L = 3 it fits whole, with no ellipses needed. Forwards, which is the lesson on the forward pass:

h(0)=x,z(l)=W(l)h(l−1)+b(l),h(l)=φ(z(l))    (l=1,2),y^=softmax(z(3)),\mathbf{h}^{(0)} = \mathbf{x}, \quad \mathbf{z}^{(l)} = \mathbf{W}^{(l)}\mathbf{h}^{(l-1)} + \mathbf{b}^{(l)}, \quad \mathbf{h}^{(l)} = \varphi\left(\mathbf{z}^{(l)}\right) \;\; (l = 1, 2), \quad \hat{\mathbf{y}} = \text{softmax}\left(\mathbf{z}^{(3)}\right),

keeping the h(l−1)\mathbf{h}^{(l-1)} and the z(l)\mathbf{z}^{(l)} along the way. Backwards, from the end back to the beginning:

δ(3)=y^−y,δ(2)=((W(3))⊤δ(3))⊙φ′(z(2)),δ(1)=((W(2))⊤δ(2))⊙φ′(z(1)),\begin{aligned} \boldsymbol{\delta}^{(3)} &= \hat{\mathbf{y}} - \mathbf{y}, \\[2pt] \boldsymbol{\delta}^{(2)} &= \left(\left(\mathbf{W}^{(3)}\right)^{\top}\boldsymbol{\delta}^{(3)}\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(2)}\right), \\[2pt] \boldsymbol{\delta}^{(1)} &= \left(\left(\mathbf{W}^{(2)}\right)^{\top}\boldsymbol{\delta}^{(2)}\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(1)}\right), \end{aligned}

and out of those three vectors come all six gradients:

∇W(l)ℓ=δ(l)(h(l−1))⊤,∇b(l)ℓ=δ(l),l=1,2,3.\nabla_{\mathbf{W}^{(l)}}\ell = \boldsymbol{\delta}^{(l)}\left(\mathbf{h}^{(l-1)}\right)^{\top}, \qquad \nabla_{\mathbf{b}^{(l)}}\ell = \boldsymbol{\delta}^{(l)}, \qquad l = 1, 2, 3.

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 LL 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 6363 parameters, 126126 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 h(l−1)\mathbf{h}^{(l-1)} and the z(l)\mathbf{z}^{(l)} 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 network from the opening, with x = (1, 0.5), y = 1, a sigmoid in both layers and the loss ½(ŷ − y)². Step through the sixteen steps with the arrows and keep an eye on one number: δ(2)₁, the error of the output neuron, computed at the third step and reused as the first factor in five of the six steps that follow. That is the whole saving. Compare the fourth step with the fifth too: two different weights, the same first factor, and the only thing that changes is what each one had in front of it.

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.

import numpy as np

# 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)
numpy

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 ∂ℓ/∂h(1)=(−0.079, 0.068)\partial \ell / \partial \mathbf{h}^{(1)} = (-0.079,\ 0.068): a single error, −0.113-0.113, shared between the two hidden neurons according to their weights 0.70.7 and −0.6-0.6, which is why the second coordinate flips sign. The masking is the (0.238, 0.222)(0.238,\ 0.222) right beside it, the derivative of the sigmoid at each pre-activation.

Now look at ∇W(1)ℓ\nabla_{\mathbf{W}^{(1)}}\ell, which reads (−0.019, −0.009)(-0.019,\ -0.009) on top and (0.015, 0.008)(0.015,\ 0.008) below. Its first column is δ(1)\boldsymbol{\delta}^{(1)} untouched, since x1=1x_1 = 1; the second is δ(1)\boldsymbol{\delta}^{(1)} halved, since x2=0.5x_2 = 0.5. 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.

import numpy as np

# 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)
numpy

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 z4(1)=−1.5z^{(1)}_4 = -1.5, and the second neuron of the second layer has z2(2)=−1.25z^{(2)}_2 = -1.25: both are switched off for this review, and both assign a zero to their δ\boldsymbol{\delta}. That zero shows up twice in ∇W(2)ℓ\nabla_{\mathbf{W}^{(2)}}\ell, and for two different reasons. Its second row is null because δ2(2)=0\delta^{(2)}_2 = 0, meaning none of that neuron's four weights gets corrected; its fourth column is null because h4(1)=0h^{(1)}_4 = 0, 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 6363 gradients agree with the probe to 10−1010^{-10}, which is the probe's own error and not the algorithm's. And getting them by probing cost 126126 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 δ(1)=(0.475931,  −0.725931,  −0.725931,  0)⊤\boldsymbol{\delta}^{(1)} = (0.475931,\; -0.725931,\; -0.725931,\; 0)^{\top}, and that review's input is x=(0,0,1,0,1,1,0,1)⊤\mathbf{x} = (0, 0, 1, 0, 1, 1, 0, 1)^{\top}. What is ∂ℓ/∂W24(1)\partial \ell / \partial \mathbf{W}^{(1)}_{24}, 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 ll, with W(l)∈Rdl×dl−1\mathbf{W}^{(l)} \in \mathbb{R}^{d_l \times d_{l-1}}.

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 h(l)\mathbf{h}^{(l)} as soon as layer l+1l+1 has consumed it. What happens?

Write the backward pass of an LL-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 LL vectors δ\boldsymbol{\delta}, also in forward order.
  • gradients(deltas, hs) receives those δ\boldsymbol{\delta} and the list hs, where hs[l] is what goes into layer l+1l+1 and hs[0] is x\mathbf{x}. It returns the list of the ∇W(l)ℓ\nabla_{\mathbf{W}^{(l)}}\ell, 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 (θt+1=θt−η ∇θL\theta_{t+1} = \theta_t - \eta\,\nabla_{\theta}\mathcal{L}), 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 BB at a time, so the errors stop being columns and L\mathcal{L} goes back to being the mean of the ℓ\ell. 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 δ\boldsymbol{\delta} 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.

Was this helpful?
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
    paperRumelhart, Hinton and Williams, 1986Nature 323EN

    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
    bookGoodfellow, Bengio and Courville, 2016deeplearningbook.orgEN

    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.