BoTier Tutorial

The following code snippet shows a minimal example of using BoTier’s hierarchical scalarization as a composite objective.

In this example, our primary goal is to maximize the \(\sin(2 \pi x)\) function to a value of min. 0.5. If this is satisfied, the value of x should be minimized.

import torch
from gpytorch.mlls import ExactMarginalLogLikelihood
from botorch.models import SingleTaskGP
from botorch.fit import fit_gpytorch_mll
from botorch.acquisition.monte_carlo import qExpectedImprovement
from botorch.optim import optimize_acqf

import numpy as np
from matplotlib import pyplot as plt

from botier import AuxiliaryObjective, HierarchyScalarizationObjective

We first define the ‘auxiliary objectives’ that eventually make up the overall optimization objective:

objectives = [
    AuxiliaryObjective(output_index=0, abs_threshold=0.5, upper_bound=1.0, lower_bound=-1.0),
    AuxiliaryObjective(maximize=False, calculation=lambda y, x: x[..., 0], abs_threshold=0.0, lower_bound=0.0, upper_bound=1.0),
]
global_objective = HierarchyScalarizationObjective(objectives, k=1E2, normalized_objectives=True)

The first objective is the first model output (index 0) with an absolute threshold of 0.5. The best value is 1.0, and the worst value is -1.0. The second objective is a known value (known=True) that is calculated as the first input parameter (x) with an absolute threshold of 0.0. The best obtainable value is 0.0, and the worst value is 1.0. The second objective is only dependent on the input parameters (known = True), and is calculated from the input parameter x using the function passed as the calculation argument. For a detailed explanation of the AuxiliaryObjective class, refer to the API documentation.

That is it! Now we can generate some training data

train_x = torch.rand(5, 1).double()
train_y = torch.sin(2 * torch.pi * train_x)

And finally, we can run our optimization campaign using BoTorch

budget = 20
for n in range(budget):

    # fit a simple BoTorch surrogate model
    surrogate = SingleTaskGP(train_x, train_y)
    mll = ExactMarginalLogLikelihood(surrogate.likelihood, surrogate)
    fit_gpytorch_mll(mll)

    # instantiate a BoTorch Monte-Carlo acquisition function using the botier.HierarchyScalarizationObjective as the 'objective' argument
    acqf = qExpectedImprovement(
        model=surrogate,
        objective=global_objective,
        best_f=torch.max(train_y)
    )

    new_candidate, _ = optimize_acqf(acqf, bounds=torch.tensor([[0.0], [1.0]]), q=1, num_restarts=5, raw_samples=512)


    # evaluate the global objective
    new_candidate_y = torch.sin(2 * torch.pi * new_candidate)

    # update the training points
    train_x = torch.cat([train_x, new_candidate])
    train_y = torch.cat([train_y, new_candidate_y])

    print(f"iteration {n + 1}: candidate={new_candidate.item()}, objective={new_candidate_y.item()}")

We can now visaualize the optimization process

plt.plot(np.linspace(0, 1, 100), torch.sin(2 * torch.pi * torch.linspace(0, 1, 100)), label="true function", zorder=0)
plt.scatter(train_x.numpy(), train_y.numpy(), s=25, marker="x", cmap="spring", c=np.arange(len(train_x)), label="selected points")
plt.colorbar()
plt.legend()
plt.show()