Skip to content

Fix/20256 early stopping reset#22358

Draft
pctablet505 wants to merge 2 commits intokeras-team:masterfrom
pctablet505:fix/20256-early-stopping-reset
Draft

Fix/20256 early stopping reset#22358
pctablet505 wants to merge 2 commits intokeras-team:masterfrom
pctablet505:fix/20256-early-stopping-reset

Conversation

@pctablet505
Copy link
Collaborator

This pull request addresses issues related to the reuse of callback instances in Keras, specifically ensuring that stateful attributes are properly reset when callbacks like EarlyStopping and ReduceLROnPlateau are used multiple times. It also adds a regression test to prevent future issues with callback reuse across different models.

Callback state reset improvements:

Testing and regression coverage:

Problem

When the same EarlyStopping (or ReduceLROnPlateau) callback instance is reused across multiple model.fit() calls (a common pattern in hyperparameter loops), self.best is never cleared in _reset(). This means the second fit() starts with a best value from the previous run: if the first model had a very low loss, the second model may stop immediately on epoch 1 simply because its loss hasn't beaten a stale threshold from an entirely different model.

Root Cause

EarlyStopping._reset() and ReduceLROnPlateau._reset() reset wait, stopped_epoch, and best_weights, but forgot to reset best to None. The MonitorCallback base initializes best in __init__, but _reset() is what runs at the start of each fit() — so after the first run, best is permanently stale.

Fix

Add self.best = None to both EarlyStopping._reset() and ReduceLROnPlateau._reset(). A None sentinel is already handled by the comparison logic in both callbacks (it forces acceptance of the first epoch's value).

Files Changed

  • keras/src/callbacks/early_stopping.py — add self.best = None in _reset()
  • keras/src/callbacks/reduce_lr_on_plateau.py — add self.best = None in _reset()
  • keras/src/callbacks/early_stopping_test.py — regression test: shared callback reused across 3 models in a loop, each run must last > patience epochs

…ween 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 keras-team#20256
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical issue where Keras callbacks like EarlyStopping and ReduceLROnPlateau retained stale self.best values when reused across multiple training runs. This led to incorrect early stopping or learning rate reduction behavior. The changes ensure that these stateful attributes are properly reset at the beginning of each training session, guaranteeing consistent and expected callback functionality. A new regression test has also been added to prevent recurrence of this issue.

Highlights

  • EarlyStopping Callback Reset: Ensured self.best is reset to None in EarlyStopping.on_train_begin to prevent stale values when reusing the callback across multiple training runs.
  • ReduceLROnPlateau Callback Reset: Ensured self.best is reset to None in ReduceLROnPlateau._reset for correct behavior upon reuse, preventing comparison against stale best values.
  • Regression Test Added: Introduced a new regression test case for EarlyStopping to validate its proper reuse across multiple models, specifically addressing the issue of retained state.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • keras/src/callbacks/early_stopping.py
    • Added self.best = None to the on_train_begin method to ensure proper state reset for the best monitored value.
  • keras/src/callbacks/early_stopping_test.py
    • Introduced test_early_stopping_reuse_across_models to validate the correct behavior of EarlyStopping when reused across different models, specifically checking for state reset.
  • keras/src/callbacks/reduce_lr_on_plateau.py
    • Added self.best = None to the _reset method to clear the best monitored value on callback reuse.
Activity
  • No specific review comments or activities have been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly fixes a state reset issue in EarlyStopping and ReduceLROnPlateau callbacks when they are reused, by ensuring the best monitored value is reset at the start of each training run. The included regression test for EarlyStopping is a valuable addition. I've added a couple of suggestions to improve code consistency and documentation clarity.

# Allow instances to be re-used
self.wait = 0
self.stopped_epoch = 0
self.best = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While this change is correct, for consistency with other callbacks like ReduceLROnPlateau, consider extracting the state-resetting logic from on_train_begin (lines 89-93) into a private _reset() method. This would align with the Keras API design guideline of having similar APIs for objects that perform similar functions, improving code clarity and maintainability.

For example:

def on_train_begin(self, logs=None):
    self._reset()

def _reset(self):
    # Allow instances to be re-used
    self.wait = 0
    self.stopped_epoch = 0
    self.best = None
    self.best_weights = None
    self.best_epoch = 0
References
  1. Objects that do interchangeable things should have identical or very close APIs. Both EarlyStopping and ReduceLROnPlateau are callbacks that monitor metrics and reset state. Using a _reset method in both would improve consistency. (link)

@@ -74,6 +74,7 @@ def __init__(

def _reset(self):
"""Resets wait counter and cooldown counter."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the addition of resetting self.best, the docstring for the _reset method is now incomplete. Please update it to reflect that best is also reset.

Suggested change
"""Resets wait counter and cooldown counter."""
"""Resets wait counter, cooldown counter, and best value."""
References
  1. Docstrings should be kept up-to-date and accurately describe what the code does. The docstring for _reset should be updated to include the fact that self.best is also being reset. (link)

@codecov-commenter
Copy link

codecov-commenter commented Mar 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.95%. Comparing base (95e74a9) to head (4f614c1).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #22358   +/-   ##
=======================================
  Coverage   82.95%   82.95%           
=======================================
  Files         595      595           
  Lines       66040    66042    +2     
  Branches    10305    10305           
=======================================
+ Hits        54785    54787    +2     
  Misses       8639     8639           
  Partials     2616     2616           
Flag Coverage Δ
keras 82.78% <100.00%> (+<0.01%) ⬆️
keras-jax 60.84% <100.00%> (+<0.01%) ⬆️
keras-numpy 55.03% <50.00%> (-0.01%) ⬇️
keras-openvino 49.10% <0.00%> (+<0.01%) ⬆️
keras-tensorflow 62.06% <100.00%> (+<0.01%) ⬆️
keras-torch 60.87% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants