Long-running Keras training can keep updating weights after validation performance has stopped improving. EarlyStopping watches an epoch-level metric from model.fit() and ends the run once the configured wait for improvement is exhausted.
The callback can monitor val_loss when model.fit() receives validation data. mode=“min” treats a lower value as better, while min_delta sets the smallest decrease that resets the patience counter.
The demonstration uses patience=2 and restore_best_weights=True. Training should stop before the requested 20 epochs, and evaluating the restored model should reproduce the best validation loss recorded in the training history.
Related: How to use ModelCheckpoint in Keras
Related: How to create a custom callback in Keras
Related: How to resume Keras training from a checkpoint
Related: How to set the Keras backend
import keras import numpy as np keras.utils.set_random_seed(42) x_train = np.linspace(-1.0, 1.0, 120, dtype="float32").reshape(-1, 1) y_train = (2.0 * x_train) + 0.5 x_val = np.linspace(-0.8, 0.8, 40, dtype="float32").reshape(-1, 1) y_val = (2.0 * x_val) + 0.5
model = keras.Sequential( [ keras.layers.Input(shape=(1,)), keras.layers.Dense(8, activation="relu"), keras.layers.Dense(1), ] ) model.compile( optimizer=keras.optimizers.SGD(learning_rate=0.35), loss="mse", metrics=["mae"], )
early_stop = keras.callbacks.EarlyStopping( monitor="val_loss", mode="min", min_delta=0.0001, patience=2, restore_best_weights=True, verbose=1, )
mode=“max” suits a monitored metric that should increase, such as val_accuracy. start_from_epoch delays monitoring while the model completes warm-up epochs.
history = model.fit( x_train, y_train, validation_data=(x_val, y_val), epochs=20, batch_size=16, callbacks=[early_stop], verbose=0, )
The callbacks argument accepts a list, so EarlyStopping can run beside logging or checkpoint callbacks.
best_epoch = int(np.argmin(history.history["val_loss"])) + 1 best_val_loss = min(history.history["val_loss"]) restored_val_loss, _ = model.evaluate(x_val, y_val, verbose=0) epochs_run = len(history.history["loss"]) if epochs_run >= 20: raise RuntimeError("EarlyStopping did not stop training before epoch 20") if not np.isclose(restored_val_loss, best_val_loss, rtol=1e-5, atol=1e-7): raise RuntimeError("restored weights do not match the best validation loss") print("epochs requested: 20") print(f"epochs run: {epochs_run}") print(f"best epoch: {best_epoch}") print(f"best val_loss: {best_val_loss:.4f}") print(f"restored val_loss: {restored_val_loss:.4f}") print(f"history keys: {sorted(history.history.keys())}")
history.history must contain the monitored key. If val_loss is absent, pass validation data to model.fit() or monitor an epoch metric already present in the training logs.
$ python3 early_stopping_demo.py Epoch 3: early stopping Restoring model weights from the end of the best epoch: 1. epochs requested: 20 epochs run: 3 best epoch: 1 best val_loss: 0.0474 restored val_loss: 0.0474 history keys: ['loss', 'mae', 'val_loss', 'val_mae']
The exact loss values can vary by backend and hardware.
This observation shows the callback ended training after the patience window instead of consuming the full epoch budget.
This observation shows restore_best_weights=True returned the model to the best monitored epoch.
$ python3 early_stopping_demo.py Epoch 3: early stopping Restoring model weights from the end of the best epoch: 1. epochs requested: 20 epochs run: 3 best epoch: 1 best val_loss: 0.0474 restored val_loss: 0.0474 history keys: ['loss', 'mae', 'val_loss', 'val_mae']