-
Notifications
You must be signed in to change notification settings - Fork 79
First draft Continuous optimizer #828
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
base: dev/recommender
Are you sure you want to change the base?
Changes from 6 commits
0de44da
6cbdd39
5236c3b
0eed2f9
355ce33
9cb1d30
e3d2983
d34f290
3c44679
5d12775
ccad3c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,33 +3,57 @@ | |
| from __future__ import annotations | ||
|
|
||
| import gc | ||
| import warnings | ||
| from abc import ABC | ||
| from typing import TYPE_CHECKING | ||
| from collections.abc import Callable, Iterable | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| from attrs import define, field | ||
| from attrs import define, field, fields | ||
| from attrs.converters import optional | ||
| from attrs.validators import ge, gt, instance_of | ||
| from typing_extensions import override | ||
|
|
||
| from baybe.acquisition import qLogEI, qLogNEHVI | ||
| from baybe.acquisition.base import AcquisitionFunction | ||
| from baybe.acquisition.utils import convert_acqf | ||
| from baybe.exceptions import ( | ||
| IncompatibleAcquisitionFunctionError, | ||
| InfeasibilityError, | ||
| ) | ||
| from baybe.objectives.base import Objective | ||
| from baybe.recommenders.pure.base import PureRecommender | ||
| from baybe.searchspace import SearchSpace | ||
| from baybe.recommenders.pure.bayesian.continuous import ( | ||
| recommend_continuous_torch, | ||
| ) | ||
| from baybe.recommenders.pure.bayesian.discrete import ( | ||
| recommend_discrete_with_subsets, | ||
| recommend_discrete_without_subsets, | ||
| ) | ||
| from baybe.recommenders.pure.bayesian.hybrid import ( | ||
| recommend_hybrid_with_subsets, | ||
| recommend_hybrid_without_subsets, | ||
| ) | ||
| from baybe.recommenders.pure.bayesian.botorch.optimizers.base import OptimizerProtocol | ||
| from baybe.recommenders.pure.bayesian.botorch.optimizers.basic import GradientOptimizer | ||
| from baybe.searchspace import ( | ||
| SearchSpace, | ||
| SubspaceContinuous, | ||
| SubspaceDiscrete, | ||
| ) | ||
| from baybe.settings import Settings | ||
| from baybe.surrogates import GaussianProcessSurrogate | ||
| from baybe.surrogates.base import ( | ||
| Surrogate, | ||
| SurrogateProtocol, | ||
| ) | ||
| from baybe.utils.validation import preprocess_dataframe, validate_object_names | ||
| from baybe.utils.sampling_algorithms import DiscreteSamplingMethod | ||
|
|
||
| if TYPE_CHECKING: | ||
| from botorch.acquisition import AcquisitionFunction as BoAcquisitionFunction | ||
| from torch import Tensor | ||
|
|
||
|
|
||
| def _autoreplicate(surrogate: SurrogateProtocol, /) -> SurrogateProtocol: | ||
|
|
@@ -55,6 +79,39 @@ class BayesianRecommender(PureRecommender, ABC): | |
| ) | ||
| """The acquisition function. When omitted, a default is used.""" | ||
|
|
||
| optimizer: OptimizerProtocol = field( | ||
| alias="optimizer", | ||
| default=GradientOptimizer(), | ||
| ) | ||
| """The acquisition function optimizer.""" | ||
|
|
||
| #TODO: Move fields to respective optimizers | ||
| hybrid_sampler: DiscreteSamplingMethod | None = field( | ||
| converter=optional(DiscreteSamplingMethod), default=None | ||
| ) | ||
| """Strategy used for sampling the discrete subspace when performing hybrid search | ||
| space optimization.""" | ||
|
|
||
| sampling_percentage: float = field(default=1.0) | ||
| """Percentage of discrete search space that is sampled when performing hybrid search | ||
| space optimization. Ignored when ``hybrid_sampler="None"``.""" | ||
|
|
||
| n_restarts: int = field(validator=[instance_of(int), gt(0)], default=10) | ||
| """Number of times gradient-based optimization is restarted from different initial | ||
| points. **Does not affect purely discrete optimization**. | ||
| """ | ||
|
|
||
| n_raw_samples: int = field(validator=[instance_of(int), gt(0)], default=64) | ||
| """Number of raw samples drawn for the initialization heuristic in gradient-based | ||
| optimization. **Does not affect purely discrete optimization**. | ||
| """ | ||
|
|
||
| max_n_subsets: int = field(default=10, validator=[instance_of(int), ge(1)]) | ||
| """Maximum number of subsets to evaluate when subset-generating constraints are | ||
| present (e.g., continuous cardinality constraints). If the total number of | ||
| subsets exceeds this limit, a random subset of that size is sampled for | ||
| optimization instead of performing an exhaustive search.""" | ||
|
|
||
| # TODO: The objective is currently only required for validating the recommendation | ||
| # context. Once multi-target support is complete, we might want to refactor | ||
| # the validation mechanism, e.g. by | ||
|
|
@@ -196,6 +253,147 @@ def recommend( | |
| else: | ||
| raise | ||
|
|
||
| @override | ||
| def _recommend_discrete( | ||
|
Collaborator
Author
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. dispatching logic moved from BotorchRecommender to BayesianRecommender to prepare for removal of BotorchRecommender as discussed
Collaborator
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. Just one question: Do we then still need the @OverRide annotations? I do not think that those internal functions specialized to the individual search space types are defined on the level of the
Collaborator
Author
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. There are functions with that name in the |
||
| self, | ||
| subspace_discrete: SubspaceDiscrete, | ||
| candidates_exp: pd.DataFrame, | ||
| batch_size: int, | ||
| ) -> pd.Index: | ||
| """Generate recommendations from a discrete search space. | ||
|
|
||
| Dispatches to the appropriate optimization routine depending on whether | ||
| subset constraints are present. | ||
|
|
||
| Args: | ||
| subspace_discrete: The discrete subspace from which to generate | ||
| recommendations. | ||
| candidates_exp: The experimental representation of all discrete candidate | ||
| points to be considered. | ||
| batch_size: The size of the recommendation batch. | ||
|
|
||
| Returns: | ||
| The dataframe indices of the recommended points in the provided | ||
| experimental representation. | ||
| """ | ||
| if subspace_discrete.n_subsets > 0: | ||
| return recommend_discrete_with_subsets( | ||
| self, subspace_discrete, candidates_exp, batch_size | ||
| ) | ||
| return recommend_discrete_without_subsets( | ||
| self, subspace_discrete, candidates_exp, batch_size | ||
| ) | ||
|
|
||
| @override | ||
| def _recommend_continuous( | ||
| self, | ||
| subspace_continuous: SubspaceContinuous, | ||
| batch_size: int, | ||
| ) -> pd.DataFrame: | ||
| """Generate recommendations from a continuous search space. | ||
|
|
||
| Args: | ||
| subspace_continuous: The continuous subspace from which to generate | ||
| recommendations. | ||
| batch_size: The size of the recommendation batch. | ||
|
|
||
| Raises: | ||
| IncompatibleAcquisitionFunctionError: If a non-Monte Carlo acquisition | ||
| function is used with a batch size > 1. | ||
|
|
||
| Returns: | ||
| A dataframe containing the recommendations as individual rows. | ||
| """ | ||
| assert self._objective is not None | ||
| if ( | ||
| batch_size > 1 | ||
| and not self._get_acquisition_function(self._objective).supports_batching | ||
| ): | ||
| raise IncompatibleAcquisitionFunctionError( | ||
| f"The '{self.__class__.__name__}' only works with Monte Carlo " | ||
| f"acquisition functions for batch sizes > 1." | ||
| ) | ||
|
|
||
| points, _ = recommend_continuous_torch(self, subspace_continuous, batch_size) | ||
|
|
||
| return pd.DataFrame(points, columns=subspace_continuous.parameter_names) | ||
|
|
||
| @override | ||
| def _recommend_hybrid( | ||
| self, | ||
| searchspace: SearchSpace, | ||
| candidates_exp: pd.DataFrame, | ||
| batch_size: int, | ||
| ) -> pd.DataFrame: | ||
| """Generate recommendations from a hybrid search space. | ||
|
|
||
| Dispatches to the appropriate optimization routine depending on whether | ||
| subset constraints are present. | ||
|
|
||
| Args: | ||
| searchspace: The search space in which the recommendations should be made. | ||
| candidates_exp: The experimental representation of the candidates | ||
| of the discrete subspace. | ||
| batch_size: The size of the calculated batch. | ||
|
|
||
| Returns: | ||
| The recommended points. | ||
| """ | ||
| if searchspace.n_subsets > 0: | ||
| return recommend_hybrid_with_subsets( | ||
| self, searchspace, candidates_exp, batch_size | ||
| ) | ||
| return recommend_hybrid_without_subsets( | ||
| self, searchspace, candidates_exp, batch_size | ||
| ) | ||
|
|
||
| def _optimize_over_subsets( | ||
| self, | ||
| subset_callables: Iterable[Callable[[], tuple[Any, Tensor]]], | ||
| ) -> tuple[Any, Tensor]: | ||
| """Optimize across subsets and return the result with the best acqf value. | ||
|
|
||
| Each callable performs optimization for one subset configuration and returns | ||
| a ``(result, acquisition_value)`` tuple. Subsets that raise | ||
| ``InfeasibilityError`` are silently skipped. | ||
|
|
||
| Args: | ||
| subset_callables: An iterable of zero-argument callables. Each callable | ||
| runs the optimization for one subset and returns | ||
| ``(result, acqf_value)``. It may raise ``InfeasibilityError`` if the | ||
| subset is infeasible. | ||
|
|
||
| Raises: | ||
| InfeasibilityError: If none of the subsets has a feasible solution. | ||
|
|
||
| Returns: | ||
| The result and acquisition value of the best subset. | ||
| """ | ||
| from botorch.exceptions.errors import InfeasibilityError as BoInfeasibilityError | ||
|
|
||
| results_all: list = [] | ||
| acqf_values_all: list[Tensor] = [] | ||
|
|
||
| for optimize_fn in subset_callables: | ||
| try: | ||
| result, acqf_value = optimize_fn() | ||
| results_all.append(result) | ||
| acqf_values_all.append(acqf_value) | ||
| except (BoInfeasibilityError, InfeasibilityError): | ||
| pass | ||
|
|
||
| if not results_all: | ||
| raise InfeasibilityError( | ||
| "No feasible solution could be found. Potentially the specified " | ||
| "constraints are too restrictive, i.e. there may be too many " | ||
| "constraints or thresholds may have been set too tightly. " | ||
| "Consider relaxing the constraints to improve the chances " | ||
| "of finding a feasible solution." | ||
| ) | ||
|
|
||
| best_idx = np.argmax(acqf_values_all) | ||
| return results_all[best_idx], acqf_values_all[best_idx] | ||
|
|
||
| def acquisition_values( | ||
| self, | ||
| candidates: pd.DataFrame, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """Acquisition function optimizers.""" | ||
|
|
||
| from baybe.recommenders.pure.bayesian.botorch.optimizers.basic import GradientOptimizer | ||
|
|
||
| __all__ = [ | ||
| "GradientOptimizer", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """Base protocol for all optimizers.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Protocol, runtime_checkable, TypeAlias, ClassVar | ||
| from collections.abc import Callable | ||
|
|
||
| from baybe.searchspace import SearchSpace | ||
| from baybe.searchspace.core import SearchSpaceType | ||
|
|
||
| Optimand: TypeAlias = Callable[[Tensor], Tensor] | ||
| "Type alias for the callable to be optimized." | ||
|
|
||
| if TYPE_CHECKING: | ||
| from botorch.acquisition import AcquisitionFunction as BoAcquisitionFunction | ||
| from torch import Tensor | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class OptimizerProtocol(Protocol): | ||
|
Collaborator
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. Give this a Flag about the supported |
||
| """Type protocol specifying the interface optimizers need to implement.""" | ||
|
|
||
| # Use slots so that derived classes also remain slotted | ||
| # See also: https://www.attrs.org/en/stable/glossary.html#term-slotted-classes | ||
| __slots__ = () | ||
|
|
||
| compatibility: ClassVar[SearchSpaceType] | ||
| """Class variable reflecting the search space compatibility.""" | ||
|
|
||
| def __call__( | ||
|
StefanPSchmid marked this conversation as resolved.
|
||
| self, | ||
| batch_size: int, | ||
| acquisition_function: Optimand, | ||
|
Collaborator
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. I appreciate the type alias but I think the name
Collaborator
Author
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. After some brainstorming with @fabianliebig, how about
Collaborator
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. Or maybe something like CandidateScorer? |
||
| searchspace: SearchSpace, | ||
| fixed_parameters: dict[int, float] | None = None, | ||
| ) -> tuple[Tensor, Tensor]: | ||
| """Recommend a batch of points from the given search space. | ||
|
|
||
| Args: | ||
| batch_size: The size of the recommendation batch. | ||
| acquisition_function: The acquisition function to be optimized. | ||
| searchspace: The search space from which to generate recommendations. | ||
| fixed_parameters: A dictionary mapping parameter indices to fixed values. | ||
|
|
||
| Returns: | ||
| The recommendations and corresponding acquisition values. | ||
| """ | ||
| ... | ||
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.
fields moved from BotorchRecommender to BayesianRecommender for now, ultimate goal is to store them in the respective Optimizers where needed; preparation to remove BotorchRecommender (as discussed should be done in this PR, once we have decided on the BayesianRecommender looks)
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.
Reasonable to just have them here for the moment imo