Problem
Training minimizes a loss on a finite set of examples. But nobody wants the answer to "how well do you know these ten examples?" They want to know how the model will do tomorrow, on inputs it has not met. A model can score perfectly on its training set by memorizing it and still be useless, so the training score alone says almost nothing about what you care about.
Every practical choice in machine learning is, underneath, a bet about this gap: how big a model, how much data, when to stop training, how to measure success honestly. If you cannot tell memorizing from learning, you cannot tell a good model from a lucky one.
Intuition
A student who memorizes last year's exam gets a perfect score on last year's exam and learns nothing about the subject. A student who understands the material may miss a couple of old questions but does well on a new paper. The old exam is the training set; the new paper is the world. We measure understanding by setting some questions aside and never practising on them.
Two things make a new paper harder than the old one. If the student has too much freedom to bend an answer around every quirk of the practice questions, they fit noise. And if the new paper covers a different syllabus than the practice set, even a student who understood everything may fail. These are the two halves of this page: capacity and distribution shift.
Generalization means doing well across P, not just on the dots you trained on. A shifted P' is a different world.
Mechanics
True risk and empirical risk
Suppose examples (x, y) are drawn from some unknown distribution P, and a loss ℓ scores a prediction f(x) against y (see loss functions). The quantity we want to be small is the true risk, the expected loss over P:
R(f) = E(x,y)∼P [ ℓ(f(x), y) ]
We can't compute it, because we don't know P. What we can compute is the empirical risk, the average loss over the n training examples:
R̂(f) = (1 / n) Σi ℓ(f(xi), yi)
Training is empirical risk minimization: pick the f in some family that makes R̂ small. The generalization gap is R(f) − R̂(f). For a fixed f chosen without looking at the data, R̂ is an unbiased estimate of R and the gap averages to zero. But the f we actually use was chosen because it had a small R̂, so its training loss is optimistically biased and the gap is typically positive.
The i.i.d. assumption
The argument above leans on one assumption: the examples are independent and identically distributed. Identically means training and future data come from the same P. Independent means one example tells you nothing extra about another. When this holds, a held-out set is a fair stand-in for the future, and its average loss is an unbiased estimate of R(f), with a standard error that shrinks like 1/√m for m test examples. When it fails (duplicated rows, several records per patient, data ordered by time), the held-out number is too good, which is the subject of data splits and later of data leakage.
Capacity
Capacity is how rich a family of functions the model can represent: the degree of a polynomial, the depth of a tree, the number of parameters in a network. More capacity lets the model fit more patterns, including accidental ones. Three regimes follow:
- Too little capacity (underfitting): the family can't express the real pattern, so both training and held-out error stay high.
- Enough: training error is low and held-out error is close to it.
- Too much (overfitting): training error keeps falling, often to zero, while held-out error rises, because the extra freedom is spent on noise in the sample.
Classical theory formalizes this with capacity measures such as VC dimension, which bound the gap in terms of capacity divided by the number of examples. The worked example below shows the shape without any theory.
Double descent
The picture above, held-out error falling and then rising as capacity grows, is not the whole story for modern models. Belkin and co-authors (2019) and Nakkiran and co-authors (2019) report that when capacity grows past the point where the model can exactly fit the training data (the interpolation threshold), held-out error can fall again, giving a second descent. It is observed for neural networks, random features and some linear models, and the exact behaviour depends on how the fit is chosen among the many that interpolate. This page does not compute it; see the references. The practical lesson is narrower than "bigger is always safe": a peak near the threshold is real, and which side of it you are on matters.
Distribution shift
If the data a model meets in use comes from P' ≠ P, then held-out error measured on P says little about it. Common cases: covariate shift (inputs change, the relation from x to y does not), label shift (class frequencies change) and concept drift (the relation itself changes over time). A flexible model is especially fragile outside the region its training inputs covered, because nothing constrained it there. Monitoring for this after deployment is its own topic.
Estimating the gap in scikit-learn
The tools all do the same thing: keep some data out of fitting and score on it.
train_test_split makes one split. cross_val_score repeats the idea over several splits.
learning_curve scores at growing training sizes and validation_curve scores while
one hyperparameter, such as capacity, changes. Both return training and validation scores so you can read the gap directly.
Same ten points, three model sizes. The degree-9 curve (10 parameters for 10 points) hits every dot, so its training error is zero.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
train_test_split |
arrays of equal length; test_size, train_size, shuffle=True, stratify, random_state |
One random split into train and test parts, returned interleaved: X_train, X_test, y_train, y_test. |
_split.py |
ShuffleSplit |
n_splits, test_size, random_state |
Repeated random splits; what train_test_split uses for one shuffled split. |
_split.py |
KFold |
n_splits, shuffle, random_state |
Each example is held out exactly once across the folds. | _split.py |
cross_val_score |
estimator, X, y, cv, scoring → array of cv scores |
Fit and score on each fold; the spread of the scores shows how noisy the estimate is. | _validation.py |
learning_curve |
train_sizes, cv → sizes, train scores, validation scores |
Score versus amount of training data: does more data close the gap? | _validation.py |
validation_curve |
param_name, param_range, cv → train and validation scores |
Score versus one hyperparameter, such as capacity: the tool for finding where overfitting starts. | _validation.py |
Worked example
Fitting ten points with growing capacity
The true curve is y = sin(2πx). We take ten evenly spaced inputs x = 0, 1/9, …, 1 and add a fixed pattern of noise between −0.35 and +0.35, giving training targets 0.25, 0.34, 1.13, 1.22, 0.14, -0.69, -0.62, -0.78, -0.94, 0.15. A polynomial of degree d has d + 1 parameters, fit by least squares. Held-out error is the mean squared error against the true curve on 50 evenly spaced inputs in [0, 1]. This is the error that remains after noise is removed, so it measures how well the fit recovered the pattern.
| Degree | Parameters | Training MSE | Held-out MSE | Gap |
|---|---|---|---|---|
| 1 | 2 | 0.333 | 0.224 | -0.109 |
| 3 | 4 | 0.077 | 0.009 | -0.068 |
| 5 | 6 | 0.064 | 0.002 | -0.061 |
| 9 | 10 | ≈ 0 | 0.067 | +0.067 |
Degree 1 is too stiff: both errors are large (0.333 and 0.224). Degree 3 captures the wave: training error drops to 0.077 and held-out error to 0.009. Degree 5 is the best of the four on held-out data at 0.002. Degree 9 reaches a training error of essentially zero, because 10 parameters can pass through 10 points, yet its held-out error is 0.067, about 33 times worse than degree 5. Training error fell at every step; held-out error did not. Choosing the degree by training error would pick the worst model on new data.
Notice the sign of the gap column. Held-out error here is measured against the noiseless curve, so it is below training error for the smaller models, since the training targets contain noise that nothing could predict. This is a quirk of the setup, not a rule: with noisy test targets, held-out error would exceed it by the noise variance. What matters is that the trend separates after degree 5.
The same fit on shifted inputs
Now evaluate the same fitted models at 11 inputs between 1.0 and 1.5, outside the range [0, 1] they were trained on, against the true curve. The training and test inputs no longer share a distribution.
| Degree | Held-out MSE in [0, 1] | MSE in [1.0, 1.5] |
|---|---|---|
| 1 | 0.224 | 2.769 |
| 3 | 0.009 | 60.0 |
| 5 | 0.002 | 158.2 |
| 9 | 0.067 | 10,715,267 |
The cubic that looked excellent at 0.009 is off by 60.0 out there, and degree 9 by 10,715,267. High-degree polynomials are unconstrained beyond the data, so they diverge. Nothing in the training error or in an in-distribution test could have warned you. The only defence is to test on data resembling what the model will actually face.
Implementation
import numpy as np
from sklearn.model_selection import train_test_split, validation_curve
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=0
)
model = make_pipeline(PolynomialFeatures(), LinearRegression())
degrees = np.arange(1, 10)
train_scores, val_scores = validation_curve(
model, X_train, y_train,
param_name="polynomialfeatures__degree", param_range=degrees, cv=5,
scoring="neg_mean_squared_error",
)
# train_scores, val_scores: [len(degrees), 5]. Average over axis 1.
# Training error keeps falling; validation error turns up past the best degree.
best = degrees[val_scores.mean(axis=1).argmax()]
final = model.set_params(polynomialfeatures__degree=best).fit(X_train, y_train)
final.score(X_test, y_test) # touch the test set once, at the very end
The test set is used a single time, after every choice has been made. Choose the degree on the validation scores, not on the test split, or the test score becomes another training signal and stops being an honest estimate.
Tradeoffs
- Hold out more or fewer. A bigger test set gives a steadier estimate but leaves less to train on. With few examples, cross-validation reuses data at the price of fitting the model several times.
- The held-out estimate is noisy. With m test examples its standard error is about σ/√m. Two models within that margin are not distinguishable, and picking the best of many models on one split quietly overfits the split.
- More capacity, more data, or more constraint. A gap that shrinks as you add data (visible in a learning curve) says collect more; a gap that doesn't says simplify the model or add regularization.
- I.i.d. is an assumption, not a fact. Time series, repeated measurements and data scraped from the web break it, and the reported test score is then optimistic in ways that are hard to see.
- Interpolating is not always bad. Large neural networks routinely reach zero training error and still generalize, which the classical picture alone does not explain. Treat "zero training error means overfit" as a rule of thumb for small models, not a law.
Check yourself
Why is a model's training loss usually lower than its true risk?
Because the model was selected, and its parameters tuned, to make the training loss small. The same examples were used to choose and to score it, so the score is biased downward.
In the worked example, why is it wrong to pick the degree with the lowest training error?
Training error falls with every added parameter and reaches zero at degree 9, where held-out error is about thirty times worse than at degree 5. Capacity must be chosen on data the fit never saw.
You tune a hyperparameter on the test set until the score looks good. What have you lost?
The test set's independence. The reported score now reflects choices made to fit it, so it no longer estimates performance on fresh data. A separate validation set exists to absorb that tuning.
Connections
Read first
Read next
References
- Hastie, Tibshirani and Friedman. The Elements of Statistical Learning, 2nd ed., chapter 7, "Model Assessment and Selection". Springer, 2009.
- Vapnik. The Nature of Statistical Learning Theory. Springer, 1995, for empirical risk minimization and VC dimension.
- Belkin, Hsu, Ma and Mandal. Reconciling modern machine-learning practice and the classical bias–variance trade-off. PNAS, 2019 (arXiv 1812.11118).
- Nakkiran et al. Deep Double Descent: Where Bigger Models and More Data Hurt. ICLR 2020 (arXiv 1912.02292).
- Quiñonero-Candela, Sugiyama, Schwaighofer and Lawrence (eds.). Dataset Shift in Machine Learning. MIT Press, 2009.
- scikit-learn 1.9.1:
train_test_split,learning_curve,validation_curve.