Overfitting is the most common way a model that looks excellent in a notebook fails in the real world. This page explains how to recognise it and the regularization techniques that prevent it, with a runnable example.
A model overfits when it learns patterns that exist only in its training data — including random noise — and therefore performs worse on new data. The opposite failure, underfitting, is a model too simple to capture the real pattern, which does poorly everywhere. The goal of machine learning is not to fit the training set; it is to generalise to examples the model has never seen.
Regularization is the name for any technique that deliberately limits how closely a model can fit its training data, trading a little training accuracy for better accuracy on unseen data.
Training error is always optimistic. A deep network or a deep decision tree can drive training error to zero on almost any dataset, even one with random labels (Zhang et al., 2017 demonstrated this for image classifiers). If you judge a model by training accuracy you will ship models that fail in production. Every serious workflow therefore holds back validation and test data, and every modern model family ships with regularization knobs whose defaults matter.
The picture is more nuanced for very large neural networks. Heavily over-parameterised models can sit past the classic U-shaped curve, where adding parameters lowers test error again, a phenomenon called double descent (Belkin et al., 2019; Nakkiran et al., 2020). That does not make regularization obsolete: weight decay, data augmentation, dropout and early stopping are still standard in training large models, and fine-tuning a big pretrained model on a few thousand examples overfits quickly without them. The practical message is unchanged — always measure on held-out data.
The three panels fit the same nine noisy points. A flat line ignores the trend (high bias). A straight sloped line captures the trend and ignores the noise. A curve that passes exactly through every point has zero training error but swings wildly between points (high variance): a new point from the same process will likely be far from it. This is the bias-variance trade-off: expected test error decomposes into bias squared, plus variance, plus irreducible noise, and model complexity moves error between the first two.
The standard diagnostic is the learning curve. Training loss keeps falling as the model trains longer, but validation loss reaches a minimum and then rises: from that point the model is memorising. A large and growing gap between the two curves is the signature of overfitting; both curves high and flat signals underfitting.
The geometry explains why L1 creates sparsity. Minimising loss subject to a penalty is equivalent to finding where the loss contours first touch the constraint region. The L1 region is a diamond with corners on the axes, and contours usually touch at a corner, where one weight is exactly zero. The L2 circle has no corners, so the touching point usually has both weights small but non-zero.
This scikit-learn script fits a degree-15 polynomial with and without regularization and compares training and validation error.
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
rng = np.random.default_rng(0)
X = rng.uniform(-3, 3, size=(60, 1))
y = np.sin(X).ravel() + rng.normal(0, 0.3, size=60)
Xtr, Xva, ytr, yva = train_test_split(X, y, test_size=0.4, random_state=0)
models = {
"no regularization": LinearRegression(),
"ridge alpha=1": Ridge(alpha=1.0),
"lasso alpha=0.01": Lasso(alpha=0.01, max_iter=50_000),
}
for name, reg in models.items():
m = make_pipeline(PolynomialFeatures(15), StandardScaler(), reg).fit(Xtr, ytr)
tr = mean_squared_error(ytr, m.predict(Xtr))
va = mean_squared_error(yva, m.predict(Xva))
nz = np.sum(np.abs(m[-1].coef_) > 1e-6)
print(f"{name:18s} train={tr:.3f} val={va:.3f} nonzero coefs={nz}")
The unregularized model has the lowest training error and a much higher validation error. Ridge and lasso give up a little training error for a lower validation error, and lasso keeps only a handful of the sixteen polynomial terms. In Keras the equivalent tools are kernel_regularizer=keras.regularizers.l2(1e-4), layers.Dropout(0.3) and the EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True) callback; in PyTorch, pass weight_decay to the optimiser (prefer AdamW, which decouples it correctly).
| Technique | Mechanism | Pros | Cons |
|---|---|---|---|
| L2 / weight decay | Penalise squared weights | Stable, almost always helps, one knob | Does not remove features |
| L1 / lasso | Penalise absolute weights | Sparse, interpretable models | Unstable with correlated features |
| Elastic net | L1 + L2 | Sparsity with stability | Two hyperparameters |
| Early stopping | Stop at best validation loss | Free, saves compute | Needs a clean validation set |
| Data augmentation | More varied training data | Often the largest gain for vision and audio | Must preserve labels; domain-specific |
| Dropout | Random unit masking | Cheap ensemble effect in deep nets | Slows convergence; less useful with batch norm in CNNs |
| Cross-validation | Better error estimate | Detects overfitting reliably | Does not prevent it; costs k fits |