Input Features as Output Objectives

This notebook demonstrates how to put objectives on input features or a combination of input features. Possible usecases are favoring lower or higher amounts of an ingredient or to take into account a known (linear) cost function. In case of categorical inputs it can be used to penalize the optimizer for choosing specific categories.

Imports

import numpy as np

import bofire.strategies.api as strategies
import bofire.surrogates.api as surrogates
from bofire.benchmarks.api import Himmelblau
from bofire.data_models.features.api import CategoricalInput, ContinuousOutput
from bofire.data_models.objectives.api import (
    MaximizeObjective,
    MaximizeSigmoidObjective,
)
from bofire.data_models.strategies.api import MultiplicativeSoboStrategy
from bofire.data_models.surrogates.api import (
    BotorchSurrogates,
    CategoricalDeterministicSurrogate,
    LinearDeterministicSurrogate,
)

Setup an Example

We use Himmelblau as example with an additional objective on x_2 which pushes it to be larger 3 during the optimization. In addition, we introduce a categorical feature called x_cat which is mapped by an CategoricalDeterministicSurrogate to a continuous output called y_cat.

bench = Himmelblau()
experiments = bench.f(bench.domain.inputs.sample(10), return_complete=True)

domain = bench.domain

# setup extra feature `y_x2` that is the same as `x_2` and is taken into account in the optimization by a sigmoid objective
domain.outputs.features.append(
    ContinuousOutput(key="y_x2", objective=MaximizeSigmoidObjective(tp=3, steepness=10))
)
experiments["y_x2"] = experiments.x_2


# add extra categorical input feature and corresponding output feature
domain.inputs.features.append(CategoricalInput(key="x_cat", categories=["a", "b", "c"]))
domain.outputs.features.append(
    ContinuousOutput(key="y_cat", objective=MaximizeObjective())
)

# generate random values for the new categorical feature
experiments["x_cat"] = np.random.choice(["a", "b", "c"], size=experiments.shape[0])

The LinearDeterministicSurrogate can be used to model that y_x2 = x_2.

surrogate_data = LinearDeterministicSurrogate(
    inputs=domain.inputs.get_by_keys(["x_2"]),
    outputs=domain.outputs.get_by_keys(["y_x2"]),
    coefficients={"x_2": 1},
    intercept=0,
)
surrogate = surrogates.map(surrogate_data)
surrogate.predict(experiments[domain.inputs.get_keys()].copy())
y_x2_pred y_x2_sd
0 1.997736 0.0
1 -2.719967 0.0
2 2.290222 0.0
3 2.292196 0.0
4 -3.815097 0.0
5 0.449665 0.0
6 2.882852 0.0
7 -0.247336 0.0
8 3.497335 0.0
9 -2.830277 0.0

The CategoricalDeterministicSurrogate can be used to map categories to specific continuous values.

categorical_surrogate_data = CategoricalDeterministicSurrogate(
    inputs=domain.inputs.get_by_keys(["x_cat"]),
    outputs=domain.outputs.get_by_keys(["y_cat"]),
    mapping={"a": 1, "b": 0.2, "c": 0.3},
)

surrogate = surrogates.map(categorical_surrogate_data)

surrogate.predict(experiments[domain.inputs.get_keys()].copy())

experiments["y_cat"] = surrogate.predict(experiments[domain.inputs.get_keys()].copy())[
    "y_cat_pred"
]

experiments
x_1 x_2 y valid_y y_x2 x_cat y_cat
0 -5.522035 1.997736 534.625622 1 1.997736 a 1.0
1 3.467733 -2.719967 17.817910 1 -2.719967 a 1.0
2 -0.206985 2.290222 78.964711 1 2.290222 b 0.2
3 1.382206 2.292196 46.335673 1 2.292196 c 0.3
4 -0.664918 -3.815097 254.055367 1 -3.815097 c 0.3
5 4.832463 0.449665 167.763130 1 0.449665 a 1.0
6 4.749012 2.882852 245.118911 1 2.882852 c 0.3
7 3.140534 -0.247336 16.343525 1 -0.247336 a 1.0
8 -3.616145 3.497335 33.676573 1 3.497335 a 1.0
9 2.746860 -2.830277 53.619167 1 -2.830277 c 0.3

Next we setup a SoboStrategy using the custom surrogates for outputs y_x2 and y_cat and ask for a candidate. Note that the surrogate specs for output y is automatically generated and defaulted to be a SingleTaskGPSurrogate.

strategy_data = MultiplicativeSoboStrategy(
    domain=domain,
    surrogate_specs=BotorchSurrogates(
        surrogates=[surrogate_data, categorical_surrogate_data]
    ),
)
strategy = strategies.map(strategy_data)
strategy.tell(experiments)
strategy.ask(4)
x_1 x_2 x_cat y_pred y_cat_pred y_x2_pred y_sd y_cat_sd y_x2_sd y_des y_x2_des y_cat_des
0 -2.432430 5.999801 a -84.299131 1.0 5.999801 98.300606 0.0 0.0 84.299131 1.000000 1.0
1 -2.471754 3.449480 a -71.909130 1.0 3.449480 70.431686 0.0 0.0 71.909130 0.988956 1.0
2 -3.065456 6.000000 a -59.528692 1.0 6.000000 79.227715 0.0 0.0 59.528692 1.000000 1.0
3 1.499031 6.000000 a 39.695362 1.0 6.000000 84.050011 0.0 0.0 -39.695362 1.000000 1.0