5. Calculus for learning
Why this layer exists
Training a model means choosing its weights. Chapter 4 fixed what “good weights” means: low cross-entropy on the training data, equivalently high likelihood. For a categorical distribution the best weights had a closed form, the normalised counts. For a network they do not, because every output probability depends on all the weights at once, and there are far too many weights to search. What is cheap to compute, at the current weights, is how the loss would change if each weight moved a little. That vector of sensitivities is the gradient, and it points in the direction in which the loss rises fastest. Gradient descent takes a small step the other way and repeats.
Every model trained in this book is trained this way: the bigram, MLP and transformer language models of Part II, the chat fine-tune of chapter 13, and the QLoRA fine-tune of chapter 46 on dixie’s 12 GB RTX 3060, where the base weights stay frozen and only a small set of added low-rank weights receives the steps. The optimizers used on real models rescale and smooth the step, per coordinate and over time; the step is still built from the gradient. The one knob this chapter explains fully is the learning rate, the step size. Too small and training crawls; too large and the loss oscillates and then grows without bound until it overflows to inf and turns into nan. The threshold between the two is a number you can compute, and the lab computes it for a real problem and watches the runs on either side of it.
Chapter 6 builds an engine that computes gradients automatically. The only way to trust such an engine is to compare its answers with a slow, independent estimate, and the numerical gradient built here is that estimate.
Mechanism
The derivative is a local linear approximation
Zoom in far enough on the graph of a smooth function and it looks like a straight line. The slope of that line at is the derivative , and the line itself is the approximation
good for small . Precisely, is the number that makes the error of this approximation small compared with itself:
when the limit exists. The ratio is the slope of the chord from to ; the limit is the slope the chords approach as the second point slides in. Write the error as
The definition says exactly that .
The reading that matters for learning: if , a small decrease in lowers ; if , a small increase does; and the size of says how much changes per unit of , to first order. says nothing to first order, which is what happens at a minimum.
Derivatives from the definition
The functions that appear in the losses and layers of this book are built from powers, the exponential and the logarithm. Each derivative follows from the limit.
Squares. Expand and cancel:
Positive integer powers. Multiplying out gives , then terms with one factor of (each ), then terms with or higher powers. This is the binomial expansion:
Subtract and divide by : the first remaining term is and every other term still carries at least one factor of , so
The exponential. , so
Everything rests on the second factor. Take the power series as the definition of the exponential; then
and for the terms after the 1 add up to at most , which goes to 0 with . The limit is 1, and
is the base for which the exponential’s slope at 0 is exactly 1; for any other base , has slope there. At the ratio is 1.0005.
The logarithm. means , for . Let , the change in the log; then , so , and
As , too, since the logarithm is continuous, and the denominator is a chord slope of the exponential tending to . So
Sum, product and chain rules
Sums and constant multiples. The difference quotient of is times that of plus times that of , and limits respect sums, so . Differentiation is linear.
Products. Write and , with everything on the right evaluated at and both remainders small compared with . Multiply:
Every term in the brackets, divided by , goes to 0: , and , vanish by definition. What is left is the product rule, .
Composition. For , first approximate the inner function, then the outer one at the point . The inner function moves by , and the outer function, moved from by , changes by with as . Substituting :
The bracket, divided by , goes to 0. Its first term does because . For the second, as , and stays bounded, so ; when the term is outright. The coefficient of is the derivative:
This is the chain rule. A composition of local linear approximations is a local linear approximation whose slope is the product of the slopes. Chapter 3 showed that composing linear maps multiplies their matrices; the chain rule for functions of many variables is the same statement with each slope replaced by a matrix, and chapter 6 is built on it.
Two consequences come at once. Any real power: for , , so the chain rule gives , extending the integer rule. And the quotient rule is not needed separately: , whose derivative is by the power rule and the chain rule.
A worked composite. Take
This is the cross-entropy of a two-outcome model whose probability for the observed outcome is , since . Peel it from the outside. The outer function is with , contributing . The inner function’s derivative is the derivative of , itself a composite: outer , inner , so . Multiply:
the last step multiplying top and bottom by . At this is , and the chord slope also comes out . The derivative is always negative and tends to 0 as grows: raising the logit of the correct outcome always lowers the loss, by less and less once the model is confident. Exercise (a) meets the same function from the other side.
Partial derivatives
A loss depends on many numbers. For , the partial derivative is the ordinary derivative in with every other coordinate held fixed. For :
treating as a constant in the first and in the second.
Partials combine into a local linear approximation in dimensions. Move from by a small step , shape (d,), one coordinate at a time. The first move changes by about ; the second by about , with that partial evaluated at the already-shifted point, which differs from its value at by an amount that vanishes as as long as the partials are continuous, as they are for every function in this book. Summing the moves,
The gradient points uphill
Collect the partials into a vector, the gradient:
shape (d,), the same shape as . The sum above is a dot product, so the local approximation is
This is the form chapter 4 used, ahead of time, to find the maximum-likelihood distribution. Now ask which direction increases fastest. Take a unit vector and step a distance along it, . The rate of change of along , the directional derivative, is
where is the angle between and , by chapter 2’s identity with . Only depends on the direction. It is largest, 1, at : rises fastest along , at rate . It is smallest, , at : falls fastest along . At the rate is 0: directions perpendicular to the gradient are, to first order, level, and the gradient is perpendicular to the contour through .
Gradient descent
Given a loss over parameters , shape (P,), gradient descent repeats
with a learning rate . The local approximation with says what one step does:
a decrease whenever the gradient is nonzero and is small enough for the approximation to hold. Descent stops making progress where , a stationary point. In general that can be a minimum, a maximum or a saddle, and a minimum need not be the lowest one; for the loss this chapter fits, there is a single stationary point and it is the minimum. The only question left is what “small enough” means.
How large a step: one dimension
Take the simplest function with a minimum, with . Its derivative is , so one step of gradient descent is
The iterates go to the minimum at 0 exactly when , that is, , which for positive and is
Within that range there are two regimes, split at , where one step lands exactly on the minimum. Below it the factor is between 0 and 1: every step moves toward the minimum without passing it, and a small means many short steps. Above it the factor is between and 0: each step overshoots to the other side, landing closer than it started, and the iterates zig-zag inward. At the factor is and the iterate bounces between and forever; beyond it each overshoot lands farther out, and grows geometrically.
The constant is the curvature, the second derivative : how fast the slope changes. Any smooth function looks like this near a minimum . There , so the local approximation applied to gives , and a gradient step from moves the error to : the same recursion with . The stability limit is 2 divided by the curvature.
Many parameters: curvature by direction
The loss of this chapter’s lab is exactly quadratic in its parameters, of the form
with , and of shape (P,), and a symmetric (P, P) matrix. The double sum is ; its partial derivative in collects the terms with and those with , giving by symmetry. So , and differentiating once more, : is the matrix of second partial derivatives, the Hessian. A gradient step subtracts from the error:
In general points in a different direction from , so the coordinates are coupled. But some directions are only stretched: for a scalar . Such a is an eigenvector of , and its eigenvalue. Along an eigenvector the step is , which is the one-dimensional recursion with : is the curvature in that direction. A symmetric (P, P) matrix has mutually perpendicular eigenvectors, the spectral theorem (exercise (c) finds them by hand for ), so any error splits into components that evolve independently, each multiplied by its own per step.
Three facts follow. The loss has a unique minimum at only if every , a bowl in every direction; a zero eigenvalue is a direction along which the loss is flat. The iteration converges only if every factor has magnitude below 1, so
the steepest direction sets the largest usable learning rate. And the flattest direction sets the pace: its factor is , and with below that is at least . When the ratio , the condition number, is large, the safe step is tiny for the flat directions and plain gradient descent is slow. np.linalg.eigvalsh returns the eigenvalues of a symmetric matrix in ascending order.
The analysis is exact only for a quadratic, but near a minimum every smooth loss is close to one, for the reason the one-dimensional case gave. A learning rate that makes a real training run blow up is, locally, above of the loss surface where the run happens to be.
The linear-regression gradient
The lab fits the model to examples. , shape (n, d), holds the inputs as rows ; has shape (d,), is a scalar and the targets have shape (n,). The loss is the mean squared error
where is the residual of example . In index notation first. is linear in the parameters, with and . By the chain rule with outer function , , and by linearity
The first sum runs over the row index of , which is how an entry of is formed: . In matrix form,
Shapes: is (n, d) @ (d,), giving (n,); the scalar broadcasts across it; the residual vector is (n,); is (d, n), so is (d,), the shape of , as a gradient must be. Read as a sum, the gradient is the average over examples of : each example pulls along its own input, in proportion to how wrong its prediction is.
The bias fits the same pattern if it is treated as a weight on an input that is always 1. Append a column of ones to get , shape (n, d+1), and stack the parameters into , shape (d+1,). Then and both results above are one formula, . It is linear in with matrix
which is symmetric, since , and is the Hessian. The minimum is where the gradient vanishes, , and subtracting that from the gradient at any gives : the previous section’s quadratic exactly, with no approximation. The largest stable learning rate for linear regression is of this matrix, which depends only on the inputs. For one input feature,
with bars denoting averages over the examples: the entries of are , and . Exercise (c) evaluates it for the lab data.
Numerical gradients
The definition of the derivative suggests computing it directly: pick a small and evaluate the forward difference . How wrong is it? Extend the local approximation to more terms, . Setting gives ; differentiating times in and then setting leaves only on the right, so , the -th derivative over . This is the Taylor expansion:
Subtract and divide by :
The error shrinks in proportion to : . The central difference evaluates on both sides. Replacing by flips the sign of the odd-power terms:
the even-power terms cancelling in the subtraction. Divide by :
The error is now : dividing by 10 divides it by 100. For the same two evaluations per coordinate, the central difference is far more accurate, and it is what numerical_grad uses.
Taylor’s error says a smaller is always better. Floating point says otherwise. A float64 stores about 16 significant decimal digits; the unit roundoff is , and each computed value of carries an error of order . For small , and agree in most of their leading digits, and subtracting them cancels those digits and leaves the rounding errors behind at the same absolute size. Dividing by then magnifies them: the rounding part of the error is of order and grows as shrinks. The total error of the central difference is roughly
minimised where the two terms are of comparable size, around when and are of order 1. For the forward difference the same balance, against , puts the best near and the best error near as well. numerical_grad defaults to , near the central-difference optimum; exercise (d) measures both curves.
A numerical gradient costs two evaluations of per parameter. For a single (1000, 1000) weight matrix that is two million evaluations of the loss, which rules it out for training. Chapter 6 computes exact gradients by the chain rule instead, and uses numerical_grad on small inputs to check them; chapter 10 checks its attention layer the same way.
Walkthrough
The reference module is py/tinygpt/descent.py.
def numerical_grad(f, x, eps=1e-6):
"""Central-difference estimate of the gradient of scalar f at x: (f(x+h) - f(x-h)) / 2h per coordinate."""
x = np.array(x, dtype=np.float64) # a private copy we can perturb
grad = np.zeros_like(x)
for idx in np.ndindex(x.shape):
orig = x[idx]
x[idx] = orig + eps
hi = f(x)
x[idx] = orig - eps
lo = f(x)
x[idx] = orig
grad[idx] = (hi - lo) / (2 * eps)
return grad
np.array always copies, so the function perturbs a private array and the caller’s x is never modified; converting to float64 also matters, because adding to an entry of an integer array would be truncated away. np.ndindex(x.shape) yields every index tuple of an array of any shape, so the same function differentiates with respect to a vector, a bias, or a (in, out) weight matrix. Each coordinate is perturbed up, then down, then restored before the next, so every partial derivative is taken with the other coordinates at their original values, which is what a partial derivative is. f must return a scalar; grad[idx] = ... assigns one number per entry.
def mse(pred, y):
"""Mean squared error."""
r = np.asarray(pred, dtype=np.float64) - np.asarray(y, dtype=np.float64)
return float((r * r).mean())
def linreg_grad(w, b, x, y):
"""Gradient of mse(x @ w + b, y) with respect to w (shape (d,)) and b (scalar)."""
n = x.shape[0]
r = x @ w + b - y # (n,)
dw = (2.0 / n) * (x.T @ r) # (d, n) @ (n,) -> (d,)
db = (2.0 / n) * r.sum()
return dw, float(db)
mse returns a Python float rather than a numpy scalar, like the functions of chapters 2 and 4. linreg_grad is the matrix form of the derivation line for line: the residual r, shape (n,), then for the weights and for the bias. x @ w + b relies on the broadcasting of chapter 3 to add the scalar bias to every prediction. The lab test test_linreg_grad_matches_numerical checks this function against numerical_grad at a random point to a relative tolerance of , the check chapter 6 uses to verify its engine.
def fit_linreg(x, y, lr, steps):
"""Plain gradient descent from w = 0, b = 0. losses[t] is the loss before step t, plus the final loss."""
w, b = np.zeros(x.shape[1]), 0.0
losses = [mse(x @ w + b, y)]
for _ in range(steps):
dw, db = linreg_grad(w, b, x, y)
w, b = w - lr * dw, b - lr * db
losses.append(mse(x @ w + b, y))
return w, b, losses
The tuple assignment w, b = w - lr * dw, b - lr * db evaluates both right-hand sides before either name is rebound, and linreg_grad computed both gradients at the old parameters, so the step is the simultaneous update of the mechanism. losses records the loss at the starting point and after each step, steps + 1 values in all, so losses[0] is the loss of the all-zero model and losses[-1] the loss of the returned one.
Exercises
(a) Differentiate the logistic sigmoid and show that .
Answer
Write with . The chain rule gives , and by the chain rule again (outer , inner ). So
The first factor is . The second is , since . Hence .
Two things follow. The derivative can be computed from the function’s output alone, with no further exponential, which is useful when the output is already stored. And since is a product of two numbers in that sum to 1, it is at most , reached at , and it tends to 0 as grows: a sigmoid unit far into either tail passes almost no gradient back through the chain rule. The worked composite of the mechanism was , and its derivative is , which is , as the chain rule through the logarithm says it must be.
(b) Express the derivative of in terms of itself.
Answer
Write and . Their derivatives swap: and . By the product rule on , with from the chain rule,
Like the sigmoid, the derivative comes from the output: it is 1 at and tends to 0 as . The two functions are related by , and differentiating that gives the same result: , and with , and , so .
(c) The lab tests generate data as , , seed 0, with plus small noise. Estimate the largest stable learning rate for fit_linreg on this data from the quadratic analysis, and check it against test_fit_linreg_diverges_when_lr_too_large, which expects divergence at 2.0.
Answer
The Hessian needs two averages of the inputs. Computed from the test data (x.mean() and (x ** 2).mean()), and , so
Its eigenvalues, by hand: for a symmetric , is the pair of equations and . Multiply the first by , the second by , and subtract: . Doing the same to eliminate gives the same bracket times , so a nonzero needs the bracket to vanish: , whose roots are
so and . The two eigenvectors are perpendicular, the spectral theorem for this case: , using , and with that forces . The bound is
At both directions are unstable, with factors and , so the test is far over the line: the loss goes from 11.4 at the start to 72.1 after one step and after twenty.
Two ways to get the bound wrong are instructive. Ignoring the bias, the curvature in alone is and the bound would be , but the bias adds a direction of curvature and couples to through ; a run at diverges, reaching a loss of after 300 steps. And a run just over the true bound does not blow up at once. The starting error happens to be nearly perpendicular to the steep eigenvector, about : its component along it is 0.0016, against 3.6 along the flat one. At the steep factor is and that tiny component needs many steps to matter; the loss falls below at step 13 and is back up to 3.25 at step 300.
(d) For at , compute the error of the forward and central differences for , plot both against on log–log axes, and explain both slopes and the floor.
Answer
import math
x = 1.0
exact = math.exp(x)
print(" eps forward central")
for k in range(1, 13):
h = 10.0 ** -k
fwd = (math.exp(x + h) - math.exp(x)) / h
cen = (math.exp(x + h) - math.exp(x - h)) / (2 * h)
print(f"1e-{k:02d} {abs(fwd - exact):.2e} {abs(cen - exact):.2e}")
eps forward central
1e-01 1.41e-01 4.53e-03
1e-02 1.36e-02 4.53e-05
1e-03 1.36e-03 4.53e-07
1e-04 1.36e-04 4.53e-09
1e-05 1.36e-05 5.86e-11
1e-06 1.36e-06 1.63e-10
1e-07 1.40e-07 5.86e-11
1e-08 6.60e-09 6.60e-09
1e-09 2.15e-07 6.60e-09
1e-10 1.55e-06 6.73e-07
1e-11 3.26e-05 1.04e-05
1e-12 4.32e-04 2.10e-04
Each row is one decade of , so on log–log axes the slope is the change in the error’s exponent per row. For large the forward error drops one decade per row, slope 1, and matches ; the central error drops two decades per row, slope 2, and matches . These are the leading error terms of the Taylor analysis.
Both then hit a floor and turn up, with slope about : the rounding error, of order , grows as shrinks. At that estimate is , the size of both measured errors. The central difference reaches its floor sooner and lower, about near to , where its truncation error meets the rounding error; the forward difference bottoms out near , around . Near the floor the values jump around, since the rounding error depends on exactly how and round, not smoothly on . The default of numerical_grad gives here, a relative error of .
(e) Why does the gradient of the mean loss not grow with the size of the dataset? What changes if the loss is the sum of squared residuals instead of their mean?
Answer
The gradient of a mean is the mean of the per-example gradients, by linearity: . For examples drawn from the same distribution, that average estimates a fixed expectation, and by chapter 4 it concentrates around it as grows rather than growing with . The Hessian is also an average, of , so its eigenvalues, and with them the stability bound, do not depend on either. A learning rate tuned on one dataset size carries over to another.
With the sum, the gradient and the Hessian are both times larger. The minimiser is the same, but every eigenvalue is multiplied by and the stability bound divided by . On the test data the bound falls from 0.9487 to 0.009487: gradient descent on the summed loss converges at , to a sum of squares of 0.0091 (100 times the mean-loss minimum of ), and diverges at , to after 300 steps. Training loops average the loss over each batch for this reason: a change of batch size then changes how noisy the gradient is, not its scale.
Lab
Implement every function in py/labs/ch05/starter.py, then run
make lab CH=05 IMPL=mine
until it passes. Read py/labs/ch05/test_lab.py first. It pins what the prose leaves open: numerical_grad must return for to within and must leave its input unchanged, including a 2-D input; mse of [1, 2] against [1, 4] must be exactly 2.0; linreg_grad must agree with numerical_grad at a random point; and fit_linreg must recover and to within 0.01 in 500 steps at , produce a non-increasing loss at , and end with a higher loss than it started with at . Derive linreg_grad on paper before writing it; the numerical test will say whether the derivation is right.
Then sweep the learning rate. Save this script outside the repository, for example as /tmp/lr_sweep.py:
import numpy as np
from tinygpt.descent import fit_linreg
rng = np.random.default_rng(0)
x = rng.normal(size=(100, 1))
y = 3.0 * x[:, 0] - 2.0 + 0.01 * rng.normal(size=100)
xb = np.hstack([x, np.ones((100, 1))]) # (100, 2): the bias is a weight on a constant input
hessian = (2 / 100) * xb.T @ xb # (2, 2)
curvatures = np.linalg.eigvalsh(hessian)
print("curvatures", curvatures.round(4), " stability bound 2/max =", round(2 / curvatures.max(), 4))
for lr in [0.01, 0.1, 0.5, 0.9, 1.1]:
w, b, losses = fit_linreg(x, y, lr=lr, steps=300)
hit = next((t for t, loss in enumerate(losses) if loss < 1e-3), None)
if hit is not None:
print(f"lr={lr:<4} loss < 1e-3 after {hit:3d} steps w={w[0]:.4f} b={b:.4f}")
else:
low = int(np.argmin(losses))
print(f"lr={lr:<4} diverged: lowest loss {losses[low]:.3g} at step {low}, {losses[-1]:.3g} at step 300")
and run it from py/ with PYTHONPATH=. uv run python /tmp/lr_sweep.py. The data is the test data. Each run takes 300 steps; the script reports the first step at which the loss is below , with the parameters after all 300 steps, or, for a run that never gets there, its lowest loss and its final loss. Change the import to from labs.ch05.starter import fit_linreg to run yours; the output should be the same:
curvatures [1.7565 2.108 ] stability bound 2/max = 0.9487
lr=0.01 loss < 1e-3 after 267 steps w=2.9858 b=-1.9907
lr=0.1 loss < 1e-3 after 25 steps w=3.0006 b=-2.0006
lr=0.5 loss < 1e-3 after 3 steps w=3.0006 b=-2.0006
lr=0.9 loss < 1e-3 after 9 steps w=3.0006 b=-2.0006
lr=1.1 diverged: lowest loss 0.851 at step 20, 3.31e+66 at step 300
Match each line to the analysis before moving on.
- The bound is 0.9487. The four learning rates below it converge and the one above it does not. The minimum loss is , the variance of the noise the data was generated with () up to sampling, and it sits at , , not exactly at 3 and , because the least-squares fit also fits the noise.
- Small rates are slow at the rate the flat direction sets. At the flat direction’s factor is , and the excess loss in that direction, for a component , shrinks by its square, 0.9651, per step. Starting from an excess of 11.42, reaching (an excess of ) takes steps; the run took 267, and after 300 it has still not settled to four decimal places. At the same estimate gives 24.4 and the run took 25.
- The fastest rate is not the largest stable one. At the factors are and , both small, and three steps suffice. The best single learning rate for a quadratic makes the two extreme factors equal and opposite, , factors . At , still stable, both factors are negative, and : the iterates overshoot on every step, the zig-zag of the figure, and convergence takes 9 steps.
- Divergence can hide. At the steep factor is , so that component grows, and the flat factor is , so that one shrinks. The loss falls for 20 steps, to 0.851, before it turns. The reason is the starting point: as exercise (c) found, the initial error has a component of only 0.0016 along the steep direction and 3.6 along the flat one, so the unstable part starts about times smaller in loss terms and gains on it by a factor of 2.0 per step (), so it needs about 22 steps to overtake it; the total loss turns at step 20, as the two become comparable. After that the loss grows by per step, reaching at step 300. A training curve that falls and then explodes need not mean the run went somewhere new; an unstable direction may have been growing from the start.
Finally, add 0.94, 0.95 and 0.96 to the list, straddling the bound. All three reach in 12 or 13 steps, because the flat direction does most of the work. After 300 steps the run at 0.94 has converged to the minimum, the one at 0.95 has started to climb back up (to from a low of at step 25), and the one at 0.96 is back at 3.25. Stopping at the first step below a threshold would have scored all three as successes.
Further reading
- Gilbert Strang, Calculus (3rd ed., 2017; free from MIT OpenCourseWare), chapters 2–4 and 6. Derivatives from the definition, their applications and the chain rule (chapters 2–4), and the exponential and logarithm (chapter 6).
- Marc Peter Deisenroth, A. Aldo Faisal and Cheng Soon Ong, Mathematics for Machine Learning (Cambridge University Press, 2020), chapter 5. Partial derivatives, gradients of vector- and matrix-valued functions, the multivariate chain rule, and Taylor series, in the notation of machine learning.
- Jorge Nocedal and Stephen J. Wright, Numerical Optimization (2nd ed., 2006), chapter 2. Optional depth: conditions for a minimum, Taylor’s theorem in many variables, and an overview of the line-search and trust-region methods that go beyond a fixed step.