|
12 | 12 | from botorch.acquisition import LearnedObjective |
13 | 13 | from botorch.acquisition.objective import ( |
14 | 14 | ConstrainedMCObjective, |
| 15 | + ExpectationPosteriorTransform, |
15 | 16 | GenericMCObjective, |
16 | 17 | IdentityMCObjective, |
17 | 18 | LinearMCObjective, |
18 | 19 | MCAcquisitionObjective, |
19 | 20 | PosteriorTransform, |
20 | 21 | ScalarizedPosteriorTransform, |
21 | 22 | ) |
| 23 | +from botorch.exceptions.errors import UnsupportedError |
22 | 24 | from botorch.models.deterministic import PosteriorMeanModel |
23 | 25 | from botorch.models.pairwise_gp import PairwiseGP |
| 26 | +from botorch.posteriors import GPyTorchPosterior |
24 | 27 | from botorch.sampling.samplers import SobolQMCNormalSampler |
25 | 28 | from botorch.utils import apply_constraints |
26 | 29 | from botorch.utils.testing import _get_test_posterior, BotorchTestCase |
| 30 | +from gpytorch.distributions import MultitaskMultivariateNormal, MultivariateNormal |
| 31 | +from gpytorch.lazy import lazify |
27 | 32 | from torch import Tensor |
28 | 33 |
|
29 | 34 |
|
@@ -83,6 +88,143 @@ def test_scalarized_posterior_transform(self): |
83 | 88 | self.assertTrue(torch.equal(val, val_expected)) |
84 | 89 |
|
85 | 90 |
|
| 91 | +class TestExpectationPosteriorTransform(BotorchTestCase): |
| 92 | + def test_init(self): |
| 93 | + # Without weights. |
| 94 | + tf = ExpectationPosteriorTransform(n_w=5) |
| 95 | + self.assertEqual(tf.n_w, 5) |
| 96 | + self.assertTrue(torch.allclose(tf.weights, torch.ones(5, 1) * 0.2)) |
| 97 | + # Errors with weights. |
| 98 | + with self.assertRaisesRegex(ValueError, "a tensor of size"): |
| 99 | + ExpectationPosteriorTransform(n_w=3, weights=torch.ones(5, 1)) |
| 100 | + with self.assertRaisesRegex(ValueError, "non-negative"): |
| 101 | + ExpectationPosteriorTransform(n_w=3, weights=-torch.ones(3, 1)) |
| 102 | + # Successful init with weights. |
| 103 | + weights = torch.tensor([[1.0, 2.0], [2.0, 4.0], [3.0, 6.0]]) |
| 104 | + tf = ExpectationPosteriorTransform(n_w=3, weights=weights) |
| 105 | + self.assertTrue(torch.allclose(tf.weights, weights / torch.tensor([6.0, 12.0]))) |
| 106 | + |
| 107 | + def test_evaluate(self): |
| 108 | + for dtype in (torch.float, torch.double): |
| 109 | + tkwargs = {"dtype": dtype, "device": self.device} |
| 110 | + # Without weights. |
| 111 | + tf = ExpectationPosteriorTransform(n_w=3) |
| 112 | + Y = torch.rand(3, 6, 2, **tkwargs) |
| 113 | + self.assertTrue( |
| 114 | + torch.allclose(tf.evaluate(Y), Y.view(3, 2, 3, 2).mean(dim=-2)) |
| 115 | + ) |
| 116 | + # With weights - weights intentionally doesn't use tkwargs. |
| 117 | + weights = torch.tensor([[1.0, 2.0], [2.0, 1.0]]) |
| 118 | + tf = ExpectationPosteriorTransform(n_w=2, weights=weights) |
| 119 | + expected = (Y.view(3, 3, 2, 2) * weights.to(Y)).sum(dim=-2) / 3.0 |
| 120 | + self.assertTrue(torch.allclose(tf.evaluate(Y), expected)) |
| 121 | + |
| 122 | + def test_expectation_posterior_transform(self): |
| 123 | + tkwargs = {"dtype": torch.float, "device": self.device} |
| 124 | + # Without weights, simple expectation, single output, no batch. |
| 125 | + # q = 2, n_w = 3. |
| 126 | + org_loc = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], **tkwargs) |
| 127 | + org_covar = torch.tensor( |
| 128 | + [ |
| 129 | + [1.0, 0.8, 0.7, 0.3, 0.2, 0.1], |
| 130 | + [0.8, 1.0, 0.9, 0.25, 0.15, 0.1], |
| 131 | + [0.7, 0.9, 1.0, 0.2, 0.2, 0.05], |
| 132 | + [0.3, 0.25, 0.2, 1.0, 0.7, 0.6], |
| 133 | + [0.2, 0.15, 0.2, 0.7, 1.0, 0.7], |
| 134 | + [0.1, 0.1, 0.05, 0.6, 0.7, 1.0], |
| 135 | + ], |
| 136 | + **tkwargs |
| 137 | + ) |
| 138 | + org_mvn = MultivariateNormal(org_loc, lazify(org_covar)) |
| 139 | + org_post = GPyTorchPosterior(mvn=org_mvn) |
| 140 | + tf = ExpectationPosteriorTransform(n_w=3) |
| 141 | + tf_post = tf(org_post) |
| 142 | + self.assertIsInstance(tf_post, GPyTorchPosterior) |
| 143 | + self.assertEqual(tf_post.sample().shape, torch.Size([1, 2, 1])) |
| 144 | + tf_mvn = tf_post.mvn |
| 145 | + self.assertIsInstance(tf_mvn, MultivariateNormal) |
| 146 | + expected_loc = torch.tensor([2.0, 5.0], **tkwargs) |
| 147 | + # This is the average of each 3 x 3 block. |
| 148 | + expected_covar = torch.tensor([[0.8667, 0.1722], [0.1722, 0.7778]], **tkwargs) |
| 149 | + self.assertTrue(torch.allclose(tf_mvn.loc, expected_loc)) |
| 150 | + self.assertTrue( |
| 151 | + torch.allclose(tf_mvn.covariance_matrix, expected_covar, atol=1e-3) |
| 152 | + ) |
| 153 | + |
| 154 | + # With weights, 2 outputs, batched. |
| 155 | + tkwargs = {"dtype": torch.double, "device": self.device} |
| 156 | + # q = 2, n_w = 2, m = 2, leading to 8 values for loc and 8x8 cov. |
| 157 | + org_loc = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], **tkwargs) |
| 158 | + # We have 2 4x4 matrices with 0s as filler. Each block is for one outcome. |
| 159 | + # Each 2x2 sub block corresponds to `n_w`. |
| 160 | + org_covar = torch.tensor( |
| 161 | + [ |
| 162 | + [1.0, 0.8, 0.3, 0.2, 0.0, 0.0, 0.0, 0.0], |
| 163 | + [0.8, 1.4, 0.2, 0.1, 0.0, 0.0, 0.0, 0.0], |
| 164 | + [0.3, 0.2, 1.2, 0.5, 0.0, 0.0, 0.0, 0.0], |
| 165 | + [0.2, 0.1, 0.5, 1.0, 0.0, 0.0, 0.0, 0.0], |
| 166 | + [0.0, 0.0, 0.0, 0.0, 1.0, 0.7, 0.4, 0.3], |
| 167 | + [0.0, 0.0, 0.0, 0.0, 0.7, 0.8, 0.3, 0.2], |
| 168 | + [0.0, 0.0, 0.0, 0.0, 0.4, 0.3, 1.4, 0.5], |
| 169 | + [0.0, 0.0, 0.0, 0.0, 0.3, 0.2, 0.5, 1.2], |
| 170 | + ], |
| 171 | + **tkwargs |
| 172 | + ) |
| 173 | + # Making it batched by adding two more batches, mostly the same. |
| 174 | + org_loc = org_loc.repeat(3, 1) |
| 175 | + org_loc[1] += 100 |
| 176 | + org_loc[2] += 1000 |
| 177 | + org_covar = org_covar.repeat(3, 1, 1) |
| 178 | + # Construct the transform with weights. |
| 179 | + weights = torch.tensor([[1.0, 3.0], [2.0, 1.0]]) |
| 180 | + tf = ExpectationPosteriorTransform(n_w=2, weights=weights) |
| 181 | + # Construct the posterior. |
| 182 | + org_mvn = MultitaskMultivariateNormal( |
| 183 | + # The return of mvn.loc and the required input are different. |
| 184 | + # We constructed it according to the output of mvn.loc, |
| 185 | + # reshaping here to have the required `b x n x t` shape. |
| 186 | + org_loc.view(3, 2, 4).transpose(-2, -1), |
| 187 | + lazify(org_covar), |
| 188 | + interleaved=True, # To test the error. |
| 189 | + ) |
| 190 | + org_post = GPyTorchPosterior(mvn=org_mvn) |
| 191 | + # Error if interleaved. |
| 192 | + with self.assertRaisesRegex(UnsupportedError, "interleaved"): |
| 193 | + tf(org_post) |
| 194 | + # Construct the non-interleaved posterior. |
| 195 | + org_mvn = MultitaskMultivariateNormal( |
| 196 | + org_loc.view(3, 2, 4).transpose(-2, -1), |
| 197 | + lazify(org_covar), |
| 198 | + interleaved=False, |
| 199 | + ) |
| 200 | + org_post = GPyTorchPosterior(mvn=org_mvn) |
| 201 | + self.assertTrue(torch.equal(org_mvn.loc, org_loc)) |
| 202 | + tf_post = tf(org_post) |
| 203 | + self.assertIsInstance(tf_post, GPyTorchPosterior) |
| 204 | + self.assertEqual(tf_post.sample().shape, torch.Size([1, 3, 2, 2])) |
| 205 | + tf_mvn = tf_post.mvn |
| 206 | + self.assertIsInstance(tf_mvn, MultitaskMultivariateNormal) |
| 207 | + expected_loc = torch.tensor([[1.6667, 3.6667, 5.25, 7.25]], **tkwargs).repeat( |
| 208 | + 3, 1 |
| 209 | + ) |
| 210 | + expected_loc[1] += 100 |
| 211 | + expected_loc[2] += 1000 |
| 212 | + # This is the weighted average of each 2 x 2 block. |
| 213 | + expected_covar = torch.tensor( |
| 214 | + [ |
| 215 | + [1.0889, 0.1667, 0.0, 0.0], |
| 216 | + [0.1667, 0.8, 0.0, 0.0], |
| 217 | + [0.0, 0.0, 0.875, 0.35], |
| 218 | + [0.0, 0.0, 0.35, 1.05], |
| 219 | + ], |
| 220 | + **tkwargs |
| 221 | + ).repeat(3, 1, 1) |
| 222 | + self.assertTrue(torch.allclose(tf_mvn.loc, expected_loc, atol=1e-3)) |
| 223 | + self.assertTrue( |
| 224 | + torch.allclose(tf_mvn.covariance_matrix, expected_covar, atol=1e-3) |
| 225 | + ) |
| 226 | + |
| 227 | + |
86 | 228 | class TestMCAcquisitionObjective(BotorchTestCase): |
87 | 229 | def test_abstract_raises(self): |
88 | 230 | with self.assertRaises(TypeError): |
|
0 commit comments