Early Stopping Strategies
Training loss keeps dropping — validation loss doesn't have to
If you train a sufficiently flexible model long enough, training loss will keep decreasing almost monotonically — the model has enough capacity to fit the training set arbitrarily well, including its noise. Validation loss behaves differently: it typically decreases alongside training loss early on, reaches a minimum, and then starts rising again as the model begins overfitting to training-set-specific patterns that don't generalize. Early stopping is the strategy of halting training at or near that validation minimum, rather than running for a fixed number of epochs regardless of what validation performance is doing.
The basic algorithm
After every epoch (or every N steps), evaluate the model on a held-out validation set and record the metric you care about (validation loss, accuracy, F1, etc.). Keep track of the best score seen so far and save a checkpoint when it improves. Define a patience value — the number of evaluations to wait without improvement before stopping. If patience is exhausted, stop training and restore the best saved checkpoint, not the final one. This last step matters: the final checkpoint is often already past the point of overfitting.
class EarlyStopper:
def __init__(self, patience=5, min_delta=1e-4, mode="min"):
self.patience = patience
self.min_delta = min_delta
self.mode = mode
self.best_score = float("inf") if mode == "min" else -float("inf")
self.counter = 0
self.best_state = None
def step(self, metric, model_state):
improved = (
metric < self.best_score - self.min_delta
if self.mode == "min"
else metric > self.best_score + self.min_delta
)
if improved:
self.best_score = metric
self.counter = 0
self.best_state = model_state # deep-copy in real usage
else:
self.counter += 1
return self.counter >= self.patience # True => stop training
# Usage inside a training loop:
# stopper = EarlyStopper(patience=5)
# for epoch in range(max_epochs):
# train_one_epoch(...)
# val_loss = evaluate(...)
# if stopper.step(val_loss, model.state_dict()):
# model.load_state_dict(stopper.best_state)
# breakChoosing patience and min_delta
Patience too low and you'll stop at the first noisy validation blip, before the model has actually converged — validation metrics fluctuate epoch to epoch even when the true trend is still improving, especially with small validation sets or high learning rates. Patience too high and early stopping barely saves any compute or protection against overfitting. A min_delta threshold (requiring improvement to exceed a small margin, not just any improvement) prevents the counter from resetting on negligible noise-level gains. In practice, patience is usually set relative to how noisy the validation curve looks and how expensive each epoch is — 3–10 epochs is common for small-to-medium models, more for setups with high per-epoch variance like small batch sizes or heavy data augmentation.
Early stopping is an implicit regularizer
| Strategy | What it monitors | Risk if misconfigured |
|---|---|---|
| Fixed epoch budget | Nothing — just a step count | Over/underfits depending on luck in picking the number |
| Early stopping on val loss | Validation loss trend | Stops too early on noisy curves without patience |
| Early stopping on val metric (e.g. F1) | Task-specific metric | Metric can plateau while loss still improves, or vice versa |
| Learning-rate-aware stopping | Val metric + LR schedule | Needs coordination with LR decay to avoid premature stop |
Don't tune hyperparameters using the same validation set you stop on
What's Next?
Early stopping works hand-in-hand with how you split your data into train, validation, and test sets in the first place — get that split wrong (for example, with leakage between sets) and no stopping strategy will save you from a misleading performance estimate.
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.