Bayesian optimisation with continuous and discrete parameters#
In this example, NUBO is used for sequential single-point optimisation with continuous and discrete parameters. Additionally to the bounds, a dictionary containing the dimensions as keys and the possible values as values have to be specified. The Hartmann6D
synthetic test function acts as a substitute for a black-box objective function, such as an experiment or a simulation. We use the analytical acquisiton function UpperConfidenceBound
with \(\beta = 1.96^2\) corresponding to the
95% confidence interval of the Gaussian distribution. We optimise this acquisition function with the L-BFGS-B algorithm with 5 starts to avoid getting stuck in a local maximum. The optimisation loop is run for 40 iterations and finds a solution close to the true optimum of -3.3224.
[1]:
import torch
from nubo.acquisition import ExpectedImprovement, UpperConfidenceBound
from nubo.models import GaussianProcess, fit_gp
from nubo.optimisation import single
from nubo.test_functions import Hartmann6D
from nubo.utils import gen_inputs
from gpytorch.likelihoods import GaussianLikelihood
# test function
func = Hartmann6D(minimise=False)
dims = 6
# specify bounds and discrete values
bounds = torch.tensor([[0., 0., 0., 0., 0., 0.], [1., 1., 1., 1., 1., 1.]])
discrete = {0: [0.2, 0.4, 0.6, 0.8], 4: [0.3, 0.6, 0.9]}
# training data
x_train = gen_inputs(num_points=dims*5,
num_dims=dims,
bounds=bounds)
y_train = func(x_train)
# Bayesian optimisation loop
iters = 40
for iter in range(iters):
# specify Gaussian process
likelihood = GaussianLikelihood()
gp = GaussianProcess(x_train, y_train, likelihood=likelihood)
# fit Gaussian process
fit_gp(x_train, y_train, gp=gp, likelihood=likelihood, lr=0.1, steps=200)
# specify acquisition function
# acq = ExpectedImprovement(gp=gp, y_best=torch.max(y_train))
acq = UpperConfidenceBound(gp=gp, beta=1.96**2)
# optimise acquisition function
x_new, _ = single(func=acq, method="L-BFGS-B", bounds=bounds, discrete=discrete, num_starts=5)
# evaluate new point
y_new = func(x_new)
# add to data
x_train = torch.vstack((x_train, x_new))
y_train = torch.hstack((y_train, y_new))
# print new best
if y_new > torch.max(y_train[:-1]):
print(f"New best at evaluation {len(y_train)}: \t Inputs: {x_new.numpy().reshape(dims).round(4)}, \t Outputs: {-y_new.numpy().round(4)}")
# results
best_iter = int(torch.argmax(y_train))
print(f"Evaluation: {best_iter+1} \t Solution: {-float(y_train[best_iter]):.4f}")
New best at evaluation 41: Inputs: [0.4 0.328 1. 0.323 0.3 1. ], Outputs: [-0.984]
New best at evaluation 42: Inputs: [0.2 0.3743 1. 0.3532 0.3 0.9297], Outputs: [-1.165]
New best at evaluation 43: Inputs: [0.2 0.3774 1. 0.3439 0.3 0.8455], Outputs: [-1.2931]
New best at evaluation 44: Inputs: [0.2 0.317 1. 0.3622 0.3 0.8081], Outputs: [-1.3629]
New best at evaluation 46: Inputs: [0.2 0.3075 1. 0.3253 0.3 0.7696], Outputs: [-1.4464]
New best at evaluation 47: Inputs: [0.2 0.274 1. 0.2838 0.3 0.7108], Outputs: [-1.5098]
New best at evaluation 48: Inputs: [0.2 0.2371 0.7806 0.2867 0.3 0.7069], Outputs: [-2.4508]
New best at evaluation 49: Inputs: [0.2 0.1603 0.7432 0.2521 0.3 0.7525], Outputs: [-2.526]
New best at evaluation 50: Inputs: [0.2 0.2488 0.6924 0.2493 0.3 0.766 ], Outputs: [-2.642]
New best at evaluation 51: Inputs: [0.2 0.1938 0.6262 0.2829 0.3 0.7344], Outputs: [-2.9647]
New best at evaluation 52: Inputs: [0.2 0.1336 0.5351 0.283 0.3 0.5795], Outputs: [-3.1219]
New best at evaluation 54: Inputs: [0.2 0.2051 0.4751 0.3168 0.3 0.6334], Outputs: [-3.2154]
New best at evaluation 56: Inputs: [0.2 0.1743 0.4045 0.2843 0.3 0.6907], Outputs: [-3.239]
New best at evaluation 60: Inputs: [0.2 0.1582 0.4587 0.2535 0.3 0.6505], Outputs: [-3.2954]
New best at evaluation 61: Inputs: [0.2 0.1474 0.4688 0.2743 0.3 0.6572], Outputs: [-3.315]
New best at evaluation 62: Inputs: [0.2 0.1487 0.4694 0.2756 0.3 0.6573], Outputs: [-3.3152]
Evaluation: 62 Solution: -3.3152