-
-
Notifications
You must be signed in to change notification settings - Fork 239
MO-SMAC merge #1222
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
Open
jeroenrook
wants to merge
71
commits into
automl:development
Choose a base branch
from
jeroenrook:mosmac-merge
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
MO-SMAC merge #1222
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
f6ee35b
Add MO facade with todos
2b97fca
Add NoAggregatuonStrategy
556ad37
Update aggregation strategy
672389f
Limit value to bounds region
09160b7
Factor out creating a unique list
1359f19
More debug logging
733f94d
Factor out sorting of costs
3e015c0
Better docstring
171958b
Add MO acq maximizer
0fe8e7d
Update acq optimizer
0059155
Stop local search after max steps is reached
5b0a1bf
Abstract away population trimming and pareto front calculation
a0bed50
Add MO intensifier draft
325cb5c
Add comment
227ceb7
Add todos
c320f04
Pass rh's incumbents to acquisition function
67eefec
Add incumbents data structure in runhistory
b297a98
Add property for incumbents
6042bed
Add EHVI acq fun
a96172d
Update PHVI
75a2077
Add ACLib runner draft
4b2d101
Merge branch 'development' into mosmac
jeroenrook a5902d5
Native objective support
jeroenrook 5e7d880
Fix typo
jeroenrook 3cdf96a
Initial modifications for mo facade
jeroenrook 087d7c8
Make the HV based acquisition functions work
jeroenrook 1b20106
Logic fix
jeroenrook a057733
AClib runner
jeroenrook 6c0bcd1
AClib runner fixes
jeroenrook 71409ce
MO utils initial expansion
jeroenrook 0587938
MO intensifier
jeroenrook d05fc42
Merge branch 'development' into mosmac
jeroenrook bd31d32
Expanded debugging message
jeroenrook 4322cfb
Allow saving the intensifier when no incumbent is chosen yet.
jeroenrook 6113c18
Bugfix for passing checks when MO model with features
jeroenrook 8cd499f
Added support to retrain the surrogate model and acquisition loop in …
jeroenrook a26b7c9
Added a minimal number of configuration that need to be yielded befor…
jeroenrook 37ae763
Remove sleep call used for testing
jeroenrook 9b85222
Only compute Pareto fronts on the same subset of isb_keys.
jeroenrook 8c114c0
Compute actual isb differences
jeroenrook 2bc7383
Aclib runner
jeroenrook 6ddc94c
Reset counter when retrain is triggered
jeroenrook 24a749f
Comparison on one config from the incumbent
jeroenrook 944425b
Make dask runner work
jeroenrook 8496461
Added different intermediate update methods that can be mixed with th…
jeroenrook da0bb6b
Make normalization of costs in the mo setting a choice
jeroenrook 2ca601c
In the native MO setting the EPM are trained by using the costs retri…
jeroenrook 603182a
Generic HVI class
jeroenrook a109f48
Decomposed the intensifier decision logic and created mixins to easil…
jeroenrook 17ce0a3
Changed the intensifier
jeroenrook fd317b0
Commit everythin
jeroenrook b50db2b
csvs
jeroenrook 38b22d4
Merge remote-tracking branch 'origin/main' into mosmac
jeroenrook 69d466b
README change
jeroenrook fdd33f6
README change
jeroenrook bf2a2f0
Even bigger push
jeroenrook 1d71cf4
Merge remote-tracking branch 'origin/development' into mosmac-merge
jeroenrook 7d7290d
Remove EHVI acquisition function
jeroenrook aec7609
README
jeroenrook cb9eab6
Fix failing tests. Disentangle normalisation and aggregation
jeroenrook 373dc08
Fix failing pytests
jeroenrook 85f822a
Merge remote-tracking branch 'automl/development' into mosmac-merge
cc2762d
resolving tests
c6c4b8b
intensifier fix for MF. Passes tests
f390582
fix merging retrain. test passes
04643e5
format: ruff
benjamc dd2ff58
build(setup.py): add dependency pygmo
benjamc 5b0c318
style: pydocstyle, flake
benjamc 2fab658
readd paretofront
benjamc 31eda8c
refactor(pareto_front.py): delete illegal functions
benjamc cf824ae
fix some mypy
benjamc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rename file to hypervolume |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,329 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
import numpy as np | ||
import pygmo | ||
from ConfigSpace import Configuration | ||
|
||
from smac.acquisition.function.abstract_acquisition_function import ( | ||
AbstractAcquisitionFunction, | ||
) | ||
from smac.runhistory import RunHistory | ||
from smac.runhistory.encoder import AbstractRunHistoryEncoder | ||
from smac.utils.logging import get_logger | ||
from smac.utils.multi_objective import normalize_costs | ||
|
||
# import torch | ||
# from botorch.acquisition.multi_objective import ExpectedHypervolumeImprovement | ||
# from botorch.models.model import Model | ||
# from botorch.utils.multi_objective.box_decompositions.non_dominated import ( | ||
# NondominatedPartitioning, | ||
# ) | ||
|
||
__copyright__ = "Copyright 2022, automl.org" | ||
__license__ = "3-clause BSD" | ||
|
||
logger = get_logger(__name__) | ||
|
||
# class _PosteriorProxy(object): | ||
# def __init__(self) -> None: | ||
# self.mean: Tensor = [] | ||
# self.variance: Tensor = [] | ||
|
||
# class _ModelProxy(Model, ABC): | ||
# def __init__(self, model: AbstractModel, objective_bounds: list[tuple[float, float]]): | ||
# super(_ModelProxy).__init__() | ||
# self.model = model | ||
# self._objective_bounds = objective_bounds | ||
# | ||
# def posterior(self, X: Tensor, **kwargs: Any) -> _PosteriorProxy: | ||
# """Docstring | ||
# X: A `b x q x d`-dim Tensor, where `d` is the dimension of the | ||
# feature space, `q` is the number of points considered jointly, | ||
# and `b` is the batch dimension. | ||
# | ||
# | ||
# A `Posterior` object, representing a batch of `b` joint distributions | ||
# over `q` points and `m` outputs each. | ||
# """ | ||
# assert X.shape[1] == 1 | ||
# X = X.reshape([X.shape[0], -1]).numpy() # 3D -> 2D | ||
# | ||
# # predict | ||
# # start_time = time.time() | ||
# # print(f"Start predicting ") | ||
# mean, var_ = self.model.predict_marginalized(X) | ||
# normalized_mean = np.array([normalize_costs(m, self._objective_bounds) for m in mean]) | ||
# scale = normalized_mean / mean | ||
# var_ *= scale # Scale variance accordingly | ||
# mean = normalized_mean | ||
jeroenrook marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# # print(f"Done in {time.time() - start_time}s") | ||
# post = _PosteriorProxy() | ||
# post.mean = torch.asarray(mean).reshape(X.shape[0], 1, -1) # 2D -> 3D | ||
# post.variance = torch.asarray(var_).reshape(X.shape[0], 1, -1) # 2D -> 3D | ||
# | ||
# return post | ||
|
||
|
||
class AbstractHVI(AbstractAcquisitionFunction): | ||
def __init__(self): | ||
"""Computes for a given x the predicted hypervolume improvement as | ||
acquisition value. | ||
""" | ||
super(AbstractHVI, self).__init__() | ||
self._required_updates = ("model",) | ||
self._reference_point = None | ||
self._objective_bounds = None | ||
|
||
self._runhistory: RunHistory | None = None | ||
self._runhistory_encoder: AbstractRunHistoryEncoder | None = None | ||
|
||
@property | ||
def runhistory(self) -> RunHistory: | ||
"""Return the runhistory.""" | ||
return self._runhistory | ||
|
||
@runhistory.setter | ||
def runhistory(self, runhistory: RunHistory): | ||
self._runhistory = runhistory | ||
|
||
@property | ||
def runhistory_encoder(self) -> AbstractRunHistoryEncoder: | ||
"""Return the runhistory encoder.""" | ||
return self._runhistory_encoder | ||
|
||
@runhistory_encoder.setter | ||
def runhistory_encoder(self, runhistory_encoder: AbstractRunHistoryEncoder): | ||
self._runhistory_encoder = runhistory_encoder | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Return name of the acquisition function.""" | ||
return "Abstract Hypervolume Improvement" | ||
|
||
def _update(self, **kwargs: Any) -> None: | ||
super(AbstractHVI, self)._update(**kwargs) | ||
|
||
incumbents: list[Configuration] = kwargs.get("incumbents", None) | ||
if incumbents is None: | ||
raise ValueError("Incumbents are not passed properly.") | ||
if len(incumbents) == 0: | ||
raise ValueError( | ||
"No incumbents here. Did the intensifier properly update the incumbents in the runhistory?" | ||
) | ||
|
||
objective_bounds = np.array(self.runhistory.objective_bounds) | ||
self._objective_bounds = self.runhistory_encoder.transform_response_values(objective_bounds) | ||
self._reference_point = [1.1] * len(self._objective_bounds) | ||
|
||
def get_hypervolume(self, points: np.ndarray = None, reference_point: list = None) -> float: | ||
""" | ||
Compute the hypervolume | ||
|
||
Parameters | ||
---------- | ||
points : np.ndarray | ||
A 2d numpy array. 1st dimension is an entity and the 2nd dimension are the costs | ||
reference_point : list | ||
|
||
Return | ||
------ | ||
hypervolume: float | ||
""" | ||
# Normalize the objectives here to give equal attention to the objectives when computing the HV | ||
points = [normalize_costs(p, self._objective_bounds) for p in points] | ||
|
||
hv = pygmo.hypervolume(points) | ||
# if reference_point is None: | ||
# self._reference_point = hv.refpoint(offset=1) | ||
return hv.compute(self._reference_point) | ||
|
||
def _compute(self, X: np.ndarray) -> np.ndarray: | ||
"""Computes the PHVI values and its derivatives. | ||
|
||
Parameters | ||
---------- | ||
X: np.ndarray(N, D), The input points where the acquisition function | ||
should be evaluated. The dimensionality of X is (N, D), with N as | ||
the number of points to evaluate at and D is the number of | ||
dimensions of one X. | ||
|
||
Returns | ||
------- | ||
np.ndarray(N,1) | ||
Expected HV Improvement of X | ||
""" | ||
if len(X.shape) == 1: | ||
X = X[:, np.newaxis] | ||
|
||
# TODO non-dominated sorting of costs. Compute EHVI only until the EHVI is not expected to improve anymore. | ||
# Option 1: Supplement missing instances of population with acq. function to get predicted performance over | ||
# all instances. Idea is this prevents optimizing for the initial instances which get it stuck in local optima | ||
# Option 2: Only on instances of population | ||
# Option 3: EVHI per instance and aggregate afterwards | ||
mean, var_ = self.model.predict_marginalized(X) # Expected to be not normalized | ||
|
||
phvi = np.zeros(len(X)) | ||
for i, indiv in enumerate(mean): | ||
points = list(self.population_costs) + [indiv] | ||
hv = self.get_hypervolume(points) | ||
phvi[i] = hv - self.population_hv | ||
|
||
# if len(X) == 10000: | ||
# for op in ["max", "min", "mean", "median"]: | ||
# val = getattr(np, op)(phvi) | ||
# print(f"{op:6} - {val}") | ||
# time.sleep(1.5) | ||
|
||
return phvi.reshape(-1, 1) | ||
|
||
|
||
# class EHVI(AbstractHVI): | ||
# def __init__(self): | ||
# super(EHVI, self).__init__() | ||
# self._ehvi: ExpectedHypervolumeImprovement | None = None | ||
# | ||
# @property | ||
# def name(self) -> str: | ||
# return "Expected Hypervolume Improvement" | ||
# | ||
# def _update(self, **kwargs: Any) -> None: | ||
# super(EHVI, self)._update(**kwargs) | ||
# incumbents: list[Configuration] = kwargs.get("incumbents", None) | ||
# | ||
# # Update EHVI | ||
# # Prediction all | ||
# population_configs = incumbents | ||
# population_X = np.array([config.get_array() for config in population_configs]) | ||
# population_costs, _ = self.model.predict_marginalized(population_X) | ||
# # Normalize the objectives here to give equal attention to the objectives when computing the HV | ||
# population_costs = [normalize_costs(p, self._objective_bounds) for p in population_costs] | ||
# | ||
# # BOtorch EHVI implementation | ||
# bomodel = _ModelProxy(self.model, self._objective_bounds) | ||
# # ref_point = pygmo.hypervolume(population_costs).refpoint( | ||
# # offset=1 | ||
# # ) # TODO get proper reference points from user/cutoffs | ||
# ref_point = [1.1] * len(self._objective_bounds) | ||
# # ref_point = torch.asarray(ref_point) | ||
# # TODO partition from all runs instead of only population? | ||
# # TODO NondominatedPartitioning and ExpectedHypervolumeImprovement seem no too difficult to implement natively | ||
# # TODO pass along RNG | ||
# # Transfrom the objective space to cells based on the population | ||
# partitioning = NondominatedPartitioning(torch.asarray(ref_point), torch.asarray(population_costs)) | ||
# self._ehvi = ExpectedHypervolumeImprovement(bomodel, ref_point, partitioning) | ||
# | ||
# def _compute(self, X: np.ndarray) -> np.ndarray: | ||
# """Computes the EHVI values and its derivatives. | ||
# | ||
# Parameters | ||
# ---------- | ||
# X: np.ndarray(N, D), The input points where the acquisition function | ||
# should be evaluated. The dimensionality of X is (N, D), with N as | ||
# the number of points to evaluate at and D is the number of | ||
# dimensions of one X. | ||
# | ||
# Returns | ||
# ------- | ||
# np.ndarray(N,1) | ||
# Expected HV Improvement of X | ||
# """ | ||
# if self._ehvi is None: | ||
# raise ValueError(f"The expected hypervolume improvement is not defined yet. Call self.update.") | ||
# | ||
# if len(X.shape) == 1: | ||
# X = X[:, np.newaxis] | ||
# | ||
# # m, var_ = self.model.predict_marginalized_over_instances(X) | ||
# # Find a way to propagate the variance into the HV | ||
# boX = torch.asarray(X).reshape(X.shape[0], 1, -1) # 2D -> #3D | ||
# improvements = self._ehvi(boX).numpy().reshape(-1, 1) # TODO here are the expected hv improvements computed. | ||
# return improvements | ||
# | ||
# # TODO non-dominated sorting of costs. Compute EHVI only until the EHVI is not expected to improve anymore. | ||
# # Option 1: Supplement missing instances of population with acq. function to get predicted performance over | ||
# # all instances. Idea is this prevents optimizing for the initial instances which get it stuck in local optima | ||
# # Option 2: Only on instances of population | ||
# # Option 3: EVHI per instance and aggregate afterwards | ||
# # ehvi = np.zeros(len(X)) | ||
# # for i, indiv in enumerate(m): | ||
# # ehvi[i] = self.get_hypervolume(population_costs + [indiv]) - population_hv | ||
# # | ||
# # return ehvi.reshape(-1, 1) | ||
|
||
|
||
class PHVI(AbstractHVI): | ||
def __init__(self): | ||
super(PHVI, self).__init__() | ||
self.population_hv = None | ||
self.population_costs = None | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Return name of the acquisition function.""" | ||
return "Predicted Hypervolume Improvement" | ||
|
||
def _update(self, **kwargs: Any) -> None: | ||
super(PHVI, self)._update(**kwargs) | ||
incumbents: list[Configuration] = kwargs.get("incumbents", None) | ||
|
||
# Update PHVI | ||
# Prediction all | ||
population_configs = incumbents | ||
population_X = np.array([config.get_array() for config in population_configs]) | ||
population_costs, _ = self.model.predict_marginalized(population_X) | ||
|
||
# Compute HV | ||
population_hv = self.get_hypervolume(population_costs) | ||
|
||
self.population_costs = population_costs | ||
self.population_hv = population_hv | ||
|
||
logger.info(f"New population HV: {population_hv}") | ||
|
||
def get_hypervolume(self, points: np.ndarray = None, reference_point: list = None) -> float: | ||
""" | ||
Compute the hypervolume | ||
|
||
Parameters | ||
---------- | ||
points : np.ndarray | ||
A 2d numpy array. 1st dimension is an entity and the 2nd dimension are the costs | ||
reference_point : list | ||
|
||
Return | ||
------ | ||
hypervolume: float | ||
""" | ||
# Normalize the objectives here to give equal attention to the objectives when computing the HV | ||
points = [normalize_costs(p, self._objective_bounds) for p in points] | ||
hv = pygmo.hypervolume(points) | ||
return hv.compute(self._reference_point) | ||
|
||
def _compute(self, X: np.ndarray) -> np.ndarray: | ||
"""Computes the PHVI values and its derivatives. | ||
|
||
Parameters | ||
---------- | ||
X: np.ndarray(N, D), The input points where the acquisition function | ||
should be evaluated. The dimensionality of X is (N, D), with N as | ||
the number of points to evaluate at and D is the number of | ||
dimensions of one X. | ||
|
||
Returns | ||
------- | ||
np.ndarray(N,1) | ||
Expected HV Improvement of X | ||
""" | ||
if len(X.shape) == 1: | ||
X = X[:, np.newaxis] | ||
|
||
mean, _ = self.model.predict_marginalized(X) # Expected to be not normalized | ||
phvi = np.zeros(len(X)) | ||
for i, indiv in enumerate(mean): | ||
points = list(self.population_costs) + [indiv] | ||
hv = self.get_hypervolume(points) | ||
phvi[i] = hv - self.population_hv | ||
|
||
return phvi.reshape(-1, 1) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works