import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib import cm
import bofire.strategies.api as strategies
from bofire.benchmarks.api import GenericBenchmark
from bofire.benchmarks.single import Himmelblau
from bofire.data_models.api import Domain, Inputs, Outputs
from bofire.data_models.features.api import ContinuousInput, ContinuousOutput
from bofire.data_models.objectives.api import MinimizeObjective
from bofire.data_models.strategies.api import RandomStrategy
from bofire.runners.api import run
SMOKE_TEST = os.environ.get("SMOKE_TEST")Active Learning
Showcase of active learning in bofire. Active learning per definition focusses on fitting the model to the experimental observations best possible in an iterative manner reducing some kind of uncertainty. The ActiveLearningStrategy proposes a set of evaluation points that will gain the most information about the problem each iteration. Thus, an unknown black-box-function can be approximated without optimization. It represents an exploration-only strategy.
1-D Objective Function
For a 1-D objective function. The Himmelblau benchmark is used. \[\begin{equation} f: \mathbb{R}^2 \rightarrow \mathbb{R} \quad | \quad f(x_1, x_2) = (x_1^2 + x_2 - 11)^2 + (x_1 + x_2^2) ^2 \end{equation}\] To start the active learning strategy we need to supply some initial data points to set up the Gaussian Regression Model in the background.
himmelblau = Himmelblau()
def sample(domain: Domain):
datamodel = RandomStrategy(domain=domain)
sampler = strategies.map(data_model=datamodel)
sampled = sampler.ask(10)
return sampled
initial_points = sample(domain=himmelblau.domain)
initial_experiments = pd.concat([initial_points, himmelblau.f(initial_points)], axis=1)
display(initial_experiments)| x_1 | x_2 | y | valid_y | |
|---|---|---|---|---|
| 0 | 5.401121 | 2.675580 | 465.537862 | 1 |
| 1 | 0.576342 | 1.897418 | 84.892051 | 1 |
| 2 | -5.453175 | -1.796953 | 372.054067 | 1 |
| 3 | 1.778934 | 5.777340 | 797.029422 | 1 |
| 4 | -4.981100 | 4.589033 | 420.986575 | 1 |
| 5 | -4.193914 | -5.356077 | 307.547464 | 1 |
| 6 | -1.943967 | 5.154563 | 314.930112 | 1 |
| 7 | -5.617293 | 0.570447 | 597.331664 | 1 |
| 8 | 0.622289 | -0.861274 | 163.416950 | 1 |
| 9 | -4.774761 | 4.802698 | 403.084476 | 1 |
ActiveLearningStrategy
The ActiveLearningstrategy can be set up just as other BO strategies implemented in bofire. Just take a look into the other tutorials. Basic calls are ask() to retrieve new evaluation candidates from the acquisition function and tell() to train the model with a new observation.
Currently, the default active learning acquisition function implemented is qNegIntegratedPosteriorVariance. It focuses on minimizing the overall posterior variance by choosing a new candidate.
The ActiveLearningStrategy uses Monte-Carlo-integration to evaluate the acquisition function. The number of integration nodes significantly influences the speed of the integration. These can be adjusted by changing the parameter data_model.num_sobol_samples. Note that a sample size representing a power of \(2\) increases performance.
# Manual set up of ActiveLearning
from bofire.data_models.acquisition_functions.api import qNegIntPosVar
from bofire.data_models.strategies.api import ActiveLearningStrategy
from bofire.data_models.surrogates.api import BotorchSurrogates, SingleTaskGPSurrogate
af = qNegIntPosVar(
n_mc_samples=64, # lower the number of monte carlo samples to improve speed
)
data_model = ActiveLearningStrategy(domain=himmelblau.domain, acquisition_function=af)
recommender = strategies.map(data_model=data_model)
recommender.tell(experiments=initial_experiments)
candidates = recommender.ask(candidate_count=1)display(candidates)| x_1 | x_2 | y_pred | y_sd | y_des | |
|---|---|---|---|---|---|
| 0 | 2.185312 | -3.779142 | 370.752805 | 191.830065 | -370.752805 |
# Running the active learning strategy
n_iter = 1 if SMOKE_TEST else 20
results = initial_experiments
for _ in range(n_iter):
# run active learning strategy
X = recommender.ask(candidate_count=1)[himmelblau.domain.inputs.get_keys()]
Y = himmelblau.f(X)
XY = pd.concat([X, Y], axis=1)
recommender.tell(experiments=XY) # pass new experimental data
results = pd.concat([results, XY], axis=0, ignore_index=True)# Running a random strategy for comparison
def strategy_factory(domain: Domain):
data_model = RandomStrategy(domain=domain)
return strategies.map(data_model)
random_results = run(
himmelblau,
strategy_factory=strategy_factory,
n_iterations=n_iter,
metric=lambda domain, experiments: 1.0,
initial_sampler=sample,
n_runs=1,
n_procs=1,
)Plotting
if not SMOKE_TEST:
plt.rcParams["figure.figsize"] = (10, 4)
fig, ax = plt.subplots(1, 2)
# contour plot of himmelblau
def f(grid):
return (grid[0] ** 2 + grid[1] - 11) ** 2 + (grid[0] + grid[1] ** 2) ** 2
X_grid = np.arange(-7, 7, 0.01)
Y_grid = np.arange(-7, 7, 0.01)
mesh = np.meshgrid(X_grid, Y_grid)
Z = f(grid=mesh)
levels = np.linspace(Z.min(), Z.max(), 6)
ax[0].contourf(X_grid, Y_grid, Z, cmap=cm.viridis)
ax[0].scatter(random_results[0][0].x_1, random_results[0][0].x_2, c="white")
ax[1].contourf(X_grid, Y_grid, Z, cmap=cm.viridis)
ax[1].scatter(results.x_1, results.x_2, c="white")
ax[0].axis([-7, 7, -7, 7])
ax[0].set_xlabel("$x_1$")
ax[1].set_xlabel("$x_1$")
ax[0].set_ylabel("$x_2$")
ax[0].set_title("random strategy")
ax[1].set_title("active learning strategy")
fig.show()The plot shows the exploratory behavior of the ActiveLearningStrategy.
2-D (n-D) Objective Function
Now, we want to actively learn an objective function with a multi-dimensional output space. This is shown by an example function with \(2\) output variables. For this, we again utilize the Himmelblau benchmark function and the Ackley function.
\[\begin{equation} f: \mathbb{R}^2 \rightarrow \mathbb{R}^2 \quad | \quad f(x_1, x_2) = \begin{pmatrix} (x_1^2 + x_2 - 11)^2 + (x_1 + x_2^2) ^2 \\ -20\exp \left[-0.2{\sqrt {0.5\left(x_1^{2}+x_2^{2}\right)}}\right] -\exp \left[0.5\left(\cos 2\pi x_1+\cos 2\pi x_2\right)\right]+e+20 \end{pmatrix} \end{equation}\]
inputs = Inputs(
features=[
ContinuousInput(key="x_1", bounds=(-6, 6)),
ContinuousInput(key="x_2", bounds=(-6, 6)),
],
)
outputs = Outputs(
features=[
ContinuousOutput(key="f_0", objective=MinimizeObjective()),
ContinuousOutput(key="f_1", objective=MinimizeObjective()),
],
)
domain = Domain(inputs=inputs, outputs=outputs)
def benchmark_function(candidates):
f0 = (candidates["x_1"] ** 2 + candidates["x_2"] - 11) ** 2 + (
candidates["x_1"] + candidates["x_2"] ** 2
) ** 2
f1 = -20 * np.exp(
-0.2 * np.sqrt(0.5 * (candidates["x_1"] ** 2 + candidates["x_2"] ** 2)),
) + (
-np.exp(
0.5
* (
np.cos(2 * np.pi * candidates["x_1"])
+ np.cos(2 * np.pi * candidates["x_2"])
),
)
+ np.exp(1)
+ 20
)
return pd.DataFrame({"f_0": f0, "f_1": f1})
function = GenericBenchmark(domain=domain, func=benchmark_function)
initial_experiments = pd.concat(
[initial_points, function.f(candidates=initial_points)],
axis=1,
)For the multi-objective function we need to pass two models to the strategy as each individual output is represented by a separate model. By default, the ActiveLearningStrategy focusses on minimizing the negative integrated posterior variance of each model equally. To minimize the variances in a more specific way certain weights can be provided for each output feature. This can be done by passing a dictionary containing the individual weights for each output feature with its corresponding key to the parameter weights.
# Manual set up of ActiveLearning
weights = {
"f_0": 0.4,
"f_1": 0.6,
}
# create an instance of the acquisition function with distinct weights
af = qNegIntPosVar(weights=weights, n_mc_samples=16)
data_model = ActiveLearningStrategy(
domain=domain,
surrogate_specs=BotorchSurrogates(
surrogates=[
SingleTaskGPSurrogate(
inputs=domain.inputs,
outputs=Outputs(features=[domain.outputs[0]]),
),
SingleTaskGPSurrogate(
inputs=domain.inputs,
outputs=Outputs(features=[domain.outputs[1]]),
),
],
),
acquisition_function=af,
)
recommender = strategies.map(data_model=data_model)
recommender.tell(experiments=initial_experiments)
candidates = recommender.ask(candidate_count=1)display(candidates)| x_1 | x_2 | f_0_pred | f_1_pred | f_0_sd | f_1_sd | f_0_des | f_1_des | |
|---|---|---|---|---|---|---|---|---|
| 0 | 3.841079 | -1.367371 | 392.08179 | 8.849437 | 276.17034 | 1.971168 | -392.08179 | -8.849437 |
# Running the active learning strategy
n_iter = 1 if SMOKE_TEST else 20
results = initial_experiments
for _ in range(n_iter):
# run active learning strategy
X = recommender.ask(candidate_count=1)[domain.inputs.get_keys()]
Y = function.f(candidates=X)
XY = pd.concat([X, Y], axis=1)
recommender.tell(experiments=XY) # pass new experimental data
results = pd.concat([results, XY], axis=0, ignore_index=True)random_results = run(
function,
strategy_factory=strategy_factory,
n_iterations=n_iter,
metric=lambda domain, experiments: 1.0,
initial_sampler=sample,
n_runs=1,
n_procs=1,
)Plotting
if not SMOKE_TEST:
plt.rcParams["figure.figsize"] = (10, 8)
fig, ax = plt.subplots(2, 2)
def f1(grid):
return (
-20 * np.exp(-0.2 * np.sqrt(0.5 * (grid[0] ** 2 + grid[1] ** 2)))
- np.exp(0.5 * (np.cos(2 * np.pi * grid[0]) + np.cos(2 * np.pi * grid[1])))
+ np.exp(1)
+ 20
)
Z1 = f1(mesh)
levels = np.linspace(Z1.min(), Z1.max(), 10)
ax[0, 0].contourf(
X_grid,
Y_grid,
Z,
cmap=cm.viridis,
)
ax[0, 0].scatter(random_results[0][0].x_1, random_results[0][0].x_2, c="white")
ax[0, 1].contourf(
X_grid,
Y_grid,
Z,
cmap=cm.viridis,
)
ax[0, 1].scatter(results.x_1, results.x_2, c="white")
ax[1, 0].contourf(X_grid, Y_grid, Z1, cmap=cm.viridis, levels=levels)
ax[1, 0].scatter(
random_results[0][0].x_1,
random_results[0][0].x_2,
c="white",
edgecolors="black",
)
ax[1, 1].contourf(X_grid, Y_grid, Z1, cmap=cm.viridis, levels=levels)
ax[1, 1].scatter(results.x_1, results.x_2, c="white", edgecolors="black")
ax[0, 0].axis([-7, 7, -7, 7])
ax[1, 0].set_xlabel("$x_1$")
ax[1, 1].set_xlabel("$x_1$")
ax[0, 0].set_ylabel("$x_2$")
ax[1, 0].set_ylabel("$x_2$")
ax[0, 0].set_title("random strategy")
ax[0, 1].set_title("active learning strategy")
fig.show()