hone-python 0.10.0__tar.gz → 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hone_python-0.10.0 → hone_python-0.11.0}/PKG-INFO +45 -2
- {hone_python-0.10.0 → hone_python-0.11.0}/README.md +44 -1
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/__init__.py +2 -1
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/benchmarks/__init__.py +1 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/benchmarks/__main__.py +6 -0
- hone_python-0.11.0/hone/benchmarks/conditional.py +109 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/benchmarks/ml.py +69 -3
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/config/genetic_optimizer_config.py +3 -2
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/objective_function/objective_function.py +15 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/base.py +30 -9
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/bayesian.py +38 -2
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/cmaes.py +3 -1
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/genetic/optimizer.py +47 -13
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/genetic/variation.py +10 -3
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/random_search.py +1 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/parameters/__init__.py +1 -0
- hone_python-0.11.0/hone/parameters/conditions.py +147 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/parameters/constraints.py +22 -4
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/parameters/parameters.py +204 -43
- {hone_python-0.10.0 → hone_python-0.11.0}/pyproject.toml +1 -1
- {hone_python-0.10.0 → hone_python-0.11.0}/LICENSE +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/benchmarks/constrained.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/benchmarks/problems.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/benchmarks/runner.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/config/__init__.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/config/bayesian_config.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/config/cmaes_config.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/config/optimizer_config.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/objective_function/__init__.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/__init__.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/_gp.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/_pool.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/choice.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/genetic/__init__.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/optimizers/genetic/selection.py +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/py.typed +0 -0
- {hone_python-0.10.0 → hone_python-0.11.0}/hone/result.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hone-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: A python-based optimization engine/library.
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE
|
|
@@ -207,6 +207,49 @@ Constraints should be quick to check: hone checks them for every candidate it co
|
|
|
207
207
|
|
|
208
208
|
Starting points must satisfy the constraints. If the constraints leave nothing to search, or a region too small to find in 100,000 random draws, the optimizer raises `InfeasibleSearchSpace` when it's created, before any evaluation. Each optimizer keeps to constraints in its own way: random search and Bayesian optimization only consider points that satisfy them, CMA-ES draws a candidate again while it breaks one, and the genetic optimizer moves such a child back toward its fitter parent until it is feasible, which tends to land it near the boundary, where constrained optima usually are. On the benchmark's constrained problems the choice between optimizers follows the same pattern as without constraints; see [benchmarks/README.md](benchmarks/README.md#constrained-problems). [examples/pressure_vessel.py](examples/pressure_vessel.py) designs a pressure vessel of least cost under three constraints and finds the best published design. [examples/maintenance_schedule.py](examples/maintenance_schedule.py) chooses a maintenance schedule for a production line, scored by a noisy simulation of a year, with a constraint on technician hours and a function constraint that depends on the chosen shift; it halves the expected yearly cost of a sensible schedule chosen by hand.
|
|
209
209
|
|
|
210
|
+
### Conditional parameters
|
|
211
|
+
|
|
212
|
+
Some settings only exist when another takes certain values: a kernel's settings only for the model that has kernels, a third layer's size only with three layers or more. Write each condition as `test >> parameters`, read "then", and pass them to `SearchSpace`:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from hone import ChoiceParameter, RangeParameter, SearchSpace
|
|
216
|
+
|
|
217
|
+
model = ChoiceParameter("model", ["svm", "forest"])
|
|
218
|
+
kernel = ChoiceParameter("kernel", ["linear", "rbf"])
|
|
219
|
+
C = RangeParameter("C", 1e-3, 1e3, log=True)
|
|
220
|
+
gamma = RangeParameter("gamma", 1e-5, 1, log=True)
|
|
221
|
+
trees = RangeParameter("trees", 10, 500, integer=True)
|
|
222
|
+
|
|
223
|
+
search_space = SearchSpace(
|
|
224
|
+
[model, kernel, C, gamma, trees],
|
|
225
|
+
conditions=[
|
|
226
|
+
model["svm"] >> [kernel, C],
|
|
227
|
+
kernel["rbf"] >> gamma, # only with an svm too, since kernel is
|
|
228
|
+
model["forest"] >> trees,
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def train(model, kernel=None, C=None, gamma=None, trees=None):
|
|
233
|
+
...
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The left of `>>` is a test: a choice parameter indexed by the values that switch the parameters on, such as `model["svm"]` or `optimizer["sgd", "nesterov"]`, or a comparison of range parameters in brackets, such as `(layers >= 3) >> units_3`. The brackets are needed, since Python reads `layers >= 3 >> units_3` as `layers >= (3 >> units_3)`; hone raises an error saying so. The right is a parameter or a list of them, and each parameter can have one condition. Conditions nest: a parameter whose test uses a parameter that is switched off is off too.
|
|
237
|
+
|
|
238
|
+
A parameter that is switched off isn't part of the point. The objective isn't passed it, so give it a default, which hone checks before the run starts; results, progress and `best_params` leave it out; and a starting point must leave it out too. Two points that differ only in a parameter switched off are the same point, so they are averaged together with `noise=True` and the genetic optimizer doesn't count one as new.
|
|
239
|
+
|
|
240
|
+
A constraint applies only while every parameter it uses is switched on, and `test >> constraint` makes a constraint apply only while the test holds:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
depth = RangeParameter("depth", 2, 20, integer=True)
|
|
244
|
+
search_space = SearchSpace(
|
|
245
|
+
[model, trees, depth],
|
|
246
|
+
constraints=[model["forest"] >> (trees * depth <= 2000)],
|
|
247
|
+
conditions=[model["forest"] >> [trees, depth]],
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Every optimizer supports conditions; CMA-ES only conditions on range parameters, since it doesn't search choices. Bayesian optimization gains the most: a parameter switched off looks the same to its model whatever value it holds, so values that don't matter add no noise. On the benchmark's conditional problems its error is ×0.0004 that of searching every parameter all the time, at 10 evaluations per dimension. With conditions the genetic optimizer breeds each branch within its own niche by default (`niche_share` of 0.5, below), so a wide, easy branch doesn't crowd out a narrow, better one; see [benchmarks/README.md](benchmarks/README.md#conditional-problems). [examples/choose_a_model.py](examples/choose_a_model.py) chooses a classifier, among an SVM with three kernels, a random forest and logistic regression, together with its settings.
|
|
252
|
+
|
|
210
253
|
### Configuration
|
|
211
254
|
|
|
212
255
|
`GeneticOptimizerConfig` validates its values when created. It can also be loaded from a JSON or YAML file:
|
|
@@ -219,7 +262,7 @@ Set `seed` to make a run repeatable. `max_evaluations` sets a budget: the run st
|
|
|
219
262
|
|
|
220
263
|
Parents are chosen by tournament: `tournament_size` (default 4) individuals are drawn and the fittest wins.
|
|
221
264
|
|
|
222
|
-
When a choice parameter's best option is found late, the genetic optimizer can settle on another one: once most of the population uses one option, a child with a different option inherits numbers tuned for the wrong one and loses. `niche_share=0.5` breeds half of each generation's children within niches of parents that share their choices, so every option keeps tuning its own numbers. On material-selection-10d with 1,000 evaluations per dimension it found the right material in 10 of 10 runs, against 6 of 10 without. It costs some progress on small budgets, so it's off by default.
|
|
265
|
+
When a choice parameter's best option is found late, the genetic optimizer can settle on another one: once most of the population uses one option, a child with a different option inherits numbers tuned for the wrong one and loses. `niche_share=0.5` breeds half of each generation's children within niches of parents that share their choices, so every option keeps tuning its own numbers. On material-selection-10d with 1,000 evaluations per dimension it found the right material in 10 of 10 runs, against 6 of 10 without. It costs some progress on small budgets, so it's off by default, except in a search space with conditions: there each branch is a niche, and 0.5 is the default, since a branch with a better optimum is often harder to find.
|
|
223
266
|
|
|
224
267
|
Each value of a new individual mutates with probability `mutation_rate`: by default 1 over the number of variables, and at least 0.1, so in two variables half of them mutate. A range value moves by a random step whose typical size is a fraction of the width of its range. A choice switches to another option, and a fixed value never changes.
|
|
225
268
|
|
|
@@ -186,6 +186,49 @@ Constraints should be quick to check: hone checks them for every candidate it co
|
|
|
186
186
|
|
|
187
187
|
Starting points must satisfy the constraints. If the constraints leave nothing to search, or a region too small to find in 100,000 random draws, the optimizer raises `InfeasibleSearchSpace` when it's created, before any evaluation. Each optimizer keeps to constraints in its own way: random search and Bayesian optimization only consider points that satisfy them, CMA-ES draws a candidate again while it breaks one, and the genetic optimizer moves such a child back toward its fitter parent until it is feasible, which tends to land it near the boundary, where constrained optima usually are. On the benchmark's constrained problems the choice between optimizers follows the same pattern as without constraints; see [benchmarks/README.md](benchmarks/README.md#constrained-problems). [examples/pressure_vessel.py](examples/pressure_vessel.py) designs a pressure vessel of least cost under three constraints and finds the best published design. [examples/maintenance_schedule.py](examples/maintenance_schedule.py) chooses a maintenance schedule for a production line, scored by a noisy simulation of a year, with a constraint on technician hours and a function constraint that depends on the chosen shift; it halves the expected yearly cost of a sensible schedule chosen by hand.
|
|
188
188
|
|
|
189
|
+
### Conditional parameters
|
|
190
|
+
|
|
191
|
+
Some settings only exist when another takes certain values: a kernel's settings only for the model that has kernels, a third layer's size only with three layers or more. Write each condition as `test >> parameters`, read "then", and pass them to `SearchSpace`:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from hone import ChoiceParameter, RangeParameter, SearchSpace
|
|
195
|
+
|
|
196
|
+
model = ChoiceParameter("model", ["svm", "forest"])
|
|
197
|
+
kernel = ChoiceParameter("kernel", ["linear", "rbf"])
|
|
198
|
+
C = RangeParameter("C", 1e-3, 1e3, log=True)
|
|
199
|
+
gamma = RangeParameter("gamma", 1e-5, 1, log=True)
|
|
200
|
+
trees = RangeParameter("trees", 10, 500, integer=True)
|
|
201
|
+
|
|
202
|
+
search_space = SearchSpace(
|
|
203
|
+
[model, kernel, C, gamma, trees],
|
|
204
|
+
conditions=[
|
|
205
|
+
model["svm"] >> [kernel, C],
|
|
206
|
+
kernel["rbf"] >> gamma, # only with an svm too, since kernel is
|
|
207
|
+
model["forest"] >> trees,
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def train(model, kernel=None, C=None, gamma=None, trees=None):
|
|
212
|
+
...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The left of `>>` is a test: a choice parameter indexed by the values that switch the parameters on, such as `model["svm"]` or `optimizer["sgd", "nesterov"]`, or a comparison of range parameters in brackets, such as `(layers >= 3) >> units_3`. The brackets are needed, since Python reads `layers >= 3 >> units_3` as `layers >= (3 >> units_3)`; hone raises an error saying so. The right is a parameter or a list of them, and each parameter can have one condition. Conditions nest: a parameter whose test uses a parameter that is switched off is off too.
|
|
216
|
+
|
|
217
|
+
A parameter that is switched off isn't part of the point. The objective isn't passed it, so give it a default, which hone checks before the run starts; results, progress and `best_params` leave it out; and a starting point must leave it out too. Two points that differ only in a parameter switched off are the same point, so they are averaged together with `noise=True` and the genetic optimizer doesn't count one as new.
|
|
218
|
+
|
|
219
|
+
A constraint applies only while every parameter it uses is switched on, and `test >> constraint` makes a constraint apply only while the test holds:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
depth = RangeParameter("depth", 2, 20, integer=True)
|
|
223
|
+
search_space = SearchSpace(
|
|
224
|
+
[model, trees, depth],
|
|
225
|
+
constraints=[model["forest"] >> (trees * depth <= 2000)],
|
|
226
|
+
conditions=[model["forest"] >> [trees, depth]],
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Every optimizer supports conditions; CMA-ES only conditions on range parameters, since it doesn't search choices. Bayesian optimization gains the most: a parameter switched off looks the same to its model whatever value it holds, so values that don't matter add no noise. On the benchmark's conditional problems its error is ×0.0004 that of searching every parameter all the time, at 10 evaluations per dimension. With conditions the genetic optimizer breeds each branch within its own niche by default (`niche_share` of 0.5, below), so a wide, easy branch doesn't crowd out a narrow, better one; see [benchmarks/README.md](benchmarks/README.md#conditional-problems). [examples/choose_a_model.py](examples/choose_a_model.py) chooses a classifier, among an SVM with three kernels, a random forest and logistic regression, together with its settings.
|
|
231
|
+
|
|
189
232
|
### Configuration
|
|
190
233
|
|
|
191
234
|
`GeneticOptimizerConfig` validates its values when created. It can also be loaded from a JSON or YAML file:
|
|
@@ -198,7 +241,7 @@ Set `seed` to make a run repeatable. `max_evaluations` sets a budget: the run st
|
|
|
198
241
|
|
|
199
242
|
Parents are chosen by tournament: `tournament_size` (default 4) individuals are drawn and the fittest wins.
|
|
200
243
|
|
|
201
|
-
When a choice parameter's best option is found late, the genetic optimizer can settle on another one: once most of the population uses one option, a child with a different option inherits numbers tuned for the wrong one and loses. `niche_share=0.5` breeds half of each generation's children within niches of parents that share their choices, so every option keeps tuning its own numbers. On material-selection-10d with 1,000 evaluations per dimension it found the right material in 10 of 10 runs, against 6 of 10 without. It costs some progress on small budgets, so it's off by default.
|
|
244
|
+
When a choice parameter's best option is found late, the genetic optimizer can settle on another one: once most of the population uses one option, a child with a different option inherits numbers tuned for the wrong one and loses. `niche_share=0.5` breeds half of each generation's children within niches of parents that share their choices, so every option keeps tuning its own numbers. On material-selection-10d with 1,000 evaluations per dimension it found the right material in 10 of 10 runs, against 6 of 10 without. It costs some progress on small budgets, so it's off by default, except in a search space with conditions: there each branch is a niche, and 0.5 is the default, since a branch with a better optimum is often harder to find.
|
|
202
245
|
|
|
203
246
|
Each value of a new individual mutates with probability `mutation_rate`: by default 1 over the number of variables, and at least 0.1, so in two variables half of them mutate. A range value moves by a random step whose typical size is a fraction of the width of its range. A choice switches to another option, and a fixed value never changes.
|
|
204
247
|
|
|
@@ -10,7 +10,7 @@ from hone.optimizers import (
|
|
|
10
10
|
UnsupportedSearchSpace,
|
|
11
11
|
choose_optimizer,
|
|
12
12
|
)
|
|
13
|
-
from hone.parameters import ChoiceParameter, Constraint, FixedParameter, InfeasibleSearchSpace, RangeParameter, SearchSpace
|
|
13
|
+
from hone.parameters import ChoiceParameter, Condition, Constraint, FixedParameter, InfeasibleSearchSpace, RangeParameter, SearchSpace
|
|
14
14
|
from hone.result import GenerationStats, Progress, Result, RunInfo, print_progress
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
@@ -19,6 +19,7 @@ __all__ = [
|
|
|
19
19
|
"CMAES",
|
|
20
20
|
"CMAESConfig",
|
|
21
21
|
"ChoiceParameter",
|
|
22
|
+
"Condition",
|
|
22
23
|
"Constraint",
|
|
23
24
|
"EvaluationFailed",
|
|
24
25
|
"FixedParameter",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Benchmark problems and a runner for comparing optimizers. Run `python -m hone.benchmarks --help`."""
|
|
2
|
+
from .conditional import conditional_problems
|
|
2
3
|
from .constrained import constrained_problems
|
|
3
4
|
from .ml import ml_problems
|
|
4
5
|
from .problems import Problem, noisy_problems, quick_problems, standard_problems
|
|
@@ -6,6 +6,7 @@ from collections.abc import Sequence
|
|
|
6
6
|
from importlib.metadata import version
|
|
7
7
|
|
|
8
8
|
from . import ml
|
|
9
|
+
from .conditional import conditional_problems
|
|
9
10
|
from .constrained import constrained_problems
|
|
10
11
|
from .problems import noisy_problems, quick_problems, standard_problems
|
|
11
12
|
from .runner import BUDGETS, load_summaries, regressions, run_benchmark, save_summaries, summarize, to_markdown
|
|
@@ -28,6 +29,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
28
29
|
suite.add_argument("--ml", action="store_true", help="run the machine learning tuning problems instead (needs scikit-learn)")
|
|
29
30
|
suite.add_argument("--noisy", action="store_true", help="run the problems with noise added to every evaluation instead")
|
|
30
31
|
suite.add_argument("--constrained", action="store_true", help="run the problems with constraints instead")
|
|
32
|
+
suite.add_argument(
|
|
33
|
+
"--conditional", action="store_true", help="run the problems with conditional parameters, and their flat versions, instead"
|
|
34
|
+
)
|
|
31
35
|
parser.add_argument("--seeds", type=int, help="number of seeds per problem (default 10, or 5 with --quick)")
|
|
32
36
|
parser.add_argument(
|
|
33
37
|
"--budgets",
|
|
@@ -53,6 +57,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
53
57
|
problems, default_budgets = noisy_problems(), NOISY_BUDGETS
|
|
54
58
|
elif args.constrained:
|
|
55
59
|
problems, default_budgets = constrained_problems(), BUDGETS
|
|
60
|
+
elif args.conditional:
|
|
61
|
+
problems, default_budgets = conditional_problems(), BUDGETS
|
|
56
62
|
else:
|
|
57
63
|
problems, default_budgets = standard_problems(), BUDGETS
|
|
58
64
|
seeds = args.seeds or (5 if args.quick else 10)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Conditional test problems, each in two versions: with conditions, and with every parameter switched on.
|
|
2
|
+
|
|
3
|
+
- branches-3x2d and branches-3x5d: a choice of three branches, each a function of its own
|
|
4
|
+
numbers. The best branch is a steep, narrow bowl reaching 0; the decoys are a wide, shallow
|
|
5
|
+
bowl whose floor is 1 and a shallow Rastrigin grid whose floor is 0.5. Random points score
|
|
6
|
+
better in the decoys, so an optimizer has to look past them.
|
|
7
|
+
- nested-9: a choice of model; one model has a choice of kernel, and one kernel has numbers of
|
|
8
|
+
its own, so the best point is two conditions deep. The other model is a wide, shallow decoy
|
|
9
|
+
whose floor is 0.25.
|
|
10
|
+
- layers-7: a layer count from 1 to 5 whose larger values switch on more numbers, as a
|
|
11
|
+
network's units per layer would be. Three layers is best. Its conditions test a range, so
|
|
12
|
+
CMA-ES can search it.
|
|
13
|
+
|
|
14
|
+
The "-flat" version of each has the same parameters, function and optimum, with no conditions:
|
|
15
|
+
every parameter is always on and the function ignores those it doesn't use, which was the way
|
|
16
|
+
to search such a space before conditions. Comparing the two shows what conditions are worth.
|
|
17
|
+
"""
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from hone.parameters import ChoiceParameter, Condition, Parameter, RangeParameter, SearchSpace
|
|
23
|
+
|
|
24
|
+
from .problems import BOUND, Problem, _shift, rastrigin, sphere
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _problem(name: str, function: Callable[..., float], parameters: list[Parameter], conditions: list[Condition]) -> list[Problem]:
|
|
28
|
+
function.__name__ = name.replace("-", "_")
|
|
29
|
+
return [
|
|
30
|
+
Problem(
|
|
31
|
+
name=name,
|
|
32
|
+
function=function,
|
|
33
|
+
search_space=SearchSpace(parameters, conditions=conditions),
|
|
34
|
+
optimum=0.0,
|
|
35
|
+
tags=frozenset({"conditional"}),
|
|
36
|
+
),
|
|
37
|
+
Problem(
|
|
38
|
+
name=f"{name}-flat",
|
|
39
|
+
function=function,
|
|
40
|
+
search_space=SearchSpace(parameters),
|
|
41
|
+
optimum=0.0,
|
|
42
|
+
tags=frozenset({"conditional", "flat"}),
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _numbers(prefix: str, count: int) -> list[RangeParameter]:
|
|
48
|
+
return [RangeParameter(f"{prefix}{i}", -BOUND, BOUND) for i in range(count)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _vector(params: dict[str, Any], numbers: list[RangeParameter]) -> np.ndarray:
|
|
52
|
+
return np.array([params[p.name] for p in numbers], dtype=float)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def branches(dimensions: int) -> list[Problem]:
|
|
56
|
+
name = f"branches-3x{dimensions}d"
|
|
57
|
+
branch = ChoiceParameter("branch", ["narrow", "wide", "grid"])
|
|
58
|
+
numbers = {b: _numbers(f"{b}_", dimensions) for b in branch.choices}
|
|
59
|
+
shifts = {b: _shift(f"{name}/{b}", dimensions) for b in branch.choices}
|
|
60
|
+
# (function, scale, floor) for each branch
|
|
61
|
+
bases = {"narrow": (sphere, 4.0, 0.0), "wide": (sphere, 0.05, 1.0), "grid": (rastrigin, 0.1, 0.5)}
|
|
62
|
+
|
|
63
|
+
def function(branch: str, **params: Any) -> float:
|
|
64
|
+
base, scale, floor = bases[branch]
|
|
65
|
+
return floor + scale * float(base(_vector(params, numbers[branch]) - shifts[branch]))
|
|
66
|
+
|
|
67
|
+
parameters: list[Parameter] = [branch, *(p for b in branch.choices for p in numbers[b])]
|
|
68
|
+
return _problem(name, function, parameters, [branch[b] >> numbers[b] for b in branch.choices])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def nested() -> list[Problem]:
|
|
72
|
+
name = "nested-9"
|
|
73
|
+
model = ChoiceParameter("model", ["kernel_machine", "forest"])
|
|
74
|
+
kernel = ChoiceParameter("kernel", ["linear", "rbf"])
|
|
75
|
+
shared, width, forest = _numbers("s", 2), _numbers("g", 2), _numbers("f", 3)
|
|
76
|
+
centres = {key: _shift(f"{name}/{key}", len(group)) for key, group in [("s", shared), ("g", width), ("f", forest)]}
|
|
77
|
+
|
|
78
|
+
def function(model: str, kernel: str | None = None, **params: Any) -> float:
|
|
79
|
+
if model == "forest":
|
|
80
|
+
return 0.25 + 0.05 * float(sphere(_vector(params, forest) - centres["f"]))
|
|
81
|
+
value = float(sphere(_vector(params, shared) - centres["s"]))
|
|
82
|
+
if kernel == "linear":
|
|
83
|
+
return 0.5 + value
|
|
84
|
+
return value + 4 * float(sphere(_vector(params, width) - centres["g"]))
|
|
85
|
+
|
|
86
|
+
parameters: list[Parameter] = [model, kernel, *shared, *width, *forest]
|
|
87
|
+
conditions = [model["kernel_machine"] >> [kernel, *shared], kernel["rbf"] >> width, model["forest"] >> forest]
|
|
88
|
+
return _problem(name, function, parameters, conditions)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def layers() -> list[Problem]:
|
|
92
|
+
name = "layers-7"
|
|
93
|
+
count = RangeParameter("layers", 1, 5, integer=True)
|
|
94
|
+
rate = RangeParameter("rate", -BOUND, BOUND)
|
|
95
|
+
units = _numbers("units_", 5)
|
|
96
|
+
centre = _shift(f"{name}/units", 5)
|
|
97
|
+
best_rate = float(_shift(f"{name}/rate", 1)[0])
|
|
98
|
+
|
|
99
|
+
def function(layers: int, rate: float, **params: Any) -> float:
|
|
100
|
+
used = units[:layers]
|
|
101
|
+
return 0.5 * abs(layers - 3) + float(sphere(_vector(params, used) - centre[:layers])) + (rate - best_rate) ** 2
|
|
102
|
+
|
|
103
|
+
parameters: list[Parameter] = [count, rate, *units]
|
|
104
|
+
return _problem(name, function, parameters, [(count >= i + 1) >> units[i] for i in range(1, 5)])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def conditional_problems() -> list[Problem]:
|
|
108
|
+
"""Each conditional problem, followed by its flat version."""
|
|
109
|
+
return [*branches(2), *branches(5), *nested(), *layers()]
|
|
@@ -16,7 +16,7 @@ from typing import Callable
|
|
|
16
16
|
|
|
17
17
|
from threadpoolctl import threadpool_limits
|
|
18
18
|
|
|
19
|
-
from hone.parameters import ChoiceParameter, RangeParameter, SearchSpace
|
|
19
|
+
from hone.parameters import ChoiceParameter, Condition, Parameter, RangeParameter, SearchSpace
|
|
20
20
|
|
|
21
21
|
from .problems import Problem
|
|
22
22
|
|
|
@@ -92,11 +92,56 @@ def random_forest_wine(max_depth: int, min_samples_leaf: int, max_features: floa
|
|
|
92
92
|
return _cross_validate(model, "wine", classification=True)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def model_selection_breast_cancer(
|
|
96
|
+
model: str,
|
|
97
|
+
C: float | None = None,
|
|
98
|
+
gamma: float | None = None,
|
|
99
|
+
max_depth: int | None = None,
|
|
100
|
+
min_samples_leaf: int | None = None,
|
|
101
|
+
max_features: float | None = None,
|
|
102
|
+
n_neighbors: int | None = None,
|
|
103
|
+
weights: str | None = None,
|
|
104
|
+
) -> float:
|
|
105
|
+
"""One of three classifiers, and its settings, on the 569 breast tumour samples svm-breast-cancer uses.
|
|
106
|
+
|
|
107
|
+
A support vector classifier with an RBF kernel, a 20-tree random forest, or k nearest
|
|
108
|
+
neighbours. With conditions, only the chosen model's settings are searched.
|
|
109
|
+
"""
|
|
110
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
111
|
+
from sklearn.neighbors import KNeighborsClassifier
|
|
112
|
+
from sklearn.pipeline import make_pipeline
|
|
113
|
+
from sklearn.preprocessing import StandardScaler
|
|
114
|
+
from sklearn.svm import SVC
|
|
115
|
+
|
|
116
|
+
if model == "svm":
|
|
117
|
+
estimator = make_pipeline(StandardScaler(), SVC(C=C, gamma=gamma))
|
|
118
|
+
elif model == "forest":
|
|
119
|
+
estimator = RandomForestClassifier(
|
|
120
|
+
n_estimators=20, max_depth=max_depth, min_samples_leaf=min_samples_leaf, max_features=max_features, random_state=0, n_jobs=1
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
estimator = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=n_neighbors, weights=weights))
|
|
124
|
+
return _cross_validate(estimator, "breast_cancer", classification=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_MODEL = ChoiceParameter("model", ["svm", "forest", "knn"])
|
|
128
|
+
_SVM = [RangeParameter("C", 1e-3, 1e3, log=True), RangeParameter("gamma", 1e-5, 10, log=True)]
|
|
129
|
+
_FOREST = [
|
|
130
|
+
RangeParameter("max_depth", 1, 12, integer=True),
|
|
131
|
+
RangeParameter("min_samples_leaf", 1, 20, integer=True),
|
|
132
|
+
RangeParameter("max_features", 0.05, 1.0),
|
|
133
|
+
]
|
|
134
|
+
_KNN: list[Parameter] = [RangeParameter("n_neighbors", 1, 50, integer=True), ChoiceParameter("weights", ["uniform", "distance"])]
|
|
135
|
+
MODEL_SELECTION: list[Parameter] = [_MODEL, *_SVM, *_FOREST, *_KNN]
|
|
136
|
+
MODEL_CONDITIONS: list[Condition] = [_MODEL["svm"] >> _SVM, _MODEL["forest"] >> _FOREST, _MODEL["knn"] >> _KNN]
|
|
137
|
+
|
|
95
138
|
# The best value of each problem found by REFERENCE_SEARCH.
|
|
96
139
|
OPTIMA = {
|
|
97
140
|
"svm-breast-cancer": 0.014066138798323302, # accuracy 98.6%, found by CMA-ES
|
|
98
141
|
"kernel-ridge-diabetes": 0.4961748264794844, # R² 0.504, found by Bayesian optimization
|
|
99
142
|
"random-forest-wine": 0.005555555555555536, # accuracy 99.4%, found by Bayesian optimization
|
|
143
|
+
# Every reference run chose the support vector classifier, and the best matched svm-breast-cancer's.
|
|
144
|
+
"model-selection-breast-cancer": 0.014066138798323302,
|
|
100
145
|
}
|
|
101
146
|
|
|
102
147
|
SPACES: dict[str, tuple[Callable[..., float], list]] = {
|
|
@@ -125,9 +170,30 @@ SPACES: dict[str, tuple[Callable[..., float], list]] = {
|
|
|
125
170
|
|
|
126
171
|
|
|
127
172
|
def ml_problems() -> list[Problem]:
|
|
128
|
-
"""The machine learning tuning problems. Needs scikit-learn.
|
|
173
|
+
"""The machine learning tuning problems. Needs scikit-learn.
|
|
174
|
+
|
|
175
|
+
model-selection-breast-cancer comes twice: with conditions, and as model-selection-breast-cancer-flat
|
|
176
|
+
with every setting always searched, as before conditions, to show what they are worth.
|
|
177
|
+
"""
|
|
129
178
|
_require_sklearn()
|
|
130
|
-
|
|
179
|
+
problems = [
|
|
131
180
|
Problem(name=name, function=function, search_space=SearchSpace(parameters), optimum=OPTIMA[name], tags=frozenset({"ml"}))
|
|
132
181
|
for name, (function, parameters) in SPACES.items()
|
|
133
182
|
]
|
|
183
|
+
optimum = OPTIMA["model-selection-breast-cancer"]
|
|
184
|
+
return problems + [
|
|
185
|
+
Problem(
|
|
186
|
+
name="model-selection-breast-cancer",
|
|
187
|
+
function=model_selection_breast_cancer,
|
|
188
|
+
search_space=SearchSpace(MODEL_SELECTION, conditions=MODEL_CONDITIONS),
|
|
189
|
+
optimum=optimum,
|
|
190
|
+
tags=frozenset({"ml", "conditional"}),
|
|
191
|
+
),
|
|
192
|
+
Problem(
|
|
193
|
+
name="model-selection-breast-cancer-flat",
|
|
194
|
+
function=model_selection_breast_cancer,
|
|
195
|
+
search_space=SearchSpace(MODEL_SELECTION),
|
|
196
|
+
optimum=optimum,
|
|
197
|
+
tags=frozenset({"ml", "conditional", "flat"}),
|
|
198
|
+
),
|
|
199
|
+
]
|
|
@@ -48,8 +48,9 @@ class GeneticOptimizerConfig(OptimizerConfig):
|
|
|
48
48
|
tournament_p: float = Field(default=1.0, ge=0, le=1) # chance a tournament picks its best rather than its worst
|
|
49
49
|
# Share of children bred within niches of parents with the same choice parameter values, so
|
|
50
50
|
# each option keeps tuning its own numbers. Helps problems where the best choice is found
|
|
51
|
-
# late, on large budgets; costs on small ones. 0 turns niches off.
|
|
52
|
-
|
|
51
|
+
# late, on large budgets; costs on small ones. 0 turns niches off. None, the default, means
|
|
52
|
+
# 0.5 for a search space with conditions, where each branch is a niche, and 0 otherwise.
|
|
53
|
+
niche_share: float | None = Field(default=None, ge=0, lt=1)
|
|
53
54
|
|
|
54
55
|
@model_validator(mode="before")
|
|
55
56
|
@classmethod
|
|
@@ -43,5 +43,20 @@ class ObjectiveFunction(BaseModel):
|
|
|
43
43
|
)
|
|
44
44
|
if unknown:
|
|
45
45
|
raise ValueError(f"{self.name}() does not accept parameters: {', '.join(unknown)}")
|
|
46
|
+
# A parameter a condition switches off isn't passed, so the function needs a default for it.
|
|
47
|
+
no_default = sorted(
|
|
48
|
+
name
|
|
49
|
+
for name in names
|
|
50
|
+
if self.search_space.is_conditional(name)
|
|
51
|
+
and name in accepted
|
|
52
|
+
and accepted[name].default is inspect.Parameter.empty
|
|
53
|
+
and accepted[name].kind is not inspect.Parameter.VAR_KEYWORD
|
|
54
|
+
)
|
|
55
|
+
if no_default:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{self.name}() isn't passed {', '.join(no_default)} at points where a condition switches "
|
|
58
|
+
f"{'it' if len(no_default) == 1 else 'them'} off, so give {'it' if len(no_default) == 1 else 'each'} a default, "
|
|
59
|
+
f"such as {no_default[0]}=None"
|
|
60
|
+
)
|
|
46
61
|
if missing:
|
|
47
62
|
raise ValueError(f"{self.name}() requires parameters missing from the search space: {', '.join(missing)}")
|
|
@@ -74,10 +74,15 @@ class Optimizer(ABC):
|
|
|
74
74
|
config: The optimizer's configuration. Defaults are used if omitted.
|
|
75
75
|
initial_points: Points to evaluate first, such as a known good design or the best
|
|
76
76
|
parameters of an earlier run: one dict of parameter values, or a list of them.
|
|
77
|
-
Fixed parameters may be left out.
|
|
77
|
+
Fixed parameters may be left out, and so must parameters a condition switches off.
|
|
78
|
+
|
|
79
|
+
Every individual an optimizer breeds holds a value for every parameter, including those
|
|
80
|
+
the conditions switch off at its point, so an optimizer can keep them to use again. The
|
|
81
|
+
base class leaves them out of what it hands out and reports: trials, results and progress.
|
|
78
82
|
"""
|
|
79
83
|
config_class: ClassVar[type[OptimizerConfig]] = OptimizerConfig
|
|
80
84
|
supports_constraints: ClassVar[bool] = False # whether it keeps to a search space's constraints
|
|
85
|
+
supports_conditions: ClassVar[bool] = False # whether it handles parameters that conditions switch on and off
|
|
81
86
|
|
|
82
87
|
def __init__(
|
|
83
88
|
self,
|
|
@@ -94,6 +99,8 @@ class Optimizer(ABC):
|
|
|
94
99
|
raise TypeError(f"{type(self).__name__} needs a {self.config_class.__name__}, got {type(self.config).__name__}")
|
|
95
100
|
if search_space.constraints and not self.supports_constraints:
|
|
96
101
|
raise UnsupportedSearchSpace(f"{type(self).__name__} doesn't support constraints yet; RandomSearch does")
|
|
102
|
+
if search_space.conditions and not self.supports_conditions:
|
|
103
|
+
raise UnsupportedSearchSpace(f"{type(self).__name__} doesn't support conditional parameters yet; RandomSearch does")
|
|
97
104
|
if isinstance(initial_points, dict):
|
|
98
105
|
initial_points = [initial_points]
|
|
99
106
|
self.initial_points = [search_space.check(point) for point in initial_points or []]
|
|
@@ -177,13 +184,20 @@ class Optimizer(ABC):
|
|
|
177
184
|
reissued = [outstanding[i] for i in self._reissue if i in outstanding][:n]
|
|
178
185
|
self._reissue = [i for i in self._reissue if i in outstanding and i not in {ind.id for ind in reissued}]
|
|
179
186
|
if self._done:
|
|
180
|
-
return [
|
|
187
|
+
return [self._trial(ind) for ind in reissued]
|
|
181
188
|
ready = [ind for ind_id, ind in self._waiting.items() if ind_id not in self._handed_out]
|
|
182
189
|
limits = [None if n is None else n - len(reissued), self._budget_left()]
|
|
183
190
|
ready = ready[: min((limit for limit in limits if limit is not None), default=len(ready))]
|
|
184
191
|
self._handed_out.update(ind.id for ind in ready)
|
|
185
192
|
self._handed_out_total += len(ready)
|
|
186
|
-
return [
|
|
193
|
+
return [self._trial(ind) for ind in reissued + ready]
|
|
194
|
+
|
|
195
|
+
def _trial(self, individual: Individual) -> Trial:
|
|
196
|
+
return Trial(id=individual.id, params=self.search_space.active(individual.params), generation=individual.generation)
|
|
197
|
+
|
|
198
|
+
def _key(self, params: dict[str, Any]) -> tuple:
|
|
199
|
+
"""A hashable form of the point params are at: two that differ only where conditions switch parameters off share it."""
|
|
200
|
+
return _key(self.search_space.active(params))
|
|
187
201
|
|
|
188
202
|
def tell(self, trial: Trial | int, value: float | None = None, *, failed: bool = False) -> None:
|
|
189
203
|
"""Report the outcome of a trial from ask(), by the trial or its id.
|
|
@@ -215,7 +229,7 @@ class Optimizer(ABC):
|
|
|
215
229
|
individual.fitness = float(value)
|
|
216
230
|
if self.config.noise:
|
|
217
231
|
# Average every evaluation of the same point, so a lucky value regresses toward its mean.
|
|
218
|
-
key = _key(individual.params)
|
|
232
|
+
key = self._key(individual.params)
|
|
219
233
|
self._observations.setdefault(key, []).append(float(value))
|
|
220
234
|
self._points.setdefault(key, dict(individual.params))
|
|
221
235
|
individual.fitness = float(np.mean(self._observations[key]))
|
|
@@ -271,8 +285,8 @@ class Optimizer(ABC):
|
|
|
271
285
|
# Once the finalists have been re-evaluated, the one with the best average; before
|
|
272
286
|
# that, the most promising point by the optimizer's best estimate.
|
|
273
287
|
key = self._ranked_keys(self._finalists)[0] if self._finalists else self._finalist_order()[0]
|
|
274
|
-
return self._average(key), self._points[key]
|
|
275
|
-
return _fitness(self._best), self._best.params
|
|
288
|
+
return self._average(key), self.search_space.active(self._points[key])
|
|
289
|
+
return _fitness(self._best), self.search_space.active(self._best.params)
|
|
276
290
|
|
|
277
291
|
def _run_info(self) -> RunInfo:
|
|
278
292
|
return RunInfo(
|
|
@@ -538,7 +552,7 @@ class Optimizer(ABC):
|
|
|
538
552
|
"""Set each successful individual's fitness to the current average of every evaluation of its point."""
|
|
539
553
|
for individual in individuals:
|
|
540
554
|
if not individual.failed and individual.fitness is not None:
|
|
541
|
-
individual.fitness = self._average(_key(individual.params))
|
|
555
|
+
individual.fitness = self._average(self._key(individual.params))
|
|
542
556
|
|
|
543
557
|
def _final_population(self, generation: int) -> list[Individual]:
|
|
544
558
|
"""The final round with noise: the most promising points, each evaluated again.
|
|
@@ -561,7 +575,14 @@ class Optimizer(ABC):
|
|
|
561
575
|
return list(self._late.values())
|
|
562
576
|
|
|
563
577
|
def _initial_individuals(self, generation: int) -> list[Individual]:
|
|
564
|
-
|
|
578
|
+
"""The initial points as individuals, with a random value for each parameter a condition switches off."""
|
|
579
|
+
return [
|
|
580
|
+
Individual(
|
|
581
|
+
params={p.name: point[p.name] if p.name in point else p.sample(self.rng) for p in self.search_space.parameters},
|
|
582
|
+
generation=generation,
|
|
583
|
+
)
|
|
584
|
+
for point in self.initial_points
|
|
585
|
+
]
|
|
565
586
|
|
|
566
587
|
def _random_individual(self, generation: int) -> Individual:
|
|
567
588
|
return Individual(params=self.search_space.sample(self.rng), generation=generation)
|
|
@@ -592,7 +613,7 @@ class Optimizer(ABC):
|
|
|
592
613
|
evaluations=evaluations,
|
|
593
614
|
failures=failures,
|
|
594
615
|
best_fitness=succeeded[0].fitness,
|
|
595
|
-
best_params=
|
|
616
|
+
best_params=self.search_space.active(succeeded[0].params),
|
|
596
617
|
worst_fitness=succeeded[-1].fitness,
|
|
597
618
|
mean=float(np.mean(fitnesses)),
|
|
598
619
|
median=float(np.median(fitnesses)),
|
|
@@ -7,6 +7,9 @@ simulations, experiments, training runs. It makes the most of a small budget.
|
|
|
7
7
|
Range parameters are scaled to [0, 1], on a log scale where they have one; integers are searched
|
|
8
8
|
as numbers and rounded. Each choice
|
|
9
9
|
parameter becomes one column per choice, holding 1 for the chosen one and 0 for the rest.
|
|
10
|
+
A parameter a condition switches off is encoded the same way whatever value it holds: a range at
|
|
11
|
+
INACTIVE, and a choice with every column 0. So points that differ only where parameters are
|
|
12
|
+
switched off are the same point to the model, and the values they happen to hold add no noise.
|
|
10
13
|
"""
|
|
11
14
|
import math
|
|
12
15
|
from typing import Any, List
|
|
@@ -20,12 +23,14 @@ from hone.config import BayesianConfig
|
|
|
20
23
|
from hone.optimizers._gp import GaussianProcess
|
|
21
24
|
from hone.optimizers.base import Individual, Optimizer, UnsupportedSearchSpace
|
|
22
25
|
from hone.parameters import ChoiceParameter, Parameter, RangeParameter, SearchSpace
|
|
26
|
+
from hone.parameters.conditions import ChoiceTest, holds
|
|
23
27
|
|
|
24
28
|
GLOBAL_CANDIDATES = 1000 # random candidates per search of the model
|
|
25
29
|
LOCAL_CANDIDATES = 500 # candidates near the best points found so far
|
|
26
30
|
REFINED = 5 # the best candidates are refined by gradient search
|
|
27
31
|
REFIT_EVERY = 5 # past FIT_EVERY_UNTIL observations, refit the model's hyperparameters only this often
|
|
28
32
|
FIT_EVERY_UNTIL = 50
|
|
33
|
+
INACTIVE = 0.5 # where a range switched off by a condition is encoded
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
class BayesianOptimizer(Optimizer):
|
|
@@ -46,6 +51,7 @@ class BayesianOptimizer(Optimizer):
|
|
|
46
51
|
"""
|
|
47
52
|
config_class = BayesianConfig
|
|
48
53
|
supports_constraints = True
|
|
54
|
+
supports_conditions = True
|
|
49
55
|
config: BayesianConfig
|
|
50
56
|
|
|
51
57
|
def __init__(
|
|
@@ -209,6 +215,8 @@ class _Encoding:
|
|
|
209
215
|
|
|
210
216
|
def __init__(self, search_space: SearchSpace) -> None:
|
|
211
217
|
self.names = search_space.names # not the search space itself, whose function constraints may not pickle
|
|
218
|
+
# The conditions alone, to tell which parameters a point switches off.
|
|
219
|
+
self._conditions = SearchSpace(search_space.parameters, conditions=search_space.conditions) if search_space.conditions else None
|
|
212
220
|
self._blocks: list[tuple[RangeParameter | ChoiceParameter, slice]] = []
|
|
213
221
|
self._constant: dict[str, Any] = {}
|
|
214
222
|
column = 0
|
|
@@ -234,14 +242,42 @@ class _Encoding:
|
|
|
234
242
|
self.numeric = np.array(numeric, dtype=int)
|
|
235
243
|
|
|
236
244
|
def encode(self, params: dict) -> np.ndarray:
|
|
245
|
+
on = self._conditions.active(params) if self._conditions is not None else params
|
|
237
246
|
x = np.zeros(self.columns)
|
|
238
247
|
for p, block in self._blocks:
|
|
239
|
-
if
|
|
248
|
+
if p.name not in on:
|
|
249
|
+
x[block] = INACTIVE if isinstance(p, RangeParameter) else 0.0
|
|
250
|
+
elif isinstance(p, RangeParameter):
|
|
240
251
|
x[block] = p.to_unit(params[p.name])
|
|
241
252
|
else:
|
|
242
253
|
x[block.start + p.choices.index(params[p.name])] = 1.0
|
|
243
254
|
return x
|
|
244
255
|
|
|
256
|
+
def _switch_off(self, X: np.ndarray) -> np.ndarray:
|
|
257
|
+
"""X with the columns of parameters each row switches off set as encode() sets them."""
|
|
258
|
+
if self._conditions is None:
|
|
259
|
+
return X
|
|
260
|
+
blocks = {p.name: (p, block) for p, block in self._blocks}
|
|
261
|
+
on: dict[str, np.ndarray] = {}
|
|
262
|
+
for parameter in self._conditions._order:
|
|
263
|
+
test = self._conditions._tests.get(parameter.name)
|
|
264
|
+
if test is None:
|
|
265
|
+
on[parameter.name] = np.ones(len(X), dtype=bool)
|
|
266
|
+
continue
|
|
267
|
+
mask = np.logical_and.reduce([on[name] for name in test.names()])
|
|
268
|
+
if isinstance(test, ChoiceTest):
|
|
269
|
+
if test.name in blocks:
|
|
270
|
+
p, block = blocks[test.name]
|
|
271
|
+
assert isinstance(p, ChoiceParameter)
|
|
272
|
+
mask &= X[:, [block.start + p.choices.index(value) for value in test.values]].max(axis=1) > 0.5
|
|
273
|
+
else:
|
|
274
|
+
for row in np.flatnonzero(mask):
|
|
275
|
+
mask[row] = holds(test, self.decode(X[row]))
|
|
276
|
+
on[parameter.name] = mask
|
|
277
|
+
for p, block in self._blocks:
|
|
278
|
+
X[~on[p.name], block] = INACTIVE if isinstance(p, RangeParameter) else 0.0
|
|
279
|
+
return X
|
|
280
|
+
|
|
245
281
|
def decode(self, x: np.ndarray) -> dict:
|
|
246
282
|
values = dict(self._constant)
|
|
247
283
|
for p, block in self._blocks:
|
|
@@ -261,7 +297,7 @@ class _Encoding:
|
|
|
261
297
|
chosen = np.argmax(X[:, block], axis=1)
|
|
262
298
|
X[:, block] = 0.0
|
|
263
299
|
X[np.arange(len(X)), block.start + chosen] = 1.0
|
|
264
|
-
return X
|
|
300
|
+
return self._switch_off(X)
|
|
265
301
|
|
|
266
302
|
def spread(self, rng: np.random.Generator, count: int) -> np.ndarray:
|
|
267
303
|
"""count points spread over the space: a Latin hypercube for numbers, random choices otherwise."""
|
|
@@ -39,7 +39,8 @@ class CMAES(Optimizer):
|
|
|
39
39
|
interact, which makes it strong on smooth continuous problems.
|
|
40
40
|
|
|
41
41
|
Range parameters, continuous or integer, are searched; fixed parameters are passed
|
|
42
|
-
through. Choice parameters are not supported: use GeneticOptimizer for those.
|
|
42
|
+
through. Choice parameters are not supported: use GeneticOptimizer for those. A range a
|
|
43
|
+
condition switches off keeps its place in the distribution; its value is ignored.
|
|
43
44
|
|
|
44
45
|
The search starts at the centre of every range. With initial points, it evaluates them
|
|
45
46
|
first and starts at the best one instead; a smaller initial_step then keeps the search
|
|
@@ -52,6 +53,7 @@ class CMAES(Optimizer):
|
|
|
52
53
|
"""
|
|
53
54
|
config_class = CMAESConfig
|
|
54
55
|
supports_constraints = True
|
|
56
|
+
supports_conditions = True
|
|
55
57
|
config: CMAESConfig
|
|
56
58
|
|
|
57
59
|
def __init__(
|