Skip to content

Commit

Permalink
Merge branch 'optuna:master' into terminator-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Alnusjaponica committed May 18, 2023
2 parents 3e4ce91 + cde5850 commit 1240cd2
Show file tree
Hide file tree
Showing 17 changed files with 337 additions and 210 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Examples can be found in [optuna/optuna-examples](https://github.com/optuna/optu

## Integrations

[Integrations modules](https://optuna.readthedocs.io/en/stable/tutorial/10_key_features/003_efficient_optimization_algorithms.html#integration-modules-for-pruning), which allow pruning, or early stopping, of unpromising trials are available for the following libraries:
[Integrations modules](https://optuna-integration.readthedocs.io/en/stable/index.html), which allow pruning, or early stopping, of unpromising trials are available for the following libraries:

* [AllenNLP](https://github.com/optuna/optuna-examples/tree/main/allennlp)
* [Catalyst](https://github.com/optuna/optuna-examples/tree/main/pytorch/catalyst_simple.py)
Expand Down
5 changes: 5 additions & 0 deletions docs/source/reference/integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ optuna.integration

The :mod:`~optuna.integration` module contains classes used to integrate Optuna with external machine learning frameworks.

.. note::
Optuna's integration modules for third-party libraries have started migrating from Optuna itself to a package called
`optuna-integration`. Please check the `repository <https://github.com/optuna/optuna-integration>`_ and
the `documentation <https://optuna-integration.readthedocs.io/en/latest/index.html>`_.

For most of the ML frameworks supported by Optuna, the corresponding Optuna integration class serves only to implement a callback object and functions, compliant with the framework's specific callback API, to be called with each intermediate step in the model training. The functionality implemented in these callbacks across the different ML frameworks includes:

(1) Reporting intermediate model scores back to the Optuna trial using :func:`optuna.trial.Trial.report`,
Expand Down
170 changes: 137 additions & 33 deletions optuna/samplers/_cmaes.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@


class _CmaEsAttrKeys(NamedTuple):
optimizer: str
n_restarts: str
optimizer: Callable[[int], str]
generation: Callable[[int], str]
popsize: Callable[[], str]
n_restarts: Callable[[], str]
n_restarts_with_large: str
poptype: str
small_n_eval: str
large_n_eval: str


class CmaEsSampler(BaseSampler):
Expand Down Expand Up @@ -93,6 +98,9 @@ def objective(trial):
size. In Proceedings of the IEEE Congress on Evolutionary Computation (CEC 2005),
pages 1769–1776. IEEE Press, 2005.
<http://www.cmap.polytechnique.fr/~nikolaus.hansen/cec2005ipopcmaes.pdf>`_
- `N. Hansen. Benchmarking a BI-Population CMA-ES on the BBOB-2009 Function Testbed.
GECCO Workshop, 2009.
<https://dl.acm.org/doi/10.1145/1570256.1570333>`_
- `Raymond Ros, Nikolaus Hansen. A Simple Modification in CMA-ES Achieving Linear Time and
Space Complexity. 10th International Conference on Parallel Problem Solving From Nature,
Sep 2008, Dortmund, Germany. inria-00287367.
Expand Down Expand Up @@ -152,8 +160,10 @@ def objective(trial):
restart_strategy:
Strategy for restarting CMA-ES optimization when converges to a local minimum.
If given :obj:`None`, CMA-ES will not restart (default).
If given 'ipop', CMA-ES will restart with increasing population size.
If :obj:`None` is given, CMA-ES will not restart (default).
If 'ipop' is given, CMA-ES will restart with increasing population size.
if 'bipop' is given, CMA-ES will restart with the population size
increased or decreased.
Please see also ``inc_popsize`` parameter.
.. note::
Expand All @@ -162,12 +172,14 @@ def objective(trial):
https://github.com/optuna/optuna/releases/tag/v2.1.0.
popsize:
A population size of CMA-ES. When set ``restart_strategy = 'ipop'``, this is used
as the initial population size.
A population size of CMA-ES. When ``restart_strategy = 'ipop'``
or ``restart_strategy = 'bipop'`` is specified,
this is used as the initial population size.
inc_popsize:
Multiplier for increasing population size before each restart.
This argument will be used when setting ``restart_strategy = 'ipop'``.
This argument will be used when ``restart_strategy = 'ipop'``
or ``restart_strategy = 'bipop'`` is specified.
consider_pruned_trials:
If this is :obj:`True`, the PRUNED trials are considered for sampling.
Expand Down Expand Up @@ -246,7 +258,7 @@ def __init__(
self._search_space = IntersectionSearchSpace()
self._consider_pruned_trials = consider_pruned_trials
self._restart_strategy = restart_strategy
self._popsize = popsize
self._initial_popsize = popsize
self._inc_popsize = inc_popsize
self._use_separable_cma = use_separable_cma
self._with_margin = with_margin
Expand Down Expand Up @@ -299,15 +311,14 @@ def __init__(
"It is prohibited to pass `source_trials` argument when using separable CMA-ES."
)

# TODO(c-bata): Support BIPOP-CMA-ES.
if restart_strategy not in (
"ipop",
"bipop",
None,
):
raise ValueError(
"restart_strategy={} is unsupported. Please specify: 'ipop' or None.".format(
restart_strategy
)
"restart_strategy={} is unsupported. "
"Please specify: 'ipop', 'bipop', or None.".format(restart_strategy)
)

# TODO(knshnb): Support sep-CMA-ES with margin.
Expand Down Expand Up @@ -369,10 +380,38 @@ def sample_relative(
search_space, transform_step=not self._with_margin, transform_0_1=True
)

optimizer, n_restarts = self._restore_optimizer(completed_trials)
if self._initial_popsize is None:
self._initial_popsize = 4 + math.floor(3 * math.log(len(trans.bounds)))

popsize: int = self._initial_popsize
n_restarts: int = 0
n_restarts_with_large: int = 0
poptype: str = "small"
small_n_eval: int = 0
large_n_eval: int = 0
if len(completed_trials) != 0:
latest_trial = completed_trials[-1]

popsize_attr_key = self._attr_keys.popsize()
if popsize_attr_key in latest_trial.system_attrs:
popsize = latest_trial.system_attrs[popsize_attr_key]
else:
popsize = self._initial_popsize

n_restarts_attr_key = self._attr_keys.n_restarts()
n_restarts = latest_trial.system_attrs.get(n_restarts_attr_key, 0)
n_restarts_with_large = latest_trial.system_attrs.get(
self._attr_keys.n_restarts_with_large, 0
)
poptype = latest_trial.system_attrs.get(self._attr_keys.poptype, "small")
small_n_eval = latest_trial.system_attrs.get(self._attr_keys.small_n_eval, 0)
large_n_eval = latest_trial.system_attrs.get(self._attr_keys.large_n_eval, 0)

optimizer = self._restore_optimizer(completed_trials, n_restarts)
if optimizer is None:
n_restarts = 0
optimizer = self._init_optimizer(trans, study.direction, population_size=self._popsize)
optimizer = self._init_optimizer(
trans, study.direction, population_size=self._initial_popsize
)

if optimizer.dim != len(trans.bounds):
if self._warn_independent_sampling:
Expand All @@ -391,9 +430,9 @@ def sample_relative(
completed_trials, optimizer.generation, n_restarts
)

if len(solution_trials) >= optimizer.population_size:
if len(solution_trials) >= popsize:
solutions: List[Tuple[np.ndarray, float]] = []
for t in solution_trials[: optimizer.population_size]:
for t in solution_trials[:popsize]:
assert t.value is not None, "completed trials must have a value"
if isinstance(optimizer, cmaes.CMAwM):
x = np.array(t.system_attrs["x_for_tell"])
Expand All @@ -406,14 +445,39 @@ def sample_relative(

if self._restart_strategy == "ipop" and optimizer.should_stop():
n_restarts += 1
popsize = optimizer.population_size * self._inc_popsize
popsize = popsize * self._inc_popsize
optimizer = self._init_optimizer(
trans, study.direction, population_size=popsize, randomize_start_point=True
)

if self._restart_strategy == "bipop" and optimizer.should_stop():
n_restarts += 1

n_eval = popsize * optimizer.generation
if poptype == "small":
small_n_eval += n_eval
else: # poptype == "large"
large_n_eval += n_eval

if small_n_eval < large_n_eval:
poptype = "small"
popsize_multiplier = self._inc_popsize**n_restarts_with_large
popsize = math.floor(
self._initial_popsize
* popsize_multiplier ** (self._cma_rng.uniform() ** 2)
)
else:
poptype = "large"
n_restarts_with_large += 1
popsize = self._initial_popsize * (self._inc_popsize**n_restarts_with_large)

optimizer = self._init_optimizer(
trans, study.direction, population_size=popsize, randomize_start_point=True
)

# Store optimizer.
optimizer_str = pickle.dumps(optimizer).hex()
optimizer_attrs = self._split_optimizer_str(optimizer_str)
optimizer_attrs = self._split_optimizer_str(optimizer_str, n_restarts)
for key in optimizer_attrs:
study._storage.set_trial_system_attr(trial._trial_id, key, optimizer_attrs[key])

Expand All @@ -432,8 +496,19 @@ def sample_relative(
study._storage.set_trial_system_attr(
trial._trial_id, generation_attr_key, optimizer.generation
)
popsize_attr_key = self._attr_keys.popsize()
study._storage.set_trial_system_attr(trial._trial_id, popsize_attr_key, popsize)
n_restarts_attr_key = self._attr_keys.n_restarts()
study._storage.set_trial_system_attr(trial._trial_id, n_restarts_attr_key, n_restarts)
study._storage.set_trial_system_attr(
trial._trial_id, self._attr_keys.n_restarts, n_restarts
trial._trial_id, self._attr_keys.n_restarts_with_large, n_restarts_with_large
)
study._storage.set_trial_system_attr(trial._trial_id, self._attr_keys.poptype, poptype)
study._storage.set_trial_system_attr(
trial._trial_id, self._attr_keys.small_n_eval, small_n_eval
)
study._storage.set_trial_system_attr(
trial._trial_id, self._attr_keys.large_n_eval, large_n_eval
)

external_values = trans.untransform(params)
Expand All @@ -449,51 +524,80 @@ def _attr_keys(self) -> _CmaEsAttrKeys:
else:
attr_prefix = "cma:"

def optimizer_key_template(restart: int) -> str:
if self._restart_strategy is None:
return attr_prefix + "optimizer"
else:
return attr_prefix + "{}:restart_{}:optimizer".format(
self._restart_strategy, restart
)

def generation_attr_key_template(restart: int) -> str:
if self._restart_strategy is None:
return attr_prefix + "generation"
else:
return attr_prefix + "restart_{}:generation".format(restart)
return attr_prefix + "{}:restart_{}:generation".format(
self._restart_strategy, restart
)

def popsize_attr_key_template() -> str:
if self._restart_strategy is None:
return attr_prefix + "popsize"
else:
return attr_prefix + "{}:popsize".format(self._restart_strategy)

def n_restarts_attr_key_template() -> str:
if self._restart_strategy is None:
return attr_prefix + "n_restarts"
else:
return attr_prefix + "{}:n_restarts".format(self._restart_strategy)

return _CmaEsAttrKeys(
attr_prefix + "optimizer",
attr_prefix + "n_restarts",
optimizer_key_template,
generation_attr_key_template,
popsize_attr_key_template,
n_restarts_attr_key_template,
attr_prefix + "n_restarts_with_large",
attr_prefix + "poptype",
attr_prefix + "small_n_eval",
attr_prefix + "large_n_eval",
)

def _concat_optimizer_attrs(self, optimizer_attrs: Dict[str, str]) -> str:
def _concat_optimizer_attrs(self, optimizer_attrs: Dict[str, str], n_restarts: int = 0) -> str:
return "".join(
optimizer_attrs["{}:{}".format(self._attr_keys.optimizer, i)]
optimizer_attrs["{}:{}".format(self._attr_keys.optimizer(n_restarts), i)]
for i in range(len(optimizer_attrs))
)

def _split_optimizer_str(self, optimizer_str: str) -> Dict[str, str]:
def _split_optimizer_str(self, optimizer_str: str, n_restarts: int = 0) -> Dict[str, str]:
optimizer_len = len(optimizer_str)
attrs = {}
for i in range(math.ceil(optimizer_len / _SYSTEM_ATTR_MAX_LENGTH)):
start = i * _SYSTEM_ATTR_MAX_LENGTH
end = min((i + 1) * _SYSTEM_ATTR_MAX_LENGTH, optimizer_len)
attrs["{}:{}".format(self._attr_keys.optimizer, i)] = optimizer_str[start:end]
attrs["{}:{}".format(self._attr_keys.optimizer(n_restarts), i)] = optimizer_str[
start:end
]
return attrs

def _restore_optimizer(
self,
completed_trials: "List[optuna.trial.FrozenTrial]",
) -> Tuple[Optional["CmaClass"], int]:
n_restarts: int = 0,
) -> Optional["CmaClass"]:
# Restore a previous CMA object.
for trial in reversed(completed_trials):
optimizer_attrs = {
key: value
for key, value in trial.system_attrs.items()
if key.startswith(self._attr_keys.optimizer)
if key.startswith(self._attr_keys.optimizer(n_restarts))
}
if len(optimizer_attrs) == 0:
continue

optimizer_str = self._concat_optimizer_attrs(optimizer_attrs)
n_restarts: int = trial.system_attrs.get(self._attr_keys.n_restarts, 0)
return pickle.loads(bytes.fromhex(optimizer_str)), n_restarts
return None, 0
optimizer_str = self._concat_optimizer_attrs(optimizer_attrs, n_restarts)
return pickle.loads(bytes.fromhex(optimizer_str))
return None

def _init_optimizer(
self,
Expand Down
Loading

0 comments on commit 1240cd2

Please sign in to comment.