Skip to content

Commit fdfd780

Browse files
committed
update cspace generation
1 parent 451bada commit fdfd780

File tree

4 files changed

+191
-46
lines changed

4 files changed

+191
-46
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ such as custom selection, recombination, or adaptation routines.
147147

148148
#### Tuning <a name="tuning"></a>
149149

150-
To facilitate **automated hyperparameter tuning**, ModularCMAES now provides functionality to **create and manipulate configuration spaces** directly compatible with popular optimization and AutoML tools, such as **SMAC**, **BOHB**, or **Optuna**. This functionality allows users to systematically explore both **algorithmic module combinations** and **numerical hyperparameters** (e.g., population size, learning rates, damping coefficients).
150+
To facilitate **automated hyperparameter tuning**, ModularCMAES now provides functionality to **create and manipulate configuration spaces** directly compatible with popular optimization and AutoML tools, such as **SMAC**, **BOHB**. This functionality allows users to systematically explore both **algorithmic module combinations** and **numerical hyperparameters** (e.g., population size, learning rates, damping coefficients).
151151

152152
#### Configuration Space Generation
153153

@@ -177,7 +177,7 @@ Example:
177177

178178
```python
179179
# Get only the module configuration space (no numeric parameters)
180-
cs_modules = get_configspace(only_modules=True)
180+
cs_modules = get_configspace(add_popsize=False, add_sigma=False, add_learning_rates=False)
181181
```
182182

183183
#### Creating Settings from a Configuration
@@ -191,8 +191,15 @@ from ConfigSpace import Configuration
191191
# Sample or load a configuration
192192
config = cs.sample_configuration()
193193

