From db1028c73ed770fdd6eac777030aaf725fa7f5fc Mon Sep 17 00:00:00 2001 From: RAHUL KUMAR Date: Thu, 5 Mar 2026 15:49:17 +0530 Subject: [PATCH 1/2] Fix EarlyStopping and ReduceLROnPlateau not resetting best metric between runs When reusing an EarlyStopping callback across multiple model.fit() calls (e.g., in a for loop training different models), self.best was not reset in on_train_begin(). This caused subsequent runs to compare against the best metric from a previous run, triggering premature early stopping. Reset self.best to None in EarlyStopping.on_train_begin() and ReduceLROnPlateau._reset() so the first epoch of each new training run is always accepted as an improvement, matching the behavior of a fresh callback instance. Fixes https://github.com/keras-team/keras/issues/20256 --- keras/src/callbacks/early_stopping.py | 1 + keras/src/callbacks/early_stopping_test.py | 27 +++++++++++++++++++++ keras/src/callbacks/reduce_lr_on_plateau.py | 1 + 3 files changed, 29 insertions(+) diff --git a/keras/src/callbacks/early_stopping.py b/keras/src/callbacks/early_stopping.py index 30fef26b8d9e..0e13613164b2 100644 --- a/keras/src/callbacks/early_stopping.py +++ b/keras/src/callbacks/early_stopping.py @@ -88,6 +88,7 @@ def on_train_begin(self, logs=None): # Allow instances to be re-used self.wait = 0 self.stopped_epoch = 0 + self.best = None self.best_weights = None self.best_epoch = 0 diff --git a/keras/src/callbacks/early_stopping_test.py b/keras/src/callbacks/early_stopping_test.py index d4b127675e7b..7a6853e0ad5b 100644 --- a/keras/src/callbacks/early_stopping_test.py +++ b/keras/src/callbacks/early_stopping_test.py @@ -126,6 +126,33 @@ def test_early_stopping_reuse(self): ) self.assertGreaterEqual(len(history2.epoch), patience) + @pytest.mark.requires_trainable_backend + def test_early_stopping_reuse_across_models(self): + # Regression test for https://github.com/keras-team/keras/issues/20256 + # When reusing EarlyStopping across multiple model.fit() calls with + # different models, self.best must be reset so that the new model + # isn't compared against a stale best value from a previous run. + patience = 5 + data = np.random.random((100, 1)) + labels = np.where(data > 0.5, 1, 0) + + stopper = callbacks.EarlyStopping( + monitor="loss", patience=patience + ) + + for _ in range(3): + model = models.Sequential( + [ + layers.Dense(10, activation="relu"), + layers.Dense(1, activation="sigmoid"), + ] + ) + model.compile(optimizer="sgd", loss="binary_crossentropy") + history = model.fit( + data, labels, callbacks=[stopper], verbose=0, epochs=50 + ) + self.assertGreater(len(history.epoch), patience) + @pytest.mark.requires_trainable_backend def test_early_stopping_with_baseline(self): baseline = 0.6 diff --git a/keras/src/callbacks/reduce_lr_on_plateau.py b/keras/src/callbacks/reduce_lr_on_plateau.py index b9c40afc4e92..391f4d3902ac 100644 --- a/keras/src/callbacks/reduce_lr_on_plateau.py +++ b/keras/src/callbacks/reduce_lr_on_plateau.py @@ -74,6 +74,7 @@ def __init__( def _reset(self): """Resets wait counter and cooldown counter.""" + self.best = None self.cooldown_counter = 0 self.wait = 0 From 4f614c122ceb80c1d32d9303ae182a6f3f13667c Mon Sep 17 00:00:00 2001 From: RAHUL KUMAR Date: Thu, 5 Mar 2026 17:25:33 +0530 Subject: [PATCH 2/2] style: run pre-commit hooks (ruff formatting) --- keras/src/callbacks/early_stopping_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keras/src/callbacks/early_stopping_test.py b/keras/src/callbacks/early_stopping_test.py index 7a6853e0ad5b..5054567bf345 100644 --- a/keras/src/callbacks/early_stopping_test.py +++ b/keras/src/callbacks/early_stopping_test.py @@ -136,9 +136,7 @@ def test_early_stopping_reuse_across_models(self): data = np.random.random((100, 1)) labels = np.where(data > 0.5, 1, 0) - stopper = callbacks.EarlyStopping( - monitor="loss", patience=patience - ) + stopper = callbacks.EarlyStopping(monitor="loss", patience=patience) for _ in range(3): model = models.Sequential(