Skip to content

Commit fc6fdba

Browse files
pjpollotfacebook-github-bot
authored andcommitted
Add Posterior Standard Deviation acquisition function (#2060)
Summary: ## Motivation I am a machine learning engineer actively researching Bayesian optimization solutions to apply in my company's products. Lately, I've been trying to find the right balance between exploitation and exploration by incorporating pure exploration into a sequential batch Bayesian optimization algorithm. `qNegIntegratedPosteriorVariance` was a suitable choice for this purpose, but it proved a bit slower when compared to the Posterior Standard Deviation acquisition function that I'm introducing in this pull request. The acquisition function simply returns the posterior standard deviation of the Gaussian process model, so the time complexity remains relatively low when compared to a Monte Carlo acquisition function. ### Have you read the [Contributing Guidelines on pull requests](https://github.com/pytorch/botorch/blob/main/CONTRIBUTING.md#pull-requests)? Yes. Pull Request resolved: #2060 Test Plan: `PosteriorStandardDeviation` class I implemented is extremely similar to `PosteriorMean`, so the implementation and the unit tests also look almost the same. I just made sure to define a variance for `MockPosterior` in order to effectively run the unit tests. As I may have some doubts about batch unit testing in order to verify if my solution only applies for `q=1`, I would be glad to hear it from you about my current implementation! PS: I guess there is no problem with the integration of the new acquisition function in the documentation! ⬇️ <img width="809" alt="doc screenshot" src="https://github.com/pytorch/botorch/assets/47068641/52e2cdb2-806c-4718-8c0d-63f7a0ac8efb"> ## Related PRs (If this PR adds or changes functionality, please take some time to update the docs at https://github.com/pytorch/botorch, and link to your PR here.) Reviewed By: Balandat Differential Revision: D50559929 Pulled By: saitcakmak fbshipit-source-id: 2c0b98d535315cd33b38eeb67a89e50682716ff8
1 parent 58da970 commit fc6fdba

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

botorch/acquisition/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
LogNoisyExpectedImprovement,
2121
NoisyExpectedImprovement,
2222
PosteriorMean,
23+
PosteriorStandardDeviation,
2324
ProbabilityOfImprovement,
2425
qAnalyticProbabilityOfImprovement,
2526
UpperConfidenceBound,
@@ -91,6 +92,7 @@
9192
"PairwiseBayesianActiveLearningByDisagreement",
9293
"PairwiseMCPosteriorVariance",
9394
"PosteriorMean",
95+
"PosteriorStandardDeviation",
9496
"PriorGuidedAcquisitionFunction",
9597
"ProbabilityOfImprovement",
9698
"ProximalAcquisitionFunction",

botorch/acquisition/analytic.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,58 @@ def forward(self, X: Tensor) -> Tensor:
869869
return self._mean_and_sigma(X, compute_sigma=False)[0] @ self.weights
870870

871871

872+
class PosteriorStandardDeviation(AnalyticAcquisitionFunction):
873+
r"""Single-outcome Posterior Standard Deviation.
874+
875+
An acquisition function for pure exploration.
876+
Only supports the case of q=1. Requires the model's posterior to have
877+
`mean` and `variance` properties. The model must be either single-outcome
878+
or combined with a `posterior_transform` to produce a single-output posterior.
879+
880+
Example:
881+
>>> model = SingleTaskGP(train_X, train_Y)
882+
>>> PSTD = PosteriorMean(model)
883+
>>> std = PSTD(test_X)
884+
"""
885+
886+
def __init__(
887+
self,
888+
model: Model,
889+
posterior_transform: Optional[PosteriorTransform] = None,
890+
maximize: bool = True,
891+
) -> None:
892+
r"""Single-outcome Posterior Mean.
893+
894+
Args:
895+
model: A fitted single-outcome GP model (must be in batch mode if
896+
candidate sets X will be)
897+
posterior_transform: A PosteriorTransform. If using a multi-output model,
898+
a PosteriorTransform that transforms the multi-output posterior into a
899+
single-output posterior is required.
900+
maximize: If True, consider the problem a maximization problem. Note
901+
that if `maximize=False`, the posterior standard deviation is negated.
902+
As a consequence,
903+
`optimize_acqf(PosteriorStandardDeviation(gp, maximize=False))`
904+
actually returns -1 * minimum of the posterior standard deviation.
905+
"""
906+
super().__init__(model=model, posterior_transform=posterior_transform)
907+
self.maximize = maximize
908+
909+
@t_batch_mode_transform(expected_q=1)
910+
def forward(self, X: Tensor) -> Tensor:
911+
r"""Evaluate the posterior standard deviation on the candidate set X.
912+
913+
Args:
914+
X: A `(b1 x ... bk) x 1 x d`-dim batched tensor of `d`-dim design points.
915+
916+
Returns:
917+
A `(b1 x ... bk)`-dim tensor of Posterior Mean values at the
918+
given design points `X`.
919+
"""
920+
_, std = self._mean_and_sigma(X)
921+
return std if self.maximize else -std
922+
923+
872924
# --------------- Helper functions for analytic acquisition functions. ---------------
873925

874926

test/acquisition/test_analytic.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
LogProbabilityOfImprovement,
2222
NoisyExpectedImprovement,
2323
PosteriorMean,
24+
PosteriorStandardDeviation,
2425
ProbabilityOfImprovement,
2526
ScalarizedPosteriorMean,
2627
UpperConfidenceBound,
@@ -292,6 +293,48 @@ def test_posterior_mean_batch(self):
292293
PosteriorMean(model=mm2)
293294

294295

296+
class TestPosteriorStandardDeviation(BotorchTestCase):
297+
def test_posterior_stddev(self):
298+
for dtype in (torch.float, torch.double):
299+
mean = torch.rand(3, 1, device=self.device, dtype=dtype)
300+
std = torch.rand_like(mean)
301+
mm = MockModel(MockPosterior(mean=mean, variance=std.square()))
302+
303+
acqf = PosteriorStandardDeviation(model=mm)
304+
X = torch.rand(3, 1, 2, device=self.device, dtype=dtype)
305+
pm = acqf(X)
306+
self.assertTrue(torch.equal(pm, std.view(-1)))
307+
308+
acqf = PosteriorStandardDeviation(model=mm, maximize=False)
309+
X = torch.rand(3, 1, 2, device=self.device, dtype=dtype)
310+
pm = acqf(X)
311+
self.assertTrue(torch.equal(pm, -std.view(-1)))
312+
313+
# check for proper error if multi-output model
314+
mean2 = torch.rand(1, 2, device=self.device, dtype=dtype)
315+
std2 = torch.rand_like(mean2)
316+
mm2 = MockModel(MockPosterior(mean=mean2, variance=std2.square()))
317+
with self.assertRaises(UnsupportedError):
318+
PosteriorStandardDeviation(model=mm2)
319+
320+
def test_posterior_stddev_batch(self):
321+
for dtype in (torch.float, torch.double):
322+
mean = torch.rand(3, 1, 1, device=self.device, dtype=dtype)
323+
std = torch.rand_like(mean)
324+
mm = MockModel(MockPosterior(mean=mean, variance=std.square()))
325+
acqf = PosteriorStandardDeviation(model=mm)
326+
X = torch.empty(3, 1, 1, device=self.device, dtype=dtype)
327+
pm = acqf(X)
328+
self.assertTrue(torch.equal(pm, std.view(-1)))
329+
# check for proper error if multi-output model
330+
mean2 = torch.rand(3, 1, 2, device=self.device, dtype=dtype)
331+
std2 = torch.rand_like(mean2)
332+
mm2 = MockModel(MockPosterior(mean=mean2, variance=std2.square()))
333+
msg = "Must specify a posterior transform when using a multi-output model."
334+
with self.assertRaisesRegex(UnsupportedError, msg):
335+
PosteriorStandardDeviation(model=mm2)
336+
337+
295338
class TestProbabilityOfImprovement(BotorchTestCase):
296339
def test_probability_of_improvement(self):
297340
for dtype in (torch.float, torch.double):

0 commit comments

Comments
 (0)