hone-python 0.9.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.9.0 → hone_python-0.11.0}/PKG-INFO +75 -2
- {hone_python-0.9.0 → hone_python-0.11.0}/README.md +74 -1
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/__init__.py +4 -1
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/benchmarks/__init__.py +2 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/benchmarks/__main__.py +10 -0
- hone_python-0.11.0/hone/benchmarks/conditional.py +109 -0
- hone_python-0.11.0/hone/benchmarks/constrained.py +172 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/benchmarks/ml.py +69 -3
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/config/genetic_optimizer_config.py +3 -2
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/objective_function/objective_function.py +15 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/base.py +42 -10
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/bayesian.py +67 -10
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/cmaes.py +26 -4
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/genetic/optimizer.py +91 -15
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/genetic/variation.py +10 -3
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/random_search.py +2 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/parameters/__init__.py +2 -0
- hone_python-0.11.0/hone/parameters/conditions.py +147 -0
- hone_python-0.11.0/hone/parameters/constraints.py +336 -0
- hone_python-0.11.0/hone/parameters/parameters.py +466 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/result.py +8 -3
- {hone_python-0.9.0 → hone_python-0.11.0}/pyproject.toml +1 -1
- hone_python-0.9.0/hone/parameters/parameters.py +0 -237
- {hone_python-0.9.0 → hone_python-0.11.0}/LICENSE +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/benchmarks/problems.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/benchmarks/runner.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/config/__init__.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/config/bayesian_config.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/config/cmaes_config.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/config/optimizer_config.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/objective_function/__init__.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/__init__.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/_gp.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/_pool.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/choice.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/genetic/__init__.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/optimizers/genetic/selection.py +0 -0
- {hone_python-0.9.0 → hone_python-0.11.0}/hone/py.typed +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
|
|
@@ -178,6 +178,78 @@ RangeParameter("n_estimators", 10, 1000, integer=True, log=True)
|
|
|
178
178
|
|
|
179
179
|
Every parameter the objective function requires must be in the search space. The optimizer checks this before it starts.
|
|
180
180
|
|
|
181
|
+
### Constraints
|
|
182
|
+
|
|
183
|
+
Limits across parameters are written with the parameters themselves and passed to `SearchSpace`. Every optimizer then only asks for points that satisfy every constraint, so the objective is never evaluated outside them:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from hone import RangeParameter, SearchSpace
|
|
187
|
+
|
|
188
|
+
width = RangeParameter("width", 1, 10)
|
|
189
|
+
height = RangeParameter("height", 1, 10)
|
|
190
|
+
search_space = SearchSpace([width, height], constraints=[width * height <= 40, width >= 2 * height])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Range parameters and numbers combine with `+`, `-`, `*`, `/`, `**` and `abs()`, and `<=` or `>=` makes the result a constraint; `<` and `>` mean the same. A point where a constraint can't be computed, such as one that divides by zero, counts as breaking it. There are no equality constraints: an equality leaves nothing to sample, so remove a variable instead and compute it inside the objective. Write a range as two constraints, since Python reads `0 <= width - height <= 5` as two comparisons joined by `and`, and hone raises an error rather than keep one half.
|
|
194
|
+
|
|
195
|
+
For anything an expression can't say, such as a square root or a choice parameter, use a function, which takes parameters by name and is satisfied when it returns zero or less:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from hone import Constraint
|
|
199
|
+
|
|
200
|
+
def stress(width, height, material):
|
|
201
|
+
return bending_stress(width, height) - ALLOWED[material]
|
|
202
|
+
|
|
203
|
+
search_space = SearchSpace([width, height, material], constraints=[Constraint.function(stress)])
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Constraints should be quick to check: hone checks them for every candidate it considers, which can be many times the number of evaluations. Expression constraints are saved with a result; a function constraint saves only its name, so after `Result.load` pass the function again with `rebuild_optimizer(functions={"stress": stress})`.
|
|
207
|
+
|
|
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
|
+
|
|
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
|
+
|
|
181
253
|
### Configuration
|
|
182
254
|
|
|
183
255
|
`GeneticOptimizerConfig` validates its values when created. It can also be loaded from a JSON or YAML file:
|
|
@@ -190,7 +262,7 @@ Set `seed` to make a run repeatable. `max_evaluations` sets a budget: the run st
|
|
|
190
262
|
|
|
191
263
|
Parents are chosen by tournament: `tournament_size` (default 4) individuals are drawn and the fittest wins.
|
|
192
264
|
|
|
193
|
-
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.
|
|
194
266
|
|
|
195
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.
|
|
196
268
|
|
|
@@ -326,6 +398,7 @@ Requires Python 3.12 or later and [Poetry](https://python-poetry.org/) 2.
|
|
|
326
398
|
| `make benchmark` | Run the full benchmark suite and update `benchmarks/results.md` |
|
|
327
399
|
| `make benchmark-noisy` | Run the noisy problems and update `benchmarks/noisy-results.md` |
|
|
328
400
|
| `make benchmark-ml` | Run the machine learning tuning problems and update `benchmarks/ml-results.md` |
|
|
401
|
+
| `make benchmark-constrained` | Run the constrained problems and update `benchmarks/constrained-results.md` |
|
|
329
402
|
|
|
330
403
|
To release, raise `version` in `pyproject.toml` and add a section for it to `CHANGELOG.md`. Once the change is on `main` and its checks pass, CI tags the version, publishes a GitHub release with that section as its notes and the built package attached, and uploads the same package to PyPI as `hone-python`.
|
|
331
404
|
|
|
@@ -157,6 +157,78 @@ RangeParameter("n_estimators", 10, 1000, integer=True, log=True)
|
|
|
157
157
|
|
|
158
158
|
Every parameter the objective function requires must be in the search space. The optimizer checks this before it starts.
|
|
159
159
|
|
|
160
|
+
### Constraints
|
|
161
|
+
|
|
162
|
+
Limits across parameters are written with the parameters themselves and passed to `SearchSpace`. Every optimizer then only asks for points that satisfy every constraint, so the objective is never evaluated outside them:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from hone import RangeParameter, SearchSpace
|
|
166
|
+
|
|
167
|
+
width = RangeParameter("width", 1, 10)
|
|
168
|
+
height = RangeParameter("height", 1, 10)
|
|
169
|
+
search_space = SearchSpace([width, height], constraints=[width * height <= 40, width >= 2 * height])
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Range parameters and numbers combine with `+`, `-`, `*`, `/`, `**` and `abs()`, and `<=` or `>=` makes the result a constraint; `<` and `>` mean the same. A point where a constraint can't be computed, such as one that divides by zero, counts as breaking it. There are no equality constraints: an equality leaves nothing to sample, so remove a variable instead and compute it inside the objective. Write a range as two constraints, since Python reads `0 <= width - height <= 5` as two comparisons joined by `and`, and hone raises an error rather than keep one half.
|
|
173
|
+
|
|
174
|
+
For anything an expression can't say, such as a square root or a choice parameter, use a function, which takes parameters by name and is satisfied when it returns zero or less:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from hone import Constraint
|
|
178
|
+
|
|
179
|
+
def stress(width, height, material):
|
|
180
|
+
return bending_stress(width, height) - ALLOWED[material]
|
|
181
|
+
|
|
182
|
+
search_space = SearchSpace([width, height, material], constraints=[Constraint.function(stress)])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Constraints should be quick to check: hone checks them for every candidate it considers, which can be many times the number of evaluations. Expression constraints are saved with a result; a function constraint saves only its name, so after `Result.load` pass the function again with `rebuild_optimizer(functions={"stress": stress})`.
|
|
186
|
+
|
|
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
|
+
|
|
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
|
+
|
|
160
232
|
### Configuration
|
|
161
233
|
|
|
162
234
|
`GeneticOptimizerConfig` validates its values when created. It can also be loaded from a JSON or YAML file:
|
|
@@ -169,7 +241,7 @@ Set `seed` to make a run repeatable. `max_evaluations` sets a budget: the run st
|
|
|
169
241
|
|
|
170
242
|
Parents are chosen by tournament: `tournament_size` (default 4) individuals are drawn and the fittest wins.
|
|
171
243
|
|
|
172
|
-
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.
|
|
173
245
|
|
|
174
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.
|
|
175
247
|
|
|
@@ -305,6 +377,7 @@ Requires Python 3.12 or later and [Poetry](https://python-poetry.org/) 2.
|
|
|
305
377
|
| `make benchmark` | Run the full benchmark suite and update `benchmarks/results.md` |
|
|
306
378
|
| `make benchmark-noisy` | Run the noisy problems and update `benchmarks/noisy-results.md` |
|
|
307
379
|
| `make benchmark-ml` | Run the machine learning tuning problems and update `benchmarks/ml-results.md` |
|
|
380
|
+
| `make benchmark-constrained` | Run the constrained problems and update `benchmarks/constrained-results.md` |
|
|
308
381
|
|
|
309
382
|
To release, raise `version` in `pyproject.toml` and add a section for it to `CHANGELOG.md`. Once the change is on `main` and its checks pass, CI tags the version, publishes a GitHub release with that section as its notes and the built package attached, and uploads the same package to PyPI as `hone-python`.
|
|
310
383
|
|
|
@@ -10,7 +10,7 @@ from hone.optimizers import (
|
|
|
10
10
|
UnsupportedSearchSpace,
|
|
11
11
|
choose_optimizer,
|
|
12
12
|
)
|
|
13
|
-
from hone.parameters import ChoiceParameter, FixedParameter, 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,11 +19,14 @@ __all__ = [
|
|
|
19
19
|
"CMAES",
|
|
20
20
|
"CMAESConfig",
|
|
21
21
|
"ChoiceParameter",
|
|
22
|
+
"Condition",
|
|
23
|
+
"Constraint",
|
|
22
24
|
"EvaluationFailed",
|
|
23
25
|
"FixedParameter",
|
|
24
26
|
"GenerationStats",
|
|
25
27
|
"GeneticOptimizer",
|
|
26
28
|
"GeneticOptimizerConfig",
|
|
29
|
+
"InfeasibleSearchSpace",
|
|
27
30
|
"Optimizer",
|
|
28
31
|
"OptimizerConfig",
|
|
29
32
|
"Progress",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""Benchmark problems and a runner for comparing optimizers. Run `python -m hone.benchmarks --help`."""
|
|
2
|
+
from .conditional import conditional_problems
|
|
3
|
+
from .constrained import constrained_problems
|
|
2
4
|
from .ml import ml_problems
|
|
3
5
|
from .problems import Problem, noisy_problems, quick_problems, standard_problems
|
|
4
6
|
from .runner import (
|
|
@@ -6,6 +6,8 @@ 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
|
|
10
|
+
from .constrained import constrained_problems
|
|
9
11
|
from .problems import noisy_problems, quick_problems, standard_problems
|
|
10
12
|
from .runner import BUDGETS, load_summaries, regressions, run_benchmark, save_summaries, summarize, to_markdown
|
|
11
13
|
|
|
@@ -26,6 +28,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
26
28
|
suite.add_argument("--quick", action="store_true", help="run the small CI subset with fewer seeds and evaluations")
|
|
27
29
|
suite.add_argument("--ml", action="store_true", help="run the machine learning tuning problems instead (needs scikit-learn)")
|
|
28
30
|
suite.add_argument("--noisy", action="store_true", help="run the problems with noise added to every evaluation instead")
|
|
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
|
+
)
|
|
29
35
|
parser.add_argument("--seeds", type=int, help="number of seeds per problem (default 10, or 5 with --quick)")
|
|
30
36
|
parser.add_argument(
|
|
31
37
|
"--budgets",
|
|
@@ -49,6 +55,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
49
55
|
problems, default_budgets = ml.ml_problems(), ml.BUDGETS
|
|
50
56
|
elif args.noisy:
|
|
51
57
|
problems, default_budgets = noisy_problems(), NOISY_BUDGETS
|
|
58
|
+
elif args.constrained:
|
|
59
|
+
problems, default_budgets = constrained_problems(), BUDGETS
|
|
60
|
+
elif args.conditional:
|
|
61
|
+
problems, default_budgets = conditional_problems(), BUDGETS
|
|
52
62
|
else:
|
|
53
63
|
problems, default_budgets = standard_problems(), BUDGETS
|
|
54
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()]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Constrained test problems, whose optima lie on a constraint's boundary, as they usually do in engineering.
|
|
2
|
+
|
|
3
|
+
- boundary-sphere and boundary-ellipsoid: a shifted sphere or ellipsoid with a linear constraint
|
|
4
|
+
that cuts off its unconstrained optimum, so the answer is on the cutting plane. Both optima are
|
|
5
|
+
known exactly.
|
|
6
|
+
- rosenbrock-disk: Rosenbrock's function inside the disk x² + y² ≤ 2, whose optimum (1, 1) sits
|
|
7
|
+
on the disk's edge.
|
|
8
|
+
- pressure-vessel, tension-spring and welded-beam: three standard engineering design problems,
|
|
9
|
+
scored against the best designs published for them. The welded beam's stress formulas need
|
|
10
|
+
square roots, so its constraints are functions; the others are expressions.
|
|
11
|
+
"""
|
|
12
|
+
import math
|
|
13
|
+
import zlib
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from hone.parameters import Constraint, RangeParameter, SearchSpace
|
|
19
|
+
|
|
20
|
+
from .problems import BOUND, Problem, _shift
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normal(name: str, dimensions: int) -> np.ndarray:
|
|
24
|
+
"""A fixed unit vector for the named problem, with no component near zero."""
|
|
25
|
+
rng = np.random.default_rng(zlib.crc32(f"{name}/normal".encode()))
|
|
26
|
+
w = rng.uniform(0.3, 1.0, dimensions) * rng.choice([-1.0, 1.0], dimensions)
|
|
27
|
+
return w / np.linalg.norm(w)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _boundary(name: str, dimensions: int, weights: np.ndarray) -> Problem:
|
|
31
|
+
"""Minimize sum(weights * (x - c)²) subject to w·x ≤ w·c - 1, which the unconstrained optimum c breaks.
|
|
32
|
+
|
|
33
|
+
The optimum is at c + z with z = -D⁻¹w / (wᵀD⁻¹w), where D is diag(weights), and its value is 1 / (wᵀD⁻¹w).
|
|
34
|
+
"""
|
|
35
|
+
names = [f"x{i}" for i in range(dimensions)]
|
|
36
|
+
parameters = [RangeParameter(n, -BOUND, BOUND) for n in names]
|
|
37
|
+
centre, w = _shift(name, dimensions), _normal(name, dimensions)
|
|
38
|
+
limit = float(w @ centre) - 1.0
|
|
39
|
+
terms = [float(wi) * p for wi, p in zip(w, parameters)]
|
|
40
|
+
plane = terms[0]
|
|
41
|
+
for term in terms[1:]:
|
|
42
|
+
plane = plane + term
|
|
43
|
+
inverse = 1.0 / weights
|
|
44
|
+
best = centre - inverse * w / float(w @ (inverse * w))
|
|
45
|
+
assert np.all(np.abs(best) <= BOUND), f"{name}: the constrained optimum must lie inside the bounds"
|
|
46
|
+
|
|
47
|
+
def function(**params: Any) -> float:
|
|
48
|
+
x = np.array([params[n] for n in names], dtype=float)
|
|
49
|
+
return float(np.sum(weights * (x - centre) ** 2))
|
|
50
|
+
|
|
51
|
+
function.__name__ = name.replace("-", "_")
|
|
52
|
+
space = SearchSpace(parameters, [plane <= limit])
|
|
53
|
+
return Problem(name=name, function=function, search_space=space, optimum=1.0 / float(w @ (inverse * w)), tags=frozenset({"constrained"}))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def boundary_sphere(dimensions: int) -> Problem:
|
|
57
|
+
return _boundary(f"boundary-sphere-{dimensions}d", dimensions, np.ones(dimensions))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def boundary_ellipsoid(dimensions: int) -> Problem:
|
|
61
|
+
# Axis weights from 1 to 1,000: wide enough to need scaling, narrow enough to keep the optimum in bounds.
|
|
62
|
+
return _boundary(f"boundary-ellipsoid-{dimensions}d", dimensions, 10.0 ** (3 * np.arange(dimensions) / max(dimensions - 1, 1)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def rosenbrock_disk() -> Problem:
|
|
66
|
+
x, y = RangeParameter("x", -1.5, 1.5), RangeParameter("y", -1.5, 1.5)
|
|
67
|
+
|
|
68
|
+
def rosenbrock_disk(x: float, y: float) -> float:
|
|
69
|
+
return (1 - x) ** 2 + 100 * (y - x**2) ** 2
|
|
70
|
+
|
|
71
|
+
space = SearchSpace([x, y], [x**2 + y**2 <= 2])
|
|
72
|
+
return Problem(name="rosenbrock-disk-2d", function=rosenbrock_disk, search_space=space, optimum=0.0, tags=frozenset({"constrained"}))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def pressure_vessel() -> Problem:
|
|
76
|
+
"""A cylindrical vessel with hemispherical heads, at least 1,296,000 cubic inches, of least cost.
|
|
77
|
+
|
|
78
|
+
Shell and head thicknesses come in sixteenths of an inch, so they are integers here. The best
|
|
79
|
+
published design costs 6059.714 (Ts = 13/16, Th = 7/16, R = 42.098, L = 176.637).
|
|
80
|
+
"""
|
|
81
|
+
shell, head = RangeParameter("shell", 1, 99, integer=True), RangeParameter("head", 1, 99, integer=True)
|
|
82
|
+
radius, length = RangeParameter("radius", 10, 200), RangeParameter("length", 10, 200)
|
|
83
|
+
|
|
84
|
+
def pressure_vessel(shell: int, head: int, radius: float, length: float) -> float:
|
|
85
|
+
ts, th = shell / 16, head / 16
|
|
86
|
+
return 0.6224 * ts * radius * length + 1.7781 * th * radius**2 + 3.1661 * ts**2 * length + 19.84 * ts**2 * radius
|
|
87
|
+
|
|
88
|
+
space = SearchSpace(
|
|
89
|
+
[shell, head, radius, length],
|
|
90
|
+
[
|
|
91
|
+
shell / 16 >= 0.0193 * radius,
|
|
92
|
+
head / 16 >= 0.00954 * radius,
|
|
93
|
+
math.pi * radius**2 * length + 4 / 3 * math.pi * radius**3 >= 1_296_000,
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
return Problem(name="pressure-vessel", function=pressure_vessel, search_space=space, optimum=6059.714335, tags=frozenset({"constrained", "mixed", "engineering"}))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def tension_spring() -> Problem:
|
|
100
|
+
"""A coil spring of least weight under limits on deflection, shear stress, surge frequency and diameter.
|
|
101
|
+
|
|
102
|
+
The best published design weighs 0.0126652 (d = 0.051690, D = 0.356750, N = 11.287126).
|
|
103
|
+
"""
|
|
104
|
+
d, coil, n = RangeParameter("wire", 0.05, 2.0), RangeParameter("coil", 0.25, 1.3), RangeParameter("turns", 2.0, 15.0)
|
|
105
|
+
|
|
106
|
+
def tension_spring(wire: float, coil: float, turns: float) -> float:
|
|
107
|
+
return (turns + 2) * coil * wire**2
|
|
108
|
+
|
|
109
|
+
space = SearchSpace(
|
|
110
|
+
[d, coil, n],
|
|
111
|
+
[
|
|
112
|
+
coil**3 * n / (71785 * d**4) >= 1,
|
|
113
|
+
(4 * coil**2 - d * coil) / (12566 * (coil * d**3 - d**4)) + 1 / (5108 * d**2) <= 1,
|
|
114
|
+
140.45 * d / (coil**2 * n) >= 1,
|
|
115
|
+
d + coil <= 1.5,
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
return Problem(name="tension-spring", function=tension_spring, search_space=space, optimum=0.012665233, tags=frozenset({"constrained", "engineering"}))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# The welded beam's load and material: load P (lb), length L (in), Young's and shear moduli (psi).
|
|
122
|
+
P, L, E, G = 6000.0, 14.0, 30e6, 12e6
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _shear(weld: float, length: float, thickness: float) -> float:
|
|
126
|
+
primary = P / (math.sqrt(2) * weld * length)
|
|
127
|
+
moment = P * (L + length / 2)
|
|
128
|
+
r = math.sqrt(length**2 / 4 + ((weld + thickness) / 2) ** 2)
|
|
129
|
+
j = 2 * math.sqrt(2) * weld * length * (length**2 / 12 + ((weld + thickness) / 2) ** 2)
|
|
130
|
+
secondary = moment * r / j
|
|
131
|
+
return math.sqrt(primary**2 + primary * secondary * length / r + secondary**2)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _buckling(thickness: float, width: float) -> float:
|
|
135
|
+
return 4.013 * E * math.sqrt(thickness**2 * width**6 / 36) / L**2 * (1 - thickness / (2 * L) * math.sqrt(E / (4 * G)))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def welded_beam() -> Problem:
|
|
139
|
+
"""A beam welded to a support, of least cost under limits on shear, bending, buckling and deflection.
|
|
140
|
+
|
|
141
|
+
The best published design costs 1.724852 (h = 0.205730, l = 3.470489, t = 9.036624, b = 0.205730).
|
|
142
|
+
"""
|
|
143
|
+
weld, length = RangeParameter("weld", 0.1, 2.0), RangeParameter("length", 0.1, 10.0)
|
|
144
|
+
thickness, width = RangeParameter("thickness", 0.1, 10.0), RangeParameter("width", 0.1, 2.0)
|
|
145
|
+
|
|
146
|
+
def welded_beam(weld: float, length: float, thickness: float, width: float) -> float:
|
|
147
|
+
return 1.10471 * weld**2 * length + 0.04811 * thickness * width * (14 + length)
|
|
148
|
+
|
|
149
|
+
def shear(weld: float, length: float, thickness: float) -> float:
|
|
150
|
+
return _shear(weld, length, thickness) - 13600
|
|
151
|
+
|
|
152
|
+
def buckling(thickness: float, width: float) -> float:
|
|
153
|
+
return P - _buckling(thickness, width)
|
|
154
|
+
|
|
155
|
+
space = SearchSpace(
|
|
156
|
+
[weld, length, thickness, width],
|
|
157
|
+
[
|
|
158
|
+
Constraint.function(shear),
|
|
159
|
+
6 * P * L / (width * thickness**2) <= 30000,
|
|
160
|
+
weld <= width,
|
|
161
|
+
0.10471 * weld**2 + 0.04811 * thickness * width * (14 + length) <= 5,
|
|
162
|
+
4 * P * L**3 / (E * thickness**3 * width) <= 0.25,
|
|
163
|
+
Constraint.function(buckling),
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
return Problem(name="welded-beam", function=welded_beam, search_space=space, optimum=1.724852, tags=frozenset({"constrained", "engineering"}))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def constrained_problems(dimensions: tuple[int, ...] = (2, 5, 10)) -> list[Problem]:
|
|
170
|
+
"""The constrained suite: boundary sphere and ellipsoid in each dimension, Rosenbrock in a disk, and three engineering designs."""
|
|
171
|
+
problems = [factory(d) for d in dimensions for factory in (boundary_sphere, boundary_ellipsoid)]
|
|
172
|
+
return problems + [rosenbrock_disk(), pressure_vessel(), tension_spring(), welded_beam()]
|
|
@@ -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
|
+
]
|