Multi-objective optimization¶
Amplify-BBOpt provides two approaches for multi-objective optimization:
Single-objective aggregation |
Direct multi-objective optimization |
|
|---|---|---|
Class |
||
Black-box return type |
|
|
Surrogate model approximates |
Weighted sum of objectives (aggregated value) |
Each objective value (before aggregation) |
Best suited for |
Fixed weights between objectives |
Dynamically adjusting weights per iteration |
Single-objective aggregation¶
Combine multiple objectives into a single float value via a weighted sum, and optimize using the standard Optimizer. Because the surrogate model directly approximates the weighted sum, it becomes specialized for that weight configuration. This approach is best suited when the weights are predetermined and will not change.
from amplify_bbopt import blackbox, KMTrainer, Optimizer, RealVariable
# Weights for each objective
w_g = 10
w_h = 2
@blackbox
def bbfunc(
x: float = RealVariable((0, 1)),
y: float = RealVariable((0, 1)),
z: float = RealVariable((0, 1)),
) -> float:
return w_g * g(x, y) + w_h * h(y, z)
optimizer = Optimizer(
blackbox=bbfunc,
trainer=KMTrainer(),
client=my_client,
)
optimizer.add_random_training_data(num_data=10)
optimizer.optimize(10)
Note
With this approach, a single surrogate model approximates the weighted sum. If the weights change, past training data no longer represents the new weighted-sum target, so weight changes must be made with care.
Direct multi-objective optimization¶
By combining a black-box function that returns list[float] with MultiOptimizer, each objective is approximated by its own independent surrogate model. Because each surrogate model is trained independently for its own objective, you can learn the shape of each objective separately without letting a dominant objective adversely affect the others’ surrogate models. Also, since objective values are stored separately, you can change weights via weight at each iteration without collecting the training data again (see Dynamic weight adjustment for details).
Defining the black-box function¶
For multi-objective optimization, the black-box function should return list[float].
from amplify_bbopt import blackbox, IntegerVariable
@blackbox
def my_multi_blackbox_func(
x0: int = IntegerVariable((0, 10)),
x1: int = IntegerVariable((0, 10)),
) -> list[float]:
y0 = simulator0(x0, x1)
y1 = simulator1(x0, x1)
return [y0, y1]
The black-box function above handles the case where multiple objectives share common decision variables. For other dependencies between objectives, see Dependencies between objective functions.
Tip
If you want to set static weights between objectives, include the weights directly in the return value of the black-box function.
@blackbox
def my_multi_blackbox_func(...):
y0 = simulator0(...)
y1 = simulator1(...)
return [2.0 * y0, y1] # y0 weighted twice as heavily
In this case, best comparison is also based on the scaled values (in the example above, 2.0 * y0 and y1).
To dynamically change weights as optimization progresses, see Dynamic weight adjustment.
Instantiating MultiOptimizer¶
Use MultiOptimizer for multi-objective optimization. Pass a list of Trainer instances to trainer — one per objective. Each element must be a distinct instance; reusing the same instance across objectives raises a ValueError.
from amplify_bbopt import KMTrainer, MultiOptimizer
optimizer = MultiOptimizer(
blackbox=my_multi_blackbox_func,
trainer=[KMTrainer(), KMTrainer()], # one Trainer per objective
client=my_client,
)
Caution
Trainer is an object that updates its internal state on each training call. Reusing the same instance across objectives causes training data from one objective to mix into another, corrupting the surrogate models.
Caution
As with single-objective optimization, you can specify a data transformation for each objective’s training data via surrogate_data_transformer. For multi-objective, pass a list of DataTransformer instances — one per objective. Each element must be a distinct instance; reusing the same instance raises a ValueError.
from amplify_bbopt import ExpScaler, KMTrainer, MultiOptimizer
optimizer = MultiOptimizer(
blackbox=my_multi_blackbox_func,
trainer=[KMTrainer(), KMTrainer()],
client=my_client,
surrogate_data_transformer=[ExpScaler(), ExpScaler()], # one DataTransformer per objective
)
Pass None for any objective that does not need a transformation.
optimizer = MultiOptimizer(
blackbox=my_multi_blackbox_func,
trainer=[KMTrainer(), KMTrainer()],
client=my_client,
surrogate_data_transformer=[ExpScaler(), None], # transform first objective only
)
Running optimization¶
The basic flow for adding initial training data and running optimization is the same as single-objective. However, when passing existing data via training_data, the shape of y differs: for single-objective, y must be a 1D array of shape (n_samples,), but for multi-objective it must be a 2D array of shape (n_samples, n_objectives).
# Add initial random training data
optimizer.add_random_training_data(num_data=10)
# Run the optimization cycles
optimizer.optimize(10)
import numpy as np
from amplify_bbopt import Dataset
data_x = ... # shape: (n_samples, n_variables)
data_y = ... # shape: (n_samples, n_objectives) ← 2D
optimizer = MultiOptimizer(
blackbox=my_multi_blackbox_func,
trainer=[KMTrainer(), KMTrainer()],
client=my_client,
training_data=Dataset(data_x, data_y),
)
Retrieving optimization results¶
As with single-objective, use best to retrieve the best solution found. For multi-objective, objective is a list[float] — one value per objective.
print(optimizer.best.values) # Best solution (input values)
print(optimizer.best.objective) # Objective value: list[float]
Note
The best solution is the one that minimizes the weighted sum of objective values — each value multiplied by its surrogate model’s weight (weight). When weights are fixed (including the default of 1.0), this criterion is meaningful. However, when weights are adjusted dynamically, each iteration uses different weights yet best re-evaluates all samples with the current weights only, so comparisons may not be consistent across iterations (see Best solution when using dynamic weights for details).