Skip to content

Commit 74df353

Browse files
authored
Merge pull request #324 from jpfeil/add-fcmaes-sampler
Add fcmaes sampler
2 parents 6a644ab + 8e5859b commit 74df353

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

package/samplers/fcmaes/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Jacob Pfeil
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

package/samplers/fcmaes/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
author: Jacob Pfeil
3+
title: Optuna Wrapper for Fast CMA-ES
4+
description:
5+
tags: [sampler, cmaes, fcmaes]
6+
optuna_versions: [4.5.0]
7+
license: MIT License
8+
---
9+
10+
## Abstract
11+
12+
C++ optimized implementation of CMA-ES using the fcmaes library (https://github.com/dietmarwo/fast-cma-es). To use this package, please install the `fcmaes` library.
13+
14+
## APIs
15+
16+
FastCmaesSampler
17+
18+
## Example
19+
20+
```python
21+
from __future__ import annotations
22+
23+
import numpy as np
24+
import optuna
25+
import optunahub
26+
27+
28+
def SphereIntCOM(x: np.ndarray, z: np.ndarray, c: np.ndarray) -> float:
29+
return sum(x * x) + sum(z * z) + len(c) - sum(c[:, 0])
30+
31+
32+
def objective(trial: optuna.Trial) -> float:
33+
x1 = trial.suggest_float("x1", -5, 5)
34+
x2 = trial.suggest_float("x2", -5, 5)
35+
36+
z1 = trial.suggest_int("z1", -1, 1)
37+
z2 = trial.suggest_int("z2", -2, 2)
38+
39+
c1 = trial.suggest_categorical("c1", [0, 1, 2])
40+
c2 = trial.suggest_categorical("c2", [0, 1, 2])
41+
42+
return SphereIntCOM(
43+
np.array([x1, x2]).reshape(-1, 1),
44+
np.array([z1, z2]).reshape(-1, 1),
45+
np.array([c1, c2]).reshape(-1, 1),
46+
)
47+
48+
49+
module = optunahub.load_module(
50+
package="samplers/fcmaes",
51+
)
52+
53+
study = optuna.create_study(sampler=module.FastCmaesSampler())
54+
study.optimize(objective, n_trials=20)
55+
print(study.best_params)
56+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .sampler import FastCmaesSampler
2+
3+
4+
__all__ = ["FastCmaesSampler"]

package/samplers/fcmaes/example.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
This example is only for sampler.
3+
You can verify your sampler code using this file as well.
4+
Please feel free to remove this file if necessary.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import numpy as np
10+
import optuna
11+
import optunahub
12+
13+
14+
def SphereIntCOM(x: np.ndarray, z: np.ndarray, c: np.ndarray) -> float:
15+
return sum(x * x) + sum(z * z) + len(c) - sum(c[:, 0])
16+
17+
18+
def objective(trial: optuna.Trial) -> float:
19+
x1 = trial.suggest_float("x1", -5, 5)
20+
x2 = trial.suggest_float("x2", -5, 5)
21+
22+
z1 = trial.suggest_int("z1", -1, 1)
23+
z2 = trial.suggest_int("z2", -2, 2)
24+
25+
c1 = trial.suggest_categorical("c1", [0, 1, 2])
26+
c2 = trial.suggest_categorical("c2", [0, 1, 2])
27+
28+
return SphereIntCOM(
29+
np.array([x1, x2]).reshape(-1, 1),
30+
np.array([z1, z2]).reshape(-1, 1),
31+
np.array([c1, c2]).reshape(-1, 1),
32+
)
33+
34+
35+
module = optunahub.load_module(package="samplers/fcmaes")
36+
37+
38+
study = optuna.create_study(sampler=module.FastCmaesSampler(16))
39+
study.optimize(objective, n_trials=20)
40+
print(study.best_params)

package/samplers/fcmaes/sampler.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
from typing import Callable
5+
from typing import Dict
6+
from typing import List
7+
from typing import NamedTuple
8+
from typing import Optional
9+
from uuid import uuid4
10+
11+
import numpy as np
12+
import optuna
13+
from optuna._transform import _SearchSpaceTransform
14+
from optuna.distributions import BaseDistribution
15+
from optuna.samplers import BaseSampler
16+
from optuna.search_space import IntersectionSearchSpace
17+
from optuna.study import Study
18+
from optuna.study import StudyDirection
19+
from optuna.trial import FrozenTrial
20+
from optuna.trial import TrialState
21+
import pandas as pd
22+
from scipy.optimize import Bounds
23+
24+
from fcmaes.cmaescpp import ACMA_C
25+
26+
27+
def estimate_non_zero_parameters(trials: List[FrozenTrial]) -> Optional[np.ndarray]:
28+
threshold = 0.0
29+
30+
values = [
31+
trial.value
32+
for trial in trials
33+
if trial.state == TrialState.COMPLETE
34+
and isinstance(trial.value, float)
35+
and trial.value > 0.0
36+
]
37+
38+
if values:
39+
threshold = np.percentile(values, 75)
40+
41+
params: List[Dict] = [
42+
trial.params
43+
for trial in trials
44+
if isinstance(trial.value, float) and trial.value > threshold
45+
]
46+
47+
df = pd.DataFrame.from_dict(params) # type: ignore
48+
49+
mean = df.mean()
50+
51+
return mean.values # type:ignore
52+
53+
return None
54+
55+
56+
class _AttrKeys(NamedTuple):
57+
optimizer: Callable[[], str]
58+
generation: Callable[[], str]
59+
60+
61+
class FastCmaesSampler(BaseSampler):
62+
def __init__(
63+
self,
64+
popsize: int,
65+
search_space: dict[str, BaseDistribution] | None = None,
66+
seed: int = 42,
67+
):
68+
self.signature = str(uuid4())
69+
self.popsize = popsize
70+
self.search_space = search_space
71+
self.seed = seed
72+
self.optimizer: Optional[ACMA_C] = None
73+
74+
self._intersection_search_space = IntersectionSearchSpace()
75+
76+
self.iterations = 0
77+
78+
self.ask_queue: List[Dict] = []
79+
80+
def _init_optimizer(self, x0: np.ndarray, bounds: Bounds, popsize: int) -> ACMA_C:
81+
return ACMA_C(len(bounds.lb), bounds, x0=x0, popsize=popsize, input_sigma=0.5)
82+
83+
def infer_relative_search_space(
84+
self, study: Study, trial: FrozenTrial
85+
) -> Dict[str, BaseDistribution]:
86+
if self.search_space is not None:
87+
return self.search_space
88+
89+
search_space: Dict[str, BaseDistribution] = {}
90+
for name, distribution in self._intersection_search_space.calculate(study).items():
91+
if distribution.single():
92+
continue
93+
search_space[name] = distribution
94+
95+
return search_space
96+
97+
def sample_relative(
98+
self,
99+
study: Study,
100+
trial: FrozenTrial,
101+
search_space: dict[str, BaseDistribution],
102+
) -> dict[str, Any]:
103+
if search_space == {}:
104+
return {}
105+
106+
if len(self.ask_queue) > 0:
107+
return self.ask_queue.pop()
108+
109+
trans = _SearchSpaceTransform(search_space, transform_0_1=True)
110+
111+
bounds = Bounds(trans.bounds[:, 0].flatten(), trans.bounds[:, 1].flatten()) # type: ignore
112+
113+
completed_trials = self._get_trials(study)
114+
115+
if self.optimizer is None:
116+
self.optimizer = self._init_optimizer(
117+
estimate_non_zero_parameters(study.trials), # type: ignore
118+
bounds,
119+
popsize=self.popsize,
120+
)
121+
122+
solution_trials = self._get_solution_trials(completed_trials, self.iterations)
123+
124+
if len(solution_trials) >= self.popsize:
125+
# Prepare solutions list
126+
solutions: List = []
127+
values: List = []
128+
129+
for t in solution_trials[: self.popsize]:
130+
assert t.value is not None, "completed trials must have a value"
131+
# Convert Optuna's representation to fcmaes.cmaescpp's internal representation.
132+
133+
value = t.value if study.direction == StudyDirection.MINIMIZE else -t.value
134+
135+
solution = trans.transform(t.params)
136+
137+
solutions.append(solution)
138+
values.append(value)
139+
140+
self.optimizer.tell(np.array(values), np.array(solutions))
141+
142+
self.iterations += 1
143+
144+
solution = self.optimizer.ask()
145+
146+
generation_attr_key = self._attr_keys.generation()
147+
148+
study._storage.set_trial_system_attr(trial._trial_id, generation_attr_key, self.iterations)
149+
150+
for row in solution:
151+
self.ask_queue.append(trans.untransform(row))
152+
153+
return self.ask_queue.pop()
154+
155+
def sample_independent(
156+
self,
157+
study: Study,
158+
trial: FrozenTrial,
159+
param_name: str,
160+
param_distribution: BaseDistribution,
161+
) -> Any:
162+
independent_sampler = optuna.samplers.RandomSampler()
163+
return independent_sampler.sample_independent(study, trial, param_name, param_distribution)
164+
165+
def _get_solution_trials(
166+
self, trials: List[FrozenTrial], generation: int
167+
) -> List[FrozenTrial]:
168+
generation_attr_key = self._attr_keys.generation()
169+
return [t for t in trials if generation == t.system_attrs.get(generation_attr_key, -1)]
170+
171+
@property
172+
def _attr_keys(self) -> _AttrKeys:
173+
def optimizer_key_template() -> str:
174+
return self.signature + "optimizer"
175+
176+
def generation_attr_key_template() -> str:
177+
return self.signature + "generation"
178+
179+
return _AttrKeys(
180+
optimizer_key_template,
181+
generation_attr_key_template,
182+
)
183+
184+
def _get_trials(self, study: optuna.Study) -> List[FrozenTrial]:
185+
complete_trials = []
186+
for t in study._get_trials(deepcopy=False, use_cache=True):
187+
if t.state == TrialState.COMPLETE:
188+
complete_trials.append(t)
189+
return complete_trials

0 commit comments

Comments
 (0)