Backpropagation › Numerical Gradient Checking
Language AI HandbookAll chapters 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 , the centered finite difference approximation of is:
where:
- : the loss when we perturb weight by
- : the loss when we perturb weight by
- : the denominator giving the centered difference (more accurate than one-sided)
The centered difference approximates the true derivative with error, compared to for the one-sided difference. Using typically gives sufficient accuracy. Choosing too small causes numerical catastrophe: floating-point subtraction of nearly equal numbers loses precision, making the approximation noisy. Choosing too large increases the approximation error from the Taylor series truncation. The sweet spot is around for float64 arithmetic.
Relative Error
To check whether the analytical and numerical gradients agree, compute the relative error:
where:
- : the gradient computed by backpropagation
- : the gradient approximated by finite differences
- The in the denominator prevents division by zero for near-zero gradients
Guidelines for acceptable relative error:
- Below : excellent, backprop is almost certainly correct
- Between and : possibly suspicious, check further
- Above : likely a bug in backpropagation
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 resultsParam 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 , confirming our backpropagation implementation is correct.
When to Use Gradient Checking
Gradient checking is slow: it requires two forward passes per parameter, making it in the number of parameters. Use it for debugging, not production training. The standard workflow is:
- Run gradient check on a small version of your model with a handful of inputs
- Once the check passes, disable it and train normally
- 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.