From b68624669423c72551a022e2619dd40b5324c076 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 5 Nov 2025 16:18:56 +0100 Subject: [PATCH 01/14] Draft ProjectionKernel classes --- baybe/kernels/__init__.py | 8 ++++- baybe/kernels/_gpytorch.py | 58 +++++++++++++++++++++++++++++++++++ baybe/kernels/composite.py | 63 +++++++++++++++++++++++++++++++++++++- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 baybe/kernels/_gpytorch.py diff --git a/baybe/kernels/__init__.py b/baybe/kernels/__init__.py index 9323a2b631..1e5393f1b9 100644 --- a/baybe/kernels/__init__.py +++ b/baybe/kernels/__init__.py @@ -14,7 +14,12 @@ RFFKernel, RQKernel, ) -from baybe.kernels.composite import AdditiveKernel, ProductKernel, ScaleKernel +from baybe.kernels.composite import ( + AdditiveKernel, + ProductKernel, + ProjectionKernel, + ScaleKernel, +) __all__ = [ "AdditiveKernel", @@ -24,6 +29,7 @@ "PiecewisePolynomialKernel", "PolynomialKernel", "ProductKernel", + "ProjectionKernel", "RBFKernel", "RFFKernel", "RQKernel", diff --git a/baybe/kernels/_gpytorch.py b/baybe/kernels/_gpytorch.py new file mode 100644 index 0000000000..adff632fed --- /dev/null +++ b/baybe/kernels/_gpytorch.py @@ -0,0 +1,58 @@ +"""GPyTorch kernel implementations.""" + +from typing import Any + +import torch +from gpytorch.kernels import Kernel +from torch import Tensor + +from baybe.utils.torch import DTypeFloatTorch + +_ConvertibleToTensor = Any +"""A type alias for objects convertible to tensors.""" + + +class ProjectionKernel(Kernel): + """GPyTorch implementation of :class:`baybe.kernels.composite.ProjectionKernel`.""" + + def __init__( + self, + base_kernel: Kernel, + *, + n_projections: int, + projection_matrix: _ConvertibleToTensor | None = None, + learn_projection: bool = False, + ): + super().__init__() + + self.base_kernel = base_kernel + self.n_projections = n_projections + self.learn_projection = learn_projection + + if projection_matrix is not None: + self._set_projection_matrix( + torch.as_tensor(projection_matrix, dtype=DTypeFloatTorch) + ) + + @staticmethod + def _make_projection_matrix(n_input_dims: int, n_projections) -> Tensor: + """Generate a random Gaussian projection matrix.""" + return torch.randn(n_projections, n_input_dims, dtype=DTypeFloatTorch).div( + n_projections**0.5 + ) + + def _set_projection_matrix(self, matrix: Tensor, /) -> None: + """Set the projection matrix as a parameter or buffer.""" + type_ = torch.nn.Parameter if self.learn_projection else torch.nn.Buffer + self.projection_matrix = type_(matrix) + + def forward(self, x1: Tensor, x2: Tensor, **kwargs): + """Apply the base kernel to the projected input tensors.""" + if not hasattr(self, "projection_matrix"): + self._set_projection_matrix( + self._make_projection_matrix(self.n_projections, x1.size(-1)) + ) + + x1_proj = x1 @ self.projection_matrix + x2_proj = x2 @ self.projection_matrix + return self.base_kernel(x1_proj, x2_proj, **kwargs) diff --git a/baybe/kernels/composite.py b/baybe/kernels/composite.py index dab7abbf58..b968f10aa8 100644 --- a/baybe/kernels/composite.py +++ b/baybe/kernels/composite.py @@ -4,9 +4,10 @@ from functools import reduce from operator import add, mul +import numpy as np from attrs import define, field from attrs.converters import optional as optional_c -from attrs.validators import deep_iterable, gt, instance_of, min_len +from attrs.validators import deep_iterable, ge, gt, instance_of, min_len from attrs.validators import optional as optional_v from typing_extensions import override @@ -83,5 +84,65 @@ def to_gpytorch(self, *args, **kwargs): return reduce(mul, (k.to_gpytorch(*args, **kwargs) for k in self.base_kernels)) +@define(frozen=True) +class ProjectionKernel(CompositeKernel): + """A random projection kernel for dimensionality reduction.""" + + base_kernel: Kernel = field(validator=instance_of(Kernel)) + """The kernel to apply after projection.""" + + n_projections: int | None = field( + default=None, validator=optional_v([instance_of(int), ge(0)]), kw_only=True + ) + """The number of projections used (i.e. dimensionality of the projection space). + + Must be provided if no projection matrix is specified. + """ + + projection_matrix: np.ndarray | None = field( + default=None, converter=optional_c(np.asarray), kw_only=True + ) + """A pre-specified projection matrix. + + Must be provided if no number of projections is specified. + """ + + learn_projection: bool = field( + default=False, validator=instance_of(bool), kw_only=True + ) + """Boolean specifying if the projection matrix should be learned. + + If a projection matrix is provided and learning is activated, the provided matrix + is used as initial value. + """ + + @projection_matrix.validator + def _validate_projection_matrix(self, attribute: field, value: np.ndarray | None): + if value is None: + if self.n_projections is None: + raise ValueError( + "Either a projection matrix or the number of projections " + "must be specified." + ) + return + if value.ndim != 2: + raise ValueError( + f"The projection matrix must be 2-dimensional, " + f"but has shape {value.shape}." + ) + + @override + def to_gpytorch(self, **kwargs): + from baybe.kernels._gpytorch import ProjectionKernel as GPytorchProjectionKernel + + gpytorch_kernel = self.base_kernel.to_gpytorch(**kwargs) + return GPytorchProjectionKernel( + gpytorch_kernel, + n_projections=self.n_projections, + projection_matrix=self.projection_matrix, + learn_projection=self.learn_projection, + ) + + # Collect leftover original slotted classes processed by `attrs.define` gc.collect() From a4aa23a2d7d1d011195b4ecd4b5cecee22143f0d Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 5 Nov 2025 16:48:16 +0100 Subject: [PATCH 02/14] Add ProjectionKernelFactory class --- baybe/kernels/composite.py | 28 ++++++++---- .../gaussian_process/kernel_factory.py | 43 +++++++++++++++++-- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/baybe/kernels/composite.py b/baybe/kernels/composite.py index b968f10aa8..7f38ac0904 100644 --- a/baybe/kernels/composite.py +++ b/baybe/kernels/composite.py @@ -84,6 +84,22 @@ def to_gpytorch(self, *args, **kwargs): return reduce(mul, (k.to_gpytorch(*args, **kwargs) for k in self.base_kernels)) +_field_n_projections = field( + default=None, validator=optional_v([instance_of(int), ge(0)]), kw_only=True +) +"""Attrs field for :attr:`baybe.kernels.ProjectionKernel.n_projections`.""" + +_field_projection_matrix = field( + default=None, converter=optional_c(np.asarray), kw_only=True +) +"""Attrs field for :attr:`baybe.kernels.ProjectionKernel.projection_matrix`.""" + +_field_learn_projection = field( + default=False, validator=instance_of(bool), kw_only=True +) +"""Attrs field for :attr:`baybe.kernels.ProjectionKernel.learn_projection`.""" + + @define(frozen=True) class ProjectionKernel(CompositeKernel): """A random projection kernel for dimensionality reduction.""" @@ -91,25 +107,19 @@ class ProjectionKernel(CompositeKernel): base_kernel: Kernel = field(validator=instance_of(Kernel)) """The kernel to apply after projection.""" - n_projections: int | None = field( - default=None, validator=optional_v([instance_of(int), ge(0)]), kw_only=True - ) + n_projections: int | None = _field_n_projections """The number of projections used (i.e. dimensionality of the projection space). Must be provided if no projection matrix is specified. """ - projection_matrix: np.ndarray | None = field( - default=None, converter=optional_c(np.asarray), kw_only=True - ) + projection_matrix: np.ndarray | None = _field_projection_matrix """A pre-specified projection matrix. Must be provided if no number of projections is specified. """ - learn_projection: bool = field( - default=False, validator=instance_of(bool), kw_only=True - ) + learn_projection: bool = _field_learn_projection """Boolean specifying if the projection matrix should be learned. If a projection matrix is provided and learning is activated, the provided matrix diff --git a/baybe/surrogates/gaussian_process/kernel_factory.py b/baybe/surrogates/gaussian_process/kernel_factory.py index b3ae4983eb..8616cfd829 100644 --- a/baybe/surrogates/gaussian_process/kernel_factory.py +++ b/baybe/surrogates/gaussian_process/kernel_factory.py @@ -5,11 +5,18 @@ import gc from typing import TYPE_CHECKING, Protocol +import numpy as np from attrs import define, field from attrs.validators import instance_of from typing_extensions import override from baybe.kernels.base import Kernel +from baybe.kernels.composite import ( + ProjectionKernel, + _field_learn_projection, + _field_n_projections, + _field_projection_matrix, +) from baybe.searchspace import SearchSpace from baybe.serialization.mixin import SerialMixin @@ -17,6 +24,11 @@ from torch import Tensor +def to_kernel_factory(x: Kernel | KernelFactory, /) -> KernelFactory: + """Wrap a kernel into a plain kernel factory (with factory passthrough).""" + return x.to_factory() if isinstance(x, Kernel) else x + + class KernelFactory(Protocol): """A protocol defining the interface expected for kernel factories.""" @@ -41,9 +53,34 @@ def __call__( return self.kernel -def to_kernel_factory(x: Kernel | KernelFactory, /) -> KernelFactory: - """Wrap a kernel into a plain kernel factory (with factory passthrough).""" - return x.to_factory() if isinstance(x, Kernel) else x +@define(frozen=True) +class ProjectionKernelFactory(KernelFactory, SerialMixin): + """A factory producing projected kernels.""" + + base_kernel_factory: KernelFactory = field( + alias="kernel_or_factory", converter=to_kernel_factory + ) + + n_projections: int | None = _field_n_projections + """See :class:`baybe.kernels.ProjectionKernel`.""" + + projection_matrix: np.ndarray | None = _field_projection_matrix + """See :class:`baybe.kernels.ProjectionKernel`.""" + + learn_projection: bool = _field_learn_projection + """See :class:`baybe.kernels.ProjectionKernel`.""" + + @override + def __call__( + self, searchspace: SearchSpace, train_x: Tensor, train_y: Tensor + ) -> Kernel: + base_kernel = self.base_kernel_factory(searchspace, train_x, train_y) + return ProjectionKernel( + base_kernel=base_kernel, + n_projections=self.n_projections, + projection_matrix=self.projection_matrix, + learn_projection=self.learn_projection, + ) # Collect leftover original slotted classes processed by `attrs.define` From 4b9c4c9da1aa53a5abc1f108633a5f273f596383 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Fri, 14 Nov 2025 09:38:28 +0100 Subject: [PATCH 03/14] Fix base kernel instantiation --- baybe/kernels/composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/baybe/kernels/composite.py b/baybe/kernels/composite.py index 7f38ac0904..aa409e03ce 100644 --- a/baybe/kernels/composite.py +++ b/baybe/kernels/composite.py @@ -145,7 +145,7 @@ def _validate_projection_matrix(self, attribute: field, value: np.ndarray | None def to_gpytorch(self, **kwargs): from baybe.kernels._gpytorch import ProjectionKernel as GPytorchProjectionKernel - gpytorch_kernel = self.base_kernel.to_gpytorch(**kwargs) + gpytorch_kernel = self.base_kernel.to_gpytorch(ard_num_dims=self.n_projections) return GPytorchProjectionKernel( gpytorch_kernel, n_projections=self.n_projections, From 32229da5e2bd7e9642f6f18ea08e7b60c152b236 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Thu, 20 Nov 2025 09:04:22 +0100 Subject: [PATCH 04/14] Clean up draft code * Add missing type hint * Fix order of dimensions * Create matrix as new tensor * Normalize matrix correctly --- baybe/kernels/_gpytorch.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/baybe/kernels/_gpytorch.py b/baybe/kernels/_gpytorch.py index adff632fed..60e10aeadc 100644 --- a/baybe/kernels/_gpytorch.py +++ b/baybe/kernels/_gpytorch.py @@ -30,16 +30,15 @@ def __init__( self.learn_projection = learn_projection if projection_matrix is not None: - self._set_projection_matrix( - torch.as_tensor(projection_matrix, dtype=DTypeFloatTorch) - ) + matrix = torch.tensor(projection_matrix, dtype=DTypeFloatTorch) + self._set_projection_matrix(matrix) @staticmethod - def _make_projection_matrix(n_input_dims: int, n_projections) -> Tensor: + def _make_projection_matrix(n_input_dims: int, n_projections: int) -> Tensor: """Generate a random Gaussian projection matrix.""" - return torch.randn(n_projections, n_input_dims, dtype=DTypeFloatTorch).div( - n_projections**0.5 - ) + matrix = torch.randn(n_input_dims, n_projections, dtype=DTypeFloatTorch) + matrix = matrix / torch.norm(matrix, dim=0, keepdim=True) + return matrix def _set_projection_matrix(self, matrix: Tensor, /) -> None: """Set the projection matrix as a parameter or buffer.""" @@ -50,7 +49,7 @@ def forward(self, x1: Tensor, x2: Tensor, **kwargs): """Apply the base kernel to the projected input tensors.""" if not hasattr(self, "projection_matrix"): self._set_projection_matrix( - self._make_projection_matrix(self.n_projections, x1.size(-1)) + self._make_projection_matrix(x1.size(-1), self.n_projections) ) x1_proj = x1 @ self.projection_matrix From 6d9bf6a0eb5e126059a98e2c65af083873af4db5 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Mon, 24 Nov 2025 14:46:17 +0100 Subject: [PATCH 05/14] Simplify gpytorch projection kernel class to bare minimum --- baybe/kernels/_gpytorch.py | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/baybe/kernels/_gpytorch.py b/baybe/kernels/_gpytorch.py index 60e10aeadc..7924a92f3a 100644 --- a/baybe/kernels/_gpytorch.py +++ b/baybe/kernels/_gpytorch.py @@ -18,40 +18,23 @@ class ProjectionKernel(Kernel): def __init__( self, base_kernel: Kernel, + projection_matrix: _ConvertibleToTensor, *, - n_projections: int, - projection_matrix: _ConvertibleToTensor | None = None, learn_projection: bool = False, ): super().__init__() self.base_kernel = base_kernel - self.n_projections = n_projections self.learn_projection = learn_projection - if projection_matrix is not None: - matrix = torch.tensor(projection_matrix, dtype=DTypeFloatTorch) - self._set_projection_matrix(matrix) - - @staticmethod - def _make_projection_matrix(n_input_dims: int, n_projections: int) -> Tensor: - """Generate a random Gaussian projection matrix.""" - matrix = torch.randn(n_input_dims, n_projections, dtype=DTypeFloatTorch) - matrix = matrix / torch.norm(matrix, dim=0, keepdim=True) - return matrix - - def _set_projection_matrix(self, matrix: Tensor, /) -> None: - """Set the projection matrix as a parameter or buffer.""" - type_ = torch.nn.Parameter if self.learn_projection else torch.nn.Buffer - self.projection_matrix = type_(matrix) + matrix = torch.tensor(projection_matrix, dtype=DTypeFloatTorch) + if self.learn_projection: + self.register_parameter("projection_matrix", torch.nn.Parameter(matrix)) + else: + self.register_buffer("projection_matrix", matrix) def forward(self, x1: Tensor, x2: Tensor, **kwargs): """Apply the base kernel to the projected input tensors.""" - if not hasattr(self, "projection_matrix"): - self._set_projection_matrix( - self._make_projection_matrix(x1.size(-1), self.n_projections) - ) - x1_proj = x1 @ self.projection_matrix x2_proj = x2 @ self.projection_matrix return self.base_kernel(x1_proj, x2_proj, **kwargs) From 6f8a0b1437f26485bdf6aa8036e29a84dc9a8499 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Mon, 24 Nov 2025 14:47:06 +0100 Subject: [PATCH 06/14] Rename class to GPyTorchProjectionKernel --- baybe/kernels/_gpytorch.py | 2 +- baybe/kernels/composite.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/baybe/kernels/_gpytorch.py b/baybe/kernels/_gpytorch.py index 7924a92f3a..86c7f4d8b5 100644 --- a/baybe/kernels/_gpytorch.py +++ b/baybe/kernels/_gpytorch.py @@ -12,7 +12,7 @@ """A type alias for objects convertible to tensors.""" -class ProjectionKernel(Kernel): +class GPyTorchProjectionKernel(Kernel): """GPyTorch implementation of :class:`baybe.kernels.composite.ProjectionKernel`.""" def __init__( diff --git a/baybe/kernels/composite.py b/baybe/kernels/composite.py index aa409e03ce..bb7723c4fb 100644 --- a/baybe/kernels/composite.py +++ b/baybe/kernels/composite.py @@ -143,10 +143,11 @@ def _validate_projection_matrix(self, attribute: field, value: np.ndarray | None @override def to_gpytorch(self, **kwargs): - from baybe.kernels._gpytorch import ProjectionKernel as GPytorchProjectionKernel + from baybe.kernels._gpytorch import GPyTorchProjectionKernel - gpytorch_kernel = self.base_kernel.to_gpytorch(ard_num_dims=self.n_projections) - return GPytorchProjectionKernel( + n_projections = self.projection_matrix.shape[-1] + gpytorch_kernel = self.base_kernel.to_gpytorch(ard_num_dims=n_projections) + return GPyTorchProjectionKernel( gpytorch_kernel, n_projections=self.n_projections, projection_matrix=self.projection_matrix, From 8bdb46cf671ca82480f63ffd5d2a63a26a81168d Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Mon, 24 Nov 2025 15:07:22 +0100 Subject: [PATCH 07/14] Clean up projection kernel class --- baybe/kernels/composite.py | 55 ++++++++------------------------------ 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/baybe/kernels/composite.py b/baybe/kernels/composite.py index bb7723c4fb..a8c69edc9a 100644 --- a/baybe/kernels/composite.py +++ b/baybe/kernels/composite.py @@ -5,9 +5,9 @@ from operator import add, mul import numpy as np -from attrs import define, field +from attrs import Attribute, define, field from attrs.converters import optional as optional_c -from attrs.validators import deep_iterable, ge, gt, instance_of, min_len +from attrs.validators import deep_iterable, gt, instance_of, min_len from attrs.validators import optional as optional_v from typing_extensions import override @@ -84,57 +84,25 @@ def to_gpytorch(self, *args, **kwargs): return reduce(mul, (k.to_gpytorch(*args, **kwargs) for k in self.base_kernels)) -_field_n_projections = field( - default=None, validator=optional_v([instance_of(int), ge(0)]), kw_only=True -) -"""Attrs field for :attr:`baybe.kernels.ProjectionKernel.n_projections`.""" - -_field_projection_matrix = field( - default=None, converter=optional_c(np.asarray), kw_only=True -) -"""Attrs field for :attr:`baybe.kernels.ProjectionKernel.projection_matrix`.""" - -_field_learn_projection = field( - default=False, validator=instance_of(bool), kw_only=True -) -"""Attrs field for :attr:`baybe.kernels.ProjectionKernel.learn_projection`.""" - - @define(frozen=True) class ProjectionKernel(CompositeKernel): - """A random projection kernel for dimensionality reduction.""" + """A projection kernel for dimensionality reduction.""" base_kernel: Kernel = field(validator=instance_of(Kernel)) - """The kernel to apply after projection.""" + """The kernel applied to the projected inputs.""" - n_projections: int | None = _field_n_projections - """The number of projections used (i.e. dimensionality of the projection space). + projection_matrix: np.ndarray = field(converter=np.asarray) + """The projection matrix.""" - Must be provided if no projection matrix is specified. - """ - - projection_matrix: np.ndarray | None = _field_projection_matrix - """A pre-specified projection matrix. - - Must be provided if no number of projections is specified. - """ - - learn_projection: bool = _field_learn_projection + learn_projection: bool = field( + default=True, validator=instance_of(bool), kw_only=True + ) """Boolean specifying if the projection matrix should be learned. - If a projection matrix is provided and learning is activated, the provided matrix - is used as initial value. - """ + If ``True``, the provided projection matrix is used as initial value.""" @projection_matrix.validator - def _validate_projection_matrix(self, attribute: field, value: np.ndarray | None): - if value is None: - if self.n_projections is None: - raise ValueError( - "Either a projection matrix or the number of projections " - "must be specified." - ) - return + def _validate_projection_matrix(self, _: Attribute, value: np.ndarray): if value.ndim != 2: raise ValueError( f"The projection matrix must be 2-dimensional, " @@ -149,7 +117,6 @@ def to_gpytorch(self, **kwargs): gpytorch_kernel = self.base_kernel.to_gpytorch(ard_num_dims=n_projections) return GPyTorchProjectionKernel( gpytorch_kernel, - n_projections=self.n_projections, projection_matrix=self.projection_matrix, learn_projection=self.learn_projection, ) From 4c53379b62fb5d7539d01b48cf6b3678515eb305 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Mon, 24 Nov 2025 15:29:06 +0100 Subject: [PATCH 08/14] Clean up projection kernel factory class --- .../gaussian_process/kernel_factory.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/baybe/surrogates/gaussian_process/kernel_factory.py b/baybe/surrogates/gaussian_process/kernel_factory.py index 8616cfd829..614966c783 100644 --- a/baybe/surrogates/gaussian_process/kernel_factory.py +++ b/baybe/surrogates/gaussian_process/kernel_factory.py @@ -5,20 +5,15 @@ import gc from typing import TYPE_CHECKING, Protocol -import numpy as np from attrs import define, field -from attrs.validators import instance_of +from attrs.validators import ge, instance_of from typing_extensions import override from baybe.kernels.base import Kernel -from baybe.kernels.composite import ( - ProjectionKernel, - _field_learn_projection, - _field_n_projections, - _field_projection_matrix, -) +from baybe.kernels.composite import ProjectionKernel from baybe.searchspace import SearchSpace from baybe.serialization.mixin import SerialMixin +from baybe.surrogates.gaussian_process.presets.default import DefaultKernelFactory if TYPE_CHECKING: from torch import Tensor @@ -58,17 +53,19 @@ class ProjectionKernelFactory(KernelFactory, SerialMixin): """A factory producing projected kernels.""" base_kernel_factory: KernelFactory = field( - alias="kernel_or_factory", converter=to_kernel_factory + alias="kernel_or_factory", + factory=DefaultKernelFactory, + converter=to_kernel_factory, ) + """The factory producing the base kernel applied to the projected inputs.""" - n_projections: int | None = _field_n_projections - """See :class:`baybe.kernels.ProjectionKernel`.""" + n_projections: int = field(validator=[instance_of(int), ge(1)]) + """The number of projections to be used.""" - projection_matrix: np.ndarray | None = _field_projection_matrix - """See :class:`baybe.kernels.ProjectionKernel`.""" - - learn_projection: bool = _field_learn_projection - """See :class:`baybe.kernels.ProjectionKernel`.""" + learn_projection: bool = field( + default=True, validator=instance_of(bool), kw_only=True + ) + """Boolean specifying if the projection matrix should be learned.""" @override def __call__( @@ -77,8 +74,7 @@ def __call__( base_kernel = self.base_kernel_factory(searchspace, train_x, train_y) return ProjectionKernel( base_kernel=base_kernel, - n_projections=self.n_projections, - projection_matrix=self.projection_matrix, + projection_matrix=projection_matrix, learn_projection=self.learn_projection, ) From 6f624d24070480c07e8492302624931cf01a57f8 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 26 Nov 2025 09:20:55 +0100 Subject: [PATCH 09/14] Implement projection matrix initialization strategies --- .../gaussian_process/kernel_factory.py | 145 ++++++++++++++++-- 1 file changed, 131 insertions(+), 14 deletions(-) diff --git a/baybe/surrogates/gaussian_process/kernel_factory.py b/baybe/surrogates/gaussian_process/kernel_factory.py index 614966c783..09a31ad78a 100644 --- a/baybe/surrogates/gaussian_process/kernel_factory.py +++ b/baybe/surrogates/gaussian_process/kernel_factory.py @@ -3,17 +3,18 @@ from __future__ import annotations import gc +from enum import Enum from typing import TYPE_CHECKING, Protocol +import numpy as np from attrs import define, field -from attrs.validators import ge, instance_of -from typing_extensions import override +from attrs.validators import instance_of +from typing_extensions import assert_never, override from baybe.kernels.base import Kernel -from baybe.kernels.composite import ProjectionKernel +from baybe.kernels.composite import AdditiveKernel, ProjectionKernel from baybe.searchspace import SearchSpace from baybe.serialization.mixin import SerialMixin -from baybe.surrogates.gaussian_process.presets.default import DefaultKernelFactory if TYPE_CHECKING: from torch import Tensor @@ -24,6 +25,96 @@ def to_kernel_factory(x: Kernel | KernelFactory, /) -> KernelFactory: return x.to_factory() if isinstance(x, Kernel) else x +class ProjectionMatrixInitialization(Enum): + """Initialization strategies for kernel projection matrices.""" + + MASKING = "MASKING" + """Axis-aligned masking (random selection of input dimensions).""" + + ORTHONORMAL = "ORTHONORMAL" + """Random orthonormal basis.""" + + PLS = "PLS" + """Partial Least Squares (PLS) directions.""" + + SPHERICAL = "SPHERICAL" + """Uniform random sampling on the unit sphere.""" + + +def _make_projection_matrices( + n_projections: int, + n_matrices: int, + initialization: ProjectionMatrixInitialization, + train_x: np.ndarray, + train_y: np.ndarray, +) -> np.ndarray: + """Create a collection of projection matrices. + + Args: + n_projections: The number of projections in each matrix. + n_matrices: The number of projection matrices to create. + initialization: The initialization strategy to use. + train_x: The training inputs. + train_y: The training outputs. + + Returns: + An array of shape ``(n_matrices, n_input_dims, n_projections)`` containing the + created matrices. + """ + n_input_dims = train_x.shape[-1] + + if n_matrices == 0: + return np.empty((0, n_input_dims, n_projections)) + + if initialization is ProjectionMatrixInitialization.MASKING: + matrices = [] + for _ in range(n_matrices): + matrix = np.eye(n_input_dims) + matrix = matrix[ + :, np.random.choice(n_input_dims, n_projections, replace=False) + ] + matrices.append(matrix) + + elif initialization is ProjectionMatrixInitialization.ORTHONORMAL: + matrices = [] + for _ in range(n_matrices): + random_matrix = np.random.randn(n_input_dims, n_projections) + q, _ = np.linalg.qr(random_matrix) + matrices.append(q[:, :n_projections]) + + elif initialization is ProjectionMatrixInitialization.PLS: + from sklearn.cross_decomposition import PLSRegression + + pls = PLSRegression(n_components=n_projections) + pls.fit(train_x, train_y) + M = pls.x_rotations_ + + # IMPROVE: One could use the remaining PLS directions for the next matrices + # until they are exhausted, then switch to orthonormal. + matrices = [ + M, + *_make_projection_matrices( + n_projections, + n_matrices - 1, + ProjectionMatrixInitialization.ORTHONORMAL, + train_x, + train_y, + ), + ] + + elif initialization is ProjectionMatrixInitialization.SPHERICAL: + matrices = [] + for _ in range(n_matrices): + matrix = np.random.randn(n_input_dims, n_projections) + matrix = matrix / np.linalg.norm(matrix, axis=0, keepdims=True) + matrices.append(matrix) + + else: + assert_never(initialization) + + return np.stack(matrices) if n_matrices > 1 else matrices[0][None, ...] + + class KernelFactory(Protocol): """A protocol defining the interface expected for kernel factories.""" @@ -52,31 +143,57 @@ def __call__( class ProjectionKernelFactory(KernelFactory, SerialMixin): """A factory producing projected kernels.""" + n_projections: int = field(validator=instance_of(int)) + """The number of projections to be used in each projection matrix.""" + + n_matrices: int = field(validator=instance_of(int)) + """The number of projection matrices to be used.""" + + initialization: ProjectionMatrixInitialization = field( + converter=ProjectionMatrixInitialization + ) + """The initialization strategy for the projection matrices.""" + base_kernel_factory: KernelFactory = field( alias="kernel_or_factory", - factory=DefaultKernelFactory, converter=to_kernel_factory, ) - """The factory producing the base kernel applied to the projected inputs.""" - - n_projections: int = field(validator=[instance_of(int), ge(1)]) - """The number of projections to be used.""" + """The factory creating the base kernel to be applied to the projected inputs.""" learn_projection: bool = field( default=True, validator=instance_of(bool), kw_only=True ) - """Boolean specifying if the projection matrix should be learned.""" + """See :attr:`baybe.kernels.composite.ProjectionKernel.learn_projection`.""" + + @base_kernel_factory.default + def _default_base_kernel_factory(self) -> KernelFactory: + from baybe.surrogates.gaussian_process.presets.default import ( + DefaultKernelFactory, + ) + + return DefaultKernelFactory() @override def __call__( self, searchspace: SearchSpace, train_x: Tensor, train_y: Tensor ) -> Kernel: base_kernel = self.base_kernel_factory(searchspace, train_x, train_y) - return ProjectionKernel( - base_kernel=base_kernel, - projection_matrix=projection_matrix, - learn_projection=self.learn_projection, + projection_matrices = _make_projection_matrices( + self.n_matrices, + self.n_projections, + self.initialization, + train_x.numpy(), + train_y.numpy(), ) + kernels = [ + ProjectionKernel( + base_kernel=base_kernel, + projection_matrix=m, + learn_projection=self.learn_projection, + ) + for m in projection_matrices + ] + return AdditiveKernel(kernels) if self.n_matrices > 1 else kernels[0] # Collect leftover original slotted classes processed by `attrs.define` From 31a3711676592c14ab0d169b8693b79024bb6ddc Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 26 Nov 2025 12:33:39 +0100 Subject: [PATCH 10/14] Draft example --- docs/references.bib | 16 ++ docs/userguide/transfer_learning.md | 1 + .../Custom_Surrogates/projection_kernel.py | 201 ++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 examples/Custom_Surrogates/projection_kernel.py diff --git a/docs/references.bib b/docs/references.bib index 307529d299..a28f0a728c 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -8,3 +8,19 @@ @inproceedings{NIPS2007_66368270 volume = {20}, year = {2007} } +@InProceedings{pmlr-v235-hvarfner24a, + title = {Vanilla {B}ayesian Optimization Performs Great in High Dimensions}, + author = {Hvarfner, Carl and Hellsten, Erik Orm and Nardi, Luigi}, + booktitle = {Proceedings of the 41st International Conference on Machine Learning}, + pages = {20793--20817}, + year = {2024}, + editor = {Salakhutdinov, Ruslan and Kolter, Zico and Heller, Katherine and Weller, Adrian and Oliver, Nuria and Scarlett, Jonathan and Berkenkamp, Felix}, + volume = {235}, + series = {Proceedings of Machine Learning Research}, + month = {21--27 Jul}, + publisher = {PMLR}, + pdf = {https://raw.githubusercontent.com/mlresearch/v235/main/assets/hvarfner24a/hvarfner24a.pdf}, + url = {https://proceedings.mlr.press/v235/hvarfner24a.html}, + abstract = {High-dimensional optimization problems have long been considered the Achilles’ heel of Bayesian optimization algorithms. Spurred by the curse of dimensionality, a large collection of algorithms aim to make BO more performant in this setting, commonly by imposing various simplifying assumptions on the objective, thereby decreasing its presumed complexity. In this paper, we identify the degeneracies that make vanilla BO poorly suited to high-dimensional tasks, and further show how existing algorithms address these degeneracies through the lens of model complexity. Motivated by the model complexity measure, we derive an enhancement to the prior assumptions that are typical of the vanilla BO algorithm, which reduces the complexity to manageable levels without imposing structural restrictions on the objective. Our modification - a simple scaling of the Gaussian process lengthscale prior in the dimensionality - reveals that standard BO works drastically better than previously thought in high dimensions. Our insights are supplemented by substantial out-performance of existing state-of-the-art on multiple commonly considered real-world high-dimensional tasks.} +} + diff --git a/docs/userguide/transfer_learning.md b/docs/userguide/transfer_learning.md index 6f509df31b..83b20a4f27 100644 --- a/docs/userguide/transfer_learning.md +++ b/docs/userguide/transfer_learning.md @@ -186,6 +186,7 @@ on the optimization: ``` ```{bibliography} +# :filter: docname in docnames ``` [`TaskParameter`]: baybe.parameters.categorical.TaskParameter \ No newline at end of file diff --git a/examples/Custom_Surrogates/projection_kernel.py b/examples/Custom_Surrogates/projection_kernel.py new file mode 100644 index 0000000000..bd00c9db71 --- /dev/null +++ b/examples/Custom_Surrogates/projection_kernel.py @@ -0,0 +1,201 @@ +# # Gaussian Processes in High-Dimensional Spaces + +# A common challenge when applying Gaussian process models to high-dimensional spaces is +# to avoid model overfitting, which can easily happen when the hyperparameters of the +# model are not chosen with sufficient care {cite:p}`pmlr-v235-hvarfner24a`. While there +# exist various strategies to mitigate this issue, a simple yet effective approach is +# to model the data on a lower-dimensional subspace of the original parameter space. +# By reducing the effective dimensionality, this technique improves sample efficiency +# and reduces the risk of overfitting. Here, we demonstrate how to use BayBE's +# {class}`~baybe.kernels.composite.ProjectionKernel` to implement this idea. + + +# ## Imports + +import os +from typing import Any + +import numpy as np +import pandas as pd +import seaborn as sns +from matplotlib import pyplot as plt +from scipy.special import expit +from scipy.stats import spearmanr +from sklearn.metrics import r2_score + +from baybe.kernels import MaternKernel, ProjectionKernel +from baybe.parameters import NumericalContinuousParameter +from baybe.searchspace import SearchSpace +from baybe.surrogates import GaussianProcessSurrogate +from baybe.surrogates.base import Surrogate +from baybe.surrogates.gaussian_process.kernel_factory import ProjectionKernelFactory +from baybe.surrogates.gaussian_process.presets.default import DefaultKernelFactory +from baybe.targets import NumericalTarget +from baybe.utils.dataframe import arrays_to_dataframes +from baybe.utils.random import set_random_seed + +# ## Settings + +# Before we start, let us define some general settings for this example: + +# Simulation Settings +SMOKE_TEST = "SMOKE_TEST" in os.environ +N_MC = 1 if SMOKE_TEST else 10 + +# Data Settings +N_TRAIN_DATA = [2] if SMOKE_TEST else [20, 50, 100, 200] +N_TEST_DATA = 100 + +# Problem Settings +N_DIMENSIONS_TOTAL = 3 if SMOKE_TEST else 20 +N_DIMENSIONS_SUBSPACE = 2 + +# Projection Kernel Settings +N_PROJECTIONS = 2 # Dimensionality of the assumed subspace +N_MATRICES = 1 if SMOKE_TEST else 3 # Number of projection matrices to use + +set_random_seed(1337) + +# ## The Scenario + +# To keep things simple, we consider a single target that needs to be modeled: + +objective = NumericalTarget("t").to_objective() + +# We consider a situation where the available regression parameters span a rather +# high-dimensional space: + +parameters = [ + NumericalContinuousParameter(f"p{i}", (-1, 1)) for i in range(N_DIMENSIONS_TOTAL) +] +searchspace = SearchSpace.from_product(parameters) + +# However, we assume that the actual relationship between the regression parameters and +# the target can be expressed via a low-dimensional function that is embedded into the +# higher-dimensional parameter space. To benchmark the proposed approach, let us +# generate this ground truth subspace by randomly drawing some unit-length vectors +# spanning it: + +subspace = np.random.randn(N_DIMENSIONS_TOTAL, N_DIMENSIONS_SUBSPACE) +subspace /= np.linalg.norm(subspace, axis=0, keepdims=True) + + +# As a last component to describe the modeling scenario, we define the corresponding +# ground truth function that operates on this subspace: + + +@arrays_to_dataframes(searchspace.parameter_names, ["t"]) +def low_dimensional_function(array: np.ndarray, /) -> np.ndarray: + """The low-dimensional ground truth function. + + Computes the sigmoid transformation of the norm of its input. + """ + assert array.shape[-1] == N_DIMENSIONS_TOTAL + return expit(np.linalg.norm(array @ subspace, axis=1, keepdims=True)) + + +# ## Subspace Modeling + +# Now, let us turn to the modeling side. First, we define a little helper that lets +# us easily train and evaluate different surrogate models: + + +def predict( + model: Surrogate, train_data: pd.DataFrame, test_data: pd.DataFrame +) -> pd.DataFrame: + """Fit and evaluate a surrogate model.""" + model.fit(searchspace, objective, train_data) + return model.posterior_stats(test_data[list(searchspace.parameter_names)]) + + +# We compare the following models: +# - **Vanilla:** +# The vanilla Gaussian process model operating on the **full** parameter space. +# - **Learned Projection:** +# A Gaussian process model operating on **learned** low-dimensional subspaces. +# - **Ideal Projection:** +# A Gaussian process model operating on the **ground truth** low-dimensional subspace. + +models: dict[str, Surrogate] = { + "Vanilla": GaussianProcessSurrogate(), + "Learned Projection": GaussianProcessSurrogate( + ProjectionKernelFactory( + n_projections=N_PROJECTIONS, + n_matrices=N_MATRICES, + initialization="PLS", + kernel_or_factory=DefaultKernelFactory(), + learn_projection=True, + ) + ), + "Ideal Projection": GaussianProcessSurrogate( + ProjectionKernel( + MaternKernel(), + projection_matrix=subspace, + learn_projection=False, + ) + ), +} + +# We can now evaluate the regression performance of these models for different training +# data set sizes. The entire process is repeated for several Monte Carlo iterations: + +metrics: list[dict[str, Any]] = [] +for mc in range(N_MC): + test_data = searchspace.continuous.sample_uniform(N_TEST_DATA) + test_data["t"] = low_dimensional_function(test_data) + for n_train_data in N_TRAIN_DATA: + train_data = searchspace.continuous.sample_uniform(n_train_data) + train_data["t"] = low_dimensional_function(train_data) + for model_name, model in models.items(): + predictions = predict(model, train_data, test_data) + r2 = r2_score(test_data["t"], predictions["t_mean"]) + correlation = spearmanr(test_data["t"], predictions["t_mean"]).correlation + metrics.append( + { + "mc_iteration": mc, + "model": model_name, + "n_train_data": n_train_data, + "rank_correlation": correlation, + "r2_score": r2, + } + ) + +df = pd.DataFrame.from_records(metrics) + +# ## Results + +# Finally, we visualize the resulting regression metrics. As expected, when operating on +# the ground truth subspace, the Gaussian process can easily capture the structure of +# the data, even for relatively small training set sizes. More importantly, even when +# learning the subspace from the training data itself (the realistic scenario where the +# ground truth is unknown), the projection approach identifies patterns in the data +# significantly earlier compared to the vanilla approach. This effect is more prominent +# for the underlying ordering of the target data (as reflected by the rank correlation) +# than for the actual prediction values. Nevertheless, when applied in a Bayesian +# optimization context, rank correlation is oftentimes the more important metric, since +# data-driven optimization heavily relies on correctly ranking candidates. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) + +# fmt: off +sns.lineplot(df, x="n_train_data", y="rank_correlation", hue="model", ax=ax1) +ax1.set_title("Spearman Correlation") +ax1.set_xlabel("Number of Training Points") +ax1.set_ylabel("Rank Correlation") +# fmt: on + +# fmt: off +sns.lineplot(df, x="n_train_data", y="r2_score", hue="model", ax=ax2) +ax2.set_title("R² Score") +ax2.set_xlabel("Number of Training Points") +ax2.set_ylabel("R² Score") +# fmt: on + +plt.tight_layout() +if not SMOKE_TEST: + plt.savefig("projection_kernel.svg") +plt.show() + +# ```{bibliography} +# :filter: docname in docnames +# ``` From fbeefef8f76d8dbeff9059e62b8a6daed69422f2 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 26 Nov 2025 13:38:49 +0100 Subject: [PATCH 11/14] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b2a663f4..450bffa1e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- `ProjectionKernel` class and corresponding `ProjectionKernelFactory` class + ## [0.14.2] - 2026-01-14 ### Added - `NumericalTarget.match_*` constructors now accept a `mismatch_instead` argument. If From 35688078857d5171d303415770e7a5cfec6f56c1 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 26 Nov 2025 17:20:53 +0100 Subject: [PATCH 12/14] Enable serialization and add hypothesis strategy --- baybe/kernels/composite.py | 6 +++-- baybe/serialization/core.py | 12 +++++++++ baybe/serialization/utils.py | 19 +++++++++++-- tests/hypothesis_strategies/kernels.py | 37 +++++++++++++++++++++++--- 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/baybe/kernels/composite.py b/baybe/kernels/composite.py index a8c69edc9a..e769fd4470 100644 --- a/baybe/kernels/composite.py +++ b/baybe/kernels/composite.py @@ -5,7 +5,7 @@ from operator import add, mul import numpy as np -from attrs import Attribute, define, field +from attrs import Attribute, cmp_using, define, field from attrs.converters import optional as optional_c from attrs.validators import deep_iterable, gt, instance_of, min_len from attrs.validators import optional as optional_v @@ -91,7 +91,9 @@ class ProjectionKernel(CompositeKernel): base_kernel: Kernel = field(validator=instance_of(Kernel)) """The kernel applied to the projected inputs.""" - projection_matrix: np.ndarray = field(converter=np.asarray) + projection_matrix: np.ndarray = field( + converter=np.asarray, eq=cmp_using(eq=np.array_equal) + ) """The projection matrix.""" learn_projection: bool = field( diff --git a/baybe/serialization/core.py b/baybe/serialization/core.py index 6381bac26c..b8ff3d44e8 100644 --- a/baybe/serialization/core.py +++ b/baybe/serialization/core.py @@ -9,6 +9,7 @@ import attrs import cattrs +import numpy as np import pandas as pd from cattrs.strategies import configure_union_passthrough @@ -104,6 +105,15 @@ def _unstructure_dataframe_hook(df: pd.DataFrame) -> str: return base64.b64encode(pickled_df).decode("utf-8") +_unstructure_ndarray_hook = _unstructure_dataframe_hook + + +def _structure_ndarray_hook(obj: str, _) -> np.ndarray: + """Deserialize a numpy ndarray.""" + pickled_array = base64.b64decode(obj.encode("utf-8")) + return pickle.loads(pickled_array) + + def block_serialization_hook(obj: Any) -> NoReturn: # noqa: DOC101, DOC103 """Prevent serialization of the passed object. @@ -163,6 +173,8 @@ def select_constructor_hook(specs: dict, cls: type[_T]) -> _T: ) converter.register_unstructure_hook(pd.DataFrame, _unstructure_dataframe_hook) converter.register_structure_hook(pd.DataFrame, _structure_dataframe_hook) +converter.register_unstructure_hook(np.ndarray, _unstructure_ndarray_hook) +converter.register_structure_hook(np.ndarray, _structure_ndarray_hook) converter.register_unstructure_hook(datetime, lambda x: x.isoformat()) converter.register_structure_hook(datetime, lambda x, _: datetime.fromisoformat(x)) converter.register_unstructure_hook(timedelta, lambda x: f"{x.total_seconds()}s") diff --git a/baybe/serialization/utils.py b/baybe/serialization/utils.py index d5a5fd6a7f..37435464b8 100644 --- a/baybe/serialization/utils.py +++ b/baybe/serialization/utils.py @@ -2,18 +2,33 @@ from typing import Any +import numpy as np import pandas as pd +def serialize_ndarray(array: np.ndarray, /) -> Any: + """Serialize a numpy ndarray.""" + from baybe.serialization import converter + + return converter.unstructure(array) + + +def deserialize_ndarray(serialized_array: Any, /) -> np.ndarray: + """Deserialize a numpy ndarray.""" + from baybe.serialization import converter + + return converter.structure(serialized_array, np.ndarray) + + def serialize_dataframe(df: pd.DataFrame, /) -> Any: """Serialize a pandas dataframe.""" - from baybe.searchspace.core import converter + from baybe.serialization import converter return converter.unstructure(df) def deserialize_dataframe(serialized_df: Any, /) -> pd.DataFrame: """Deserialize a pandas dataframe.""" - from baybe.searchspace.core import converter + from baybe.serialization import converter return converter.structure(serialized_df, pd.DataFrame) diff --git a/tests/hypothesis_strategies/kernels.py b/tests/hypothesis_strategies/kernels.py index 2f2ac87432..1cf5a6c847 100644 --- a/tests/hypothesis_strategies/kernels.py +++ b/tests/hypothesis_strategies/kernels.py @@ -3,6 +3,8 @@ from enum import Enum import hypothesis.strategies as st +from hypothesis.extra.numpy import arrays +from typing_extensions import assert_never from baybe.kernels.basic import ( LinearKernel, @@ -14,7 +16,12 @@ RFFKernel, RQKernel, ) -from baybe.kernels.composite import AdditiveKernel, ProductKernel, ScaleKernel +from baybe.kernels.composite import ( + AdditiveKernel, + ProductKernel, + ProjectionKernel, + ScaleKernel, +) from tests.hypothesis_strategies.basic import positive_finite_floats from tests.hypothesis_strategies.priors import priors @@ -25,6 +32,7 @@ class KernelType(Enum): SINGLE = "SINGLE" ADDITIVE = "ADDITIVE" PRODUCT = "PRODUCT" + PROJECTION = "PROJECTION" linear_kernels = st.builds( @@ -121,6 +129,26 @@ def single_kernels(draw: st.DrawFn): return base_kernel +@st.composite +def projection_kernels(draw: st.DrawFn): + """Generate :class:`baybe.kernels.composite.ProjectionKernel`.""" + MAX_DIM = 5 + shape = draw( + st.tuples( + st.integers(min_value=1, max_value=MAX_DIM), + st.integers(min_value=1, max_value=MAX_DIM), + ) + ) + projection_matrix = draw( + arrays(float, shape, elements=st.floats(allow_nan=False, allow_infinity=False)) + ) + base_kernel = draw(single_kernels()) + learn_projection = draw(st.booleans()) + return ProjectionKernel( + base_kernel, projection_matrix, learn_projection=learn_projection + ) + + @st.composite def kernels(draw: st.DrawFn): """Generate :class:`baybe.kernels.base.Kernel`.""" @@ -128,9 +156,12 @@ def kernels(draw: st.DrawFn): if kernel_type is KernelType.SINGLE: return draw(single_kernels()) - - base_kernels = draw(st.lists(single_kernels(), min_size=2)) if kernel_type is KernelType.ADDITIVE: + base_kernels = draw(st.lists(single_kernels(), min_size=2)) return AdditiveKernel(base_kernels) if kernel_type is KernelType.PRODUCT: + base_kernels = draw(st.lists(single_kernels(), min_size=2)) return ProductKernel(base_kernels) + if kernel_type is KernelType.PROJECTION: + return draw(projection_kernels()) + assert_never(kernel_type) From 24c3852b63c0afb703db5b96321a4676e5e765d8 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 26 Nov 2025 17:29:13 +0100 Subject: [PATCH 13/14] Fix wrong argument order --- .../gaussian_process/kernel_factory.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/baybe/surrogates/gaussian_process/kernel_factory.py b/baybe/surrogates/gaussian_process/kernel_factory.py index 09a31ad78a..01aadd9123 100644 --- a/baybe/surrogates/gaussian_process/kernel_factory.py +++ b/baybe/surrogates/gaussian_process/kernel_factory.py @@ -94,11 +94,11 @@ def _make_projection_matrices( matrices = [ M, *_make_projection_matrices( - n_projections, - n_matrices - 1, - ProjectionMatrixInitialization.ORTHONORMAL, - train_x, - train_y, + n_projections=n_projections, + n_matrices=n_matrices - 1, + initialization=ProjectionMatrixInitialization.ORTHONORMAL, + train_x=train_x, + train_y=train_y, ), ] @@ -179,11 +179,11 @@ def __call__( ) -> Kernel: base_kernel = self.base_kernel_factory(searchspace, train_x, train_y) projection_matrices = _make_projection_matrices( - self.n_matrices, - self.n_projections, - self.initialization, - train_x.numpy(), - train_y.numpy(), + n_projections=self.n_projections, + n_matrices=self.n_matrices, + initialization=self.initialization, + train_x=train_x.numpy(), + train_y=train_y.numpy(), ) kernels = [ ProjectionKernel( From 06276d8afde118e62425d999c1e6d748f6e94988 Mon Sep 17 00:00:00 2001 From: AdrianSosic Date: Wed, 26 Nov 2025 13:42:36 +0100 Subject: [PATCH 14/14] Add rendered svg --- .../Custom_Surrogates/projection_kernel.svg | 1834 +++++++++++++++++ 1 file changed, 1834 insertions(+) create mode 100644 examples/Custom_Surrogates/projection_kernel.svg diff --git a/examples/Custom_Surrogates/projection_kernel.svg b/examples/Custom_Surrogates/projection_kernel.svg new file mode 100644 index 0000000000..0ef571aabb --- /dev/null +++ b/examples/Custom_Surrogates/projection_kernel.svg @@ -0,0 +1,1834 @@ + + + + + + + + 2025-11-27T09:53:17.925349 + image/svg+xml + + + Matplotlib v3.10.7, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +