Exemple #1
0
def infer_objective_thresholds(
    model: Model,
    objective_weights: Tensor,  # objective_directions
    bounds: Optional[List[Tuple[float, float]]] = None,
    outcome_constraints: Optional[Tuple[Tensor, Tensor]] = None,
    linear_constraints: Optional[Tuple[Tensor, Tensor]] = None,
    fixed_features: Optional[Dict[int, float]] = None,
    subset_idcs: Optional[Tensor] = None,
    Xs: Optional[List[Tensor]] = None,
    X_observed: Optional[Tensor] = None,
) -> Tensor:
    """Infer objective thresholds.

    This method uses the model-estimated Pareto frontier over the in-sample points
    to infer absolute (not relativized) objective thresholds.

    This uses a heuristic that sets the objective threshold to be a scaled nadir
    point, where the nadir point is scaled back based on the range of each
    objective across the current in-sample Pareto frontier.

    See `botorch.utils.multi_objective.hypervolume.infer_reference_point` for
    details on the heuristic.

    Args:
        model: A fitted botorch Model.
        objective_weights: The objective is to maximize a weighted sum of
            the columns of f(x). These are the weights. These should not
            be subsetted.
        bounds: A list of (lower, upper) tuples for each column of X.
        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. These should not be subsetted.
        linear_constraints: A tuple of (A, b). For k linear constraints on
            d-dimensional x, A is (k x d) and b is (k x 1) such that
            A x <= b.
        fixed_features: A map {feature_index: value} for features that
            should be fixed to a particular value during generation.
        subset_idcs: The indices of the outcomes that are modeled by the
            provided model. If subset_idcs not None, this method infers
            whether the model is subsetted.
        Xs: A list of m (k_i x d) feature tensors X. Number of rows k_i can
            vary from i=1,...,m.
        X_observed: A `n x d`-dim tensor of in-sample points to use for
            determining the current in-sample Pareto frontier.

    Returns:
        A `m`-dim tensor of objective thresholds, where the objective
            threshold is `nan` if the outcome is not an objective.
    """
    if X_observed is None:
        if bounds is None:
            raise ValueError("bounds is required if X_observed is None.")
        elif Xs is None:
            raise ValueError("Xs is required if X_observed is None.")
        _, X_observed = _get_X_pending_and_observed(
            Xs=Xs,
            objective_weights=objective_weights,
            outcome_constraints=outcome_constraints,
            bounds=bounds,
            linear_constraints=linear_constraints,
            fixed_features=fixed_features,
        )
    num_outcomes = objective_weights.shape[0]
    if subset_idcs is None:
        # check if only a subset of outcomes are modeled
        nonzero = objective_weights != 0
        if outcome_constraints is not None:
            A, _ = outcome_constraints
            nonzero = nonzero | torch.any(A != 0, dim=0)
        expected_subset_idcs = nonzero.nonzero().view(-1)
        if model.num_outputs > expected_subset_idcs.numel():
            # subset the model so that we only compute the posterior
            # over the relevant outcomes
            subset_model_results = subset_model(
                model=model,
                objective_weights=objective_weights,
                outcome_constraints=outcome_constraints,
            )
            model = subset_model_results.model
            objective_weights = subset_model_results.objective_weights
            outcome_constraints = subset_model_results.outcome_constraints
            subset_idcs = subset_model_results.indices
        else:
            # model is already subsetted.
            subset_idcs = expected_subset_idcs
            # subset objective weights and outcome constraints
            objective_weights = objective_weights[subset_idcs]
            if outcome_constraints is not None:
                outcome_constraints = (
                    outcome_constraints[0][:, subset_idcs],
                    outcome_constraints[1],
                )
    else:
        objective_weights = objective_weights[subset_idcs]
        if outcome_constraints is not None:
            outcome_constraints = (
                outcome_constraints[0][:, subset_idcs],
                outcome_constraints[1],
            )
    with torch.no_grad():
        pred = not_none(model).posterior(not_none(X_observed)).mean
    if outcome_constraints is not None:
        cons_tfs = get_outcome_constraint_transforms(outcome_constraints)
        # pyre-ignore [16]
        feas = torch.stack([c(pred) <= 0 for c in cons_tfs],
                           dim=-1).all(dim=-1)
        pred = pred[feas]
    if pred.shape[0] == 0:
        raise AxError("There are no feasible observed points.")
    obj_mask = objective_weights.nonzero().view(-1)
    obj_weights_subset = objective_weights[obj_mask]
    obj = pred[..., obj_mask] * obj_weights_subset
    pareto_obj = obj[is_non_dominated(obj)]
    objective_thresholds = infer_reference_point(
        pareto_Y=pareto_obj,
        scale=0.1,
    )
    # multiply by objective weights to return objective thresholds in the
    # unweighted space
    objective_thresholds = objective_thresholds * obj_weights_subset
    full_objective_thresholds = torch.full(
        (num_outcomes, ),
        float("nan"),
        dtype=objective_weights.dtype,
        device=objective_weights.device,
    )
    obj_idcs = subset_idcs[obj_mask]
    full_objective_thresholds[obj_idcs] = objective_thresholds.clone()
    return full_objective_thresholds
