Source code for deephyper.stopper._lcmodel_stopper

from numbers import Number

import numpy as np

from deephyper.stopper._stopper import Stopper
from deephyper.stopper.lce import BayesianLearningCurveRegressor


def area_learning_curve(z, f, z_max) -> float:
    assert len(z) == len(f)
    assert z[-1] <= z_max
    area = 0
    for i in range(1, len(z)):
        # z: is always monotinic increasing but not f!
        area += (z[i] - z[i - 1]) * f[i - 1]
    if z[-1] < z_max:
        area += (z_max - z[-1]) * f[-1]
    return area


[docs] class LCModelStopper(Stopper): """Stopper based on learning curve extrapolation (LCE). This is to evaluate if the iterations of the learning algorithm should be stopped. .. list-table:: :widths: 25 25 25 :header-rows: 1 * - Single-Objective - Multi-Objectives - Failures * - ✅ - ❌ - ❌ The LCE is based on a parametric learning curve model (LCM) which is modeling the score as a function of the number of training steps. Training steps can correspond to the number of training epochs, the number of training batches, the number of observed samples or any other quantity that is iterated through during the training process. The LCE is based on the following steps: 1. An early stopping condition is always checked first. If the early stopping condition is met, the LCE is not applied. 2. Then, some safeguard conditions are checked to ensure that the LCE can be applied (number of observed steps must be greater or equal to the number of parameters of the LCM). 3. If the LCM cannot be fitted (number of observed steps is less than number of parameters of the model), then the last observed step is compared to hitorical performance of others at the same step to check if it is a low-performing outlier (outlier in the direction of performing worse!) using the IQR criterion. 4. If the LCM can be fitted, a least square fit is performed to estimate the parameters of the LCM. 5. The probability of the current LC to perform worse than the best observed score at the maximum iteration is computed using Monte-Carlo Markov Chain (MCMC). To use this stopper, you need to install the following dependencies: .. code-block:: bash $ jax>=0.3.25 $ numpyro Args: max_steps (int): The maximum number of training steps which can be performed. min_steps (int, optional): The minimum number of training steps which can be performed. Defaults to ``4``. It is better to have at least as many steps as the number of parameters of the fitted learning curve model. For example, if ``lc_model="mmf4"`` then ``min_steps`` should be at least ``4``. lc_model (str, optional): The parameteric learning model to use. It should be a string in the following list: `` ["lin2", "loglin2", "loglin3", "loglin4", "pow3","mmf4", "vapor3", "logloglin2", "hill3", "logpow3", "pow4", "exp4", "janoschek4", "weibull4", "ilog2"]``. The number in the name corresponds to the number of parameters of the parametric model. Defaults to ``"mmf4"``. min_obs_to_fit_lc_model (int, optional): The minimum number of observed scores to fit the learning curve model. Defaults to ``4`` because the ``"mmf4"`` model has 4 parameters. min_done_for_outlier_detection (int, optional): The minimum number of observed scores at the same step to check for if it is a lower-bound outlier. Defaults to ``10``. iqr_factor_for_outlier_detection (float, optional): The IQR factor for outlier detection. The higher it is the more inclusive the condition will be (i.e. if set very large it is likely not going to detect any outliers). Defaults to ``1.5``. prob_promotion (float, optional): The threshold probabily to stop the iterations. If the current learning curve has a probability greater than ``prob_promotion`` to be worse that the best observed score accross all evaluations then the current iterations are stopped. Defaults to ``0.9`` (i.e. probability of 0.9 of being worse). early_stopping_patience (float, optional): The patience of the early stopping condition. If it is an ``int`` it is directly corresponding to a number of iterations. If it is a ``float`` then it is corresponding to a proportion between [0,1] w.r.t. ``max_steps``. Defaults to ``0.25`` (i.e. 25% of ``max_steps``). reduction_factor (int, optional): The reduction factor of the number of steps to wait before stopping the evaluation. Defaults to ``1`` to extrapolate the learning curve at every step. objective_returned (str, optional): The returned objective. It can be a value in `` ["last", "max", "alc"]`` where ``"last"`` corresponds to the last observed score, ``"max"`` corresponds to the maximum observed score and ``"alc"`` corresponds to the area under the learning curve. Defaults to "last". random_state (int or np.RandomState, optional): The random state of estimation process. Defaults to ``None``. Raises: ValueError: parameters are not valid. """ def __init__( self, max_steps: int, min_steps: int = 1, lc_model="mmf4", min_obs_to_fit_lc_model=4, min_done_for_outlier_detection=10, iqr_factor_for_outlier_detection=1.5, prob_promotion=0.9, early_stopping_patience=0.25, reduction_factor=1, objective_returned="last", random_state=None, ) -> None: super().__init__(max_steps=max_steps) self.min_steps = min_steps self._f_model = BayesianLearningCurveRegressor.get_parametrics_model_func(lc_model) self._f_model_nparams = int(lc_model[-1]) self._min_obs_to_fit_lc_model = min_obs_to_fit_lc_model self._reduction_factor = reduction_factor self.min_done_for_outlier_detection = min_done_for_outlier_detection self.iqr_factor_for_outlier_detection = iqr_factor_for_outlier_detection self.prob_promotion = prob_promotion if type(early_stopping_patience) is int: self.early_stopping_patience = early_stopping_patience elif type(early_stopping_patience) is float: self.early_stopping_patience = int(early_stopping_patience * self.max_steps) else: raise ValueError("early_stopping_patience must be int or float") self.objective_returned = objective_returned self._rung = 0 self._random_state = random_state self._batch_size = 100 self.lc_model = None self._lc_objectives = [] def _refresh_lc_model(self): batch_has_increased = False if self._batch_size < len(self.observed_budgets): self._batch_size += 100 batch_has_increased = True if self.lc_model is None or batch_has_increased: self.lc_model = BayesianLearningCurveRegressor( f_model=self._f_model, f_model_nparams=self._f_model_nparams, random_state=self._random_state, batch_size=self._batch_size, ) def _compute_halting_step(self): return (self.min_steps - 1) + self._reduction_factor**self._rung def _retrieve_best_objective(self) -> float: search_id, _ = self.job.id.split(".") objectives = [] for obj in self.job.storage.load_out_from_all_jobs(search_id): if isinstance(obj, Number): objectives.append(obj) if len(objectives) > 0: return np.max(objectives) else: return np.max(self.observations[1]) def _get_competiting_objectives(self, rung) -> list: search_id, _ = self.job.id.split(".") values = self.job.storage.load_metadata_from_all_jobs(search_id, f"_completed_rung_{rung}") # Filter out non numerical values (e.g., "F" for failed jobs) values = [v for v in values if isinstance(v, Number)] return values
[docs] def observe(self, budget: float, objective: float): super().observe(budget, objective) self._budget = self.observed_budgets[-1] self._lc_objectives.append(self.objective) self._objective = self._lc_objectives[-1] # For Early-Stopping based on Patience if ( not (hasattr(self, "_local_best_objective")) or self._objective > self._local_best_objective ): self._local_best_objective = self._objective self._local_best_step = self.step halting_step = self._compute_halting_step() if self._budget >= halting_step: self.job.storage.store_job_metadata( self.job.id, f"_completed_rung_{self._rung}", self._objective )
[docs] def stop(self) -> bool: # Enforce Pre-conditions Before Learning-Curve based Early Discarding if super().stop(): self.infos_stopped = "max steps reached" return True if self.step - self._local_best_step >= self.early_stopping_patience: self.infos_stopped = "early stopping" return True # This condition will enforce the stopper to stop the evaluation at the first step # for the first evaluation (The FABOLAS method does the same, bias the first samples with # small budgets) self.best_objective = self._retrieve_best_objective() halting_step = self._compute_halting_step() if self.step < self.min_steps: if self.step >= halting_step: self._rung += 1 return False if self.step < self._min_obs_to_fit_lc_model: if self.step >= halting_step: competing_objectives = self._get_competiting_objectives(self._rung) if len(competing_objectives) > self.min_done_for_outlier_detection: q1 = np.quantile( competing_objectives, q=0.25, ) q3 = np.quantile( competing_objectives, q=0.75, ) iqr = q3 - q1 # lower than the minimum of a box plot if self._objective < q1 - self.iqr_factor_for_outlier_detection * iqr: self.infos_stopped = "outlier" return True self._rung += 1 return False # Check if the halting budget condition is met if self.step < halting_step: return False # Check if the evaluation should be stopped based on LC-Model # Fit and predict the performance of the learning curve model self._refresh_lc_model() z_train = self.observed_budgets y_train = self._lc_objectives z_train, y_train = np.asarray(z_train), np.asarray(y_train) self.lc_model.fit(z_train, y_train, update_prior=True) # Check if the configuration is promotable based on its predicted objective value p = self.lc_model.prob( X=[self.max_steps], condition=lambda y_hat: y_hat <= self.best_objective )[0] # Return whether the configuration should be stopped if p <= self.prob_promotion: self._rung += 1 else: self.infos_stopped = f"prob={p:.3f}" return True
@property def objective(self): if self.objective_returned == "last": return self.observations[-1][-1] elif self.objective_returned == "max": return max(self.observations[-1]) elif self.objective_returned == "alc": z, y = self.observations return area_learning_curve(z, y, z_max=self.max_steps) else: raise ValueError("objective_returned must be one of 'last', 'best', 'alc'")