ブラックボックス最適化と流体シミュレーションによる翼形状の最適化¶
航空機をはじめとする様々な輸送機器や各種エネルギー装置においては、様々な『翼』が使われており、この断面形状(翼型)は、これらの工業製品の性能や効率に対して大きな影響を有します。
一般的に、翼型の最適化は、揚力(またはダウンフォース)の最大化及び抵抗力の最小化を目的として様々な手法(試行錯誤)により実施することができます。しかし、翼型の評価(揚力や抵抗力の取得)には実験や流体シミュレーションが必要で、1回の試行における評価コスト(時間や費用)は比較的高いと考えられます。従って、複数のパラメータに基づく翼型を全探索的に最適化するのは困難です。
本チュートリアルでは、Amplify を用いた FMQA により、限られた評価回数での翼型の最適化を実施します。最適化過程では、流れ場に設置された翼に作用する揚力 $F_L$ と抗力 $F_D$ の比(揚抗比):
$$ r_{LD} = F_L/F_D $$
の最大化を目的とします。翼型の構築手法として、様々な方法が考えられますが、今回、翼形は、複素平面上の円に ジューコフスキー変換という写像を適用することで構築します。また、目的関数として揚抗比の負値($-r_{LD}$)を用い、目的関数値の取得(評価)には、流体シミュレーションを用います。
また、今回は入力変数として整数を取る非バイナリ変数を対象に最適化を実施します。目的関数や獲得関数を QUBO 式で記述するために、非バイナリ変数からバイナリ決定変数に変換する必要があります。この目的のために、本サンプルプログラムでは one-hot エンコーディングを採用し、必要となるエンコーダー・デコーダーの実装も紹介します。
ブラックボックス最適化や FMQA の基本知識については、『量子アニーリング・イジングマシンによるブラックボックス最適化』をご覧ください。また、FMQA を活用した他の応用ケースとして、『ブラックボックス最適化による化学プラントにおける生産量最大化』も紹介されていますので、ご覧ください。
本ノートブックは以下の構成となっています。
- 1. 翼型の定義及び流体シミュレーション
- 1.1. 翼型構築クラスの説明
- 1.2. 流体解析クラスの説明
- 2. FMQA のプログラム実装(整数入力値)
- 2.1. 乱数の初期化
- 2.2. クライアントの設定
- 2.3. PyTorch による FM の実装
- 2.4. 初期教師データの作成
- 2.5. FMQA サイクルの実行クラス(非バイナリ決定変数)
- 2.6. One-hot エンコーダー・デコーダーの実装
- 3. FMQA による最適翼型の探索
- 3.1. FMQA の実行
- 3.2. FMQA 最適化過程における目的関数値の推移
- 3.3. 本サンプルコードによる FMQA 実行例
- 3.4. 補足
※本オンラインデモ & チュートリアル環境では、連続実行時間が20分程度に制限されています。必要に応じて、最適化サイクル回数 $N$ (後述)を小さくして実行したり、本サンプルプログラムをご自身の環境にコピーしていただき、実行してください。その場合、翼生成及び流体シミュレーションに関する以下のライブラリを適宜ダウンロードし、次のようなディレクトリ構成で保存し、サンプルコードを実行してください。
├ fmqa_3_aerofoil.ipynb(本サンプルプログラム)
└ utils/
├ __init__.py
(空ファイル)
├ joukowski.py
└ lbm.py
1. 翼形状の定義及び流体シミュレーション¶
本サンプルプログラムでは、次の翼型生成クラスと流体解析クラスが用意されています。
- 翼型生成クラス・・・
joukowski.WingGenerator
- 流体解析クラス・・・
lbm.Solver
本節では、予め用意されたこれらのクラスを用いて、ある翼型周りの流体シミュレーションを実施し、今回の FMQA における目的関数に直接関係する、揚抗比 $r_{L/D}$ の取得方法を説明します。
1.1. 翼型構築クラスの説明¶
本サンプルプログラムでは、翼型の構築手法として、ジューコフスキー変換を用います。ジューコフスキー変換は、
$$ z(x,y) = \zeta(\xi, \eta) + \frac{c^2}{\zeta(\xi, \eta)} $$
で記述され、$\zeta\rightarrow z$ の2つの複素平面の写像を表します。本サンプルプログラムで考慮する翼型は、$\zeta$ 平面において中心が $(\xi_0, \eta_0)$ で点 $(c,0)$ を通る円にジューコフスキー変換を適用したものを、$z$ 平面上で $\alpha$ だけ回転させたものです。$\xi_0$, $\eta_0$, $\alpha$ はパラメータで、それぞれ翼厚、反り、迎角に対応します。
さっそく、翼型生成クラスである joukowski.WingGenerator
を使い、翼型を生成してみましょう。写像に関するパラメータ $(\xi_0, \eta_0,
\alpha)$ は実数ですが、離散最適化を行うために、各パラメータを有限個の候補の中から選ぶことにします。このような操作を整数インデックス化と呼びます。
from utils import joukowski
# 翼型生成クラス、joukowski.WingGenerator のインスタンス化
wing_generator = joukowski.WingGenerator()
# 翼型生成パラメータの整数インデックステーブルの表示
wing_generator.show_index_table()
wing_generator.show_index_table()
により、翼型生成に用いられる整数インデックスとパラメータ値 $(\xi_0, \eta_0,
\alpha)$
との対応を確認することができます。例えば、$\xi_0=5.50$, $\eta_0=3.75$, $\alpha=20.00$ deg
は、整数インデックスを用いて、$\xi_0[10]=5.50$、$\eta_0[15]=3.75$、$\alpha[20]=20.00$ deg
と指定できることが分かります。このパラメータを用いて、翼型を生成し、表示すると次のようになります。
様々な整数インデックス値を用いて翼型を構築し、その際の写像パラメータと翼形状を確認してみましょう。
# 翼型生成(写像変換)の実施。xi0, eta0, alpha は整数インデックスで指定。インデックスのテーブルは wing_generator.show_index_table() の出力参照
wing = wing_generator.generate_wing(10, 15, 20)
# 構築された翼型を表示
wing.draw()
# 変換の際の写像パラメータを表示
wing_generator.show_parameters("My wing")
1.2. 流体解析クラスの説明¶
今回、翼型の評価には、流体シミュレーションを用います。様々な流体シミュレーション手法が開発されていますが、本チュートリアルでは、計算コストと境界条件設定の容易さから、格子ボルツマン法による2次元流体解析手法を採用します。格子ボルツマン法による流体ソルバーは、流体解析クラス
lbm.Solver
から利用可能です。
例として、先ほど上で構築した翼型周りの流れのシミュレーションを以下のように実施します。与えられた条件では、一度のシミュレーションに20秒程度の計算時間を要しますので、ご注意ください。シミュレーション終了後の可視化図には、流速ベクトル(黒矢印)と翼型へ作用する流体力(揚力及び抵抗の合成力;赤矢印)が図示されます。また、色は渦度を示します。
今回は、翼に作用する揚力 $F_L$ と抗力 $F_D$ の比(揚抗比) $r_{LD} = F_L/F_D$ の最大化を目指しますので、目的関数として揚抗比の負値 $-r_{LD}$ を考慮し、これを最小化するような最適化を実施します。下記のシミュレーションの最後に $-r_{LD}$ を表示します。
from utils import lbm
# シミュレーション格子サイズ(nx:横、ny:縦)
nx, ny = 400, 160
# lbm.Solver クラスのインスタンス化。
solver = lbm.Solver(nx, ny)
# 上で構築した翼型 wing を境界条件(形状モデル)として追加
solver.add_model(wing)
# 時間ステップ 3000 回分のシミュレーション実施
solver.integrate(steps=3000)
# シミュレーション結果から、目的関数である揚抗比の負値を計算(今回の FMQA では、この値を最小化する)
cost = -solver.fy_average / solver.fx_average
# 目的関数の評価値を出力
print(f"{cost=:.2f}")
2. FMQA のプログラム実装(整数入力値)¶
ここでは、FMQA のプログラム実装を行います。FMQA 部分の実装の多くは、『量子アニーリング・イジングマシンによるブラックボックス最適化』と同様ですので、詳細はそちらの解説をご覧ください。
今回の FMQA では、バイナリ変数ではなく、整数ベクトルを目的関数への入力値として考慮している、という大きな相違点があることにご注意ください。FMQA における整数入力値の取り扱いは、『2.6. One-hot エンコーダー・デコーダーの実装』をご覧ください。
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()
client.parameters.timeout = 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}$ の決め方は様々ですが、乱数を用いたり、現象に対する知見に基づき機械学習に適した値を用いたりします。FMQA 毎に新規に初期教師データを構築する必要はなく、過去に実施した実験やシミュレーションの結果から、教師データを構築しても構いません。
今回は、非バイナリである整数入力値を考慮するため、乱数で整数入力ベクトルを生成し、さらに one-hot に基づいてバイナリベクトルに変換する関数
input_generator
を用います。input_generator
は、後に紹介する one-hot エンコーダー・デコーダークラス EncDecOneHot
内で定義されています。
def gen_training_data_flow_onehot(D: int, N0: int, true_func, input_generator):
X = np.zeros((N0, D))
y = np.zeros(N0)
print("")
print("###################################################")
print(" Making initial training data")
print("###################################################")
for i in range(N0):
print(f"Generating training data set #{i}.")
# (0 or 1)の要素からなる入力ベクトルをランダムに決定
x = input_generator()
# 既に全く同じ入力ベクトルが与えられている場合、入力値を再生成
is_identical = True
while is_identical:
is_identical = False
for j in range(i + 1):
if np.all(x == X[j, :]):
x = input_generator()
is_identical = True
break
# 目的関数 f(x) で入力値 x を評価し、入出力ペア (x,f(x)) を教師データにコピー
X[i, :] = x
y[i] = true_func(x)
print(f"------------------------")
return X, y
2.5. FMQA サイクルの実行クラス¶
FMQA_NB.cycle
では、事前に準備した初期教師データを用い、FMQA サイクルを $N-N_0$ 回実施します。FMQA_NB.step
は、FMQA を1サイクルのみ行う関数で、FMQA_NB.cycle
から $N-N_0$ 回呼び出されます。
非バイナリ変数を対象とした FMQA の場合、各変数をバイナリ変数にエンコードする必要があります。FMQA_NB
クラスでは、one-hot
エンコードを前提とします。非バイナリ変数のエンコード及びデコードは、それぞれ encoder_decoder.encoder
及び
encoder_decoder.decoder
として本クラスのインスタンス時に与えられます。また、nbins_array
は、各非バイナリ変数のエンコードに用いる総 bin 数(可変)のリストです。
from amplify import VariableGenerator, solve, one_hot, sum
import matplotlib.pyplot as plt
import sys
# FMQA for non-binary inputs
class FMQA_NB:
def __init__(self, D, N, N0, k, true_func, client, encoder_decoder) -> 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.nbins_array = encoder_decoder.nbins_array
self.encoder = encoder_decoder.encoder
self.decoder = encoder_decoder.decoder
self.y = None
# 教師データに基づいて N-N0 回の FMQA を教師データを追加しながら繰り返し実施するメンバー関数
def cycle(self, X, y, log=False) -> np.ndarray:
# one-hot 制約条件の重み係数
constraint_weight = max([1, int(np.abs(y).max() + 0.5)])
print("")
print("###################################################")
print(f" Starting FMQA cycles... {constraint_weight=}")
print("###################################################")
pred_x = X[0]
pred_y = 1e18
for i in range(self.N - self.N0):
print(f"FMQA Cycle #{i} ")
try:
x_hat = self.step(X, y, constraint_weight)
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, :]):
# バイナリベクトルから整数ベクトルへデコードし、inputs へコピー
inputs = self.decoder(x_hat)
# 周辺の値を取得(inputs は整数ベクトル)
inputs += np.random.randint(-1, 2, len(inputs))
for i_inp in range(len(inputs)):
if inputs[i_inp] < 0:
inputs[i_inp] = 0
elif inputs[i_inp] > self.nbins_array[i_inp] - 1:
inputs[i_inp] = self.nbins_array[i_inp] - 1
# 整数ベクトルからバイナリベクトルへエンコードし、x_hat へコピー
x_hat = self.encoder(inputs)
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("")
print(f"------------------------")
self.y = y
return pred_x
# 1回のFMQAを実施するメンバー関数
def step(self, X, y, constraint_weight) -> 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": 0.5},
lr_sche_class=torch.optim.lr_scheduler.StepLR,
lr_sche_params={"step_size": 50, "gamma": 0.9},
)
# 学習済みモデルから、FM パラメータの抽出
v, w, w0 = list(model.parameters())
v = v.detach().numpy()
w = w.detach().numpy()[0]
w0 = w0.detach().numpy()[0]
# ここから量子アニーリング (QA) を実施
gen = VariableGenerator() # 変数ジェネレータを宣言
q = gen.array("Binary", self.D) # 決定変数の作成
cost = self.__FM_as_QUBO(q, w0, w, v) # FM パラメータから QUBO として FM を定義
# 一つの変数が一つの値を持つように one-hot 制約
constraint_list = []
ista = 0
iend = 0
for i in range(len(self.nbins_array)):
iend += self.nbins_array[i]
constraint_list.append(one_hot(q[ista:iend]))
ista = iend
constraints = sum(constraint_list)
# 目的関数と制約条件を足し合わし、Amplify のソルバーに受け渡し
model = cost + constraint_weight * constraints
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 サイクル内で実施した N 回の目的関数評価値の履歴をプロットする関数
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 サイクル時の目的関数評価値
plt.xlabel("i-th evaluation of f(x)", fontsize=18)
plt.ylabel("f(x)", fontsize=18)
plt.tick_params(labelsize=18)
return fig
2.6. One-hot エンコーダー・デコーダーの実装¶
翼型クラスの generate_wing
メソッドを使う際、入力として整数インデックス(例
generate_wing(10, 15, 20)
)が必要です。一方、QUBO
への入力は、バイナリ変数である必要があるため、整数インデックスからバイナリ変数へのエンコーディングが必要となります。今回は、one-hot
エンコーディングを用います。例えば、1から4の値を取りうる整数を4ビットで表す one-hot エンコーディングでは、
- 1 は
[1, 0, 0, 0]
- 2 は
[0, 1, 0, 0]
- 3 は
[0, 0, 1, 0]
- 4 は
[0, 0, 0, 1]
のようにバイナリベクトルで表現されます。以下の EncDecOneHot
クラスでは、整数インデックスからバイナリ変数への変換・逆変換を行う関数(encoder()
、decoder()
)や、整数インデックス入力ベクトルを乱数で生成し、バイナリ変数行列へ変換する関数
gen_random_input()
が定義されています。
class EncDecOneHot:
def __init__(self, D: int, nbins_array):
self.D = D
self.nbins_array = nbins_array
def gen_random_input(self):
ista = 0
iend = 0
x = np.zeros(self.D, int)
for i in range(len(self.nbins_array)):
iend += self.nbins_array[i]
idx = np.random.randint(ista, iend, 1)[0]
x[idx] = 1
ista = iend
return x
def encoder(self, inputs):
if len(inputs) != len(self.nbins_array):
raise RuntimeError("inputs should be the same length as nbins_array!")
x = np.zeros(self.D, int)
i_offset = 0
for i in range(len(inputs)):
x[inputs[i] + i_offset] = 1
i_offset += self.nbins_array[i]
return x
def decoder(self, x):
bit_locs = np.where(x == 1)[0]
if len(bit_locs) != len(self.nbins_array):
raise RuntimeError("bit_locs should be the same length as nbins_array!")
i_offset = 0
for i in range(1, len(bit_locs)):
i_offset += self.nbins_array[i - 1]
bit_locs[i] -= i_offset
return bit_locs
3. FMQA による最適翼型の探索¶
3.1. FMQAの実行¶
それでは、1節で紹介した流体シミュレーションソルバーを目的関数として、2節で実装した FMQA により、揚抗比の負値を最小化する最適化を実施します。
以下のコードでは、目的関数を評価できる回数 $N$ を 5 回、そのうち初期データの生成のための評価回数 $N_0$ を 3 回としています。これは、最低限の動作確認のための設定であり、本来のブラックボックス最適化の為の評価回数の一例は、出力例『3.3. 本サンプルコードによる FMQA 実行例』をご覧ください。
出力例『3.3. 本サンプルコードによる FMQA 実行例』では、目的関数を評価できる回数 $N$ を40 回、そのうち初期データの生成のための評価回数 $N_0$ を20回としています。従って、$N-N_0=20$ 回、FMQA のサイクル(機械学習、量子アニーリング・イジングマシンによる最適解の求解、目的関数の評価)を実施します。この設定では、全 FMQA サイクルの終了までおよそ15~20分程度の時間を要しますので、ご注意ください。
# 乱数シード値を初期化
seed_everything()
# 翼型生成クラス、joukowski.WingGenerator のインスタンス化
wing_generator = joukowski.WingGenerator()
# 翼型生成パラメータの整数インデックステーブルの表示
wing_generator.show_index_table()
# 各変数の one-hot エンコーディングの bin サイズ
nbins_array = np.array(
[wing_generator.nbins_xi0, wing_generator.nbins_eta0, wing_generator.nbins_alpha]
)
# バイナリ変数の全体のサイズ(=QUBOが取り扱うバイナリ決定変数のサイズ)
D = wing_generator.nbins_xi0 + wing_generator.nbins_eta0 + wing_generator.nbins_alpha
# One-hot エンコーダー・デコーダークラス(EncDecOneHot)のインスタンス化
enc_dec_one_hot = EncDecOneHot(D, nbins_array)
# 目的関数評価回数のカウンター
n_eval = 0
# 目的関数(翼型の構築 → シミュレーション実施 → 目的関数値の計算)
def obj_func(x):
global n_eval
# バイナリ変数から整数へデコード(写像パラメータインデックス)
i_xi0, i_eta0, i_alpha = enc_dec_one_hot.decoder(x)
# 翼型生成(写像変換)の実施。xi0, eta0, alpha を整数インデックスで指定。
# インデックスのテーブルは wing_generator.show_index_table() の出力参照
wing = wing_generator.generate_wing(i_xi0, i_eta0, i_alpha)
# 変換の際の写像パラメータを表示
wing_generator.show_parameters("My wing")
# シミュレーション格子サイズ(nx:横、ny:縦)
nx, ny = 400, 160
# lbm.Solver クラスのインスタンス化。
solver = lbm.Solver(nx, ny)
# 上で構築した翼型 wing を境界条件(形状モデル)として追加
solver.add_model(wing)
# 時間ステップ 5000 回分のシミュレーション実施
solver.integrate(steps=5000)
# 揚抗比の負値
cost = -solver.fy_average / solver.fx_average
# 流体シミュレーションが発散した場合の処理
if np.isnan(cost):
cost = 0
print(f"#{n_eval} evaluation finished. cost:{cost:.2f}")
n_eval += 1
return cost
N = 5 # 関数を評価できる回数。『3.3. 本サンプルコードによる FMQA 実行例』では 40 と設定
N0 = 3 # 初期教師データのサンプル数。『3.3. 本サンプルコードによる FMQA 実行例』では 20 と設定
k = 20 # FMにおけるベクトルの次元(ハイパーパラメータ)
# 初期教師データの生成
X, y = gen_training_data_flow_onehot(D, N0, obj_func, enc_dec_one_hot.gen_random_input)
# FMQA のインスタンス化
fmqa_solver = FMQA_NB(D, N, N0, k, obj_func, client, enc_dec_one_hot)
# FMQA サイクルの実行
pred_x = fmqa_solver.cycle(X, y)
# 最適化結果の出力
print("###################################################")
print(" Optimal input values and corresponding output")
print("###################################################")
print("pred x:", pred_x)
print("pred value:", obj_func(pred_x))
3.2. FMQA 最適化過程における目的関数値の推移¶
初期教師データ作成時にランダムに生成した入力値に対して得られた $N_0$ 個の目標関数値及び $N-N_0$ サイクルの FMQA 最適化過程における目標関数値の推移を以下にプロットします。それぞれ、青色及び赤色で示されています。また、出力例を、『3.3. 本サンプルコードによる FMQA 実行例』に紹介しています。十分大きな $N$ を条件とした場合、FMQA 最適化サイクルにより得られた入力値 $\hat{x}$ により、目的関数値の最小値が平均的に次々と更新される様子が示されます。
ここで、FMQA サイクル中であっても、比較的大きな目的関数値を出力するような入力値 $\hat{x}$ を推定する場合があります。これには様々な理由がありますが、主に
- 現在の教師データに新たに探索された入力値と同じ入力値が含まれる場合、その近傍に位置する入力値をランダムに探索し、$\hat{x}$
とし、目的関数を評価している(
FMQA_NB.cycle
参照)、 - ある FMQA サイクル時点における獲得関数 $g(\boldsymbol{x})$ の精度が十分高くない、
等の理由が考えられます。いずれにしても、ブラックボックス最適化により効率的に翼型の最適化を実施することができました!
fig = fmqa_solver.plot_history()
3.3. 本サンプルコードによる FMQA 実行例¶
一般的に、FixstarsClient
で採用されているヒューリスティクスというアルゴリズムの原理上、得られる解に完全な再現性はありませんが、本サンプルコードを実行した際に得られる、典型的な標準出力及び画像出力を以下に紹介します。※得られる値が異なる場合があります。
以下では、評価回数や初期教師データ数を次のように設定し、実行しています。
N = 40 # 関数を評価できる回数 N0 = 20 # 初期教師データのサンプル数
-
『3.1. FMQAの実行』に記載のコードを上の $N$, $N_0$ で実行すると、次のような標準出力が逐次表示されます。
-
まず、初期教師データ作成中に生成される、乱数に基づく入力値に対して構築された翼型とその流体シミュレーション結果が逐次出力されます($N_0$ 回)。
[Index table] 0 1 2 3 4 5 6 7 8 9 10 11 xi0[i] 1.0 1.45 1.9 2.35 2.8 3.25 3.7 4.15 4.6 5.05 5.5 5.95 \ eta0[i] 0.0 0.25 0.5 0.75 1.0 1.25 1.5 1.75 2.0 2.25 2.5 2.75 alpha[i] 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12 13 14 15 16 17 18 19 20 21 22 xi0[i] 6.4 6.85 7.3 7.75 8.2 8.65 9.1 9.55 - - - \ eta0[i] 3.0 3.25 3.5 3.75 4.0 4.25 4.5 4.75 5.0 5.25 5.5 alpha[i] 12.0 13.0 14.0 15.0 16.0 17.0 18.0 19.0 20.0 21.0 22.0 23 24 25 26 27 28 29 30 31 32 33 xi0[i] - - - - - - - - - - - \ eta0[i] 5.75 6.0 6.25 6.5 6.75 7.0 7.25 7.5 7.75 8.0 8.25 alpha[i] 23.0 24.0 25.0 26.0 27.0 28.0 29.0 30.0 31.0 32.0 33.0 34 35 36 37 38 39 xi0[i] - - - - - - eta0[i] 8.5 8.75 9.0 9.25 9.5 9.75 alpha[i] 34.0 35.0 36.0 37.0 38.0 39.0 ################################################### Making initial training data ################################################### Generating training data set #0. My wing: xi0[12]=6.40, eta0[0]=0.00, alpha[3]=3.00deg
#0 evaluation finished. cost:-0.53 ------------------------ Generating training data set #1. My wing: xi0[3]=2.35, eta0[39]=9.75, alpha[9]=9.00deg
#1 evaluation finished. cost:-1.86 ------------------------
・
・
・ -
初期教師データ構築が終了後、$N-N_0$ 回の FMQA サイクルが始まります。サイクル中は、FMQA の一度の試行ごとに次のような出力が逐次表示されます。
################################################### Starting FMQA cycles... constraint_weight=2 ################################################### FMQA Cycle #0 My wing: xi0[6]=3.70, eta0[23]=5.75, alpha[24]=24.00deg
#20 evaluation finished. cost:-2.09 variable updated, pred_y=-2.0937129113571267 ------------------------ FMQA Cycle #1 My wing: xi0[3]=2.35, eta0[23]=5.75, alpha[24]=24.00deg
#21 evaluation finished. cost:-1.95 ------------------------
・
・
・ -
$N-N_0$ 回の FMQA サイクルの終了後、得られた最適と考えられるバイナリ入力ベクトルと共にそれに基づいて構築された翼型周りの流体シミュレーション結果及び目的関数値が次のように出力されます。
################################################### Optimal input values and corresponding output ################################################### pred x: [0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] My wing: xi0[3]=2.35, eta0[39]=9.75, alpha[15]=15.00deg
#40 evaluation finished. cost:-2.74 pred value: -2.735594392729547
-
-
『3.2. FMQA 最適化過程における目的関数値の推移』に記載の
fmqa_reactor.plot_history()
による出力画像は次のようになり、ブラックボックス最適化過程において、揚抗比の負値の最小値が次々更新される様子が示されています。ブラックボックス最適化により得られた最適翼型の揚抗比は 2.74 となり、ランダム探索(初期教師データ構築)過程で取得された翼型の最大揚抗比(2.35)に比べて 17% 向上しています。
3.4. 補足¶
本サンプルプログラムをそのまま実行する場合、得られる最適翼の揚抗比 $r_{LD}$ はおよそ 2.7 となります。一般的な航空機における揚抗比は、巡行中では 15 程度と言われており、本ブラックボックス最適化で得られる最適翼に比べてより大きな揚抗比を示しています。この理由として、次の2つを紹介します。
-
ジューコフスキー変換
今回は簡便のため、3つのパラメータで翼型を定義できるジューコフスキー変換を用いましたが、本変換で得られる翼型は必ずしも高揚抗比を実現する翼型ではありません。
-
翼のアスペクト比
揚抗比を大きくするには、アスペクト比を大きく(より薄く長い翼)すれば良いのですが、薄く長い翼のシミュレーションには、より大きな解像度が必要で、そのためにはより大きな計算コストが必要となります。今回は、手軽ににシミュレーション実施できる条件ということで、デフォルトで取り得る写像パラメータの範囲を適切に調整し、翼の長さに対して、比較的厚い翼形状(小アスペクト比)を考慮しています。
いずれにしても、本サンプルプログラムで考慮されたアスペクト比及びジューコフスキー変換の枠組みの中で、ブラックボックス最適化による最適翼形状の探索を実施することができています。