botorch icon indicating copy to clipboard operation
botorch copied to clipboard

[Bug] batch_initial_conditions must be given if there are non-linear inequality constraints

Open gugeyao opened this issue 2 years ago • 5 comments

🐛 Bug

In the for loop of function optimize_acqf in optimize.py, batch_initial_conditions is set to None when passing arguments, which introduces the error batch_initial_conditions must be given if there are non-linear inequality constraints

gugeyao avatar Aug 21 '22 22:08 gugeyao

Thanks for flagging this! Looks like we ignore batch_initial_conditions whenever using sequential optimization (pointer). And this doesn't play well with non-linear constraints since we require batch_initial_conditions. Two easy fixes here: Either reuse the same batch_initial_conditions or do not support sequential with non-linear constraints. Thoughts @Balandat, @dme65 ?

saitcakmak avatar Aug 22 '22 19:08 saitcakmak

Also, I don't think we should be quietly throwing away batch_initial_conditions in either case. So, reusing batch_initial_conditions or requiring a batch x q x d batch_initial_conditions and chopping it up over the q dimension and feeding it through that way seems like a better solution to me.

saitcakmak avatar Aug 22 '22 19:08 saitcakmak

Related discussion here: https://github.com/pytorch/botorch/issues/1326

tl;dr is that it's really hard to come up with a generic initializer for generic nonlinear constraints, so I don't think we should support that.

From that issue:

I just pushed https://github.com/Balandat/botorch/commit/fedd2e7c2ca4af6d16f0dafab2983835a62225c3 which allows you to use a ic_gen kwarg to optimize_acqf that allows you to implement your own custom initial condition generator. The API of that will need to comply with that of gen_batch_initial_conditions.

So this would allow people to still use custom initializers if they want to implement them themselves. We'll also need to update the logic so as to pass down batch_initial_conditions in the sequential optimization.

Balandat avatar Aug 23 '22 03:08 Balandat

Thanks for your @saitcakmak @Balandat reply!

I am wondering if it works when I use a for loop and my old candidates as batch_initial_conditions to generate a series of candidates instead of using the option sequence = true.

The results I got is that the simulation kept generating the same 10 candidates when I iterated Bayesian optimization. My first question is if I should not generate 10 candidates with a for loop (code below). Keeping generating the same 10 candidates means that the acquisition function ended up choosing the batch_initial_conditions (old candidates) as the candidates. So my second question is if this is caused by the fact I set num_restarts=1 where the gradient found a local minimum.

In the acquisition function, I have

    new_x = []
    for k in range(BATCH_SIZE):
        # train_x = torch.cat([train_x, new_x]), use much older train_x to generate initial seeds.
        X_init = train_x[k+i*BATCH_SIZE:k+1+i*BATCH_SIZE].unsqueeze(0)
        candidates, _ = optimize_acqf(
            acq_function=acq_func,
            bounds=standard_bounds,
            q=1,
            batch_initial_conditions = X_init,
            num_restarts=1, #Number of starting points for multistart acquisition function optimization                                                                  
            raw_samples=RAW_SAMPLES,  # used for initialization heuristic                                                                                                 
            #options={"batch_limit": 1, "maxiter": 200}, # batch_limit: number of restarts                                                                               
            sequential=True,
            # constrain absolute values on the normalized values                                                                                                         
            inequality_constraints=inequality_constraints,
            nonlinear_inequality_constraints=[nonlinear_constraints1, nonlinear_constraints2,nonlinear_constraints3]

        )
        # observe new values                                                                                                                                             
        nx = unnormalize(candidates.detach(), bounds=bounds)
        new_x.append(nx.detach().numpy()[0])
    new_x = np.array(new_x)

gugeyao avatar Aug 23 '22 16:08 gugeyao

The key point in sequential candidate generation is you need to let the acquisition function know about the candidates you already generated. This will make sure that those are taken into account and not generated again, like you observed. You do this by updating the X_pending to include the points you generated. Here's the bit of code that does that.

saitcakmak avatar Aug 23 '22 17:08 saitcakmak