ML Atlas

Machine learning from zero · Lesson 3.2 · 15 of 33 lessons ready · 10 min

Validation and cross-validation

In short

Choosing settings with the test set spoils it. Cross-validation tests a model on every part of the data in turn and shows how stable its score really is.

Warm-up Two questions from earlier lessons. A minute, and it never blocks the lesson.
From lesson 3.1: Overfitting: when a model memorises Why does more training data usually make a flexible model safer?

Show the answer

Correct answer: Because with more points the noise averages out and the curve cannot chase single points

Noise in individual points stays, but with many points it cancels out, so the fit follows the real trend.

From lesson 2.6: A metric and threshold for the cost of mistakes A miss costs 10, a false alarm 1, and the model is calibrated. What threshold follows from decision theory?

Show the answer

Correct answer: 1 / (1 + 10) ≈ 0.09, because an alarm already pays off at a small risk

An alarm pays off when p · 10 > (1 − p) · 1, that is p > 1/11. In the lab the threshold 0.09 costs 125. The cheapest threshold 0.06 costs 111 only on the scores it was picked on.

In the last lesson we picked the best polynomial degree by peeking at the test set. It felt harmless, but think about what it means: we tried several models and kept the one the test set liked best. The test set is no longer an unseen exam — it has become a judge we consulted while preparing. How do we choose settings without spoiling our only honest measurement?

Predict, then check You raise the number of folds k from 2 to 10 (178 wines in all). What happens to the gap between the worst and the best fold?
How sure are you?

Interactive lab

This interactive lab runs in a browser with JavaScript enabled. The text around it describes what the lab shows.

Show the answer

Correct answer: It grows, because each test fold has fewer wines

For k = 2 the worst fold scores 94.4% and the best 97.8% (a gap of 3.4 points). For k = 10 the range is 83.3% to 100.0% (16.7 points), because a test fold holds only 17 or 18 wines and one mistake weighs more than 5 points. The mean hardly moves (96.1% and 96.7%).

Try this

  • Set the k slider to its smallest value and read the accuracy of each fold. How different are the folds from each other?
  • Increase the k slider step by step. Watch the mean and the spread, and notice that each fold now contains fewer wines.
  • At a large k, look for folds with unusually low or high accuracy. With only a handful of wines in a fold, a single mistake moves that fold's score a lot.
  • Compare the spread with the differences between settings you might want to tune. Is a small gain in the mean bigger than the spread?

Now that you've tried it, let's see what was actually going on.

The idea in plain words

The first fix is a third pile of data. Split the rows into training, validation and test sets. Train candidate models on the training set, compare them on the validation set, pick the winner, and only then open the test set, once, to report the final score. The validation set absorbs all the peeking so the test set does not have to.

The problem: every split costs data. With a small dataset, a validation set big enough to be reliable leaves too little to train on, and lesson 2 showed that a small set gives a jumpy score. Cross-validation solves this by reusing the data cleverly.

Imagine judging a new cook. Tasting one dish tells you little: maybe it was her best, maybe her worst. So you ask for several dishes on different days and look at the average and how much they vary. Cross-validation does the same with a model.

Why it works

In k-fold cross-validation the data (excluding the final test set) are cut into k equal parts called folds. The model is trained k times. Each time, one fold is held out for validation and the remaining k − 1 folds are used for training. In the end every row has been used for validation exactly once and for training k − 1 times. Each training run sees this share of the data:

training share = (k − 1) / k

The result is not one score but k scores, one per fold. Their mean is a more reliable estimate than any single split, because it does not depend on one lucky or unlucky division of the rows. Their spread is just as valuable: it shows how much the score depends on which rows the model happened to get. Two models whose means differ by less than that spread cannot really be told apart.

The choice of k is a trade-off. A small k (say 2) trains each model on only half the data, so every model is weaker than the one you will finally build, and the estimate is pessimistic. A large k trains on almost all the data, but each fold is small, so individual fold scores become noisier, and the computation takes k times as long as a single fit. Values of 5 or 10 are a common compromise.

The example in this lesson is the Wine dataset: 178 wines from the same region of Italy, made from three different grape cultivars, each described by 13 results of a chemical analysis. The task is to tell the three cultivars apart.

Common mistake

