Problem
The generalization page defined the gap between training error and error on new data. When a model does badly, that gap is the first thing to look at, because it points to opposite fixes. Add capacity to a model that is too simple and it improves. Add capacity to a model that already memorizes and it gets worse.
So the working problem is diagnosis: from numbers you can measure on a training set and a validation set, decide which situation you are in before you spend time on the wrong remedy.
Intuition
A student who reads only the chapter titles fails every test (underfit: not enough learned). A student who memorizes the answers to last year's exam scores perfectly on it and badly on this year's (overfit: learned the specific questions, not the subject). The first student's practice scores and real scores are both low. The second student's practice score is high and real score is low.
That is the whole diagnostic. With a noise floor σ² that no model can beat:
- Underfitting (high bias). Training error is high, close to validation error, and both sit well above the noise floor. More data does not help; more capacity does.
- Overfitting (high variance). Training error is low, validation error is much higher, and the gap shrinks as the training set grows. More data, a simpler model or regularization help.
- A good fit. Both errors are close to the noise floor and close to each other.
Training error alone cannot tell you any of this, since a flexible enough model drives it to zero. The signal is always the comparison with held-out data (see data splits and cross-validation).
Errors above 0.5 are clipped at the top. The line's curves meet high; the degree-9 curves meet low, but only after enough data.
Mechanics
Two curves that diagnose
A learning curve plots training and validation error against the number of training rows. It answers "would more data help?": if the two curves have already met, the answer is no; if they are still apart, yes. A validation curve (also called a complexity curve) plots the same two errors against one hyperparameter, such as polynomial degree, tree depth or regularization strength, with the data fixed. Underfitting is the left side, where both are high; overfitting is the right side, where training keeps falling and validation turns up. Both are computed with cross-validation, so the validation number comes from rows the model was not fitted on.
Left of the minimum the model overfits; right of it, it underfits. Values above 0.6 are clipped.
Remedies by diagnosis
- For underfitting: a more flexible model (more features, deeper trees, more layers), less regularization, or better features; train longer if the optimizer has not converged.
- For overfitting: more (or augmented) data, a simpler model, regularization (an L2 or L1 penalty, dropout), early stopping, or ensembling to average away variance.
- For both: check the labels and the split first. Noisy labels raise the floor, and leakage can make a model look better than it is.
Memorization
Overfitting at its extreme is memorization: a model with enough capacity can fit labels that are pure noise. Deep networks have been shown to reach zero training error on randomly shuffled labels (Zhang et al., 2017), which is why low training error says nothing about whether anything was learned. The worked example does the same with a decision tree. Regularization and early stopping work by making this memorization costly or by stopping before it happens.
Early stopping
For models trained iteratively, such as boosted trees or neural networks, the number of training steps acts like a capacity knob: early on the model is simple, and every step lets it fit more detail.
Watching validation error and stopping at its minimum picks the right capacity automatically. scikit-learn's validation_curve and learning_curve do the bookkeeping for the other cases.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
learning_curve |
train_sizes, cv, scoring |
Returns the training sizes and arrays of train and validation scores, one row per size and one column per fold. | _validation.py |
_translate_train_sizes |
fractions or counts to row counts | Turns the requested sizes into absolute numbers of rows, checking they fit the data. | _validation.py |
_incremental_fit_estimator |
partial_fit models |
Used when exploit_incremental_learning is true: grows the training set step by step without refitting from scratch. |
_validation.py |
validation_curve |
param_name, param_range |
Scores the estimator for each value of one hyperparameter, with cross-validation, returning train and validation scores. | _validation.py |
DecisionTreeClassifier |
max_depth=None, min_samples_leaf=1, ccp_alpha=0.0 |
The defaults grow until every leaf is pure, the memorizing end of the capacity scale. The three parameters are the knobs that pull it back. | _classes.py |
Worked example
Learning curves for three model sizes
Data: y = sin(2πx) + noise (σ = 0.3, so the noise floor is σ² = 0.09), x uniform on 0 to 1. Models are polynomials of degree 1, 4 and 9. For each training size the table gives the median over 30 random training sets of the training error, and the error on a fixed 400-row validation set. (The median is used because a degree-9 fit on 15 points sometimes goes wild, and a mean would be dominated by those fits.)
| Rows | Degree 1 train / val | Degree 4 train / val | Degree 9 train / val |
|---|---|---|---|
| 15 | 0.236 / 0.346 | 0.050 / 0.148 | 0.028 / 7.593 |
| 20 | 0.251 / 0.350 | 0.056 / 0.139 | 0.033 / 0.926 |
| 40 | 0.272 / 0.336 | 0.075 / 0.121 | 0.066 / 0.136 |
| 80 | 0.269 / 0.331 | 0.088 / 0.115 | 0.081 / 0.112 |
| 160 | 0.277 / 0.329 | 0.087 / 0.110 | 0.083 / 0.105 |
| 320 | 0.283 / 0.327 | 0.092 / 0.110 | 0.089 / 0.104 |
The straight line stalls at about 0.327 validation error however much data it gets, with training error 0.283 right beside it: underfitting. It cannot bend. Degree 9 on 15 rows has training error 0.028 and validation error 7.593, a huge gap: overfitting. By 320 rows the gap is 0.016 and validation error is 0.104, close to the floor. Degree 4 is the sensible middle, already at 0.110 with a small gap.
Regularization pulls the overfit model back
Keep degree 9 and 15 training rows, and add an L2 penalty α on the coefficients (ridge regression, intercept unpenalized). Median over 30 draws, same validation set:
| α | Train error | Validation error |
|---|---|---|
| 0 | 0.028 | 7.593 |
| 0.0001 | 0.041 | 0.261 |
| 0.001 | 0.044 | 0.217 |
| 0.01 | 0.051 | 0.167 |
| 0.1 | 0.074 | 0.179 |
| 1 | 0.173 | 0.297 |
| 10 | 0.384 | 0.492 |
With α = 0 the fit is 7.593 on validation data. A small penalty brings it down to 0.167 at α = 0.01, while training error rises from 0.028 to 0.051: worse on the data it saw, better on new data. A large penalty (α = 10) underfits, with both errors high. Strength of regularization is a capacity dial, and the validation curve over it has the same U shape as the one over degree.
Fitting coin flips
60 points with two random coordinates and labels that are independent coin flips (a fair 0/1 each, so there is nothing to learn). A fully grown decision tree reaches 100% training accuracy using 22 leaves. On 400 fresh points drawn the same way its accuracy is 52.2%, which is chance. A model that always predicts the majority label would have scored 55% on the training rows. The perfect training score is the tree's capacity talking, not the data's.
Early stopping on boosted trees
On the noisy-sine problem from the gradient boosting page, gradient boosting with a learning rate of 1.0 has validation error 0.388 after one round, a minimum of 0.314 after round 2, and 0.463 after round 60. Each extra round fits more of the training noise. Stopping at the minimum, using held-out data to find it, is a free form of regularization.
Implementation
import numpy as np
from sklearn.model_selection import learning_curve, validation_curve
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import Ridge
model = make_pipeline(PolynomialFeatures(9), StandardScaler(), Ridge(alpha=1e-2))
# does more data help?
sizes, train, val = learning_curve(
model, X, y, cv=5, train_sizes=np.linspace(0.1, 1.0, 6),
scoring="neg_mean_squared_error",
)
print(-train.mean(axis=1), -val.mean(axis=1)) # rows: sizes, columns: folds
# which capacity is right?
alphas = np.logspace(-4, 1, 6)
train, val = validation_curve(
model, X, y, param_name="ridge__alpha", param_range=alphas,
cv=5, scoring="neg_mean_squared_error",
)
The scores are returned negated for losses (neg_mean_squared_error), so higher is better; flip the sign before reading them as errors.
Both functions refit the model many times, so on large data use a subsample or fewer sizes.
Tradeoffs
- Diagnosis needs honest validation data. If the validation set is tuned against repeatedly, it leaks into the model and the gap looks smaller than it is. Keep a final test set untouched.
- Noise changes the picture. With noisy labels the floor is above zero, so "training error is not zero" is not a sign of underfitting; compare with the floor, not with 0.
- Remedies cost something. More data is slow or expensive; regularization adds a hyperparameter; early stopping needs a validation split that the model then cannot train on; simpler models may underfit.
- Modern models blur the line. Very large networks can interpolate their training data and still generalize, so a zero training error does not always mean the usual overfitting (see double descent on the generalization page). Judge by held-out performance.
- Curves are noisy. One split gives a jagged curve. Use cross-validation and look at the trend, as the medians above do.
Check yourself
Training error 0.02 and validation error 0.35, with a noise floor of 0.05. What is the diagnosis and what would you try first?
Overfitting: the gap is large and training error is below the floor. Try more data, a simpler model or stronger regularization, and check the learning curve to see whether the gap is closing.
Training error 0.40 and validation error 0.42, with a noise floor of 0.05. Would doubling the data help?
No. This is underfitting: both errors are high and already together. Add capacity or better features instead.
A network reaches 100% training accuracy on shuffled labels. What does that tell you about its 100% on the real labels?
That the training score alone proves nothing, since the network can memorize. Only held-out accuracy shows whether it learned a pattern.
Connections
Read first
- Generalization intro
Read next
- Regularization core
References
- Hastie, Tibshirani and Friedman. The Elements of Statistical Learning, chapter 7: model assessment and selection.
- Zhang, Bengio, Hardt, Recht and Vinyals. Understanding deep learning requires rethinking generalization. ICLR 2017. arXiv:1611.03530.
- Belkin, Hsu, Ma and Mandal. Reconciling modern machine-learning practice and the classical bias–variance trade-off. PNAS 2019. arXiv:1812.11118.
- scikit-learn 1.9.1:
learning_curve,validation_curve.