ML Atlas

Machine learning from zero · Lesson 3.6 · 19 of 33 lessons ready · 12 min

Early stopping

In short

A network trained too long memorises wrong labels. With 20% random labels, 100 epochs instead of early stopping cut test accuracy from 86.1% to 76.3%.

Warm-up Two questions from earlier lessons. A minute, and it never blocks the lesson.
From lesson 3.5: Tuning hyperparameters without cheating 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%.

From lesson 3.4: Regularisation: ridge and lasso How does the ridge penalty differ from the lasso penalty?

Show the answer

Correct answer: Ridge pulls every weight towards zero, lasso sets some of them exactly to zero

Ridge penalises squared weights and zeroes none; lasso penalises absolute values and zeroes the weights one by one. In the lab ridge at α = 398 has 10 non-zero weights, lasso at α = 15.8 only 3.

A neural network learns in epochs: one epoch is one pass over all the training examples. After each epoch the training loss usually falls. The question from lesson 3.1 comes back in a new form: how many epochs are too many? Early stopping answers it with the validation set: training stops when the validation loss stops falling, and the model goes back to its best epoch.

Predict, then check A small neural network learns to recognise handwritten digits from 299 images. In 20% of them the label is a random digit instead of the true one. The lowest validation loss comes in epoch 28. You train for 100 epochs, though, instead of stopping at the best one. What happens to the accuracy on the test?
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 falls from 86.1% to 76.3%, because the network memorises the wrong labels

The weights of epoch 28 get 86.1% on the 997 test images, after 100 epochs 76.3%: 9.7 points less. Meanwhile the validation loss rises from 0.48 to 0.84, and of the 51 wrong labels the network ends up repeating all of them.

Try it

  • The lab opens on clean labels. Switch to the random labels and keep a patience of 5. In which epoch does training stop, which weights are kept, and is that the best epoch of the whole run?
  • Raise the patience to 10. What changes in the kept epoch and in the test accuracy?
  • Go back to clean labels. What does training to the end cost now?

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

The idea in plain words

At the start of training the network learns what many images share: the shape of a zero, an eight, a seven. The training and validation losses fall together. Then the network starts fitting individual examples, including the ones with a wrong label. The training loss keeps falling, while the validation loss starts to rise. That is the overfitting of lesson 3.1, only along the axis of epochs instead of the number of parameters.

Early stopping watches the validation loss. It remembers the epoch with the lowest loss and counts the epochs without an improvement. When there are as many as the patience, training stops and the network goes back to the weights of the best epoch. In the lab, with a patience of 10, training stops in epoch 38 and the weights of epoch 28 are kept: 86.1% on the test instead of 76.3% after 100 epochs.

Too little patience hurts too. The validation curve wobbles from epoch to epoch. With a patience of 5 training stops as early as epoch 23 and keeps the weights of epoch 18 (85.0% on the test), although the best was epoch 28.

Jak to policzono
  • Digits: Optical Recognition of Handwritten Digits, UCI, CC BY 4.0 licence. 1797 images of 8×8 pixels.
  • Stratified split: 299 images to learn from, 501 for validation, 997 for the test. In the noisy version each training label is replaced, with probability 20%, by a random digit: 51 labels changed (a random digit sometimes hits the true one).
  • Network 64 → 32 → 10, ReLU, cross-entropy, SGD with momentum 0.9, step 0.05, batches of 32 images, 100 epochs. Validation loss computed after every epoch.
  • Validation and test have the true labels. The noise is in the training data only.

Why it works

An example with a wrong label is hard for the network: the image looks like a three, but the label says "seven". To lower the loss on such an example, the network has to learn a rule that fits only that example. It usually does so late, once the easy, shared patterns are learned. In the lab the network repeats 10 of the 51 wrong labels in epoch 28, and all 51 after 100 epochs. On its own training data it then has 99.7% accuracy: it agrees even with the labels that are false.

Rules fitted to wrong labels spoil the answers on new images. The validation set has the true labels, so its loss rises exactly when the network starts learning the noise. That is why the validation loss, not the training loss, tells you when to stop.

This is not a quirk of one run. In the code below the same experiment runs in scikit-learn on 10 other splits and noise draws: the best epoch falls between 13 and 23, and training to 100 epochs lowers the test accuracy in every one of the 10 runs, on average from 87.2% to 76.4%. The lab’s best epoch (28) falls outside that range because it also depends on the starting weights and the batch order: the lab’s own network with 9 other random seeds does best between epochs 12 and 25, and training to 100 epochs lowers the test accuracy every time.

Common mistake

True or false: "training longer always gives a better model". False. Training longer always fits the training data better, but not new data. With random labels, 100 epochs give 99.7% accuracy on the training set and 76.3% on the test, while stopping at epoch 28 gives 86.1% on the test.

A note the other way round: with clean labels training to the end spoils almost nothing. The lowest validation loss comes in epoch 30 (93.9% on the test), and after 100 epochs the accuracy is 94.5%. Early stopping does not always save accuracy, but it always saves time and guards against data that are worse than you think.

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 does early stopping use to decide when to stop training?
Show the answer