Cross-validation protects you only if everything learned from data happens inside each fold. If you scale features or select the "best" features using all rows first and then cross-validate, information from the validation folds has already leaked into training, and the scores are too optimistic. The second mistake is to tune dozens of settings by cross-validation and report the best cross-validated score as the final result. After so many comparisons, the winner is partly lucky; that is why a separate test set, used once, still matters.

With honest evaluation in our toolkit we can explore more powerful models without fooling ourselves. In the next lesson we meet the most readable of them: the decision tree.

Quiz

Three questions to check yourself. Pick an answer, or open the answer under a question. Keyboard: arrow keys choose, Enter or Space confirms.

1. In 5-fold cross-validation, how many times is each row used for validation?
Show the answer

Correct answer: Exactly once — in one of the five folds

Each row belongs to one fold, and each fold serves as the validation set in exactly one of the five runs.

2. Model A has a mean cross-validated accuracy slightly above model B, but the spread across folds is much larger than the difference. What follows?
Show the answer

Correct answer: The data do not show a reliable difference between these models

When the difference in means is smaller than the variation between folds, it could easily be due to chance.

3. Why is scaling the features on all rows before cross-validation a mistake?
Show the answer

Correct answer: Because statistics from validation folds leak into training and inflate scores

Every step that learns from data must be fitted on the training folds only, or the validation folds are no longer unseen.

Key takeaways

  • Choosing settings with the test set spoils it; use a validation set or cross-validation instead.
  • In k-fold cross-validation every row is used for validation exactly once.
  • The mean of the fold scores estimates performance; the spread shows how much it depends on the split.
  • All data-driven preparation must happen inside each fold to avoid leakage.

Going further: formula, code, practice

The math

In k-fold cross-validation the n rows are cut into k folds. Each fit trains on n · (k − 1) / k rows and tests on the remaining n / k. The score is the average over the folds: CV = (1/k) · Σ score_f. The spread is their standard deviation s = √( Σ (score_f − CV)² / (k − 1) ).

A fold of m test rows gives an accuracy that moves in steps of 1 / m. If the true accuracy is p, its standard error is about √( p · (1 − p) / m ). That grows as m shrinks, which is why single folds get noisier as k rises, even though the mean gets less pessimistic. With 178 wines and k = 10, one mistake in a fold of 17 or 18 wines costs 5.6 to 5.9 points.

Fold scores are not fully independent: the training sets overlap. So s / √k understates the real uncertainty of the mean.

In code (Python)

The 178 wines with 5-nearest-neighbours and standardised features. To see the pattern behind the lab's single shuffle, the gap between the worst and the best fold is averaged over 20 shuffles. The folds are stratified and shuffled with a seed, so the numbers are not the lab's 3.4 and 16.7 points:

import numpy as np
from sklearn.datasets import load_wine
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_wine(return_X_y=True)
model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))  # scaler is refitted inside every fold
for k in (2, 10):
    runs = [cross_val_score(model, X, y, cv=StratifiedKFold(k, shuffle=True, random_state=s)) for s in range(20)]
    gap = np.mean([a.max() - a.min() for a in runs])
    print(f"k={k:2d}: mean accuracy {np.mean(runs):.3f}, average worst-to-best gap {gap:.3f}")

Output

k= 2: mean accuracy 0.956, average worst-to-best gap 0.020
k=10: mean accuracy 0.966, average worst-to-best gap 0.113

The mean barely moves (0.956 and 0.966), but the average gap between the worst and the best fold grows from 0.020 to 0.113 as k goes from 2 to 10.

For practitioners
  • Fit every learned step (scaling, imputing, feature selection) inside each fold. In scikit-learn, put them in a Pipeline and pass that to cross_val_score.
  • For classification use stratified folds, so that every fold has all classes. cross_val_score does this by default for classifiers when cv is a number, but that default does not shuffle.
  • Shuffle before you cut folds if the rows are ordered, for example by class or by date. load_wine is sorted by class, so pass StratifiedKFold(k, shuffle=True, random_state=0), as above. For time series do not shuffle: use a forward-chaining split.
  • If rows belong to groups (several rows per patient), keep each group in one fold with GroupKFold. Otherwise the same patient is in training and in validation.
  • Report the mean and the spread. If two models differ by less than the spread, do not call one of them better.

Go deeper in the Atlas

Encyclopedia entries on the ideas in this lesson: