Skip to content

Commit 4b8e801

Browse files
committed
Merge branch 'feature/batch_bo_issue_1229' of https://github.com/automl/SMAC3 into feature/batch_bo_issue_1229
2 parents 6d4a808 + b01a1f8 commit 4b8e801

11 files changed

Lines changed: 111 additions & 22 deletions

File tree

.github/workflows/recent_reminder.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
echo "$(<recent_issues.md) <br />" >> mail.html
3333
- name: Send mail
3434
id: mail
35-
uses: dawidd6/action-send-mail@v4
35+
uses: dawidd6/action-send-mail@v5
3636
with:
3737
server_address: ${{secrets.MAIL_SERVER_ADDRESS}}
3838
server_port: ${{secrets.MAIL_SERVER_PORT}}

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# 2.3.1
22

3+
## Bugfixes
4+
- Addressing situations where the acquisition function suggests configurations that have already been sampled in prior iterations (#1216)
5+
36
## Misc
47
- New SMAC logo
58
- Fix doc link in README
@@ -11,6 +14,15 @@
1114
## Improvements
1215
- Submit trials to runners in SMBO instead of running configs directly (#937)
1316

17+
## Improvements
18+
- `target_function` becomes optional in Facade when using ask and tell exclusively (#946)
19+
20+
## Documentation
21+
- Ask and tell without initial design and warmstarting
22+
23+
## Bugfixes
24+
- Ask and tell without initial design may no longer return a config from the initial design - if it is not "removed".
25+
1426
# 2.3.0
1527

1628
## Features

docs/advanced_usage/5_ask_and_tell.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ and report the results of the trial.
1515
different budgets, they, obviously, can not be considered. However, all user-provided configurations will flow
1616
into the intensification process.
1717

18+
Notice: if you are exclusively using the ask-and-tell interface and do not use `smac.optimize()`, then smac no longer
19+
is responsible for the evaluation of the trials and therefore the Facade no longer will require a specified `target_algorithm` argument.
1820

1921
Please have a look at our [ask-and-tell example](../examples/1%20Basics/3_ask_and_tell.md).

examples/1_basics/3_ask_and_tell.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
# Flags: doc-Runnable
33
44
This examples show how to use the Ask-and-Tell interface.
5+
6+
Notice, that the ask-and-tell interface will still use the initial design specified in the facade.
7+
Should you wish to add your own evaluated configurations instead or deactivate the initial
8+
design all together, please refer to the warmstarting example in conjunction with this one.
59
"""
610

711
from ConfigSpace import Configuration, ConfigurationSpace, Float
@@ -52,7 +56,7 @@ def train(self, config: Configuration, seed: int = 0) -> float:
5256
# Now we use SMAC to find the best hyperparameters
5357
smac = HyperparameterOptimizationFacade(
5458
scenario,
55-
model.train,
59+
target_function=model.train,
5660
intensifier=intensifier,
5761
overwrite=True,
5862
)
@@ -68,7 +72,14 @@ def train(self, config: Configuration, seed: int = 0) -> float:
6872
smac.tell(info, value)
6973

7074
# After calling ask+tell, we can still optimize
71-
# Note: SMAC will optimize the next 90 trials because 10 trials already have been evaluated
75+
# Note: SMAC will optimize the next 90 trials because 10 trials already have been evaluated.
76+
# If we however choose not to call optimize; e.g. because we want to manage heavy
77+
# computation of model.train completely outside smac, but still use it to suggest new
78+
# configurations, then n_trials will only be relevant for the initial design in combination
79+
# with initial design max_ratio! In fact in an only ask+tell case, we could even set
80+
# target_function=None in the constructor, because smac wouldn't even need to know
81+
# what the target function is. But that will prevent us from calling optimize and validate later
82+
# on.
7283
incumbent = smac.optimize()
7384

7485
# Get cost of default configuration

examples/1_basics/8_warmstart.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,19 @@ def evaluate(self, config: Configuration, seed: int = 0) -> float:
5656
intensifier = HyperparameterOptimizationFacade.get_intensifier(scenario, max_config_calls=1)
5757
smac = HyperparameterOptimizationFacade(
5858
scenario,
59-
task.evaluate,
59+
target_function=task.evaluate,
6060
intensifier=intensifier,
6161
overwrite=True,
6262

6363
# Modify the initial design to use our custom initial design
6464
initial_design=HyperparameterOptimizationFacade.get_initial_design(
6565
scenario,
66-
n_configs=0, # Do not use the default initial design
67-
additional_configs=configurations # Use the configurations previously evaluated as initial design
66+
n_configs=0, # Do not use the default initial design at all
67+
68+
# You can pass the configurations as additional_configs, which will specify their
69+
# origin to be the initial design. However, this is not necessary and we can just
70+
# smac.tell the configurations.
71+
# additional_configs=configurations # Use the configurations previously evaluated as initial design
6872
# This only passes the configurations but not the cost!
6973
# So in order to actually use the custom, pre-evaluated initial design
7074
# we need to tell those trials, like below.
@@ -80,4 +84,6 @@ def evaluate(self, config: Configuration, seed: int = 0) -> float:
8084
smac.tell(info, value)
8185

8286
# Optimize as usual
83-
smac.optimize()
87+
# Notice, that since we added three configurations, n_trials for the remaining optimization
88+
# is effectively 27 in optimize().
89+
smac.optimize()

smac/facade/abstract_facade.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,12 @@ class AbstractFacade:
5858
----------
5959
scenario : Scenario
6060
The scenario object, holding all environmental information.
61-
target_function : Callable | str | AbstractRunner
61+
target_function : Callable | str | AbstractRunner | None, defaults to None
6262
This function is called internally to judge a trial's performance. If a string is passed,
6363
it is assumed to be a script. In this case, ``TargetFunctionScriptRunner`` is used to run the script.
64+
In the rare case that only ``ask`` and ``tell`` and not ``optimize`` is used to optimize
65+
the hyperparameters, the target_function argument can be None, because SMAC no longer is
66+
charge of the evaluation of the configuration and thus does not need to know about it.
6467
model : AbstractModel | None, defaults to None
6568
The surrogate model.
6669
acquisition_function : AbstractAcquisitionFunction | None, defaults to None
@@ -105,7 +108,7 @@ class AbstractFacade:
105108
def __init__(
106109
self,
107110
scenario: Scenario,
108-
target_function: Callable | str | AbstractRunner,
111+
target_function: Callable | str | AbstractRunner | None = None,
109112
*,
110113
model: AbstractModel | None = None,
111114
acquisition_function: AbstractAcquisitionFunction | None = None,
@@ -176,8 +179,10 @@ def __init__(
176179
self._overwrite = overwrite
177180

178181
# Prepare the algorithm executer
179-
runner: AbstractRunner
180-
if isinstance(target_function, AbstractRunner):
182+
runner: AbstractRunner | None
183+
if isinstance(target_function, AbstractRunner) or target_function is None:
184+
# in case the target_function is None (e.g. we purely use ask & tell)
185+
# we let smbo.optimize raise an error
181186
runner = target_function
182187
elif isinstance(target_function, str):
183188
runner = TargetFunctionScriptRunner(
@@ -193,7 +198,7 @@ def __init__(
193198
)
194199

195200
# In case of multiple jobs, we need to wrap the runner again using DaskParallelRunner
196-
if (n_workers := scenario.n_workers) > 1 or dask_client is not None:
201+
if ((n_workers := scenario.n_workers) > 1 or dask_client is not None) and runner is not None:
197202
if dask_client is not None and n_workers > 1:
198203
logger.warning(
199204
"Provided `dask_client`. Ignore `scenario.n_workers`, directly set `n_workers` in `dask_client`."
@@ -265,7 +270,7 @@ def meta(self) -> dict[str, Any]:
265270

266271
meta = {
267272
"facade": {"name": self.__class__.__name__},
268-
"runner": self._runner.meta,
273+
"runner": self._runner.meta if self._runner is not None else None,
269274
"model": self._model.meta,
270275
"acquisition_maximizer": self._acquisition_maximizer.meta,
271276
"acquisition_function": self._acquisition_function.meta,

smac/initial_design/abstract_initial_design.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ def __init__(
8282
)
8383

8484
# If the number of configurations is too large, we reduce it
85-
_n_configs = int(max(1, min(self._n_configs, (max_ratio * scenario.n_trials))))
85+
if self._n_configs > 1:
86+
_n_configs = int(max(1, min(self._n_configs, (max_ratio * scenario.n_trials))))
87+
else:
88+
_n_configs = self._n_configs
89+
8690
if self._n_configs != _n_configs:
8791
logger.info(
8892
f"Reducing the number of initial configurations from {self._n_configs} to "

smac/main/config_selector.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from smac.acquisition.maximizer.random_search import RandomSearch
1818
from smac.callback.callback import Callback
1919
from smac.initial_design import AbstractInitialDesign
20+
from smac.main.exceptions import ConfigurationSpaceExhaustedException
2021
from smac.model.abstract_model import AbstractModel
2122
from smac.model.gaussian_process import GaussianProcess
2223
from smac.model.random_forest import RandomForest
@@ -119,15 +120,16 @@ def _set_components(
119120

120121
self._initial_design_configs = initial_design.select_configurations()
121122
if len(self._initial_design_configs) == 0:
122-
raise RuntimeError("SMAC needs initial configurations to work.")
123+
# raise RuntimeError("SMAC needs initial configurations to work.")
124+
logger.warning("No initial configurations were sampled.")
123125

124126
@property
125127
def meta(self) -> dict[str, Any]:
126128
"""Returns the meta data of the created object."""
127129
return {
128130
"name": self.__class__.__name__,
129131
"retrain_after": self._retrain_after,
130-
"retries": self._max_new_config_tries,
132+
"max_new_config_tries": self._max_new_config_tries,
131133
"min_trials": self._min_trials,
132134
}
133135

@@ -261,10 +263,31 @@ def __iter__(self) -> Iterator[Configuration]:
261263

262264
# We exit the loop if we have tried to add the same configuration too often
263265
if failed_counter == self._max_new_config_tries:
264-
logger.warning(
265-
f"Could not return a new configuration after {self._max_new_config_tries} retries." ""
266-
)
267-
return
266+
logger.warning(f"Could not return a new configuration after {failed_counter} retries.")
267+
break
268+
269+
# if we don't have enough configurations, we want to sample random configurations
270+
if not retrain:
271+
logger.warning(
272+
"Did not find enough configuration from the acquisition function. Sampling random configurations."
273+
)
274+
random_configs_retries = 0
275+
while counter < self._retrain_after and random_configs_retries < self._max_new_config_tries:
276+
config = self._scenario.configspace.sample_configuration()
277+
if config not in self._processed_configs:
278+
counter += 1
279+
config.origin = "Random Search (max retries, no candidates)"
280+
self._processed_configs.append(config)
281+
self._call_callbacks_on_end(config)
282+
yield config
283+
retrain = counter == self._retrain_after
284+
self._call_callbacks_on_start()
285+
else:
286+
random_configs_retries += 1
287+
288+
if random_configs_retries == self._max_new_config_tries:
289+
logger.warning(f"Could not return a new configuration after {random_configs_retries} retries.")
290+
raise ConfigurationSpaceExhaustedException()
268291

269292
def _call_callbacks_on_start(self) -> None:
270293
for callback in self._callbacks:

smac/main/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class ConfigurationSpaceExhaustedException(Exception):
2+
"""Exception indicating that the configuration space is exhausted and no more configurations
3+
can be sampled. This is usually raised when the maximum number of configurations has been
4+
reached or when the configuration space has been fully explored.
5+
"""
6+
7+
pass

smac/main/smbo.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ class SMBO:
4040
----------
4141
scenario : Scenario
4242
The scenario object, holding all environmental information.
43-
runner : AbstractRunner
43+
runner : AbstractRunner | None
4444
The runner (containing the target function) is called internally to judge a trial's performance.
45+
In the rare case that ``optimize`` is never called and SMBO is operated with ``ask`` and ``tell`` only,
46+
the runner is allowed to be None
4547
runhistory : Runhistory
4648
The runhistory stores all trials.
4749
intensifier : AbstractIntensifier
@@ -60,7 +62,7 @@ class SMBO:
6062
def __init__(
6163
self,
6264
scenario: Scenario,
63-
runner: AbstractRunner,
65+
runner: AbstractRunner | None,
6466
runhistory: RunHistory,
6567
intensifier: AbstractIntensifier,
6668
overwrite: bool = False,
@@ -290,6 +292,11 @@ def optimize(self, *, data_to_scatter: dict[str, Any] | None = None) -> Configur
290292
callback.on_start(self)
291293

292294
dask_data_to_scatter = {}
295+
if self._runner is None:
296+
raise ValueError(
297+
"Runner is not set in SMBO. Likely issue is that the target_function was not set in the Facade."
298+
)
299+
293300
if isinstance(self._runner, DaskParallelRunner) and data_to_scatter is not None:
294301
dask_data_to_scatter = dict(data_to_scatter=self._runner._client.scatter(data_to_scatter, broadcast=True))
295302
elif data_to_scatter is not None:
@@ -435,6 +442,12 @@ def _add_results(self) -> None:
435442
"""Adds results from the runner to the runhistory. Although most of the functionality could be written
436443
in the tell method, we separate it here to make it accessible for the automatic optimization procedure only.
437444
"""
445+
if self._runner is None:
446+
raise ValueError(
447+
"Runner is not set in SMBO. Likely issue is that the target_function was not set "
448+
"in the Facade. So we cannot query the runner for results."
449+
)
450+
438451
# Check if there is any result
439452
for trial_info, trial_value in self._runner.iter_results():
440453
# Add the results of the run to the run history
@@ -578,6 +591,11 @@ def validate(
578591
The averaged cost of the configuration. In case of multi-fidelity, the cost of each objective is
579592
averaged.
580593
"""
594+
if self._runner is None:
595+
raise ValueError(
596+
"Runner is not set in SMBO. Likely issue is that the target_function was not set in the Facade."
597+
)
598+
581599
if seed is None:
582600
seed = self._scenario.seed
583601

0 commit comments

Comments
 (0)