ML Atlas

Machine learning from zero · Lesson 3.5 · 18 of 33 lessons ready · 14 min

Tuning hyperparameters without cheating

In short

The best of 9 tree settings scores 93.7% in cross-validation and 88.3% on new biopsies. Picking the winner inflates its score, just as picking the seed does.

Warm-up Two questions from earlier lessons. A minute, and it never blocks the lesson.
From lesson 3.4: Regularisation: ridge and lasso In the lab lasso keeps 3 features and the test R² falls from 0.470 to 0.386. What does that mean?

Show the answer

Correct answer: That a simpler model costs some accuracy: both goals have to be weighed

Fewer features make a model easier to use and explain, but here it explains a smaller share of the variation in the outcome. The strength of the penalty is chosen on validation, weighing simplicity against accuracy.

From lesson 3.3: Learning curves: more data or a better model? Why does training accuracy fall as the training data grow?

Show the answer

Correct answer: Because memorising every example gets harder, so the model generalises

With 50 images the model gets all of them right, with 1438 it gets 98.0%. Validation, in turn, rises from 82.3% to 95.8%.

In lesson 3.2 cross-validation scored one setting of a model. In practice you try many and take the best. That is called tuning. The settings you choose before training (the depth of a tree, the strength of the penalty α from lesson 3.4) are hyperparameters: the model does not learn them from the data, you choose them. Today you will see that the winner's score in tuning is too optimistic, and why.

Predict, then check A decision tree recognises malignant tumours from 398 biopsies. You try 9 settings: 3 depths times 3 minimum numbers of biopsies in a leaf. You score each one in 5-fold cross-validation. The winner gets 93.7%. What will it get on 171 new biopsies it has never seen?
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: Less, for example 88.3%, because we also picked favourable noise

On the 171 test biopsies the winner gets 88.3%, 5.4 points less than in cross-validation. Part of that is selection, part is the small test: one biopsy is worth 0.6 points. Picking the best of 20 split seeds inflates the score by 2.7 points. This split is on the pessimistic side: on 50 other splits (code below) the winner loses 1.4 points on average.

Try it

  • With the grid, move the budget from 4 to 9. What does the winner score in cross-validation, and what on the test?
  • Switch to random search and click "new draw" a few times. Is the winner ever better on the test than in validation?
  • In the second part move the seed slider from 1 to 20. How does the best score grow, and how far does it move from the mean of 200 splits?

Once you have checked it in the lab, let's name what you see.

The idea in plain words

Tuning means comparing many settings on validation data and choosing the best one. Grid search tries every combination of a few values. Random search draws combinations from the whole range. On the same budget a 3×3 grid tries only 3 depths: 1, 6 and 12. Nine random tries usually see more different values of each hyperparameter.

Every validation score is the setting's true quality plus a bit of luck: which biopsies happened to land in which part. When you pick the best score, you also pick the setting that luck favoured most. On new data that luck is gone. This is the winner's curse. It grows with the number of settings you compare and shrinks with the size of the data.

The same happens when you look for the best split of the data. The same logistic regression on 200 random splits scores 93.8% on the test on average. If you try seeds 1–20 and report the best one, you get 96.5%, 2.7 points more. The code is the same; you only picked the luck.

Jak to policzono
  • Biopsies: Breast Cancer Wisconsin (Diagnostic), UCI, CC BY 4.0 licence. 569 biopsies, 10 "mean" measurements each.
  • Tuning: stratified split, 398 biopsies for tuning and 171 for the test. A CART tree (Gini), 10 depths times 11 minimum numbers of biopsies in a leaf, i.e. 110 settings, each in 5-fold cross-validation on the 398 biopsies.
  • Winner: the best mean over the 5 parts; on a tie the simpler tree. On the test, the tree trained on all 398 biopsies is scored.
  • Seeds: logistic regression (C = 1) on standardised measurements, 200 random 70/30 splits, 171 test biopsies in each.

Why it works

Take 9 settings of almost the same true quality. Their validation scores differ mostly by noise. The highest of 9 scores is almost always one where the noise was positive. In the lab the best CV gets 373 of 398 biopsies right, and several settings are only one or two biopsies behind. Differences that small are largely chance.

Selection shows in random mode: over 200 random draws of 9 settings, the winner scores higher in validation than on the test in every single one, by 3.6 points on average. The test played no part in the choice, so it carries no selection, only the ordinary noise of a finite set. All 200 draws, though, are different settings on one and the same data split, and that split landed on the pessimistic side: even with no choice at all, the 110 settings score 0.6 points higher in validation than on the test on average. In the code below, on 50 different splits, the winner loses 1.4 points on average, and in 17 of the 50 splits validation does not come out higher than the test.

That is why the test cannot be used for choosing. If you chose a setting by its test score, the best cell would get 92.4% on the test. That number would be inflated too, only harder to notice, because no data would be left to check it.

Common mistake

True or false: "the score of the tuning winner is a forecast for new data". False. The winner's validation score is inflated, because it is the maximum of many noisy numbers. In the lab the winner of 9 settings has 93.7% in validation and 88.3% on the test. In the code below, over 50 splits and 110 settings, the winner scores 93.3% in validation on average and 91.9% on the held-out test.

A fair forecast comes from one of two routes. One: a held-out test that you use for no choice at all and look at once, at the end. Two: nested cross-validation, the whole tuning repeated inside every part of an outer cross-validation. On all 569 biopsies the winner gets 94.0% in ordinary cross-validation, while the nested one gives 91.9%. The second number is closer to what the model will do tomorrow.

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. What is a hyperparameter?
Show the answer

Correct answer: A setting chosen before training that the model does not learn from the data itself

Hyperparameters such as the depth of a tree or the strength of the penalty α are chosen by you, usually on validation. Weights and the thresholds in a tree are learned by the model itself.

2. The tuning winner scores 93.7% in cross-validation. What should you expect on new data?
Show the answer

Correct answer: Rather less, because picking the best of many scores also picks the luck

That is the winner's curse. In the lab the winner of 9 settings gets 88.3% on the test; over 200 random draws its validation score is higher than its test score every time.

3. How do you fairly score a model whose hyperparameters you chose by cross-validation?
Show the answer

Correct answer: Check it on a held-out test or with nested cross-validation around the tuning

A held-out test and nested cross-validation took no part in the choice. In the code ordinary cross-validation gives 94.0%, the nested one 91.9%.

Key takeaways

  • You choose hyperparameters yourself, before training; tune them on validation, never on the test.
  • The tuning winner's score is inflated: picking the best score also picks the luck (the winner's curse).
  • Picking the best split seed is tuning on the test set too.
  • A fair forecast comes from a held-out test looked at once, or from nested cross-validation.

Going further: formula, code, practice

The math

Let the true quality of setting k be μₖ and its validation score Ŝₖ = μₖ + εₖ, where the noise εₖ has mean 0. For one setting E[Ŝₖ] = μₖ. For the winner k = argmaxₖ Ŝₖ, on average E[Ŝ_k] ≥ μ_k*: the maximum of several noisy numbers is on average higher than the true quality of the one chosen. With m settings of equal quality E[max εₖ] grows with m roughly like σ · √(2 · ln m).

One test biopsy at n = 171 is 1/171, i.e. 0.6 points. The standard error of an accuracy p on n examples: √(p(1 − p)/n).

Nested cross-validation: an outer loop splits the data into K parts; for each part the whole tuning (an inner cross-validation) runs on the others only, and the chosen model is scored on the part held out.

In code (Python)

The same experiment in scikit-learn: the winner's curse on 50 splits with the 110 tree settings, ordinary and nested cross-validation on all biopsies, and the best of 20 split seeds.

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

X, y = load_breast_cancer(return_X_y=True)
X = X[:, :10]  # the 10 "mean" measurements, as in the lab
grid = {"max_depth": [1, 2, 3, 4, 5, 6, 7, 8, 10, 12], "min_samples_leaf": [1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48]}  # the lab's 110 settings
tree = DecisionTreeClassifier(random_state=0)
r = []
for s in range(50):
    tr, te = train_test_split(np.arange(len(y)), test_size=0.3, stratify=y, random_state=s)
    search = GridSearchCV(tree, grid, cv=StratifiedKFold(5, shuffle=True, random_state=s)).fit(X[tr], y[tr])
    r.append((search.best_score_, search.score(X[te], y[te])))  # the winner's CV score, then the winner on the held-out test
r = np.array(r)
print(f"winner of 110 settings, 50 splits: CV {r[:, 0].mean():.1%}, held-out test {r[:, 1].mean():.1%}; CV higher in {(r[:, 0] > r[:, 1]).sum()} of 50")
outer = StratifiedKFold(5, shuffle=True, random_state=0)
flat = GridSearchCV(tree, grid, cv=outer).fit(X, y).best_score_  # the winner's score on the same folds that chose it
nested = cross_val_score(GridSearchCV(tree, grid, cv=StratifiedKFold(5, shuffle=True, random_state=1)), X, y, cv=outer).mean()  # the search runs inside each outer fold
print(f"all 569 biopsies: winner's CV {flat:.1%}, nested CV {nested:.1%}")
model = make_pipeline(StandardScaler(), LogisticRegression())
acc = []
for s in range(200):  # 200 random splits, 30% test, as in the lab
    tr, te = train_test_split(np.arange(len(y)), test_size=0.3, random_state=s)
    acc.append(model.fit(X[tr], y[tr]).score(X[te], y[te]))
acc = np.array(acc)
best = acc.reshape(10, 20).max(axis=1)  # ten people, each tries 20 seeds and reports the best one
print(f"logistic regression, 200 seeds: mean {acc.mean():.1%}; best of 20 seeds {best.mean():.1%} on average (+{(best.mean() - acc.mean()) * 100:.1f} points), higher in {(best > acc.mean()).sum()} of 10")

Output

winner of 110 settings, 50 splits: CV 93.3%, held-out test 91.9%; CV higher in 33 of 50
all 569 biopsies: winner's CV 94.0%, nested CV 91.9%
logistic regression, 200 seeds: mean 93.7%; best of 20 seeds 96.4% on average (+2.6 points), higher in 10 of 10

The splits differ from the lab's, so the numbers differ too. The picture is the same: the tuning winner scores 93.3% in validation on average and 91.9% on the held-out test; validation is higher in 33 splits of 50. Nested cross-validation gives 91.9% instead of 94.0%, and the best of 20 seeds inflates the score by 2.6 points, for every one of the 10 people.

For practitioners
  • In scikit-learn: GridSearchCV and RandomizedSearchCV. The best_score_ is inflated for the same reason as in the lab; score the winner with score on a held-out test.
  • Nested cross-validation: cross_val_score(GridSearchCV(...), X, y, cv=...). The tuning then runs inside every outer part.
  • With many hyperparameters of which only a few matter, random search usually finds a good setting faster than a grid (Bergstra and Bengio, 2012).
  • Set the random seed once, before the experiment, and write it down. Report the mean and spread over several splits or from cross-validation, not the score of the best seed.

Go deeper in the Atlas

Encyclopedia entries on the ideas in this lesson: