Type at least 2 letters. Enter opens the first result, Esc closes.

    Calculus for ML · Lesson 7 · Chain rule, automatic differentiation and backpropagation

    Numerical differentiation and gradient checking

    Intermediate 60 min 3 interactive figures 4 videos Colab notebook

    Before this lesson: lesson 03 (Taylor's theorem with remainder, the Taylor test), lesson 06 (the gradients you will check), and floating point and cancellation from linear algebra lesson 13. Time: about 60 minutes, plus the notebook.


    Why this matters #

    A hand-written backward pass, a custom CUDA or Triton kernel, a new loss term, the adjoint of a PDE solver: each is a gradient that can be wrong without any error message. Training still runs; it is only slower or worse. A finite-difference check needs nothing but evaluations of ff, so it is the standard unit test for gradients (torch.autograd.gradcheck, jax.test_util.check_grads).

    It is also a small, complete lesson in floating point: a formula that is exact as h→0h \to 0 gives garbage at h=10−12h = 10^{-12}. In science codes, the same ideas appear as the gradient test of data-assimilation systems. The complex step is a standard way to get derivatives from simulation code that has no automatic differentiation.

    1. Predict first #

    2. The picture #

    Secant slopes. The derivative f′(x)f'(x) is the slope of the tangent. A finite difference uses the slope of a secant through two nearby points:

    • the forward difference D+(h)=f(x+h)−f(x)hD_+(h) = \dfrac{f(x+h) - f(x)}{h} uses the points xx and x+hx + h;
    • the central difference D0(h)=f(x+h)−f(x−h)2hD_0(h) = \dfrac{f(x+h) - f(x-h)}{2h} uses the points x−hx - h and x+hx + h.

    The figure shows both secants next to the tangent.

    Interactive figure: loading…

    Press "Halve h" a few times. The forward secant turns toward the tangent slowly: its error halves at each step (first order). The central secant is almost parallel to the tangent already: its error falls by 4 at each step (second order). Drag the point xx to a place where the curve bends strongly: the forward error grows, because it is proportional to the curvature f′′f''.

    The central secant is better because it is symmetric. The curvature makes the forward secant too steep on one side; with points on both sides of xx, the two curvature errors cancel. Only an h2h^2 term is left (Theorem 1 below).

    Two errors with opposite trends.

    • Truncation error. The secant is not the tangent. Its slope differs from f′(x)f'(x) by a term that shrinks like hh (forward) or h2h^2 (central). This is pure math: it exists even with exact arithmetic.
    • Rounding error. In float64 every computed value of ff has a relative error of about ε≈2.2⋅10−16\varepsilon \approx 2.2\cdot 10^{-16} (machine epsilon). For small hh, f(x+h)f(x+h) and f(x)f(x) agree in their leading digits. The subtraction cancels those digits, and what is left is mostly rounding noise. This is catastrophic cancellation (linear algebra lesson 13). Dividing by a small hh makes the noise large: this error grows like 1/h1/h.

    On a log–log plot, the total error against hh is a V. The right arm is truncation, with slope +1+1 (forward) or +2+2 (central). The left arm is rounding, with slope −1-1. The best step sits at the bottom of the V, far from "as small as possible".

    Interactive figure: loading…

    Move the probe from right to left. On the right arm the error falls as hh falls. At the dashed lines (the optimal steps predicted in section 3) the forward and central errors turn around. Left of them, the digits of f(x+h)f(x+h) and f(x)f(x) in the readout are mostly the same: the subtraction cancels the grey digits and keeps only the pink ones, which are rounding noise. Below h≈10−16h \approx 10^{-16}, x+hx + h rounds to xx and the difference is 0. The complex step subtracts nothing and stays at full accuracy. Switch to float32: the V moves up and to the right.

    The complex step leaves the real axis. It evaluates f(x+ih)f(x + ih), with i=−1i = \sqrt{-1}, and keeps the imaginary part. For a real-analytic ff, f(x+ih)≈f(x)+ih f′(x)f(x + ih) \approx f(x) + ih\,f'(x): the derivative appears in the imaginary part, and nothing is subtracted. So there is no cancellation and no left arm.

    In nn dimensions a finite-difference gradient moves one coordinate at a time: nn or 2n2n evaluations of ff. Reverse-mode automatic differentiation gives the whole gradient for a small constant times one evaluation (lesson 05). So finite differences are a check, never a way to train.

    Quick check

    You compute a central difference with h=0.01h = 0.01 and get an error of 4⋅10−54\cdot10^{-5}. Rounding is negligible here. About what error do you expect with h=0.001h = 0.001?

    3. Definitions and main results #

    Notation. h>0h > 0 is the step. ε=2−52≈2.2⋅10−16\varepsilon = 2^{-52} \approx 2.2\cdot10^{-16} is float64 machine epsilon. ff is a real function of a real xx, smooth near xx. We use ε\varepsilon as the size of the relative error of one computed value of ff. (One correctly rounded operation has error at most u=ε/2u = \varepsilon/2, the unit roundoff; a computed ff has several roundings, so ε\varepsilon is a fair round number.)

    Theorem 1 (truncation error) #

    If f∈C2f \in C^2, then D+(h)=f′(x)+h2f′′(ξ)D_+(h) = f'(x) + \tfrac{h}{2} f''(\xi) for some ξ∈(x,x+h)\xi \in (x, x+h). If f∈C3f \in C^3, then D0(h)=f′(x)+h26f′′′(η)D_0(h) = f'(x) + \tfrac{h^2}{6} f'''(\eta) for some η∈(x−h,x+h)\eta \in (x-h, x+h).

    So the forward difference is first order, O(h)O(h), and the central difference is second order, O(h2)O(h^2).

    Proof.

    1. Taylor with the Lagrange remainder (lesson 03): f(x+h)=f(x)+hf′(x)+h22f′′(ξ)f(x+h) = f(x) + h f'(x) + \tfrac{h^2}{2} f''(\xi).
    2. Subtract f(x)f(x) and divide by hh: this is the forward statement.
    3. One order more, on both sides:
    f(x±h)=f(x)±hf′(x)+h22f′′(x)±h36f′′′(η±).f(x \pm h) = f(x) \pm h f'(x) + \tfrac{h^2}{2} f''(x) \pm \tfrac{h^3}{6} f'''(\eta_\pm).
    1. Subtract the two lines. The even terms f(x)f(x) and h22f′′(x)\tfrac{h^2}{2} f''(x) cancel:
    f(x+h)−f(x−h)=2hf′(x)+h36(f′′′(η+)+f′′′(η−)).f(x+h) - f(x-h) = 2h f'(x) + \tfrac{h^3}{6}\big(f'''(\eta_+) + f'''(\eta_-)\big).
    1. Divide by 2h2h. The average of f′′′(η+)f'''(\eta_+) and f′′′(η−)f'''(\eta_-) lies between the two values, so it equals f′′′(η)f'''(\eta) for some η\eta (intermediate value theorem). ■\blacksquare

    The same expansions give the second difference for the second derivative:

    f(x+h)−2f(x)+f(x−h)h2=f′′(x)+O(h2).\frac{f(x+h) - 2f(x) + f(x-h)}{h^2} = f''(x) + O(h^2).
    Richardson extrapolation

    If ff is smooth, only even powers of hh appear in the central difference: D0(h)=f′(x)+c h2+O(h4)D_0(h) = f'(x) + c\,h^2 + O(h^4). Halving the step gives D0(h/2)=f′(x)+c h2/4+O(h4)D_0(h/2) = f'(x) + c\,h^2/4 + O(h^4). Take the combination that cancels the h2h^2 term:

    R(h)=4D0(h/2)−D0(h)3=f′(x)+O(h4).R(h) = \frac{4D_0(h/2) - D_0(h)}{3} = f'(x) + O(h^4).

    This is Richardson extrapolation. It turns two second-order values into one fourth-order value. It is the idea behind Romberg integration and many high-order finite-difference stencils. The rounding arm becomes worse, so the best step is larger (∝ε1/5\propto \varepsilon^{1/5}).

    Theorem 2 (the optimal step) #

    Error model. Each computed value f~\tilde f satisfies ∣f~−f∣≤εF|\tilde f - f| \le \varepsilon F, with F≈∣f∣F \approx |f| near xx. Near xx, ∣f′′∣≤M2|f''| \le M_2 and ∣f′′′∣≤M3|f'''| \le M_3. Then the total errors are bounded by

    E+(h)=M2h2+2εFh,E0(h)=M3h26+εFh,E_+(h) = \frac{M_2 h}{2} + \frac{2\varepsilon F}{h}, \qquad E_0(h) = \frac{M_3 h^2}{6} + \frac{\varepsilon F}{h},

    and the bounds are smallest at

    h+∗=2εF/M2,E+(h+∗)=2εFM2;h0∗=(3εFM3)1/3,E0(h0∗)=12 32/3(εF)2/3M31/3.h_+^* = 2\sqrt{\varepsilon F/M_2},\quad E_+(h_+^*) = 2\sqrt{\varepsilon F M_2}; \qquad h_0^* = \Big(\frac{3\varepsilon F}{M_3}\Big)^{1/3},\quad E_0(h_0^*) = \tfrac12\, 3^{2/3} (\varepsilon F)^{2/3} M_3^{1/3}.

    Why this is true (intuition). One term falls and one term rises as hh shrinks. The sum is smallest where the two terms are of the same size: the bottom of the V.

    Proof.

    1. Forward: the computed numerator is f(x+h)−f(x)+δ1−δ2f(x+h) - f(x) + \delta_1 - \delta_2 with ∣δi∣≤εF|\delta_i| \le \varepsilon F. After dividing by hh, the rounding part is at most 2εF/h2\varepsilon F/h. Add the truncation bound M2h/2M_2 h/2 from Theorem 1.
    2. Set the derivative to 0: ddhE+=M2/2−2εF/h2=0\tfrac{d}{dh}E_+ = M_2/2 - 2\varepsilon F/h^2 = 0 gives h+∗h_+^*. There the two terms are equal, each εFM2\sqrt{\varepsilon F M_2}, so E+=2εFM2E_+ = 2\sqrt{\varepsilon F M_2}.
    3. Central: the rounding part is at most 2εF/(2h)=εF/h2\varepsilon F/(2h) = \varepsilon F/h; truncation at most M3h2/6M_3 h^2/6.
    4. ddhE0=M3h/3−εF/h2=0\tfrac{d}{dh}E_0 = M_3 h/3 - \varepsilon F/h^2 = 0 gives h0∗h_0^*. There εF/h0∗=M3h0∗2/3\varepsilon F/h_0^* = M_3 h_0^{*2}/3, so E0=M3h0∗2/6+M3h0∗2/3=M3h0∗2/2E_0 = M_3 h_0^{*2}/6 + M_3 h_0^{*2}/3 = M_3 h_0^{*2}/2, which is the formula. ■\blacksquare

    Numbers (float64, F=M2=M3=1F = M_2 = M_3 = 1).

    best step h∗h^* best error correct digits
    forward 2ε≈3.0⋅10−82\sqrt\varepsilon \approx 3.0\cdot10^{-8} ≈3.0⋅10−8\approx 3.0\cdot10^{-8} about 8 (half)
    central (3ε)1/3≈8.7⋅10−6(3\varepsilon)^{1/3} \approx 8.7\cdot10^{-6} ≈3.8⋅10−11\approx 3.8\cdot10^{-11} about 10–11 (two thirds)

    In general, a formula of order mm has h∗∝ε1/(m+1)h^* \propto \varepsilon^{1/(m+1)} and best error ∝εm/(m+1)\propto \varepsilon^{m/(m+1)}. These are worst-case bounds; measured errors are often a bit smaller.

    Scale. If ff changes on the length scale ∣x∣|x| (for example xkx^k or log⁡x\log x), then F/M2∼x2F/M_2 \sim x^2 and h∗∝ε ∣x∣h^* \propto \sqrt{\varepsilon}\,|x|. So use a relative step hi=c⋅max⁡(1,∣xi∣)h_i = c \cdot \max(1, |x_i|), with c=ε≈1.5⋅10−8c = \sqrt\varepsilon \approx 1.5\cdot10^{-8} (forward) or c=ε1/3≈6⋅10−6c = \varepsilon^{1/3} \approx 6\cdot10^{-6} (central). A fixed h=10−6h = 10^{-6} at xi=106x_i = 10^6 changes xix_i only in about its 13th digit.

    Quick check

    In float64, for ff with FF, M2M_2 of order 1, which step gives the most accurate forward difference?

    Theorem 3 (the complex step) #

    Let ff be analytic near xx and real on the real axis, so that f(x)f(x) and all its derivatives at a real xx are real. Then

    Im⁡f(x+ih)h=f′(x)−h26f′′′(x)+O(h4),Re⁡f(x+ih)=f(x)+O(h2).\frac{\operatorname{Im} f(x + ih)}{h} = f'(x) - \frac{h^2}{6} f'''(x) + O(h^4), \qquad \operatorname{Re} f(x + ih) = f(x) + O(h^2).

    Proof.

    1. Taylor series in the direction ihih, with i2=−1i^2 = -1 and i3=−ii^3 = -i:
    f(x+ih)=f(x)+ihf′(x)−h22f′′(x)−ih36f′′′(x)+O(h4).f(x+ih) = f(x) + ih f'(x) - \tfrac{h^2}{2} f''(x) - \tfrac{ih^3}{6} f'''(x) + O(h^4).
    1. All derivatives at xx are real. So the imaginary part is hf′(x)−h36f′′′(x)+O(h5)h f'(x) - \tfrac{h^3}{6} f'''(x) + O(h^5), and the real part is f(x)−h22f′′(x)+O(h4)f(x) - \tfrac{h^2}{2} f''(x) + O(h^4).
    2. Divide the imaginary part by hh. ■\blacksquare

    Why h=10−20h = 10^{-20} works. No two nearly equal numbers are subtracted. The imaginary part, about hf′(x)h f'(x), is computed with a relative error of about ε\varepsilon for any hh down to underflow. The truncation term h2f′′′/6h^2 f'''/6 is about 10−4010^{-40}. So the result is accurate to machine precision, from one complex evaluation per directional derivative. A complex evaluation costs a few real ones (a complex multiply is 4 real multiplies and 2 adds).

    What the code must satisfy. ff must be analytic, and the code must be complex-safe:

    • no abs: it returns the modulus, a real number, so the derivative of that term is lost. Use np.where(z.real >= 0, z, -z) instead;
    • comparisons and max only on the real part;
    • no complex conjugation (np.vdot, .conj(), conjugate transposes);
    • no cast to real, and complex dtypes everywhere.

    The complex step is close to forward-mode automatic differentiation with dual numbers (lesson 05): dual numbers use ϵ2=0\epsilon^2 = 0, the complex step uses i2=−1i^2 = -1, and the difference is the O(h2)O(h^2) term.

    Quick check

    You apply the complex step to code that computes np.abs(x) * np.exp(x) at x=2x = 2. What do you get?

    Gradients, Jacobians and directional checks #

    For f:Rn→Rf: \R^n \to \R, the central-difference gradient is

    gnum,i=f(x+hiei)−f(x−hiei)2hi,i=1,…,n,g_{\text{num},i} = \frac{f(x + h_i e_i) - f(x - h_i e_i)}{2h_i}, \qquad i = 1, \dots, n,

    where eie_i is the ii-th unit vector: 2n2n evaluations of ff. For F:Rn→RmF: \R^n \to \R^m, the same difference of output vectors gives column ii of the m×nm \times n Jacobian.

    A cheaper test uses one random direction vv: the directional check

    f(x+hv)−f(x−hv)2h≈∇f(x)⊤v,\frac{f(x + hv) - f(x - hv)}{2h} \approx \nabla f(x)^\top v ,

    two evaluations for the whole gradient. Compare it with the claimed g⊤vg^\top v. A few random directions catch almost every bug.

    The relative error #

    Compare the claimed gradient gg with gnumg_{\text{num}} by the relative error

    rel(g,gnum)=∥g−gnum∥2max⁡(∥g∥2, ∥gnum∥2)(rel=0 if both are zero).\text{rel}(g, g_{\text{num}}) = \frac{\|g - g_{\text{num}}\|_2}{\max\big(\|g\|_2,\ \|g_{\text{num}}\|_2\big)} \qquad (\text{rel} = 0 \text{ if both are zero}).

    Gradient sizes differ by orders of magnitude between layers. An absolute difference of 10−610^{-6} is a bug when ∥g∥=10−8\|g\| = 10^{-8} and noise when ∥g∥=104\|g\| = 10^4. The relative error does not depend on the scale. It lies in [0,2][0, 2]. (Some libraries divide by ∥g∥+∥gnum∥\|g\| + \|g_{\text{num}}\|; the result differs at most by a factor of 2.)

    Thresholds for a smooth function in float64 with a central difference: rel<10−7\text{rel} < 10^{-7} is fine; rel>10−3\text{rel} > 10^{-3} is almost surely a bug; in between, look closer (kinks, scale, step).

    A reliable gradient check #

    1. float64. In float32 (ε≈1.2⋅10−7\varepsilon \approx 1.2\cdot10^{-7}) the best central error is about 2.5⋅10−52.5\cdot10^{-5}: too coarse to separate a small bug from rounding.
    2. Central differences with h≈ε1/3max⁡(1,∣xi∣)h \approx \varepsilon^{1/3}\max(1, |x_i|), about 10−610^{-6} to 10−510^{-5} times the scale. If you are unsure, try two or three steps: a real bug does not depend on hh.
    3. Relative error with the thresholds above.
    4. No kinks. ReLU, max, abs, hinge losses, sorting and top-kk are not differentiable everywhere. If x±hx \pm h crosses a kink, the numerical derivative is wrong (figure below).
    5. Determinism. Turn off dropout and data augmentation, fix seeds, put batch normalization in a fixed mode: ff must be the same function at every evaluation.
    6. The same ff on both sides. Include every loss term (regularization, auxiliary losses) in both ff and gg.
    7. A generic point. Use random inputs and weights, not zeros or a symmetric initialization, where many terms vanish and bugs hide.
    8. Large models. Check a random subset of coordinates or a few random directions, layer by layer, on a tiny batch.
    Interactive figure: loading…

    Make the step hh smaller than the distance ∣x∣|x| to the kink. While ∣x∣<h|x| < h, the points x−hx - h and x+hx + h lie on different sides of 0, and the central secant mixes the two slopes: the check fails although the analytic derivative is correct. As soon as h≤∣x∣h \le |x|, the check passes. Only the ratio x/hx/h matters, so the axes are in units of the step (for example 10−610^{-6}).

    Library defaults, as a reference. torch.autograd.gradcheck uses central differences with eps=1e-6 and passes when every entry satisfies

    ∣analytic−numerical∣≤atol+rtol⋅∣numerical∣,|\text{analytic} - \text{numerical}| \le \texttt{atol} + \texttt{rtol}\cdot|\text{numerical}| ,

    with atol=1e-5 and rtol=1e-3. It expects float64 inputs. SciPy's scipy.optimize.check_grad returns the absolute norm ∥g−gnum∥\|g - g_{\text{num}}\| with forward differences; divide by the norms yourself.

    The Taylor test (lesson 03) avoids a single threshold: the remainder ∣f(x+td)−f(x)−t g⊤d∣|f(x + td) - f(x) - t\, g^\top d| shrinks like t2t^2 with a correct gg and only like tt with a wrong one.

    Quick check

    A gradient check of a smooth loss in float64, with central differences and h=10−6h = 10^{-6}, gives a relative error of 10−210^{-2}. What is the most likely cause?

    Answers to section 1. (1) h=10−8h = 10^{-8}, close to h∗=2ε≈3⋅10−8h^* = 2\sqrt{\varepsilon} \approx 3\cdot10^{-8} (here F=M2=eF = M_2 = e). The error is about 7⋅10−97\cdot10^{-9}: about 8 correct digits. At 10−410^{-4} truncation dominates (error 1.4⋅10−41.4\cdot10^{-4}); at 10−1210^{-12} rounding dominates (error 4⋅10−44\cdot10^{-4}). (2) No difference formula can: in float64, 1+10−20=11 + 10^{-20} = 1, so the numerator is exactly 0. The complex step can: Im⁡e1+10−20i/10−20\operatorname{Im} e^{1 + 10^{-20}i}/10^{-20} equals ee to all 16 digits.

    4. Worked example #

    (a) Exact truncation errors. Take f(x)=x3f(x) = x^3 at x=1x = 1: f′(1)=3f'(1) = 3, f′′(1)=6f''(1) = 6, f′′′=6f''' = 6. For a cubic every truncation error can be written exactly.

    Forward. Expand (1+h)3=1+3h+3h2+h3(1+h)^3 = 1 + 3h + 3h^2 + h^3. So

    D+(h)=(1+h)3−1h=3+3h+h2.D_+(h) = \frac{(1+h)^3 - 1}{h} = 3 + 3h + h^2 .

    The leading error term 3h3h is h2f′′(1)\tfrac{h}{2} f''(1). With h=0.1h = 0.1: D+=3.31D_+ = 3.31, error 0.310.31. With h=0.05h = 0.05: D+=3.1525D_+ = 3.1525, error 0.15250.1525, about half: first order.

    Central. (1+h)3−(1−h)3=6h+2h3(1+h)^3 - (1-h)^3 = 6h + 2h^3. So

    D0(h)=6h+2h32h=3+h2,D_0(h) = \frac{6h + 2h^3}{2h} = 3 + h^2 ,

    exactly h26f′′′\tfrac{h^2}{6} f'''. With h=0.1h = 0.1: 3.013.01, error 0.010.01. With h=0.05h = 0.05: 3.00253.0025, error 0.00250.0025, a quarter: second order.

    Richardson. (4⋅3.0025−3.01)/3=(12.01−3.01)/3=3\big(4 \cdot 3.0025 - 3.01\big)/3 = (12.01 - 3.01)/3 = 3, exact, because the error was a pure h2h^2 term.

    Complex step. Expand with i2=−1i^2 = -1 and i3=−ii^3 = -i:

    (1+ih)3=1+3ih+3(ih)2+(ih)3=1+3ih−3h2−ih3.(1 + ih)^3 = 1 + 3ih + 3(ih)^2 + (ih)^3 = 1 + 3ih - 3h^2 - ih^3 .

    So

    Im⁡f(1+ih)h=3h−h3h=3−h2:\frac{\operatorname{Im} f(1+ih)}{h} = \frac{3h - h^3}{h} = 3 - h^2 :

    the same size of error as the central difference, with the opposite sign (Theorem 3: −h26⋅6-\tfrac{h^2}{6}\cdot 6). With h=0.1h = 0.1: 2.992.99. With h=10−20h = 10^{-20} the error is 10−4010^{-40}.

    (b) Optimal steps. A cubic at x=1x = 1 is a poor test of rounding (float64 computes (1+h)3−1(1+h)^3 - 1 almost exactly), so take f=exf = e^x at x=1x = 1, where F=M2=M3=eF = M_2 = M_3 = e.

    • Forward: h+∗=2εe/e=2ε≈3.0⋅10−8h_+^* = 2\sqrt{\varepsilon e/e} = 2\sqrt{\varepsilon} \approx 3.0\cdot10^{-8}, with error bound 2εe⋅e=2eε≈8.1⋅10−82\sqrt{\varepsilon e\cdot e} = 2e\sqrt{\varepsilon} \approx 8.1\cdot10^{-8}.
    • Central: h0∗=(3ε)1/3≈8.7⋅10−6h_0^* = (3\varepsilon)^{1/3} \approx 8.7\cdot10^{-6}, with error bound 1232/3(εe)2/3e1/3≈1.0⋅10−10\tfrac12 3^{2/3}(\varepsilon e)^{2/3} e^{1/3} \approx 1.0\cdot10^{-10}.

    Section 5 measures 6.6⋅10−96.6\cdot10^{-9} at h=10−8h = 10^{-8} (forward) and 5.9⋅10−115.9\cdot10^{-11} at h=10−5h = 10^{-5} (central): below the bounds, as expected for worst-case bounds.

    (c) A gradient check by hand. f(x,y)=x2yf(x, y) = x^2 y at (1,2)(1, 2). The true gradient is (2xy,x2)=(4,1)(2xy, x^2) = (4, 1). A buggy backward pass returns g=(4,1.01)g = (4, 1.01): a 1% error in one entry.

    ff is quadratic in xx and linear in yy, so central differences are exact here (the f′′′f''' term is 0): gnum=(4,1)g_{\text{num}} = (4, 1). Then

    ∥g−gnum∥=∥(0, 0.01)∥=0.01,max⁡(∥g∥,∥gnum∥)=16+1.0201≈4.1255,\|g - g_{\text{num}}\| = \|(0,\ 0.01)\| = 0.01, \qquad \max(\|g\|, \|g_{\text{num}}\|) = \sqrt{16 + 1.0201} \approx 4.1255,
    rel=0.014.1255≈2.4⋅10−3,\text{rel} = \frac{0.01}{4.1255} \approx 2.4\cdot10^{-3},

    far above 10−710^{-7}: a bug. With the correct gg and h=10−6h = 10^{-6}, float64 gives rel≈10−11\text{rel} \approx 10^{-11}.

    5. In code #

    NumPy and SciPy check the numbers above. The notebook has these cells and the exercises.

    import numpy as np
    from scipy.optimize import check_grad
    
    eps = np.finfo(float).eps                       # 2.2e-16 (float64 machine epsilon)
    
    def forward(f, x, h):
        return (f(x + h) - f(x)) / h
    
    def central(f, x, h):
        return (f(x + h) - f(x - h)) / (2 * h)
    
    def complex_step(f, x, h):
        return f(x + 1j * h).imag / h
    
    # 1. The V for f = exp at x = 1 (f'(1) = e). Columns: forward, central, complex step.
    for h in (1e-2, 1e-5, 1e-8, 1e-12, 1e-20):
        errs = [abs(m(np.exp, 1.0, h) - np.e) for m in (forward, central, complex_step)]
        print(f"h={h:.0e}  " + "  ".join(f"{e:.1e}" for e in errs))
    print(f"theory h*: {2 * np.sqrt(eps):.1e} (forward), {(3 * eps) ** (1 / 3):.1e} (central)")
    # h=1e-02  1.4e-02  4.5e-05  4.5e-05
    # h=1e-05  1.4e-05  5.9e-11  4.5e-11
    # h=1e-08  6.6e-09  6.6e-09  0.0e+00
    # h=1e-12  4.3e-04  2.1e-04  0.0e+00
    # h=1e-20  2.7e+00  2.7e+00  0.0e+00     <- x + h == x: the difference is 0
    # theory h*: 3.0e-08 (forward), 8.7e-06 (central)
    
    # 2. A gradient check with a relative error
    def numerical_gradient(f, x, h=1e-6):
        g = np.zeros_like(x)
        for i in range(x.size):
            e = np.zeros_like(x)
            e[i] = h * max(1.0, abs(x[i]))          # relative step
            g[i] = (f(x + e) - f(x - e)) / (2 * e[i])
        return g
    
    def rel_error(a, b):
        den = max(np.linalg.norm(a), np.linalg.norm(b))
        return 0.0 if den == 0 else np.linalg.norm(a - b) / den
    
    f = lambda z: z[0] ** 2 * z[1]
    grad_ok = lambda z: np.array([2 * z[0] * z[1], z[0] ** 2])
    grad_bug = lambda z: np.array([2 * z[0] * z[1], 1.01 * z[0] ** 2])   # 1% error in one entry
    x0 = np.array([1.0, 2.0])
    g_num = numerical_gradient(f, x0)
    print(f"{rel_error(grad_ok(x0), g_num):.0e} {rel_error(grad_bug(x0), g_num):.1e}")   # 7e-12 2.4e-03
    
    # 3. A kink: ReLU at 1e-7 from 0, with the step 1e-6
    relu = lambda z: np.maximum(z, 0.0)
    print(central(relu, 1e-7, 1e-6))               # 0.55, but the true derivative is 1
    
    # 4. SciPy's check_grad returns an absolute error (forward differences)
    print(f"{check_grad(f, grad_bug, x0):.4f}")    # 0.0100
    

    Read the output. The forward error falls like hh down to about 10−810^{-8}, and the central error like h2h^2 down to about 10−510^{-5}; below these steps rounding takes over. The rounding arm is noisy (single steps can be lucky), so judge the trend, not one point. The complex step is exact to the last digit at every small hh.

    6. Where it shows up #

    • Unit tests for gradients. torch.autograd.gradcheck (and gradgradcheck for second derivatives) and jax.test_util.check_grads, which compares forward and reverse mode with finite differences. Every custom autograd function, fused kernel or hand-written layer should pass one, including the layers of lesson 06.
    • Debugging training. When a model trains badly after a change in the loss or a layer, a gradient check on a tiny batch is the fastest way to rule out a wrong gradient.
    • Adjoints in science codes. Hand-written adjoints of PDE solvers and of variational data assimilation are tested with the gradient test, the ratio (J(x+αd)−J(x))/(α ∇J(x)⊤d)→1\big(J(x + \alpha d) - J(x)\big)/\big(\alpha\, \nabla J(x)^\top d\big) \to 1 as α→0\alpha \to 0, and with the Taylor test. A linear operator and its adjoint (for example seismic modelling and migration) are tested with the dot-product test ⟨Au,v⟩=⟨u,A⊤v⟩\langle Au, v\rangle = \langle u, A^\top v\rangle for random u,vu, v.
    • Black-box simulators. When legacy code (for example Fortran without AD) must give sensitivities, finite differences or the complex step (recompile with complex types) are the practical options. Aerospace design uses the complex step for this.
    • Stencils. The same Taylor analysis gives the order of accuracy of finite-difference stencils in PDE solvers and in grid-based physics-informed residuals.

    7. Common mistakes #

    • float32 for the check. A correct gradient fails, or a small bug hides in the noise.
    • hh "as small as possible". h=10−12h = 10^{-12} is on the rounding arm of the V. Also wrong: one fixed absolute hh for inputs of very different sizes; use a relative step.
    • Absolute error. ∥g−gnum∥=10−5\|g - g_{\text{num}}\| = 10^{-5} means nothing without ∥g∥\|g\|.
    • Crossing a kink. A failure at one sample of a ReLU network may be a kink, not a bug: move the point or use a smooth activation, then check again.
    • A non-deterministic ff. Dropout, random augmentation, or batch normalization in training mode with a changing batch make f(x+h)−f(x−h)f(x+h) - f(x-h) meaningless.
    • Complex step with unsafe code. np.abs(z) returns x2+h2\sqrt{x^2 + h^2}, a real number: part of the derivative silently becomes 0. Use np.where(z.real >= 0, z, -z) instead.
    • One passing point as proof. A bug term can vanish at one point; check a few random points.

    8. Check yourself #

    1. Central difference of f(x)=x4f(x) = x^4 at x=1x = 1 with h=0.1h = 0.1: the value, the error, and the leading-term prediction?
      AnswerD0=(1.14−0.94)/0.2=(1.4641−0.6561)/0.2=4.04D_0 = (1.1^4 - 0.9^4)/0.2 = (1.4641 - 0.6561)/0.2 = 4.04, error 0.040.04. Prediction h26f′′′(1)=0.016⋅24=0.04\tfrac{h^2}{6} f'''(1) = \tfrac{0.01}{6} \cdot 24 = 0.04 (in fact D0=4+4h2D_0 = 4 + 4h^2 exactly).
    2. In float32 (ε≈1.2⋅10−7\varepsilon \approx 1.2\cdot10^{-7}) with F=M2=M3=1F = M_2 = M_3 = 1: optimal steps and best error bounds?
      AnswerForward: h∗=2ε≈6.9⋅10−4h^* = 2\sqrt\varepsilon \approx 6.9\cdot10^{-4}, error ≈6.9⋅10−4\approx 6.9\cdot10^{-4}. Central: h∗=(3ε)1/3≈7.1⋅10−3h^* = (3\varepsilon)^{1/3} \approx 7.1\cdot10^{-3}, error ≈2.5⋅10−5\approx 2.5\cdot10^{-5}. Only 3–4 digits, which is why checks run in float64.
    3. A claimed gradient is (2,−1,0.5)(2, -1, 0.5); the central difference gives (2,−1,0.505)(2, -1, 0.505). Relative error and verdict?
      Answer∥g−gnum∥=0.005\|g - g_{\text{num}}\| = 0.005 and max⁡(∥g∥,∥gnum∥)≈2.2924\max(\|g\|, \|g_{\text{num}}\|) \approx 2.2924, so rel≈2.2⋅10−3\text{rel} \approx 2.2\cdot10^{-3}. For a smooth function in float64 this is almost surely a bug, in the third entry.
    4. f(x)=max⁡(x,0)f(x) = \max(x, 0) at x=10−7x = 10^{-7}, central difference with h=10−6h = 10^{-6}: what do you get, and why?
      Answer(1.1⋅10−6−0)/(2⋅10−6)=0.55(1.1\cdot10^{-6} - 0)/(2\cdot10^{-6}) = 0.55 instead of 1: x−h<0x - h < 0 crosses the kink. Use a point farther than hh from 0, or a smaller step for this point.
    5. Why can the complex step use h=10−20h = 10^{-20} while the central difference cannot?
      AnswerAt x=1x = 1, x±10−20x \pm 10^{-20} rounds to xx, so the central numerator is exactly 0. The complex step stores hf′(x)h f'(x) in the imaginary part without any subtraction, so its relative error stays about ε\varepsilon; its truncation error h2f′′′/6h^2 f'''/6 is negligible.
    6. Why divide by max⁡(∥g∥,∥gnum∥)\max(\|g\|, \|g_{\text{num}}\|) and not by ∥g∥\|g\| alone?
      AnswerIt is symmetric in the two gradients, bounded (by 2), and defined when g=0g = 0 but gnum≠0g_{\text{num}} \ne 0 (common with ReLUs). Only "both exactly zero" needs a rule: return 0.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. Write the forward and central difference formulas and their orders.

    2. Why is the central difference second order?

    3. How big is the rounding error of a finite difference with step hh?

    4. What are the optimal steps and best errors of the forward and central differences in float64 (all scales 1)?

    5. Write the complex-step formula for f′(x)f'(x).

    6. Why can the complex step use h=10−20h = 10^{-20}?

    7. Which relative error do we use in a gradient check?

    8. A gradient check fails at one point of a ReLU network. What should you check before you look for a bug?

    9. Name three rules for a reliable gradient check.

    Open the review →

    9. Interview angle #

    • "How do you know your backward pass is correct?" — A gradient check: float64, central differences, h≈10−6h \approx 10^{-6} relative to the scale, relative error with thresholds, no kinks, randomness off, a few random coordinates or directions per layer.
    • "Why not train with finite differences?" — 2n2n evaluations per gradient (nn parameters, about 10910^9 for an LLM) against a small constant times one evaluation for reverse mode, and at best about 10−1010^{-10} accuracy.
    • "Why is the central difference better, and which step do you use?" — The even Taylor terms cancel, so the error is O(h2)O(h^2). Balancing h2h^2 against ε/h\varepsilon/h gives h∝ε1/3≈10−5h \propto \varepsilon^{1/3} \approx 10^{-5}.
    • "Why does a smaller step make the result worse?" — Cancellation: f(x+h)f(x+h) and f(x)f(x) share their leading digits, the difference is mostly rounding noise, and dividing by hh magnifies it.
    • "What is the complex step?" — Im⁡f(x+ih)/h\operatorname{Im} f(x+ih)/h: error O(h2)O(h^2) and no cancellation, so h=10−20h = 10^{-20} gives machine precision; it needs analytic, complex-safe code.

    10. Practice #

    The notebook for this lesson runs in Colab. It has five exercises, each with a check cell and a folded solution:

    1. forward_diff, central_diff, complex_step; then reproduce the V plot.
    2. optimal_step(method, eps, F, M): the step of Theorem 2, compared with the observed minimum of the V.
    3. numerical_gradient and numerical_jacobian with relative steps.
    4. gradient_check: relative error of a hand-written MLP backward pass; find the planted bug.
    5. A kink experiment: how often a gradient check of a ReLU layer fails, as a function of hh.

    11. Watch and read #

    The practical recipe: central differences, a relative error (with a sum in the denominator) and thresholds like the ones in this lesson.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Regularization terms, dropout and other traps; compare with the rules in section 3.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Forward, backward and central differences derived from Taylor series, with their orders.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Why and how to check derivatives with finite differences, and how accurate the approximation is.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play