Exemple #2
0
    def test_infer_reference_point(self):
        tkwargs = {"device": self.device}
        for dtype in (torch.float, torch.double):
            tkwargs["dtype"] = dtype
            Y = torch.tensor([
                [-13.9599, -24.0326],
                [-19.6755, -11.4721],
                [-18.7742, -11.9193],
                [-16.6614, -12.3283],
                [-17.7663, -11.9941],
                [-17.4367, -12.2948],
                [-19.4244, -11.9158],
                [-14.0806, -22.0004],
            ], **tkwargs)

            # test empty pareto_Y and no max_ref_point
            with self.assertRaises(BotorchError):
                infer_reference_point(pareto_Y=Y[:0])

            # test max_ref_point does not change when there exists a better Y point
            max_ref_point = Y.min(dim=0).values
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y)
            self.assertTrue(torch.equal(max_ref_point, ref_point))
            # test scale_max_ref_point
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y,
                                              scale_max_ref_point=True)
            better_than_ref = (Y > max_ref_point).all(dim=-1)
            Y_better_than_ref = Y[better_than_ref]
            ideal_better_than_ref = Y_better_than_ref.max(dim=0).values
            self.assertTrue(
                torch.equal(
                    max_ref_point - 0.1 *
                    (ideal_better_than_ref - max_ref_point),
                    ref_point,
                ))
            # test case when there does not exist a better Y point
            max_ref_point = torch.tensor([-2.2, -2.3], **tkwargs)
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y)
            self.assertTrue((ref_point < Y).all(dim=-1).any())
            nadir = Y.min(dim=0).values
            ideal = Y.max(dim=0).values
            expected_ref_point = nadir - 0.1 * (ideal - nadir)
            self.assertTrue(torch.allclose(ref_point, expected_ref_point))
            # test with scale
            expected_ref_point = nadir - 0.2 * (ideal - nadir)
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y,
                                              scale=0.2)
            self.assertTrue(torch.allclose(ref_point, expected_ref_point))

            # test case when one objective is better than max_ref_point, and
            # one objective is worse
            max_ref_point = torch.tensor([-2.2, -12.1], **tkwargs)
            expected_ref_point = nadir - 0.1 * (ideal - nadir)
            expected_ref_point = torch.min(expected_ref_point, max_ref_point)
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y)
            self.assertTrue(torch.equal(expected_ref_point, ref_point))
            # test case when one objective is better than max_ref_point, and
            # one objective is worse with scale_max_ref_point
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y,
                                              scale_max_ref_point=True)
            nadir2 = torch.min(nadir, max_ref_point)
            expected_ref_point = nadir2 - 0.1 * (ideal - nadir2)
            self.assertTrue(torch.equal(expected_ref_point, ref_point))

            # test case when size of pareto_Y is 0
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y[:0])
            self.assertTrue(torch.equal(max_ref_point, ref_point))
            # test case when size of pareto_Y is 0 with scale_max_ref_point
            ref_point = infer_reference_point(
                max_ref_point=max_ref_point,
                pareto_Y=Y[:0],
                scale_max_ref_point=True,
                scale=0.2,
            )
            self.assertTrue(
                torch.equal(max_ref_point - 0.2 * max_ref_point.abs(),
                            ref_point))
            # test case when size of pareto_Y is 1
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y[:1])
            expected_ref_point = Y[0] - 0.1 * Y[0].abs()
            self.assertTrue(torch.equal(expected_ref_point, ref_point))
            # test case when size of pareto_Y is 1 with scale parameter
            ref_point = infer_reference_point(max_ref_point=max_ref_point,
                                              pareto_Y=Y[:1],
                                              scale=0.2)
            expected_ref_point = Y[0] - 0.2 * Y[0].abs()
            self.assertTrue(torch.equal(expected_ref_point, ref_point))

            # test no max_ref_point specified
            expected_ref_point = nadir - 0.2 * (ideal - nadir)
            ref_point = infer_reference_point(pareto_Y=Y, scale=0.2)
            self.assertTrue(torch.allclose(ref_point, expected_ref_point))
            ref_point = infer_reference_point(pareto_Y=Y)