Downhill: Gradient Descent, and the Two Steps Everyone Skips
Compute the exact ceiling on a learning rate, then watch a brute-force search over 3,600 directions rediscover the gradient without being told.
On this page
The previous chapter ended with a valley.
Not a metaphorical one: an actual curve, the loss plotted against a single parameter, dipping down and coming back up. And the loss under it was not picked because it was tidy — it was derived, from a statement about the noise in the measurements, and the squared error came out the other end as a consequence rather than a convention.
So we have a landscape with a bottom, and a reason to believe the bottom is the right place to be. What we do not have is a way to get there.
This chapter builds one, and it is the algorithm that trains every model in the rest of this course — every one, without exception, up to and including the ones with hundreds of billions of parameters. It fits in about twenty lines. The two hard parts are not in those twenty lines, and they are the two things almost every explanation skips:
- Why the minus sign. The update subtracts the gradient. Every tutorial writes it; very few say why the gradient is the direction that goes up, which is the only fact that makes the minus sign anything other than an act of faith.
- How big a step. "Too large diverges, too small is slow" is true and useless. There is an exact number, it is computable from the loss, and this chapter computes it twice — once for a toy parabola and once for the actual data.
The setup, and why you cannot just search
Link to the section: The setup, and why you cannot just searchRestated so this chapter stands on its own: the eight parts from the conveyor belt of Chapter 1, but asked a different question. Not accept or reject — that comes back later — but predict a part's weight from its width.
import numpy as np
WIDTH = np.array([18.0, 19.5, 20.2, 21.0, 24.0, 25.5, 23.0, 26.0])
WEIGHT = np.array([47.0, 52.0, 49.0, 55.0, 61.0, 66.0, 70.0, 58.0])
x = WIDTH - WIDTH.mean() # 22.15 mm
y = WEIGHT - WEIGHT.mean() # 57.25 gThe measurements are centred, exactly as in Chapter 1 and for a reason that returns with interest before this chapter is over. The model is a line, , and the loss is the mean squared error the previous chapter derived:
Two parameters. Why not just try lots of values? Let us actually do it — a grid from to and to , in steps of :
grid 501 x 1001 = 501,501 evaluations in 3.67 s
best found: a = 2.1000, b = -0.0000, L = 24.592450Half a million evaluations to pin two numbers down to two decimal places — and that second is wall clock on one machine, so a rerun lands anywhere from three to six; the evaluation count and the minimum are the part that reproduces. Gradient descent, at the end of this chapter, gets four decimal places in eight steps and the full float64 answer in thirty-six.
But speed is not the argument, and this is the point that decides the whole course. Grid search costs evaluations for parameters at values each. With a thousand values per axis:
| model | parameters | grid evaluations |
|---|---|---|
| this line | 2 | |
| the XOR network of Chapter 5 | 9 | |
| a small multilayer network | 20,000 |
The third row is not a big number, it is a meaningless one — there are roughly atoms in the observable universe. Search does not get slower as models grow; it stops existing. Everything that follows exists because of that table.
A derivative is a measurement you can take
Link to the section: A derivative is a measurement you can takeFix for a moment so there is one parameter and one curve, which is the picture the last chapter left you with. Take a point on it, , and ask: if I nudge by a small amount , how much does the loss move, per unit of nudge?
That ratio is a rise over run — the slope of the straight line through two points on the curve. As shrinks, the two points slide together and the line becomes the tangent. Its slope is the derivative : the rate at which the loss changes per unit of change in . Not an approximation of anything, and not an infinitely small quantity. A limit of ordinary ratios.
It is worth running, because the numbers say something the definition does not:
def loss1(a):
return np.mean((a * x - y) ** 2)
for h in [1.0, 1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-12, 1e-14]:
q = (loss1(1.0 + h) - loss1(1.0)) / h
print(f"h = {h:<8.0e} slope estimate = {q:.10f} error = {abs(q + 16.385):.3e}")h = 1e+00 slope estimate = -8.9400000000 error = 7.445e+00
h = 1e-02 slope estimate = -16.3105500000 error = 7.445e-02
h = 1e-04 slope estimate = -16.3842555001 error = 7.445e-04
h = 1e-06 slope estimate = -16.3849925556 error = 7.444e-06
h = 1e-08 slope estimate = -16.3850003787 error = 3.787e-07
h = 1e-10 slope estimate = -16.3850444324 error = 4.443e-05
h = 1e-12 slope estimate = -16.3851154866 error = 1.155e-04
h = 1e-14 slope estimate = -17.0530256582 error = 6.680e-01Two things happen here and both are load-bearing.
The error is not vaguely proportional to — it is exactly . Divide by a hundred, the error divides by a hundred, to four significant figures every time. That constant is not decoration: it is half the second derivative of the loss, and it is the first appearance of an idea two sections from now — that a curve near a point looks like a line plus a correction proportional to .
And then the pattern breaks. Below the estimate gets worse, and at it is wrong in the second digit. Nothing mathematical happened; the last chapter's floating-point box did. and agree in their first ten digits, subtracting them destroys those digits, and dividing the wreckage by a tiny number amplifies what is left. There is a best — here around , roughly the square root of the machine epsilon — and going smaller is not more careful, it is less. Remember that; a function at the end of this chapter depends on it.
The exact slope, from calculus rather than measurement, is . So we can stop measuring and start deriving.
Composition, and the chain rule
Link to the section: Composition, and the chain ruleHere is the idea the rest of the course is built on, stated once, plainly.
To compose two functions is to feed one into the other: . Nothing more.
A deep network is not like a composition. It is one. A layer is a function; stacking layers is composing them; "depth" is the number of functions in the chain. When Chapter 5 builds a network, it is building and nothing else. Which means the single most important rule of calculus, for our purposes, is the one that differentiates a composition:
Rates multiply. If changes three times as fast as , and changes twice as fast as , then changes six times as fast as . That is the whole content, and it is why a signal passing back through ten layers gets multiplied by ten numbers — which is why Chapter 6 spends a section on what happens when those numbers are all slightly less than one.
Use it on our loss. Write the residual , so that . Each depends on through the inner function , whose derivative is . Chain rule, term by term:
Those curly symbols mark a partial derivative: differentiate with respect to one variable and treat every other as a constant. Nothing new happens — it is the same limit as before, taken along one axis. Collect the partials into a vector and you have the gradient:
At the point that vector is . Two numbers. The question is what they mean, and this is the first step everyone skips.
Why the gradient points uphill
Link to the section: Why the gradient points uphillThe gradient is a vector of slopes along the axes. That is all we have proved. It is not obvious — it should not be obvious — that assembling them into a vector produces something that points anywhere in particular.
So define the thing we actually want. Pick a unit vector , a direction. The directional derivative is the rate the loss changes as you walk that way:
The chain rule turns this into something computable. Walking along changes at rate and at rate , and the contributions add:
The rate of change in any direction is the dot product of the gradient with that direction. And now the punchline, which is one line of geometry. Writing the dot product with the angle between the vectors,
since has length 1. The only thing you control is , which is largest at and smallest at half a turn, degrees. So:
- The steepest ascent is along itself, and the slope there is exactly .
- The steepest descent is along , and the slope there is .
- Perpendicular to the gradient, the loss does not change at all. That is why a contour map's lines cross the gradient at right angles.
That is the minus sign. Not a convention, not a sign flip someone chose: the direction of fastest decrease is the negative gradient because is minimised at half a turn, and for no other reason.
Since this is a claim about all directions, test it against all directions. Sample 3,600 of them, one per tenth of a degree, and measure each one by nudging:
theta = np.array([1.0, 4.0])
g = grad(theta)
print("gradient ", g)
print("its length ", np.linalg.norm(g))
print("its angle ", np.degrees(np.arctan2(g[1], g[0])) % 360, "degrees")
best = max(
((loss(theta + 1e-6 * u) - loss(theta - 1e-6 * u)) / 2e-6, np.degrees(ang))
for ang, u in (
(a, np.array([np.cos(a), np.sin(a)])) for a in np.arange(3600) * 2 * np.pi / 3600
)
)
print("steepest slope", best[0], "at", best[1], "degrees")gradient [-16.385 8. ]
its length 18.23371122399386
its angle 153.97598928042032 degrees
steepest slope 18.233709624837502 at 154.0 degreesA search that knows nothing about gradients, over 3,600 directions, finds its steepest climb at 154.0 degrees — the gradient's own direction, to within the 0.1-degree resolution of the search. And the slope it finds there, 18.2337, is the gradient's length to six figures. The theorem is not a story about what gradients mean; it is a measurable fact, and that is the measurement.
Why a small step downhill actually helps
Link to the section: Why a small step downhill actually helpsNow the second skipped step. We know which way is down. It does not follow that walking that way lowers the loss, because "down" is a statement about an infinitesimal nudge and a step is not infinitesimal.
The bridge is linearisation. Near a point, a smooth function is its tangent plus a correction:
That is the first-order Taylor expansion. The discarded is the curvature — the same term that made the slope table's estimate wrong by exactly . Put in the step we intend to take, :
The loss drops by . Every part of that is non-negative, so the promise is real — for a small enough , because the neglected term grows like and eventually eats it. That is the whole theory. Here is the promise being kept, and then broken:
eta = 0.2 promised 66.49364500 delivered -16.01619240 ratio -0.240868
eta = 0.1 promised 33.24682250 delivered 12.61936315 ratio 0.379566
eta = 0.01 promised 3.32468225 delivered 3.11840766 ratio 0.937957
eta = 0.001 promised 0.33246822 delivered 0.33040548 ratio 0.993796
eta = 0.0001 promised 0.03324682 delivered 0.03322620 ratio 0.999380
eta = 1e-05 promised 0.00332468 delivered 0.00332448 ratio 0.999938Read it from the bottom. As shrinks the delivered drop converges on the promised one — ratio 0.99938, then 0.99994 — which is Taylor's theorem being correct. Read it from the top and at the delivered "drop" is negative sixteen. The step went downhill and the loss went up.
So the update rule is
and it comes with a condition nobody states, which is that is small enough. Small enough compared to what, exactly, is the next section.
The learning rate has a ceiling, and it is computable
Link to the section: The learning rate has a ceiling, and it is computableStart with the simplest valley there is, , where . One step of gradient descent is
The position is multiplied by every step. That is a geometric sequence, and geometric sequences have exactly one rule: they shrink when the multiplier is smaller than 1 in absolute value and grow otherwise. So , which is .
The boundary is at exactly. Not "around 1", not "1 is usually too big". At the multiplier is and the point bounces between and forever, neither approaching nor escaping. Below it, converge; above it, diverge. The interval splits again at , where the multiplier changes sign: below that the approach is monotone, above it the point overshoots and alternates sides, and at exactly the multiplier is 0 and one single step lands on the minimum.
Four regimes, from four lines of algebra. Go and cross the boundaries yourself:
And now the interesting one:
Now the general rule, which falls out of the same argument. The multiplier was really , and near a minimum a multi-parameter loss has one such number per direction — the eigenvalues of the matrix of second derivatives. Every direction has to be stable at once, so the ceiling is set by the largest:
For , , ceiling 1, which is what we just derived. For our belt, the second-derivative matrix is with the two-column matrix of inputs, and its eigenvalues are 2 and 14.89, so the ceiling is . That is a prediction with five significant figures in it. Test it:
lr=0.1343 -> L = 24.5924
lr=0.13431 -> L = 24.5924
lr=0.13432 -> L = 4707.8 BLEW UP
lr=0.13433 -> L = 4.00452e+16 BLEW UP
lr=0.1344 -> L = 1.18229e+107 BLEW UPFive decimal places of agreement between a line of linear algebra and a hundred thousand iterations of a for loop.
And here is where Chapter 1 comes back. Everything above used the centred measurements. Run the identical code on raw millimetres and grams and the eigenvalues are 0.0298 and 998.1 instead of 2 and 14.89. The ceiling collapses from 0.134 to 0.002004 — just as exactly, converging at lr=0.002003 and blowing up at lr=0.002004.
Worse than the ceiling is the ratio between the eigenvalues. The condition number measures how far from round the valley is: a long thin trench forces a rate small enough for the steep walls, and then the floor of the trench gets walked at that same crawl. Ours goes from 7.44 centred to 33,452 raw. With the best rate each version can take:
| features | condition number | best rate | steps to within 1% of the optimum |
|---|---|---|---|
| centred | 7.44 | 0.1184 | 10 |
| raw millimetres and grams | 33,452 | 0.0020037 | 79,513 |
Same data, same code, same answer at the end — and eight thousand times the work, because nobody subtracted a mean. In Chapter 1 the same omission cost the perceptron a factor of six thousand in epochs, and the diagnosis there was geometric: the data floated far from the origin. It is the same geometry here in an optimisation costume, and it is why input normalisation is not hygiene advice but arithmetic.1
Twenty lines
Link to the section: Twenty linesNothing above needed a library. Here is the whole optimiser.
def loss(theta):
a, b = theta
return np.mean((a * x + b - y) ** 2)
def grad(theta):
a, b = theta
residual = a * x + b - y
return np.array([np.mean(2 * residual * x), np.mean(2 * residual)])
def descend(theta, lr, steps):
theta = np.array(theta, dtype=float)
for _ in range(steps):
theta = theta - lr * grad(theta)
return theta
theta = descend([0.0, 0.0], lr=0.05, steps=60)
print(theta, loss(theta))[ 2.10040296e+00 -2.76445533e-15] 24.592448791134984The closed-form least-squares answer for these eight points is , , with a loss of . The loop found it to eight significant figures without knowing that a closed form exists — which matters, because from Chapter 5 onward there will not be one.
The trajectory, since watching it is the point:
0 a=0.000000 b=0.000000 L=57.437500
1 a=1.563750 b=0.000000 L=26.736582
2 a=1.963288 b=-0.000000 L=24.732418
5 a=2.098116 b=-0.000000 L=24.592488
10 a=2.100400 b=-0.000000 L=24.592449
60 a=2.100403 b=-0.000000 L=24.592449Most of the distance is covered in the first two steps, because the gradient is largest when you are furthest from the bottom and shrinks as you approach. Gradient descent slows down automatically near a minimum. That is a feature and it is also, in Chapter 6, a problem.
Where else the slope is zero
Link to the section: Where else the slope is zeroThe argument so far has a hole in it. The step stops when , and we have been calling that "the minimum". A point with zero gradient is a critical point, and being a minimum is only one of the ways to be one:
- a local minimum: uphill in every direction, but possibly not the lowest such point anywhere;
- a local maximum: downhill in every direction;
- a saddle point: uphill in some directions and downhill in others. The surface has , which is zero at the origin, where the function is a minimum along the -axis and a maximum along the -axis at the same time.
Gradient descent cannot tell these apart, because it only ever looks at the gradient, and the gradient is zero at all three.
Our line has one critical point and it is the answer — a squared-error loss over a linear model is convex, a single bowl, and descent on it cannot fail to find the global minimum. That property does not survive contact with this course. The loss of a neural network is not convex, and from Chapter 5 onward "the minimum" is not a thing that exists: there are many, of different depths, and which one you get depends on where you started. That is one sentence and stays one sentence, because the theory is large and the practical consequence is small.
You can see the whole consequence on one curve. Take , which has two valleys of different depths:
x = -1.046681 f(x) = -0.352386 minimum
x = 0.101031 f(x) = 0.005026 maximum
x = 0.945649 f(x) = -0.152639 minimumLanding in the shallow valley is 56.7% worse in loss, and the algorithm has no way to know, because from inside a valley every direction is uphill. There is no repair for this in gradient descent and none is coming. What there is, in practice, is the finding that it matters far less than this picture suggests — in the very high dimensions of a real network most critical points turn out to be saddles rather than traps,2 and Chapter 5 measures how often a small network actually gets stuck.
Cheaper steps: stochastic, minibatch, momentum
Link to the section: Cheaper steps: stochastic, minibatch, momentumOne thing about grad above should bother you: it sums over the entire dataset for every step. Eight parts is nothing. A million is a million gradient computations to move the parameters once.
The escape is that the gradient is an average, and an average can be estimated from a sample. Compute it on a random handful — a minibatch — and step on that. The estimate is noisy; it is also unbiased, and hundreds of cheap noisy steps beat one expensive exact one. On a hundred thousand synthetic parts, counting per-example gradients rather than steps:
| method | steps to within 0.1% of the optimum | per-example gradients |
|---|---|---|
| full batch | 7 | 700,000 |
| minibatch of 32 | 100 | 3,200 |
| one example at a time | 17,580 | 17,580 |
Two hundred and nineteen times less arithmetic to reach the same place. And the extreme — one example at a time, the original stochastic approximation of Robbins and Monro3 — is not the winner: it is five times worse than batches of 32, because 32 examples cost almost nothing more than one on hardware that multiplies matrices, while the noise falls off with the square root of the batch size. That trade-off is why every training script you will ever read has a batch_size in it.
Momentum is the other cheap fix, and it is aimed squarely at the trench. In a badly conditioned valley the steps zig-zag across the narrow direction while creeping along the long one. Momentum keeps a running average of past gradients, so the oscillating components cancel and the consistent one accumulates:4
Two extra lines. On the raw uncentred belt — condition number 33,452, the worst case we have — at the best rate plain descent can take:
momentum beta=0.0 -> 79,513 steps to 1%
momentum beta=0.9 -> 1,609 steps to 1%
momentum beta=0.99 -> 461 steps to 1%A factor of 172 for two lines of code. Chapter 6 turns this into Adam; the mechanism is already here.
The check you will need in Chapter 5
Link to the section: The check you will need in Chapter 5Every gradient in this chapter was derived by hand and could therefore be wrong. The fix is the slope table from the beginning: measure the derivative numerically and compare. Use the central difference, , which cancels the leading error term and is a great deal more accurate for the same .
def numeric_grad(f, theta, h=1e-5):
theta = np.asarray(theta, dtype=float)
out = np.zeros_like(theta)
for i in range(theta.size):
bump = np.zeros_like(theta)
bump[i] = h
out[i] = (f(theta + bump) - f(theta - bump)) / (2 * h)
return out
def gradcheck(f, df, theta, h=1e-5):
analytic = np.asarray(df(theta), dtype=float)
numeric = numeric_grad(f, theta, h)
return np.max(np.abs(analytic - numeric) / np.maximum(1e-8, np.abs(analytic) + np.abs(numeric)))The relative form of the comparison matters: an absolute difference of is a disaster on a gradient of size and irrelevant on one of size .
relative error: 1.8929136036763527e-11
with 2 dropped: 0.33333333331650744The first line is the hand-derived gradient above. The second is the same function with the factor of 2 left off one component — a typo of a single character — and the check catches it immediately. Anything below about is agreement; anything above is a bug. Keep this function: Chapter 5 uses it to debug an automatic differentiation engine, and it is the only reason a wrong gradient is findable at all.
Where this goes next
Link to the section: Where this goes nextEverything in this chapter rested on one assumption that was never stated: that you can write down.
For a line with two parameters, that was a line of algebra. It stops being one almost immediately. Ask a symbolic algebra system for the derivative of a network's loss with respect to a single first-layer weight, for a single example, and count the arithmetic in the answer:
| network | operations in one partial derivative |
|---|---|
| four hidden units, one layer | 40 |
| four hidden units, two layers | 301 |
| four hidden units, three layers | 1,717 |
The third row is a network with 57 parameters — a network so small it would be a footnote in Chapter 6 — and writing its gradient out by hand means about 97,869 operations for one training example. There is no notation that rescues this. What rescues it is the observation that the chain rule applied to a composition has enormous structure, that the same intermediate quantities appear over and over, and that computing them in the right order gets all the derivatives for roughly the price of one forward pass. That is Chapter 5.
But there is a smaller problem first, and it is waiting immediately.
We now have a machine that will roll downhill on any differentiable loss. Point it at the belt's original question — accept or reject, a target that is 1 or 0 — put a sigmoid on the output so it predicts a probability, and minimise squared error. It will run. It will also barely move when it is most wrong, and the gradient says why:
| output | prediction | truth | gradient with squared error | gradient with cross-entropy |
|---|---|---|---|---|
| 0.5000 | 1 | |||
| 0.1192 | 1 | |||
| 0.0025 | 1 | |||
| 1 |
A model that is confidently, catastrophically wrong — predicting 0.0000454 when the answer is 1 — produces a squared-error gradient of . It has no idea it is in trouble. The other column, from a loss we have not derived yet, reports 1.0: maximum urgency, exactly where it is deserved.
Which raises the question the next chapter opens with. The last chapter said a loss is an assumption about the noise, and squared error assumes Gaussian noise. What noise model does a yes-or-no answer have — and what loss comes out when you run the same derivation on it?
Sources and method
Link to the section: Sources and methodThe method is older than all of these: Cauchy described it in a note to the Académie des Sciences in 1847, as a way of solving systems of equations by walking downhill on the sum of their squared residuals. Also worth reading alongside this chapter: Sebastian Ruder's An overview of gradient descent optimization algorithms (arXiv:1609.04747), which covers momentum through Adam in fourteen readable pages; chapter 3 of Nocedal and Wright's Numerical Optimization (2nd ed., Springer, 2006), whose theorem 3.3 gives the convergence rate of steepest descent on a quadratic in terms of the condition number — it is the theory behind why conditioning decides the step count, though it treats line search rather than the fixed-step ceiling measured above, or §5.8 and §7.1 of Deisenroth, Faisal and Ong's Mathematics for Machine Learning for the same ground with less machinery; §6.1 of Prince's Understanding Deep Learning and §4.3 of Goodfellow, Bengio and Courville's Deep Learning; Dive into Deep Learning §12.1–12.3, which has the minibatch analysis with more measurements than there is room for here; and chapter 4 of Géron's Hands-On Machine Learning (3rd ed.), the most practical treatment of the learning rate as a thing you tune rather than derive. The MIT 6.390 notes put gradient descent before classification, as this course does and for the same reason.
References
Link to the section: References-
LeCun, Y., Bottou, L., Orr, G. B. and Müller, K.-R. Efficient BackProp, in Neural Networks: Tricks of the Trade (Springer, 1998), pp. 9–50. Section 4.3 gives the recommendation and section 5.1 the argument used in the detail box above: centring and scaling inputs changes the eigenvalues of the second-derivative matrix, and therefore the number of steps, not merely the numerical comfort. ↩
-
Dauphin, Y. N., Pascanu, R., Gulcehre, C., Cho, K., Ganguli, S. and Bengio, Y. Identifying and attacking the saddle point problem in high-dimensional non-convex optimization, arXiv:1406.2572 (2014). The argument that in high dimensions critical points are overwhelmingly saddles rather than local minima, since a minimum requires every one of thousands of directions to curve upward at once. ↩
-
Robbins, H. and Monro, S. A Stochastic Approximation Method. Annals of Mathematical Statistics 22(3), pp. 400–407 (1951). The paper that established that a noisy estimate of a gradient is enough, given a step size that shrinks in the right way. ↩
-
Polyak, B. T. Some methods of speeding up the convergence of iteration methods. USSR Computational Mathematics and Mathematical Physics 4(5), pp. 1–17 (1964). The heavy-ball method, which is the momentum update above, twenty-two years before backpropagation reached this field. ↩