背景¶
ブラックボックス最適化¶
FMQA は、ブラックボックス最適化手法の一つです。通常、数理最適化では、何らかの目的関数 $y = f(\boldsymbol{x})$ を最小化(あるいは最大化)するような決定変数 $\boldsymbol{x}$ の組を求めることを目的とします。ここで、$\boldsymbol{x}$ はサイズが $d$ で各要素が 0 または 1 の値をとるバイナリ変数ベクトルを仮定します。
$$ \begin{aligned} \mathrm{Minimize}&\,\, y = f(\boldsymbol{x}) \\ \mathrm{subject\,\,to\,\,}&\boldsymbol{x} \in [0,1]^d \end{aligned} $$
ここで、目的関数 $y = f(\boldsymbol{x})$ に関する情報(関数形、勾配、劣モジュラ性、凸性等)が分かっている場合、効率的な最適化が可能です。 例えば、Amplify のデモ・チュートリアルで紹介しているいくつかの最適化問題のように、$f(\boldsymbol{x})$ の関数が既知(かつ $\boldsymbol{x}$ の2次式)の場合、$f(\boldsymbol{x})$ を目的関数とすることで、直接、二次制約なし二値最適化(QUBO: Quadratic Unconstrained Binary Optimization)としての最適化実施が可能です。
一方、物理現象や社会現象に対するシミュレーションや実験によって得られる値を最小化(または最大化)する最適化の場合、目的関数 $f(\boldsymbol{x})$ はシミュレーションあるいは実験ということになり、目的関数を具体的な式で記述することはできません。このような未知の目的関数 $f(\boldsymbol{x})$ に対して行う数理最適化のことをブラックボックス最適化と呼びます。また、そのような目的関数の評価(シミュレーションや実験の実施)には、一般的に比較的大きなコストが必要なため、決定変数の集合が有限であっても全検索による最適化は困難な場合が多く、できるだけ少ない目的関数の評価回数での最適化が要求されます。
FMQA の概要¶
FMQA は、機械学習と量子アニーリングを組み合わせたブラックボックス最適化手法です。以下の図のようなサイクルを繰り返し、ブラックボックス関数の 2 次多項式による良い近似と最小値を与える入力の両方を同時に探索していく手法です。
まず、ブラックボックス関数を近似するような 2 次多項式を機械学習を用いて計算します。次に、イジングマシンを用いてその 2 次多項式が最小となるような入力 $x$ を求めます。そのあと、イジングマシンによって求めた $x$ をブラックボックス関数に入力します。機械学習により得られた 2 次多項式がブラックボックス関数を十分に良く近似できていれば、$\boldsymbol{x}$ はブラックボックス関数に入力しても、小さな値を出力することが期待できます。そうでない場合も、ブラックボックス関数を評価したデータを機械学習の教師データに追加して再度機械学習を行うことで、次の学習でより良いブラックボックス関数の多項式近似が得られることが期待されます。
ブラックボックス関数の 2 次多項式近似は Factorization Machine (FM) と呼ばれるモデルを使用します。FM は以下のような多項式で表されるモデルです。ここで、$d$ はブラックボックス関数の入力の長さを表す定数、$\boldsymbol{v}$、$\boldsymbol{w}$、$w_0$ はモデルのパラメータ、$k$ はパラメータのサイズを表すハイパーパラメータです。
$$ \begin{aligned} f(\boldsymbol{x} | \boldsymbol{w}, \boldsymbol{v}) &= w_0 + \langle \boldsymbol{w}, \boldsymbol{x}\rangle + \sum_{i=1}^d \sum_{j=i+1}^d \langle \boldsymbol{v}_i, \boldsymbol{v}_j \rangle x_i x_j \\ &=w_0 + \sum_{i=1}^d w_i x_i + \sum_{i=1}^d \sum_{j=i+1}^d \sum_{f=1}^k v_{if}v_{jf}x_ix_j \\ &=w_0 + \sum_{i=1}^d w_i x_i + \frac{1}{2}\sum_{f=1}^k\left(\left(\sum_{i=1}^d v_{i f} x_i\right)^2 - \sum_{i=1}^d v_{i f}^2 x_i^2\right) \end{aligned} $$
機械学習モデルとして FM を用いることに以下のような利点があります。
- モデルが 2 次多項式であるため、イジングマシンによる最小化が可能
- モデルの推論の計算量をパラメータで設定可能
ハイパーパラメータ $k$ はブラックボックス関数の入力の長さ $d$ 以下の正の整数であり、FM モデルのパラメータ数を調整する効果があります。$k=d$ のとき、モデルには QUBO の相互作用項と同じ自由度がある一方、$k$ を小さくすることでパラメータ数を減らし過学習を抑制することができます。
FMQA の手順¶
FMQA は、以下のように初期教師データの準備を行い、機械学習モデルによる推論と最適化を繰り返します。
- 初期教師データの準備
- 初期教師データとして、$N_0$ 個の入力サンプル $\{\boldsymbol{x}_1, \boldsymbol{x}_2, \cdots, \boldsymbol{x}_{N_0}\}$ と、対応する $N_0$ 個の出力 $\{f(\boldsymbol{x}_1), f(\boldsymbol{x}_2), \cdots, f(\boldsymbol{x}_{N_0})\}$ を用意する
- FMQA による最適化サイクルの実行
- Factorization Machine により機械学習モデルを作成する
- 教師データを用いて 1 で作成したモデルを学習させる
- 学習済みモデルに対して、イジングマシンを用いてその最小値を与える入力 $\hat{\boldsymbol{x}}$ を得る
- ブラックボックス関数 $f(\boldsymbol{x})$ を評価することで $\hat{y} = f(\hat{\boldsymbol{x}})$ を求め教師データに
$(\hat{\boldsymbol{x}}, \hat{y})$ を追加する
上記 1-4 を $N$ 回繰り返す。
本サンプルコードでは、FMQA を PyTorch および Amplify SDK を用いて実行する方法を紹介します。ただし、学習済みモデルの最小化を行う部分においては、量子アニーリング (QA) ではなく、GPU を用いたアニーリングマシンである Fixstars Amplify Annealing Engine (Amplify AE) を用います。
ここからは、実際に FMQA によりブラックボックス最適化を行うサンプルコードを説明します。
ソルバークライアントの設定¶
まず、FMQA の最適化サイクル中に用いるソルバーを設定します。本サンプルプログラムでは、Amplify AE を用います。
from amplify import FixstarsClient
from datetime import timedelta
# ソルバークライアントを Amplify AE に設定
client = FixstarsClient()
# ローカル環境等で実行する場合はコメントを外して Amplify AEのアクセストークンを入力してください
# client.token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 最適化の実行時間を 2 秒に設定
client.parameters.timeout = timedelta(milliseconds=2000)
ブラックボックス関数の定義¶
それでは、ブラックボックス最適化の対象となるブラックボックス関数を定義します。ブラックボックス関数として使用できるのは、0 または 1 の値からなるバイナリ変数の 1 次元配列を入力とし、実数を出力とする関数です。ただし、整数や実数からなる入力配列も考慮可能です。そのような活用例はこちらに紹介しています。
実用的には、使用するブラックボックス関数として、物理現象や社会現象に対するシミュレーションを行う関数や、実験によって得られる値を返す関数などが考えられます。これらは数式で表せず、性質も明らかでないため、ブラックボックス最適化に適しています。
しかし、今回のチュートリアルでは、シミュレーションや実験の代わりに、適当な関数をブラックボックス関数として用意します。以下では make_blackbox_func
関数を定義して、要素数が $d$ であるような NumPy 1 次元配列を入力とし、実数を出力とする関数を作成しています。
import numpy as np
from typing import Callable, Any
# 乱数シードの固定
seed = 1234
rng = np.random.default_rng(seed)
def make_blackbox_func(d: int) -> Callable[[np.ndarray], float]:
"""入力が長さ d のバリナリ値のベクトルで出力が float であるような関数を返却する"""
rng = np.random.default_rng(seed)
Q = rng.random((d, d))
Q = (Q + Q.T) / 2
Q = Q - np.mean(Q)
def blackbox(x: np.ndarray) -> float:
assert x.shape == (d,) # x は要素数 d の一次元配列
return x @ Q @ x # type: ignore
return blackbox
ここで作成される関数は 2 次関数としましたが、以降は make_blackbox_func
により作られる関数が 2
次であることやその他の性質については知らないものとして、関数の推定と最小化を行います。
機械学習によるモデルの学習¶
FMQA のうち、機械学習によりモデルの最適なパラメータを学習する部分のプログラム実装を行います。以下の図の右下部分に相当し、入力は教師データであり、出力はモデルです。
まず、Factorization Machine によるモデルを表す TorchFM
クラスを PyTorch を用いて定義します。
Factorization Machine は以下の式で表されます。
$$ \begin{aligned} f(\boldsymbol{x} | \boldsymbol{w}, \boldsymbol{v}) &= \underset{\color{red}{\mathtt{out\_linear}}}{\underline{ w_0 + \sum_{i=1}^d w_i x_i} } + \underset{\color{red}{\mathtt{out\_quadratic}}}{\underline{\frac{1}{2} \left[\underset{\color{red}{\mathtt{out\_1}}}{\underline{ \sum_{f=1}^k\left(\sum_{i=1}^d v_{i f} x_i\right)^2 }} - \underset{\color{red}{\mathtt{out\_2}}}{\underline{ \sum_{f=1}^k\sum_{i=1}^d v_{i f}^2 x_i^2 }} \right] }} \end{aligned} $$
このモデルの入力 $x$ はブラックボックス関数の入力と同じ長さ $d$ のベクトルであり、パラメータは以下の 3 種類です。
- $v$: $d\times k$ の 2 次元配列
- $w$: 長さ $d$ の 1 次元ベクトル
- $w_0$: スカラー
ハイパーパラメータは $k$ のみで、これは $d$ 以下の正の整数で与えます。
以下で定義する TorchFM
クラスは torch.nn.Module
を継承しており、入力ベクトル $x$ のサイズ $d$ とハイパーパラメータ
$k$
から構築されます。ハイパーパラメータ $k$ はモデルのパラメータ数を制御するためのもので、大きくするほどパラメータは多くなり精度が向上しますが、一方で過学習が起こりやすくなる傾向があります。
TorchFM
クラスはモデルのパラメータの $v$, $w$, $w_0$
をアトリビュートに持ち、学習を進めることによってこれらのパラメータを更新します。また、forward
メソッドは入力 $x$ から $y$
の推定値を上式に従って出力します。パラメータ
$v$, $w$, $w_0$ はイジングマシンによる最適化を行うときに必要なので、これらを出力する関数 get_parameters
も定義しておきます。
import torch
import torch.nn as nn
# 乱数シードの固定
torch.manual_seed(seed)
class TorchFM(nn.Module):
def __init__(self, d: int, k: int):
"""モデルを構築する
Args:
d (int): 入力ベクトルのサイズ
k (int): パラメータ k
"""
super().__init__()
self.d = d
self.v = torch.randn((d, k), requires_grad=True)
self.w = torch.randn((d,), requires_grad=True)
self.w0 = torch.randn((), requires_grad=True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""入力 x を受け取って y の推定値を出力する
Args:
x (torch.Tensor): (データ数 × d) の 2 次元 tensor
Returns:
torch.Tensor: y の推定値 の 1次元 tensor (サイズはデータ数)
"""
out_linear = torch.matmul(x, self.w) + self.w0
out_1 = torch.matmul(x, self.v).pow(2).sum(1)
out_2 = torch.matmul(x.pow(2), self.v.pow(2)).sum(1)
out_quadratic = 0.5 * (out_1 - out_2)
out = out_linear + out_quadratic
return out
def get_parameters(self) -> tuple[np.ndarray, np.ndarray, float]:
"""パラメータ v, w, w0 を出力する"""
np_v = self.v.detach().numpy().copy()
np_w = self.w.detach().numpy().copy()
np_w0 = self.w0.detach().numpy().copy()
return np_v, np_w, float(np_w0)
次に、TorchFM
クラスの機械学習を行う関数 train()
を定義します。入力は教師データ $x, y$ と
TorchFM
モデルのインスタンスです。train()
関数を呼ぶことで TorchFM
のパラメータの学習が行われます。
一般的な機械学習と同様に、教師データを学習データと検証データに分割し、学習データを用いてパラメータの最適化、検証データを用いて学習中のモデル検証を行います。エポックごとにモデルの検証を行い、検証データに対して最も予測精度の高かったエポックにおけるパラメータを保存して、これを学習後のモデルとします。
from torch.utils.data import TensorDataset, DataLoader, random_split
from tqdm.auto import tqdm, trange
import copy
def train(
x: np.ndarray,
y: np.ndarray,
model: TorchFM,
) -> None:
"""FM モデルの学習を行う
Args:
x (np.ndarray): 学習データ (入力ベクトル)
y (np.ndarray): 学習データ (出力値)
model (TorchFM): TorchFM モデル
"""
# イテレーション数
epochs = 2000
# モデルの最適化関数
optimizer = torch.optim.AdamW([model.v, model.w, model.w0], lr=0.1)
# 損失関数
loss_func = nn.MSELoss()
# データセットの用意
x_tensor, y_tensor = (
torch.from_numpy(x).float(),
torch.from_numpy(y).float(),
)
dataset = TensorDataset(x_tensor, y_tensor)
train_set, valid_set = random_split(dataset, [0.8, 0.2])
train_loader = DataLoader(train_set, batch_size=8, shuffle=True)
valid_loader = DataLoader(valid_set, batch_size=8, shuffle=True)
# 学習の実行
min_loss = 1e18 # 損失関数の最小値を保存
best_state = model.state_dict() # モデルの最も良いパラメータを保存
# `range` の代わりに `tqdm` モジュールを用いて進捗を表示
for _ in trange(epochs, leave=False):
# 学習フェイズ
for x_train, y_train in train_loader:
optimizer.zero_grad()
pred_y = model(x_train)
loss = loss_func(pred_y, y_train)
loss.backward()
optimizer.step()
# 検証フェイズ
with torch.no_grad():
loss = 0
for x_valid, y_valid in valid_loader:
out_valid = model(x_valid)
loss += loss_func(out_valid, y_valid)
if loss < min_loss:
# 損失関数の値が更新されたらパラメータを保存
best_state = copy.deepcopy(model.state_dict())
min_loss = loss
# モデルを学習済みパラメータで更新
model.load_state_dict(best_state)
Amplify によるモデルの最小化¶
次に、推論された機械学習モデルの最小化を行う anneal
関数の実装を行います。FMQA サイクルの図 (以下に再掲) の左下部分に相当し、入力は学習後のモデル
TorchFM
クラス、出力はモデルを最小化するようなベクトル $x$ です。
先ほど学習した TorchFM
クラスのモデルに対して以下の最適化問題を解くことで、推論されたモデルを最小化するような入力 $x$ を求めます。
$$ \underset{x}{\mathrm{argmin}} \quad \underset{\color{red}{\mathtt{out\_linear}}}{\underline{ w_0 + \sum_{i=1}^d w_i x_i} } + \underset{\color{red}{\mathtt{out\_quadratic}}}{\underline{\frac{1}{2} \left[\underset{\color{red}{\mathtt{out\_1}}}{\underline{ \sum_{f=1}^k\left(\sum_{i=1}^d v_{i f} x_i\right)^2 }} - \underset{\color{red}{\mathtt{out\_2}}}{\underline{ \sum_{f=1}^k\sum_{i=1}^d v_{i f}^2 x_i^2 }} \right] }} $$
この最適化問題において決定変数は $x$ です。これはブラックボックス関数への入力ベクトルと同じく、長さ $d$ の 1 次元バイナリ変数ベクトルです。また、学習フェイズではパラメータだった $v$, $w$, $w_0$ はここでは定数です。
与えられたモデルに対して Amplify で最適化を実行する anneal
関数を以下のように定義します。anneal
関数では
VariableGenerator
を用いて長さ $d$ の 1 次元バイナリ変数ベクトル x
を作成し、バイナリ変数配列 $x$ と
TorchFM
クラスから取得した $v$, $w$, $w_0$ を用いて、Factorization Machine の式に従い最適化対象の目的関数を作成します。
その後、作成した Amplify のモデルと先に作成したソルバークライアント FixstarsClient
を使用して、目的関数の最小化を実行します。
from amplify import VariableGenerator, Model, solve, Poly
def anneal(torch_model: TorchFM) -> np.ndarray:
"""FM モデルのパラメータを受け取り、それらのパラメータにより記述される FM モデルの最小値を与える x を求める"""
# 長さ d のバイナリ変数の配列を作成
gen = VariableGenerator()
x = gen.array("Binary", torch_model.d)
# TorchFM からパラメータ v, w, w0 を取得
v, w, w0 = torch_model.get_parameters()
# 目的関数を作成
out_linear = w0 + (x * w).sum()
out_1 = ((x[:, np.newaxis] * v).sum(axis=0) ** 2).sum() # type: ignore
out_2 = ((x[:, np.newaxis] * v) ** 2).sum()
objective: Poly = out_linear + (out_1 - out_2) / 2
# 組合せ最適化モデルを構築
amplify_model = Model(objective)
# 最小化を実行(構築したモデルと、始めに作ったソルバークライアントを引数として渡す)
result = solve(amplify_model, client)
if len(result.solutions) == 0:
raise RuntimeError("No solution was found.")
# モデルを最小化する入力ベクトルを返却
return x.evaluate(result.best.values).astype(int)
FMQA の実行¶
以上によりFMQA の中核である、機械学習を行う train
関数と最適化を行う anneal
関数を定義しました。これらを用いて実際に FMQA
を実行します。
まず、ブラックボックス最適化の対象となる blackbox
関数を次のように作成します。この関数は $0$ または $1$ からなる長さ $d = 100$ の NumPy
一次元ベクトルを受け取り float を返却します。
d = 100
blackbox = make_blackbox_func(d)
初期教師データの作成¶
次に、入力ベクトル $\boldsymbol{x}$ に対して blackbox
関数 $y = f(\boldsymbol{x})$ を評価することで、$N_0$
個の初期教師データを作成します。通常、 blackbox
関数はシミュレーションや実験の結果に相当するため、過去のデータ等を用いて作成することになります。
今回は、模擬的に適当な関数を blackbox
関数として用意したため、以下のようにランダムな $N_0$ 個の入力ベクトル $x$ を用いて初期教師データを作成する
init_training_data
関数を定義します。この関数は、blackbox
関数と初期教師データの数 $N_0$
を受け取り、初期教師データとして
$N_0$ 個の入力ベクトル $\boldsymbol{x}$ と対応する出力 $y$ を返します。
def init_training_data(d: int, n0: int):
"""n0 組の初期教師データを作成する"""
assert n0 < 2**d
# n0 個の 長さ d の入力値を乱数を用いて作成
x = rng.choice(np.array([0, 1]), 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)
# blackbox 関数を評価して入力値に対応する n0 個の出力を得る
y = np.zeros(n0)
for i in range(n0):
y[i] = blackbox(x[i])
return x, y
N0 = 60 # 初期教師データの数
x_init, y_init = init_training_data(d, N0)
$N_0$ 組の初期教師データが作成できました。
print(x_init.shape, y_init.shape)
FMQA サイクルの実行¶
上で作成した $N_0$ 組のデータを初期学習データとして、以下の図にしたがって FMQA のサイクルを実行します。
1 回のサイクルごとに以下の操作を行います。
- モデルの学習
TorchFM
クラスのモデルを構築し初期教師データx = x_init
,y = y_init
とモデルに対してtrain
関数を呼ぶことで学習を行う
- モデルの最小化
anneal
関数にTorchFM
クラスの学習済みモデルを渡すことでモデルを最小化する $\hat{x}$ を得る- $\hat{x}$ が既に教師データ
x
に含まれている場合は $\hat{x}$ の一部を変更して教師データが重複しないようにする
- モデルの評価
- $\hat{x}$ をブラックボックス関数に入力し出力 $\hat{y}$ を得る
- $\hat{x}$ と $\hat{y}$ をそれぞれ教師データ
x
およびy
に追加する
上記を実行する実装の例は次の通りです。実行にはおよそ数分の計算時間を要するため、出力例をサンプルコード実行例に示します。
# FMQA サイクルの実行回数
N = 10
# 教師データの初期化
x, y = x_init, y_init
# N 回のイテレーションを実行
# `range` の代わりに `tqdm` モジュールを用いて進捗を表示
for i in trange(N):
# 機械学習モデルの作成
model = TorchFM(d, k=10)
# モデルの学習の実行
train(x, y, model)
# 学習済みモデルの最小値を与える入力ベクトルの値を取得
x_hat = anneal(model)
# x_hat が重複しないようにする
while (x_hat == x).all(axis=1).any():
flip_idx = rng.choice(np.arange(d))
x_hat[flip_idx] = 1 - x_hat[flip_idx]
# 推定された入力ベクトルを用いてブラックボックス関数を評価
y_hat = blackbox(x_hat)
# 評価した値をデータセットに追加
x = np.vstack((x, x_hat))
y = np.append(y, y_hat)
tqdm.write(f"FMQA cycle {i}: found y = {y_hat}; current best = {np.min(y)}")
上のセルの実行後、x
と y
には $N_0 + N = 70$ 回のブラックボックス関数の評価における入力および出力が保存されています。
print(x.shape, y.shape)
以下のようにしてブラックボックス関数の評価の最小値を与える入力とその値が得られます。
min_idx = np.argmin(y)
print(f"best x = {x[min_idx]}")
print(f"best y = {y[min_idx]}")
評価値の推移のプロット¶
以下に$N_0$ 個の初期教師データと $N$ 回の FMQA サイクルで最適化された評価値の推移をプロットします。初期教師データを青色で、FMQA サイクルによって得られた評価値を赤色で示します。
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6, 4))
ax = fig.add_subplot()
# 初期教師データ生成のブラックボックス関数の評価値
ax.plot(
range(N0),
y[:N0],
marker="o",
linestyle="-",
color="b",
)
# FMQA サイクルのブラックボックス関数の評価値
ax.plot(
range(N0, N0 + N),
y[N0:],
marker="o",
linestyle="-",
color="r",
)
ax.set_xlabel("number of iterations", fontsize=18)
ax.set_ylabel("f(x)", fontsize=18)
ax.tick_params(labelsize=18)
plt.show()
サンプルコード実行例¶
一般的に、Fixstars Amplify AE で採用されているヒューリスティクスというアルゴリズムの原理上、得られる解に完全な再現性はありませんが、本サンプルコードを実行した際に得られる、典型的な出力結果を以下に示します。
参考文献¶
- K. Kitai, J. Guo, S. Ju, S. Tanaka, K. Tsuda, J. Shiomi, and R. Tamura, "Designing metamaterials
with
quantum annealing and factorization machines", Physical Review Research 2, 013319
(2020).
- このサンプルコードで紹介したブラックボックス最適化手法は本論文で Factorization Mechine with Quantum Anealing (FMQA) として提案されたものです
- T. Inoue, Y. Seki, S. Tanaka, N. Togawa, K. Ishizaki, and S. Noda, "Towards optimization of
photonic-crystal surface-emitting lasers via quantum annealing," Opt. Express 30, 43503-43512 (2022).
- フォトニック結晶レーザーの設計において FMQA によるブラックボックス最適化手法が活用されています
- 田中 宗, 山下 将司, 関 優也, アニーリングマシンによるブラックボックス最適化, 日本神経回路学会誌,
2022, 29 巻, 4 号, p. 164-173
- 機械学習とアニーリングに基づくブラックボックス最適化に関する解説記事です