Correct answer: The validation loss: training stops when it stops falling

The training loss falls almost to the end, even when the network learns noise. The validation loss, computed on data outside training, rises when the model starts to overfit.

2. With 20% random labels the network has 99.7% on training and 76.3% on the test after 100 epochs. What does it mean?
Show the answer

Correct answer: The network has memorised the wrong labels too, and that spoils its answers on new data

High training accuracy with noise in the labels means the network has fitted the noise too. The weights of epoch 28 get 86.1% on the test.

3. A patience of 5 stops training in epoch 23 and keeps epoch 18, although the lowest loss is in epoch 28. Why?
Show the answer

Correct answer: Because the validation curve wobbles, and too little patience takes a wobble for the peak

The validation loss does not fall evenly: a few worse epochs in a row can be chance. With a patience of 10 training reaches epoch 28.

Key takeaways

  • An epoch is one pass over the training data; the training loss almost always falls, the validation loss does not.
  • Early stopping halts training when the validation loss has not improved for a few epochs (the patience) and goes back to the best weights.
  • Training too long on wrong labels teaches the network the noise: the test falls from 86.1% to 76.3%.
  • Too little patience stops training on a chance wobble of the curve.

Going further: formula, code, practice

The math

Validation loss after epoch e: L_val(e) = −(1/m) · Σᵢ log p_e(yᵢ | xᵢ) on m validation images (cross-entropy).

The rule with patience P: e = argmin_{e ≤ t} L_val(e); training stops at the first epoch t with t − e ≥ P and returns the weights w(e). If no such epoch comes, it returns w(e) over the whole run.

Early stopping acts much like the penalty of lesson 3.4. With gradient descent and a small step the weights start near zero and move away from it with every epoch. Fewer epochs mean smaller weights: for a linear model with squared loss, stopping after t steps has roughly the same effect as ridge with α of the order of 1 / (step · t).

In code (Python)

The same experiment in scikit-learn: 10 different splits and noise draws, a network 64 → 32 → 10 trained epoch by epoch, keeping the weights of the epoch with the lowest validation loss.

import copy
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import log_loss

X, y = load_digits(return_X_y=True)
X = X / 16
def run(seed):  # 300 digits to learn from, 500 for validation, the other 997 for the test
    used, te = train_test_split(np.arange(len(y)), train_size=800, stratify=y, random_state=seed)
    tr, va = train_test_split(used, test_size=500, stratify=y[used], random_state=seed)
    rng = np.random.default_rng(seed)
    noisy = y[tr].copy()
    flip = rng.random(len(tr)) < 0.2  # 20% of the training labels replaced by a random digit
    noisy[flip] = rng.integers(0, 10, flip.sum())
    net = MLPClassifier(hidden_layer_sizes=(32,), solver="sgd", learning_rate_init=0.05, momentum=0.9, batch_size=32, alpha=0, random_state=seed)
    best, best_loss = None, np.inf
    for epoch in range(1, 101):
        net.partial_fit(X[tr], noisy, classes=np.arange(10))
        loss = log_loss(y[va], net.predict_proba(X[va]), labels=np.arange(10))
        if loss < best_loss:
            best, best_loss, best_epoch = copy.deepcopy(net), loss, epoch  # early stopping keeps the weights of the best epoch
    return best_epoch, best.score(X[te], y[te]), net.score(X[te], y[te])
r = np.array([run(s) for s in range(10)])
print(f"10 splits and noise draws: lowest validation loss at epoch {r[:, 0].min():.0f} to {r[:, 0].max():.0f}; test at that epoch {r[:, 1].mean():.1%}, after 100 epochs {r[:, 2].mean():.1%}; lower after 100 in {(r[:, 2] < r[:, 1]).sum()} of 10")

Output

10 splits and noise draws: lowest validation loss at epoch 13 to 23; test at that epoch 87.2%, after 100 epochs 76.4%; lower after 100 in 10 of 10

The splits, starting weights and noise differ from the lab's, so the numbers differ too. The picture is the same: the best epoch comes early (between 13 and 23), and training to 100 epochs lowers the test accuracy in all 10 runs, on average from 87.2% to 76.4%.

For practitioners
  • In Keras: EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True). Without restore_best_weights=True the weights of the last epoch are kept, not those of the best one.
  • In PyTorch you write the loop yourself: after every epoch compute the validation loss and save a copy of the weights whenever it is the lowest.
  • In scikit-learn: MLPClassifier(early_stopping=True, n_iter_no_change=...) sets aside part of the training data for validation; it watches the validation accuracy, not the loss. In gradient boosting the same idea is called n_iter_no_change or early_stopping_rounds.
  • The number of epochs chosen on validation is a hyperparameter too: the validation score at the best epoch is slightly inflated (lesson 3.5), so do the final evaluation on a held-out test.

Go deeper in the Atlas

Encyclopedia entries on the ideas in this lesson: