def optimize_qparego_and_get_observation(model, train_obj, sampler): """Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization of the qParEGO acquisition function, and returns a new candidate and observation.""" acq_func_list = [] for _ in range(BATCH_SIZE): weights = sample_simplex(problem.num_objectives, **tkwargs).squeeze() objective = GenericMCObjective( get_chebyshev_scalarization(weights=weights, Y=train_obj)) acq_func = qExpectedImprovement( # pyre-ignore: [28] model=model, objective=objective, best_f=objective(train_obj).max(), sampler=sampler, ) acq_func_list.append(acq_func) # optimize candidates, _ = optimize_acqf_list( acq_function_list=acq_func_list, bounds=standard_bounds, num_restarts=NUM_RESTARTS, raw_samples=RAW_SAMPLES, # used for intialization heuristic options={ "batch_limit": 5, "maxiter": 200 }, ) # observe new values new_x = unnormalize(candidates.detach(), bounds=problem.bounds) new_obj = problem(new_x) return new_x, new_obj
def get_PosteriorMean( model: Model, objective_weights: Tensor, outcome_constraints: Optional[Tuple[Tensor, Tensor]] = None, X_observed: Optional[Tensor] = None, X_pending: Optional[Tensor] = None, **kwargs: Any, ) -> AcquisitionFunction: r"""Instantiates a PosteriorMean acquisition function. Note: If no OutcomeConstraints given, return an analytic acquisition function. This requires {optimizer_kwargs: {joint_optimization: True}} or an optimizer that does not assume pending point support. Args: objective_weights: The objective is to maximize a weighted sum of the columns of f(x). These are the weights. outcome_constraints: A tuple of (A, b). For k outcome constraints and m outputs at f(x), A is (k x m) and b is (k x 1) such that A f(x) <= b. (Not used by single task models) X_observed: A tensor containing points observed for all objective outcomes and outcomes that appear in the outcome constraints (if there are any). X_pending: A tensor containing points whose evaluation is pending (i.e. that have been submitted for evaluation) present for all objective outcomes and outcomes that appear in the outcome constraints (if there are any). Returns: PosteriorMean: The instantiated acquisition function. """ if X_observed is None: raise ValueError("There are no feasible observed points.") # construct Objective module if kwargs.get("chebyshev_scalarization", False): obj_tf = get_chebyshev_scalarization( weights=objective_weights, Y=torch.stack(kwargs.get("Ys")).transpose(0, 1).squeeze(-1), ) else: obj_tf = get_objective_weights_transform(objective_weights) def obj_fn(samples: Tensor, X: Optional[Tensor] = None) -> Tensor: return obj_tf(samples) if outcome_constraints is None: objective = GenericMCObjective(objective=obj_fn) else: con_tfs = get_outcome_constraint_transforms(outcome_constraints) inf_cost = get_infeasible_cost(X=X_observed, model=model, objective=obj_fn) objective = ConstrainedMCObjective(objective=obj_fn, constraints=con_tfs or [], infeasible_cost=inf_cost) # Use qSimpleRegret, not analytic posterior, to handle arbitrary objective fns. acq_func = qSimpleRegret(model, objective=objective) return acq_func
def test_get_chebyshev_scalarization(self): tkwargs = {"device": self.device} Y_train = torch.rand(4, 2, **tkwargs) Y_bounds = torch.stack( [ Y_train.min(dim=-2, keepdim=True).values, Y_train.max(dim=-2, keepdim=True).values, ], dim=0, ) for dtype in (torch.float, torch.double): for batch_shape in (torch.Size([]), torch.Size([3])): tkwargs["dtype"] = dtype Y_test = torch.rand(batch_shape + torch.Size([5, 2]), **tkwargs) Y_train = Y_train.to(**tkwargs) Y_bounds = Y_bounds.to(**tkwargs) normalized_Y_test = normalize(Y_test, Y_bounds) # test wrong shape with self.assertRaises(BotorchTensorDimensionError): get_chebyshev_scalarization(weights=torch.zeros( 3, **tkwargs), Y=Y_train) weights = torch.ones(2, **tkwargs) # test batch Y with self.assertRaises(NotImplementedError): get_chebyshev_scalarization(weights=weights, Y=Y_train.unsqueeze(0)) # basic test objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + 0.05 * normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test different alpha objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train, alpha=1.0) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # Test different weights weights = torch.tensor([0.3, 0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed))
def optimize_qparego_and_get_observation(model, train_obj, train_con, sampler, obj_func, time_list, global_start_time): """Samples a set of random weights for each candidate in the batch, performs sequential greedy optimization of the qParEGO acquisition function, and returns a new candidate and observation.""" acq_func_list = [] for _ in range(1): # sample random weights weights = sample_simplex(problem.num_objs, **tkwargs).squeeze() # construct augmented Chebyshev scalarization scalarization = get_chebyshev_scalarization(weights=weights, Y=train_obj) # initialize ConstrainedMCObjective constrained_objective = get_constrained_mc_objective(train_obj=train_obj, train_con=train_con, scalarization=scalarization) train_y = torch.cat([train_obj, train_con], dim=-1) acq_func = qExpectedImprovement( # pyre-ignore: [28] model=model, objective=constrained_objective, best_f=constrained_objective(train_y).max(), sampler=sampler, ) acq_func_list.append(acq_func) # optimize candidates, _ = optimize_acqf_list( acq_function_list=acq_func_list, bounds=standard_bounds, num_restarts=20, raw_samples=1024, # used for intialization heuristic options={"batch_limit": 5, "maxiter": 200}, ) # observe new values new_x = candidates.detach() new_obj = [] new_con = [] for x in new_x: res = obj_func(x) y = res['objs'] c = res['constraints'] new_obj.append(y) new_con.append(c) global_time = time.time() - global_start_time time_list.append(global_time) new_obj = torch.tensor(new_obj, **tkwargs).reshape(new_x.shape[0], -1) new_con = torch.tensor(new_con, **tkwargs).reshape(new_x.shape[0], -1) print(f'evaluate {new_x.shape[0]} configs on real objective') return new_x, new_obj, new_con
def qparego_candidates_func( train_x: "torch.Tensor", train_obj: "torch.Tensor", train_con: Optional["torch.Tensor"], bounds: "torch.Tensor", ) -> "torch.Tensor": """Quasi MC-based extended ParEGO (qParEGO) for constrained multi-objective optimization. The default value of ``candidates_func`` in :class:`~optuna.integration.BoTorchSampler` with multi-objective optimization when the number of objectives is larger than three. .. seealso:: :func:`~optuna.integration.botorch.qei_candidates_func` for argument and return value descriptions. """ n_objectives = train_obj.size(-1) weights = sample_simplex(n_objectives).squeeze() scalarization = get_chebyshev_scalarization(weights=weights, Y=train_obj) if train_con is not None: train_y = torch.cat([train_obj, train_con], dim=-1) constraints = [] n_constraints = train_con.size(1) for i in range(n_constraints): constraints.append(lambda Z, i=i: Z[..., -n_constraints + i]) objective = ConstrainedMCObjective( objective=lambda Z: scalarization(Z[..., :n_objectives]), constraints=constraints, ) else: train_y = train_obj objective = GenericMCObjective(scalarization) train_x = normalize(train_x, bounds=bounds) model = SingleTaskGP(train_x, train_y, outcome_transform=Standardize(m=train_y.size(-1))) mll = ExactMarginalLogLikelihood(model.likelihood, model) fit_gpytorch_model(mll) acqf = qExpectedImprovement( model=model, best_f=objective(train_y).max(), sampler=SobolQMCNormalSampler(num_samples=256), objective=objective, ) 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 }, sequential=True, ) candidates = unnormalize(candidates.detach(), bounds=bounds) return candidates
def get_NEI( model: Model, objective_weights: Tensor, outcome_constraints: Optional[Tuple[Tensor, Tensor]] = None, X_observed: Optional[Tensor] = None, X_pending: Optional[Tensor] = None, **kwargs: Any, ) -> AcquisitionFunction: r"""Instantiates a qNoisyExpectedImprovement acquisition function. Args: model: The underlying model which the acqusition function uses to estimate acquisition values of candidates. objective_weights: The objective is to maximize a weighted sum of the columns of f(x). These are the weights. outcome_constraints: A tuple of (A, b). For k outcome constraints and m outputs at f(x), A is (k x m) and b is (k x 1) such that A f(x) <= b. (Not used by single task models) X_observed: A tensor containing points observed for all objective outcomes and outcomes that appear in the outcome constraints (if there are any). X_pending: A tensor containing points whose evaluation is pending (i.e. that have been submitted for evaluation) present for all objective outcomes and outcomes that appear in the outcome constraints (if there are any). mc_samples: The number of MC samples to use (default: 512). qmc: If True, use qMC instead of MC (default: True). prune_baseline: If True, prune the baseline points for NEI (default: True). chebyshev_scalarization: Use augmented Chebyshev scalarization. Returns: qNoisyExpectedImprovement: The instantiated acquisition function. """ if X_observed is None: raise ValueError("There are no feasible observed points.") # construct Objective module if kwargs.get("chebyshev_scalarization", False): if "Ys" not in kwargs: raise ValueError("Chebyshev Scalarization requires Ys argument") Y_tensor = torch.cat(kwargs.get("Ys"), dim=-1) obj_tf = get_chebyshev_scalarization(weights=objective_weights, Y=Y_tensor) else: obj_tf = get_objective_weights_transform(objective_weights) if outcome_constraints is None: objective = GenericMCObjective(objective=obj_tf) else: con_tfs = get_outcome_constraint_transforms(outcome_constraints) inf_cost = get_infeasible_cost(X=X_observed, model=model, objective=obj_tf) objective = ConstrainedMCObjective(objective=obj_tf, constraints=con_tfs or [], infeasible_cost=inf_cost) return get_acquisition_function( acquisition_function_name="qNEI", model=model, objective=objective, X_observed=X_observed, X_pending=X_pending, prune_baseline=kwargs.get("prune_baseline", True), mc_samples=kwargs.get("mc_samples", 512), qmc=kwargs.get("qmc", True), # pyre-fixme[6]: Expected `Optional[int]` for 9th param but got # `Union[float, int]`. seed=torch.randint(1, 10000, (1, )).item(), )
def _get_acquisition_func( model: Model, acquisition_function_name: str, objective_weights: Tensor, outcome_constraints: Optional[Tuple[Tensor, Tensor]] = None, X_observed: Optional[Tensor] = None, X_pending: Optional[Tensor] = None, mc_objective: Type[GenericMCObjective] = GenericMCObjective, constrained_mc_objective: Optional[ Type[ConstrainedMCObjective] ] = ConstrainedMCObjective, mc_objective_kwargs: Optional[Dict] = None, **kwargs: Any, ) -> AcquisitionFunction: r"""Instantiates a acquisition function. Args: model: The underlying model which the acqusition function uses to estimate acquisition values of candidates. acquisition_function_name: Name of the acquisition function. objective_weights: The objective is to maximize a weighted sum of the columns of f(x). These are the weights. outcome_constraints: A tuple of (A, b). For k outcome constraints and m outputs at f(x), A is (k x m) and b is (k x 1) such that A f(x) <= b. (Not used by single task models) X_observed: A tensor containing points observed for all objective outcomes and outcomes that appear in the outcome constraints (if there are any). X_pending: A tensor containing points whose evaluation is pending (i.e. that have been submitted for evaluation) present for all objective outcomes and outcomes that appear in the outcome constraints (if there are any). mc_objective: GenericMCObjective class, used for constructing a MC-objective. If constructing a penalized MC-objective, pass in PenalizedMCObjective together with mc_objective_kwargs . constrained_mc_objective: ConstrainedMCObjective class, used when applying constraints on the outcomes. mc_objective_kwargs: kwargs for constructing MC-objective. For GenericMCObjective, leave it as None. For PenalizedMCObjective, it needs to be specified in the format of kwargs. mc_samples: The number of MC samples to use (default: 512). qmc: If True, use qMC instead of MC (default: True). prune_baseline: If True, prune the baseline points for NEI (default: True). chebyshev_scalarization: Use augmented Chebyshev scalarization. Returns: The instantiated acquisition function. """ if X_observed is None: raise ValueError("There are no feasible observed points.") # construct Objective module if kwargs.get("chebyshev_scalarization", False): with torch.no_grad(): Y = model.posterior(X_observed).mean obj_tf = get_chebyshev_scalarization(weights=objective_weights, Y=Y) else: obj_tf = get_objective_weights_transform(objective_weights) def objective(samples: Tensor, X: Optional[Tensor] = None) -> Tensor: return obj_tf(samples) if outcome_constraints is None: mc_objective_kwargs = {} if mc_objective_kwargs is None else mc_objective_kwargs objective = mc_objective(objective=objective, **mc_objective_kwargs) else: if constrained_mc_objective is None: raise ValueError( "constrained_mc_objective cannot be set to None " "when applying outcome constraints." ) if issubclass(mc_objective, PenalizedMCObjective): raise RuntimeError( "Outcome constraints are not supported for PenalizedMCObjective." ) con_tfs = get_outcome_constraint_transforms(outcome_constraints) inf_cost = get_infeasible_cost(X=X_observed, model=model, objective=objective) objective = constrained_mc_objective( objective=objective, constraints=con_tfs or [], infeasible_cost=inf_cost ) return get_acquisition_function( acquisition_function_name=acquisition_function_name, model=model, objective=objective, X_observed=X_observed, X_pending=X_pending, prune_baseline=kwargs.get("prune_baseline", True), mc_samples=kwargs.get("mc_samples", 512), qmc=kwargs.get("qmc", True), # pyre-fixme[6]: Expected `Optional[int]` for 9th param but got # `Union[float, int]`. seed=torch.randint(1, 10000, (1,)).item(), marginalize_dim=kwargs.get("marginalize_dim"), )
def test_get_chebyshev_scalarization(self): tkwargs = {"device": self.device} Y_train = torch.rand(4, 2, **tkwargs) Y_bounds = torch.stack( [ Y_train.min(dim=-2, keepdim=True).values, Y_train.max(dim=-2, keepdim=True).values, ], dim=0, ) for dtype in (torch.float, torch.double): for batch_shape in (torch.Size([]), torch.Size([3])): tkwargs["dtype"] = dtype Y_test = torch.rand(batch_shape + torch.Size([5, 2]), **tkwargs) Y_train = Y_train.to(**tkwargs) Y_bounds = Y_bounds.to(**tkwargs) normalized_Y_test = normalize(Y_test, Y_bounds) # test wrong shape with self.assertRaises(BotorchTensorDimensionError): get_chebyshev_scalarization(weights=torch.zeros( 3, **tkwargs), Y=Y_train) weights = torch.ones(2, **tkwargs) # test batch Y with self.assertRaises(NotImplementedError): get_chebyshev_scalarization(weights=weights, Y=Y_train.unsqueeze(0)) # basic test objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + 0.05 * normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test different alpha objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train, alpha=1.0) Y_transformed = objective_transform(Y_test) expected_Y_transformed = normalized_Y_test.min( dim=-1).values + normalized_Y_test.sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # Test different weights weights = torch.tensor([0.3, 0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test that when minimizing an objective (i.e. with a negative weight), # normalized Y values are shifted from [0,1] to [-1,0] weights = torch.tensor([0.3, -0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train) Y_transformed = objective_transform(Y_test) normalized_Y_test[..., -1] = normalized_Y_test[..., -1] - 1 expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test that with no observations there is no normalization weights = torch.tensor([0.3, 0.7], **tkwargs) objective_transform = get_chebyshev_scalarization( weights=weights, Y=Y_train[:0]) Y_transformed = objective_transform(Y_test) expected_Y_transformed = (weights * Y_test).min( dim=-1).values + 0.05 * (weights * Y_test).sum(dim=-1) self.assertTrue( torch.equal(Y_transformed, expected_Y_transformed)) # test that with one observation, we normalize by subtracting Y_train single_Y_train = Y_train[:1] objective_transform = get_chebyshev_scalarization( weights=weights, Y=single_Y_train) Y_transformed = objective_transform(Y_test) normalized_Y_test = Y_test - single_Y_train expected_Y_transformed = (weights * normalized_Y_test).min( dim=-1).values + 0.05 * (weights * normalized_Y_test).sum(dim=-1) self.assertTrue( torch.allclose(Y_transformed, expected_Y_transformed))