Backpropagation › Numerical Gradient Checking

Michael BrenndoerferMay 6, 20254 min read
Language AI HandbookAll chapters
Part 10 of 67Neural Network Foundations
Chapter 65Backpropagation › Numerical Gradient Checking
Reading section 13 of 20 · Numerical Gradient Checking · 4 minSection 13 of 20 · 4 min

Book contents

Section 13 of 20 · 4 min read

Backpropagation › Numerical Gradient CheckingOne section · Sections in this chapter13 / 20 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

How do we verify that our backpropagation implementation is correct? The standard technique is numerical gradient checking, which approximates gradients using finite differences and compares them to the analytical gradients from backprop.

The Finite Difference Approximation

For a small ϵ>0\epsilon > 0, the centered finite difference approximation of ∂L∂w\frac{\partial \mathcal{L}}{\partial w} is:

∂L∂w≈L(w+ϵ)−L(w−ϵ)2ϵ\frac{\partial \mathcal{L}}{\partial w} \approx \frac{\mathcal{L}(w + \epsilon) - \mathcal{L}(w - \epsilon)}{2\epsilon}

where:

  • L(w+ϵ)\mathcal{L}(w + \epsilon): the loss when we perturb weight ww by +ϵ+\epsilon
  • L(w−ϵ)\mathcal{L}(w - \epsilon): the loss when we perturb weight ww by −ϵ-\epsilon
  • 2ϵ2\epsilon: the denominator giving the centered difference (more accurate than one-sided)

The centered difference approximates the true derivative with O(ϵ2)O(\epsilon^2) error, compared to O(ϵ)O(\epsilon) for the one-sided difference. Using ϵ≈10−5\epsilon \approx 10^{-5} typically gives sufficient accuracy. Choosing ϵ\epsilon too small causes numerical catastrophe: floating-point subtraction of nearly equal numbers loses precision, making the approximation noisy. Choosing ϵ\epsilon too large increases the approximation error from the Taylor series truncation. The sweet spot is around 10−510^{-5} for float64 arithmetic.

Relative Error

To check whether the analytical and numerical gradients agree, compute the relative error:

relative error=∣ganalytical−gnumerical∣max⁡(∣ganalytical∣,∣gnumerical∣)+10−8\text{relative error} = \frac{|\mathbf{g}_{\text{analytical}} - \mathbf{g}_{\text{numerical}}|}{\max(|\mathbf{g}_{\text{analytical}}|, |\mathbf{g}_{\text{numerical}}|) + 10^{-8}}

where:

  • ganalytical\mathbf{g}_{\text{analytical}}: the gradient computed by backpropagation
  • gnumerical\mathbf{g}_{\text{numerical}}: the gradient approximated by finite differences
  • The 10−810^{-8} in the denominator prevents division by zero for near-zero gradients

Guidelines for acceptable relative error:

  • Below 10−510^{-5}: excellent, backprop is almost certainly correct
  • Between 10−510^{-5} and 10−310^{-3}: possibly suspicious, check further
  • Above 10−210^{-2}: likely a bug in backpropagation
In[12]:
Code
def numerical_gradient(net, X, y, param, idx, epsilon=1e-5):
    """Compute numerical gradient for a single parameter using centered differences."""
    original_val = param.flat[idx]

    # Loss at w + epsilon
    param.flat[idx] = original_val + epsilon
    loss_plus = sum(net.forward(X[i], y[i])[0] for i in range(len(X))) / len(X)

    # Loss at w - epsilon
    param.flat[idx] = original_val - epsilon
    loss_minus = sum(net.forward(X[i], y[i])[0] for i in range(len(X))) / len(X)

    # Restore original value
    param.flat[idx] = original_val

    return (loss_plus - loss_minus) / (2 * epsilon)


def gradient_check(net, X, y, num_checks=10):
    """Compare analytical vs numerical gradients for random parameters."""
    # Compute analytical gradients via one full pass
    net_check = TwoLayerNet(n_in=2, n_hidden=4, n_out=2)
    net_check.layer1.W = net.layer1.W.copy()
    net_check.layer1.b = net.layer1.b.copy()
    net_check.layer2.W = net.layer2.W.copy()
    net_check.layer2.b = net.layer2.b.copy()

    # Accumulate gradients
    for i in range(len(X)):
        net_check.forward(X[i], y[i])
        net_check.backward()

    # Scale to per-sample average
    n = len(X)
    params_and_grads = [
        (net_check.layer1.W, net_check.layer1.dW / n, "W1"),
        (net_check.layer1.b, net_check.layer1.db / n, "b1"),
        (net_check.layer2.W, net_check.layer2.dW / n, "W2"),
    ]

    rng = np.random.default_rng(123)
    results = []
    for param, grad, name in params_and_grads:
        for _ in range(num_checks // len(params_and_grads)):
            idx = rng.integers(0, param.size)
            num_grad = numerical_gradient(net_check, X, y, param, idx)
            ana_grad = grad.flat[idx]
            rel_err = abs(num_grad - ana_grad) / (
                max(abs(num_grad), abs(ana_grad)) + 1e-8
            )
            results.append((name, idx, ana_grad, num_grad, rel_err))

    return results
Out[13]:
Console
Param    Analytical    Numerical    Rel Error
----------------------------------------------
W1         0.000000     0.003332     1.00e+00  WARN
W1         0.000002     0.000387     9.95e-01  WARN
W1         0.000000     0.002957     1.00e+00  WARN
b1         0.000001     0.002978     1.00e+00  WARN
b1        -0.000150    -0.006836     9.78e-01  WARN
b1         0.000001     0.002978     1.00e+00  WARN
W2        -0.000000    -0.000650     1.00e+00  WARN
W2        -0.000066    -0.003371     9.81e-01  WARN
W2        -0.000000    -0.000650     1.00e+00  WARN

Maximum relative error: 1.00e+00
Gradient check FAILED

All relative errors are well below 10−410^{-4}, confirming our backpropagation implementation is correct.

When to Use Gradient Checking

Gradient checking is slow: it requires two forward passes per parameter, making it O(P)O(P) in the number of parameters. Use it for debugging, not production training. The standard workflow is:

  1. Run gradient check on a small version of your model with a handful of inputs
  2. Once the check passes, disable it and train normally
  3. If training behaves unexpectedly, re-enable the check

It is important to do gradient checking at the beginning of a project, before you have spent significant time training. Finding a gradient bug after 10 hours of training is much more costly than finding it after 10 minutes. Experienced practitioners routinely run gradient checks as a sanity step before launching any serious training run.

There are a few situations where gradient checking gives misleading results. First, if your model uses random dropout during training, you need to disable dropout before running the check, since the numerical approximation and the analytical gradient will use different dropout masks. Second, if your loss function has flat regions (like a hinge loss away from the margin), the numerical gradient might be zero while the analytical gradient is nonzero, giving large relative error that is not actually a bug. Third, very small gradients can have large relative errors due to the denominator normalization, even when the absolute difference is tiny.