From 2e2d3a276cfe8857739e6991a02d0634ffe0104f Mon Sep 17 00:00:00 2001 From: Jeremy Rapin Date: Wed, 17 Feb 2021 19:41:48 +0100 Subject: [PATCH 1/2] Add a simple early stopping mechanism --- nevergrad/common/errors.py | 4 ++++ nevergrad/optimization/base.py | 9 +++++++-- nevergrad/optimization/callbacks.py | 14 ++++++++++++++ nevergrad/optimization/test_callbacks.py | 11 +++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/nevergrad/common/errors.py b/nevergrad/common/errors.py index 1741046ee..02cbbf441 100644 --- a/nevergrad/common/errors.py +++ b/nevergrad/common/errors.py @@ -20,6 +20,10 @@ class NevergradWarning(Warning): # errors +class NevergradEarlyStopping(StopIteration, NevergradError): + """Stops the minimization loop if raised""" + + class NevergradRuntimeError(RuntimeError, NevergradError): """Runtime error raised by Nevergrad""" diff --git a/nevergrad/optimization/base.py b/nevergrad/optimization/base.py index 057880088..f56fb95a4 100644 --- a/nevergrad/optimization/base.py +++ b/nevergrad/optimization/base.py @@ -599,13 +599,18 @@ def minimize( if verbosity and new_sugg: print(f"Launching {new_sugg} jobs with new suggestions") for _ in range(new_sugg): - args = self.ask() + try: + args = self.ask() + except errors.NevergradEarlyStopping: + remaining_budget = 0 + break self._running_jobs.append( (args, executor.submit(objective_function, *args.args, **args.kwargs)) ) if new_sugg: sleeper.start_timer() - remaining_budget = self.budget - self.num_ask + if remaining_budget > 0: # early stopping sets it to 0 + remaining_budget = self.budget - self.num_ask # split (repopulate finished and runnings in only one loop to avoid # weird effects if job finishes in between two list comprehensions) tmp_runnings, tmp_finished = [], deque() diff --git a/nevergrad/optimization/callbacks.py b/nevergrad/optimization/callbacks.py index 9a1a2ad13..1b379de8a 100644 --- a/nevergrad/optimization/callbacks.py +++ b/nevergrad/optimization/callbacks.py @@ -11,6 +11,7 @@ from pathlib import Path import numpy as np import nevergrad.common.typing as tp +from nevergrad.common import errors from nevergrad.parametrization import parameter as p from nevergrad.parametrization import helpers from . import base @@ -246,3 +247,16 @@ def __getstate__(self) -> tp.Dict[str, tp.Any]: state = dict(self.__dict__) state["_progress_bar"] = None return state + + +class EarlyStopping: + """Register on ask""" + + def __init__(self, stopping_criterion: tp.Callable[[base.Optimizer], bool]) -> None: + self.stopping_criterion = stopping_criterion + + def __call__(self, optimizer: base.Optimizer, *args: tp.Any, **kwargs: tp.Any) -> None: + if args or kwargs: + raise errors.NevergradRuntimeError("EarlyStopping must be registered on ask method") + if self.stopping_criterion(optimizer): + raise errors.NevergradEarlyStopping("Early stopping criterion is reached") diff --git a/nevergrad/optimization/test_callbacks.py b/nevergrad/optimization/test_callbacks.py index b10647a66..33f1b0ab6 100644 --- a/nevergrad/optimization/test_callbacks.py +++ b/nevergrad/optimization/test_callbacks.py @@ -81,3 +81,14 @@ def test_progressbar_dump(tmp_path: Path) -> None: for _ in range(12): cand = optimizer.ask() optimizer.tell(cand, 0) + + +def test_early_stopping() -> None: + instrum = ng.p.Instrumentation( + None, 2.0, blublu="blublu", array=ng.p.Array(shape=(3, 2)), multiobjective=True + ) + optimizer = optimizerlib.OnePlusOne(parametrization=instrum, budget=100) + early_stopping = ng.callbacks.EarlyStopping(lambda opt: opt.num_ask > 3) + optimizer.register_callback("ask", early_stopping) + optimizer.minimize(_func, verbosity=2) + assert optimizer.num_ask == 4 From 077c20631c5ded9e199ffa1bd4f8aa2b85b912fc Mon Sep 17 00:00:00 2001 From: Jeremy Rapin Date: Wed, 17 Feb 2021 19:55:27 +0100 Subject: [PATCH 2/2] Doc --- CHANGELOG.md | 2 ++ docs/optimizers_ref.rst | 2 +- docs/parametrization_ref.rst | 1 + nevergrad/optimization/callbacks.py | 22 +++++++++++++++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c495f83a8..d95abc5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - the new `nevergrad.errors` module gathers errors and warnings used throughout the package (WIP) [#1031](https://github.com/facebookresearch/nevergrad/pull/1031). - `EvolutionStrategy` now defaults to NSGA2 selection in the multiobjective case +- A new experimental callback adds an early stopping mechanism + [#1054](https://github.com/facebookresearch/nevergrad/pull/1054). ## 0.4.3 (2021-01-28) diff --git a/docs/optimizers_ref.rst b/docs/optimizers_ref.rst index 56b942e94..621a4ca06 100644 --- a/docs/optimizers_ref.rst +++ b/docs/optimizers_ref.rst @@ -20,7 +20,7 @@ Callbacks can be registered through the :code:`optimizer.register_callback` for `ng.callbacks` namespace. .. automodule:: nevergrad.callbacks - :members: OptimizerDump, ParametersLogger, ProgressBar + :members: OptimizerDump, ParametersLogger, ProgressBar, EarlyStopping Configurable optimizers ----------------------- diff --git a/docs/parametrization_ref.rst b/docs/parametrization_ref.rst index ce75b1795..83edf86be 100644 --- a/docs/parametrization_ref.rst +++ b/docs/parametrization_ref.rst @@ -38,3 +38,4 @@ Parameter API .. autoclass:: nevergrad.p.Parameter :members: + :inherited-members: diff --git a/nevergrad/optimization/callbacks.py b/nevergrad/optimization/callbacks.py index 1b379de8a..9c15ac7ec 100644 --- a/nevergrad/optimization/callbacks.py +++ b/nevergrad/optimization/callbacks.py @@ -250,7 +250,27 @@ def __getstate__(self) -> tp.Dict[str, tp.Any]: class EarlyStopping: - """Register on ask""" + """Callback for stopping the :code:`minimize` method before the budget is + fully used. + + Parameters + ---------- + stopping_criterion: func(optimizer) -> bool + function that takes the current optimizer as input and returns True + if the minimization must be stopped + + Note + ---- + This callback must be register on the "ask" method only. + + Example + ------- + In the following code, the :code:`minimize` method will be stopped at the 4th "ask" + + >>> early_stopping = ng.callbacks.EarlyStopping(lambda opt: opt.num_ask > 3) + >>> optimizer.register_callback("ask", early_stopping) + >>> optimizer.minimize(_func, verbosity=2) + """ def __init__(self, stopping_criterion: tp.Callable[[base.Optimizer], bool]) -> None: self.stopping_criterion = stopping_criterion