Skip to content

Commit 29877b8

Browse files
sdaultonfacebook-github-bot
authored andcommitted
utility for greedily selecting an approximate HV maximizing subset (#2936)
Summary: Pull Request resolved: #2936 This adds a utility for obtaining an approximate HV maximizing set using a sequential greedy algorithm. Reviewed By: bletham Differential Revision: D77696625 fbshipit-source-id: 5f1203d99c5422c5691acf64348e4ce5847f5640
1 parent 853a4c4 commit 29877b8

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

botorch/utils/multi_objective/hypervolume.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
from __future__ import annotations
2323

24+
import random
25+
2426
import warnings
2527
from collections.abc import Callable
2628
from copy import deepcopy
@@ -833,3 +835,56 @@ def _hypervolumes(self) -> Tensor:
833835
.to(self.ref_point) # for m > 2, the partitioning is on the CPU
834836
.view(self._batch_sample_shape)
835837
)
838+
839+
840+
def get_hypervolume_maximizing_subset(
841+
n: int, Y: Tensor, ref_point: Tensor
842+
) -> tuple[Tensor, Tensor]:
843+
"""Find an approximately hypervolume-maximizing subset of size `n`.
844+
845+
This greedily selects points from Y to maximize the hypervolume of
846+
the subset sequentially. This has bounded error since hypervolume is
847+
submodular.
848+
849+
Args:
850+
n: The size of the subset to return.
851+
Y: A `n' x m`-dim tensor of outcomes.
852+
ref_point: A `m`-dim tensor containing the reference point.
853+
854+
Returns:
855+
A two-element tuple containing
856+
- A `n x m`-dim tensor of outcomes.
857+
- A `n`-dim tensor of indices of the outcomes in the original set.
858+
"""
859+
if Y.ndim != 2:
860+
raise NotImplementedError(
861+
"Only two dimensions are supported (no additional) batch dims."
862+
)
863+
elif Y.shape[0] < n:
864+
raise ValueError(
865+
f"Y has fewer points ({Y.shape[0]}) than the requested subset size ({n})."
866+
)
867+
Y_subset = torch.zeros(0, Y.shape[1], dtype=Y.dtype, device=Y.device)
868+
selected_indices = []
869+
remaining_idcs = set(range(Y.shape[0]))
870+
best_hv = 0.0
871+
for _ in range(n):
872+
# Add each point and compute the hypervolume
873+
best_idx = None
874+
for i in remaining_idcs:
875+
partitioning = DominatedPartitioning(
876+
ref_point=ref_point, Y=torch.cat((Y_subset, Y[i : i + 1]), dim=0)
877+
)
878+
hv = partitioning.compute_hypervolume().item()
879+
if hv > best_hv:
880+
best_idx = i
881+
best_hv = hv
882+
if best_idx is None:
883+
# no arm improved HV, so select a random arm. This will only happen if Y is
884+
# not a Pareto frontier, where all points are better than the reference
885+
# point
886+
best_idx = random.choice(list(remaining_idcs))
887+
remaining_idcs.remove(best_idx)
888+
selected_indices.append(best_idx)
889+
Y_subset = torch.cat((Y_subset, Y[best_idx : best_idx + 1]), dim=0)
890+
return Y_subset, torch.tensor(selected_indices, dtype=torch.long, device=Y.device)

test/utils/multi_objective/test_hypervolume.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
import torch
1010
from botorch.exceptions.errors import BotorchError, BotorchTensorDimensionError
11-
from botorch.utils.multi_objective.hypervolume import Hypervolume, infer_reference_point
11+
from botorch.utils.multi_objective.hypervolume import (
12+
get_hypervolume_maximizing_subset,
13+
Hypervolume,
14+
infer_reference_point,
15+
)
1216
from botorch.utils.testing import BotorchTestCase
1317

1418
EPS = 1e-4
@@ -300,3 +304,64 @@ def test_infer_reference_point(self):
300304
pareto_Y=Y[:0],
301305
max_ref_point=torch.tensor([float("nan"), -1e5], **tkwargs),
302306
)
307+
308+
309+
class TestGetHypervolumeMaximizingSubset(BotorchTestCase):
310+
def test_get_hypervolume_maximizing_subset(self) -> None:
311+
# test invalid shapes
312+
ref_point = torch.torch.zeros(2)
313+
for invalid_y in (torch.zeros(2), torch.zeros(1, 1, 2)):
314+
with self.assertRaisesRegex(
315+
NotImplementedError,
316+
r"Only two dimensions are supported \(no additional\) batch dims.",
317+
):
318+
get_hypervolume_maximizing_subset(n=1, Y=invalid_y, ref_point=ref_point)
319+
# test n > Y.shape[0]
320+
with self.assertRaisesRegex(
321+
ValueError,
322+
r"Y has fewer points \(1\) than the requested subset size \(2\).",
323+
):
324+
get_hypervolume_maximizing_subset(
325+
n=2, Y=torch.zeros(1, 2), ref_point=ref_point
326+
)
327+
for dtype in (torch.float, torch.double):
328+
Y = torch.tensor(
329+
[
330+
[-13.9599, -24.0326],
331+
[-19.6755, -11.4721],
332+
[-18.7742, -11.9193],
333+
[-16.6614, -12.3283],
334+
[-17.7663, -11.9941],
335+
[-17.4367, -12.2948],
336+
[-19.4244, -11.9158],
337+
[-14.0806, -22.0004],
338+
],
339+
dtype=dtype,
340+
device=self.device,
341+
)
342+
ref_point = torch.tensor([-20.0, -20.0], dtype=dtype, device=self.device)
343+
344+
Y_subset, idcs = get_hypervolume_maximizing_subset(
345+
n=3, Y=Y, ref_point=ref_point
346+
)
347+
self.assertTrue(torch.equal(Y_subset, Y[idcs]))
348+
self.assertTrue(
349+
torch.equal(
350+
idcs, torch.tensor([3, 4, 1], dtype=torch.long, device=self.device)
351+
)
352+
)
353+
# test without `n` pareto optimal points
354+
Y = torch.tensor(
355+
[[-5.0, -5.0], [-10.0, -10.0]],
356+
dtype=dtype,
357+
device=self.device,
358+
)
359+
Y_subset, idcs = get_hypervolume_maximizing_subset(
360+
n=2, Y=Y, ref_point=ref_point
361+
)
362+
self.assertTrue(torch.equal(Y_subset, Y))
363+
self.assertTrue(
364+
torch.equal(
365+
idcs, torch.tensor([0, 1], dtype=torch.long, device=self.device)
366+
)
367+
)

0 commit comments

Comments
 (0)