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

Simplify and extend meta recommender logic #457

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1fe2cad
Add test case for switching hysteresis
AdrianSosic Dec 13, 2024
2008ba7
Adjust logic to make the hysteresis test pass
AdrianSosic Dec 13, 2024
36ef187
Update __str__ method
AdrianSosic Dec 13, 2024
87ccd41
Make arguments to MetaRecommender.select_recommender optional
AdrianSosic Dec 17, 2024
8d0a840
Add Partition class
AdrianSosic Dec 17, 2024
42d038a
Add BatchSizeControlledMetaRecommender class
AdrianSosic Dec 17, 2024
b947bc2
Remove current state logic of meta recommenders
AdrianSosic Dec 18, 2024
f70a7aa
Reimplement SequentialMetaRecommender logic and test
AdrianSosic Dec 20, 2024
a684662
Reimplement StreamingSequentialMetaRecommender
AdrianSosic Dec 20, 2024
0741be1
Improve docstrings
AdrianSosic Dec 20, 2024
cae0869
Adjust recommender retrieval in Campaign class
AdrianSosic Dec 20, 2024
5b32fcb
Add class variable indicating statefulness of the recommender type
AdrianSosic Jan 10, 2025
7aa3b14
Replace default factory method with inline factory
AdrianSosic Jan 10, 2025
a799ae5
Use None for unitialized step indicator
AdrianSosic Jan 10, 2025
2124c9d
Allow meta recommender to be composed of other meta recommenders
AdrianSosic Jan 10, 2025
fba7167
Rename to BatchSizeAdaptiveMetaRecommender
AdrianSosic Jan 10, 2025
4466b0f
Update CHANGELOG.md
AdrianSosic Jan 10, 2025
281ad57
Deprecate obsolete methods of MetaRecommender
AdrianSosic Jan 10, 2025
7a9f123
Add missing class docstring
AdrianSosic Jan 10, 2025
7c980eb
Add `get_inner_recommender` method
AdrianSosic Jan 10, 2025
c427f39
Make BaseSequentialMetaRecommender public
AdrianSosic Jan 16, 2025
40f4d71
Refine `get_inner_recommender` name and docstrings
AdrianSosic Jan 16, 2025
2d30912
Validate first interval of BatchSizeAdaptiveMetaRecommender
AdrianSosic Jan 16, 2025
0af5c2c
Fix/improve docstrings
AdrianSosic Jan 16, 2025
e7b929b
Add hypothesis strategy for Partition
AdrianSosic Jan 16, 2025
33f0dd9
Rename attribute of StreamingSequentialMetaRecommender
AdrianSosic Jan 16, 2025
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `arrays_to_dataframes` decorator to create lookups from array-based callables
- `DiscreteConstraint.get_valid` to conveniently access valid candidates
- Functionality for persisting benchmarking results on S3 from a manual pipeline run
- `remain_switched` option to `TwoPhaseMetaRecommender`
- `BatchSizeAdaptiveMetaRecommender` for selecting recommenders based on batch size
- `is_stateful` class variable to `MetaRecommenders`
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved

### Changed
- `SubstanceParameter` encodings are now computed exclusively with the
Expand All @@ -29,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `CustomDiscreteParameter` does not allow duplicated rows in `data` anymore
- De-/activating Polars via `BAYBE_DEACTIVATE_POLARS` now requires passing values
compatible with `strtobool`
- All arguments to `MetaRecommender.select_recommender` are now optional
- `MetaRecommender`s can now be composed of other `MetaRecommender`s

### Fixed
- Rare bug arising from degenerate `SubstanceParameter.comp_df` rows that caused
Expand Down Expand Up @@ -58,6 +63,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `SubstanceEncoding` value `RDKIT`. As a replacement, `RDKIT2DDESCRIPTORS` can be used.
- The `metadata` attribute of `SubspaceDiscrete` no longer exists. Metadata is now
exclusively handled by the `Campaign` class.
- `get_current_recommender` and `get_next_recommender` of `MetaRecommender` have become
obsolete and calling them is no longer possible

