-
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
Draft
StefanPSchmid
wants to merge
2
commits into
dev/recommender
Choose a base branch
from
feature/refactor-recommender
base: dev/recommender
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.
Draft
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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
Some comments aren't visible on the classic Files Changed page.
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
7 changes: 7 additions & 0 deletions
7
baybe/recommenders/pure/bayesian/botorch/optimizers/__init__.py
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,7 @@ | ||
| """Acquisition function optimizers.""" | ||
|
|
||
| from baybe.recommenders.pure.bayesian.botorch.optimizers.basic import GradientOptimizer | ||
|
|
||
| __all__ = [ | ||
| "GradientOptimizer", | ||
| ] |
40 changes: 40 additions & 0 deletions
40
baybe/recommenders/pure/bayesian/botorch/optimizers/base.py
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,40 @@ | ||
| """Base protocol for all optimizers.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Protocol, runtime_checkable | ||
|
|
||
| from baybe.searchspace import SearchSpace | ||
|
|
||
| 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__ = () | ||
|
|
||
| def __call__( | ||
|
StefanPSchmid marked this conversation as resolved.
|
||
| self, | ||
| batch_size: int, | ||
| acquisition_function: BoAcquisitionFunction, | ||
|
StefanPSchmid marked this conversation as resolved.
|
||
| 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. | ||
| """ | ||
| ... | ||
111 changes: 111 additions & 0 deletions
111
baybe/recommenders/pure/bayesian/botorch/optimizers/basic.py
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,111 @@ | ||
| """Low-level optimizers of acquisition functions.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import gc | ||
| from typing import TYPE_CHECKING | ||
| from typing_extensions import override | ||
|
|
||
| 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. | ||
| """ | ||
|
|
||
| @override | ||
| def __call__( | ||
|
AdrianSosic marked this conversation as resolved.
|
||
| self, | ||
| batch_size: int, | ||
| acquisition_function: BoAcquisitionFunction, | ||
|
AVHopp marked this conversation as resolved.
|
||
| 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() | ||
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.
Comment from @AVHopp on other PR:
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.Comment from @AdrianSosic on other PR:
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.@StefanPSchmid's new thoughts/questions:
acquisition_function, which also can haveNoneat this field and is only set later to a sensible default. I was thinking of doing the same here, i.e. inrecommendthere would be a new functionsetup_optimizerthat sets a default (depending on the search space type) when the given optimizer isNone. The problem from my POV currently is that I am still trying to make it work that when the user gives some arguments toBotorchRecommenderfor theOptimizer(e.g.sequential_continuous), that they are arguments of the Recommender, and not available at the setup stage (if I understood the code directly).BayesianRecommenderand that gets then optimized with a specificOptimizer. Then there would also not be this dychtomy where these arguments should be. One thing of which I am very unsure is how this would be handled with backwards compatibility, currently trying to get it to work even when old interface ofBotorchRecommenderis called (see point above).So to summarize - I would remove the
BotorchRecommender, put the dispatching logic intoBayesianRecommenderand implement a_setup_acquisition_functionstyle function for the optimizer too. What do you think?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 dropping the
BotorchRecommenderis the right thing to do 👍🏼 At least from what I can currently overlook. But we need to ensure backward compatibility here, so we need some deprecation for it.Regarding the
Nonediscussion: I think it really depends on what we expect from theOptimizerobject. I think your current idea is that you want to dispatch between a conti/disc/hybrid optimizer depending on the given search space, in which case you'd indeed need a delayed default. However, the alternative is that we simply require the optimizer to be able to handle all three kinds of search spaces. While neither of the simple optimizers fulfills it, we'll later have composite optimizers that do so. For example, the alternating optimizer can handle all spaces (with purely conti/disc just being special cases where the sequence stops immediately after the first subspace). And due to the recursive interface, composite optimizers also fulfill the required type, meaning we could set an appropriate composite recommender as default. Similarly, all hybrid optimizers trivially also support purely conti/disc spaces.But this doesn't need to be decided now, we can follow up on it later when all puzzle pieces are in place.
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.
Note that I left some thoughts about this in the old PR, explaining a bit the potential role of
None(which might however not be accurate now that I see this discussion 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.
@AdrianSosic can you elaborate on why this is a breaking change already? Is it only because it is public? Because right now, it can still simply be ignored, right?
Let me see if I get @AdrianSosic's idea correctly and whether we can agree on it:
PureOptimizerwhich handle exactly one kind of space (probably not user facing) andCompositeOptimizerswhich combinePureOptimizers(or otherCompositeOptimizers?) to handle all kinds of search spacesCompositeOptimizeras default which knows how to handle all sorts of search spacesPureOptimizer. However, this will then be (automatically) be wrapped into aCompositeOptimizerI have to say that I am not sure what I think of this idea. While I like it from a code design perspective, it makes me wonder what other implications this would have. For example, wouldn't we then somehow implicitly assume that every search space is hybrid if our recommendation procedure always needs to know how to handle hybrid spaces? Also, for a user wanting to only do some optimization in a discrete space, it might be weird if things are being wrapped into some other object that could then also be used for other spaces.
Currently, I'd thus prefer the delayed setting of the optimizer based on the search space as this seems simpler to me and follows the design of the acquisition function - which makes sense, given that the optimizer is something that is also closely related to it.