-
Notifications
You must be signed in to change notification settings - Fork 404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request] Cardinality constraint #1749
Comments
cc @dme65 , @bletham , @qingfeng10. Is the 5 at a time a hard constraint or some ideal number that you’d like to stick to? I wonder if some of the modeling+optimization approaches from https://arxiv.org/pdf/2203.01900.pdf could be useful. |
The custom sampler here is just something to generate the initial conditions for the optimizer to use for multi-start optimization. A lazy implementation could call |
Hi, I call this kind of constraint an Currently I am implementing this in our own botorch wrapper focusing on materials discovery. Hopefully it is finished next week. In this method the NchooseK constraints are transformed into the botorch definition of a nonlinear inequality: You can take this defintions and just give it to Then you just need to setup initial conditions adhering to the constraints. This can be done by our Polytopesampler based on the polytopesampler of botorch which is also able to handle NChooseK constraints. Here is an example in which the NChooseK constraint is combined with an additional linear equality (mixture constraint) from bofire.data_models.constraints.api import (
LinearInequalityConstraint,
LinearEqualityConstraint,
NChooseKConstraint,
)
from bofire.data_models.domain.api import Domain
from bofire.data_models.features.api import CategoricalInput, ContinuousInput
import bofire.strategies.api as strategies
import bofire.data_models.strategies.api as data_models
from bofire.utils.torch_tools import get_nchoosek_constraints
features = [ContinuousInput(key=f"if{i+1}", lower_bound=0, upper_bound=1) for i in range(10)]
constraints = [
LinearEqualityConstraint(features=[f"if{i+1}" for i in range(10)], coefficients=[1.0]*10, rhs=1),
NChooseKConstraint(features=[f"if{i+1}" for i in range(10)], min_count=0, max_count=5, none_also_valid=True)
]
domain = Domain(
input_features=features,
constraints=constraints,
)
data_model = data_models.PolytopeSampler(domain=domain)
sampler = strategies.PolytopeSampler(data_model=data_model)
# this generates the samples, it is currently a bit slow as we generate the complete combinatorics, this can be easily speedup
samples = sampler.ask(5, return_all=False)
# this gives you callables for nonlinear inequality constraint to handle it in optimize_acqf
callables = get_nchoosek_constraints(domain=domain) This are the generated samples, fulfilling both constraints. In the next weeks this should be all automatically get integrated. Note that bofire is still in a kind of alpha stage ;) |
@jduerholt thanks for sharing BoFire! Excited to see where this goes. For the discussions you mentioned: |
Thanks for the feedback everyone! Through a referenced issue, I came across this comment which has a much better solution to our problem than what I originally came up with. We based the following code on that solution, which works like a charm: from botorch.acquisition import AcquisitionFunction
from botorch.optim import optimize_acqf
import torch
from torch import Tensor
from torch.quasirandom import SobolEngine
def narrow_gaussian(x: Tensor, epsilon: float):
return torch.exp(-0.5 * (x / epsilon) ** 2)
def build_cardinality_constraint(cardinality: int, epsilon: float = 1e-2):
"""
Builds a constraint function that checks whether the vector has a cardinality
no higher than specified.
If the constraint is met, the result will be >= 0.
"""
def cardinality_constraint(x: Tensor):
"""
Checks whether the vector has a cardinality no higher than specified.
If the constraint is met, the result will be >= 0.
"""
return narrow_gaussian(x, epsilon).sum(dim=-1) - x.shape[-1] + cardinality
return cardinality_constraint
def generate_cardinality_limited_points(
n: int,
x_dim: int,
cardinality: int,
q: int = 1,
):
"""
Generate initial points that are of the specified cardinality.
"""
X = SobolEngine(dimension=x_dim, scramble=True).draw(n * q).to(torch.double)
x_idx = torch.arange(X.shape[0]).unsqueeze(-1)
y_idx = torch.argsort(torch.rand(n * q, x_dim), dim=-1)[..., : x_dim - cardinality]
X[x_idx, y_idx] = 0
X = X.reshape(n, q, x_dim)
return X
def get_cardinality_limited_batch_initial_conditions(
num_restarts: int,
raw_samples: int,
acqf: AcquisitionFunction,
x_dim: int,
cardinality: int,
q: int = 1,
):
"""
Get initial conditions that are of the specified cardinality.
"""
X = generate_cardinality_limited_points(
raw_samples, x_dim=x_dim, cardinality=cardinality, q=q
)
return X[acqf(X).topk(num_restarts).indices]
cardinality_constraint = build_cardinality_constraint(cardinality)
batch_initial_conditions = get_cardinality_limited_batch_initial_conditions(...)
candidate, _ = optimize_acqf(
nonlinear_inequality_constraints=[cardinality_constraint],
batch_initial_conditions=batch_initial_conditions,
...
) We did run into the next problem, which is that nonlinear constraints have not been implemented in botorch for q>1 (or for knowledge gradient). These constraints are handled through the |
Glad to hear you were able to make some progress on this.
Doing this should hopefully not be too hard if the constraints are intra-point constraints that apply separately to each of the
For KG it's going to be a bit trickier, since the locations of the fantasy observations also need to satisfy those constraints. That won't really be an overly challenging problem to hook up, but my concern is that you'll end up with a huge number of nonlinear constraints in the acquisition function optimization problem that way. By default that optimization will use SLSQP which is exceedingly slow with many constraints. Do you need to use KG here? |
Yes, the points in a batch are independent. So far I've given it one attempt, but got stuck on the shape of
We can use other acquisition functions, though KG seems to perform well on our specific challenge. As for it being exceedingly slow, for our challenge it's not really a problem if the computation of a single batch takes a few hours, so that might be something we can test. |
@swierh: are you already on it making the non-linear constraints also possible for q>1? Else, I can also give it a try as we also need it. I tested so far only with q=1. A quick fix could also be to optimize with @Balandat: concerning speed, for larger q-batches and higher dimensional problems with a lot of constraints, one could also try how |
Yeah that's also possible. I've used IPOPT quite a bit in grad school, at the time it wasn't exactly easy to build and run on different platforms with a python interface - but I assume this has improved since? If you gave this a try I'd love to hear about how it goes. |
We are using it to generate D-optimal designs using |
@jduerholt I'm not currently working on it, and won't have much time for at least the coming week, so feel free to give it a try. |
@jduerholt Awesome! Thanks so much for the effort you and @Balandat put into it! Resolved by #1793 |
🚀 Feature Request
We've got a problem where we have a constraint on the number of non-zero inputs. Out of 10 possible features, we can only select 5 at a time for any sample in the q-batch. This is because in a certain sample we can only use a maximum of 5 different ingredients which we have to pick from 10 possible ingredients.
We tried implementing this by using a penalized acquisition function by adding a loss on the cardinality. This works, but a constraint seems like a better place for this.
Also, when we add a normal inequality constraint (e.g. x1 + x2 > 0), the optimizer doesn't seem to find solutions with low cardinality.
We've experimented with adding the cardinality constraint as a non-linear constraint, but it appears that have to write a custom sampler to get this working.
Is there a cleaner way of doing this, what would you suggest as the right approach for this?
All help will be much appreciated!
The code we've implemented for the penalty is as follows:
And we then wrap our acquisition function as follows:
The text was updated successfully, but these errors were encountered: