From eb0e5b89c54433239549711d5d870d87d5cb7979 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 31 Oct 2024 01:35:07 -0700 Subject: [PATCH 01/34] Initial files --- package/samplers/pyribs/LICENSE | 21 +++++++++++++++++++++ package/samplers/pyribs/__init__.py | 3 +++ 2 files changed, 24 insertions(+) create mode 100644 package/samplers/pyribs/LICENSE create mode 100644 package/samplers/pyribs/__init__.py diff --git a/package/samplers/pyribs/LICENSE b/package/samplers/pyribs/LICENSE new file mode 100644 index 00000000..51c2bdfd --- /dev/null +++ b/package/samplers/pyribs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Bryon Tjanaka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/samplers/pyribs/__init__.py b/package/samplers/pyribs/__init__.py new file mode 100644 index 00000000..22f54fe9 --- /dev/null +++ b/package/samplers/pyribs/__init__.py @@ -0,0 +1,3 @@ +from .sampler import PyribsSampler + +__all__ = ["PyribsSampler"] From c0c685bda1a417721319f39b3cf1d032909b7a18 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 31 Oct 2024 15:09:46 -0700 Subject: [PATCH 02/34] Add reqs --- package/samplers/pyribs/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 package/samplers/pyribs/requirements.txt diff --git a/package/samplers/pyribs/requirements.txt b/package/samplers/pyribs/requirements.txt new file mode 100644 index 00000000..4572f714 --- /dev/null +++ b/package/samplers/pyribs/requirements.txt @@ -0,0 +1,3 @@ +optuna +optunahub +ribs From 3de68cf68d22567c07407141b96e757e6278d978 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Wed, 6 Nov 2024 13:47:14 -0800 Subject: [PATCH 03/34] Rename to CmaMaeSampler --- package/samplers/{pyribs => cmamae}/LICENSE | 0 package/samplers/cmamae/__init__.py | 3 +++ package/samplers/{pyribs => cmamae}/requirements.txt | 0 package/samplers/pyribs/__init__.py | 3 --- 4 files changed, 3 insertions(+), 3 deletions(-) rename package/samplers/{pyribs => cmamae}/LICENSE (100%) create mode 100644 package/samplers/cmamae/__init__.py rename package/samplers/{pyribs => cmamae}/requirements.txt (100%) delete mode 100644 package/samplers/pyribs/__init__.py diff --git a/package/samplers/pyribs/LICENSE b/package/samplers/cmamae/LICENSE similarity index 100% rename from package/samplers/pyribs/LICENSE rename to package/samplers/cmamae/LICENSE diff --git a/package/samplers/cmamae/__init__.py b/package/samplers/cmamae/__init__.py new file mode 100644 index 00000000..7df7e3e2 --- /dev/null +++ b/package/samplers/cmamae/__init__.py @@ -0,0 +1,3 @@ +from .sampler import CmaMaeSampler + +__all__ = ["CmaMaeSampler"] diff --git a/package/samplers/pyribs/requirements.txt b/package/samplers/cmamae/requirements.txt similarity index 100% rename from package/samplers/pyribs/requirements.txt rename to package/samplers/cmamae/requirements.txt diff --git a/package/samplers/pyribs/__init__.py b/package/samplers/pyribs/__init__.py deleted file mode 100644 index 22f54fe9..00000000 --- a/package/samplers/pyribs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .sampler import PyribsSampler - -__all__ = ["PyribsSampler"] From 85d7a31c97d7e9b29e8e8c9fa19cc109e2efba1c Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 7 Nov 2024 12:03:56 -0800 Subject: [PATCH 04/34] Start sampler --- package/samplers/cmamae/README.md | 110 +++++++++++++++++++ package/samplers/cmamae/example.py | 37 +++++++ package/samplers/cmamae/sampler.py | 165 +++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 package/samplers/cmamae/README.md create mode 100644 package/samplers/cmamae/example.py create mode 100644 package/samplers/cmamae/sampler.py diff --git a/package/samplers/cmamae/README.md b/package/samplers/cmamae/README.md new file mode 100644 index 00000000..8724563c --- /dev/null +++ b/package/samplers/cmamae/README.md @@ -0,0 +1,110 @@ +--- +author: Bryon Tjanaka +title: Please fill in the title of the feature here. (e.g., Gaussian-Process Expected Improvement Sampler) +description: Please fill in the description of the feature here. (e.g., This sampler searches for each trial based on expected improvement using Gaussian process.) +tags: [Please fill in the list of tags here. (e.g., sampler, visualization, pruner)] +optuna_versions: ['Please fill in the list of versions of Optuna in which you have confirmed the feature works, e.g., 3.6.1.'] +license: MIT License +--- + + + +Please read the [tutorial guide](https://optuna.github.io/optunahub-registry/recipes/001_first.html) to register your feature in OptunaHub. +You can find more detailed explanation of the following contents in the tutorial. +Looking at [other packages' implementations](https://github.com/optuna/optunahub-registry/tree/main/package) will also help you. + +## Abstract + +You can provide an abstract for your package here. +This section will help attract potential users to your package. + +**Example** + +This package provides a sampler based on Gaussian process-based Bayesian optimization. The sampler is highly sample-efficient, so it is suitable for computationally expensive optimization problems with a limited evaluation budget, such as hyperparameter optimization of machine learning algorithms. + +## Class or Function Names + +Please fill in the class/function names which you implement here. + +**Example** + +- GPSampler + +## Installation + +If you have additional dependencies, please fill in the installation guide here. +If no additional dependencies is required, **this section can be removed**. + +**Example** + +```shell +$ pip install scipy torch +``` + +If your package has `requirements.txt`, it will be automatically uploaded to the OptunaHub, and the package dependencies will be available to install as follows. + +```shell + pip install -r https://hub.optuna.org/{category}/{your_package_name}/requirements.txt +``` + +## Example + +Please fill in the code snippet to use the implemented feature here. + +**Example** + +```python +import optuna +import optunahub + + +def objective(trial): + x = trial.suggest_float("x", -5, 5) + return x**2 + + +sampler = optunahub.load_module(package="samplers/gp").GPSampler() +study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=100) +``` + +## Others + +Please fill in any other information if you have here by adding child sections (###). +If there is no additional information, **this section can be removed**. + + diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py new file mode 100644 index 00000000..7e3d45dc --- /dev/null +++ b/package/samplers/cmamae/example.py @@ -0,0 +1,37 @@ +import optuna +import optunahub + +from sampler import CmaMaeSampler + +# TODO: Replace above import with this. +# module = optunahub.load_module("samplers/pyribs") +# PyribsSampler = module.PyribsSampler + + +def objective(trial: optuna.trial.Trial) -> float: + x = trial.suggest_float("x", -10, 10) + y = trial.suggest_float("y", -10, 10) + return -(x**2 + y**2) + 2, x, y + + +if __name__ == "__main__": + sampler = CmaMaeSampler( + param_names=["x", "y"], + archive_dims=[20, 20], + archive_ranges=[(-10, 10), (-10, 10)], + archive_learning_rate=0.1, + archive_threshold_min=-10, + n_emitters=15, + emitter_x0={ + "x": 5, + "y": 5 + }, + emitter_sigma0=0.1, + emitter_batch_size=36, + ) + study = optuna.create_study(sampler=sampler) + study.optimize(objective, n_trials=100) + print(study.best_trial.params) + + fig = optuna.visualization.plot_optimization_history(study) + fig.write_image("cmamae_optimization_history.png") diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py new file mode 100644 index 00000000..f780e5b5 --- /dev/null +++ b/package/samplers/cmamae/sampler.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np +import optunahub +from optuna.distributions import BaseDistribution +from optuna.study import Study +from optuna.trial import FrozenTrial, TrialState +from ribs.archives import GridArchive +from ribs.emitters import EvolutionStrategyEmitter +from ribs.schedulers import Scheduler + +SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler + + +class CmaMaeSampler(SimpleBaseSampler): + """A sampler using CMA-MAE as implemented in pyribs. + + `CMA-MAE `_ is a quality diversity + algorithm that has demonstrated state-of-the-art performance in a variety of + domains. `pyribs `_ is a bare-bones Python library for + quality diversity optimization algorithms. For a primer on CMA-MAE and + pyribs, we recommend referring to the series of `pyribs tutorials + `_. + + For simplicity, this implementation provides a default instantiation of + CMA-MAE with a `GridArchive + `_ and + `EvolutionStrategyEmitter + `_ + with improvement ranking, all wrapped up in a `Scheduler + `_. + + Args: + param_names: List of names of parameters to optimize. + archive_dims: Number of archive cells in each dimension of the measure + space, e.g. ``[20, 30, 40]`` indicates there should be 3 dimensions + with 20, 30, and 40 cells. (The number of dimensions is implicitly + defined in the length of this argument). + archive_ranges: Upper and lower bound of each dimension of the measure + space for the archive, e.g. ``[(-1, 1), (-2, 2)]`` indicates the + first dimension should have bounds :math:`[-1,1]` (inclusive), and + the second dimension should have bounds :math:`[-2,2]` (inclusive). + ``ranges`` should be the same length as ``dims``. + archive_learning_rate: The learning rate for threshold updates in the + archive. + archive_threshold_min: The initial threshold value for all the cells in + the archive. + n_emitters: Number of emitters to use in CMA-MAE. + emitter_x0: Mapping from parameter names to their initial values. + emitter_sigma0: Initial step size / standard deviation of the + distribution from which solutions are sampled in the emitter. + emitter_batch_size: Number of solutions for each emitter to generate on + each iteration. + """ + + def __init__( + self, + *, + param_names: list[str], + archive_dims: list[int], + archive_ranges: list[tuple[float, float]], + archive_learning_rate: float, + archive_threshold_min: float, + n_emitters: int, + emitter_x0: dict[str, float], + emitter_sigma0: float, + emitter_batch_size: int, + ) -> None: + super().__init__() + + self._validate_params(param_names, emitter_x0) + self._param_names = param_names[:] + + emitter_x0_np = self._convert_to_pyribs_params(emitter_x0) + + archive = GridArchive( + solution_dim=len(param_names), + dims=archive_dims, + ranges=archive_ranges, + learning_rate=archive_learning_rate, + threshold_min=archive_threshold_min, + ) + result_archive = GridArchive( + solution_dim=len(param_names), + dims=archive_dims, + ranges=archive_ranges, + ) + emitters = [ + EvolutionStrategyEmitter( + archive, + x0=emitter_x0_np, + sigma0=emitter_sigma0, + ranker="imp", + selection_rule="mu", + restart_rule="basic", + batch_size=emitter_batch_size, + ) for _ in range(n_emitters) + ] + + # Number of solutions generated in each batch from pyribs. + self._batch_size = n_emitters * emitter_batch_size + + self._scheduler = Scheduler( + archive, + emitters, + result_archive=result_archive, + ) + + def _validate_params(self, param_names: list[str], + emitter_x0: dict[str, float]) -> None: + dim = len(param_names) + param_set = set(param_names) + if dim != len(param_set): + raise ValueError( + "Some elements in param_names are duplicated. Please make it a unique list." + ) + + if set(param_names) != emitter_x0.keys(): + raise ValueError( + "emitter_x0 does not contain the parameters listed in param_names. " + "Please provide an initial value for each parameter.") + + def _convert_to_pyribs_params(self, params: dict[str, float]) -> np.ndarray: + np_params = np.empty(len(self._param_names), dtype=float) + for i, p in enumerate(self._param_names): + np_params[i] = params[p] + return np_params + + def _convert_to_optuna_params(self, params: np.ndarray) -> dict[str, float]: + dict_params = {} + for i, p in enumerate(self._param_names): + dict_params[p] = params[i] + return dict_params + + def sample_relative( + self, study: Study, trial: FrozenTrial, + search_space: dict[str, BaseDistribution]) -> dict[str, float]: + # Note: Batch optimization means we need to enqueue trials. + # https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html#optuna.study.Study.enqueue_trial + if trial.number % self._batch_size == 0: + sols = self._scheduler.ask() + for sol in sols: + params = self._convert_to_optuna_params(sol) + study.enqueue_trial(params) + + # Probably, this trial is taken from the queue, so we do not have to take it? + # but I need to look into it. + return trial + + def after_trial( + self, + study: Study, + trial: FrozenTrial, + state: TrialState, + values: Sequence[float] | None, + ) -> None: + # TODO + if trial.number % self._batch_size == self._batch_size - 1: + results = [ + t.values[trial.number - self._batch_size + 1:trial.number + 1] + for t in study.trials + ] + scheduler.tell From 24acc288a1a17af7ee52fe0253558232a189fab5 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Fri, 8 Nov 2024 07:39:29 +0100 Subject: [PATCH 05/34] Add a test example to template --- template/example.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 template/example.py diff --git a/template/example.py b/template/example.py new file mode 100644 index 00000000..4fda5315 --- /dev/null +++ b/template/example.py @@ -0,0 +1,36 @@ +""" +This example is only for sampler. +You can verify your sampler code using this file as well. +Please feel free to remove this file if necessary. +""" + +from __future__ import annotations + +import optuna +import optunahub + + +def objective(trial: optuna.Trial) -> float: + x = trial.suggest_float("x", -5, 5) + y = trial.suggest_float("x", -5, 5) + return x**2 + y**2 + + +# TODO: Change the variables here to test your package. +package_name = "samplers/your_sampler" +repo_owner = "Your GitHub Account Name" +ref = "Your Git Branch Name" +test_local = True + +if test_local: + sampler = optunahub.load_local_module( + package=package_name, registry_root="./package/" + ).YourSampler() +else: + sampler = optunahub.load_module( + package=package_name, repo_owner=repo_owner, ref=ref + ).YourSampler() + +study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=30) +print(study.best_trials) From 6ff8e63b47ff21aa4240e50af83837ba9df084ab Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Fri, 8 Nov 2024 08:47:18 +0100 Subject: [PATCH 06/34] Fix --- template/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/example.py b/template/example.py index 4fda5315..ee95f9e4 100644 --- a/template/example.py +++ b/template/example.py @@ -12,7 +12,7 @@ def objective(trial: optuna.Trial) -> float: x = trial.suggest_float("x", -5, 5) - y = trial.suggest_float("x", -5, 5) + y = trial.suggest_float("y", -5, 5) return x**2 + y**2 From c6b88b7106a95aa7f388e153a1af395dd699ff96 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Fri, 8 Nov 2024 14:09:02 -0800 Subject: [PATCH 07/34] Update sampler --- package/samplers/cmamae/example.py | 25 +++++++++--- package/samplers/cmamae/sampler.py | 62 +++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py index 7e3d45dc..07866703 100644 --- a/package/samplers/cmamae/example.py +++ b/package/samplers/cmamae/example.py @@ -1,5 +1,6 @@ import optuna import optunahub +from optuna.study import StudyDirection from sampler import CmaMaeSampler @@ -21,17 +22,29 @@ def objective(trial: optuna.trial.Trial) -> float: archive_ranges=[(-10, 10), (-10, 10)], archive_learning_rate=0.1, archive_threshold_min=-10, - n_emitters=15, + n_emitters=1, emitter_x0={ "x": 5, "y": 5 }, emitter_sigma0=0.1, - emitter_batch_size=36, + emitter_batch_size=5, + ) + study = optuna.create_study( + sampler=sampler, + directions=[ + # pyribs maximizes objectives. + StudyDirection.MAXIMIZE, + # The remaining values are measures, which do not have an + # optimization direction. + # TODO: Currently, using StudyDirection.NOT_SET is not allowed as + # Optuna assumes we either minimize or maximize. + StudyDirection.MINIMIZE, + StudyDirection.MINIMIZE, + ], ) - study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=100) - print(study.best_trial.params) - fig = optuna.visualization.plot_optimization_history(study) - fig.write_image("cmamae_optimization_history.png") + # TODO: Visualization. + # fig = optuna.visualization.plot_optimization_history(study) + # fig.write_image("cmamae_optimization_history.png") diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index f780e5b5..942fd63c 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -1,10 +1,11 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Iterable import numpy as np import optunahub -from optuna.distributions import BaseDistribution +from optuna.distributions import BaseDistribution, FloatDistribution from optuna.study import Study from optuna.trial import FrozenTrial, TrialState from ribs.archives import GridArchive @@ -68,11 +69,16 @@ def __init__( emitter_sigma0: float, emitter_batch_size: int, ) -> None: - super().__init__() self._validate_params(param_names, emitter_x0) self._param_names = param_names[:] + # NOTE: SimpleBaseSampler must know Optuna search_space information. + search_space = { + name: FloatDistribution(-1e9, 1e9) for name in self._param_names + } + super().__init__(search_space=search_space) + emitter_x0_np = self._convert_to_pyribs_params(emitter_x0) archive = GridArchive( @@ -108,6 +114,8 @@ def __init__( result_archive=result_archive, ) + self._values_to_tell: list[list[float]] = [] + def _validate_params(self, param_names: list[str], emitter_x0: dict[str, float]) -> None: dim = len(param_names) @@ -122,6 +130,11 @@ def _validate_params(self, param_names: list[str], "emitter_x0 does not contain the parameters listed in param_names. " "Please provide an initial value for each parameter.") + def _validate_param_names(self, given_param_names: Iterable[str]) -> None: + if set(self._param_names) != set(given_param_names): + raise ValueError("The given param names must match the param names " + "initially passed to this sampler.") + def _convert_to_pyribs_params(self, params: dict[str, float]) -> np.ndarray: np_params = np.empty(len(self._param_names), dtype=float) for i, p in enumerate(self._param_names): @@ -137,17 +150,16 @@ def _convert_to_optuna_params(self, params: np.ndarray) -> dict[str, float]: def sample_relative( self, study: Study, trial: FrozenTrial, search_space: dict[str, BaseDistribution]) -> dict[str, float]: + self._validate_param_names(search_space.keys()) + # Note: Batch optimization means we need to enqueue trials. - # https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html#optuna.study.Study.enqueue_trial - if trial.number % self._batch_size == 0: - sols = self._scheduler.ask() - for sol in sols: - params = self._convert_to_optuna_params(sol) - study.enqueue_trial(params) + solutions = self._scheduler.ask() + next_params = self._convert_to_optuna_params(solutions[0]) + for solution in solutions[1:]: + params = self._convert_to_optuna_params(solution) + study.enqueue_trial(params) - # Probably, this trial is taken from the queue, so we do not have to take it? - # but I need to look into it. - return trial + return next_params def after_trial( self, @@ -156,10 +168,24 @@ def after_trial( state: TrialState, values: Sequence[float] | None, ) -> None: - # TODO - if trial.number % self._batch_size == self._batch_size - 1: - results = [ - t.values[trial.number - self._batch_size + 1:trial.number + 1] - for t in study.trials - ] - scheduler.tell + # TODO: Is it safe to assume the parameters will always come back in the + # order that they were sent out by the scheduler? Pyribs makes that + # assumption and stores the solutions internally. If not, maybe we can + # retrieve solutions based on their trial ID? + + self._validate_param_names(trial.params.keys()) + + # Store the trial result. + self._values_to_tell.append(values) + + # If we have not retrieved the whole batch of solutions, then we should + # not tell() the results to the scheduler yet. + if len(self._values_to_tell) != self._batch_size: + return + + # Tell the batch results to external sampler once the batch is ready. + values = np.asarray(self._values_to_tell) + self._scheduler.tell(objective=values[:, 0], measures=values[:, 1:]) + + # Empty the results. + self._values_to_tell = [] From e02890d161fcfeb0098f84fa4e8e0102375d2822 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Fri, 8 Nov 2024 14:20:23 -0800 Subject: [PATCH 08/34] pre-commit fixes --- package/samplers/cmamae/__init__.py | 1 + package/samplers/cmamae/example.py | 11 ++++----- package/samplers/cmamae/sampler.py | 37 ++++++++++++++++------------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/package/samplers/cmamae/__init__.py b/package/samplers/cmamae/__init__.py index 7df7e3e2..e724cd42 100644 --- a/package/samplers/cmamae/__init__.py +++ b/package/samplers/cmamae/__init__.py @@ -1,3 +1,4 @@ from .sampler import CmaMaeSampler + __all__ = ["CmaMaeSampler"] diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py index 07866703..c38f8507 100644 --- a/package/samplers/cmamae/example.py +++ b/package/samplers/cmamae/example.py @@ -1,15 +1,15 @@ import optuna -import optunahub from optuna.study import StudyDirection - from sampler import CmaMaeSampler + # TODO: Replace above import with this. # module = optunahub.load_module("samplers/pyribs") # PyribsSampler = module.PyribsSampler -def objective(trial: optuna.trial.Trial) -> float: +def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: + """Returns an objective followed by two measures.""" x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) return -(x**2 + y**2) + 2, x, y @@ -23,10 +23,7 @@ def objective(trial: optuna.trial.Trial) -> float: archive_learning_rate=0.1, archive_threshold_min=-10, n_emitters=1, - emitter_x0={ - "x": 5, - "y": 5 - }, + emitter_x0={"x": 5, "y": 5}, emitter_sigma0=0.1, emitter_batch_size=5, ) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 942fd63c..8c2d206e 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -4,14 +4,17 @@ from typing import Iterable import numpy as np -import optunahub -from optuna.distributions import BaseDistribution, FloatDistribution +from optuna.distributions import BaseDistribution +from optuna.distributions import FloatDistribution from optuna.study import Study -from optuna.trial import FrozenTrial, TrialState +from optuna.trial import FrozenTrial +from optuna.trial import TrialState +import optunahub from ribs.archives import GridArchive from ribs.emitters import EvolutionStrategyEmitter from ribs.schedulers import Scheduler + SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler @@ -69,14 +72,11 @@ def __init__( emitter_sigma0: float, emitter_batch_size: int, ) -> None: - self._validate_params(param_names, emitter_x0) self._param_names = param_names[:] # NOTE: SimpleBaseSampler must know Optuna search_space information. - search_space = { - name: FloatDistribution(-1e9, 1e9) for name in self._param_names - } + search_space = {name: FloatDistribution(-1e9, 1e9) for name in self._param_names} super().__init__(search_space=search_space) emitter_x0_np = self._convert_to_pyribs_params(emitter_x0) @@ -102,7 +102,8 @@ def __init__( selection_rule="mu", restart_rule="basic", batch_size=emitter_batch_size, - ) for _ in range(n_emitters) + ) + for _ in range(n_emitters) ] # Number of solutions generated in each batch from pyribs. @@ -114,10 +115,9 @@ def __init__( result_archive=result_archive, ) - self._values_to_tell: list[list[float]] = [] + self._values_to_tell: list[Sequence[float]] = [] - def _validate_params(self, param_names: list[str], - emitter_x0: dict[str, float]) -> None: + def _validate_params(self, param_names: list[str], emitter_x0: dict[str, float]) -> None: dim = len(param_names) param_set = set(param_names) if dim != len(param_set): @@ -128,12 +128,15 @@ def _validate_params(self, param_names: list[str], if set(param_names) != emitter_x0.keys(): raise ValueError( "emitter_x0 does not contain the parameters listed in param_names. " - "Please provide an initial value for each parameter.") + "Please provide an initial value for each parameter." + ) def _validate_param_names(self, given_param_names: Iterable[str]) -> None: if set(self._param_names) != set(given_param_names): - raise ValueError("The given param names must match the param names " - "initially passed to this sampler.") + raise ValueError( + "The given param names must match the param names " + "initially passed to this sampler." + ) def _convert_to_pyribs_params(self, params: dict[str, float]) -> np.ndarray: np_params = np.empty(len(self._param_names), dtype=float) @@ -148,8 +151,8 @@ def _convert_to_optuna_params(self, params: np.ndarray) -> dict[str, float]: return dict_params def sample_relative( - self, study: Study, trial: FrozenTrial, - search_space: dict[str, BaseDistribution]) -> dict[str, float]: + self, study: Study, trial: FrozenTrial, search_space: dict[str, BaseDistribution] + ) -> dict[str, float]: self._validate_param_names(search_space.keys()) # Note: Batch optimization means we need to enqueue trials. @@ -185,6 +188,8 @@ def after_trial( # Tell the batch results to external sampler once the batch is ready. values = np.asarray(self._values_to_tell) + # TODO: This assumes the objective is the first value while measures are + # the remaining values; we should document this somewhere. self._scheduler.tell(objective=values[:, 0], measures=values[:, 1:]) # Empty the results. From dbd500f2eb160f9a3f2a7781f00b0a29425d2373 Mon Sep 17 00:00:00 2001 From: y0z Date: Thu, 14 Nov 2024 12:00:46 +0900 Subject: [PATCH 09/34] Update example --- template/example.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/template/example.py b/template/example.py index ee95f9e4..42f59a7d 100644 --- a/template/example.py +++ b/template/example.py @@ -18,17 +18,19 @@ def objective(trial: optuna.Trial) -> float: # TODO: Change the variables here to test your package. package_name = "samplers/your_sampler" -repo_owner = "Your GitHub Account Name" -ref = "Your Git Branch Name" test_local = True if test_local: + # This is an example of how to load a sampler from your local optunahub-registry. sampler = optunahub.load_local_module( - package=package_name, registry_root="./package/" + package=package_name, + registry_root="./", # Path to the root of the optunahub-registry. ).YourSampler() else: + # This is an example of how to load a sampler from your fork of the optunahub-registry. + # Please remove repo_owner and ref arguments before submitting a pull request. sampler = optunahub.load_module( - package=package_name, repo_owner=repo_owner, ref=ref + package=package_name, repo_owner="Your GitHub Account ID", ref="Your Git Branch Name" ).YourSampler() study = optuna.create_study(sampler=sampler) From c47d1e4f4617c95a2eee3d15614f56b53d509171 Mon Sep 17 00:00:00 2001 From: y0z Date: Thu, 14 Nov 2024 17:05:08 +0900 Subject: [PATCH 10/34] Replace optunahub.load_module('simple').SimpleBaseSampler to optunahub.samplers.SimpleBaseSampler --- package/samplers/gp_pims/sampler.py | 5 +---- .../grey_wolf_optimization/grey_wolf_optimization.py | 2 +- package/samplers/hebo/sampler.py | 5 +---- package/samplers/nelder_mead/nelder_mead.py | 5 +---- package/samplers/simple/README.md | 7 ++++++- package/samplers/simple/example.py | 2 +- package/samplers/smac_sampler/sampler.py | 3 +-- package/samplers/whale_optimization/whale_optimization.py | 5 +---- 8 files changed, 13 insertions(+), 21 deletions(-) diff --git a/package/samplers/gp_pims/sampler.py b/package/samplers/gp_pims/sampler.py index 92469fa1..6ee587ff 100644 --- a/package/samplers/gp_pims/sampler.py +++ b/package/samplers/gp_pims/sampler.py @@ -469,10 +469,7 @@ def acq(self, x: np.ndarray) -> np.ndarray: return ((self.maximums - mean) / std).ravel() -SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler - - -class PIMSSampler(SimpleBaseSampler): # type: ignore +class PIMSSampler(optunahub.samplers.SimpleBaseSampler): def __init__( self, search_space: dict[str, optuna.distributions.BaseDistribution], diff --git a/package/samplers/grey_wolf_optimization/grey_wolf_optimization.py b/package/samplers/grey_wolf_optimization/grey_wolf_optimization.py index edf65987..1017088d 100644 --- a/package/samplers/grey_wolf_optimization/grey_wolf_optimization.py +++ b/package/samplers/grey_wolf_optimization/grey_wolf_optimization.py @@ -10,7 +10,7 @@ import optunahub -class GreyWolfOptimizationSampler(optunahub.load_module("samplers/simple").SimpleBaseSampler): # type: ignore +class GreyWolfOptimizationSampler(optunahub.samplers.SimpleBaseSampler): def __init__( self, search_space: dict[str, BaseDistribution] | None = None, diff --git a/package/samplers/hebo/sampler.py b/package/samplers/hebo/sampler.py index 38423d24..b76cd5bf 100644 --- a/package/samplers/hebo/sampler.py +++ b/package/samplers/hebo/sampler.py @@ -17,10 +17,7 @@ from hebo.optimizers.hebo import HEBO -SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler - - -class HEBOSampler(SimpleBaseSampler): # type: ignore +class HEBOSampler(optunahub.samplers.SimpleBaseSampler): def __init__(self, search_space: dict[str, BaseDistribution]) -> None: super().__init__(search_space) self._hebo = HEBO(self._convert_to_hebo_design_space(search_space)) diff --git a/package/samplers/nelder_mead/nelder_mead.py b/package/samplers/nelder_mead/nelder_mead.py index 5b39d1e5..887da5b6 100644 --- a/package/samplers/nelder_mead/nelder_mead.py +++ b/package/samplers/nelder_mead/nelder_mead.py @@ -10,10 +10,7 @@ from .generate_initial_simplex import generate_initial_simplex -SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler - - -class NelderMeadSampler(SimpleBaseSampler): # type: ignore +class NelderMeadSampler(optunahub.samplers.SimpleBaseSampler): """A sampler based on the Nelder-Mead simplex algorithm. This algorithm does not support conditional parameters that make a tree-structured search space. Sampling for such parameters is delegated to independent_sampler (default: RandomSampler). diff --git a/package/samplers/simple/README.md b/package/samplers/simple/README.md index eced9191..08c3a158 100644 --- a/package/samplers/simple/README.md +++ b/package/samplers/simple/README.md @@ -7,6 +7,8 @@ optuna_versions: [3.6.1] license: MIT License --- +`SimpleBaseSampler` has been moved to [`optunahub.samplers`](https://optuna.github.io/optunahub/samplers.html). Please use [`optunahub.samplers.SimpleBaseSampler`](https://optuna.github.io/optunahub/generated/optunahub.samplers.SimpleBaseSampler.html#optunahub.samplers.SimpleBaseSampler) instead of this package. + ## Class or Function Names - SimpleBaseSampler @@ -14,8 +16,11 @@ license: MIT License ## Example ```python +import optunahub + class UserDefinedSampler( - optunahub.load_module("samplers/simple").SimpleBaseSampler + optunahub.samplers.SimpleBaseSampler + # optunahub.load_module("samplers/simple").SimpleBaseSampler ): ... ``` diff --git a/package/samplers/simple/example.py b/package/samplers/simple/example.py index 4e536dc3..ed976519 100644 --- a/package/samplers/simple/example.py +++ b/package/samplers/simple/example.py @@ -12,7 +12,7 @@ import optunahub -class UserDefinedSampler(optunahub.load_module("samplers/simple").SimpleBaseSampler): # type: ignore +class UserDefinedSampler(optunahub.samplers.SimpleBaseSampler): def __init__(self, search_space: dict[str, BaseDistribution] | None = None) -> None: super().__init__(search_space) self._rng = np.random.RandomState() diff --git a/package/samplers/smac_sampler/sampler.py b/package/samplers/smac_sampler/sampler.py index 10c43204..964748d9 100644 --- a/package/samplers/smac_sampler/sampler.py +++ b/package/samplers/smac_sampler/sampler.py @@ -34,12 +34,11 @@ from smac.scenario import Scenario -SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler _SMAC_INSTANCE_KEY = "smac:instance" _SMAC_SEED_KEY = "smac:seed" -class SMACSampler(SimpleBaseSampler): # type: ignore +class SMACSampler(optunahub.samplers.SimpleBaseSampler): """ A sampler that uses SMAC3 v2.2.0. diff --git a/package/samplers/whale_optimization/whale_optimization.py b/package/samplers/whale_optimization/whale_optimization.py index b8337ac0..7f805968 100644 --- a/package/samplers/whale_optimization/whale_optimization.py +++ b/package/samplers/whale_optimization/whale_optimization.py @@ -7,10 +7,7 @@ import optunahub -SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler - - -class WhaleOptimizationSampler(SimpleBaseSampler): # type: ignore +class WhaleOptimizationSampler(optunahub.samplers.SimpleBaseSampler): def __init__( self, search_space: dict[str, optuna.distributions.BaseDistribution] | None = None, From 4d775e78ec347e0065c64d6f4d7d1871d3dfc00a Mon Sep 17 00:00:00 2001 From: y0z Date: Thu, 14 Nov 2024 18:20:05 +0900 Subject: [PATCH 11/34] Update README of AutoSampler --- package/samplers/auto_sampler/README.md | 6 +++++- .../auto_sampler/images/autosampler.png | Bin 0 -> 38928 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 package/samplers/auto_sampler/images/autosampler.png diff --git a/package/samplers/auto_sampler/README.md b/package/samplers/auto_sampler/README.md index 78ffb581..0b9e1809 100644 --- a/package/samplers/auto_sampler/README.md +++ b/package/samplers/auto_sampler/README.md @@ -9,7 +9,11 @@ license: MIT License ## Abstract -This package automatically selects an appropriate sampler for the provided search space based on the developers' recommendation. +This package automatically selects an appropriate sampler for the provided search space based on the developers' recommendation. The following article provide detaled information about AutoSampler. + +- 📰 [AutoSampler: Automatic Selection of Optimization Algorithms in Optuna](https://medium.com/optuna/autosampler-automatic-selection-of-optimization-algorithms-in-optuna-1443875fd8f9) + +![Concept of AutoSampler](images/autosampler.png) ## Class or Function Names diff --git a/package/samplers/auto_sampler/images/autosampler.png b/package/samplers/auto_sampler/images/autosampler.png new file mode 100644 index 0000000000000000000000000000000000000000..5f31d397af7ecfccfc4cb24b8e2d704e124171b6 GIT binary patch literal 38928 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 E2V{)pH2?qr literal 0 HcmV?d00001 From 865b0556206b7cc17929bb5ac1dbe9e6e60efceb Mon Sep 17 00:00:00 2001 From: Yoshihiko Ozaki <30489874+y0z@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:22:15 +0900 Subject: [PATCH 12/34] Update template/example.py Co-authored-by: Naoto Mizuno --- template/example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/example.py b/template/example.py index 42f59a7d..b7d249d4 100644 --- a/template/example.py +++ b/template/example.py @@ -16,7 +16,7 @@ def objective(trial: optuna.Trial) -> float: return x**2 + y**2 -# TODO: Change the variables here to test your package. +# TODO: Change package_name to test your package. package_name = "samplers/your_sampler" test_local = True From 337b602f16eee40e3f4424ac78f3dff51007d04a Mon Sep 17 00:00:00 2001 From: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> Date: Fri, 15 Nov 2024 05:50:56 +0100 Subject: [PATCH 13/34] Update package/samplers/simple/README.md --- package/samplers/simple/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/package/samplers/simple/README.md b/package/samplers/simple/README.md index 08c3a158..11bf34a9 100644 --- a/package/samplers/simple/README.md +++ b/package/samplers/simple/README.md @@ -20,7 +20,6 @@ import optunahub class UserDefinedSampler( optunahub.samplers.SimpleBaseSampler - # optunahub.load_module("samplers/simple").SimpleBaseSampler ): ... ``` From 861095bfcdb4250597dd41bc1775f402ec82469b Mon Sep 17 00:00:00 2001 From: Yoshihiko Ozaki <30489874+y0z@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:29:50 +0900 Subject: [PATCH 14/34] Update package/samplers/auto_sampler/README.md Co-authored-by: Naoto Mizuno --- package/samplers/auto_sampler/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/samplers/auto_sampler/README.md b/package/samplers/auto_sampler/README.md index 0b9e1809..eaad4307 100644 --- a/package/samplers/auto_sampler/README.md +++ b/package/samplers/auto_sampler/README.md @@ -9,7 +9,7 @@ license: MIT License ## Abstract -This package automatically selects an appropriate sampler for the provided search space based on the developers' recommendation. The following article provide detaled information about AutoSampler. +This package automatically selects an appropriate sampler for the provided search space based on the developers' recommendation. The following article provides detailed information about AutoSampler. - 📰 [AutoSampler: Automatic Selection of Optimization Algorithms in Optuna](https://medium.com/optuna/autosampler-automatic-selection-of-optimization-algorithms-in-optuna-1443875fd8f9) From 3323b0f08dfa7b38e8f1208d4ced26c0d1f95265 Mon Sep 17 00:00:00 2001 From: y0z Date: Fri, 15 Nov 2024 14:46:38 +0900 Subject: [PATCH 15/34] Introduce early detection for dependencies --- package/samplers/auto_sampler/_sampler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package/samplers/auto_sampler/_sampler.py b/package/samplers/auto_sampler/_sampler.py index 55669274..8815ee8f 100644 --- a/package/samplers/auto_sampler/_sampler.py +++ b/package/samplers/auto_sampler/_sampler.py @@ -6,6 +6,7 @@ from typing import Any from typing import TYPE_CHECKING +import cmaes as _ # NOQA from optuna.distributions import CategoricalDistribution from optuna.logging import get_logger from optuna.samplers import BaseSampler @@ -18,6 +19,8 @@ from optuna.samplers._lazy_random_state import LazyRandomState from optuna.search_space import IntersectionSearchSpace from optuna.trial import TrialState +import scipy as _ # NOQA +import torch as _ # NOQA if TYPE_CHECKING: From 3528121c188ba8f9748bd74d3163ce50adca58f5 Mon Sep 17 00:00:00 2001 From: y0z Date: Thu, 21 Nov 2024 12:20:13 +0900 Subject: [PATCH 16/34] Update docs --- recipes/002_registration.py | 7 +++---- template/README.md | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/recipes/002_registration.py b/recipes/002_registration.py index 484db2be..17ec7abc 100644 --- a/recipes/002_registration.py +++ b/recipes/002_registration.py @@ -80,13 +80,12 @@ - `Abstract `__ section that describes the summary of your package. It should be a markdown paragraph. This section will help attract potential users to your package. -- `Class or Function Names `__ section that describes the classes or functions provided by the package. If you provide multiple classes or functions, you should list them in this section. Note that the section must be a markdown list. If you provide only one class or function, you can simply write the class or function name. Note that the documentation of the classes or functions must be written in their docstrings. If you want to refer to the documentation, please leave the source code link, or write them in the following `Others` section. For example: +- `APIs `__ section that describes the APIs provided by the package. The documentation format is arbitrary, it is helpful to provide enough information about classes, functions, arguments, etc. to use your package. At least the important class/function names that you implemented should be listed here. .. code-block:: markdown - - `DemoSampler1` - - `DemoSampler2` - - `demo_function1` + - `ClassName(*, argment1: argument_type)` + - `argument1`: Description of `argument1`. - An `Installation `__ section that describes how to install the additional dependencies if required. If your package contains ``requirements.txt``, it will be available at ``https://hub.optuna.org/{category}/{your_package_name}/requirements.txt``. Then, the package dependencies can be installed as follows. diff --git a/template/README.md b/template/README.md index d4e40702..07c32905 100644 --- a/template/README.md +++ b/template/README.md @@ -40,13 +40,28 @@ This section will help attract potential users to your package. This package provides a sampler based on Gaussian process-based Bayesian optimization. The sampler is highly sample-efficient, so it is suitable for computationally expensive optimization problems with a limited evaluation budget, such as hyperparameter optimization of machine learning algorithms. -## Class or Function Names +## APIs -Please fill in the class/function names which you implement here. +Please provide API documentation describing how to use your package's functionalities. +The documentation format is arbitrary, but at least the important class/function names that you implemented should be listed here. +More users will take advantage of your package by providing detailed and helpful documentation. **Example** -- GPSampler +- `MoCmaSampler(*, search_space: dict[str, BaseDistribution] | None = None, popsize: int | None = None, seed: int | None = None)` + - `search_space`: A dictionary containing the search space that defines the parameter space. The keys are the parameter names and the values are [the parameter's distribution](https://optuna.readthedocs.io/en/stable/reference/distributions.html). If the search space is not provided, the sampler will infer the search space dynamically. + Example: + ```python + search_space = { + "x": optuna.distributions.FloatDistribution(-5, 5), + "y": optuna.distributions.FloatDistribution(-5, 5), + } + MoCmaSampler(search_space=search_space) + ``` + - `popsize`: Population size of the CMA-ES algorithm. If not provided, the population size will be set based on the search space dimensionality. If you have a sufficient evaluation budget, it is recommended to increase the popsize. + - `seed`: Seed for random number generator. + +Note that because of the limitation of the algorithm, only non-conditional numerical parameters are sampled by the MO-CMA-ES algorithm, and categorical parameters and conditional parameters are handled by random sampling. ## Installation From cfa16b8f047a827cc6fd8541765aa4c5e5029c47 Mon Sep 17 00:00:00 2001 From: Yoshihiko Ozaki <30489874+y0z@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:11:58 +0900 Subject: [PATCH 17/34] Update template/README.md Co-authored-by: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> --- template/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/README.md b/template/README.md index 07c32905..2ef91a7a 100644 --- a/template/README.md +++ b/template/README.md @@ -61,7 +61,7 @@ More users will take advantage of your package by providing detailed and helpful - `popsize`: Population size of the CMA-ES algorithm. If not provided, the population size will be set based on the search space dimensionality. If you have a sufficient evaluation budget, it is recommended to increase the popsize. - `seed`: Seed for random number generator. -Note that because of the limitation of the algorithm, only non-conditional numerical parameters are sampled by the MO-CMA-ES algorithm, and categorical parameters and conditional parameters are handled by random sampling. +Note that because of the limitation of the algorithm, only non-conditional numerical parameters can be sampled by the MO-CMA-ES algorithm, and categorical and conditional parameters are handled by random search. ## Installation From ffb1cc5b5445181564ff23a7c5e14f0ccfb0d336 Mon Sep 17 00:00:00 2001 From: Shuhei Watanabe <47781922+nabenabe0928@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:45:46 +0100 Subject: [PATCH 18/34] Suggestions from the meeting (#1) * Changes from the meeting * Changes from the meeting * Update SimpleBaseSampler --- package/samplers/cmamae/sampler.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 8c2d206e..abe24f0a 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -15,10 +15,7 @@ from ribs.schedulers import Scheduler -SimpleBaseSampler = optunahub.load_module("samplers/simple").SimpleBaseSampler - - -class CmaMaeSampler(SimpleBaseSampler): +class CmaMaeSampler(optunahub.samplers.SimpleBaseSampler): """A sampler using CMA-MAE as implemented in pyribs. `CMA-MAE `_ is a quality diversity @@ -115,7 +112,8 @@ def __init__( result_archive=result_archive, ) - self._values_to_tell: list[Sequence[float]] = [] + self._values_to_tell: list[list[float]] = [] + self._stored_trial_numbers: list[int] = [] def _validate_params(self, param_names: list[str], emitter_x0: dict[str, float]) -> None: dim = len(param_names) @@ -179,7 +177,15 @@ def after_trial( self._validate_param_names(trial.params.keys()) # Store the trial result. - self._values_to_tell.append(values) + direction0 = study.directions[0] + minimize_in_optuna = study.StudyDirection.MINIMIZE == direction0 + assert values is not None, "MyPy redefinition." + modified_values = list([float(v) for v in values]) + if minimize_in_optuna: + # The direction of the first objective (pyribs maximizes). + modified_values[0] = -values[0] + self._values_to_tell.append(modified_values) + self._stored_trial_numbers.append(trial.number) # If we have not retrieved the whole batch of solutions, then we should # not tell() the results to the scheduler yet. @@ -187,10 +193,11 @@ def after_trial( return # Tell the batch results to external sampler once the batch is ready. - values = np.asarray(self._values_to_tell) + values_to_tell = np.asarray(self._values_to_tell)[np.argsort(self._stored_trial_numbers)] # TODO: This assumes the objective is the first value while measures are # the remaining values; we should document this somewhere. - self._scheduler.tell(objective=values[:, 0], measures=values[:, 1:]) + self._scheduler.tell(objective=values_to_tell[:, 0], measures=values_to_tell[:, 1:]) # Empty the results. self._values_to_tell = [] + self._stored_trial_numbers = [] From 4618267ef06d68213ce36719e3c2b02c48474d32 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 21 Nov 2024 12:47:41 -0800 Subject: [PATCH 19/34] Tweak --- package/samplers/cmamae/sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index abe24f0a..2f64e0e7 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -178,7 +178,7 @@ def after_trial( # Store the trial result. direction0 = study.directions[0] - minimize_in_optuna = study.StudyDirection.MINIMIZE == direction0 + minimize_in_optuna = direction0 == study.StudyDirection.MINIMIZE assert values is not None, "MyPy redefinition." modified_values = list([float(v) for v in values]) if minimize_in_optuna: From b45f4bebd0320ba3847cd5d40cb0c3c4f22836e6 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 21 Nov 2024 13:06:27 -0800 Subject: [PATCH 20/34] Fix import --- package/samplers/cmamae/sampler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 2f64e0e7..fe8e66f4 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -7,6 +7,7 @@ from optuna.distributions import BaseDistribution from optuna.distributions import FloatDistribution from optuna.study import Study +from optuna.study import StudyDirection from optuna.trial import FrozenTrial from optuna.trial import TrialState import optunahub @@ -178,7 +179,7 @@ def after_trial( # Store the trial result. direction0 = study.directions[0] - minimize_in_optuna = direction0 == study.StudyDirection.MINIMIZE + minimize_in_optuna = direction0 == StudyDirection.MINIMIZE assert values is not None, "MyPy redefinition." modified_values = list([float(v) for v in values]) if minimize_in_optuna: From 13b197113e4d394bf884fdc002da4cd22c629112 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 21 Nov 2024 15:09:31 -0800 Subject: [PATCH 21/34] Update example --- package/samplers/cmamae/example.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py index c38f8507..77546663 100644 --- a/package/samplers/cmamae/example.py +++ b/package/samplers/cmamae/example.py @@ -3,35 +3,32 @@ from sampler import CmaMaeSampler -# TODO: Replace above import with this. -# module = optunahub.load_module("samplers/pyribs") -# PyribsSampler = module.PyribsSampler - - def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: """Returns an objective followed by two measures.""" x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) - return -(x**2 + y**2) + 2, x, y + return x**2 + y**2, x, y if __name__ == "__main__": sampler = CmaMaeSampler( param_names=["x", "y"], archive_dims=[20, 20], - archive_ranges=[(-10, 10), (-10, 10)], + archive_ranges=[(-1, 1), (-1, 1)], archive_learning_rate=0.1, archive_threshold_min=-10, n_emitters=1, - emitter_x0={"x": 5, "y": 5}, + emitter_x0={ + "x": 0, + "y": 0, + }, emitter_sigma0=0.1, - emitter_batch_size=5, + emitter_batch_size=20, ) study = optuna.create_study( sampler=sampler, directions=[ - # pyribs maximizes objectives. - StudyDirection.MAXIMIZE, + StudyDirection.MINIMIZE, # The remaining values are measures, which do not have an # optimization direction. # TODO: Currently, using StudyDirection.NOT_SET is not allowed as @@ -40,8 +37,4 @@ def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: StudyDirection.MINIMIZE, ], ) - study.optimize(objective, n_trials=100) - - # TODO: Visualization. - # fig = optuna.visualization.plot_optimization_history(study) - # fig.write_image("cmamae_optimization_history.png") + study.optimize(objective, n_trials=10000) From 3ace673c1ded289f9d237e87482f23e1d3bc14cd Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 21 Nov 2024 15:34:49 -0800 Subject: [PATCH 22/34] Add README and update example --- package/samplers/cmamae/README.md | 194 +++++++++++++++++------------ package/samplers/cmamae/example.py | 13 +- package/samplers/cmamae/sampler.py | 14 ++- 3 files changed, 133 insertions(+), 88 deletions(-) diff --git a/package/samplers/cmamae/README.md b/package/samplers/cmamae/README.md index 8724563c..d2274574 100644 --- a/package/samplers/cmamae/README.md +++ b/package/samplers/cmamae/README.md @@ -1,110 +1,150 @@ --- author: Bryon Tjanaka -title: Please fill in the title of the feature here. (e.g., Gaussian-Process Expected Improvement Sampler) -description: Please fill in the description of the feature here. (e.g., This sampler searches for each trial based on expected improvement using Gaussian process.) -tags: [Please fill in the list of tags here. (e.g., sampler, visualization, pruner)] -optuna_versions: ['Please fill in the list of versions of Optuna in which you have confirmed the feature works, e.g., 3.6.1.'] +title: CMA-MAE Sampler +description: This sampler searches for solutions using CMA-MAE, a quality diversity algorihm implemented in pyribs. +tags: [sampler, quality diversity] +optuna_versions: [4.0.0] license: MIT License --- - - -Please read the [tutorial guide](https://optuna.github.io/optunahub-registry/recipes/001_first.html) to register your feature in OptunaHub. -You can find more detailed explanation of the following contents in the tutorial. -Looking at [other packages' implementations](https://github.com/optuna/optunahub-registry/tree/main/package) will also help you. - ## Abstract -You can provide an abstract for your package here. -This section will help attract potential users to your package. - -**Example** - -This package provides a sampler based on Gaussian process-based Bayesian optimization. The sampler is highly sample-efficient, so it is suitable for computationally expensive optimization problems with a limited evaluation budget, such as hyperparameter optimization of machine learning algorithms. +This package provides a sampler using CMA-MAE as implemented in pyribs. +[CMA-MAE](https://dl.acm.org/doi/abs/10.1145/3583131.3590389) is a quality +diversity algorithm that has demonstrated state-of-the-art performance in a +variety of domains. [Pyribs](https://pyribs.org) is a bare-bones Python library +for quality diversity optimization algorithms. For a primer on CMA-MAE, quality +diversity, and pyribs, we recommend referring to the series of +[pyribs tutorials](https://docs.pyribs.org/en/stable/tutorials.html). + +For simplicity, this implementation provides a default instantiation of CMA-MAE +with a +[GridArchive](https://docs.pyribs.org/en/stable/api/ribs.archives.GridArchive.html) +and +[EvolutionStrategyEmitter](https://docs.pyribs.org/en/stable/api/ribs.emitters.EvolutionStrategyEmitter.html) +with improvement ranking, all wrapped up in a +[Scheduler](https://docs.pyribs.org/en/stable/api/ribs.schedulers.Scheduler.html). +However, it is possible to implement many variations of CMA-MAE and other +quality diversity algorithms using pyribs. ## Class or Function Names -Please fill in the class/function names which you implement here. - -**Example** - -- GPSampler +- CmaMaeSampler ## Installation -If you have additional dependencies, please fill in the installation guide here. -If no additional dependencies is required, **this section can be removed**. - -**Example** - -```shell -$ pip install scipy torch -``` - -If your package has `requirements.txt`, it will be automatically uploaded to the OptunaHub, and the package dependencies will be available to install as follows. - ```shell - pip install -r https://hub.optuna.org/{category}/{your_package_name}/requirements.txt +$ pip install ribs ``` ## Example -Please fill in the code snippet to use the implemented feature here. - -**Example** - ```python import optuna import optunahub +from optuna.study import StudyDirection + +module = optunahub.load_module("samplers/cmamae") +CmaMaeSampler = module.CmaMaeSampler + + +def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: + """Returns an objective followed by two measures.""" + x = trial.suggest_float("x", -10, 10) + y = trial.suggest_float("y", -10, 10) + return x**2 + y**2, x, y + + +if __name__ == "__main__": + sampler = CmaMaeSampler( + param_names=["x", "y"], + archive_dims=[20, 20], + archive_ranges=[(-1, 1), (-1, 1)], + archive_learning_rate=0.1, + archive_threshold_min=-10, + n_emitters=1, + emitter_x0={ + "x": 0, + "y": 0, + }, + emitter_sigma0=0.1, + emitter_batch_size=20, + ) + study = optuna.create_study( + sampler=sampler, + directions=[ + StudyDirection.MINIMIZE, + # The remaining directions are for the measures, which do not have + # an optimization direction. However, we set MINIMIZE as a + # placeholder direction. + StudyDirection.MINIMIZE, + StudyDirection.MINIMIZE, + ], + ) + study.optimize(objective, n_trials=10000) +``` +## Others -def objective(trial): - x = trial.suggest_float("x", -5, 5) - return x**2 +### Reference +#### CMA-MAE -sampler = optunahub.load_module(package="samplers/gp").GPSampler() -study = optuna.create_study(sampler=sampler) -study.optimize(objective, n_trials=100) -``` +Matthew Fontaine and Stefanos Nikolaidis. 2023. Covariance Matrix Adaptation +MAP-Annealing. In Proceedings of the Genetic and Evolutionary Computation +Conference (GECCO '23). Association for Computing Machinery, New York, NY, USA, +456–465. https://doi.org/10.1145/3583131.3590389 -## Others +#### Pyribs -Please fill in any other information if you have here by adding child sections (###). -If there is no additional information, **this section can be removed**. +Bryon Tjanaka, Matthew C Fontaine, David H Lee, Yulun Zhang, Nivedit Reddy +Balam, Nathaniel Dennler, Sujay S Garlanka, Nikitas Dimitri Klapsis, and +Stefanos Nikolaidis. 2023. Pyribs: A Bare-Bones Python Library for Quality +Diversity Optimization. In Proceedings of the Genetic and Evolutionary +Computation Conference (GECCO '23). Association for Computing Machinery, New +York, NY, USA, 220–229. https://doi.org/10.1145/3583131.3590374 - diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py index 77546663..5b68f7c0 100644 --- a/package/samplers/cmamae/example.py +++ b/package/samplers/cmamae/example.py @@ -1,6 +1,10 @@ import optuna from optuna.study import StudyDirection -from sampler import CmaMaeSampler +import optunahub + + +module = optunahub.load_module("samplers/cmamae") +CmaMaeSampler = module.CmaMaeSampler def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: @@ -29,10 +33,9 @@ def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: sampler=sampler, directions=[ StudyDirection.MINIMIZE, - # The remaining values are measures, which do not have an - # optimization direction. - # TODO: Currently, using StudyDirection.NOT_SET is not allowed as - # Optuna assumes we either minimize or maximize. + # The remaining directions are for the measures, which do not have + # an optimization direction. However, we set MINIMIZE as a + # placeholder direction. StudyDirection.MINIMIZE, StudyDirection.MINIMIZE, ], diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index fe8e66f4..d6b2d38b 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -19,12 +19,12 @@ class CmaMaeSampler(optunahub.samplers.SimpleBaseSampler): """A sampler using CMA-MAE as implemented in pyribs. - `CMA-MAE `_ is a quality diversity - algorithm that has demonstrated state-of-the-art performance in a variety of - domains. `pyribs `_ is a bare-bones Python library for - quality diversity optimization algorithms. For a primer on CMA-MAE and - pyribs, we recommend referring to the series of `pyribs tutorials - `_. + `CMA-MAE `_ is a quality + diversity algorithm that has demonstrated state-of-the-art performance in a + variety of domains. `Pyribs `_ is a bare-bones Python + library for quality diversity optimization algorithms. For a primer on + CMA-MAE, quality diversity, and pyribs, we recommend referring to the series + of `pyribs tutorials `_. For simplicity, this implementation provides a default instantiation of CMA-MAE with a `GridArchive @@ -33,6 +33,8 @@ class CmaMaeSampler(optunahub.samplers.SimpleBaseSampler): `_ with improvement ranking, all wrapped up in a `Scheduler `_. + However, it is possible to implement many variations of CMA-MAE and other + quality diversity algorithms using pyribs. Args: param_names: List of names of parameters to optimize. From 401551b99ff9d6cf0e612f3e60495c035dbd89b6 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 21 Nov 2024 15:37:09 -0800 Subject: [PATCH 23/34] Clean up TODOs --- package/samplers/cmamae/sampler.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index d6b2d38b..3557f7de 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -36,6 +36,10 @@ class CmaMaeSampler(optunahub.samplers.SimpleBaseSampler): However, it is possible to implement many variations of CMA-MAE and other quality diversity algorithms using pyribs. + Note that this sampler assumes the objective function will return a list of + values. The first value will be the objective, and the remaining values will + be the measures. + Args: param_names: List of names of parameters to optimize. archive_dims: Number of archive cells in each dimension of the measure @@ -172,11 +176,6 @@ def after_trial( state: TrialState, values: Sequence[float] | None, ) -> None: - # TODO: Is it safe to assume the parameters will always come back in the - # order that they were sent out by the scheduler? Pyribs makes that - # assumption and stores the solutions internally. If not, maybe we can - # retrieve solutions based on their trial ID? - self._validate_param_names(trial.params.keys()) # Store the trial result. @@ -197,8 +196,6 @@ def after_trial( # Tell the batch results to external sampler once the batch is ready. values_to_tell = np.asarray(self._values_to_tell)[np.argsort(self._stored_trial_numbers)] - # TODO: This assumes the objective is the first value while measures are - # the remaining values; we should document this somewhere. self._scheduler.tell(objective=values_to_tell[:, 0], measures=values_to_tell[:, 1:]) # Empty the results. From ea70e5f17bfb19c168aaafebf720c4804ee5663c Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Fri, 22 Nov 2024 04:19:05 +0100 Subject: [PATCH 24/34] Add NSGAII with TPE init --- .../samplers/nsgaii_with_tpe_warmup/LICENSE | 21 ++++++++++ .../samplers/nsgaii_with_tpe_warmup/README.md | 40 +++++++++++++++++++ .../nsgaii_with_tpe_warmup/__init__.py | 35 ++++++++++++++++ .../nsgaii_with_tpe_warmup/example.py | 16 ++++++++ 4 files changed, 112 insertions(+) create mode 100644 package/samplers/nsgaii_with_tpe_warmup/LICENSE create mode 100644 package/samplers/nsgaii_with_tpe_warmup/README.md create mode 100644 package/samplers/nsgaii_with_tpe_warmup/__init__.py create mode 100644 package/samplers/nsgaii_with_tpe_warmup/example.py diff --git a/package/samplers/nsgaii_with_tpe_warmup/LICENSE b/package/samplers/nsgaii_with_tpe_warmup/LICENSE new file mode 100644 index 00000000..f6547d35 --- /dev/null +++ b/package/samplers/nsgaii_with_tpe_warmup/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Shuhei Watanabe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/samplers/nsgaii_with_tpe_warmup/README.md b/package/samplers/nsgaii_with_tpe_warmup/README.md new file mode 100644 index 00000000..c03445c6 --- /dev/null +++ b/package/samplers/nsgaii_with_tpe_warmup/README.md @@ -0,0 +1,40 @@ +--- +author: Shuhei Watanabe +title: NSGAIISampler Using TPESampler for the Initialization +description: This sampler uses TPESampler for the initialization to warm start. +tags: [sampler, tpe, nsgaii, warmstart] +optuna_versions: [4.1.0] +license: MIT License +--- + +## Abstract + +This sampler uses `TPESampler` instead of `RandomSampler` for the initialization of `NSGAIISampler`. + +## APIs + +- NSGAIIWithTPEWarmupSampler + +This class takes the identical interface as the Optuna [`NSGAIISampler`](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html). + +## Example + +```python +from __future__ import annotations + +import optuna +import optunahub + + +def objective(trial: optuna.Trial) -> tuple[float, float]: + x = trial.suggest_float("x", -5, 5) + y = trial.suggest_float("y", -5, 5) + return x**2 + y**2, (x - 2) ** 2 + (y - 2) ** 2 + + +package_name = "samplers/nsgaii_with_tpe_warmup" +sampler = optunahub.load_module(package=package_name).NSGAIIWithTPEWarmupSampler() +study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=60) + +``` diff --git a/package/samplers/nsgaii_with_tpe_warmup/__init__.py b/package/samplers/nsgaii_with_tpe_warmup/__init__.py new file mode 100644 index 00000000..444f6f81 --- /dev/null +++ b/package/samplers/nsgaii_with_tpe_warmup/__init__.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import Any +from typing import TYPE_CHECKING + +from optuna.samplers import NSGAIISampler +from optuna.samplers import TPESampler +from optuna.samplers.nsgaii._sampler import _GENERATION_KEY + + +if TYPE_CHECKING: + from optuna import Study + from optuna.distributions import BaseDistribution + from optuna.trial import FrozenTrial + + +MAX_INT32 = (1 << 31) - 1 + + +class NSGAIIWithTPEWarmupSampler(NSGAIISampler): + def sample_relative( + self, + study: Study, + trial: FrozenTrial, + search_space: dict[str, BaseDistribution], + ) -> dict[str, Any]: + if trial.number < self._population_size: + seed = self._rng.rng.randint(MAX_INT32) + sampler = TPESampler( + multivariate=True, constraints_func=self._constraints_func, seed=seed + ) + study._storage.set_trial_system_attr(trial._trial_id, _GENERATION_KEY, 0) + return sampler.sample_relative(study, trial, search_space) + + return super().sample_relative(study, trial, search_space) diff --git a/package/samplers/nsgaii_with_tpe_warmup/example.py b/package/samplers/nsgaii_with_tpe_warmup/example.py new file mode 100644 index 00000000..6a98ef1f --- /dev/null +++ b/package/samplers/nsgaii_with_tpe_warmup/example.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import optuna +import optunahub + + +def objective(trial: optuna.Trial) -> tuple[float, float]: + x = trial.suggest_float("x", -5, 5) + y = trial.suggest_float("y", -5, 5) + return x**2 + y**2, (x - 2) ** 2 + (y - 2) ** 2 + + +package_name = "samplers/nsgaii_with_tpe_warmup" +sampler = optunahub.load_module(package=package_name).NSGAIIWithTPEWarmupSampler() +study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=60) From e75c251bc2fb11e76a961fae86860a6eb08f3377 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Fri, 22 Nov 2024 04:24:11 +0100 Subject: [PATCH 25/34] Fix example --- package/samplers/nsgaii_with_tpe_warmup/README.md | 2 +- package/samplers/nsgaii_with_tpe_warmup/example.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/samplers/nsgaii_with_tpe_warmup/README.md b/package/samplers/nsgaii_with_tpe_warmup/README.md index c03445c6..fe66e995 100644 --- a/package/samplers/nsgaii_with_tpe_warmup/README.md +++ b/package/samplers/nsgaii_with_tpe_warmup/README.md @@ -34,7 +34,7 @@ def objective(trial: optuna.Trial) -> tuple[float, float]: package_name = "samplers/nsgaii_with_tpe_warmup" sampler = optunahub.load_module(package=package_name).NSGAIIWithTPEWarmupSampler() -study = optuna.create_study(sampler=sampler) +study = optuna.create_study(sampler=sampler, directions=["minimize"]*2) study.optimize(objective, n_trials=60) ``` diff --git a/package/samplers/nsgaii_with_tpe_warmup/example.py b/package/samplers/nsgaii_with_tpe_warmup/example.py index 6a98ef1f..07d7a3fe 100644 --- a/package/samplers/nsgaii_with_tpe_warmup/example.py +++ b/package/samplers/nsgaii_with_tpe_warmup/example.py @@ -12,5 +12,5 @@ def objective(trial: optuna.Trial) -> tuple[float, float]: package_name = "samplers/nsgaii_with_tpe_warmup" sampler = optunahub.load_module(package=package_name).NSGAIIWithTPEWarmupSampler() -study = optuna.create_study(sampler=sampler) +study = optuna.create_study(sampler=sampler, directions=["minimize"] * 2) study.optimize(objective, n_trials=60) From a2102986a50d6243468b949183e203a35e92a7e4 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Fri, 22 Nov 2024 01:29:23 -0800 Subject: [PATCH 26/34] Make scheduler public --- package/samplers/cmamae/sampler.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 3557f7de..15e0e4f2 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -113,7 +113,8 @@ def __init__( # Number of solutions generated in each batch from pyribs. self._batch_size = n_emitters * emitter_batch_size - self._scheduler = Scheduler( + # Public to allow access for, e.g., visualization. + self.scheduler = Scheduler( archive, emitters, result_archive=result_archive, @@ -161,7 +162,7 @@ def sample_relative( self._validate_param_names(search_space.keys()) # Note: Batch optimization means we need to enqueue trials. - solutions = self._scheduler.ask() + solutions = self.scheduler.ask() next_params = self._convert_to_optuna_params(solutions[0]) for solution in solutions[1:]: params = self._convert_to_optuna_params(solution) @@ -196,7 +197,7 @@ def after_trial( # Tell the batch results to external sampler once the batch is ready. values_to_tell = np.asarray(self._values_to_tell)[np.argsort(self._stored_trial_numbers)] - self._scheduler.tell(objective=values_to_tell[:, 0], measures=values_to_tell[:, 1:]) + self.scheduler.tell(objective=values_to_tell[:, 0], measures=values_to_tell[:, 1:]) # Empty the results. self._values_to_tell = [] From e89e7a95445068056f4be53db9e2ee86071701fc Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Fri, 22 Nov 2024 01:29:32 -0800 Subject: [PATCH 27/34] Add tags --- package/samplers/cmamae/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/samplers/cmamae/README.md b/package/samplers/cmamae/README.md index d2274574..03736fa3 100644 --- a/package/samplers/cmamae/README.md +++ b/package/samplers/cmamae/README.md @@ -2,7 +2,7 @@ author: Bryon Tjanaka title: CMA-MAE Sampler description: This sampler searches for solutions using CMA-MAE, a quality diversity algorihm implemented in pyribs. -tags: [sampler, quality diversity] +tags: [sampler, quality diversity, pyribs] optuna_versions: [4.0.0] license: MIT License --- From 82e866e6cafa34c0d54338e279fd87e584a74576 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 25 Nov 2024 07:27:05 +0100 Subject: [PATCH 28/34] Follow the Optuna convention --- package/samplers/cmamae/README.md | 26 ++++++++++++-------------- package/samplers/cmamae/example.py | 20 ++++++-------------- package/samplers/cmamae/sampler.py | 25 ++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/package/samplers/cmamae/README.md b/package/samplers/cmamae/README.md index 03736fa3..f712515c 100644 --- a/package/samplers/cmamae/README.md +++ b/package/samplers/cmamae/README.md @@ -31,6 +31,12 @@ quality diversity algorithms using pyribs. - CmaMaeSampler +Please take a look at: + +- [GridArchive](https://docs.pyribs.org/en/stable/api/ribs.archives.GridArchive.html), and +- [EvolutionStrategyEmitter](https://docs.pyribs.org/en/stable/api/ribs.emitters.EvolutionStrategyEmitter.html) + for the details of each argument. + ## Installation ```shell @@ -42,22 +48,24 @@ $ pip install ribs ```python import optuna import optunahub -from optuna.study import StudyDirection module = optunahub.load_module("samplers/cmamae") CmaMaeSampler = module.CmaMaeSampler -def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: +def objective(trial: optuna.trial.Trial) -> float: """Returns an objective followed by two measures.""" x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) - return x**2 + y**2, x, y + trial.set_user_attr("x", x) + trial.set_user_attr("y", y) + return x**2 + y**2 if __name__ == "__main__": sampler = CmaMaeSampler( param_names=["x", "y"], + measure_names=["x", "y"], archive_dims=[20, 20], archive_ranges=[(-1, 1), (-1, 1)], archive_learning_rate=0.1, @@ -70,17 +78,7 @@ if __name__ == "__main__": emitter_sigma0=0.1, emitter_batch_size=20, ) - study = optuna.create_study( - sampler=sampler, - directions=[ - StudyDirection.MINIMIZE, - # The remaining directions are for the measures, which do not have - # an optimization direction. However, we set MINIMIZE as a - # placeholder direction. - StudyDirection.MINIMIZE, - StudyDirection.MINIMIZE, - ], - ) + study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=10000) ``` diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py index 5b68f7c0..a78efe8c 100644 --- a/package/samplers/cmamae/example.py +++ b/package/samplers/cmamae/example.py @@ -1,5 +1,4 @@ import optuna -from optuna.study import StudyDirection import optunahub @@ -7,16 +6,19 @@ CmaMaeSampler = module.CmaMaeSampler -def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: +def objective(trial: optuna.trial.Trial) -> float: """Returns an objective followed by two measures.""" x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) - return x**2 + y**2, x, y + trial.set_user_attr("x", x) + trial.set_user_attr("y", y) + return x**2 + y**2 if __name__ == "__main__": sampler = CmaMaeSampler( param_names=["x", "y"], + measure_names=["x", "y"], archive_dims=[20, 20], archive_ranges=[(-1, 1), (-1, 1)], archive_learning_rate=0.1, @@ -29,15 +31,5 @@ def objective(trial: optuna.trial.Trial) -> tuple[float, float, float]: emitter_sigma0=0.1, emitter_batch_size=20, ) - study = optuna.create_study( - sampler=sampler, - directions=[ - StudyDirection.MINIMIZE, - # The remaining directions are for the measures, which do not have - # an optimization direction. However, we set MINIMIZE as a - # placeholder direction. - StudyDirection.MINIMIZE, - StudyDirection.MINIMIZE, - ], - ) + study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=10000) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 15e0e4f2..006a185d 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -67,6 +67,7 @@ def __init__( self, *, param_names: list[str], + measure_names: list[str], archive_dims: list[int], archive_ranges: list[tuple[float, float]], archive_learning_rate: float, @@ -77,7 +78,8 @@ def __init__( emitter_batch_size: int, ) -> None: self._validate_params(param_names, emitter_x0) - self._param_names = param_names[:] + self._param_names = param_names.copy() + self._measure_names = measure_names.copy() # NOTE: SimpleBaseSampler must know Optuna search_space information. search_space = {name: FloatDistribution(-1e9, 1e9) for name in self._param_names} @@ -182,8 +184,25 @@ def after_trial( # Store the trial result. direction0 = study.directions[0] minimize_in_optuna = direction0 == StudyDirection.MINIMIZE - assert values is not None, "MyPy redefinition." - modified_values = list([float(v) for v in values]) + if values is None: + raise RuntimeError( + f"{self.__class__.__name__} does not support Failed trials, " + f"but trial#{trial.number} failed." + ) + user_attrs = trial.user_attrs + if any(measure_name not in user_attrs for measure_name in self._measure_names): + raise KeyError( + f"All of measure in measure_names={self._measure_names} must be set to " + "trial.user_attrs. Please call trial.set_user_attr(, ) " + "for each measure." + ) + + self._raise_error_if_multi_objective(study) + modified_values = [ + float(values[0]), + float(user_attrs[self._measure_names[0]]), + float(user_attrs[self._measure_names[1]]), + ] if minimize_in_optuna: # The direction of the first objective (pyribs maximizes). modified_values[0] = -values[0] From 8d346a9cfcdaeeb2b705a37b0f23c49918f66818 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 25 Nov 2024 08:41:48 +0100 Subject: [PATCH 29/34] Apply bryon's comments --- package/samplers/cmamae/README.md | 6 +++--- package/samplers/cmamae/example.py | 6 +++--- package/samplers/cmamae/sampler.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package/samplers/cmamae/README.md b/package/samplers/cmamae/README.md index f712515c..7801df9f 100644 --- a/package/samplers/cmamae/README.md +++ b/package/samplers/cmamae/README.md @@ -57,15 +57,15 @@ def objective(trial: optuna.trial.Trial) -> float: """Returns an objective followed by two measures.""" x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) - trial.set_user_attr("x", x) - trial.set_user_attr("y", y) + trial.set_user_attr("m0", 2 * x) + trial.set_user_attr("m1", x + y) return x**2 + y**2 if __name__ == "__main__": sampler = CmaMaeSampler( param_names=["x", "y"], - measure_names=["x", "y"], + measure_names=["m0", "m1"], archive_dims=[20, 20], archive_ranges=[(-1, 1), (-1, 1)], archive_learning_rate=0.1, diff --git a/package/samplers/cmamae/example.py b/package/samplers/cmamae/example.py index a78efe8c..2903a81a 100644 --- a/package/samplers/cmamae/example.py +++ b/package/samplers/cmamae/example.py @@ -10,15 +10,15 @@ def objective(trial: optuna.trial.Trial) -> float: """Returns an objective followed by two measures.""" x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) - trial.set_user_attr("x", x) - trial.set_user_attr("y", y) + trial.set_user_attr("m0", 2 * x) + trial.set_user_attr("m1", x + y) return x**2 + y**2 if __name__ == "__main__": sampler = CmaMaeSampler( param_names=["x", "y"], - measure_names=["x", "y"], + measure_names=["m0", "m1"], archive_dims=[20, 20], archive_ranges=[(-1, 1), (-1, 1)], archive_learning_rate=0.1, diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 006a185d..a0106326 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -36,9 +36,9 @@ class CmaMaeSampler(optunahub.samplers.SimpleBaseSampler): However, it is possible to implement many variations of CMA-MAE and other quality diversity algorithms using pyribs. - Note that this sampler assumes the objective function will return a list of - values. The first value will be the objective, and the remaining values will - be the measures. + Note that this sampler assumes the measures are set to user_attrs of each trial. + To do so, please call ``trial.set_user_attr("YOUR MEASURE NAME", measure_value)`` for each + measure. Args: param_names: List of names of parameters to optimize. From 1d303b3e9a9d75095debbdcf91edc3bec3d46243 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 25 Nov 2024 08:43:35 +0100 Subject: [PATCH 30/34] Refactor --- package/samplers/cmamae/sampler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index a0106326..34fa32f3 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -42,6 +42,7 @@ class CmaMaeSampler(optunahub.samplers.SimpleBaseSampler): Args: param_names: List of names of parameters to optimize. + measure_names: List of names of measures. archive_dims: Number of archive cells in each dimension of the measure space, e.g. ``[20, 30, 40]`` indicates there should be 3 dimensions with 20, 30, and 40 cells. (The number of dimensions is implicitly From 8462391824285ede93eaf7d2678f5761976bb35c Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Wed, 27 Nov 2024 15:14:46 +0100 Subject: [PATCH 31/34] Fix --- package/samplers/cmamae/sampler.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package/samplers/cmamae/sampler.py b/package/samplers/cmamae/sampler.py index 34fa32f3..77f119ff 100644 --- a/package/samplers/cmamae/sampler.py +++ b/package/samplers/cmamae/sampler.py @@ -81,6 +81,11 @@ def __init__( self._validate_params(param_names, emitter_x0) self._param_names = param_names.copy() self._measure_names = measure_names.copy() + if len(set(self._measure_names)) != 2: + raise ValueError( + "measure_names must be a list of two unique measure names, " + f"but got measure_names={measure_names}." + ) # NOTE: SimpleBaseSampler must know Optuna search_space information. search_space = {name: FloatDistribution(-1e9, 1e9) for name in self._param_names} @@ -193,9 +198,9 @@ def after_trial( user_attrs = trial.user_attrs if any(measure_name not in user_attrs for measure_name in self._measure_names): raise KeyError( - f"All of measure in measure_names={self._measure_names} must be set to " - "trial.user_attrs. Please call trial.set_user_attr(, ) " - "for each measure." + f"All of measures in measure_names={self._measure_names} must be set to " + "trial.user_attrs. Please call `trial.set_user_attr(, )` " + "for each measure in your objective function." ) self._raise_error_if_multi_objective(study) From 718e40a18a438d40fa5d740dbd4e0d11ac263398 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 2 Dec 2024 15:03:22 +0100 Subject: [PATCH 32/34] Fix smac example --- package/samplers/smac_sampler/README.md | 30 ++++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/package/samplers/smac_sampler/README.md b/package/samplers/smac_sampler/README.md index ac335ab8..460b9caa 100644 --- a/package/samplers/smac_sampler/README.md +++ b/package/samplers/smac_sampler/README.md @@ -20,16 +20,34 @@ pip install -r https://hub.optuna.org/samplers/smac_sampler/requirements.txt ## Example ```python -search_space = { - "x": FloatDistribution(-10, 10), - "y": IntDistribution(0, 10), +import optuna +import optunahub -} -sampler = SMACSampler(search_space) + +module = optunahub.load_module("samplers/smac_sampler") +SMACSampler = module.SMACSampler + + +def objective(trial: optuna.trial.Trial) -> float: + x = trial.suggest_float("x", -10, 10) + y = trial.suggest_int("y", -10, 10) + return x**2 + y**2 + + +n_trials = 100 +sampler = SMACSampler( + { + "x": optuna.distributions.FloatDistribution(-10, 10), + "y": optuna.distributions.IntDistribution(-10, 10), + }, + n_trials=n_trials, +) study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=n_trials) +print(study.best_trial.params) ``` -See [`example.py`](https://github.com/optuna/optunahub-registry/blob/main/package/samplers/hebo/example.py) for a full example. +See [`example.py`](https://github.com/optuna/optunahub-registry/blob/main/package/samplers/smac_sampler/example.py) for a full example. ![History Plot](images/smac_sampler_history.png "History Plot") ## Others From 81fa08372eba05ee62467bf636018a9f50a2c85a Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Mon, 2 Dec 2024 15:06:29 +0100 Subject: [PATCH 33/34] Add seed option to SMAC --- package/samplers/smac_sampler/sampler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/samplers/smac_sampler/sampler.py b/package/samplers/smac_sampler/sampler.py index 964748d9..77584ac0 100644 --- a/package/samplers/smac_sampler/sampler.py +++ b/package/samplers/smac_sampler/sampler.py @@ -99,6 +99,7 @@ def __init__( self, search_space: dict[str, BaseDistribution], n_trials: int = 100, + seed: int = -1, *, surrogate_model_type: str = "rf", acq_func_type: str = "ei_log", @@ -113,7 +114,7 @@ def __init__( ) -> None: super().__init__(search_space) self._cs, self._hp_scale_value = self._convert_to_config_space_design_space(search_space) - scenario = Scenario(configspace=self._cs, deterministic=True, n_trials=n_trials) + scenario = Scenario(configspace=self._cs, deterministic=True, n_trials=n_trials, seed=seed) surrogate_model = self._get_surrogate_model( scenario, surrogate_model_type, From 2733097ff358b70c872008d9fac7c02756919106 Mon Sep 17 00:00:00 2001 From: nabenabe0928 Date: Tue, 3 Dec 2024 21:37:09 +0100 Subject: [PATCH 34/34] Add a description of seed --- package/samplers/smac_sampler/sampler.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package/samplers/smac_sampler/sampler.py b/package/samplers/smac_sampler/sampler.py index 77584ac0..8c3f412a 100644 --- a/package/samplers/smac_sampler/sampler.py +++ b/package/samplers/smac_sampler/sampler.py @@ -55,6 +55,9 @@ class SMACSampler(optunahub.samplers.SimpleBaseSampler): initial design. This argument does not have to be precise, but it is better to be exact for better performance. + seed: + Seed for random number generator. + If ``None`` is given, seed is generated randomly. surrogate_model_type: What model to use for the probabilistic model. Either "gp" (Gaussian process), "gp_mcmc" (Gaussian process with MCMC), or "rf" @@ -99,7 +102,7 @@ def __init__( self, search_space: dict[str, BaseDistribution], n_trials: int = 100, - seed: int = -1, + seed: int | None = None, *, surrogate_model_type: str = "rf", acq_func_type: str = "ei_log", @@ -114,7 +117,9 @@ def __init__( ) -> None: super().__init__(search_space) self._cs, self._hp_scale_value = self._convert_to_config_space_design_space(search_space) - scenario = Scenario(configspace=self._cs, deterministic=True, n_trials=n_trials, seed=seed) + scenario = Scenario( + configspace=self._cs, deterministic=True, n_trials=n_trials, seed=seed or -1 + ) surrogate_model = self._get_surrogate_model( scenario, surrogate_model_type,