-
Notifications
You must be signed in to change notification settings - Fork 79
Refactor recommender #826
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: main
Are you sure you want to change the base?
Refactor recommender #826
Changes from all commits
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 |
|---|---|---|
| @@ -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,40 @@ | ||
| """Base protocol for all optimizers.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Protocol, runtime_checkable | ||
|
|
||
| from baybe.acquisition.base import AcquisitionFunction | ||
| from baybe.searchspace import SearchSpace | ||
|
|
||
| if TYPE_CHECKING: | ||
| from torch import Tensor | ||
|
|
||
|
|
||
| @runtime_checkable | ||
| class OptimizerProtocol(Protocol): | ||
| """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__ = () | ||
|
|
||
| def __call__( | ||
|
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. This interface is the critical part. Right now, it doesn't look wrong and seems to serve the purpose. Let's closely monitor this while developing the PR |
||
| self, | ||
| batch_size: int, | ||
| acquisition_function: AcquisitionFunction, | ||
| 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. | ||
| """ | ||
| ... | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| """Low-level optimizers of acquisition functions.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import gc | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from attrs import define, field | ||
| from attrs.validators import gt, instance_of | ||
|
|
||
| from baybe.recommenders.pure.bayesian.botorch.optimizers.base import OptimizerProtocol | ||
| from baybe.searchspace import SearchSpace | ||
| from baybe.utils.basic import flatten | ||
|
|
||
| if TYPE_CHECKING: | ||
| from botorch.acquisition import AcquisitionFunction as BoAcquisitionFunction | ||
| from torch import Tensor | ||
|
|
||
|
|
||
| @define(kw_only=True) | ||
| class GradientOptimizer(OptimizerProtocol): | ||
| """Acquisition function optimizer using gradient-based optimization.""" | ||
|
|
||
| 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**. | ||
| """ | ||
|
|
||
| sequential_continuous: bool = field(default=True) | ||
| """Flag defining whether to apply sequential greedy or batch optimization in | ||
| **continuous** search spaces. In discrete/hybrid spaces, sequential greedy | ||
| optimization is applied automatically. | ||
| """ | ||
|
|
||
| def __call__( | ||
|
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 think you are missing the |
||
| self, | ||
| batch_size: int, | ||
| acquisition_function: BoAcquisitionFunction, | ||
|
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. In contrast to the
|
||
| searchspace: SearchSpace, | ||
| fixed_parameters: dict[int, float] | None = None, | ||
| ) -> tuple[Tensor, Tensor]: | ||
| """Recommend from a search space using gradient-based optimization. | ||
|
|
||
| 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. | ||
|
|
||
| Raises: | ||
| NotImplementedError: If the search space has a discrete component. | ||
| ValueError: If the search space has cardinality constraints. | ||
| """ | ||
| import torch | ||
| from botorch.optim import optimize_acqf | ||
|
|
||
| if not searchspace.discrete.is_empty: | ||
| raise NotImplementedError( | ||
| "Gradient-based optimization is not implemented " | ||
| "for non-empty discrete search spaces." | ||
| ) | ||
|
|
||
| if searchspace.continuous.n_subsets > 0: | ||
| raise ValueError( | ||
| f"'{self.__class__.__name__}' " | ||
| f"expects a continuous subspace without cardinality constraints." | ||
| ) | ||
|
|
||
| points, acqf_values = optimize_acqf( | ||
| acq_function=acquisition_function, | ||
| bounds=torch.from_numpy( | ||
| searchspace.continuous.comp_rep_bounds.to_numpy(copy=True) | ||
| ), | ||
| q=batch_size, | ||
| num_restarts=self.n_restarts, | ||
| raw_samples=self.n_raw_samples, | ||
| fixed_features=fixed_parameters or None, | ||
| equality_constraints=flatten( | ||
| c.to_botorch( | ||
| searchspace.continuous.parameters, | ||
| batch_size=batch_size if c.is_interpoint else None, | ||
| ) | ||
| for c in searchspace.continuous.constraints_lin_eq | ||
| ) | ||
| or None, | ||
| inequality_constraints=flatten( | ||
| c.to_botorch( | ||
| searchspace.continuous.parameters, | ||
| batch_size=batch_size if c.is_interpoint else None, | ||
| ) | ||
| for c in searchspace.continuous.constraints_lin_ineq | ||
| ) | ||
| or None, | ||
| sequential=self.sequential_continuous, | ||
| ) | ||
|
|
||
| return points, acqf_values | ||
|
|
||
|
|
||
| # Collect leftover original slotted classes processed by `attrs.define` | ||
| gc.collect() | ||
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.
I think allowing
Nonehere is currently causing typing problems. I think the patter should actually just beoptimizer: OptimizerProtocolwhich is then automatically being set in the derived classes. That is, theBotorchRecommenderwould set the default direclty without the check for whether or not this isNone, and at some point, we might be able to give a reasonable default working for all kind of search spaces already here.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.
Was going in the same direction but more critically so:
Nonedoesn't make sense – we always need an optimization procedure. Without it, the recommender is non-functionalBotorchRecommenderaltogether, and instead use theBayesianRecommenderas a non-abstract class in the future. Reason: so far, theBotorchRecommenderencapsulated the specific botorch routines in its body. With the refactoring, this is no longer the case and the part is modularized. That is, we move from inheritance to composition --> aBayesianRecommendertakes over the role of the previousBotorchRecommenderwhen it is constructed with the corresponding botorch routines as arguments.