多目的最適化

複数の目的関数を扱う場合、実装アプローチとして主に 2 つの方法があります。

単一の目的関数へ落とし込む

複数の目的関数を直接考慮する

使用クラス

Optimizer

MultiOptimizer

ブラックボックス戻り値

float

list[float]

サロゲートモデル化対象

目的関数値の重み付き和(集約後の値)

各目的関数値(集約前の値)

適しているケース

目的関数間の重みが固定

目的関数間の重みを最適化中に動的に調整

単一の目的関数へ落とし込む方法

複数の目的関数を重み付き和として 1 つの float 値にまとめ、通常の Optimizer で最適化します。重みが最初から決まっており変更する予定がない場合に適しています。サロゲートモデルが「最終的に最小化したい値(重み付き和)」を直接近似するため、その重み設定に特化したモデルが得られます。

from amplify_bbopt import blackbox, KMTrainer, Optimizer, RealVariable

# それぞれの目的関数の重み
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

この方法では、1 つのサロゲートモデルが重み付き和を近似します。重みが変わる場合、過去の学習データは新しい目的に対して不適切になるため、重みの変更は慎重に行う必要があります。

複数の目的関数を直接考慮する方法

list[float] を返すブラックボックス関数と MultiOptimizer を組み合わせることで、各目的関数を個別のサロゲートモデルで近似しながら最適化できます。

各サロゲートモデルが目的関数ごとに独立して近似されるため、支配的なある目的関数による、サロゲートモデルへの悪影響を避けながら、それぞれの目的の形状を個別に学習できます。また、各目的値を個別に保存しているため、重みを weight でイテレーションごとに変更しても学習データを取り直す必要がありません(詳細はこちら)。

ブラックボックス関数の定義

多目的最適化では、ブラックボックス関数の戻り値を 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]

上記のブラックボックス関数は、複数の目的関数が共通の決定変数を考慮するという依存関係を考慮しています。様々な依存関係については、こちらを参照してください。

Tip

目的関数間に静的な重みを設定したい場合は、ブラックボックス関数の返り値に重みを直接含めてください。

@blackbox
def my_multi_blackbox_func(...):
    y0 = simulator0(...)
    y1 = simulator1(...)
    return [2.0 * y0, y1]  # y0 を 2 倍の重みで扱う場合

この場合、best の比較もスケーリング済みの値(上記例では 2.0 * y0y1)に基づいて行われます。

最適化の進行に応じて重みを動的に変更したい場合は、こちらを参照してください。

MultiOptimizer のインスタンス化

多目的最適化には MultiOptimizer を使用します。trainer に目的関数の数だけ Trainer インスタンスのリストを渡します。各目的ごとに別々のインスタンスを生成する必要があり、同一インスタンスを複数の要素に使い回すと ValueError が発生します。

from amplify_bbopt import KMTrainer, MultiOptimizer

optimizer = MultiOptimizer(
    blackbox=my_multi_blackbox_func,
    trainer=[KMTrainer(), KMTrainer()],  # 目的関数ごとに1つの Trainer
    client=my_client,
)

Caution

Trainer は学習のたびに内部状態を更新するオブジェクトです。同一インスタンスを複数の目的に使うと、2 つ目以降の学習時に前の目的の学習データが混入し、サロゲートモデルが誤ったデータで更新されます。

Caution

単目的最適化と同様にsurrogate_data_transformer で各目的関数の学習データに対する変換を指定できます。多目的の場合は、DataTransformer インスタンスのリストを目的関数ごとに 1 つずつ渡します。各目的ごとに別々のインスタンスを生成する必要があり、同一インスタンスを複数の要素に使い回すと 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()],  # 目的関数ごとに1つの DataTransformer
)

変換が不要な目的には None を指定します。

optimizer = MultiOptimizer(
    blackbox=my_multi_blackbox_func,
    trainer=[KMTrainer(), KMTrainer()],
    client=my_client,
    surrogate_data_transformer=[ExpScaler(), None],  # 1つ目の目的のみ変換する場合
)

最適化の実行

初期学習データの追加と最適化の実行は基本的な流れは単目的と同様ですが、既存データを training_data として渡す場合は y の shape が異なります。単目的では (n_samples,) の 1D 配列ですが、多目的では (n_samples, n_objectives) の 2D 配列が必要です。

# 初期ランダム学習データを追加する
optimizer.add_random_training_data(num_data=10)

# 最適化サイクルを実行する
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),
)

最適化結果の取得

単目的と同様に best で最良解を取得できます。多目的の場合、objective は各目的関数値からなる list[float] となります。

print(optimizer.best.values)    # ベスト解(入力値)
print(optimizer.best.objective) # 目的関数値: list[float]

Note

ベスト解は、目的関数間の重みが固定(またはデフォルトの1.0)であることを各目的関数値にサロゲートモデルの重み(weight)を掛けた加重和が最小となる解として選択されます。目的関数間の重みが固定(またはデフォルトの1.0)の場合は、このようなベスト解は意味を持ちますが、目的関数間の重みが動的に変化する場合では、その限りではありません(詳細はこちら)。