Source code for optuna.terminator.median_erroreval
from __future__ import annotations
import sys
import numpy as np
from optuna._experimental import experimental_class
from optuna.study import StudyDirection
from optuna.terminator.erroreval import BaseErrorEvaluator
from optuna.terminator.improvement.evaluator import BaseImprovementEvaluator
from optuna.trial import FrozenTrial
from optuna.trial._state import TrialState
[docs]
@experimental_class("4.0.0")
class MedianErrorEvaluator(BaseErrorEvaluator):
"""An error evaluator that returns the ratio to initial median.
This error evaluator is introduced as a heuristics in the following paper:
- `A stopping criterion for Bayesian optimization by the gap of expected minimum simple
regrets <https://proceedings.mlr.press/v206/ishibashi23a.html>`__
Args:
paired_improvement_evaluator:
The ``improvement_evaluator`` instance which is set with this ``error_evaluator``.
warm_up_trials:
A parameter specifies the number of initial trials to be discarded before
the calculation of median. Default to 10.
In optuna, the first 10 trials are often random sampling.
The ``warm_up_trials`` can exclude them from the calculation.
n_initial_trials:
A parameter specifies the number of initial trials considered in the calculation of
median after `warm_up_trials`. Default to 20.
threshold_ratio:
A parameter specifies the ratio between the threshold and initial median.
Default to 0.01.
"""
def __init__(
self,
paired_improvement_evaluator: BaseImprovementEvaluator,
warm_up_trials: int = 10,
n_initial_trials: int = 20,
threshold_ratio: float = 0.01,
) -> None:
if warm_up_trials < 0:
raise ValueError("`warm_up_trials` is expected to be a non-negative integer.")
if n_initial_trials <= 0:
raise ValueError("`n_initial_trials` is expected to be a positive integer.")
if threshold_ratio <= 0.0 or not np.isfinite(threshold_ratio):
raise ValueError("`threshold_ratio_to_initial_median` is expected to be a positive.")
self._paired_improvement_evaluator = paired_improvement_evaluator
self._warm_up_trials = warm_up_trials
self._n_initial_trials = n_initial_trials
self._threshold_ratio = threshold_ratio
self._threshold: float | None = None
def evaluate(
self,
trials: list[FrozenTrial],
study_direction: StudyDirection,
) -> float:
if self._threshold is not None:
return self._threshold
trials = [trial for trial in trials if trial.state == TrialState.COMPLETE]
if len(trials) < (self._warm_up_trials + self._n_initial_trials):
return (
-sys.float_info.min
) # Do not terminate. It assumes that improvement must non-negative.
trials.sort(key=lambda trial: trial.number)
criteria = []
for i in range(1, self._n_initial_trials + 1):
criteria.append(
self._paired_improvement_evaluator.evaluate(
trials[self._warm_up_trials : self._warm_up_trials + i], study_direction
)
)
criteria.sort()
self._threshold = criteria[len(criteria) // 2]
assert self._threshold is not None
self._threshold = min(sys.float_info.max, self._threshold * self._threshold_ratio)
return self._threshold