既存の契約に独自Componentを追加する#
前提は、既存のComponentの契約に収まる差し替えを実装することです。
このページでできるようになること
このページを終えると、拡張点を選び、最小の契約を確認し、登録してテストできます。
拡張ガイドラインで拡張点の分類を確認します。契約や実行フレームワーク自体を変更する場合は、フレームワーク拡張へ進みます。
importの方針は Canonical Imports にまとめています。
拡張点を選ぶ#
ビルトインコンポーネントの設定変更で足りるなら、ビルトインコンポーネントの差し替えまたはHookを使います。新しい探索、予測、評価、生成ロジックが必要な場合は、対応する契約を持つコンポーネントを選びます。
変更 |
拡張点 |
最小実装境界 |
|---|---|---|
候補を生成し、評価結果を消費する |
|
|
予測モデル |
|
|
予測を候補スコアに変換する |
|
|
graph-native経路で評価バックエンドを拡張する |
|
|
Stage互換経路で同期Evaluatorを差し替える |
|
|
世代実行ポリシー |
|
|
互換実行単位 |
|
|
各契約で必要なメソッドは、Algorithmリファレンス、Surrogateリファレンス、Acquisitionリファレンス、Stageリファレンスで確認します。似た名前でも契約が同じとは限らないため、境界を確認せずに別コンポーネントのメソッドを再利用してはいけません。
最小の契約を確認する#
実装前に、既存の抽象基底クラスの抽象メソッド、contract()、実行時に参照される属性を確認します。Algorithm.ask()とAlgorithm.tell()はStateViewを読み、ProposalBatchまたはStatePatchを返します。Surrogateは配列を受け取り、SurrogatePredictionを返します。StageはOptimizationStateを受け取り、更新後の状態を返します。
graph-native経路のComponent拡張#
Algorithmなどのgraph-nativeコンポーネントは、ask(...) -> ProposalBatchとtell(FeedbackBatch, StateView) -> StatePatchの境界で拡張します。評価を追加する場合は、GenomeBatch → EvaluationAdapter → EvaluationPayload → Evaluator → ObservationBatchの経路に接続します。
Stage互換経路の同期Evaluator拡張#
この例では、Stage互換経路の評価処理だけをEvaluatorに置き換えます。実装境界はevaluate_batch(x, problem) -> EvaluationResultで、EvaluationResultは1次元または2次元のf、g、cv配列を受け取ります。
import numpy as np
from saealib import EvaluationResult, Evaluator
class BatchSphereEvaluator(Evaluator):
def evaluate_batch(self, x, problem):
x = np.atleast_2d(np.asarray(x, dtype=float))
values = np.sum(x**2, axis=1)
return EvaluationResult(
f=values[:, None],
g=np.empty((len(x), 0)),
cv=np.zeros(len(x)),
)
独自EvaluatorをOptimizer.set_evaluator()に渡すと、組み込みの同期評価アダプターが呼び出します。EvaluationResultは行数と配列形状を検証します。
登録または組み込む#
直接使う場合は、公開メソッドOptimizer.set_evaluator(evaluator)にインスタンスを渡します。設定ファイルやプリセットが名前でクラスを構築する場合だけ、@register()を追加してRegistryに登録します。
import numpy as np
from saealib import Optimizer, Problem, register
@register()
class NamedBatchSphereEvaluator(BatchSphereEvaluator):
pass
problem = Problem(
lambda x: np.sum(x**2),
dim=3,
n_obj=1,
direction=np.array([-1.0]),
lb=[-5.0] * 3,
ub=[5.0] * 3,
)
optimizer = Optimizer(problem).set_evaluator(NamedBatchSphereEvaluator())
Problem の評価関数を常にこのEvaluatorで置き換えるとは限らないため、set_evaluator() を呼んだ構成を実際に使って検証します。
最小テストを置く#
まず evaluate_batch() の形状と値を固定した小さな単体テストを書きます。次に Optimizer(problem).set_evaluator(...) を短い max_fe で実行し、契約検証と実行経路を確認します。抽象基底クラスの一覧化や実行フレームワークの変更はこのガイドの範囲外です。対応するAPIリファレンスまたはフレームワーク拡張のドキュメントを参照してください。
外部OperatorをnativeなComponentへ移植する#
pymooアダプターで外部演算子をラップすると、そのライブラリへのランタイム依存が残ります。pymooアダプターはGAの既定バッチモードで個体群単位のベクトル化を維持し、ラップした演算子をバッチ全体またはゲート済み部分集合に対して一度だけ呼び出すため、要素ごとの呼び出しオーバーヘッドをなくす目的だけなら通常は移植不要です。ランタイム依存をなくしたい場合や、ソフトウェア論文向けにロジックをsaealibネイティブコードとして直接監査・引用可能にしたい場合に演算子を移植します。中核のロジックは通常変わらず、主な変更は必要に応じた配列形状の調整とsaealibのRNGの使用です。
移植時に間違えやすい点は、個体レベルの確率ゲートを置く場所です。saealibのGAはCrossover.probを自ら判定し、ゲートを通過した親グループだけをcrossover_batch()へ渡します。一方、Mutation.mutate_batch()は行ごとに1つのself.probゲートを抽選し、通過しなかった行を変更せずに残す必要があります。pymooの組み込みMutationもDEAPのtoolbox.mutate()(algorithms.varAndのmutpbによって外部でゲートされます)もゲートを演算子の外に置くため、移植後のMutationには元のコードになかったゲート配列が通常必要です。
pymooから移植する#
独自のpymoo Mutation は通常、batch全体のX(形状(n_individuals, dim))に対してベクトル化します:
import numpy as np
from pymoo.core.mutation import Mutation as PymooMutationBase
class MyPymooMutation(PymooMutationBase):
def __init__(self, sigma=1.0):
super().__init__()
self.sigma = sigma
def _do(self, problem, X, random_state=None, **kwargs):
rng = random_state if random_state is not None else np.random.default_rng()
Xp = X.copy()
mask = rng.random(Xp.shape) < 0.5
Xp[mask] += rng.normal(0, self.sigma, size=Xp.shape)[mask]
return Xp
nativeなsaealib Mutationへ移植しても、batch軸は維持されます。_do()本体はmutate_batch()へほぼそのまま対応し、主な追加はsaealibの行ごとのprobゲートです:
import numpy as np
from saealib.operators import Mutation
class MyMutation(Mutation):
def __init__(self, prob=1.0, *, sigma=1.0, prob_var=0.5):
super().__init__()
self.prob = prob
self.sigma = sigma
self.prob_var = prob_var
def mutate_batch(self, candidates_batch, mutate_range, rng=np.random.default_rng()):
candidates_batch = np.asarray(candidates_batch, dtype=float)
n, dim = candidates_batch.shape
gate = rng.random(n) < self.prob
result = candidates_batch.copy()
if not np.any(gate):
return result
selected = result[gate]
mask = rng.random((len(selected), dim)) < self.prob_var
noise = rng.normal(0, self.sigma, size=selected.shape)
selected[mask] += noise[mask]
result[gate] = selected
return result
DEAPから移植する#
DEAPのOperatorは一度に1個体または1組の親を処理するため、nativeなsaealibへの移植では先頭にbatch軸を追加し、その軸に沿ってベクトル化する必要があります。独自の交叉は次のようになります:
import random
def my_cx(ind1, ind2, swap_rate=0.5):
for i in range(len(ind1)):
if random.random() < swap_rate:
ind1[i], ind2[i] = ind2[i], ind1[i]
return ind1, ind2
親の組と次元の両方に対してベクトル化することで、nativeなCrossoverになります。また、random.random()をsaealibから渡されるrngに置き換え、DEAPが暗黙に持つグローバルRNG状態を、実行ごとのsaealibのnp.random.Generator(minimize(..., seed=...)で再現可能)に置き換えます:
import numpy as np
from saealib.operators import Crossover
class MyCrossover(Crossover):
def __init__(self, prob, swap_rate=0.5):
super().__init__()
self.prob = prob
self.swap_rate = swap_rate
def crossover_batch(self, parents_batch, bounds=None, rng=np.random.default_rng()):
n_pair, _, dim = parents_batch.shape
p1, p2 = parents_batch[:, 0, :], parents_batch[:, 1, :]
mask = rng.random((n_pair, dim)) < self.swap_rate
c1 = np.where(mask, p2, p1)
c2 = np.where(mask, p1, p2)
return np.stack((c1, c2), axis=1)
toolbox.mateはvarAndのcxpbによって外部からゲートされ、saealibのGAと一致するため、突然変異とは異なり、ここで追加のゲートは不要です。
AlgorithmとProblemの移植#
検索アルゴリズム全体(生存選択、アーカイブ管理、DEのようなインデックスに結び付いた状態)の移植は、同じ意味で機械的な書き換えではありません。Algorithm.ask()/tell()をゼロから再実装することになります。pymooでは、外部ライブラリとの連携で説明しているエンジンモードのPymooAlgorithmアダプターが書き換えなしで対応します。他のライブラリについては、ネイティブなAlgorithmサブクラスに必要な実装をAlgorithmで確認してください。
コンポーネントの責務を分ける#
既存の実行契約を保ったまま拡張する場合は、担当する責務と状態の境界を先に決めます。
Component |
主な責務 |
状態境界 |
|---|---|---|
|
候補を提案しフィードバックを消費する |
|
Operator |
交叉、突然変異、親選択、生存選択 |
Algorithmから渡される候補とRNG |
|
予測モデルでfit/predictする |
Archiveからの学習データと予測 |
|
評価計画、候補選択、フィードバックの流れ |
|
|
既存の逐次経路で1つの操作 |
|
各コンポーネントを実装する前に、対応するConceptページで契約と組み込み実装を確認します。Algorithm.ask()とAlgorithm.tell()、Strategy.build_graph()、Stage.execute()は異なる境界です。
実装順とStageの移行#
ProblemとSearchSpaceが返すデータ形式を確認します。公開名前空間から依存コンポーネントを取得します。
コンポーネント単体の契約と状態アクセスを実装します。
Optimizerの差し替えAPIで組み合わせます。Feedbackの候補ID、評価の出所、チェックポイント動作を確認します。
既存Stageを構造化パイプラインへ接続する場合はstage_component(stage)を明示します。新しいgraph-nativeコンポーネントではStageのOptimizationState境界を再利用せず、ComponentContract、StateView、StatePatchを実装します。
詳細はAlgorithm、OptimizationStrategy、Stageを参照してください。