Problem#

Layer 1: Use

Problemは、目的関数、目的方向、制約、比較方法、SearchSpaceを一つにまとめます。Optimizerと各コンポーネントは、このProblemを使って同じ最適化対象を参照します。

ベクトル表現では、lb、ub、またはvariablesからVectorSpaceが作られます。非ベクトル表現では、spaceにSearchSpaceを渡します。SearchSpaceとGenomeの設計はSearchSpaceで説明します。

Problemの役割#

Problem のコンストラクタは次の引数を受け取ります。

Problem(
    func, dim, n_obj, direction, lb=None, ub=None,
    comparator=None, constraints=None, *,
    eps_cv=1e-6, eps_obj=1e-6, handler=None, variables=None,
    space=None, evaluation_adapter=None,
)

func:評価器が使う目的関数です。ベクトル問題では通常、設計変数の配列を受け取り、非ベクトル問題では評価アダプターが生成したペイロードを受け取ることがあります。dim:設計変数の次元数です。spaceがdimを提供する場合は省略できます。n_obj:目的関数の数です。

direction:目的ごとの最適化方向を示す(n_obj,)形状の配列です。各要素は+1(最大化)または-1(最小化)でなければなりません。それ以外の値を渡すと構築時に例外が発生します。

lb/ub:ベクトル設計空間の下限と上限です。variablesもspaceも表現を提供しない場合に必要です。variablesが指定されると、境界は各変数の範囲から導出されます。

comparator:解を比較するComparator。 省略した場合、n_obj == 1ならSingleObjectiveComparator、n_obj > 1ならNSGA2Comparatorが自動的に選ばれます。 渡したComparatorのdirectionが未設定(None)であれば、Problemのdirectionがそのまま注入されます。

constraints:不等式制約(InequalityConstraint)のリストです。定義方法は制約付き最適化で説明します。

eps_cv/eps_obj:それぞれ実行可能性判定の許容誤差、目的関数値の同値判定の許容誤差を表します。 eps_cvはコンストラクタ実行時に既定のhandler/comparatorへ引き継がれるだけで、構築後にproblem.eps_cvを直接書き換えても実行時の挙動には反映されません。 実際に使われる閾値はhandler.feasibility_thresholdであり、Optimizerの実行中は毎世代comparator/pareto_archiveへ同期されます。

handler:制約違反の集約や目的関数の補正を担うConstraintHandler。 省略時はStaticToleranceHandler(eps_cv=eps_cv)が使われます。

variables:各設計変数の型をVariableのリストで指定します。連続変数だけの問題では省略でき、その場合は各次元がContinuousVariableとして扱われます。整数変数とカテゴリ変数を混在させる場合、ここにIntegerVariable/CategoricalVariableを含めると、Crossover/Mutationが変数型ごとに異なる演算子を自動的に割り当てます。

space:Genomeの表現と、サンプリング、検証、比較、距離計算などのサービスを提供する SearchSpace です。 space を渡し、variables、lb、ub を省略した場合は、dim をSearchSpaceから取得します。 非ベクトルのSearchSpaceでは、lb と ub が存在しない場合があります。

evaluation_adapter:GenomeBatchを目的関数が受け取れる入力へ変換するEvaluationAdapterです。表現と目的関数の入力形式が一致しない場合は、Problemに明示的に設定します。

注釈

旧バージョンのチュートリアルにはweight=という引数を使う例がありますが、現行のProblemにこの引数は存在しません。 weight=を渡すとTypeErrorになります。

Problemが保持するもの#

directionは、符号だけを表すsaealib全体で統一された±1配列です。一方、WeightedSumComparatorやDecompositionComparatorが受け取るweightsは、複数の目的をスカラー値へ集約するための非負の重みであり、方向とは独立した別の概念です。

この役割分担のもとでは、目的の重要度そのもの(スケーリング)をweightsで表現することはできません。 目的関数値の大きさを調整したい場合はfuncの内部でスケーリングします。 directionは符号だけを、weightsは集約の重み配分だけを表す、という2軸に整理されています。

不変条件#

direction は目的ごとに +1 または -1 でなければならず、dim、変数、探索空間の表現は互いに整合していなければなりません。これらが一致しないと、構築時の検証や評価時の候補処理が失敗します。

Problemの拡張#

独自Variableの実装方法#

Variable(ABC)は、lb/ubという2つのプロパティとrepair(x)というメソッドだけを要求します。 組み込みのContinuousVariable/IntegerVariable/CategoricalVariableは、いずれも自分の定義域へ値を射影するだけの薄い実装であり、これ以外の変数型(周期変数、対数スケール変数など)が必要な場合はVariableを直接継承すればよいです。

次の例は、値を切り詰めるのではなく境界で折り返す変数です。 角度のように、上限を超えた値が下限側から連続しているとみなしたい設計変数に使えます。

import numpy as np
from saealib import Variable


class PeriodicVariable(Variable):
    def __init__(self, lb: float, ub: float):
        self._lb = float(lb)
        self._ub = float(ub)

    @property
    def lb(self) -> float:
        return self._lb

    @property
    def ub(self) -> float:
        return self._ub

    def repair(self, x):
        span = self._ub - self._lb
        return self._lb + np.mod(np.asarray(x, dtype=float) - self._lb, span)

ContinuousVariable.repair()がnp.clipで範囲外の値を境界に留めるのに対し、この実装はnp.modで範囲外の値を反対側の境界から巻き戻します。 Variableが表す値は、Population配列上で扱われる「エンコード済みfloat空間」上の値である点に注意してください。 CategoricalVariableのようにカテゴリ値と内部インデックスの対応が必要な変数型では、repair()はインデックス空間上で完結させ、実際のカテゴリ値への変換は別のメソッドで行います。

外部ライブラリアダプタ#

PymooProblem(pymoo_problem, *, eq_tolerance=1e-6, **problem_kwargs)は、構築済みのpymoo問題インスタンス(組み込みベンチマークや既存の研究コード)をProblemとしてラップし、problem_kwargs(comparator、handler、eps_cv、eps_obj)はProblem.__init__へそのまま渡されます。 pymooの問題は常に最小化なのでdirectionは常に全て-1になり、pymooの不等式制約(G)はInequalityConstraintへ、等式制約(H)はEqualityConstraintへ、いずれも符号を変えずにそのままマッピングされます。

pymooの問題はバッチ評価(_evaluate(X, out))を前提としており、PymooProblem.evaluate_batchはラップした問題自身のevaluate(X, return_as_dictionary=True)をバッチ全体に対して1回だけ呼び出すことで、これを直接活用します。 SerialEvaluatorは利用可能な場合これを自動的に使用します。 一方、SerialEvaluatorを介さずProblem.evaluate/evaluate_constraintsを直接呼んで個体単位で評価する呼び出し元に対しては、xをキーとする1スロットのキャッシュにより、候補ごとにpymoo呼び出しは正確に1回で済みます。evaluate_constraints()とevaluate()は同じxに対して連続して呼ばれるため、キャッシュがなければ制約の数だけpymoo呼び出しが発生し、さらに目的関数の分も加わってしまいます。

pymoo extraのインストールについてはインストールを参照してください。

関連コンポーネント#

  • Comparator:comparator引数で解の比較方法を差し替える

  • ConstraintHandler:handler引数で制約違反の扱い方を差し替える

  • Crossover / Mutation:variablesで定義した変数型ごとに適用される演算子

  • 制約付き最適化:制約の定義方法と組み込みのConstraintHandlerの選び方

参照#