194+
# Or for defaults
195+
default = cs.default_configuration()
196+
197+
# The config can be edited
198+
config['sampler'] = 'HALTON'
199+
194200
# Convert the configuration to a Settings object
195-
settings = settings_from_config(dim=10, config=config)
201+
# Note that keyword arguments like lb in the next example, can be passed to settings like so
202+
settings = settings_from_config(dim=10, config=config, lb=np.ones(10))
196203
```
197204

198205
The resulting `Settings` object can then be passed directly to the C++ backend:

modcma/c_maes/__init__.py

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
ForbiddenGreaterThanRelation,
1212
UniformIntegerHyperparameter,
1313
NormalFloatHyperparameter,
14-
EqualsCondition
14+
EqualsCondition,
1515
)
1616

1717
from .cmaescpp import (
@@ -38,9 +38,7 @@
3838
) # pyright: ignore[reportMissingModuleSource]
3939

4040

41-
42-
43-
def get_module_options(name: str) -> tuple:
41+
def _get_module_options(name: str) -> tuple:
4442
if not hasattr(Modules, name):
4543
raise NameError(f"Modules has no member {name}")
4644

@@ -51,15 +49,16 @@ def get_module_options(name: str) -> tuple:
5149
module_class = default_value.__class__
5250
if issubclass(module_class, Enum):
5351
other_values = [
54-
x for x in module_class.__members__.values() if x is not default_value
52+
x.name for x in module_class.__members__.values()
53+
if x is not default_value
5554
]
56-
return tuple([default_value] + other_values)
55+
return tuple([default_value.name] + other_values)
5756
raise TypeError(f"{name} has a unparsable type {type(default_value)}")
5857

5958

6059
def get_all_module_options() -> dict:
6160
return {
62-
name: get_module_options(name)
61+
name: _get_module_options(name)
6362
for name in dir(Modules)
6463
if not name.startswith("_")
6564
}
@@ -76,7 +75,7 @@ def _make_numeric_parameter(
7675

7776
if isinstance(default, int):
7877
return UniformIntegerHyperparameter(name, lb, ub, default, log=True)
79-
78+
8079
elif isinstance(default, float):
8180
db = min(default - lb, ub - default)
8281
return NormalFloatHyperparameter(name, default, 0.3 * db, lb, ub)
@@ -87,61 +86,82 @@ def _make_numeric_parameter(
8786
)
8887

8988

90-
def _get_numeric_config(
91-
dim: int,
89+
def get_configspace(
90+
dim: int = None,
91+
add_popsize: bool = True,
92+
add_sigma: bool = True,
93+
add_learning_rates: bool = True
9294
) -> ConfigurationSpace:
93-
cs = ConfigurationSpace(
94-
{
95-
"lambda0": _make_numeric_parameter("lambda0", dim, 1, 50 * dim),
96-
"mu0": _make_numeric_parameter("mu0", dim, 1, 50 * dim),
97-
98-
"sigma0": _make_numeric_parameter("sigma0", dim, 1e-15, 1e15), # TODO: should be based on lb-ub
99-
"cs": _make_numeric_parameter("cs", dim, 0, 1.0),
100-
"cc": _make_numeric_parameter("cc", dim, 0, 1.0),
101-
"cmu": _make_numeric_parameter("cmu", dim, 0, 1.0),
102-
"c1": _make_numeric_parameter("c1", dim, 0, 1.0),
103-
"damps": _make_numeric_parameter("damps", dim, 0, 10.0),
104-
}
105-
)
106-
cs.add(ForbiddenGreaterThanRelation(cs["mu0"], cs["lambda0"]))
107-
108-
return cs
109-
110-
111-
def get_configspace(dim: int = None, only_modules: bool = False) -> ConfigurationSpace:
11295
cspace = ConfigurationSpace()
11396
for name, options in get_all_module_options().items():
11497
cspace.add(Categorical(name, options, default=options[0]))
11598

116-
if only_modules:
117-
return cspace
118-
119-
if dim is None:
99+
if dim is None and (add_popsize or add_sigma or add_learning_rates):
120100
warnings.warn(
121101
"Filling configspace with default numeric values for dim=2, "
122102
"since no dim was provided and only_modules was set to False"
123103
)
124104
dim = 2
125105

126-
cspace.add_configuration_space("", _get_numeric_config(dim), delimiter="")
106+
if add_popsize:
107+
cspace.add(_make_numeric_parameter("lambda0", dim, 1, 50 * dim))
108+
cspace.add(_make_numeric_parameter("mu0", dim, 1, 50 * dim))
109+
cspace.add(ForbiddenGreaterThanRelation(cspace["mu0"], cspace["lambda0"]))
110+
111+
if add_sigma:
112+
cspace.add(_make_numeric_parameter("sigma0", dim, 1e-15, 1e15))
113+
114+
if add_learning_rates:
115+
cspace.add(_make_numeric_parameter("cs", dim, 0, 1.0))
116+
cspace.add(_make_numeric_parameter("cc", dim, 0, 1.0))
117+
cspace.add(_make_numeric_parameter("cmu", dim, 0, 1.0))
118+
cspace.add(_make_numeric_parameter("c1", dim, 0, 1.0))
119+
cspace.add(_make_numeric_parameter("damps", dim, 0, 10.0))
120+
127121
return cspace
128122

129123

130124
def settings_from_config(
131125
dim: int,
132126
config: Configuration,
133-
lb: np.ndarray = None,
134-
ub: np.ndarray = None
127+
**kwargs
135128
) -> Settings:
136129
modules = Modules()
137-
via_setings = {}
130+
via_setings = kwargs
138131
default_config = get_configspace(dim).get_default_configuration()
139132
for name, value in dict(config).items():
140133
if hasattr(modules, name):
134+
attr_class = type(getattr(modules, name))
135+
if issubclass(attr_class, Enum):
136+
value = getattr(attr_class, value)
141137
setattr(modules, name, value)
142138
continue
143139
if default_config[name] != value:
144140
via_setings[name] = value
145-
146-
settings = Settings(dim, modules, lb=lb, ub=ub, **via_setings)
141+
142+
settings = Settings(dim, modules, **via_setings)
147143
return settings
144+
145+
146+
__all__ = (
147+
"settings_from_config",
148+
"get_configspace",
149+
"get_all_module_options",
150+
"Settings",
151+
"Modules",
152+
"constants",
153+
"utils",
154+
"sampling",
155+
"mutation",
156+
"selection",
157+
"parameters",
158+
"bounds",
159+
"restart",
160+
"options",
161+
"repelling",
162+
"Population",
163+
"Parameters",
164+
"ModularCMAES",
165+
"center",
166+
"es",
167+
)

scripts/tuning/run_smac.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import os
2+
import time
3+
import argparse
4+
from itertools import product
5+
from functools import partial
6+
7+
import ioh
8+
import numpy as np
9+
from smac import Scenario
10+
from smac.acquisition.function import PriorAcquisitionFunction
11+
from smac import AlgorithmConfigurationFacade, HyperparameterOptimizationFacade
12+
from ConfigSpace import Configuration
13+
14+
from modcma import c_maes
15+
16+
17+
DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data"))
18+
19+
20+
def calc_aoc(logger, budget, fid, iid, dim):
21+
data = logger.data()
22+
data1 = data["None"][fid][dim][iid][0]
23+
fvals = [x["raw_y_best"] for x in data1.values()]
24+
fvals = np.array(fvals)
25+
if np.isnan(fvals).any():
26+
np.nan_to_num(fvals, copy=False, nan=1e8)
27+
if len(fvals) < budget:
28+
fvals = np.concatenate([fvals, (budget - len(fvals)) * [np.min(fvals)]])
29+
parts = np.log10(np.clip(fvals[:budget], 1e-8, 1e2)) + 8
30+
return np.mean(parts) / 10
31+
32+
33+
def get_bbob_performance(
34+
config: Configuration, instance: str, seed: int = 0, dim: int = 5
35+
):
36+
fid, iid = instance.split(",")
37+
fid = int(fid[1:])
38+
iid = int(iid[:-1])
39+
np.random.seed(seed + iid)
40+
c_maes.utils.set_seed(seed + iid)
41+
BUDGET = dim * 10_000
42+
l3 = ioh.logger.Store(
43+
triggers=[ioh.logger.trigger.ALWAYS], properties=[ioh.logger.property.RAWYBEST]
44+
)
45+
problem = ioh.get_problem(fid, iid, dim)
46+
problem.attach_logger(l3)
47+
48+
settings = c_maes.settings_from_config(
49+
dim,
50+
config,
51+
budget=BUDGET,
52+
target= problem.optimum.y + 1e-9,
53+
ub=problem.bounds.ub,
54+
lb=problem.bounds.lb
55+
)
56+
par = c_maes.Parameters(settings)
57+
58+
try:
59+
cma = c_maes.ModularCMAES(par)
60+
cma.run(problem)
61+
except Exception as e:
62+
print(
63+
f"Found target {problem.state.current_best.y} target, but exception ({e}), so run failed"
64+
)
65+
print(config)
66+
return [np.inf]
67+
68+
auc = calc_aoc(l3, BUDGET, fid, iid, dim)
69+
return [auc]
70+
71+
72+
def run_smac(fid, dim, use_learning_rates):
73+
print(f"Running SMAC with fid={fid}, lr={use_learning_rates} and d={dim}")
74+
cma_cs = c_maes.get_configspace(dim, add_learning_rates=use_learning_rates)
75+
76+
iids = range(1, 51)
77+
if fid == "all":
78+
fids = range(1, 25)
79+
max_budget = 240
80+
else:
81+
fids = [fid]
82+
max_budget = 50
83+
84+
args = list(product(fids, iids))
85+
np.random.shuffle(args)
86+
inst_feats = {str(arg): [arg[0]] for idx, arg in enumerate(args)}
87+
scenario = Scenario(
88+
cma_cs,
89+
name=str(int(time.time())) + "-" + "CMA",
90+
deterministic=False,
91+
n_trials=100_000,
92+
instances=args,
93+
instance_features=inst_feats,
94+
output_directory=os.path.join(
95+
DATA_DIR, f"BBOB_F{fid}_{dim}D_LR{use_learning_rates}"
96+
),
97+
n_workers=16,
98+
)
99+
pf = PriorAcquisitionFunction(
100+
acquisition_function=AlgorithmConfigurationFacade.get_acquisition_function(
101+
scenario
102+
),
103+
decay_beta=scenario.n_trials / 10,
104+
)
105+
eval_func = partial(get_bbob_performance, dim=dim)
106+
intensifier = HyperparameterOptimizationFacade.get_intensifier(
107+
scenario, max_config_calls=max_budget
108+
)
109+
smac = HyperparameterOptimizationFacade(
110+
scenario, eval_func, acquisition_function=pf, intensifier=intensifier
111+
)
112+
smac.optimize()
113+
114+
115+
if __name__ == "__main__":
116+
parser = argparse.ArgumentParser()
117+
parser.add_argument("--fid", type=int, default=1)
118+
parser.add_argument("--dim", type=int, default=5)
119+
parser.add_argument("--use_learning_rates", action="store_true")
120+
args = parser.parse_args()
121+
run_smac(args.fid, args.dim, args.use_learning_rates)

tests/test_c_tuning.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ def test_module_options(self):
1515
self.assertEqual(options, options | {"sample_sigma": (False, True)})
1616
self.assertEqual(options, options | {"repelling_restart": (False, True)})
1717

18-
def test_numeric_options(self):
19-
options = c_maes._get_numeric_config(2)
20-
self.assertIsInstance(options, ConfigurationSpace)
2118

2219
def test_configspace(self):
2320
cspace = c_maes.get_configspace(2)
@@ -30,7 +27,7 @@ def test_configspace(self):
3027

3128
changed = deepcopy(default)
3229
changed['cc'] = 0.1
33-
changed['sampler'] = c_maes.options.BaseSampler.HALTON
30+
changed['sampler'] = "HALTON"
3431
settings_changed = c_maes.settings_from_config(2, changed)
3532
self.assertEqual(settings_changed.cc, 0.1)
3633
self.assertEqual(settings_changed.modules.sampler, c_maes.options.BaseSampler.HALTON)

0 commit comments

Comments
 (0)