Skip to content

Commit

Permalink
Support for priors in OAK Kernel
Browse files Browse the repository at this point in the history
Summary: Added support for registering priors to the coefficients of the OrthogonalAdditiveKernel. Useful for incentivizing sparsity in the additive components, and improve identifiability between first- and second-order components.

Differential Revision: D61730632
  • Loading branch information
Carl Hvarfner authored and facebook-github-bot committed Sep 13, 2024
1 parent c895a8d commit 3d72ed5
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 1 deletion.
79 changes: 79 additions & 0 deletions botorch/models/kernels/orthogonal_additive_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
from botorch.exceptions.errors import UnsupportedError
from gpytorch.constraints import Interval, Positive
from gpytorch.kernels import Kernel
from gpytorch.priors import Prior

from torch import nn, Tensor

_positivity_constraint = Positive()
SECOND_ORDER_PRIOR_ERROR_MSG = (
"Second order is disabled, but there is a prior on the second order coefficients. "
"Please remove the second order prior or enable second order terms."
)


class OrthogonalAdditiveKernel(Kernel):
Expand All @@ -40,6 +45,9 @@ def __init__(
dtype: Optional[torch.dtype] = None,
device: Optional[torch.device] = None,
coeff_constraint: Interval = _positivity_constraint,
offset_prior: Optional[Prior] = None,
coeffs_1_prior: Optional[Prior] = None,
coeffs_2_prior: Optional[Prior] = None,
):
"""
Args:
Expand All @@ -55,6 +63,9 @@ def __init__(
"""
super().__init__(batch_shape=batch_shape)
self.base_kernel = base_kernel
if not second_order and coeffs_2_prior is not None:
raise AttributeError(SECOND_ORDER_PRIOR_ERROR_MSG)

# integration nodes, weights for [0, 1]
tkwargs = {"dtype": dtype, "device": device}
z, w = leggauss(deg=quad_deg, a=0, b=1, **tkwargs)
Expand Down Expand Up @@ -82,6 +93,29 @@ def __init__(
else None
),
)
if offset_prior is not None:
self.register_prior(
"offset_prior",
offset_prior,
self._offset_param,
self._offset_closure,
)
if coeffs_1_prior is not None:
self.register_prior(
"coeffs_1_prior",
coeffs_1_prior,
self._coeffs_1_param,
self._coeffs_1_closure,
)
if coeffs_2_prior is not None:
self.register_prior(
"coeffs_2_prior",
coeffs_2_prior,
self._coeffs_2_param,
self._coeffs_2_closure,
)

