Multiple Objectives¶
For some black-box optimization problems, there are not one, but multiple-objective functions to be considered. Such optimization problems are called multiple-objective optimization. Here, we will explain what multiple-objective optimizations are more specifically, and how we can implement them by using Amplify-BBOpt.
Dependency between objective functions¶
The objective functions considered in a multiple-objective optimization prolem are dependent via decision variables and/or constraint conditions. Thus, the purpose of multiple-objective optimization is to find a solution where these individual objectives are minimized on a desired balance. Here is very simple examples (for the purpose of explanation, these functions are not really black-box).
Dependency via decision variables¶
Let us use the below code to understand black-box objective functions having this type of dependency. As you can see, these two objective functions, obj_1
and obj_2
, are dependent via the integer variable of b
.
Now, suppose you only consider the first objective function obj_1
. The best solution to minimize the output from obj_1
would be clearly {"a": True, "b": 5}
, which yields the output of -5.
However, the second objective function obj_2
returns the sum of b
and c
. Thus, the above solution, {"a": True, "b": 5}
, is not a good choice for obj_2
. Instead of finding a solution that minimize only one of obj_1
or obj_2
, we would like to find a solution that minimize both obj_1
and obj_2
to the similar extent.
from amplify_bbopt import BinaryVariable, IntegerVariable
def obj_1(
a: bool = BinaryVariable(),
b: int = IntegerVariable(bounds=(1, 5)),
) -> int:
if a:
return -b
return b
def obj_2(
b: int = IntegerVariable(bounds=(1, 5)),
d: int = IntegerVariable(bounds=(1, 5)),
) -> int:
return b * d
Dependency via constraints¶
The below code considers the same objective functions, obj_1
and obj_2
, but obj_2
now takes (decision) variables c
and d
. Thus, these two functions no longer share the same variable b
. However, there is a constraint where:
b
ofobj_1
minusc
ofobj_2
must be0
.
This essentially imposes the same dependency via decision variables above, in a sence that second argument of obj_1
equals the first argument of obj_2
.
def obj_1(
a: bool = BinaryVariable(),
b: int = IntegerVariable(bounds=(1, 5)),
) -> int:
if a:
return -b
return b
def obj_2(
c: int = IntegerVariable(bounds=(1, 5)),
d: int = IntegerVariable(bounds=(1, 5)),
) -> int:
return c * d
# Constraint: b - c = 0
Although these two dependencies via decision variables and via constraints are not always interchangable, you can see that there are cases where multiple objective functions are dependent one another, and optimization for such objective functions need to consder these dependencies.
Considering multiple objectives¶
We saw that multiple objective functions can be dependent. Here, we explains how we can perform black-box optimization when there are multiple black-box objective functions dependenet one another. There are two methods to do so.
Method 1: using a unified objective¶
The first method is to combine all black-box objectives into one unified black-box objective function such that only one surrogate/acquisition model is constructed at each optimization cycle.
Below is an example using objective functions, obj_1
and obj_2
, demonstrated above. In this example, we will create one black-box objective function class instance with @blackbox
decorator, and this instance is named unified_obj
. In unified_obj
, there are two objectives, obj_1
and obj_2
, but the optimizer would only care about the unified output from the unified_obj
function. A surrogate/acquisition model is also constructed for unified_obj
, and the obj_1
and obj_2
are implicitly considered in this method.
In this method, you can simply pass unified_obj
to one of optimizer classes to create an optimizer instance, and start your optimization on par with typical black-box optimization with an objective function.
from amplify_bbopt import BinaryVariable, IntegerVariable, blackbox
@blackbox
def unified_obj(
a: bool = BinaryVariable(),
b: int = IntegerVariable(bounds=(1, 5)),
d: int = IntegerVariable(bounds=(1, 5)),
):
def obj_1(a: bool, b: int) -> int:
if a:
return -b
return b
def obj_2(b: int, d: int) -> int:
return b * d
out_1 = obj_1(a, b)
out_2 = obj_2(b, d)
return out_1 + out_2
Method 2: using multiple objectives¶
By contrast, in the method 2, we will consider these two objectives seperately, as they are. This means during the optimization cycles, two corresponding surrogate/acquisition models are seperately constructed. Then these models are combined (with appropriate weight if necessary) and converted to optimization model (typically a QUBO model by amplify.Model
) to proceed to find a optimial solution for the model.
Below is an example of using this method.
@blackbox
def obj_1(
a: bool = BinaryVariable(),
b: int = IntegerVariable(bounds=(1, 5)),
) -> int:
if a:
return -b
return b
@blackbox
def obj_2(
b: int = IntegerVariable(bounds=(1, 5)),
d: int = IntegerVariable(bounds=(1, 5)),
) -> int:
return b * d
For the above objective functions, you can also implement as follows, by using a constraint to impose the condition where that second argument of obj_1
equals the first argument of obj_2
. Note that the Constraint
object my_constraint
considers variables from two different objective functions.
from amplify_bbopt import equal_to
@blackbox
def obj_1(
a: bool = BinaryVariable(),
b: int = IntegerVariable(bounds=(1, 5)),
) -> int:
if a:
return -b
return b
@blackbox
def obj_2(
c: int = IntegerVariable(bounds=(1, 5)),
d: int = IntegerVariable(bounds=(1, 5)),
) -> int:
return c * d
my_constraint = equal_to(obj_1.variables.b - obj_2.variables.c, 0)
Now, we have two objective functions (and a constraint). In the next page we will describe “Multi-Objective Optimizer” to handle multiple objective functions and constraints spanning different objective functions.