Cost-Penalized Custom Objective#
import obsidian
print(f'obsidian version: ' + obsidian.__version__)
import pandas as pd
import plotly.express as px
import plotly.io as pio
pio.renderers.default = "plotly_mimetype+notebook"
obsidian version: 0.8.0
Introduction¶
In this tutorial, we will examine usage of obsidian for performing a cost-penalized optimization using a tailored objective function.
Often times, it is desireable to generate an objective function based on the input data X
. Rather than build a model to a response calculated off-line, it is best to capture the analytical form where possible. The custom objective Feature_Objective
simply allows the user to index the input variables and multiply them by coefficient(s) to generate a new objective function. In this example, we will create a Feature_Objective
based off of "Enzyme" loading, to simulate an optimization where product yield might be weighed against an expensive input.
The optimization problem then becomes multi-output: "Product" and "Penalized Enzyme." However, we can combine these further to create a single objective function using a Scalarization
. In the simplest case, we may want to add the two objectives with equal weights, which would be the default behavior of Scalarize_WeightedSum
.
In obsidian, we combine a sequence of objectives using Objective_Sequence
. Thus, finally, the final objective function is objective = Objective_Sequence([Feature_Objective, Scalarize_WeightedSum])
and single-output acquisition functions may be used to select optimal experiments.
Set up parameter space and initialize a design¶
from obsidian import Campaign, Target, ParamSpace, BayesianOptimizer
from obsidian.parameters import Param_Continuous
params = [
Param_Continuous('Temperature', -10, 30),
Param_Continuous('Concentration', 10, 150),
Param_Continuous('Enzyme', 0.01, 0.30),
]
X_space = ParamSpace(params)
target = Target('Product', aim='max')
campaign = Campaign(X_space, target, seed=0)
X0 = campaign.initialize(m_initial = 10, method = 'LHS')
X0
Temperature | Concentration | Enzyme | |
---|---|---|---|
0 | 16.0 | 17.0 | 0.1985 |
1 | 0.0 | 129.0 | 0.1695 |
2 | 8.0 | 59.0 | 0.2855 |
3 | 12.0 | 143.0 | 0.1115 |
4 | -8.0 | 87.0 | 0.2275 |
5 | 20.0 | 73.0 | 0.0535 |
6 | 4.0 | 101.0 | 0.0245 |
7 | 28.0 | 45.0 | 0.1405 |
8 | 24.0 | 115.0 | 0.2565 |
9 | -4.0 | 31.0 | 0.0825 |
Collect results (e.g. from a simulation)¶
from obsidian.experiment import Simulator
from obsidian.experiment.benchmark import cornered_parab
simulator = Simulator(X_space, cornered_parab, name='Product', eps=0.05)
y0 = simulator.simulate(X0)
Z0 = pd.concat([X0, y0], axis=1)
campaign.add_data(Z0)
campaign.data.sample(5)
Temperature | Concentration | Enzyme | Product | Iteration | |
---|---|---|---|---|---|
Observation ID | |||||
5 | 20.0 | 73.0 | 0.0535 | -4.755681 | 0 |
2 | 8.0 | 59.0 | 0.2855 | -66.782316 | 0 |
3 | 12.0 | 143.0 | 0.1115 | -85.627439 | 0 |
4 | -8.0 | 87.0 | 0.2275 | -17.129587 | 0 |
1 | 0.0 | 129.0 | 0.1695 | -45.900354 | 0 |
Fit the optimizer and visualize results¶
campaign.fit()
GP model has been fit to data with a train-score of: 1 for response: Product
from obsidian.plotting import factor_plot, optim_progress
factor_plot(campaign.optimizer, feature_id=1)
Optimize new experiment suggestions¶
from obsidian.objectives import Objective_Sequence, Feature_Objective, Scalar_WeightedSum
Note: Objectives can be passed directly to an Optimizer
, or set using campaign.set_objective()
after which the Campaign
will automatically use the objective durign campaign.suggest()
. At any time, the objective can be re-set to a new objective, or deleted using campaign.clear_objective()
.
penalize_enz_loading = Feature_Objective(X_space, indices=[2], coeff=[-5])
add_objectives = Scalar_WeightedSum(1)
campaign.set_objective(objective=Objective_Sequence([penalize_enz_loading, add_objectives]))
X_suggest, eval_suggest = campaign.suggest(m_batch = 3, optim_sequential = False)
c:\Users\kevin\miniconda3\envs\obsidian-dev\lib\site-packages\botorch\optim\optimize.py:564: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s): [OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')] Trying again with a new set of initial conditions. c:\Users\kevin\miniconda3\envs\obsidian-dev\lib\site-packages\botorch\optim\optimize.py:564: RuntimeWarning: Optimization failed on the second try, after generating a new set of initial conditions.
df_suggest = pd.concat([X_suggest, eval_suggest], axis=1)
df_suggest
Temperature | Concentration | Enzyme | Product (pred) | Product lb | Product ub | f(Product) | Objective 1 | aq Value | aq Value (joint) | aq Method | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | -1.366459 | 23.701565 | 0.027559 | 82.545038 | 52.602369 | 112.487712 | 1.704477 | 1.566681 | -1.214936 | -0.977403 | NEI |
1 | 0.748942 | 100.907522 | 0.276121 | -70.506235 | -99.886332 | -41.126134 | -0.532599 | -1.913204 | -46.057986 | -0.977403 | NEI |
2 | -10.000000 | 53.696260 | 0.060275 | 76.435947 | 46.268528 | 106.603372 | 1.615183 | 1.313808 | -1.893481 | -0.977403 | NEI |
Note: We can examine the output of various objectives within the sequence by passing them directly to optimizer.evaluate
. Here, we can explicitly see the balance of Objective 1 (product response) and Objective 2 (cost penalty) before they are combined in the weighted sum.
campaign.optimizer.evaluate(X_suggest, objective=penalize_enz_loading)
Product (pred) | Product lb | Product ub | f(Product) | Objective 1 | Objective 2 | Expected Hypervolume (joint) | Expected Pareto | |
---|---|---|---|---|---|---|---|---|
0 | 82.545038 | 52.602369 | 112.487712 | 1.704477 | 1.704477 | -0.137796 | 4.775616 | False |
1 | -70.506235 | -99.886332 | -41.126134 | -0.532599 | -0.532599 | -1.380605 | 4.775616 | False |
2 | 76.435947 | 46.268528 | 106.603372 | 1.615183 | 1.615183 | -0.301376 | 4.775616 | True |
Collect data at new suggestions¶
y_iter1 = pd.DataFrame(simulator.simulate(X_suggest), columns = ['Product'])
Z_iter1 = pd.concat([X_suggest, y_iter1, eval_suggest], axis=1)
campaign.add_data(Z_iter1)
campaign.data.tail()
Temperature | Concentration | Enzyme | Product | Iteration | Objective 1 | Product (pred) | Product lb | Product ub | f(Product) | aq Value | aq Value (joint) | aq Method | Objective 1 (max) (iter) | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Observation ID | ||||||||||||||
8 | 24.000000 | 115.000000 | 0.256500 | -166.801349 | 0 | -3.222598 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1.363311 |
9 | -4.000000 | 31.000000 | 0.082500 | 87.425397 | 0 | 1.363311 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1.363311 |
10 | -1.366459 | 23.701565 | 0.027559 | 100.139209 | 1 | 1.823846 | 82.545038 | 52.602369 | 112.487712 | 1.704477 | -1.214936 | -0.977403 | NEI | 1.823846 |
11 | 0.748942 | 100.907522 | 0.276121 | -79.689998 | 1 | -2.047438 | -70.506235 | -99.886332 | -41.126134 | -0.532599 | -46.057986 | -0.977403 | NEI | 1.823846 |
12 | -10.000000 | 53.696260 | 0.060275 | 87.065807 | 1 | 1.469179 | 76.435947 | 46.268528 | 106.603372 | 1.615183 | -1.893481 | -0.977403 | NEI | 1.823846 |
Repeat as desired¶
for iter in range(3):
campaign.fit()
X_suggest, eval_suggest = campaign.suggest(m_batch=3)
y_iter = pd.DataFrame(simulator.simulate(X_suggest))
Z_iter = pd.concat([X_suggest, y_iter, eval_suggest], axis=1)
campaign.add_data(Z_iter)
GP model has been fit to data with a train-score of: 1 for response: Product GP model has been fit to data with a train-score of: 1 for response: Product GP model has been fit to data with a train-score of: 1 for response: Product
Examine the optimization progress from the context of different elements of the compositve objective function
First, the final objective - a weighted sum of product yield and a cost-penalized input.
optim_progress(campaign, color_feature_id = 'aq Value')
Next, we can specifically examine the context of the multi-output optimization minimizing (maximizing negative) cost (Objective 2) and maximizing product (Objective 1).
campaign.set_objective(penalize_enz_loading)
optim_progress(campaign)
Finally, we can clear the objective entirely and just examine how this optimization performed from the lens of product alone.
campaign.clear_objective()
optim_progress(campaign)