-
Notifications
You must be signed in to change notification settings - Fork 445
Description
What happened?
I am performing an optimization with qLogNEI and an outcome constraint. When generating candidates with no feasible values in the training data, the acqf is zero almost everywhere, leading to candidates that ignore the constraint. This leads to degenerate behaviour, where the optimizer will keep proposing the same (infeasible) point throughout the BO loop.
I think that the acqf is zero everywhere because the compute_best_feasible_objective
does not give a good lower bound for infeasible values. I have found some issues with the current compute_best_feasible_objective
, each of which contributes some part to this problem. Sorry for the very long ticket!!
1 Normalization. The convex weights are normalized in the wrong dimension below - the weights are (32, N)
and should be normalized along N
so that the weights assigned to each point sum to 1. The current implementation essentially scales down all points to search near the origin. Solution is to just fix the dimension here.
https://github.com/pytorch/botorch/blob/1518b304f47f5cdbaf9c175e808c90b3a0a6b86d/botorch/acquisition/utils.py#L192
2 Convex search excludes boundaries. By using convex weights, we don't explore the edges of the space which is where points satisfying the threshold are likely to lie. I don't think there is a good solution to this, since we don't have access to bounds
when the class is initialized.
3 Random convex weights favour middle. By using random convex weights, we tend to explore very close to the middle. I think this is due to the Central Limit Theorem, but I haven't thought about this too deeply. Try simulating:
w = torch.rand(32, N)
(w / w.sum(dim=1, keepdim=True)) @ torch.linspace(0, 1, N)
As N
increases, the above samples tend closer and closer to 0.5
, so we don't actually get to explore the space very much. A solution would be to pass torch.cat((X, convex_weights @ X))
to get_infeasible_cost
, to at least ensure that we evaluate the minima at the points on the convex hull.
https://github.com/pytorch/botorch/blob/1518b304f47f5cdbaf9c175e808c90b3a0a6b86d/botorch/acquisition/utils.py#L198
4 Prune baseline makes this worse When we pass prune_baseline=True
(the default), we only keep the infeasible point that maximizes the objective. However, we should also keep the point that is closest to being feasible, as we otherwise only evaluate the lower bound at an area of the search space that is very far away from being feasible. Solution is to either change this default, or be less aggressive in pruning the baseline.
5 Account for objective direction This is a bit of an aside, but also an issue here. The code below assumes that the objective
is increasing in Y
, and that evaluating objective(mean - 6*std)
will give a lower bound on the objective. However if the objective is decreasing, then the lower bound should be given by objective(mean + 6*std)
. A simple solution is to compute both -
and +
, then just take the minimum of each of those.
https://github.com/pytorch/botorch/blob/1518b304f47f5cdbaf9c175e808c90b3a0a6b86d/botorch/acquisition/utils.py#L238
Please provide a minimal, reproducible example of the unexpected behavior.
We train a model on very simple linear problem of maximizing Y_1 = -X
, with the constraint that Y_2 = X > 0.81
where the greatest value in the training data is 0.8. We observe that the recommended point is at X=0.
, which is very far from satisfying the constraint. We would expect the recommended point to satisfy the constraint (since we have enough data to model the constraint function well).
Features of this problem that lead to the buggy behaviour:
- The acqf optimum is at X=0, which is the area searched due to problem (1) above
- The constraint and the objective are competing (the problem would happen if the constraint were
Y_2<0.19
)
import torch
from botorch.models import SingleTaskGP
from botorch.acquisition import qLogNoisyExpectedImprovement
from botorch.acquisition.objective import GenericMCObjective
from botorch.optim import optimize_acqf
from gpytorch.mlls import ExactMarginalLogLikelihood
from botorch import fit_gpytorch_mll
torch.set_default_dtype(torch.float64)
bounds = torch.tensor([[0.], [1.]])
train_X = torch.linspace(0.2, 0.8, steps=20).unsqueeze(-1)
train_Y = torch.cat((-train_X, train_X), dim=-1)
def constraint(Y: torch.Tensor):
# Y1 > 0.81
return -(Y[..., 1] - 0.81)
objective = GenericMCObjective(objective=lambda samples, X: samples[..., 0])
model = SingleTaskGP(train_X, train_Y)
mll = ExactMarginalLogLikelihood(model.likelihood, model)
fit_gpytorch_mll(mll)
acq_function = qLogNoisyExpectedImprovement(
model=model,
X_baseline=train_X,
constraints=[constraint],
objective=objective,
prune_baseline=False, # issue exists for True/False
)
candidates, acq_value = optimize_acqf(
acq_function,
bounds,
q=1,
num_restarts=20,
raw_samples=512,
)
print(candidates) # tensor([[0.]]), we expect to be > 0.81
print(acq_function(torch.tensor([[0.90]]))) # tensor([-42.0]), very small!
Please paste any relevant traceback/logs produced by the example provided.
BoTorch Version
0.14.1.dev103+g1518b304f (almost latest on GitHub)
Python Version
3.12.9
Operating System
WSL 2
(Optional) Describe any potential fixes you've considered to the issue outlined above.
- Fix the
dim=0
bug listed in (1) above. - Add a constant value to the unconstrained acqf/increase the minimum value. This doesn't change anything when no constraints are present, but biases the optimizer to favour satisfying the constraints when the acqf is zero everywhere. This seems to work okay, but not entirely sure of the downstream effects of doing so.
- Expose the
infeasible_obj
argument incompute_best_feasible_objective
to the qLogNEI class so that the user can manually provide a value.
Pull Request
Yes
Code of Conduct
- I agree to follow BoTorch's Code of Conduct