ブラックボックス最適化による化学プラントにおける生産量最大化¶
ブラックボックス最適化の応用例として、非線形物理現象を取り扱う数値シミュレーションを目的関数としたブラックボックス最適化に取り組みます。
ここでは、あるプラント反応装置における生産量の最大化に関する最適化を目的とします。反応装置を構成する反応器内の反応物や生成物の化学反応・輸送現象は、有限差分法による数値シミュレーションによって予測され、その予測結果に基づいて、反応器における物質生成量を最大化するような最適化を行います。
なお、本サンプルで用いるシミュレーションコードは、Python
で実装されているため、後で紹介する FMQA
クラスから直接呼ぶことができますが、そうではない場合(例:FMQA実行とシミュレーション実行のマシンが異なる場合や、目的関数が実験計測に基づく場合)でも、本サンプルコードをほぼそのまま活用いただけます。
ブラックボックス最適化や FMQA の基本知識については、『量子アニーリング・イジングマシンによるブラックボックス最適化』をご覧ください。また、FMQA を活用した他の応用ケースとして、『ブラックボックス最適化と流体シミュレーションによる翼形状の最適化』も紹介されていますので、ご覧ください。
本ノートブックは以下の構成となっています。
- 1. 問題設定
- 1.1. モデル反応器と最適化対象
- 1.2. シミュレーターの説明
- 1.3. 反応器シミュレーションの実装
- 1.4. シミュレーション実行と目的関数の定義
- 2. FMQA のプログラム実装
- 2.1. 乱数の初期化
- 2.2. クライアントの設定
- 2.3. PyTorch による FM の実装
- 2.4. 初期教師データの作成
- 2.5. FMQA サイクルの実行クラス
- 3. FMQA による最大生産量を実現する初期条件探索
- 3.1. FMQA の実行
- 3.2. FMQA 最適化過程における目的関数値の推移
- 3.3. 本サンプルコードによる FMQA 実行例
1. 問題設定と目的関数¶
本サンプルコードにおける問題設定と目的関数として用いる反応器のシミュレーションについて説明します。ただし、FMQA においては、目的関数をブラックボックスとして取り扱うため、本シミュレーションの理解は必ずしも必要ではありません。
1.1. モデル反応器と最適化対象¶
上図のような、反応性物質 A の初期濃度分布のみで制御される化学反応器があります。この反応器では、A の初期濃度分布に基づいた反応速度で、化学反応 $A \rightarrow B$ が起こり、商品である物質 B が生成されます。
生産性向上のため、A の初期濃度分布を適切に決定し、予め決められた生産時間 $\Delta t_{prod}$ 内で生成される $B$ の総量を最大化することが今回の最適化の目的です(総量の負値を最小化する問題、と考えてください)。
1.2. シミュレーターの説明¶
今回は、実際の3次元空間での反応器を考慮するのではなく、シミュレーションコストの観点から1次元反応器を対象とします。1次元反応器とは、例えば、
- 細長い箱の中の化学反応や、
- 1次元をxの方向として、y方向・z方向に均質な3次元空間の表現
などと見なすことができます。反応器の運転条件に対する最適化、という本サンプルプログラムの本質は、考慮する次元に依存しません。
反応器内の化学反応・輸送現象は次の非線形偏微分方程式で記述されます。$C_A$ は物質 A の濃度、$C_B$ は物質 B の濃度を示します。
$$ \frac{\partial C_A}{\partial t} = \alpha \frac{\partial}{\partial x}\left(\frac{\partial C_A}{\partial x}\right) - \omega $$
$$ \frac{\partial C_B}{\partial t} = \alpha \frac{\partial}{\partial x}\left(\frac{\partial C_B}{\partial x}\right) + \omega $$
$$ \omega = R_r C_A (1-C_A) \exp{(-C_B)} $$
$$ C_B = O \text{ at } t=0(初期条件) $$
ここで、$C_{B,0}$ は、B の初期濃度分布を示し、初期値はゼロ行列(初期において B は存在しない)となります。A の初期濃度分布 $C_{A,0}$ が与えられた場合の生産時間 $\Delta
t_{prod}$ 内における B の総生成量は、シミュレーション開始から $\Delta t_{prod}$ 経過した時点での反応器における $C_B$
の空間積分値に相当し、次に説明するシミュレーションクラス
Reactor
で取得することができます。
1.3. 反応器シミュレーションの実装¶
上記の反応器における反応シミュレーションには、Reactor
クラス内の integrate
関数を用います。本シミュレーターでは、上記方程式群を有限差分法を用いて解いており、有限要素法や有限体積法による市販の物理シミュレーション・ソフトウェアと同様な処理を行います。
ここで、integrate
関数は、引数として A の初期濃度分布を受け取り、シミュレーション実施後に得られる B の総生成量(濃度分布の空間積分値)を返します。
import matplotlib.pyplot as plt
import numpy as np
import time
# ある反応性物質 (A->B) の濃度 に関する次の輸送方程式(偏微分方程式)を有限差分法で解くクラス。
class Reactor:
def __init__(self, nfolds=5, alpha=1.0, dt_prod=1e-3, rr=1e4):
self.nfolds = nfolds # シミュレーション領域の空間解像度を決定するパラメータ
self.alpha = alpha # 分子拡散係数
self.dt_prod = dt_prod # 予め決められた生産時間
self.rr = rr # 反応速度に関する係数
# 2次精度中心差分法により、分布 f の2階微分を求める関数(周期境界条件を仮定)
def __dfdx2(self, f, dfdx2):
dfdx2[0] = (f[1] - 2 * f[0] + f[self.nx - 1]) / self.dx / self.dx
dfdx2[self.nx - 1] = (
(f[0] - 2 * f[self.nx - 1] + f[self.nx - 2]) / self.dx / self.dx
)
dfdx2[1 : self.nx - 1] = (
np.array([f[i + 1] - 2 * f[i] + f[i - 1] for i in range(1, self.nx - 1)])
/ self.dx
/ self.dx
)
return dfdx2
# 場の初期条件を決定
def __init_field(self, x):
self.nx = self.nfolds * len(x) # 空間離散化で用いる格子点数
self.dx = 1.0 / (self.nx - 1) # 格子点間隔
self.concn_A = np.zeros(self.nx) # Aの濃度分布
self.concn_B = np.zeros(self.nx) # Bの濃度分布
self.x_cord = np.array(
[i / self.nx - 0.5 for i in range(self.nx)]
) # 離散点の座標系
self.concn_A = np.array(
[float(x[i]) for i in range(len(x)) for j in range(self.nfolds)]
) # Aの初期場を作成
# A の初期分布 init_A に応じて、輸送方程式を dt_prod の物理時間だけ時間発展させ、B の総生成量を返す関数
def integrate(self, init_A, fig=False):
self.__init_field(init_A)
start = time.perf_counter()
omega = np.zeros(self.nx)
dfdx2 = np.zeros(self.nx)
dt = 0.25 * self.dx * self.dx / self.alpha # オイラー法での時間ステップ幅
lts = int(self.dt_prod / dt)
if fig: # 反応経過のプロット
fig = plt.figure(figsize=(6, 4))
plt.tick_params(labelsize=16)
plt.xlabel("x", fontsize=16)
plt.ylabel("Concentration", fontsize=18)
plt.plot(
self.x_cord,
self.concn_A,
linestyle="-",
linewidth=1,
color=[0.6, 0.6, 0.6],
label="$C_{A,0}$",
)
self.iter = 0
while self.iter * dt < self.dt_prod:
if fig and any(
[
self.iter == i
for i in [0, int(0.1 * lts), int(0.2 * lts), int(0.4 * lts)]
]
): # 反応経過のプロット
plt.plot(
self.x_cord, self.concn_B, linestyle="-", linewidth=2, color="r"
)
omega = (
self.rr * np.exp(-self.concn_B) * self.concn_A * (1.0 - self.concn_A)
) # 反応速度
self.concn_A = (
self.concn_A
+ (self.alpha * self.__dfdx2(self.concn_A, dfdx2) - omega) * dt
) # Aの濃度に関する時間発展
self.concn_B = (
self.concn_B
+ (self.alpha * self.__dfdx2(self.concn_B, dfdx2) + omega) * dt
) # Bの濃度に関する時間発展
self.iter += 1
if fig: # 反応経過のプロット
plt.plot(
self.x_cord,
self.concn_B,
linestyle="-",
linewidth=4,
color="r",
label="$C_B$",
)
plt.legend(fontsize=16)
self.cpu_time = time.perf_counter() - start # シミュレーションに要した時間計測
return np.sum(self.concn_B) * self.dx # 濃度の簡易的な空間積分
1.4. シミュレーション実行と目的関数の定義¶
それでは、Reactor
クラスの integrate
関数による反応シミュレーションを実行し、乱数により設定した A の初期濃度分布
$C_{A,0}$
を与えた場合の、生産時間内における B の総生成量を取得してみましょう。integrate
関数の第1引数は、1次元空間内における $C_{A,0}$
の分布を表現する1次元バイナリ配列(つまり、各座標において $C_{A,0}$ は、0又は1を取る)です。第2引数は、オプションの結果画像の出力フラグです(デフォルトでは
False
)。
実行すると乱数に基づいて決定された $C_{A,0}$ に応じて次のような結果画像が得られます。
上に示す結果画像は、A の初期濃度分布 $C_{A,0}$(灰色)及び、各時刻における B の濃度分布 $C_B$(赤色)を示します。B の濃度分布は、時刻が $t=0$(初期濃度分布、一番下の赤線)、$t=0.1 \Delta t_{prod}$(下から2番目の赤線)、$t=0.2 \Delta t_{prod}$(下から3番目の赤線)、$t=0.4 \Delta t_{prod}$(下から4番目の赤線)、$t = \Delta t_{prod}$(生産時間終了時刻、一番上の赤太線)となっています。今回、最適化によって最大化したいのは、反応器内における赤い太線の積分値となります。
時刻0では、全領域で $C_B=C_{B,0}=0$ ですが、時間とともに、化学反応が進行し、B が生成($C_B$ の増加)されています。生産時間終了時刻における B の最終的な濃度分布は赤太線で示されています。反応物 A の時間変化は示されていませんが、時間の経過と共に化学反応と分子拡散により徐々に減少していると推測されます。
以下のコードでは、乱数シード値を固定していないので、実行毎に $C_{A,0}$ が代わり、それに応じて B の総生産量も変化します。B の総生産量の最大化を実現する A の初期濃度分布 $C_{A,0}$ は、どの様な分布になるか想像できるでしょうか?
# C_A,0 を表現する1次元バイナリ配列を乱数により生成
# 1次元空間を100個の微小領域に離散化し、C_Aの初期分布を表現する
# 乱数シードを固定していないので、c_a0 の中身が実行毎に変わる
c_a0 = np.random.randint(0, 2, 100)
# Reactor クラスの integrate 関数により、反応シミュレーションを実施(オプションで結果画像の出力を選択)
amount_B = Reactor().integrate(c_a0, fig=True)
# 取得された B の総生成量を表示
print(f"Total amount of B: {amount_B:.2f}")
上記コードのように、反応器の空間を100個の微小体積で離散化する場合、A の初期濃度分布 $C_{A,0}$ ベクトルが取り得る値は、$2^{100} \sim 10^{30}$
通り程度存在します。また、1.2節に記載の反応速度式 $\omega$ の式から、時間内における B の生産量最大化には、単に反応器内の全領域を A
で満たせばよい($C_{A,0} =
$[1, 1, 1,..., 1]
)というわけではなく、できるだけ多くの A で満たしつつ、$C_A=0$
である局所領域を適切に配置するような工夫が必要となっています。このような系は、
- 探索空間が広く全探索はシミュレーションの時間的コストから非現実的
- 目的関数が未知関数(非線形偏微分方程式で記述される)であり、ブラックボックス
ということから、FMQA の活用が有効と考えられます。
また、反応器内に存在する初期に A の量によって、材料費が変わりますが、今回は、生成物 B の値段に比べ、反応物 A の費用は小さいとし、コストに対する材料費の影響は無視できるものとします。
目的関数として、シミュレーションから取得される B の総生産量の負値を返すような関数 my_obj_func
を定義します。これは、FMQA
による最適化が目的関数値を小さくするように実施されるからです。
# 目的関数(生成された物質 B の総量の負値を返す)
def my_obj_func(init_A, fig=False):
my_reactor = Reactor()
minus_total = -my_reactor.integrate(init_A, fig)
if fig:
# (オプション)目的関数値、integrate()の積分回数、シミュレーションに要したCPU時間を表示
print(f"{minus_total=:.2e}, {my_reactor.iter=}, {my_reactor.cpu_time=:.1e}s")
return minus_total
# 例①:あるバイナリベクトル c_a0(上のセルで定義)を入力したときの目的関数値
amount_B = my_obj_func(c_a0)
print(f"{amount_B=:.2f}")
# 例②:あるバイナリベクトル c_a0(上のセルで定義)を入力したときの目的関数値(ログ及び画像表示)
amount_B = my_obj_func(c_a0, fig=True)
print(f"{amount_B=:.2f}")
2. FMQA のプログラム実装¶
ここでは、FMQA のプログラム実装を行います。FMQA部分の実装は、『量子アニーリング・イジングマシンによるブラックボックス最適化』と同一ですので、詳細はそちらの解説をご覧ください。
2.1.乱数の初期化¶
実行毎に初期教師データや機械学習結果が変わらないようにするための、乱数seed値の初期化関数 seed_everything()
を定義します。
import os
import torch
import numpy as np
def seed_everything(seed=0):
os.environ["PYTHONHASHSEED"] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
from amplify import FixstarsClient
from datetime import timedelta
client = FixstarsClient()
timedelta(milliseconds=1000) # 1000 ミリ秒
# client.token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # ローカル環境等で使用する場合は、Amplify AEのアクセストークンを入力してください。
2.3.PyTorch による FM の実装¶
FM の学習と推論を PyTorch で行います。TorchFM
クラスでは、機械学習モデルとしての $g(\boldsymbol{x})$
を定義します。下式の通り、$g(\boldsymbol{x})$ 内の各項は、TorchFM
クラス内の
out_lin
、out_1
、out_2
、out_inter
に対応します。
$$ \begin{aligned} g(\boldsymbol{x} | \boldsymbol{w}, \boldsymbol{v}) &= \underset{\color{red}{\mathtt{out\_lin}}}{\underline{ w_0 + \sum_{i=1}^n w_i x_i} } + \underset{\color{red}{\mathtt{out\_inter}}}{\underline{\frac{1}{2}\left(\underset{\color{red}{\mathtt{out\_1}}}{\underline{ \sum_{f=1}^k\left(\sum_{i=1}^n v_{i f} x_i\right)^2 }} - \underset{\color{red}{\mathtt{out\_2}}}{\underline{ \sum_{f=1}^k\sum_{i=1}^n v_{i f}^2 x_i^2 }} \right) }} \end{aligned} $$
import torch.nn as nn
class TorchFM(nn.Module):
def __init__(self, d: int, k: int):
super().__init__()
self.V = nn.Parameter(torch.randn(d, k), requires_grad=True)
self.lin = nn.Linear(d, 1) # 右辺第1項及び2項は全結合ネットワーク
def forward(self, x):
out_1 = torch.matmul(x, self.V).pow(2).sum(1, keepdim=True)
out_2 = torch.matmul(x.pow(2), self.V.pow(2)).sum(1, keepdim=True)
out_inter = 0.5 * (out_1 - out_2)
out_lin = self.lin(x)
out = out_inter + out_lin
return out
次に、入出力データから FM を機械学習する関数 train()
を定義します。一般的な機械学習と同様に、教師データを学習データと検証データに分割し、学習データを用いてパラメータの最適化、検証データを用いて学習中のモデル検証を行います。train()
関数は、検証データに対して最も予測精度の高かったモデルを返します。
from torch.utils.data import TensorDataset, DataLoader
from sklearn.model_selection import train_test_split
from typing import Type
import copy
def train(
X: np.ndarray,
y: np.ndarray,
model_class: Type[nn.Module],
model_params: dict[str, int | float],
batch_size=1024,
epochs=3000,
criterion=nn.MSELoss(),
optimizer_class=torch.optim.AdamW,
opt_params={"lr": 1},
lr_sche_class=None,
lr_sche_params=None,
):
X_tensor, y_tensor = (
torch.from_numpy(X).float(),
torch.from_numpy(y).float(),
)
indices = np.array(range(X.shape[0]))
indices_train, indices_valid = train_test_split(
indices, test_size=0.2, random_state=42
)
train_set = TensorDataset(X_tensor[indices_train], y_tensor[indices_train])
valid_set = TensorDataset(X_tensor[indices_valid], y_tensor[indices_valid])
loaders = {
"train": DataLoader(train_set, batch_size=batch_size, shuffle=True),
"valid": DataLoader(valid_set, batch_size=batch_size, shuffle=False),
}
model = model_class(**model_params)
best_model_wts = copy.deepcopy(model.state_dict())
optimizer = optimizer_class(model.parameters(), **opt_params)
scheduler = None
if lr_sche_class is not None:
scheduler = lr_sche_class(optimizer, **lr_sche_params)
best_score = 1e18
for _ in range(epochs):
losses = {"train": 0.0, "valid": 0.0}
for phase in ["train", "valid"]:
if phase == "train":
model.train()
else:
model.eval()
for batch_x, batch_y in loaders[phase]:
optimizer.zero_grad()
out = model(batch_x).T[0]
loss = criterion(out, batch_y)
losses[phase] += loss.item() * batch_x.size(0)
with torch.set_grad_enabled(phase == "train"):
if phase == "train":
loss.backward()
optimizer.step()
losses[phase] /= len(loaders[phase].dataset)
with torch.no_grad():
model.eval()
if best_score > losses["valid"]:
best_model_wts = copy.deepcopy(model.state_dict())
best_score = losses["valid"]
if scheduler is not None:
scheduler.step()
with torch.no_grad():
model.load_state_dict(best_model_wts)
model.eval()
return model
2.4.初期教師データの作成¶
入力値 $\boldsymbol{x}$ に対して目的関数 $f(\boldsymbol{x})$ を評価し、$N_0$個の入出力ペア(初期教師データ)を作成します。ここでの入力値 $\boldsymbol{x}$ の決め方は様々ですが、乱数を用いたり、現象に対する知見に基づき機械学習に適した値を用いたりします。過去に実施した実験やシミュレーションの結果から、教師データを構築しても構いません。
def gen_training_data(D: int, N0: int, true_func):
assert N0 < 2**D
# N0個の入力値を乱数を用いて取得
X = np.random.randint(0, 2, size=(N0, D))
# 取得した入力値のうち重複しているものを除外し、除外した分の入力値を乱数を用いて追加
X = np.unique(X, axis=0)
while X.shape[0] != N0:
X = np.vstack((X, np.random.randint(0, 2, size=(N0 - X.shape[0], D))))
X = np.unique(X, axis=0)
y = np.zeros(N0)
# N0個の入力値に対応する出力値を目的関数を評価して取得
for i in range(N0):
if i % 10 == 0:
print(f"Generating {i}-th training data set.")
y[i] = true_func(X[i])
return X, y
2.5.FMQA サイクルの実行クラス¶
FMQA.cycle()
では、事前に準備した初期教師データを用い、FMQA サイクルを $N-N_0$ 回実施します。FMQA.step()
は、FMQA
を1サイクルのみ行う関数で、FMQA.cycle()
から $N-N_0$ 回呼び出されます。
from amplify import VariableGenerator, solve
import matplotlib.pyplot as plt
import sys
class FMQA:
def __init__(self, D: int, N: int, N0: int, k: int, true_func, client) -> None:
assert N0 < N
self.D = D
self.N = N
self.N0 = N0
self.k = k
self.true_func = true_func
self.client = client
self.y = None
# 教師データに基づいて N-N0 回のFMQAを教師データを追加しながら繰り返し実施するメンバー関数
def cycle(self, X, y, log=False) -> np.ndarray:
print(f"Starting FMQA cycles...")
pred_x = X[0]
pred_y = 1e18
for i in range(self.N - self.N0):
print(f"FMQA Cycle #{i} ", end="")
try:
x_hat = self.step(X, y)
except RuntimeError:
sys.exit(f"Unknown error, i = {i}")
# x_hat として既に全く同じ入力が教師データ内に存在する場合、その周辺の値を x_hat とする。
is_identical = True
while is_identical:
is_identical = False
for j in range(i + self.N0):
if np.all(x_hat == X[j, :]):
change_id = np.random.randint(0, self.D, 1)
x_hat[change_id.item()] = 1 - x_hat[change_id.item()]
if log:
print(f"{i=}, Identical x is found, {x_hat=}")
is_identical = True
break
# hat{x} で目的関数 f() を評価
y_hat = self.true_func(x_hat)
# 最適点近傍における入出力ペア [x_hat, y_hat] を教師データに追加
X = np.vstack((X, x_hat))
y = np.append(y, y_hat)
# 目的関数の評価値が最小値を更新したら、その入出力ペアを [pred_x, pred_y] へコピー
if pred_y > y_hat:
pred_y = y_hat
pred_x = x_hat
print(f"variable updated, {pred_y=}")
else:
print("")
# 全ての入力を全探索済みの場合は、for文を抜ける
if len(y) >= 2**self.D:
print(f"Fully searched at {i=}. Terminating FMQA cycles.")
break
self.y = y
return pred_x
# 1回のFMQAを実施するメンバー関数
def step(self, X, y) -> np.ndarray:
# FM を機械学習
model = train(
X,
y,
model_class=TorchFM,
model_params={"d": self.D, "k": self.k},
batch_size=8,
epochs=2000,
criterion=nn.MSELoss(),
optimizer_class=torch.optim.AdamW,
opt_params={"lr": 1},
)
# 学習済みモデルから、FM パラメータの抽出
v, w, w0 = list(model.parameters())
v = v.detach().numpy()
w = w.detach().numpy()[0]
w0 = w0.detach().numpy()[0]
# ここから量子アニーリング・イジングマシンによる求解を実施
gen = VariableGenerator() # 変数ジェネレータを宣言
q = gen.array("Binary", self.D) # 決定変数の作成
model = self.__FM_as_QUBO(
q, w0, w, v
) # FM パラメータから QUBO として FM を定義
result = solve(model, self.client) # 目的関数を Amplify のソルバーに受け渡し
if len(result.solutions) == 0:
raise RuntimeError("No solution was found.")
q_values = q.evaluate(result.best.values)
return q_values
# FM パラメータから QUBO として FM を定義する関数。前定義の TorchFM クラスと同様に、g(x) の関数形通りに数式を記述。
def __FM_as_QUBO(self, x, w0, w, v):
lin = w0 + (x.T @ w)
out_1 = np.array([(x * v[:, i]).sum() ** 2 for i in range(self.k)]).sum()
# 次式において、x[j] はバイナリ変数なので、x[j] = x[j]^2 であることに注意。
out_2 = np.array([(x * v[:, i] * v[:, i]).sum() for i in range(self.k)]).sum()
return lin + (out_1 - out_2) / 2
# 初期教師データ及び各 FMQA サイクル内で実施した i 回の目的関数評価値の履歴をプロットする関数
def plot_history(self):
assert self.y is not None
fig = plt.figure(figsize=(6, 4))
plt.plot(
[i for i in range(self.N0)],
self.y[: self.N0],
marker="o",
linestyle="-",
color="b",
) # 初期教師データ生成時の目的関数評価値(ランダム過程)
plt.plot(
[i for i in range(self.N0, self.N)],
self.y[self.N0 :],
marker="o",
linestyle="-",
color="r",
) # FMQA サイクル時の目的関数評価値(FMQA サイクル過程)
plt.xlabel("i-th evaluation of f(x)", fontsize=18)
plt.ylabel("f(x)", fontsize=18)
plt.tick_params(labelsize=18)
return fig
3.FMQA による最大生産量を実現する初期条件探索¶
3.1. FMQA の実行¶
それでは、1.4 節で紹介した反応器シミュレーションソルバーを目的関数として、2 節で実装した FMQA により、一定時間内での総生成量を最大化する(総生成量の負値を最小化する)最適化を実施します。
以下では、目的関数を評価できる回数 $N$ を30回、そのうち初期データの生成のための評価回数 $N_0$=20 回としています。従って、以下の例では、$N-N_0=10$ 回、FMQA のサイクル(機械学習、量子アニーリング・イジングマシンによる最適解の求解、目的関数の評価)を実施します。この設定では、最適化終了までおよそ2分程度の時間がかかります。
seed_everything() # 乱数シードを初期化
D = 100 # 決定変数の数(問題サイズ)
N = 30 # 目的関数の最大評価回数
N0 = 20 # 初期教師データのサンプル数
k = 20 # FM ハイパーパラメータ(FM パラメータ数に影響)
# 初期教師データの生成
X, y = gen_training_data(D, N0, my_obj_func)
# FMQA クラスのインスタンス化
fmqa_reactor = FMQA(D, N, N0, k, my_obj_func, client)
# FMQA サイクルの実行
pred_x = fmqa_reactor.cycle(X, y, log=True)
# 最適化結果の出力
print("pred x:", pred_x)
print("pred value:", my_obj_func(pred_x, fig=True))
3.2. FMQA 最適化過程における目的関数値の推移¶
初期教師データ作成時にランダムに生成した入力値に対して得られた $N_0$ 個の目標関数値及び $N−N_0$ サイクルの FMQA 最適化過程における目標関数値の推移を以下にプロットします。
それぞれ、青色及び赤色で示されています。FMQA 最適化サイクルにより得られた、その時点での最適と考えられる入力値(赤線)から、最小の目的関数値が次々と更新される様子が示されています。
一般的に、FixstarsClient
で採用されているヒューリスティクスというアルゴリズムの原理上、得られる解に完全な再現性はありませんが、上サンプルコード内のパラメータで求解された場合、得られる B の総生産量として 0.8 を超える解(A
の初期濃度分布)が得られます。ランダムな探索(青色)と比較し、生産量が向上していることが示されています。
fig = fmqa_reactor.plot_history()
3.3. 本サンプルコードによる FMQA 実行例¶
一般的に、FixstarsClient で採用されているヒューリスティクスというアルゴリズムの原理上、得られる解に完全な再現性はありませんが、本サンプルコードを実行した際に得られる、典型的な標準出力及び画像出力を以下に紹介します。※得られる値が異なる場合があります。
-
『3.1. FMQA の実行』に記載のコードを条件を変えずに実行すると、次のような標準出力が FMQA サイクルの進捗とともに逐次出力されます。また、最適化後の A の初期分布に基づくシミュレーション結果として、以下の画像が出力されます。
Generating 0-th training data set. Generating 10-th training data set. Starting FMQA cycles... FMQA Cycle #0 variable updated, pred_y=-0.0 FMQA Cycle #1 variable updated, pred_y=-0.7341318540009673 FMQA Cycle #2 variable updated, pred_y=-0.7836727189544249 FMQA Cycle #3 variable updated, pred_y=-0.7862647410081264 FMQA Cycle #4 FMQA Cycle #5 FMQA Cycle #6 FMQA Cycle #7 variable updated, pred_y=-0.8310535978823115 FMQA Cycle #8 FMQA Cycle #9 pred x: [1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 1. 1. 0. 1. 1. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 0. 1. 1. 1. 0. 1. 1. 1. 1. 1. 0. 1. 1. 1. 1. 1. 0. 1. 1. 1.] minus_total=-8.31e-01, my_reactor.iter=997, my_reactor.cpu_time=3.7e-01s pred value: -0.8310535978823115
-
『3.2. FMQA 最適化過程における目的関数値の推移』に記載の
fmqa_reactor.plot_history()
による出力画像は次のようになります。