Optimizerを低レベルAPIで組み立てる#

Layer 2: Compose

前提は、目的関数を Problem として表現できることです。

このページでできるようになること

このページを終えると、既定値では足りないコンポーネントを Optimizer に登録し、run() または iterate() で実行できます。

高レベルAPIの一回実行で足りない場合に低レベルAPIへ移ります。 ビルトインコンポーネントの個別選択だけなら ビルトインコンポーネントの差し替え を先に使い、世代ごとの状態取得や実行中の交換が必要ならこのページの構成にします。 importの方針は Canonical Imports にまとめています。

Optimizerを構成する#

Optimizer(problem) を作り、set_*() をチェーンして実行コンポーネントを設定します。 set_*() は連結可能な設定APIであり、設定だけを行います。 run() または iterate() が既定値解決、実行計画の構築、構成の検証を行ってから実行を始めます。

import numpy as np
from saealib import Optimizer, Problem, Termination, max_fe


def objective(x):
    return np.sum(x**2)


problem = Problem(
    objective,
    dim=3,
    n_obj=1,
    direction=np.array([-1.0]),
    lb=[-5.0] * 3,
    ub=[5.0] * 3,
)

optimizer = Optimizer(problem, seed=0).set_termination(Termination(max_fe(100)))

必要なコンポーネントだけを set_initializer()、set_algorithm()、set_surrogate_manager()、set_acquisition()、set_strategy()、set_evaluator() などで追加します。 set_*() は Optimizer 自身を返すため、設定を連結できます。

サロゲート込みで完全に構成する#

minimize は各コンポーネントを既定の組み合わせで結線しますが、個別のパラメータは調整できません。 コンポーネントを個別に生成して Optimizer へ組み立てると、この制約がなくなります。

LocalSurrogateManager にはSurrogateと学習データの作り方を渡し、AcquisitionFunctionは Optimizer.set_acquisition() で別に設定します。 この二つを同じコンストラクタ引数として扱わないでください。

import numpy as np

from saealib import (
    GA,
    IndividualBasedStrategy,
    LHSInitializer,
    Optimizer,
    Problem,
    Termination,
    max_fe,
)
from saealib.acquisition import MeanPrediction
from saealib.operators import (
    CrossoverBLXAlpha,
    MutationUniform,
    SequentialSelection,
    TruncationSelection,
)
from saealib.surrogate import GaussianKernel, LocalSurrogateManager, RBFSurrogate

DIM = 10
SEED = 0

problem = Problem(
    objective,
    dim=DIM,
    n_obj=1,
    direction=np.array([-1.0]),  # -1: minimize
    lb=[-5.0] * DIM,
    ub=[5.0] * DIM,
)

algorithm = GA(
    crossover=CrossoverBLXAlpha(0.7, 0.4),
    mutation=MutationUniform(0.3),
    parent_selection=SequentialSelection(),
    survivor_selection=TruncationSelection(),
)

surrogate_manager = LocalSurrogateManager(
    RBFSurrogate(kernel=GaussianKernel()),
)
strategy = IndividualBasedStrategy(evaluation_ratio=0.1)
initializer = LHSInitializer(
    n_init_archive=5 * DIM,
    n_init_population=4 * DIM,
    seed=SEED,
)
termination = Termination(max_fe(500))

optimizer = (
    Optimizer(problem, seed=SEED)
    .set_initializer(initializer)
    .set_algorithm(algorithm)
    .set_surrogate_manager(surrogate_manager)
    .set_acquisition(MeanPrediction())
    .set_strategy(strategy)
    .set_termination(termination)
)

ctx = optimizer.run()
archive_x = ctx.archive.get_array("x")
archive_f = ctx.archive.get_array("f")[:, 0]
best_idx = int(np.argmin(archive_f))
print("Best solution:", archive_x[best_idx])
print("Objective value:", archive_f[best_idx])
print("Evaluations:", ctx.fe)

乱数シードは、Optimizer(problem, seed=SEED) と LHSInitializer(..., seed=SEED) の両方へ同じ値を渡します。

Optimizer の seed が既定の LHSInitializer へ自動的に伝播するのは、set_initializer() を自分で呼ばない場合(minimize/maximize 経由など)だけです。

Initializer を自分で組み立てる場合は、明示的に渡す必要があります。

終了条件を組み合わせる#

Termination は複数の条件を受け取ります。

列挙した条件のいずれかが満たされた時点で実行を終了します(論理OR)。

from saealib import Termination, max_fe, max_gen

termination = Termination(max_fe(500), max_gen(200))

lambdaで独自の条件を追加することもできます。

termination = Termination(
    max_fe(500),
    lambda ctx: ctx.archive.get_array("f")[:, 0].min() < 1e-4,
)

runとiterateを使い分ける#

run() は終了まで実行し、最後の OptimizationState を返します。 iterate() は世代ごとの状態を生成するため、履歴の記録や条件付きのコンポーネント交換、独自の早期打ち切りをループの中で行えます。

        flowchart LR
    A["Configure the Optimizer"] --> B["run or iterate"]
    B --> C["Resolve defaults, build plan, validate"]
    C --> D["Run a generation"]
    D --> E{"Termination met?"}
    E -- No --> D
    E -- Yes --> F["OptimizationState"]
    
ctx = optimizer.run()
print(ctx.fe, ctx.gen)

世代ごとの状態が必要な場合は run() の代わりに iterate() を使います。

history = []
for ctx in optimizer.iterate():
    best_f = ctx.archive.get_array("f")[:, 0].min()
    history.append((ctx.gen, ctx.fe, float(best_f)))
    print(f"gen={ctx.gen:4d}  fe={ctx.fe:4d}  best_f={best_f:.6f}")

print("Evaluations:", ctx.fe)

ctx.archive は評価済み解を保持し、ctx.fe と ctx.gen は評価回数と完了世代を示します。 途中でStrategyやSurrogateManagerを交換する場合は、各世代の ctx を読んで optimizer.set_*() を呼び出します。実行環境が変化を検出してplanを再コンパイルし、次世代から反映します。 この手順はStage互換経路とgraph-native経路のどちらでも利用できます。 Component側から再コンパイルを要求する経路については OptimizationStrategy の「Behavior of runtime swapping」を参照してください。 checkpointの保存と再開は Checkpointing に委ねます。

関連するConceptとReference#