## [0.11.3] - 2024-11-06
### Fixed
Expand Down
23 changes: 18 additions & 5 deletions baybe/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,6 @@ def recommend(
def posterior(self, candidates: pd.DataFrame) -> Posterior:
"""Get the posterior predictive distribution for the given candidates.
The predictive distribution is based on the surrogate model of the last used
recommender.
Args:
candidates: The candidate points in experimental recommendations.
For details, see :meth:`baybe.surrogates.base.Surrogate.posterior`.
Expand All @@ -430,9 +427,19 @@ def posterior(self, candidates: pd.DataFrame) -> Posterior:
with torch.no_grad():
return surrogate.posterior(candidates)

def get_surrogate(self) -> SurrogateProtocol:
def get_surrogate(
self,
batch_size: int | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> SurrogateProtocol:
"""Get the current surrogate model.
Args:
batch_size: See :meth:`recommend`.
Only required when using meta recommenders that demand it.
pending_experiments: See :meth:`recommend`.
Only required when using meta recommenders that demand it.
Raises:
RuntimeError: If the current recommender does not provide a surrogate model.
Expand All @@ -453,7 +460,13 @@ def get_surrogate(self) -> SurrogateProtocol:

pure_recommender: RecommenderProtocol
if isinstance(self.recommender, MetaRecommender):
pure_recommender = self.recommender.get_current_recommender()
pure_recommender = self.recommender.select_recommender(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it guaranteed that this returns a pure recommender?

batch_size,
self.searchspace,
self.objective,
self.measurements,
pending_experiments,
)
else:
pure_recommender = self.recommender

Expand Down
48 changes: 48 additions & 0 deletions baybe/recommenders/meta/adaptive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Meta recommenders that adaptively select recommenders based on the context."""

import pandas as pd
from attrs import define, field
from attrs.validators import deep_iterable, instance_of
from typing_extensions import override

from baybe.objectives.base import Objective
from baybe.recommenders.base import RecommenderProtocol
from baybe.recommenders.meta.base import MetaRecommender
from baybe.searchspace.core import SearchSpace
from baybe.utils.interval import Partition


@define
class BatchSizeAdaptiveMetaRecommender(MetaRecommender):
"""A meta recommender that selects recommenders according to the batch size."""
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved

recommenders: list[RecommenderProtocol] = field(
converter=list, validator=deep_iterable(instance_of(RecommenderProtocol))
)
"""The recommenders for the individual batch size intervals."""

partition: Partition = field(
converter=lambda x: Partition(x) if not isinstance(x, Partition) else x
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to provide reasonable defaults for recommenders and partition ? Otherwise this class seems useless as most users have little clue how to set it up reasonably

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we could have a default here for the Partition that effectively handles the case of batch_size=1 and batch_size>1 - I guess that this is a reasonable case where people actually want to have different recommenders.

)
"""The partition mapping batch size intervals to recommenders. """

@partition.validator
def _validate_partitioning(self, _, value):
AdrianSosic marked this conversation as resolved.
Show resolved Hide resolved
if (lr := len(self.recommenders)) != (lp := len(value)):
raise ValueError(
f"The number of recommenders (given: {lr}) must be equal to the number "
f"of intervals defined by the partition (given: {lp})."
)

@override
def select_recommender(
self,
batch_size: int | None = None,
searchspace: SearchSpace | None = None,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> RecommenderProtocol:
if batch_size is None:
raise ValueError("A batch size is required.")
return self.recommenders[self.partition.get_interval_index(batch_size)]
85 changes: 23 additions & 62 deletions baybe/recommenders/meta/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

import gc
from abc import ABC, abstractmethod
from typing import Any
from typing import Any, ClassVar

import cattrs
import pandas as pd
from attrs import define, field
from attrs import define
from typing_extensions import override

from baybe.exceptions import DeprecationError
from baybe.objectives.base import Objective
from baybe.recommenders.base import RecommenderProtocol
from baybe.recommenders.pure.base import PureRecommender
Expand All @@ -22,76 +23,39 @@
class MetaRecommender(SerialMixin, RecommenderProtocol, ABC):
"""Abstract base class for all meta recommenders."""

_current_recommender: PureRecommender | None = field(default=None, init=False)
"""The current recommender."""

_used_recommender_ids: set[int] = field(factory=set, init=False)
"""Set of ids from recommenders that were used by this meta recommender."""
is_stateful: ClassVar[bool] = False
"""Boolean flag indicating if the meta recommender is stateful."""

@abstractmethod
def select_recommender(
self,
batch_size: int,
searchspace: SearchSpace,
batch_size: int | None = None,
searchspace: SearchSpace | None = None,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> PureRecommender:
) -> RecommenderProtocol:
"""Select a pure recommender for the given experimentation context.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring still says pure but i guess is should say non-meta?


See :meth:`baybe.recommenders.base.RecommenderProtocol.recommend` for details
on the method arguments.
"""

def get_current_recommender(self) -> PureRecommender:
"""Get the current recommender, if available."""
if self._current_recommender is None:
raise RuntimeError(
f"No recommendation has been requested from the "
f"'{self.__class__.__name__}' yet. Because the recommender is a "
f"'{MetaRecommender.__name__}', this means no actual recommender has "
f"been selected so far. The recommender will be available after the "
f"next '{self.recommend.__name__}' call."
)
return self._current_recommender

def get_next_recommender(
self,
batch_size: int,
searchspace: SearchSpace,
objective: Objective | None = None,
measurements: pd.DataFrame | None = None,
pending_experiments: pd.DataFrame | None = None,
) -> PureRecommender:
"""Get the recommender for the next recommendation.

Returns the next recommender in row that has not yet been used for generating
recommendations. In case of multiple consecutive calls, this means that
the same recommender instance is returned until its :meth:`recommend` method
is called.
"""Deprecated! Use :meth:`select_recommender` instead.""" # noqa: D401
raise DeprecationError(
f"'{MetaRecommender.__name__}.get_current_recommender' has become "
f"obsolete. Use "
f"'{MetaRecommender.__name__}.{self.select_recommender.__name__}' instead."
)

See :meth:`baybe.recommenders.base.RecommenderProtocol.recommend` for details
on the method arguments.
"""
# Check if the stored recommender instance can be returned
if (
self._current_recommender is not None
and id(self._current_recommender) not in self._used_recommender_ids
):
recommender = self._current_recommender

# Otherwise, fetch the next recommender waiting in row
else:
recommender = self.select_recommender(
batch_size=batch_size,
searchspace=searchspace,
objective=objective,
measurements=measurements,
pending_experiments=pending_experiments,
)
self._current_recommender = recommender

return recommender
def get_next_recommender(self) -> PureRecommender:
"""Deprecated! Use :meth:`select_recommender` instead.""" # noqa: D401
raise DeprecationError(
f"'{MetaRecommender.__name__}.get_next_recommender' has become "
f"obsolete. Use "
f"'{MetaRecommender.__name__}.{self.select_recommender.__name__}' instead."
)

@override
def recommend(
Expand All @@ -103,7 +67,7 @@ def recommend(
pending_experiments: pd.DataFrame | None = None,
) -> pd.DataFrame:
"""See :meth:`baybe.recommenders.base.RecommenderProtocol.recommend`."""
recommender = self.get_next_recommender(
recommender = self.select_recommender(
batch_size=batch_size,
searchspace=searchspace,
objective=objective,
Expand All @@ -123,15 +87,12 @@ def recommend(
}
)

recommendations = recommender.recommend(
return recommender.recommend(
batch_size=batch_size,
searchspace=searchspace,
pending_experiments=pending_experiments,
**optional_args,
)
self._used_recommender_ids.add(id(recommender))

return recommendations


# Register (un-)structure hooks
Expand Down
Loading
Loading