Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update mlos_core API to support with multi-factor optimization #730

Merged
merged 92 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 88 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
2abf2c7
load ALL metrics from the DB when feeding the optimizer
motus Mar 22, 2024
63a4e15
disable incorrect pylint warning
motus Mar 22, 2024
93d4161
fix unit tests to handle the new data types returned from teh storage…
motus Mar 22, 2024
4adf433
register multi-dimensional results in optimizer.bulk_register()
motus Mar 22, 2024
9c16ba6
update unit tests to validate multi-dimensional input to Optimizer.bu…
motus Mar 22, 2024
7874fec
fix the key/value SELECT statement in SQL Storage implementaion
motus Mar 22, 2024
6cb1fb6
fix the input to grid search optimizer .bulk_register call
motus Mar 22, 2024
e5adef5
minor fix in GridSearchOptimizer
motus Mar 22, 2024
a79e555
remove unused import
motus Mar 22, 2024
b48c698
bugfix: take the optimization direction into account when loading fai…
motus Mar 22, 2024
3c29bed
handle -Inf in try_parse_val(). check it in unit tests
motus Mar 23, 2024
5015f75
move try_parse_val fix to a separate PR; undo the sign fix in MlosCor…
motus Mar 23, 2024
6b2804d
start updating the configs to include more than one optimization target
motus Mar 23, 2024
dc84cfa
Merge branch 'main' into sergiym/opt/multiobjective
motus Mar 25, 2024
e99c591
Merge branch 'main' into sergiym/opt/multiobjective
motus Mar 25, 2024
323fba8
Merge branch 'sergiym/opt/multiobjective' of github.com:motus/MLOS in…
motus Mar 25, 2024
838ca37
add a comment re: Row._tuple() pylint warning
motus Mar 25, 2024
623f012
Merge branch 'sergiym/opt/multiobjective' into sergiym/opt/multiobjec…
motus Mar 26, 2024
6661ac0
fix the optimization_target JSON schema; handle it properly in the ba…
motus Mar 26, 2024
dd44568
use NotImplementedError instead of NotImplemented
motus Mar 26, 2024
f1e989a
add type annotations
motus Mar 26, 2024
5f70f02
fix the json schema with the new optimizer_type format
motus Mar 26, 2024
9c20101
fix teh optimization_target schema
motus Mar 26, 2024
7e3d1d2
typo fixed
motus Mar 26, 2024
70790ce
use common optimization_target definition in all JSON schemas
motus Mar 26, 2024
3c513a3
implement multi-obje4ctive optimization all the way up to MlosCoreOpt…
motus Mar 29, 2024
d466aaf
fix all pylint/myoy warnings
motus Mar 29, 2024
c60ec07
minor pylint fix
motus Mar 29, 2024
39f7f22
make some unit tests work
motus Mar 29, 2024
3a89ade
fix opt_bulk_register_test tests to use multiple scores
motus Mar 29, 2024
492beec
more unit tests fixed
motus Mar 29, 2024
712f47d
bugfix: status check in Storage.Trial.update()
motus Mar 29, 2024
ec73367
remove some old backward-compatibility checks
motus Mar 29, 2024
3dba120
more fixes in unit tests
motus Mar 29, 2024
4311464
use multiple targets in grid search tests
motus Apr 24, 2024
e28feb3
Merge branch 'main' into sergiym/opt/multiobjective_config
bpkroth Apr 29, 2024
499da79
Merge branch 'main' of https://github.com/microsoft/MLOS into sergiym…
motus Apr 29, 2024
498635e
Merge branch 'sergiym/opt/multiobjective_config' of github.com:motus/…
motus Apr 29, 2024
b192c4a
Update mlos_bench/mlos_bench/config/experiments/README.md
motus Apr 29, 2024
a39cf70
optimization_target -> optimization_targets rename
motus Apr 29, 2024
eab796b
Merge branch 'sergiym/opt/multiobjective_config' of github.com:motus/…
motus Apr 29, 2024
13fe064
make sure the schema validation fails on old-style optimization_targe…
motus Apr 29, 2024
1665583
Merge branch 'sergiym/opt/multiobjective_config' into sergiym/opt/mul…
motus Apr 29, 2024
f78f110
Merge branch 'main' of https://github.com/microsoft/MLOS into sergiym…
motus Apr 30, 2024
30b1b1b
Update mlos_bench/mlos_bench/optimizers/base_optimizer.py
motus Apr 30, 2024
d44b8db
Update mlos_bench/mlos_bench/optimizers/base_optimizer.py
motus Apr 30, 2024
0492de6
Update mlos_bench/mlos_bench/storage/base_storage.py
motus Apr 30, 2024
00cb7a7
Update mlos_bench/mlos_bench/storage/base_storage.py
motus Apr 30, 2024
a15e34c
Update mlos_bench/mlos_bench/storage/base_storage.py
motus Apr 30, 2024
db97f44
use 'scores' and '_get_scores' (plural)
motus Apr 30, 2024
04d3015
Merge branch 'sergiym/opt/multiobjective_opt' of github.com:motus/MLO…
motus Apr 30, 2024
c3cf7cb
use Literal for optimization directions
motus Apr 30, 2024
8f93bb5
typo when initializing Optimizer._opt_targets
motus Apr 30, 2024
ee57343
add unit test to check the trial metadata
motus Apr 30, 2024
70ada96
check exp_data.objectives, too
motus Apr 30, 2024
6fc6c03
Update mlos_bench/mlos_bench/optimizers/base_optimizer.py
motus Apr 30, 2024
20c4255
linter suggestion
motus Apr 30, 2024
dc93ee5
roll back the suggestion (pylint complains)
motus Apr 30, 2024
b360ff5
pylint fix
motus Apr 30, 2024
eca6ea3
make sure the Optimizer.register() method adjusts the sign of the inp…
motus May 1, 2024
00b94a5
add test for failed trial scores sign adjustment
motus May 1, 2024
2bec97d
Update mlos_core API to support with multi-factor optimization and co…
motus May 1, 2024
34536a1
fixing mlos_core unit tests
motus May 2, 2024
4c72246
more fixes to mlos_core unit tests
motus May 2, 2024
1fe72b8
fix all mlos_core unit tests to use the new API
motus May 6, 2024
e8ad236
issue a warning instead of NotImplementedError on non-null context
motus May 6, 2024
ed8aaaa
fix all unit tests in mlos_core
motus May 6, 2024
52e985a
fix the scores df dtype; handle null context properly
motus May 7, 2024
9adc92f
better warning message for opt context
motus May 7, 2024
2f175a3
drop use of NoneType (not compat. w/3.8); better checks for context;…
motus May 7, 2024
59aa3f2
explicitly pass the names of the optimization targets to mlos_core
motus May 9, 2024
718a1ce
Merge branch 'main' of github.com:microsoft/MLOS into sergiym/opt/mob…
motus May 9, 2024
243ca75
update unit tests for one-hot encoding in mlos_core
motus May 9, 2024
f98a529
pass optimization_targets to OptimizerFactory.create() method
motus May 9, 2024
ae6e14b
fix mlos_core unit tests to explicitly specify optimization_targets
motus May 9, 2024
e4b6cca
add unit tests for multi-target optimization in mlos_core
motus May 10, 2024
feae228
pass N to get_best_observations()
motus May 10, 2024
ac1a28f
Merge branch 'main' into sergiym/opt/mobj_core
motus May 10, 2024
9e33b3f
pycodestyle fixes
motus May 11, 2024
35ac77a
Merge branch 'main' of https://github.com/microsoft/MLOS into sergiym…
motus May 13, 2024
3d5d1a4
code review fixes
motus May 13, 2024
51acda1
Merge branch 'main' of https://github.com/microsoft/MLOS into sergiym…
motus May 13, 2024
65b4007
add details to get_best_observations() docstring
motus May 13, 2024
a6c5a9d
minimize diff with master
motus May 13, 2024
36370d5
Merge branch 'main' of https://github.com/microsoft/MLOS into sergiym…
motus May 13, 2024
4c48459
use multi_objective_algorithm for SMAC
motus May 13, 2024
571c232
remove unused import
motus May 13, 2024
79039ac
Merge branch 'main' into sergiym/opt/mobj_core
motus May 20, 2024
d25e4ee
rollback the optimization context change: will do it explicitly in a …
motus May 20, 2024
438b9af
Merge branch 'sergiym/opt/mobj_core' of https://github.com/motus/MLOS…
motus May 20, 2024
a0dd714
do not pass optimization context (yet) in bulk_register
motus May 20, 2024
74495fc
add a comment and link to open issue about the values used for failed…
motus May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 36 additions & 20 deletions mlos_bench/mlos_bench/optimizers/mlos_core_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ def __init__(self,
service: Optional[Service] = None):
super().__init__(tunables, config, global_config, service)

# TODO: Remove after implementing multi-target optimization in mlos_core
if len(self._opt_targets) != 1:
raise NotImplementedError(f"Multi-target optimization is not supported: {self}")
(self._opt_target, self._opt_sign) = list(self._opt_targets.items())[0]

opt_type = getattr(OptimizerType, self._config.pop(
'optimizer_type', DEFAULT_OPTIMIZER_TYPE.name))

Expand Down Expand Up @@ -79,6 +74,7 @@ def __init__(self,

self._opt: BaseOptimizer = OptimizerFactory.create(
parameter_space=self.config_space,
optimization_targets=list(self._opt_targets),
optimizer_type=opt_type,
optimizer_kwargs=self._config,
space_adapter_type=space_adapter_type,
Expand All @@ -99,26 +95,42 @@ def bulk_register(self,
configs: Sequence[dict],
scores: Sequence[Optional[Dict[str, TunableValue]]],
status: Optional[Sequence[Status]] = None) -> bool:

if not super().bulk_register(configs, scores, status):
return False

df_configs = self._to_df(configs) # Impute missing values, if necessary
df_scores = pd.Series(
[self._extract_target(score) for score in scores],
dtype=float) * self._opt_sign

df_scores = self._adjust_signs_df(
pd.DataFrame([{} if score is None else score for score in scores]))

opt_targets = list(self._opt_targets)
if status is not None:
# Select only the completed trials, set scores for failed trials to +inf.
df_status = pd.Series(status)
df_scores[df_status != Status.SUCCEEDED] = float("inf")
df_scores.loc[df_status != Status.SUCCEEDED, opt_targets] = float("inf")
motus marked this conversation as resolved.
Show resolved Hide resolved
df_status_completed = df_status.apply(Status.is_completed)
df_configs = df_configs[df_status_completed]
df_scores = df_scores[df_status_completed]
self._opt.register(df_configs, df_scores)

# Pass opt_targets columns as scores, and the rest as context.
opt_context = set(df_scores.columns).difference(opt_targets)
motus marked this conversation as resolved.
Show resolved Hide resolved
self._opt.register(df_configs, df_scores[opt_targets].astype(float),
df_scores[list(opt_context)] if opt_context else None)

if _LOG.isEnabledFor(logging.DEBUG):
(score, _) = self.get_best_observation()
_LOG.debug("Warm-up END: %s :: %s", self, score)

return True

def _extract_target(self, scores: Optional[Dict[str, TunableValue]]) -> Optional[TunableValue]:
return None if scores is None else scores[self._opt_target]
def _adjust_signs_df(self, df_scores: pd.DataFrame) -> pd.DataFrame:
"""
In-place adjust the signs of the scores for MINIMIZATION problem.
"""
for (opt_target, opt_dir) in self._opt_targets.items():
df_scores[opt_target] *= opt_dir
return df_scores

def _to_df(self, configs: Sequence[Dict[str, TunableValue]]) -> pd.DataFrame:
"""
Expand Down Expand Up @@ -175,21 +187,25 @@ def suggest(self) -> TunableGroups:

def register(self, tunables: TunableGroups, status: Status,
score: Optional[Dict[str, TunableValue]] = None) -> Optional[Dict[str, float]]:
registered_score = super().register(tunables, status, score) # With _opt_sign applied
registered_score = super().register(tunables, status, score) # Sign-adjusted for MINIMIZATION
if status.is_completed():
assert registered_score is not None
df_config = self._to_df([tunables.get_param_values()])
_LOG.debug("Score: %s Dataframe:\n%s", registered_score, df_config)
self._opt.register(df_config, pd.Series([registered_score[self._opt_target]], dtype=float))
opt_context = set(score or {}).difference(registered_score)
motus marked this conversation as resolved.
Show resolved Hide resolved
self._opt.register(
df_config,
pd.DataFrame([registered_score], dtype=float),
pd.DataFrame([{col: score[col] for col in opt_context}])
if score and opt_context else None
)
return registered_score

def get_best_observation(self) -> Union[Tuple[Dict[str, float], TunableGroups], Tuple[None, None]]:
df_config = self._opt.get_best_observation()
(df_config, df_score, _df_context) = self._opt.get_best_observations()
if len(df_config) == 0:
return (None, None)
params = configspace_data_to_tunable_values(df_config.iloc[0].to_dict())
_LOG.debug("Best observation: %s", params)
score = params.pop("score")
assert score is not None
score = float(score) * self._opt_sign # mlos_core always uses the `score` column
return ({self._opt_target: score}, self._tunables.copy().assign(params))
scores = self._adjust_signs_df(df_score).iloc[0].to_dict()
_LOG.debug("Best observation: %s score: %s", params, scores)
return (scores, self._tunables.copy().assign(params))
6 changes: 5 additions & 1 deletion mlos_core/mlos_core/optimizers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from enum import Enum
from typing import Optional, TypeVar
from typing import List, Optional, TypeVar

import ConfigSpace

Expand Down Expand Up @@ -62,6 +62,7 @@ class OptimizerFactory:
@staticmethod
def create(*,
parameter_space: ConfigSpace.ConfigurationSpace,
optimization_targets: List[str],
motus marked this conversation as resolved.
Show resolved Hide resolved
optimizer_type: OptimizerType = DEFAULT_OPTIMIZER_TYPE,
optimizer_kwargs: Optional[dict] = None,
space_adapter_type: SpaceAdapterType = SpaceAdapterType.IDENTITY,
Expand All @@ -74,6 +75,8 @@ def create(*,
----------
parameter_space : ConfigSpace.ConfigurationSpace
Input configuration space.
optimization_targets : List[str]
The names of the optimization targets to minimize.
optimizer_type : OptimizerType
Optimizer class as defined by Enum.
optimizer_kwargs : Optional[dict]
Expand Down Expand Up @@ -102,6 +105,7 @@ def create(*,

optimizer: ConcreteOptimizer = optimizer_type.value(
parameter_space=parameter_space,
optimization_targets=optimization_targets,
space_adapter=space_adapter,
**optimizer_kwargs
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class SmacOptimizer(BaseBayesianOptimizer):

def __init__(self, *, # pylint: disable=too-many-locals
parameter_space: ConfigSpace.ConfigurationSpace,
optimization_targets: List[str],
space_adapter: Optional[BaseSpaceAdapter] = None,
seed: Optional[int] = 0,
run_name: Optional[str] = None,
Expand All @@ -46,6 +47,9 @@ def __init__(self, *, # pylint: disable=too-many-locals
parameter_space : ConfigSpace.ConfigurationSpace
The parameter space to optimize.

optimization_targets : List[str]
The names of the optimization targets to minimize.

space_adapter : BaseSpaceAdapter
The space adapter class to employ for parameter space transformations.

Expand Down Expand Up @@ -86,6 +90,7 @@ def __init__(self, *, # pylint: disable=too-many-locals
"""
super().__init__(
parameter_space=parameter_space,
optimization_targets=optimization_targets,
space_adapter=space_adapter,
)

Expand Down Expand Up @@ -125,6 +130,7 @@ def __init__(self, *, # pylint: disable=too-many-locals

scenario: Scenario = Scenario(
self.optimizer_parameter_space,
objectives=self._optimization_targets,
name=run_name,
output_directory=Path(output_directory),
deterministic=True,
Expand Down Expand Up @@ -186,6 +192,10 @@ def __init__(self, *, # pylint: disable=too-many-locals
intensifier=intensifier,
random_design=random_design,
config_selector=config_selector,
multi_objective_algorithm=Optimizer_Smac.get_multi_objective_algorithm(
scenario,
# objective_weights=[1, 2], # TODO: pass weights as constructor args
),
overwrite=True,
logging_level=False, # Use the existing logger
)
Expand Down Expand Up @@ -228,15 +238,16 @@ def _dummy_target_func(config: ConfigSpace.Configuration, seed: int = 0) -> None
# -- this planned to be fixed in some future release: https://github.com/automl/SMAC3/issues/946
raise RuntimeError('This function should never be called.')

def _register(self, configurations: pd.DataFrame, scores: pd.Series, context: Optional[pd.DataFrame] = None) -> None:
def _register(self, configurations: pd.DataFrame,
scores: pd.DataFrame, context: Optional[pd.DataFrame] = None) -> None:
"""Registers the given configurations and scores.

Parameters
----------
configurations : pd.DataFrame
Dataframe of configurations / parameters. The columns are parameter names and the rows are the configurations.

scores : pd.Series
scores : pd.DataFrame
Scores from running the configurations. The index is the same as the index of the configurations.

context : pd.DataFrame
Expand All @@ -248,10 +259,11 @@ def _register(self, configurations: pd.DataFrame, scores: pd.Series, context: Op
warn(f"Not Implemented: Ignoring context {list(context.columns)}", UserWarning)
motus marked this conversation as resolved.
Show resolved Hide resolved

# Register each trial (one-by-one)
for config, score in zip(self._to_configspace_configs(configurations), scores.tolist()):
for (config, (_i, score)) in zip(self._to_configspace_configs(configurations), scores.iterrows()):
# Retrieve previously generated TrialInfo (returned by .ask()) or create new TrialInfo instance
info: TrialInfo = self.trial_info_map.get(config, TrialInfo(config=config, seed=self.base_optimizer.scenario.seed))
value: TrialValue = TrialValue(cost=score, time=0.0, status=StatusType.SUCCESS)
info: TrialInfo = self.trial_info_map.get(
config, TrialInfo(config=config, seed=self.base_optimizer.scenario.seed))
value = TrialValue(cost=list(score.astype(float)), time=0.0, status=StatusType.SUCCESS)
self.base_optimizer.tell(info, value, save=False)

# Save optimizer once we register all configs
Expand Down Expand Up @@ -293,7 +305,7 @@ def surrogate_predict(self, configurations: pd.DataFrame, context: Optional[pd.D
if context is not None:
warn(f"Not Implemented: Ignoring context {list(context.columns)}", UserWarning)
if self._space_adapter and not isinstance(self._space_adapter, IdentityAdapter):
raise NotImplementedError()
raise NotImplementedError("Space adapter not supported for surrogate_predict.")

# pylint: disable=protected-access
if len(self._observations) <= self.base_optimizer._initial_design._n_configs:
Expand Down
28 changes: 20 additions & 8 deletions mlos_core/mlos_core/optimizers/flaml_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Contains the FlamlOptimizer class.
"""

from typing import Dict, NamedTuple, Optional, Union
from typing import Dict, List, NamedTuple, Optional, Union
from warnings import warn

import ConfigSpace
Expand All @@ -32,17 +32,22 @@ class FlamlOptimizer(BaseOptimizer):

def __init__(self, *,
parameter_space: ConfigSpace.ConfigurationSpace,
optimization_targets: List[str],
space_adapter: Optional[BaseSpaceAdapter] = None,
low_cost_partial_config: Optional[dict] = None,
seed: Optional[int] = None):
"""
Create an MLOS wrapper class for FLAML.
Create an MLOS wrapper for FLAML.

Parameters
----------
parameter_space : ConfigSpace.ConfigurationSpace
The parameter space to optimize.

optimization_targets : List[str]
The names of the optimization targets to minimize.
For FLAML it must be a list with a single element, e.g., `["score"]`.

space_adapter : BaseSpaceAdapter
The space adapter class to employ for parameter space transformations.

Expand All @@ -55,9 +60,14 @@ def __init__(self, *,
"""
super().__init__(
parameter_space=parameter_space,
optimization_targets=optimization_targets,
space_adapter=space_adapter,
)

if len(self._optimization_targets) != 1:
raise ValueError("FLAML does not support multi-target optimization")
motus marked this conversation as resolved.
Show resolved Hide resolved
self._flaml_optimization_target = self._optimization_targets[0]

# Per upstream documentation, it is recommended to set the seed for
# flaml at the start of its operation globally.
if seed is not None:
Expand All @@ -72,7 +82,7 @@ def __init__(self, *,
self.evaluated_samples: Dict[ConfigSpace.Configuration, EvaluatedSample] = {}
self._suggested_config: Optional[dict]

def _register(self, configurations: pd.DataFrame, scores: pd.Series,
def _register(self, configurations: pd.DataFrame, scores: pd.DataFrame,
context: Optional[pd.DataFrame] = None) -> None:
"""Registers the given configurations and scores.

Expand All @@ -81,15 +91,16 @@ def _register(self, configurations: pd.DataFrame, scores: pd.Series,
configurations : pd.DataFrame
Dataframe of configurations / parameters. The columns are parameter names and the rows are the configurations.

scores : pd.Series
scores : pd.DataFrame
Scores from running the configurations. The index is the same as the index of the configurations.

context : None
Not Yet Implemented.
"""
if context is not None:
warn(f"Not Implemented: Ignoring context {list(context.columns)}", UserWarning)
for (_, config), score in zip(configurations.astype('O').iterrows(), scores):
for (_, config), score in zip(configurations.astype('O').iterrows(),
scores[self._flaml_optimization_target]):
cs_config: ConfigSpace.Configuration = ConfigSpace.Configuration(
self.optimizer_parameter_space, values=config.to_dict())
if cs_config in self.evaluated_samples:
Expand Down Expand Up @@ -140,7 +151,7 @@ def _target_function(self, config: dict) -> Union[dict, None]:
"""
cs_config = normalize_config(self.optimizer_parameter_space, config)
if cs_config in self.evaluated_samples:
return {'score': self.evaluated_samples[cs_config].score}
return {self._flaml_optimization_target: self.evaluated_samples[cs_config].score}

self._suggested_config = dict(cs_config) # Cleaned-up version of the config
return None # Returning None stops the process
Expand All @@ -156,7 +167,8 @@ def _get_next_config(self) -> dict:
Returns
-------
result: dict
Dictionary with a single key, `score`, if config already evaluated; `None` otherwise.
A dictionary with a single key that is equal to the name of the optimization target,
if config already evaluated; `None` otherwise.

Raises
------
Expand All @@ -182,7 +194,7 @@ def _get_next_config(self) -> dict:
self._target_function,
config=self.flaml_parameter_space,
mode='min',
metric='score',
metric=self._flaml_optimization_target,
points_to_evaluate=points_to_evaluate,
evaluated_rewards=evaluated_rewards,
num_samples=len(points_to_evaluate) + 1,
Expand Down
Loading
Loading