制約条件の考慮

決定変数に対する制約条件を考慮することが可能です。制約条件とは、最適化問題において決定変数の値がみたすべき条件のことです。このページでは、目的関数の定義 で挙げた、以下の『翼の設計において「揚力を最大化し、抵抗力を最小化する」ことを目的としたブラックボックス関数』を例として、Amplify-BBOptにおける制約条件の取り扱いについて説明します。

import WingSimulator

from amplify_bbopt import RealVariable, blackbox

@blackbox
def my_blackbox_func(
    width: float = RealVariable(bounds=(1, 20)),
    height: float = RealVariable(bounds=(1, 5)),
    angle: float = RealVariable(bounds=(0, 45)),
) -> float:
    """シミュレーションで得られた翼に働く揚力と抵抗力の比(揚抗比)の負値を返却する."""
    simulator = WingSimulator(width, height, angle)
    lift_force, drag_force = simulator.simulate()

    return -lift_force / drag_force

このブラックボックス関数の場合、width, height, angle という3つの決定変数を考慮します。各決定変数が取り得る値の範囲は、定義時に指定されています。

ここで、最適化に際して考慮する条件として、これらの決定変数を入力とする関数値や、決定変数から構成される多項式値に対する制約を課すことが可能です。例えば、width + 2 * height <= 25 という多項式に基づく制約条件であったり、g(width, height, angle) >= 0 という多項式では表現不可な (ブラックボックスな) 関数に基づく制約条件などです。制約条件の特性や、その目的に応じて、以下のハード制約・ソフト制約の2通りの実装方法があります。

ハード制約

制約条件が、決定変数に対する多項式とその値に基づく等式・不等式として表現できる場合、ハード制約として扱うことが可能です。ハード制約は、全て満たされない限り解として認められない、逆に解が得られたならば全ての制約を充足していることが (バックエンドの Amplify SDK により) 保証されている制約であるため、「ハード」と呼ばれます。Amplify-BBOpt では、こうしたハード制約を Amplify SDK の制約ヘルパー関数により考慮します。

以下は、Amplify-BBOpt で利用可能な Amplify SDK 制約ヘルパー関数の一覧です。ここで、left は決定変数に基づく多項式、right は値 (clamp の場合は値のタプル) となります。

Amplify SDK ヘルパー関数

制約式

one_hot(left)

left == 1

equal_to(left, right)

left == right

less_equal(left, right)

left <= right

greater_equal(left, right)

left >= right

clamp(left, right)

right[0] <= left <= right[1]

これらのヘルパー関数に基づき、上記ブラックボックス関数と共に考慮する前述の制約条件 width + 2 * height <= 25 は、Optimizer クラスのインスタンス時に次のように追加します。制約条件左辺の多項式に現れる決定変数オブジェクトは、以下コード例のように、ブラックボックス関数クラスのアトリビュートとして取り出すことが可能です。

Note

以下のコード内の trainer 及び client はそれぞれ、事前に定義したサロゲートモデル関数とAmplify SDK ソルバークライアントとなります。これらの準備については「最適化の実行」を参照してください。

import WingSimulator
import amplify

from amplify_bbopt import Optimizer, RealVariable, blackbox

@blackbox
def my_blackbox_func(
    width: float = RealVariable(bounds=(1, 20)),
    height: float = RealVariable(bounds=(1, 5)),
    angle: float = RealVariable(bounds=(0, 45)),
) -> float:
    """シミュレーションで得られた翼に働く揚力と抵抗力の比(揚抗比)の負値を返却する."""
    simulator = WingSimulator(width, height, angle)
    lift_force, drag_force = simulator.simulate()

    return -lift_force / drag_force


# ブラックボックス関数クラスのアトリビュートとして決定変数オブジェクトを取り出し
width = my_blackbox_func.variables.width
height = my_blackbox_func.variables.height

#  制約条件の定義
constraint = amplify.less_equal(width + 2 * height, 25)
print(constraint) #  出力: width + 2 height <= 25 (weight: 1)

#  最適化クラスのインスタンス化
optimizer = Optimizer(
    my_blackbox_func,
    trainer,
    client,
    constraints = constraint,  # 制約条件を追加
)

ソフト制約

与えられた決定変数が制約を満たさない場合に、本来のブラックボックス目的関数値 -lift_force / drag_force にペナルティ値を加算する、という方法でも制約条件を間接的に考慮することができます。このような方法は、ソフト制約を呼ばれることもあり、制約条件が多項式表現不能(ハード制約適用不可)な場合に有効です。

前述のハード制約と異なり、ソフト制約は、制約を充足しない解も、Amplify-BBOpt の視点からは依然として解と見なされます。制約は、Amplify-BBOptで明示的に定義されるのではなく、目的関数の一部として取り扱われます。

以下は、そのようなソフト制約を、1 の大きさを持つ制約ペナルティで考慮するブラックボックス関数の一例です。ソフト制約は、ペナルティ値の推定も含め、様々な実装方法が可能です。ただし、元の目的関数値の値の大きさに応じて、ペナルティ値の大きさを適切に設定する必要があることに注意してください。

@blackbox
def my_blackbox_func(
    width: float = RealVariable(bounds=(1, 20)),
    height: float = RealVariable(bounds=(1, 5)),
    angle: float = RealVariable(bounds=(0, 45)),
) -> float:
    """シミュレーションで得られた翼に働く揚力と抵抗力の比(揚抗比)の負値に制約ペナルティ値を加えたものを返却する."""
    simulator = WingSimulator(width, height, angle)
    lift_force, drag_force = simulator.simulate()

    #  制約条件を満たさない場合はペナルティ 1 を加算
    penalty  = 0
    if g(width, height, angle) < 0:
        penalty = 1

    return -lift_force / drag_force + penalty

Tip

シミュレーション (WingSimulator) が高コストな場合、制約を満たさない解に対してはシミュレーション実施前にペナルティ値のみを返却する、という方法も考えられます。

@blackbox
def my_blackbox_func(
    width: float = RealVariable(bounds=(1, 20)),
    height: float = RealVariable(bounds=(1, 5)),
    angle: float = RealVariable(bounds=(0, 45)),
) -> float:
    """シミュレーションで得られた翼に働く揚力と抵抗力の比(揚抗比)の負値を返却する."""
    
    #  制約条件を満たさない場合はペナルティ値のみで返却
    if g(width, height, angle) < 0:
        return 1

    simulator = WingSimulator(width, height, angle)
    lift_force, drag_force = simulator.simulate()

    return -lift_force / drag_force