def test_identity_mc_multi_output_objective(self): objective = IdentityMCMultiOutputObjective() with self.assertRaises(BotorchTensorDimensionError): IdentityMCMultiOutputObjective(outcomes=[0]) # test negative outcome without specifying num_outcomes with self.assertRaises(BotorchError): IdentityMCMultiOutputObjective(outcomes=[0, -1]) for batch_shape, m, dtype in itertools.product( ([], [3]), (2, 3), (torch.float, torch.double)): samples = torch.rand(*batch_shape, 2, m, device=self.device, dtype=dtype) self.assertTrue(torch.equal(objective(samples), samples))
def __init__( self, model: Model, sampler: Optional[MCSampler] = None, objective: Optional[MCMultiOutputObjective] = None, X_pending: Optional[Tensor] = None, ) -> None: r"""Constructor for the MCAcquisitionFunction base class. Args: model: A fitted model. sampler: The sampler used to draw base samples. Defaults to `SobolQMCNormalSampler(num_samples=128, collapse_batch_dims=True)`. objective: The MCMultiOutputObjective under which the samples are evaluated. Defaults to `IdentityMultiOutputObjective()`. X_pending: A `m x d`-dim Tensor of `m` design points that have points that have been submitted for function evaluation but have not yet been evaluated. """ super().__init__(model=model) if sampler is None: sampler = SobolQMCNormalSampler(num_samples=128, collapse_batch_dims=True) self.add_module("sampler", sampler) if objective is None: objective = IdentityMCMultiOutputObjective() elif not isinstance(objective, MCMultiOutputObjective): raise UnsupportedError( "Only objectives of type MCMultiOutputObjective are supported for " "Multi-Objective MC acquisition functions.") self.add_module("objective", objective) self.X_pending = None if X_pending is not None: self.set_X_pending(X_pending)
def test_identity_mc_multi_output_objective(self): objective = IdentityMCMultiOutputObjective() for batch_shape, m, dtype in itertools.product( ([], [3]), (2, 3), (torch.float, torch.double)): samples = torch.rand(*batch_shape, 2, m, device=self.device, dtype=dtype) self.assertTrue(torch.equal(objective(samples), samples))
def construct_inputs_qEHVI( model: Model, training_data: TrainingData, objective_thresholds: Tensor, objective: Optional[AcquisitionObjective] = None, **kwargs: Any, ) -> Dict[str, Any]: r"""Construct kwargs for `qExpectedHypervolumeImprovement` constructor.""" X_observed = training_data.X # compute posterior mean (for ref point computation ref pareto frontier) with torch.no_grad(): Y_pmean = model.posterior(X_observed).mean outcome_constraints = kwargs.pop("outcome_constraints", None) # For HV-based acquisition functions we pass the constraint transform directly if outcome_constraints is None: cons_tfs = None else: cons_tfs = get_outcome_constraint_transforms(outcome_constraints) # Adjust `Y_pmean` to contrain feasible points only. feas = torch.stack([c(Y_pmean) <= 0 for c in cons_tfs], dim=-1).all(dim=-1) Y_pmean = Y_pmean[feas] if objective is None: objective = IdentityMCMultiOutputObjective() ehvi_kwargs = construct_inputs_EHVI( model=model, training_data=training_data, objective_thresholds=objective_thresholds, objective=objective, # Pass `Y_pmean` that accounts for constraints to `construct_inputs_EHVI` # to ensure that correct non-dominated partitioning is produced. Y_pmean=Y_pmean, **kwargs, ) sampler = kwargs.get("sampler") if sampler is None: sampler = _get_sampler( mc_samples=kwargs.get("mc_samples", 128), qmc=kwargs.get("qmc", True) ) add_qehvi_kwargs = { "sampler": sampler, "X_pending": kwargs.get("X_pending"), "constraints": cons_tfs, "eta": kwargs.get("eta", 1e-3), } return {**ehvi_kwargs, **add_qehvi_kwargs}
def __init__( self, model: Model, sampler: Optional[MCSampler] = None, objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[List[Callable[[Tensor], Tensor]]] = None, X_pending: Optional[Tensor] = None, ) -> None: r"""Constructor for the MCAcquisitionFunction base class. Args: model: A fitted model. sampler: The sampler used to draw base samples. Defaults to `SobolQMCNormalSampler(num_samples=128, collapse_batch_dims=True)`. objective: The MCMultiOutputObjective under which the samples are evaluated. Defaults to `IdentityMultiOutputObjective()`. constraints: A list of callables, each mapping a Tensor of dimension `sample_shape x batch-shape x q x m` to a Tensor of dimension `sample_shape x batch-shape x q`, where negative values imply feasibility. X_pending: A `m x d`-dim Tensor of `m` design points that have points that have been submitted for function evaluation but have not yet been evaluated. """ super().__init__(model=model) if sampler is None: sampler = SobolQMCNormalSampler(num_samples=128, collapse_batch_dims=True) self.add_module("sampler", sampler) if objective is None: objective = IdentityMCMultiOutputObjective() elif not isinstance(objective, MCMultiOutputObjective): raise UnsupportedError( "Only objectives of type MCMultiOutputObjective are supported for " "Multi-Objective MC acquisition functions.") if (hasattr(model, "input_transform") and isinstance(model.input_transform, InputPerturbation) and constraints is not None): raise UnsupportedError( "Constraints are not supported with input perturbations, due to" "sample q-batch shape being different than that of the inputs." "Use a composite objective that applies feasibility weighting to" "samples before calculating the risk measure.") self.add_module("objective", objective) self.constraints = constraints self.X_pending = None if X_pending is not None: self.set_X_pending(X_pending)
def test_init(self): mm = MockModel(MockPosterior(mean=torch.rand(2, 1))) # test default init acqf = DummyMultiObjectiveAnalyticAcquisitionFunction(model=mm) self.assertIsInstance(acqf.objective, IdentityAnalyticMultiOutputObjective) # test custom init objective = DummyAnalyticMultiOutputObjective() acqf = DummyMultiObjectiveAnalyticAcquisitionFunction( model=mm, objective=objective) self.assertEqual(acqf.objective, objective) # test unsupported objective with self.assertRaises(UnsupportedError): DummyMultiObjectiveAnalyticAcquisitionFunction( model=mm, objective=IdentityMCMultiOutputObjective()) acqf = DummyMultiObjectiveAnalyticAcquisitionFunction(model=mm) # test set_X_pending with self.assertRaises(UnsupportedError): acqf.set_X_pending()
def construct_inputs_qNEHVI( model: Model, training_data: TrainingData, objective_thresholds: Tensor, objective: Optional[AcquisitionObjective] = None, **kwargs: Any, ) -> Dict[str, Any]: r"""Construct kwargs for `qNoisyExpectedHypervolumeImprovement` constructor.""" # This selects the objectives (a subset of the outcomes) and set each # objective threhsold to have the proper optimization direction. if objective is None: objective = IdentityMCMultiOutputObjective() outcome_constraints = kwargs.pop("outcome_constraints", None) if outcome_constraints is None: cons_tfs = None else: cons_tfs = get_outcome_constraint_transforms(outcome_constraints) sampler = kwargs.get("sampler") if sampler is None: sampler = _get_sampler( mc_samples=kwargs.get("mc_samples", 128), qmc=kwargs.get("qmc", True) ) return { "model": model, "ref_point": objective(objective_thresholds), "X_baseline": kwargs.get("X_baseline", training_data.X), "sampler": sampler, "objective": objective, "constraints": cons_tfs, "X_pending": kwargs.get("X_pending"), "eta": kwargs.get("eta", 1e-3), "prune_baseline": kwargs.get("prune_baseline", True), "alpha": kwargs.get("alpha", 0.0), "cache_pending": kwargs.get("cache_pending", True), "max_iep": kwargs.get("max_iep", 0), "incremental_nehvi": kwargs.get("incremental_nehvi", True), }
def test_q_expected_hypervolume_improvement(self): tkwargs = {"device": self.device} for dtype in (torch.float, torch.double): ref_point = [0.0, 0.0] tkwargs["dtype"] = dtype pareto_Y = torch.tensor( [[4.0, 5.0], [5.0, 5.0], [8.5, 3.5], [8.5, 3.0], [9.0, 1.0]], **tkwargs) partitioning = NondominatedPartitioning(num_outcomes=2) # the event shape is `b x q x m` = 1 x 1 x 2 samples = torch.zeros(1, 1, 2, **tkwargs) mm = MockModel(MockPosterior(samples=samples)) # test error if there is not pareto_Y initialized in partitioning with self.assertRaises(BotorchError): qExpectedHypervolumeImprovement(model=mm, ref_point=ref_point, partitioning=partitioning) partitioning.update(Y=pareto_Y) # test error if ref point has wrong shape with self.assertRaises(ValueError): qExpectedHypervolumeImprovement(model=mm, ref_point=ref_point[:1], partitioning=partitioning) X = torch.zeros(1, 1, **tkwargs) # basic test sampler = IIDNormalSampler(num_samples=1) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 0.0) # check ref point self.assertTrue( torch.equal(acqf.ref_point, torch.tensor(ref_point, **tkwargs))) # check cached indices self.assertTrue(hasattr(acqf, "q_subset_indices")) self.assertIn("q_choose_1", acqf.q_subset_indices) self.assertTrue( torch.equal( acqf.q_subset_indices["q_choose_1"], torch.tensor([[0]], device=self.device), )) # test q=2 X2 = torch.zeros(2, 1, **tkwargs) samples2 = torch.zeros(1, 2, 2, **tkwargs) mm2 = MockModel(MockPosterior(samples=samples2)) acqf.model = mm2 res = acqf(X2) self.assertEqual(res.item(), 0.0) # check cached indices self.assertTrue(hasattr(acqf, "q_subset_indices")) self.assertIn("q_choose_1", acqf.q_subset_indices) self.assertTrue( torch.equal( acqf.q_subset_indices["q_choose_1"], torch.tensor([[0], [1]], device=self.device), )) self.assertIn("q_choose_2", acqf.q_subset_indices) self.assertTrue( torch.equal( acqf.q_subset_indices["q_choose_2"], torch.tensor([[0, 1]], device=self.device), )) self.assertNotIn("q_choose_3", acqf.q_subset_indices) # now back to 1 and sure all caches were cleared acqf.model = mm res = acqf(X) self.assertNotIn("q_choose_2", acqf.q_subset_indices) self.assertIn("q_choose_1", acqf.q_subset_indices) self.assertTrue( torch.equal( acqf.q_subset_indices["q_choose_1"], torch.tensor([[0]], device=self.device), )) X = torch.zeros(1, 1, **tkwargs) samples = torch.zeros(1, 1, 2, **tkwargs) mm = MockModel(MockPosterior(samples=samples)) # basic test, no resample sampler = IIDNormalSampler(num_samples=2, seed=12345) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 0.0) self.assertEqual(acqf.sampler.base_samples.shape, torch.Size([2, 1, 1, 2])) bs = acqf.sampler.base_samples.clone() res = acqf(X) self.assertTrue(torch.equal(acqf.sampler.base_samples, bs)) # basic test, qmc, no resample sampler = SobolQMCNormalSampler(num_samples=2) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 0.0) self.assertEqual(acqf.sampler.base_samples.shape, torch.Size([2, 1, 1, 2])) bs = acqf.sampler.base_samples.clone() acqf(X) self.assertTrue(torch.equal(acqf.sampler.base_samples, bs)) # basic test, qmc, resample sampler = SobolQMCNormalSampler(num_samples=2, resample=True) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 0.0) self.assertEqual(acqf.sampler.base_samples.shape, torch.Size([2, 1, 1, 2])) bs = acqf.sampler.base_samples.clone() acqf(X) self.assertFalse(torch.equal(acqf.sampler.base_samples, bs)) # basic test for X_pending and warning acqf.set_X_pending() self.assertIsNone(acqf.X_pending) acqf.set_X_pending(None) self.assertIsNone(acqf.X_pending) acqf.set_X_pending(X) self.assertEqual(acqf.X_pending, X) res = acqf(X) X2 = torch.zeros(1, 1, 1, requires_grad=True, **tkwargs) with warnings.catch_warnings( record=True) as ws, settings.debug(True): acqf.set_X_pending(X2) self.assertEqual(acqf.X_pending, X2) self.assertEqual(len(ws), 1) self.assertTrue(issubclass(ws[-1].category, BotorchWarning)) # test objective acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, objective=IdentityMCMultiOutputObjective(), ) res = acqf(X) self.assertEqual(res.item(), 0.0) # Test that the hypervolume improvement is correct for given sample # test q = 1 X = torch.zeros(1, 1, **tkwargs) # basic test samples = torch.tensor([[[6.5, 4.5]]], **tkwargs) mm = MockModel(MockPosterior(samples=samples)) sampler = IIDNormalSampler(num_samples=1) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 1.5) # test q = 1, does not contribute samples = torch.tensor([0.0, 1.0], **tkwargs).view(1, 1, 2) sampler = IIDNormalSampler(1) mm = MockModel(MockPosterior(samples=samples)) acqf.model = mm res = acqf(X) self.assertEqual(res.item(), 0.0) # test q = 2, both points contribute X = torch.zeros(2, 1, **tkwargs) samples = torch.tensor([[6.5, 4.5], [7.0, 4.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf.model = mm res = acqf(X) self.assertEqual(res.item(), 1.75) # test q = 2, only 1 point contributes samples = torch.tensor([[6.5, 4.5], [6.0, 4.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf.model = mm res = acqf(X) self.assertEqual(res.item(), 1.5) # test q = 2, neither contributes samples = torch.tensor([[2.0, 2.0], [0.0, 0.1]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf.model = mm res = acqf(X) self.assertEqual(res.item(), 0.0) # test q = 2, test point better than current best second objective samples = torch.tensor([[6.5, 4.5], [6.0, 6.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf.model = mm res = acqf(X) self.assertEqual(res.item(), 8.0) # test q = 2, test point better than current-best first objective samples = torch.tensor([[6.5, 4.5], [9.0, 2.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 2.0) # test q = 3, all contribute X = torch.zeros(3, 1, **tkwargs) samples = torch.tensor([[6.5, 4.5], [9.0, 2.0], [7.0, 4.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 2.25) # test q = 3, not all contribute samples = torch.tensor([[6.5, 4.5], [9.0, 2.0], [7.0, 5.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 3.5) # test q = 3, none contribute samples = torch.tensor([[0.0, 4.5], [1.0, 2.0], [3.0, 0.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 0.0) # test m = 3, q=1 pareto_Y = torch.tensor( [[4.0, 2.0, 3.0], [3.0, 5.0, 1.0], [2.0, 4.0, 2.0], [1.0, 3.0, 4.0]], **tkwargs, ) partitioning = NondominatedPartitioning(num_outcomes=3, Y=pareto_Y) samples = torch.tensor([[1.0, 2.0, 6.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) ref_point = [-1.0] * 3 acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) X = torch.zeros(1, 2, **tkwargs) res = acqf(X) self.assertEqual(res.item(), 12.0) # change reference point ref_point = [0.0] * 3 acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 4.0) # test m = 3, no contribution ref_point = [1.0] * 3 acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) res = acqf(X) self.assertEqual(res.item(), 0.0) # test m = 3, q = 2 pareto_Y = torch.tensor( [[4.0, 2.0, 3.0], [3.0, 5.0, 1.0], [2.0, 4.0, 2.0]], **tkwargs) samples = torch.tensor([[1.0, 2.0, 6.0], [1.0, 3.0, 4.0]], **tkwargs).unsqueeze(0) mm = MockModel(MockPosterior(samples=samples)) ref_point = [-1.0] * 3 partitioning = NondominatedPartitioning(num_outcomes=3, Y=pareto_Y) acqf = qExpectedHypervolumeImprovement( model=mm, ref_point=ref_point, partitioning=partitioning, sampler=sampler, ) X = torch.zeros(2, 2, **tkwargs) res = acqf(X) self.assertEqual(res.item(), 22.0)
def qehvi_candidates_func( train_x: "torch.Tensor", train_obj: "torch.Tensor", train_con: Optional["torch.Tensor"], bounds: "torch.Tensor", ) -> "torch.Tensor": """Quasi MC-based batch Expected Hypervolume Improvement (qEHVI). The default value of ``candidates_func`` in :class:`~optuna.integration.BoTorchSampler` with multi-objective optimization when the number of objectives is three or less. .. seealso:: :func:`~optuna.integration.botorch.qei_candidates_func` for argument and return value descriptions. """ n_objectives = train_obj.size(-1) if train_con is not None: train_y = torch.cat([train_obj, train_con], dim=-1) is_feas = (train_con <= 0).all(dim=-1) train_obj_feas = train_obj[is_feas] constraints = [] n_constraints = train_con.size(1) for i in range(n_constraints): constraints.append(lambda Z, i=i: Z[..., -n_constraints + i]) additional_qehvi_kwargs = { "objective": IdentityMCMultiOutputObjective(outcomes=list(range(n_objectives))), "constraints": constraints, } else: train_y = train_obj train_obj_feas = train_obj additional_qehvi_kwargs = {} train_x = normalize(train_x, bounds=bounds) model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=train_y.shape[-1])) mll = ExactMarginalLogLikelihood(model.likelihood, model) fit_gpytorch_model(mll) # Approximate box decomposition similar to Ax when the number of objectives is large. # https://github.com/facebook/Ax/blob/master/ax/models/torch/botorch_moo_defaults if n_objectives > 2: alpha = 10**(-8 + n_objectives) else: alpha = 0.0 partitioning = NondominatedPartitioning(num_outcomes=n_objectives, Y=train_obj_feas, alpha=alpha) ref_point = train_obj.min(dim=0).values - 1e-8 ref_point_list = ref_point.tolist() acqf = qExpectedHypervolumeImprovement( model=model, ref_point=ref_point_list, partitioning=partitioning, sampler=SobolQMCNormalSampler(num_samples=256), **additional_qehvi_kwargs, ) standard_bounds = torch.zeros_like(bounds) standard_bounds[1] = 1 candidates, _ = optimize_acqf( acq_function=acqf, bounds=standard_bounds, q=1, num_restarts=20, raw_samples=1024, options={ "batch_limit": 5, "maxiter": 200, "nonnegative": True }, sequential=True, ) candidates = unnormalize(candidates.detach(), bounds=bounds) return candidates
def evaluate(mth, run_i, seed): print(mth, run_i, seed, '===== start =====', flush=True) def objective_function(x: torch.Tensor): # Caution: unnormalize and maximize x = unnormalize(x, bounds=problem_bounds) x = x.cpu().numpy().astype(np.float64) # caution res = problem.evaluate(x) res['objs'] = [-y for y in res['objs']] return res # Caution: negative values imply feasibility in botorch hv_diffs = [] time_list = [] global_start_time = time.time() # random seed np.random.seed(seed) torch.manual_seed(seed) # call helper functions to generate initial training data and initialize model train_x, train_obj, train_con = generate_initial_data( initial_runs, objective_function, time_list, global_start_time) # fix bug: find feasible real_initial_runs = initial_runs while real_initial_runs < max_runs: # compute feasible observations is_feas = (train_con <= 0).all(dim=-1) # compute points that are better than the known reference point better_than_ref = (train_obj > problem.ref_point).all(dim=-1) if (is_feas & better_than_ref).any(): break train_x, train_obj, train_con = expand_initial_data( train_x, train_obj, train_con, objective_function, time_list, global_start_time) real_initial_runs += 1 print('=== Expand initial data to find feasible. Iter =', real_initial_runs) mll, model = initialize_model(train_x, train_obj, train_con) # for plot X_init = train_x.cpu().numpy().astype(np.float64) Y_init = -1 * train_obj.cpu().numpy().astype(np.float64) # calculate hypervolume of init data for i in range(real_initial_runs): train_obj_i = train_obj[:i + 1] train_con_i = train_con[:i + 1] # compute pareto front is_feas_i = (train_con_i <= 0).all(dim=-1) feas_train_obj_i = train_obj_i[is_feas_i] pareto_mask = is_non_dominated(feas_train_obj_i) pareto_y = feas_train_obj_i[pareto_mask] # compute hypervolume volume = hv.compute(pareto_y) hv_diff = problem.max_hv - volume hv_diffs.append(hv_diff) # run (max_runs - real_initial_runs) rounds of BayesOpt after the initial random batch for iteration in range(real_initial_runs + 1, max_runs + 1): t0 = time.time() try: # fit the models fit_gpytorch_model(mll) # define the qEHVI acquisition modules using a QMC sampler sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES) # compute feasible observations is_feas = (train_con <= 0).all(dim=-1) # compute points that are better than the known reference point better_than_ref = (train_obj > problem.ref_point).all(dim=-1) # partition non-dominated space into disjoint rectangles partitioning = NondominatedPartitioning( num_outcomes=problem.num_objs, # use observations that are better than the specified reference point and feasible Y=train_obj[better_than_ref & is_feas], ) qEHVI = qExpectedHypervolumeImprovement( model=model, ref_point=problem.ref_point.tolist( ), # use known reference point partitioning=partitioning, sampler=sampler, # define an objective that specifies which outcomes are the objectives objective=IdentityMCMultiOutputObjective( outcomes=list(range(problem.num_objs))), # specify that the constraint is on the last outcome constraints=constraint_callable_list( problem.num_constraints, num_objs=problem.num_objs), ) # optimize and get new observation new_x, new_obj, new_con = optimize_acqf_and_get_observation( qEHVI, objective_function, time_list, global_start_time) except Exception as e: # handle numeric problem step = 2 print( '===== Exception in optimization loop, restart with 1/%d of training data: %s' % (step, str(e))) if refit == 1: mll, model = initialize_model(train_x[::step], train_obj[::step], train_con[::step]) else: mll, model = initialize_model( train_x[::step], train_obj[::step], train_con[::step], model.state_dict(), ) # fit the models fit_gpytorch_model(mll) # define the qEHVI acquisition modules using a QMC sampler sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES) # compute feasible observations is_feas = (train_con[::step] <= 0).all(dim=-1) # compute points that are better than the known reference point better_than_ref = (train_obj[::step] > problem.ref_point).all( dim=-1) # partition non-dominated space into disjoint rectangles partitioning = NondominatedPartitioning( num_outcomes=problem.num_objs, # use observations that are better than the specified reference point and feasible Y=train_obj[::step][better_than_ref & is_feas], ) qEHVI = qExpectedHypervolumeImprovement( model=model, ref_point=problem.ref_point.tolist( ), # use known reference point partitioning=partitioning, sampler=sampler, # define an objective that specifies which outcomes are the objectives objective=IdentityMCMultiOutputObjective( outcomes=list(range(problem.num_objs))), # specify that the constraint is on the last outcome constraints=constraint_callable_list( problem.num_constraints, num_objs=problem.num_objs), ) # optimize and get new observation new_x, new_obj, new_con = optimize_acqf_and_get_observation( qEHVI, objective_function, time_list, global_start_time) assert len(time_list) == iteration # update training points train_x = torch.cat([train_x, new_x]) train_obj = torch.cat([train_obj, new_obj]) train_con = torch.cat([train_con, new_con]) # update progress # compute pareto front is_feas = (train_con <= 0).all(dim=-1) feas_train_obj = train_obj[is_feas] pareto_mask = is_non_dominated(feas_train_obj) pareto_y = feas_train_obj[pareto_mask] # compute hypervolume volume = hv.compute(pareto_y) hv_diff = problem.max_hv - volume hv_diffs.append(hv_diff) # reinitialize the models so they are ready for fitting on next iteration # use the current state dict to speed up fitting # Note: they find improved performance from not warm starting the model hyperparameters # using the hyperparameters from the previous iteration if refit == 1: mll, model = initialize_model(train_x, train_obj, train_con) else: mll, model = initialize_model( train_x, train_obj, train_con, model.state_dict(), ) t1 = time.time() print( "Iter %d: x=%s, perf=%s, con=%s, hv_diff=%f, time=%.2f, global_time=%.2f" % (iteration, unnormalize(new_x, bounds=problem_bounds), -new_obj, new_con, hv_diff, t1 - t0, time_list[-1]), flush=True) # compute pareto front is_feas = (train_con <= 0).all(dim=-1) feas_train_obj = train_obj[is_feas] pareto_mask = is_non_dominated(feas_train_obj) pareto_y = feas_train_obj[pareto_mask] pf = -1 * pareto_y.cpu().numpy().astype(np.float64) # Save result X = unnormalize(train_x, bounds=problem_bounds).cpu().numpy().astype( np.float64) # caution train_obj[~is_feas] = -INFEASIBLE_OBJ_VALUE # set infeasible Y = -1 * train_obj.cpu().numpy().astype(np.float64) # plot for debugging if plot_mode == 1: plot_pf(problem, problem_str, mth, pf, Y_init) return hv_diffs, pf, X, Y, time_list
def prune_inferior_points_multi_objective( model: Model, X: Tensor, ref_point: Tensor, objective: Optional[MCMultiOutputObjective] = None, constraints: Optional[List[Callable[[Tensor], Tensor]]] = None, num_samples: int = 2048, max_frac: float = 1.0, marginalize_dim: Optional[int] = None, ) -> Tensor: r"""Prune points from an input tensor that are unlikely to be pareto optimal. Given a model, an objective, and an input tensor `X`, this function returns the subset of points in `X` that have some probability of being pareto optimal, better than the reference point, and feasible. This function uses sampling to estimate the probabilities, the higher the number of points `n` in `X` the higher the number of samples `num_samples` should be to obtain accurate estimates. Args: model: A fitted model. Batched models are currently not supported. X: An input tensor of shape `n x d`. Batched inputs are currently not supported. ref_point: The reference point. objective: The objective under which to evaluate the posterior. constraints: A list of callables, each mapping a Tensor of dimension `sample_shape x batch-shape x q x m` to a Tensor of dimension `sample_shape x batch-shape x q`, where negative values imply feasibility. num_samples: The number of samples used to compute empirical probabilities of being the best point. max_frac: The maximum fraction of points to retain. Must satisfy `0 < max_frac <= 1`. Ensures that the number of elements in the returned tensor does not exceed `ceil(max_frac * n)`. marginalize_dim: A batch dimension that should be marginalized. For example, this is useful when using a batched fully Bayesian model. Returns: A `n' x d` with subset of points in `X`, where n' = min(N_nz, ceil(max_frac * n)) with `N_nz` the number of points in `X` that have non-zero (empirical, under `num_samples` samples) probability of being pareto optimal. """ if X.ndim > 2: # TODO: support batched inputs (req. dealing with ragged tensors) raise UnsupportedError( "Batched inputs `X` are currently unsupported by " "prune_inferior_points_multi_objective") max_points = math.ceil(max_frac * X.size(-2)) if max_points < 1 or max_points > X.size(-2): raise ValueError(f"max_frac must take values in (0, 1], is {max_frac}") with torch.no_grad(): posterior = model.posterior(X=X) if posterior.event_shape.numel() > SobolEngine.MAXDIM: if settings.debug.on(): warnings.warn( f"Sample dimension q*m={posterior.event_shape.numel()} exceeding Sobol " f"max dimension ({SobolEngine.MAXDIM}). Using iid samples instead.", SamplingWarning, ) sampler = IIDNormalSampler(num_samples=num_samples) else: sampler = SobolQMCNormalSampler(num_samples=num_samples) samples = sampler(posterior) if objective is None: objective = IdentityMCMultiOutputObjective() obj_vals = objective(samples, X=X) if obj_vals.ndim > 3: if obj_vals.ndim == 4 and marginalize_dim is not None: obj_vals = obj_vals.mean(dim=marginalize_dim) else: # TODO: support batched inputs (req. dealing with ragged tensors) raise UnsupportedError( "Models with multiple batch dims are currently unsupported by" " prune_inferior_points_multi_objective.") if constraints is not None: infeas = torch.stack([c(samples) > 0 for c in constraints], dim=0).any(dim=0) if infeas.ndim == 3 and marginalize_dim is not None: # make sure marginalize_dim is not negative if marginalize_dim < 0: # add 1 to the normalize marginalize_dim since we have already # removed the output dim marginalize_dim = ( 1 + normalize_indices([marginalize_dim], d=infeas.ndim)[0]) infeas = infeas.float().mean(dim=marginalize_dim).round().bool() # set infeasible points to be the ref point obj_vals[infeas] = ref_point pareto_mask = is_non_dominated( obj_vals, deduplicate=False) & (obj_vals > ref_point).all(dim=-1) probs = pareto_mask.to(dtype=X.dtype).mean(dim=0) idcs = probs.nonzero().view(-1) if idcs.shape[0] > max_points: counts, order_idcs = torch.sort(probs, descending=True) idcs = order_idcs[:max_points] return X[idcs]