The bias–variance decomposition is usually drawn as a cartoon: two smooth curves crossing, with a U-shaped total on top. It is more convincing when you measure it. This post sets up a regression problem small enough that every term in the decomposition can be estimated by resampling, fits ridge regression on polynomial features for forty values of the penalty
Setup
The target is
Two implementation details matter more than they look:
- Standardize each feature column.
on has a tiny variance compared to , so without standardization the penalty would treat the columns completely differently. - Do not penalize the intercept. Centering
and fitting the slope-only problem is equivalent and keeps the maths clean.
import numpy as np
SIGMA, DEG = 0.3, 12
f = lambda x: np.sin(2 * np.pi * x)
def make(n, rng):
x = rng.uniform(0, 1, n)
return x, f(x) + SIGMA * rng.normal(size=n)
def features(x, mu=None, sd=None):
X = np.vander(x, DEG + 1, increasing=True)[:, 1:] # x, x^2, ..., x^12
if mu is None:
mu, sd = X.mean(0), X.std(0)
return (X - mu) / sd, mu, sd
def ridge_fit(X, y, lam):
ym = y.mean() # unpenalized intercept via centering
w = np.linalg.solve(X.T @ X + lam * np.eye(X.shape[1]), X.T @ (y - ym))
return w, ym
The closed form is
The decomposition
For a fixed input
Derivation of the three terms
Add and subtract
Square and take expectations. The three squared terms give noise, bias² and variance. The cross terms vanish:
Every term can be estimated by brute force: draw 200 training sets, fit each one for every
lams = np.logspace(-6, 2, 41)
x_grid = np.linspace(0, 1, 200)
preds = np.zeros((200, len(lams), len(x_grid))) # (training set, lambda, test point)
for r in range(200):
x_tr, y_tr = make(30, rng)
X_tr, mu, sd = features(x_tr)
for j, lam in enumerate(lams):
w, ym = ridge_fit(X_tr, y_tr, lam)
preds[r, j] = features(x_grid, mu, sd)[0] @ w + ym
fbar = preds.mean(0) # average prediction per lambda
bias2 = ((fbar - f(x_grid)) ** 2).mean(1)
var = preds.var(0).mean(1)
| λ | Train MSE | Validation MSE | Bias² | Variance | Bias² + Var + |
|---|---|---|---|---|---|
| 0.0638 | 5.715 | 0.016 | 6.156 | 6.262 | |
| 0.0678 | 1.679 | 0.007 | 1.724 | 1.820 | |
| 0.0704 | 0.649 | 0.009 | 0.597 | 0.695 | |
| 0.0750 | 0.361 | 0.014 | 0.277 | 0.381 | |
| 0.0952 | 0.165 | 0.025 | 0.050 | 0.165 | |
| 0.1428 | 0.253 | 0.088 | 0.077 | 0.255 | |
| 0.1882 | 0.238 | 0.120 | 0.023 | 0.233 | |
| 0.3456 | 0.418 | 0.289 | 0.033 | 0.412 |
Three things stand out. The training error is below the noise floor of
What the fits look like
The middle panel is what "a good fit" means in this context: not the curve that passes closest to the training points, but the one that would change least if we had drawn a different thirty points.
The regularization path
Plotting each coefficient against
TIP
In a real project, do not pick
by eye from a plot like this. Standardize the features inside the cross-validation loop (fit the scaler on the training fold only) and let something like scikit-learn's RidgeCVsearch a logarithmic grid; the shape of the validation curve above is typical and a grid of 20–40 values across six decades is plenty.
Learning curves: the other axis
Bias and variance also trade off against the amount of data. Holding
With
Takeaways
- Bias² and variance are not metaphors; they can be estimated by resampling, and their sum plus noise reproduces the measured test error.
- Ridge regression trades variance for bias one direction at a time, ordered by singular value, so it hits the nearly-collinear high-degree columns first.
- The optimal penalty depends on
. Learning curves show both the overfitting regime and the bias floor on a single plot.
Further reading
- scikit-learn user guide, Ridge regression and classification: https://scikit-learn.org/stable/modules/linear_model.html
- scikit-learn API,
RidgeCVwith built-in efficient leave-one-out cross-validation: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.RidgeCV.html - Dive into Deep Learning, chapter on generalization, overfitting and underfitting: https://d2l.ai/chapter_linear-regression/generalization.html
- Belkin et al. (2019), Reconciling modern machine learning practice and the bias-variance trade-off, for what happens when the number of parameters passes the number of points: https://arxiv.org/abs/1812.11118