The chain rule, in earnest
30 min read
Of the three prerequisites this course starts from, one is the chain rule, and up to now the course has differentiated using the version everyone learns first: two one-variable functions chained, their two derivatives multiplied. That version produced both calculations this block has needed so far, the correction of the output layer and the gradient of a lone neuron. The previous lesson, on gradient descent, left training solved except for one piece: the step knows exactly what to do with the gradient, but for a network with hidden layers there is still no way to actually compute one. This lesson is that missing piece, and it turns out to be pure calculus.
Look closely at where things actually get complicated, because it is not where it seems. Take the review network with three answers instead of two (positive, negative and neutral, the classes the lesson on loss functions used to arrive at the softmax): eight inputs, four hidden neurons, three outputs. Pick any weight in the first layer, say the one the first hidden neuron gives fun, and nudge it by a thousandth. Its neuron's value changes: so far, the usual calculation. What comes next is not usual at all, because that neuron feeds all three output neurons, all three feed into the loss, and all three have now moved. One number touched, three effects rippling out through different routes and meeting back up at the end. The one-variable rule has nowhere to put that.
What needs counting is not the neurons, it is the paths running from a weight all the way to the loss.
Of the three plausible answers (keep the heaviest path, average them, or add them all up), only one is actually right, and it is worth understanding why before you use it.
When one variable arrives by several paths
Set the network aside for a moment. Let be a function of variables , and let each of them, in turn, be a function of one and the same variable . Then depends on even without ever mentioning it, and its derivative is
one summand per intermediate variable. Each summand has the shape of the familiar rule (how much responds to , times how much responds to ), and what's new is that there are of them, and that they add. Within a path you multiply; across paths you add.
That sum is not a convention, and where it actually comes from is the one thing in this lesson that isn't bookkeeping. being differentiable at a point means that, close to it, the function looks like a linear function of the displacements , and a linear function of numbers is a sum of terms, one per number. The paths aren't competing with each other: each contributes its own share, and the shares add because the approximation they come from was already a sum to begin with.
The derivation, with the displacement written out
Move by a small amount . Every intermediate variable moves along with it, and since each is differentiable with respect to ,
where collects whatever is left over, a remainder that, divided by , tends to zero. The differentiability of at the point says the same thing one level up: the increment of is linear in the displacements, plus a remainder that's negligible next to the size of the total displacement,
Substitute the first equation into the second, divide everything by , and let tend to zero. Both remainders vanish (the second because is on the order of ), leaving exactly the formula above. The sum was already there in the second equation from the start: it arrived with the definition of the derivative itself, not with the chain rule.
Now bring that back to the network. The weight you were moving, the one the first hidden neuron gives fun, is (row , that neuron's; column , that vocabulary entry's), and the intermediate variables are the three pre-activations of the output layer, , and . There are two more links along the way, so each summand carries four factors:
None of these four is new. The first is the result the lesson on loss functions closed the output layer with, by pairing softmax and cross-entropy together. The second comes from being a sum in which appears exactly once, multiplied by its weight. The third is the derivative of the activation, tabulated for all three candidates back in the lesson on activation functions and non-linearity. And the fourth belongs to the previous lesson: the pre-activation is linear in its weights, so the derivative with respect to any one of them is the input that multiplies it. The only genuinely new fact was that the three paths add.
The object that replaces the derivative
That formula handles one weight. The network has , and fifty-one separate sums full of indices are not really an answer, this needs to go up a level.
Let . Its derivative at a point is the Jacobian, the matrix that gathers every partial derivative of every output with respect to every input,
with one row per output coordinate and one column per input coordinate. That ordering, output first, belongs to the whole course, and from it fall out the two things this block does with a Jacobian.
The first is that composing turns back into multiplying. If produces and this in turn produces , then
a product of a matrix by an one that delivers exactly the matrix you need. The dimension that gets summed over and disappears is , the intermediate variables, exactly the way the width shared by two layers disappeared in the forward pass.
And that product isn't some new object, it's the previous one stacked up. Expand entry using the definition of the matrix product:
which is exactly the sum over paths from the previous section, written for output and input : the routes from to , one per intermediate variable you could pass through. The sum over paths and the matrix product are the same calculation, and the computer performs the second one without anyone ever writing an index.
The loss is a scalar, and that is where the transpose comes from
The second thing comes from looking at the far end of the chain. returns a single number, so in its case and its Jacobian is a one-row matrix, . The gradients in this course are columns, like every vector in it, so one is the transpose of the other, . Transpose the composition above (remembering that transposing a product reverses the order of its factors):
an matrix times a vector of coordinates, which returns exactly the vector of you wanted. That's the shape of everything left in this block: a gradient comes in on the right, leaves converted into the gradient of the previous layer, and the transposed Jacobian is what performs that conversion.
Hold on to one observation this lesson doesn't actually use. A chain of Jacobians can be evaluated either by multiplying the matrices among themselves first, or by carrying the output's gradient backwards through one transpose after another. Both give the same answer, since the matrix product is associative, but they don't cost the same: the first multiplies matrices by matrices, and the second only ever multiplies matrices by vectors.
A coordinate-wise activation has a diagonal Jacobian
One concrete Jacobian is still missing, the activation's, and it comes out far cheaper than its size would suggest. With applied coordinate by coordinate, each output looks at exactly one input: depends on and on nothing else, so the moment . Of the whole matrix, only the diagonal survives:
where is the square matrix carrying on its diagonal and zeros everywhere else. Multiplying by it amounts to multiplying coordinate by coordinate, which has its own symbol, the Hadamard product:
From here on, nobody actually builds the matrix: it's products instead of , and numbers stored instead of , of which would be zeros anyway. With ReLU (rectified linear unit), the diagonal is or and acts as a mask: a hidden neuron with a negative pre-activation gets a zero in its entry, and that zero cuts off every path running through it. This is the dying ReLU from the lesson on activation functions, seen from the other side.
Softmax is the exception, and it's worth flagging: it mixes every coordinate together, so its Jacobian has not a single zero anywhere in it. It isn't needed here, because the lesson on loss functions already differentiated it together with cross-entropy, and out of that pairing came , the gradient this whole chain starts from.
One weight, three paths and three ways of measuring them
The cell feeds the review the movie is fun and i recommend it into the network of eight inputs, four hidden neurons and three outputs, and computes (the same weight as in the previous section) three different ways: a numerical probe, the sum of the three paths, and the product of the Jacobians. I set the weights by hand, so you'll see these exact numbers and no others. Run it, compare the three figures, and then look at the fourth.
Go back to the previous section's formula, but with the four factors stacked into their proper objects: the summation sign disappears inside the matrix product.
The diagonal is written out in full here (not as the previous section's cheap Hadamard product) because the goal is to show the Jacobian exactly as it is, not to save on arithmetic; is at hidden neuron and at the other three, the only one that actually moves. What does save effort is the order: evaluated left to right (row times matrix, three times over), nothing larger than a row of four numbers ever appears, which is the observation from the transpose section put into practice. That's step 3 of the cell below.
# The review above, "the movie is fun and i recommend it", and what is expected of it.
x = np.array([0., 0., 1., 0., 1., 1., 0., 1.]) # bag of words, eight inputs
y = np.array([1., 0., 0.]) # label: positive, out of three classes
# I set the weights by hand. There is not a single random number.
W1 = np.array([[-1., -1., 1., 1., 0., 1., -1., 0.],
[0., 0., -1., 0., 1., 0., 0., 0.5],
[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([[1., 0., 0.5, -1.], [-1., 0., 0., 1.], [0.5, 0.5, -0.5, 0.]])
b1, b2 = np.array([0., 0., -0.5, -1.5]), np.zeros(3)
def forward(W1):
z1 = W1 @ x + b1
e = np.exp(W2 @ np.maximum(z1, 0.) + b2) # ReLU, then softmax
y_hat = e / e.sum()
return z1, y_hat, float(-np.log(y_hat @ y)) # the one-hot picks out the correct class
i, j = 0, 2 # W1[0, 2]: hidden neuron 1, "fun"
z1, y_hat, L = forward(W1)
d_relu = (z1 > 0).astype(float)
print("z(1) =", z1, " φ'(z(1)) =", d_relu, " ŷ =", np.round(y_hat, 4), " L = %.6f" % L)
# 1. Probe: move that weight a hundred-thousandth up and down and evaluate the loss again.
W_plus, W_minus = W1.copy(), W1.copy()
W_plus[i, j], W_minus[i, j] = W1[i, j] + 1e-5, W1[i, j] - 1e-5
print("probe %+.8f" % ((forward(W_plus)[2] - forward(W_minus)[2]) / 2e-5))
# 2. One path per output neuron, with its four factors in view.
paths = (y_hat - y) * W2[:, i] * d_relu[i] * x[j]
for k in range(3):
print(" path via z(2)[%d]: (%+.6f)(%+.1f)(%.0f)(%.0f) = %+.6f"
% (k, y_hat[k] - y[k], W2[k, i], d_relu[i], x[j], paths[k]))
print("sum of the paths %+.8f" % paths.sum())
print("forgetting the third %+.8f" % paths[:2].sum())
# 3. The same derivatives stacked into Jacobians, and multiplied as they are.
J_w = np.eye(4)[:, [i]] * x[j] # dz(1)/dW1[0,2]: only neuron i sees it
J = (y_hat - y).reshape(1, 3) @ W2 @ np.diag(d_relu) @ J_w
print("shapes (1, 3) x", W2.shape, "x", np.diag(d_relu).shape, "x", J_w.shape)
print("product of Jacobians %+.8f" % J[0, 0])
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
All three agree at , and the probe is the one with the least right to: it knows nothing about the network's structure at all, it only evaluates the loss with the weight nudged a hundred-thousandth up and a hundred-thousandth down. That it matches the chain result exactly is the check worth keeping handy for whenever a hand-written derivative refuses to add up.
The fourth figure is the one that teaches what this lesson is really about. Forgetting a path (the third one, through the output neuron for the neutral class) leaves the derivative at , almost double what it should be. And look closely at the three summands, because the error isn't one of size: the first two are negative and the third is positive. That path was pulling the other way, so removing it doesn't trim the correction down, it inflates it instead. A forgotten path doesn't give you an approximate derivative, it gives you the derivative of an entirely different function.
The last line prints the shapes in the chain, . And in the row there's a zero: the fourth hidden neuron has its pre-activation at , it's switched off for this particular review, and its entry on the diagonal cuts off whatever tried to pass through it.
Test your intuition
Four questions (a sum of paths worked out by hand, why they add, the shapes of a Jacobian, and the order of the product), plus a challenge that is the entire chain rule.
A loss depends on a single variable through and . What is at ?
A margin of ±0 is accepted.
The contributions of the paths add up. Why do they add, rather than multiply or keep the largest?
Tick everything that is true about the Jacobians of this lesson.
Select every correct option. This is graded all-or-nothing: there is no partial credit.
In this lesson's network, produces and this in turn produces . What shape does have, and which product does it come from?
Write the chain rule in its vector form, with no network in the way:
jacobian(f, x)receives a function from to and a point, and returns its Jacobian at that point, of shape , computed with central differences: column is with .input_gradient(jacobians, g)receives a list of Jacobians in forward order (the first is that of the first function) and the gradient at the output, and returns the gradient at the input. It is a vector, not a matrix.
jacobian may not write into the x 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.
Everything needed to differentiate any network is now on the table, and laid out like this it's useless in practice. The cell computed the derivative of one weight, and to get it, it walked the entire chain: the error at the output layer, the second layer's matrix, the diagonal of the activation. The very next weight needs that same walk repeated, with only a single different factor at the end, and so do the other forty-nine weights in this network. With a vocabulary of twenty thousand entries and a hundred and twenty-eight hidden neurons, that's upwards of two and a half million walks, identical except for the last step, and not even the fastest computer in the world has time to spare for that.
The waste becomes obvious the moment you name it: those walks share almost everything, and whatever is shared gets recomputed once per parameter anyway. What's actually needed is to reverse the direction of travel: compute once, starting from the last layer, how much the loss owes every pre-activation in the network, and let each weight read its derivative off a quantity that's already sitting there. That procedure has a name of its own, gets derived layer by layer with every index in view, and is the next lesson, on backpropagation.
Further reading2 sources · 1 paper, 1 article
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- The Matrix Calculus You Need For Deep Learning
The vector chain rule and the Jacobian worked out in full, including the case this lesson uses: a coordinate-wise activation, hence a diagonal Jacobian. Needs no more than one-variable calculus, and is more mechanical than conceptual.
- Calculus on Computational Graphs: Backpropagation
Reads the sum over paths as a computational graph and names what this lesson leaves aside: forward mode versus reverse mode, and why reverse mode is what makes the way back cheap.