# for second order interactions, we only
if second_order:
self._rev_triu_indices = torch.tensor(
_reverse_triu_indices(dim),
Expand Down Expand Up @@ -140,6 +174,51 @@ def coeffs_2(self) -> Optional[Tensor]:
else:
return None

def _coeffs_1_param(self, m):
return m.coeffs_1

Check warning on line 178 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L178

Added line #L178 was not covered by tests

def _coeffs_2_param(self, m):
return m.coeffs_2

Check warning on line 181 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L181

Added line #L181 was not covered by tests

def _offset_param(self, m):
return m.offset

Check warning on line 184 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L184

Added line #L184 was not covered by tests

def _coeffs_1_closure(self, m, v):
return m._set_coeffs_1(v)

Check warning on line 187 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L187

Added line #L187 was not covered by tests

def _coeffs_2_closure(self, m, v):
return m._set_coeffs_2(v)

Check warning on line 190 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L190

Added line #L190 was not covered by tests

def _offset_closure(self, m, v):
return m._set_offset(v)

Check warning on line 193 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L193

Added line #L193 was not covered by tests

def _set_coeffs_1(self, value):
if not torch.is_tensor(value):
value = torch.as_tensor(value).to(self.raw_coeffs_1)
self.initialize(raw_coeffs_1=self.coeff_constraint.inverse_transform(value))

Check warning on line 198 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L196-L198

Added lines #L196 - L198 were not covered by tests

def _set_coeffs_2(self, value):
if not torch.is_tensor(value):
value = torch.as_tensor(value).to(self.raw_coeffs_2)
self.initialize(raw_coeffs_2=self.coeff_constraint.inverse_transform(value))

Check warning on line 203 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L201-L203

Added lines #L201 - L203 were not covered by tests

def _set_offset(self, value):
if not torch.is_tensor(value):
value = torch.as_tensor(value).to(self.raw_offset)
self.initialize(raw_offset=self.coeff_constraint.inverse_transform(value))

Check warning on line 208 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L206-L208

Added lines #L206 - L208 were not covered by tests

@coeffs_1.setter
def coeffs_1(self, value):
self._set_coeffs_1(value)

Check warning on line 212 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L212

Added line #L212 was not covered by tests

@coeffs_2.setter
def coeffs_2(self, value):
self._set_coeffs_2(value)

Check warning on line 216 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L216

Added line #L216 was not covered by tests

@offset.setter # offsetter, lol
def offset(self, value):
self._set_offset(value)

Check warning on line 220 in botorch/models/kernels/orthogonal_additive_kernel.py

View check run for this annotation

Codecov / codecov/patch

botorch/models/kernels/orthogonal_additive_kernel.py#L220

Added line #L220 was not covered by tests

def forward(
self,
x1: Tensor,
Expand Down
69 changes: 68 additions & 1 deletion test/models/kernels/test_orthogonal_additive_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@

import torch
from botorch.exceptions.errors import UnsupportedError
from botorch.models.kernels.orthogonal_additive_kernel import OrthogonalAdditiveKernel
from botorch.models.kernels.orthogonal_additive_kernel import (
OrthogonalAdditiveKernel,
SECOND_ORDER_PRIOR_ERROR_MSG,
)
from botorch.utils.testing import BotorchTestCase
from gpytorch.kernels import MaternKernel, RBFKernel
from gpytorch.lazy import LazyEvaluatedKernelTensor
from gpytorch.mlls.exact_marginal_log_likelihood import ExactMarginalLogLikelihood
from gpytorch.priors import LogNormalPrior
from gpytorch.priors.torch_priors import GammaPrior, HalfCauchyPrior, NormalPrior
from torch import nn, Tensor


Expand Down Expand Up @@ -118,6 +124,67 @@ def test_kernel(self):
tol = 1e-5
self.assertTrue(((K_ortho @ oak.w).squeeze(-1) < tol).all())

def test_priors(self):
n, d = 3, 5
dtypes = [torch.float, torch.double]
batch_shapes = [(), (2,), (7, 2)]
for dtype in dtypes:
for batch_shape in batch_shapes:
tkwargs = {"dtype": dtype, "device": self.device}
# test with default args and batch_shape = None in second_order
offset_prior = HalfCauchyPrior(0.1)
coeffs_1_prior = LogNormalPrior(0, 1)
coeffs_2_prior = GammaPrior(3, 6)
oak = OrthogonalAdditiveKernel(
RBFKernel(),
dim=d,
batch_shape=None,
second_order=True,
offset_prior=offset_prior,
coeffs_1_prior=coeffs_1_prior,
coeffs_2_prior=coeffs_2_prior,
)

self.assertIsInstance(oak.offset_prior, HalfCauchyPrior)
self.assertIsInstance(oak.coeffs_1_prior, LogNormalPrior)
self.assertEqual(oak.coeffs_1_prior.scale, 1)
self.assertEqual(oak.coeffs_2_prior.concentration, 3)

oak = OrthogonalAdditiveKernel(
RBFKernel(),
dim=d,
batch_shape=None,
second_order=True,
coeffs_1_prior=None,
coeffs_2_prior=coeffs_2_prior,
)
self.assertEqual(oak.coeffs_2_prior.concentration, 3)
with self.assertRaisesRegex(
AttributeError,
"'OrthogonalAdditiveKernel' object has no attribute 'coeffs_1_prior",
):
_ = oak.coeffs_1_prior
# test with batch_shape = None in second_order
oak = OrthogonalAdditiveKernel(
RBFKernel(),
dim=d,
batch_shape=batch_shape,
second_order=True,
coeffs_1_prior=coeffs_1_prior,
)
with self.assertRaisesRegex(AttributeError, SECOND_ORDER_PRIOR_ERROR_MSG):
OrthogonalAdditiveKernel(
RBFKernel(),
dim=d,
batch_shape=None,
second_order=False,
coeffs_2_prior=GammaPrior(1, 1),
)
# test no prior
oak = OrthogonalAdditiveKernel(
RBFKernel(), dim=d, batch_shape=None, second_order=True
)


def isposdef(A: Tensor) -> bool:
"""Determines whether A is positive definite or not, by attempting a Cholesky
Expand Down

0 comments on commit 3d72ed5

Please sign in to comment.