From eb0e5b89c54433239549711d5d870d87d5cb7979 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Thu, 31 Oct 2024 01:35:07 -0700 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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 c6b88b7106a95aa7f388e153a1af395dd699ff96 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Fri, 8 Nov 2024 14:09:02 -0800 Subject: [PATCH 05/14] 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 06/14] 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 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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 12/14] 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 a2102986a50d6243468b949183e203a35e92a7e4 Mon Sep 17 00:00:00 2001 From: Bryon Tjanaka Date: Fri, 22 Nov 2024 01:29:23 -0800 Subject: [PATCH 13/14] 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 14/14] 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 ---