Derivative as a limit, finite differences, step-size choice
Overview
Section titled “Overview”| Module | M01.1 · build · Python · Pass 2 · 2 to 3 h |
| You build | python/tinyllm/num/diff.py: forward_diff, central_diff, richardson |
| Contract | course/contracts/py/tinyllm/num/diff.pyi |
| Tests | course/tests/M01.1/test_diff.py (what they check: section 4) |
| Needs | nothing to call. Reading: M00.1 (exponents and logs), M00.4 (polynomials) |
| Used by | M04.1 builds numerical_grad (the heart of gradcheck) from one central_diff per coordinate · M01.2 differentiates with central_diff when you give Newton no derivative · later M02.1 and M09.3 reuse the error analysis of section 2 |
| Milestone | MS-P2 (the Pass 2 gate) |
| Optional depth | OpenStax, Calculus Volume 1 (free), sections 2.2 and 3.1 to 3.2 (limits and the derivative); Sauer, Numerical Analysis, section 5.1 (numerical differentiation and its rounding error); Nocedal and Wright, Numerical Optimization, section 8.1 (finite-difference gradients) |
Key Takeaways
Section titled “Key Takeaways”- The derivative is a limit, and a computer evaluates the quotient at one : the result is an approximation whose error you can predict (
test_hand_example). - The forward difference has truncation error proportional to ; the central difference cancels the term and has error proportional to (
test_forward_error_slope_is_one,test_central_error_slope_is_two). - Rounding adds an error of about that grows as shrinks, so the best step balances the two: for forward, for central, times (
test_default_step_is_accurate,test_default_step_scales_with_x). - Divide by the step actually taken, , not by : is rounded (
test_divides_by_the_step_actually_taken). - Richardson extrapolation combines central differences at to cancel in turn (
test_richardson_levels_raise_the_order).
How to work this chapter
Section titled “How to work this chapter”ol start M01.1 # stubs python/tinyllm/num/diff.py into your repool tests M01.1 # read the test catalog first: rung R0, you write no tests hereol check M01.1 # exit code is the verdictol diff M01.1 # after passing: your code against the reference1. Why now
Section titled “1. Why now”Pass 2 replaces the tracer’s counted bigram with a model trained by gradient descent, and gradient descent needs derivatives of a loss with respect to every weight. In L0.1 and L0.2 you will write an autograd engine that computes those derivatives by the chain rule, and every backward rule you write there can be wrong in ways that still train a little: a transposed gradient, a missing factor of 2, a sign. The only independent check is the definition of the derivative itself, evaluated numerically: nudge one input, watch the output move, divide. That check is gradcheck (M04.1), and it is built on the function you write here. It is only as trustworthy as its step size: too large and the formula is inaccurate, too small and floating-point rounding swamps the answer. This module derives the right step from first principles and implements the three difference formulas the rest of the course uses.
2. Principles
Section titled “2. Principles”| Symbol | Meaning | Type / shape |
|---|---|---|
| a function of one real variable, evaluated in float64 | Callable[[float], float] | |
| the point where we want the derivative | float | |
| the step, | float | |
| the derivative of at | float | |
| forward difference | float | |
| central difference | float | |
| “at most a constant times ” as | ||
| float64 machine epsilon, : the gap between 1 and the next float | ||
| Richardson value after levels | float |
2.1 The derivative is a limit
Section titled “2.1 The derivative is a limit”The slope of the straight line through and is the difference quotient : rise over run. As shrinks the second point slides toward the first, and if the slopes settle on one number, that number is the derivative , the slope of the tangent line:
“Settles on” has a precise meaning (the limit): for every tolerance you name, there is a step size below which every quotient is within that tolerance of . For : , which tends to . The usual rules follow from the definition the same way: , (the property that defines , M00.1), , and the chain rule , which M04.2 generalizes and every backward pass applies.
2.2 A computer cannot take a limit
Section titled “2.2 A computer cannot take a limit”A program picks one and computes one quotient. Two questions follow: how wrong is the quotient for a given , and which makes it least wrong? Both answers come from comparing near with a polynomial. If is smooth (has enough continuous derivatives), then for small
This is Taylor’s formula; M02.1 proves it and bounds the dots. Here you only need the first few terms, which you can check on : , and indeed , , .
2.3 Truncation error
Section titled “2.3 Truncation error”Subtract and divide by :
The forward difference is off by about : first order, error proportional to . Halve and the error halves. Now write the expansion at as well, , and subtract it from the one at . The even powers cancel:
The central difference is second order: halve and the error drops by 4. On a log-log plot of error against , the forward difference is a line of slope 1 and the central difference a line of slope 2; that slope is what the two slope tests measure. The error formula dropped is called truncation error, because it comes from truncating the Taylor series. The central difference is exact on quadratics (), and the forward difference is exact only on straight lines.
2.4 Rounding error
Section titled “2.4 Rounding error”Every float64 operation rounds its exact result to the nearest representable number, with relative error at most . So a computed is off by about (more if itself is a long computation). The numerator is a difference of two nearly equal numbers when is small: their leading digits cancel and the rounding errors do not. That error, about , is then divided by . Total error of the central difference:
The first term falls as shrinks, the second rises. Measured on at , where :
| error of |
Down to the error falls by 100 per decade (slope 2); below about rounding takes over and it rises again. A step of is worse than a step of .
2.5 Choosing h
Section titled “2.5 Choosing h”Minimize with and : the derivative is zero at . When and its derivatives are of size 1, that is , and the error there is about : two thirds of the 16 digits survive. The same argument for the forward difference, , gives and an error of about , half the digits. The step must also follow the scale of . Floats near are about apart, so at a step of moves by only relative and the quotient is mostly rounding. A step proportional to keeps the relative perturbation fixed; keeps it from vanishing at . The contract’s defaults are therefore
One more floating-point detail: is itself rounded, so the step the hardware took is , not . Dividing by the step actually taken makes come out as exactly 1. With and , dividing by gives instead.
2.6 Richardson extrapolation
Section titled “2.6 Richardson extrapolation”The central difference has an error series with only even powers: , where the ‘s do not depend on . Then , and the combination
cancels the term exactly. Repeating with weight at level cancels : with and , level has error and is exact on polynomials of degree up to . Because the truncation error shrinks so fast, Richardson can use a large (like 0.1), where rounding is negligible, and still reach about 12 digits. This is the method behind adaptive differentiation libraries.
3. Worked example by hand
Section titled “3. Worked example by hand”Take at , so , with . The function values: , , , , .
| Quantity | Computation | Value | Error |
|---|---|---|---|
| 12.61 | |||
| 12.01 | (here ) | ||
| 12.0025 | |||
| 12 | 0 |
The forward error is the of section 2.3 plus the next term . The central error is exactly because has no terms beyond , and quartering it at is what lets Richardson remove it completely. These numbers are the first test, test_hand_example.
4. The interface
Section titled “4. The interface”def forward_diff(f: Callable[[float], float], x: float, h: Optional[float] = None) -> float: ...def central_diff(f: Callable[[float], float], x: float, h: Optional[float] = None) -> float: ...def richardson(f: Callable[[float], float], x: float, h: float, levels: int = 2) -> float: ...h = None means the default step of section 2.5; a given h is used as is (gradcheck passes its own). Each function returns a Python float, divides by the step actually taken, and raises ValueError for a non-finite x, a step that is not finite and positive, or levels < 0.
What the tests check
Section titled “What the tests check”| Test | KIND | Checks | Why it matters downstream |
|---|---|---|---|
test_hand_example | unit, smoke | section 3: 12.61, 12.01, 12 | you and the tests agree on the definitions |
test_returns_python_float | unit | a Python float even when f returns numpy scalars | gradcheck stores results in float64 arrays |
test_central_error_slope_is_two | property | log-log slope of the central error is 2 | the truncation order of section 2.3 |
test_forward_error_slope_is_one | property | log-log slope of the forward error is 1 | the step rule differs per formula |
test_default_step_is_accurate | golden | default central error at most on five smooth functions at 26 points | M04.1’s tolerance assumes this accuracy |
test_forward_default_step_is_accurate | golden | default forward error at most | the rule |
test_default_step_scales_with_x | boundary | at to 7 digits | weights and logits are not all near 1 |
test_default_step_at_zero | boundary | a nonzero step at | activations are checked at their kink |
test_divides_by_the_step_actually_taken | boundary | gives exactly 1 | exactness on linear maps |
test_given_step_is_used_as_is | unit | h = 0.5 at gives 30000.25 | gradcheck’s eps means what it says |
test_richardson_levels_raise_the_order | property | on at 0.5 with : errors , , , for levels 0 to 3 | extrapolation works level by level |
test_richardson_exact_on_polynomials | property | exact on a quartic (1 level) and a sextic (2 levels) | the claim |
test_richardson_default_levels_is_two | unit | the default is two levels | contract defaults |
test_rejects_bad_steps | boundary | , negative, nan, inf raise ValueError | bugs surface at the call |
test_rejects_bad_points_and_levels | boundary | nan or inf , negative levels raise ValueError | nan never travels into a gradient check |
5. Pitfalls
Section titled “5. Pitfalls”| Pitfall | Symptom | Caught by |
|---|---|---|
| 1. a one-sided difference where the central one was meant | error instead of ; slope 1 on the log-log plot | test_central_error_slope_is_two, test_hand_example (mutant s01) |
| 2. an absolute step, ignoring the size of | the derivative of at is off by 3 percent | test_default_step_scales_with_x (mutant s02) |
| 3. a step proportional to alone | at : division by zero | test_default_step_at_zero (mutant s03) |
| 4. dividing by instead of the step actually taken | differentiates to | test_divides_by_the_step_actually_taken (mutant s05) |
| 5. the wrong step rule for the formula ( for central, for forward) | central error instead of ; forward error instead of | test_default_step_is_accurate (mutant s04), test_forward_default_step_is_accurate (mutant s08) |
| 6. Richardson weights instead of | the extrapolation removes nothing; worse than level 0 | test_richardson_exact_on_polynomials (mutant s06) |
| 7. Richardson steps that grow () instead of shrink | the hand example gives 12.05 | test_hand_example (mutant s07) |
| rescaling a step the caller gave | gradcheck’s eps silently becomes eps at | test_given_step_is_used_as_is (mutant s09) |
| accepting a nan or inf step | a nan derivative instead of an error | test_rejects_bad_steps (mutant s10) |
6. Where it’s used next
Section titled “6. Where it’s used next”| Direction | Module | How it uses this |
|---|---|---|
| Back | M00.1 | exponents and logarithms, and its defining property (reading) |
| Back | M00.4 | polynomials, the shape of a truncated Taylor series (reading) |
| Forward | M04.1 | numerical_grad calls central_diff(along, x_i, eps) once per coordinate of every input: the frozen-step gradient behind gradcheck |
| Forward | M01.2 | newton(f, None, x0) uses central_diff as its slope |
| Forward | M02.1 | Taylor’s formula with a proven remainder: the error terms of section 2.3 made rigorous |
| Forward | M09.3 | condition numbers and tolerance budgets generalize the rounding analysis of section 2.4 |
| Forward | L0.2 | F.gradcheck_all() checks every op of your autograd library through M04.1, and so through this file |
If you skip this module, ol check M04.1 stops with BLOCKED ... needs M01.1: build it, or pass --ref-deps.
Going further
Section titled “Going further”| Your piece | Production equivalent | What it adds | Where to look |
|---|---|---|---|
central_diff with | PyTorch torch.autograd.gradcheck | the same central difference per element, float64, eps ; also checks complex inputs and forward-mode gradients | torch/autograd/gradcheck.py (_compute_numerical_gradient) |
forward_diff with | SciPy scipy.optimize.approx_fprime, scipy.differentiate.derivative | forward steps for optimizers; adaptive step and order selection | scipy/optimize/_numdiff.py |
richardson | numdifftools | Richardson tables with automatic step search and error estimates | numdifftools/limits.py |
| (not built) | complex-step differentiation | has no subtraction, so works and the result is exact to rounding | Martins, Sturdza, and Alonso, “The complex-step derivative approximation” (2003) |
| finite differences | automatic differentiation | exact derivatives by the chain rule, no step at all: what your L0.1 autograd and M08.1 dual numbers do | M08.1, L0.1 |