Gradient descent
30 min read
The training question can now be written out in full, and writing it doesn't solve it. The previous lesson, on loss functions, put the last piece in place: a rule that compares what the network answers with what it should have answered, and sums it all up in one number. Searching for the parameters that make that number smallest is now a well-posed question, and there's still no way to answer it by trial and error. The network from the lesson on the forward pass holds numbers; with a vocabulary of twenty thousand entries and a hidden layer of neurons, that becomes , and every single one of them is a real number.
There is, though, a much smaller question that does get answered. Take one of those parameters on its own, raise it by a hundredth, leave the other untouched, and evaluate the loss again: if it went up, that parameter was moving the wrong way; if it went down, the right one. It's a local answer (it doesn't say where the minimum is, only which way the ground slopes from here), and, taken to the limit, it's had a name since the seventeenth century: the partial derivative of the loss with respect to that parameter. Probing all costs evaluations. Differentiating gives you all of them without probing a single one, and for one neuron the whole count fits on a line; getting the same result with hidden layers in the way takes up the rest of this block.
Before differentiating anything, look at the terrain itself. Fix every one of a network's parameters but two, and the loss becomes a function of those two numbers, which draws as a surface full of valleys and hillsides. The one in the explorable below comes from no real network at all: I made it up so it fits on a plane and has two bottoms instead of one.
The descent never compares the two valleys, because it can't see them. From wherever it stands, it has access to exactly one thing (how much the loss changes when you move a little in each direction), and that's all it uses to decide the next step. Right on the ridge, that slope points nowhere useful: the derivative is zero without the spot being a bottom, a saddle point, and there the descent stays put instead of moving any further down.
The gradient points uphill, and that is why it is subtracted
Call all of the network's parameters at once (the weights and biases of every layer, stacked into a single vector of coordinates), and the loss over the batch. The gradient is the vector that gathers one partial derivative per parameter, . Each of its coordinates says how fast the loss changes when one parameter moves and the rest hold still: exactly what the hundredth-step probe measured, only exact this time, and without ever evaluating anything twice. For the neuron from the lesson on the artificial neuron, with its weights and its bias , that's numbers:
The first thing worth noticing is that has the exact same shape as . That's no accident: it's precisely what lets you subtract one from the other.
What's left is to justify why you subtract that particular vector and nothing else. Take any direction, a vector of length , and ask how much the loss changes as you move a little in it. That's the directional derivative, which for a function with partial derivatives turns out to be a dot product,
and it's bounded. The Cauchy-Schwarz inequality gives
with equality only when is parallel to the gradient. Of every possible direction, then, the gradient's own direction raises the loss the most, and the opposite one lowers it the most. No other direction lowers it more per unit of step.
From there, the whole rule follows. Starting from some initial parameters , each step subtracts the scaled gradient:
where the subscript counts steps ( is the parameters after five steps, not the fifth coordinate of anything), and is the learning rate, the one number in the whole formula that you actually choose. That's gradient descent, in its entirety.
That repeating this actually lowers the loss shows up in the first-order approximation. Near , the loss looks much like its own tangent plane,
and the descent step is precisely , so
What gets subtracted is a squared norm, and so it's never negative: as long as the gradient isn't zero, the step goes down. The entire catch sits in that equation's sign, an : the equality belongs to the tangent plane, not to the loss itself, and it only holds "nearby". How near is exactly what decides.
One concession worth saying out loud: this lesson's batch is all ten reviews at once, so every step looks at every example before moving anything. That's called batch gradient descent, and it's far from the only way to fix . At the other extreme, stochastic gradient descent (SGD) sets : each step looks at a single example and costs almost nothing, but the gradient it uses is no longer the exact one from this lesson, it's a noisy estimate of it, computed from one example instead of ten. The mini-batch sits in between, with somewhere between those two extremes (a few tens or hundreds of examples, typically), and it's the option that wins out in practice. It's not only that a set of millions won't fit in memory at once: even when it would fit whole, many cheap, somewhat noisy steps tend to train faster in wall-clock time than a few exact, expensive ones. The size of the dataset pushes towards the mini-batch, but that's not the only reason to choose it, and the full batch isn't only a matter of small datasets either: it's used here because there are ten examples and it makes the arithmetic easier to follow by hand, not because ten marks any real boundary.
And letting vary brings in a word that until now was unnecessary: an epoch is one complete pass through all training examples. With , which is this lesson's case, a step and an epoch are the same thing (each step already runs through the whole set), which is why "step" has been enough on its own so far. As soon as , the two stop matching: it takes steps to complete one epoch, and training in earnest means repeating many epochs, not one.
A step too long raises the loss
To see what actually does, it helps to use a loss whose sum can be worked out in closed form, so take the smallest one there is: a single parameter, , with the minimum at and . The rule becomes
a multiplication by the same constant at every step. Iterating from ,
and a power tends to zero exactly when its base is smaller than in absolute value. The full condition is , that is, , and the factor itself tells you what happens in each stretch:
| what does | ||
|---|---|---|
| between and | shrinks without changing sign | |
| drops onto the minimum in a single step | ||
| between and | alternates sign and shrinks | |
| alternates between and forever | ||
| less than | alternates and grows: diverges |
That particular in the limit belongs to this parabola and not to every one of them. With , the derivative becomes , the factor becomes , and the condition becomes : the more sharply the loss curves, the shorter the step has to be. The awkward part is that a real network curves differently in every direction, while stays a single number shared by all of them.
The explorable dives straight into that problem, : almost flat along and very steep along . The count above, applied coordinate by coordinate, asks for in the first direction and in the second, and the second one wins.
With (the value it starts at), the factor for the steep coordinate is exactly , and the trajectory does in two dimensions exactly what the fourth row of the table does in one: alternate without ever advancing. The first button's bowl is convex (a single bottom, so whoever descends ends up in the same place), and it survives the whole range of the slider: its steepest direction only demands . The fact that one step size can work for one direction and fail for another has fixes with their own names, momentum, Adam, and they lie outside this course.
With a single layer, the gradient comes out of a line
What's left is to bring the descent from down to a concrete parameter, and with the reviews' neuron the work is already half done. It's a neuron with a sigmoid on the output and cross-entropy as the loss, exactly the pair the previous lesson derived: for one example, . The only thing missing is how depends on each weight, and that comes straight from the neuron's own definition, , where each appears exactly once, multiplied by :
Chaining the two derivatives together,
which in vector form becomes
That formula is really saying two separate things, and they're worth pulling apart. The error is a single number that multiplies every coordinate the same way: it fixes how much correction there is to go around. And decides how that correction gets shared out: a vocabulary entry that never appears in the review has and its weight doesn't budge a thousandth, while one that appears twice pulls twice as hard. The neuron only ever learns from what it has actually seen.
The batch loss is the mean of the per-example losses, so its gradient is the mean of the individual gradients:
where the second form writes the first as a single product: with the predictions and labels are columns of numbers, has shape , and the result is exactly the column of coordinates you need. The bias comes out of that same sum, minus the :
All of this cost only two derivatives, because depends on directly: between a weight and the loss there sit a sum and a sigmoid, and nothing else.
Descent by hand, and a neuron that learns on its own
The first cell is the parabola from above, with no network and no NumPy involved: five steps from at the figure's three rates, alongside the closed form. Run it and compare the two blocks.
def loss(w):
return w * w
def descend(w0, eta, steps):
w = w0
trajectory = [w]
for _ in range(steps):
w = w - eta * 2.0 * w # w <- w - eta * dL/dw
trajectory.append(w)
return trajectory
w0 = 1.0
print("%6s %7s %s" % ("eta", "factor", " ".join(("w%d" % t).rjust(9) for t in range(6))))
for eta in [0.10, 0.40, 1.05]:
print("%6.2f %+7.2f %s"
% (eta, 1 - 2 * eta, " ".join("%9.5f" % w for w in descend(w0, eta, 5))))
print()
# The closed form gives the same numbers without taking a single step.
for eta in [0.10, 0.40, 1.05]:
loop = descend(w0, eta, 5)
closed = [(1 - 2 * eta) ** t * w0 for t in range(6)]
identical = all(abs(a - b) < 1e-12 for a, b in zip(loop, closed))
print("eta = %.2f loop and formula match: %-5s final loss %10.7f"
% (eta, identical, loss(loop[-1])))
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
The three rows match the three curves in the figure. With , the factor is and five steps leave at : it moves in the right direction but slowly. With , the factor is and within two steps is already . With , the factor is , the signs alternate, the magnitudes grow, and the final loss is : two and a half times what it was at the start, after five steps that were supposed to lower it.
The second cell trains for real. The neuron is the same one from the lesson on the artificial neuron, with the same ten reviews and the same vocabulary, except this time its weights start at zero: with and , the sigmoid answers across all ten, the worst score among the three networks the previous lesson ranked. There isn't a single random number anywhere in this cell, so you'll see these exact values and no others. Run it and watch the loss fall.
# --- The usual ten reviews, their vocabulary and their labels.
reviews = [
"the movie is fun and i recommend it",
"the movie is slow and boring",
"good movie i recommend it",
"bad movie very slow",
"fun and good",
"the movie is bad",
"boring and slow",
"i recommend it",
"good movie but slow",
"fun and i recommend it",
]
y = np.array([1., 0., 1., 0., 1., 0., 0., 1., 0., 1.])
V = ["bad", "boring", "fun", "good", "movie", "recommend", "slow", "the"]
X = np.array([[t.split().count(e) for e in V] for t in reviews], dtype=float)
def cross_entropy(y_hat):
p = np.clip(y_hat, 1e-12, 1.0 - 1e-12)
return -np.mean(y * np.log(p) + (1.0 - y) * np.log(1.0 - p))
w = np.zeros(len(V)) # all weights at zero
b, eta, B = 0.0, 1.0, len(y) # bias, learning rate, batch size
for step in range(201):
y_hat = 1.0 / (1.0 + np.exp(-(X @ w + b))) # forward pass
if step % 25 == 0:
print("step %3d loss %.4f correct %2d of 10"
% (step, cross_entropy(y_hat), int(((y_hat >= 0.5) == (y == 1.)).sum())))
if step == 200:
break
error = y_hat - y # (B,)
w -= eta * (X.T @ error) / B
b -= eta * float(error.sum()) / B
by_hand = np.array([-1., -1., 1., 1., 0., 1., -1., 0.])
print("\nwith the weights I set by hand:",
"%.4f" % cross_entropy(1.0 / (1.0 + np.exp(-(X @ by_hand)))))
for entry, weight in sorted(zip(V, w), key=lambda pair: -pair[1]):
print(" %-11s %+6.3f" % (entry, weight))
print(" %-11s %+6.3f" % ("(bias)", b))
The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.
The first line is the starting point: loss , which is , and five correct out of ten, the score of any network that gives the same answer to all ten reviews. Twenty-five steps later, the loss is down to and the correct count has reached ten, and by step it's fallen all the way to . Compare that with the from the eight weights I set by eyeballing the reviews: the descent overtakes my version before step and keeps improving from there. Nobody ever told the neuron what any word means.
The final weights confirm it. The largest, , goes to recommend, and the smallest, , to slow: entirely on its own, the descent has sorted the opinion words by sign. But look at the two that expose the limit of a set of only ten examples. movie ends at and the at , and neither one says anything at all about a film. Both come out negative for the same reason, and it's a coincidence, not a meaning: movie shows up in four negative reviews and two positive ones, the in two negative and one positive, so across these ten they read as evidence against. The descent does exactly what it was asked to do, lower the loss over these ten reviews rather than over the language itself, and in doing so it hands movie a weight against the film heavier than some words carrying a real opinion ever get. With only ten examples, a neuron can't tell an opinion apart from a coincidence.
Test your intuition
Three questions: a descent worked out by hand, why the step heads in that particular direction, and what the method guarantees against what it doesn't.
The descent runs on starting from , with . What is ?
A margin of ±0.01 is accepted.
Of all the directions the parameters could move in, why does the step go in that of ?
Tick everything that is true about what gradient descent guarantees and what it does not.
Select every correct option. This is graded all-or-nothing: there is no partial credit.
The method is complete, and the neuron that has now finished learning has exactly one layer. That's not an incidental detail of the example, it's the very condition that made the last section possible: between a weight and the loss there are two steps, and two chained derivatives close out the count. Put a hidden layer in between, and the path branches. A weight in the first layer moves the output of its hidden neuron; that output feeds into every neuron of the next layer, and each one contributes to the loss in its own way. The descent's rule doesn't change by a single letter. What stops being available is the gradient itself, and without it there's nothing left to subtract.
The chain rule used twice already, the one-variable version, doesn't cover that case: it's written for a quantity that reaches the next one by a single path. How a composition gets differentiated when the intermediate variables are vectors, and what object replaces the derivative once there are many inputs and many outputs at once, is the next lesson, on the chain rule. Turning that into a procedure that walks an entire network without repeating a single computation takes up the rest of this block.
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.
- Deep Learning, ch. 4: Numerical Computation
Its §4.3 explains why the step goes against the gradient (the directional derivative) and how curvature (the Hessian's condition number) sets the longest step that will not diverge: this lesson's x² + 20y² stated in general.
- An overview of gradient descent optimization algorithms
Picks up where this lesson leaves off: momentum, Adam and the other methods that let a single learning rate serve every direction, plus splitting the batch into pieces. A review, not a from-scratch derivation.