Repository navigation
Add RGPE TL Mode and Identity Kernel #921
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
Changes from all commits
ab9258d
b9095db
1ce60ca
3c08bc7
35443bd
fb6338a
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 |
|---|---|---|
|
|
@@ -179,9 +179,22 @@ class SubstanceEncoding(ParameterEncoding): | |
| class TransferLearningMode(Enum): | ||
| """Transfer learning modes for :class:`.TaskParameter`.""" | ||
|
|
||
| IDENTITY = "IDENTITY" | ||
| """A constant task kernel that leaves the base kernel unchanged. | ||
|
|
||
| The task dimension adds no covariance, so all tasks are pooled into a single model. | ||
| """ | ||
|
|
||
| INDEX_KERNEL = "INDEX_KERNEL" | ||
| """:class:`gpytorch.kernels.IndexKernel` for arbitrary correlations.""" | ||
|
|
||
| POSITIVE_INDEX_KERNEL = "POSITIVE_INDEX_KERNEL" | ||
| """:class:`botorch.models.kernels.positive_index.PositiveIndexKernel` for positive | ||
| correlations.""" | ||
|
|
||
| RGPE = "RGPE" | ||
|
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. Maybe |
||
| """A rank-weighted ensemble of per-task Gaussian processes. | ||
|
|
||
| Adds no task kernel; instead dispatches the surrogate to a dedicated | ||
| :class:`~baybe.surrogates.transfer_learning.rgpe.RGPESurrogate`. | ||
| """ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |
| from typing import TYPE_CHECKING, ClassVar | ||
|
|
||
| import pandas as pd | ||
| from attrs import Converter, define, field | ||
| from attrs import Converter, define, evolve, field | ||
| from attrs.converters import optional as optional_c | ||
| from attrs.converters import pipe | ||
| from attrs.validators import instance_of, is_callable, optional | ||
|
|
@@ -20,13 +20,15 @@ | |
| from baybe.exceptions import ( | ||
| DeprecationError, | ||
| IncompatibleSearchSpaceError, | ||
| IncompatibleSurrogateError, | ||
| ModelNotTrainedError, | ||
| _UnsupportedSearchSpaceAttributeError, | ||
| ) | ||
| from baybe.kernels.base import Kernel | ||
| from baybe.objectives.base import Objective | ||
| from baybe.parameters.base import Parameter | ||
| from baybe.parameters.categorical import TaskParameter | ||
| from baybe.parameters.enum import TransferLearningMode | ||
| from baybe.searchspace.core import SearchSpace | ||
| from baybe.surrogates.base import Surrogate | ||
| from baybe.surrogates.gaussian_process import _override | ||
|
|
@@ -253,6 +255,14 @@ class GaussianProcessSurrogate(Surrogate): | |
| _model = field(init=False, default=None, eq=False) | ||
| """The fitted BoTorch model.""" | ||
|
|
||
| # TODO: type should be `Surrogate | None` but is currently omitted due to: | ||
| # https://github.com/python-attrs/cattrs/issues/531 | ||
| _delegate = field(init=False, default=None, eq=False) | ||
|
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 am sure that you investigated this, but why can't we simply use |
||
| """A transfer-learning surrogate to forward fitting and prediction to. | ||
|
|
||
| Set when a non-kernel mode (e.g. | ||
| :attr:`~baybe.parameters.enum.TransferLearningMode.RGPE`) is requested.""" | ||
|
|
||
| @staticmethod | ||
| def _make_input_transform(context: _ModelContext) -> Normalize: | ||
| """Create the input transform for the Gaussian process.""" | ||
|
|
@@ -352,7 +362,20 @@ def posterior_mean_function( | |
| Returns: | ||
| A mean module ready to be used as the mean of a new | ||
| :class:`GaussianProcessSurrogate`. | ||
|
|
||
| Raises: | ||
| IncompatibleSurrogateError: If the surrogate dispatches to a transfer | ||
| learning ensemble, for which posterior mean functions are not | ||
| implemented. | ||
| """ | ||
| if self._delegate is not None: | ||
| raise IncompatibleSurrogateError( | ||
| f"Providing a posterior mean function for a " | ||
| f"'{self.__class__.__name__}' that dispatches to a " | ||
| f"'{type(self._delegate).__name__}' transfer learning ensemble is not " | ||
| f"implemented." | ||
| ) | ||
|
Comment on lines
+372
to
+377
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. Is this not possible or not implemented? Because depending on this, I'd say that a |
||
|
|
||
| if self._model is None: | ||
| warnings.warn( | ||
| f"'{self.__class__.__name__}' has not been fitted yet. " | ||
|
|
@@ -381,6 +404,8 @@ def posterior_mean_function( | |
|
|
||
| @override | ||
| def to_botorch(self) -> GPyTorchModel: | ||
|
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 holds for a lot of the functions here: If I would use the same If we are able to somehow get rid of this duality regarding |
||
| if self._delegate is not None: | ||
| return self._delegate.to_botorch() | ||
|
Comment on lines
406
to
+408
|
||
| if self._model is None: | ||
| raise ModelNotTrainedError( | ||
| "The surrogate must be trained before a BoTorch model can be created." | ||
|
|
@@ -401,6 +426,9 @@ def _make_target_scaler_factory() -> type[OutcomeTransform] | None: | |
|
|
||
| @override | ||
| def _posterior(self, candidates_comp_scaled: Tensor, /) -> Posterior: | ||
| # Forward to the transfer-learning delegate if one was set up during fitting. | ||
| if self._delegate is not None: | ||
| return self._delegate._posterior(candidates_comp_scaled) | ||
| # Model being fit is guaranteed by the call in `posterior` | ||
| assert self._model is not None | ||
| return self._model.posterior(candidates_comp_scaled) | ||
|
|
@@ -590,10 +618,23 @@ def _fit(self, train_x: Tensor, train_y: Tensor) -> None: | |
|
|
||
| context = _ModelContext(self._searchspace, self._objective, self._measurements) | ||
|
|
||
| # RGPE is handled by a dedicated ensemble surrogate rather than a task kernel. | ||
| # Dispatch to it, reusing this GP's configuration for the inner models. | ||
| task_param = self._searchspace._task_parameter | ||
| if ( | ||
| task_param is not None | ||
| and task_param.override_transfer_learning_mode is TransferLearningMode.RGPE | ||
| ): | ||
|
Comment on lines
+623
to
+627
|
||
| from baybe.surrogates.transfer_learning.rgpe import RGPESurrogate | ||
|
|
||
| delegate = RGPESurrogate(base_surrogate=evolve(self)) | ||
| delegate.fit(self._searchspace, self._objective, self._measurements) | ||
| self._delegate = delegate | ||
| return | ||
|
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. Should this already return at this point? Don't we then loose some of the errors and warning regarding deprecations, misconfigurations and so on silently? |
||
|
|
||
| # Check for custom kernel + multi-task clash (only relevant when the task | ||
| # parameter has no kernel override, since the override mechanism handles task | ||
| # kernel attachment explicitly). | ||
| task_param = self._searchspace._task_parameter | ||
| has_task_override = ( | ||
| task_param is not None and task_param.override_kernel is not None | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ | |
| from baybe.kernels.basic import PositiveIndexKernel | ||
| from baybe.objectives.base import Objective | ||
| from baybe.parameters.categorical import TaskParameter | ||
| from baybe.parameters.enum import _ParameterKind | ||
| from baybe.parameters.enum import TransferLearningMode, _ParameterKind | ||
| from baybe.parameters.selectors import ( | ||
| ParameterSelectorProtocol, | ||
| TypeSelector, | ||
|
|
@@ -274,11 +274,22 @@ class BayBEFitCriterionFactory(FitCriterionFactoryProtocol): | |
| def __call__( | ||
| self, searchspace: SearchSpace, objective: Objective, measurements: pd.DataFrame | ||
| ) -> FitCriterion: | ||
| return ( | ||
| FitCriterion.MARGINAL_LOG_LIKELIHOOD | ||
| if searchspace.n_tasks == 1 | ||
| else FitCriterion.LEAVE_ONE_OUT_PSEUDOLIKELIHOOD | ||
| ) | ||
| task_param = searchspace._task_parameter | ||
|
|
||
| # Without a (multi-valued) task parameter, this is an ordinary single-task GP. | ||
| if searchspace.n_tasks == 1: | ||
| return FitCriterion.MARGINAL_LOG_LIKELIHOOD | ||
| # IDENTITY mode keeps the task dimension but makes it inert (constant task | ||
| # kernel), so the model is effectively single-task and is fit like one. | ||
| elif ( | ||
| task_param is not None | ||
|
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. DO we need that check here or is this already guaranteed since we are in the |
||
| and task_param.override_transfer_learning_mode | ||
| is TransferLearningMode.IDENTITY | ||
| ): | ||
| return FitCriterion.MARGINAL_LOG_LIKELIHOOD | ||
| # Genuine multi-task model: use LOO cross-validation pseudo-likelihood. | ||
| else: | ||
| return FitCriterion.LEAVE_ONE_OUT_PSEUDOLIKELIHOOD | ||
|
|
||
|
|
||
| # Collect leftover original slotted classes processed by `attrs.define` | ||
|
|
||
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.
Should this maybe be renamed into
IDENTITY_KERNELorCONSTANT_KERNELsuch that it fits the other members?