W0
A gradient checker you can trust
From week 1 on you will derive every gradient by hand. This unit builds the instrument that tells you whether a derivation is right: compare it with a brute-force estimate that needs no calculus at all. If they agree to about seven digits, the derivation is almost certainly correct.
Needs: single-variable derivatives and Taylor’s theorem. These are Math course units C2 and C5 on the syllabus, not yet written; until then, Deisenroth, Faisal & Ong, Mathematics for Machine Learning, §5.1. Used by: every week that has a gradient in it.
The estimate
Section titled “The estimate”Definition W0.1Finite-difference estimates#
For and a step , the forward and centered differences are
For , estimate each partial derivative the same way along a coordinate direction (the vector with in position , elsewhere):
Both are slopes of a chord. As they tend to , if is differentiable at . The centered one looks both ways and is far more accurate; for a full gradient it costs evaluations of instead of , since the forward difference can share across coordinates.
Theorem W0.2Truncation error#
Let and let be defined on an open interval containing .
- If exists and is continuous there, then for some .
- If exists and is continuous there, then for some .
So the forward error shrinks like and the centered error like : halve the step and you halve the first error but quarter the second.
Proofof W0.2
Both parts use Taylor’s theorem with the Lagrange form of the remainder.
- for some . Subtract , divide by , and subtract .
- Expand one order further on each side: , with and . Subtracting, the and terms cancel: . Divide by : . The average lies between the two values, so since is continuous, the intermediate value theorem gives between and where equals it. That gives .
Full explanationWhy the centered difference is so much better
The cancellation in step 2 is the whole story. The forward difference compares with , so the curvature tilts the chord and the error is first order. The centered chord is symmetric around : curvature lifts both ends equally, and its effect on the slope cancels. What survives is the next term, the third derivative, multiplied by .
The same symmetry trick appears all over numerical computing: the trapezoid and midpoint rules for integrals, the leapfrog integrator used in physics simulations and in Hamiltonian Monte Carlo, and Richardson extrapolation builds on it, combining two step sizes to cancel the next error term as well (it works especially well with symmetric formulas, whose errors contain only even powers of ).
The second error: rounding
Section titled “The second error: rounding”Theorem W0.2 says “make small”. A computer disagrees.
Remark W0.3Rounding error, and the best step#
A float64 number carries about 16 significant digits: its machine epsilon is . Model each computed value of as with at most a small multiple of (a well-implemented library function is accurate to a few units in the last place; forming adds a rounding term of the same order, which we absorb), and let bound near . The numerator can then be off by up to , and dividing by turns that into an error up to , which grows as shrinks. With bounding , the total error of the centered difference is at most
For the forward difference the same reasoning gives , minimised at . When and the derivative bounds are of order , that is for centered and for forward.
These are worst-case bounds; real rounding errors partly cancel, so the observed errors are usually smaller. The best step still lands in the predicted decade. Before reading on, test that:
Predict first. For f = sin at x = 1 in float64, roughly which step size h gives each estimate its smallest error?
Read the slopes (the axis runs from large on the left to small on the right). Left of each dip, the centered curve falls twice as steeply as the forward one: on log-log axes the truncation error has slope of magnitude against , which is Theorem W0.2. Right of the dips, both rise together with slope of magnitude , because rounding error does not care which formula you used. This is why CS231n recommends ; PyTorch’s gradcheck defaults to .
Full explanationWhy float32 is hopeless here
Repeat the analysis with float32, where . The best centered step becomes about , and the error bound at that step is about relative to the scale of , before any of the extra rounding inside a real model. (For at the observed float32 minimum is smaller, about , but at a step in the predicted decade.) CS231n’s authors report having sometimes seen relative errors drop from about to just by switching a correct implementation from single to double precision. A gradient check is a float64 tool.
Judging the agreement
Section titled “Judging the agreement”Definition W0.4Relative error#
For an analytic gradient and a numerical one of the same shape, and a small floor ,
An absolute difference of means nothing on its own: tiny next to a gradient of , huge next to one of . Dividing by the larger magnitude makes the measure scale-free and symmetric in and . The floor (the stub uses ) only prevents when both entries are exactly zero; it does not rescue entries whose true value is near zero (failure mode 5 in W0.6). Read the result against the CS231n scale, for a single smooth function in float64:
| Verdict | |
|---|---|
| The gradient is probably wrong. | |
| to | Suspicious. Investigate before moving on. |
| to | Acceptable only if the function has kinks (ReLU, max); too large for smooth ones. |
| Good. |
Errors accumulate with depth, so a check on a whole deep network can be looser than this.
Algorithm W0.5Gradient check#
Input: , a claimed gradient function , a test point , step , tolerance .
- Convert to a float64 copy.
- For each coordinate : set , evaluate ; set (from the original), evaluate ; restore ; store the centered difference.
- Return and whether it is below .
Cost: evaluations of and one of . That is why checks run on small inputs: a handful of data points and small layer sizes.
Where it fails
Section titled “Where it fails”Failure mode W0.6Six ways a gradient check misleads you#
- Kinks. At points where is not differentiable (ReLU at , , ), the two evaluations can land on different sides of the kink, and the estimate averages two slopes. Symptom: a few entries with large error, at inputs within of a kink. Fix: use test points away from kinks, few data points, or track whether a kink was crossed.
- Single precision. A correct gradient checked in float32 can show errors near . Fix: float64 throughout, including the copy of .
- Randomness inside . Dropout, sampling or data augmentation make and use different random draws, so is not a deterministic function of . Fix: fix the seed or switch them off during the check.
- The wrong step. Too small and rounding dominates; too large and truncation does (the plot above). Fix: ; if a check fails, retry with and before blaming the gradient.
- Negligible gradients. Where the true derivative is zero or tiny, the numerical estimate is pure noise of size about , and the relative error is large and meaningless. For at the derivative is exactly , the centered estimate is about , and the relative error is . Fix: an absolute tolerance alongside the relative one, set above that noise level (this is what PyTorch’s
atolis for). A floor only helps if it, too, sits above the noise. - An unlucky test point. A check passes at one , not everywhere. At the buggy gradient and the correct gradient of agree perfectly. With square shapes, a transposed gradient has the right shape and only its values give it away; with all-equal entries, even the values can agree. A batch of one hides mistakes in summing or averaging over the batch. Fix: random inputs, non-square shapes and a batch of at least two.
Where it fits
Section titled “Where it fits”Full explanationThree ways to get a derivative, and what the check is checking
There are three ways to differentiate a program:
| Method | How | Cost for a full gradient of | Accuracy |
|---|---|---|---|
| Symbolic | Apply differentiation rules to a formula | Can blow up in expression size | Exact |
| Numerical (this unit) | Finite differences | evaluations of | Limited by truncation and rounding |
| Automatic, reverse mode (backprop) | Record the computation, apply the chain rule backwards | Time: a small constant multiple of one evaluation, for any . Memory: proportional to the stored intermediate values | Exact up to floating point |
Reverse-mode automatic differentiation is why deep learning is possible: a model with a billion parameters gets its full gradient for roughly the price of a few forward passes. Finite differences would need two billion. So finite differences never train anything; their job is to check the other two, because they are too simple to be wrong in the same way. You will build reverse-mode differentiation yourself in Arc 2.
A cheaper check. Instead of all coordinates, pick one random direction and compare the claimed directional derivative with . That costs two evaluations whatever is. If is wrong, the error vector is non-zero, and a random direction drawn with a density (Gaussian, say, or uniform on the sphere) is exactly orthogonal to it with probability zero. With a tolerance the picture is less clean: a small error in one entry of a large gradient is diluted by roughly in the projection and can slip under the threshold. This is what JAX’s check_grads does. The coordinate-wise check, by contrast, tells you which entries are wrong.
Check yourself
Section titled “Check yourself”Check Why does the truncation error of the centered difference shrink like h² rather than h?
Subtracting the expansions of and cancels the even-power terms, including the term that limits the forward difference. The first survivor is , which becomes an error after dividing by (W0.2).
Check In float64 you shrink h from 1e-5 to 1e-12. What happens to the centered estimate?
It gets worse. Rounding error grows like (W0.3); in the plot, the error at is several orders of magnitude larger than at .
Check A smooth loss's analytic gradient has relative error 3e-3 against the numerical one in float64. What do you do?
Suspect a bug in the analytic gradient first. For a smooth function in float64 you expect about or better; is in the suspicious band. Retry with and to rule out the step.
Check Why divide by max(|a|, |b|) rather than by |a| alone?
The result does not depend on which gradient you call the reference, and a zero in one of them (common with ReLU) does not cause a division by zero.
Check The analytic gradient of f(x) = ‖x‖² is 2x. A buggy version returns x. At which test point does a gradient check fail to notice?
At , where both are the zero vector. Any point with a non-zero entry exposes the bug. This is failure mode 6 in W0.6.
Check With a random direction u, why does the directional check almost never miss a wrong gradient?
In exact arithmetic, the check misses only if , that is if is orthogonal to a non-zero error vector. The set of such directions is a hyperplane, which a random drawn with a density hits with probability zero. In floating point with a tolerance, small errors can still be missed (see Where it fits).
Build it
Section titled “Build it”Build mlkit.gradcheck
Implement the three functions in mlkit/gradcheck.py with NumPy only: numerical_grad (W0.1, centered, any array shape), relative_error (W0.4, vectorised, no loop) and check_grad (W0.5). The seven tests in tests/test_gradcheck.py are the specification. One of them, a preview of week 1, checks the gradient of with respect to a matrix.
Done when this passes: uv run pytest
Hint Iterating over an array of any shape
np.ndindex(x.shape) yields every index tuple of an array of any shape, so one loop handles vectors and matrices alike.
Hint The two tests that catch most people
test_integer_input: adding to an integer array silently does nothing. test_does_not_mutate_input: the caller’s array must come back unchanged. One line at the top of numerical_grad fixes both: work on a float64 copy.
Break it. In weeks/w00-setup/error_vs_h.py, redraw the error curves with Matplotlib for at (log-log axes, from to ), and save error_vs_h.png. Then run check_grad on , with analytic gradient where and elsewhere, at a point where one entry of equals . Predict the relative error first, then explain the result with W0.6.
Check Do the minima for eˣ at x = 1 sit at different step sizes than for sin at x = 1?
Barely. By W0.3, the centered depends on through a cube root. For at , , a ratio of ; for at the ratio is , whose cube root is about : a shift of about of a decade, which the noisy curves cannot resolve. The forward depends on , which is in both cases ( and ), so the prediction does not move at all.
Derive it. Write the proof of W0.2 on paper before opening it, then compare. If you stall, note the step in math-debt.md.
Stretch. Add directional_check(f, grad_f, x, h=1e-5, rng=None) using one random direction, with a test that it catches the buggy gradient for .
Then commit, push, and ask for a review.
Sources
Section titled “Sources”- Stanford CS231n, “Neural Networks Part 3: Gradient checks”: centered differences, relative error, the verdict scale, step size, precision, kinks.
- PyTorch documentation,
torch.autograd.gradcheck. - JAX documentation,
jax.test_util.check_grads. - Baydin, Pearlmutter, Radul & Siskind, “Automatic differentiation in machine learning: a survey”, Journal of Machine Learning Research 18 (2018): symbolic, numerical and automatic differentiation compared.
- Deisenroth, Faisal & Ong, Mathematics for Machine Learning, §5.1: derivatives and Taylor series.