FMQA によるブラックボックス最適化

FMQA(Factorization Machine with Quantum Annealing)は、評価に時間や費用のかかる関数を、少ない評価回数で最小化する方法です。 評価した結果から関数の近似モデルを学習し、そのモデルを最小にする点を QUBO ソルバーで求めて次に評価する、という手順を繰り返します。 名前の QA は量子アニーリングのことですが、QUBO を解く部分にはどの QUBO ソルバーも使えます。このページでは EasySolver を使います。

前提: 評価が高価なブラックボックス関数

$n$ 個のバイナリ変数 $x = (x_1, \ldots, x_n)$ の関数 $f(x)$ を最小化したいとします。 ただし $f$ の式は分からず、$x$ を与えると値が返ってくるだけです(ブラックボックス関数)。 しかも 1 回の評価に実験や時間のかかるシミュレーションが必要で、評価できる回数は限られています。 たとえば、20 種類の材料それぞれを使うか使わないかを決めて試作品を作り、その性能を測る、といった状況です。

$n = 20$ でも $x$ は $2^{20}$(約 100 万)通りあり、すべてを試すことはできません。 このページでは、評価できる回数を $300$ 回として、その中でできるだけ小さい $f(x)$ を見つけます。

FMQA の手順

  1. ランダムに選んだ点をいくつか評価する。
  2. それまでに評価したすべての点 $(x, f(x))$ に、Factorization Machine(FM)という 2 次のモデルを当てはめる(学習)。
  3. 学習した FM の予測式は $x$ の 2 次式、つまり QUBO なので、QUBO ソルバーで予測値を最小にする $x$ を求める。
  4. その $x$ を評価してデータに加え、2. に戻る。

モデルが小さいと予測した点を実際に評価し、予測が外れていれば、その結果が次の学習に入ってモデルが直ります。 この「予測、確認、修正」を繰り返して、少ない評価回数で最小値に近づきます。 QUBO ソルバーは $2^n$ 通り全体の中からモデルの最小点を選ぶので、何ビットも同時に変えた点へ一度に移れます。

この例のブラックボックス関数

このページでは、高価な評価の代わりに次の式を使います($n = 20$):

\[f(x) = 100 \sum_{k=1}^{3} \sin\Bigl(\sum_{i=1}^{20} a_{ki}\, x_i\Bigr)\]

係数 $a_{ki}$ は $-1$ から $1$ までの数で、プログラムの中に表として書いてあります。 この式は評価の代わりと答え合わせにだけ使い、FMQA の部分は式の中身を使いません。

  • $f(x)$ は $-300$ 以上 $300$ 以下で、3 つの和 $\sum_i a_{ki} x_i$ がそろって $\sin$ の谷($-\pi/2 + 2\pi m$)の近くにあるとき、$-300$ に近くなります。 全 $2^{20}$ 通りを調べると、最小値は $-299.92$ です。
  • $\sin$ の和なので、$x$ の 2 次式(QUBO)ではありません。FMQA はこれを 2 次式で近似しながら探します。
  • 良い解はまれです。最小値の 99% 以内($-296.92$ 以下)に入る $x$ は $51$ 個(全体の約 $0.005\%$)しかなく、 ランダムに 300 個選んでも、その中に入っている確率は $1.4\%$ です。

Factorization Machine

FM は、$f(x)$ を次の 2 次式で予測するモデルです:

\[\hat{y}(x) = w_0 + \sum_{i} w_i x_i + \sum_{i<j} \langle v_i, v_j\rangle\, x_i x_j\]

$w_0$ と $w_i$ は実数、$v_i$ は変数 $x_i$ ごとの $D$ 次元の実数ベクトルで、$\langle v_i, v_j\rangle$ はその内積です。

  • 2 次の係数を内積で表す: 2 次の係数を 1 つずつ推定すると、$n = 20$ では $190$ 個あります。 FM ではベクトル $v_i$ の成分だけを推定するので、$D = 3$ なら $60$ 個です。 $w_0$ と $w_i$ を合わせても $81$ 個なので、評価した点が少ないうちから学習できます。
  • $D = 3$ にした理由: この例の $f$ は、3 つの 1 次式 $\sum_i a_{ki} x_i$ を通してだけ $x$ に依存します。 それぞれの $\sin$ を谷の近くで放物線 $\gamma_k \bigl(\sum_i a_{ki} x_i - c_k\bigr)^2$($\gamma_k > 0$)で近似すると、 $x_i x_j$ の係数は $2 \sum_k \gamma_k a_{ki} a_{kj}$ となり、3 次元のベクトルの内積でちょうど表せます。 実際の問題ではこのような構造は分からないので、$D$ はいくつか試して決めます。 また、$x_i^2 = x_i$ を使うと、2 次の項は次のように書き換えられます($v_{id}$ は $v_i$ の第 $d$ 成分):
\[\sum_{i<j} \langle v_i, v_j\rangle\, x_i x_j = \frac{1}{2} \sum_{d=1}^{D} \Bigl[ \Bigl(\sum_i v_{id}\, x_i\Bigr)^2 - \sum_i v_{id}^2\, x_i \Bigr]\]

学習でも QUBO の式を作るときも、この形を使います。

学習

評価した点の $f$ の値を平均 $0$、標準偏差 $1$ にそろえたものを $y$ とします。 予測値と $y$ の二乗誤差の平均に、正則化項 $\lambda \bigl(\sum_i w_i^2 + \sum_{i,d} v_{id}^2\bigr)$($\lambda = 10^{-4}$)を加えたものを、 Adam という勾配法で最小化します($1000$ 回の更新)。 勾配は予測式を微分したもので、たとえば $\partial \hat{y} / \partial v_{id} = x_i \bigl(\sum_j v_{jd}\, x_j - v_{id}\bigr)$ です。 FM は評価のたびに、$v_{id}$ を乱数で初期化し直して最初から学習します。

QUBO++ プログラム

以下のプログラムは、評価回数 300 回で FMQA を実行し、最後に答え合わせとして全 $2^{20}$ 通りの中の最小値を表示します:

import numpy as np
import pyqbpp.d as qbpp

N = 20       # ビット数
K = 3        # 黒箱の中の sin の数
D = 3        # FM のベクトルの次元
INIT = 20    # 最初にランダムに選んで評価する点の数
EVALS = 300  # 黒箱を評価できる回数

# 黒箱の係数 a_ki
A = np.array([
    [0.02, 0.90, -0.71, 0.90, -0.38, -0.15, 0.66, -0.18, 0.10, -0.94,
     0.51, 0.08, -0.34, 0.58, -0.39, -0.09, -0.73, -0.19, -0.59, -0.48],
    [0.50, -0.44, -0.03, 0.96, 0.92, 0.45, 0.08, -0.45, -0.68, 0.94,
     0.03, -0.77, 0.25, 0.55, 0.23, 0.83, -0.92, 0.06, -0.08, -0.88],
    [0.28, 0.71, 0.19, -0.48, 0.68, 0.02, 0.02, 0.51, -0.70, 0.64,
     0.37, 0.57, -0.62, 0.60, -0.62, -0.84, 0.71, 0.72, 0.75, -0.06]])


# 黒箱 f(x) = 100 * Σ_k sin(Σ_i a_ki x_i)
# 本当は高価な測定やシミュレーションで、ここでは式で代用する
def blackbox(b):
    return 100 * np.sin(A @ b).sum()


# 予測値と y の二乗誤差の平均 + 正則化項 を Adam で最小化して FM を学習する
def train(X, y, rng):
    p = [np.zeros(1), np.zeros(N), rng.normal(0, 0.1, (N, D))]  # w0, w, v
    m = [np.zeros_like(a) for a in p]
    u = [np.zeros_like(a) for a in p]
    lr, b1, b2, l2 = 0.05, 0.9, 0.999, 1e-4
    for t in range(1, 1001):
        w0, w, v = p
        # 予測値 w0 + Σ_i w_i x_i + (1/2) Σ_d [(Σ_i v_id x_i)^2 - Σ_i v_id^2 x_i]
        s = X @ v  # s[n, d] = Σ_i v_id x_i
        pred = w0[0] + X @ w + 0.5 * ((s**2).sum(1) - X @ (v**2).sum(1))
        # 勾配
        r = 2 * (pred - y) / len(y)
        g = [np.array([r.sum()]),
             X.T @ r + 2 * l2 * w,
             X.T @ (r[:, None] * s) - v * (X.T @ r)[:, None] + 2 * l2 * v]
        # Adam による更新
        for j in range(3):
            m[j] = b1 * m[j] + (1 - b1) * g[j]
            u[j] = b2 * u[j] + (1 - b2) * g[j]**2
            mh = m[j] / (1 - b1**t)
            uh = u[j] / (1 - b2**t)
            p[j] = p[j] - lr * mh / (np.sqrt(uh) + 1e-8)
    return p


rng = np.random.default_rng(7)
X = []      # 評価した点
y = []      # その点の f の値
seen = set()  # 評価済みの点


def evaluate(b):
    X.append(b)
    y.append(blackbox(b))
    seen.add(tuple(b))


# 1. ランダムに選んだ点を評価する
while len(X) < INIT:
    b = rng.integers(0, 2, N)
    if tuple(b) not in seen:
        evaluate(b)
best = min(y)
print(f"eval {len(X)}: f = {best:.2f}")

x = qbpp.var("x", N)
while len(X) < EVALS:
    # 2. f の値を平均 0、標準偏差 1 にそろえて FM を学習する
    z = (np.array(y) - np.mean(y)) / np.std(y)
    w0, w, v = train(np.array(X), z, rng)

    # 3. FM の予測式を QUBO++ の式にして、最小にする x を求める
    f = w0[0]
    for i in range(N):
        f += w[i] * x[i]
    for d in range(D):
        s = 0
        q = 0
        for i in range(N):
            s += v[i, d] * x[i]
            q += v[i, d] ** 2 * x[i]
        f += 0.5 * (qbpp.sqr(s) - q)
    f.simplify_as_binary()
    sol = qbpp.EasySolver(f).search(time_limit=0.1)
    b = np.array(sol(x), dtype=int)

    # 4. 評価済みの点なら、ランダムなビットを反転してから評価する
    while tuple(b) in seen:
        i = rng.integers(N)
        b[i] = 1 - b[i]
    evaluate(b)
    if y[-1] < best:
        best = y[-1]
        print(f"eval {len(X)}: f = {best:.2f}")

# 答え合わせ: 全 2^N 通りの中の最小値
B = (np.arange(2**N)[:, None] >> np.arange(N)) & 1
fmin = (100 * np.sin(B @ A.T).sum(axis=1)).min()
print(f"minimum over all 2^{N} points: {fmin:.2f}")
  • import pyqbpp.d で、式の係数を実数(float)にしています(実数(double)係数)。 学習した FM のパラメータは実数なので、そのまま QUBO++ の式の係数にできます。学習には numpy を使います(pip install numpy)。
  • blackbox が評価の代わりの式です。FMQA の部分は blackbox が返す値だけを使います。
  • FM のパラメータは [w0, w, v](w0 は長さ 1、w は長さ N、v は N×D の numpy 配列)として持ちます。 train は、評価したすべての点の予測値と勾配を行列の計算でまとめて求め、Adam で更新します。
  • 手順 3 では、学習した予測式をそのまま QUBO++ の式 f に書き、simplify_as_binary() で $x_i^2 = x_i$ として整理してから EasySolver で解きます。 変数は 20 個なので、求解は 0.1 秒で十分です。
  • 学習が進むと、すでに評価した点が提案されることがよくあります。 同じ点を評価し直しても情報は増えないので、手順 4 で、まだ評価していない点になるまでランダムなビットを反転します。
  • 実行には 1〜2 分ほどかかります。

出力結果

eval 20: f = -200.02
eval 28: f = -236.44
eval 32: f = -255.80
eval 37: f = -261.30
eval 41: f = -292.94
eval 89: f = -293.16
eval 113: f = -297.93
minimum over all 2^20 points: -299.92

最初にランダムに選んだ 20 点の最良値は $-200.02$ で、113 回目の評価で $-297.93$ に達しています(全 $2^{20}$ 通りの最小値は $-299.92$)。 学習の初期値などに乱数を使っているので、乱数の種を変えると結果も変わります。

ランダム探索・山登り法との比較

乱数の種を変えて上のプログラムを 20 回実行し、評価回数ごとの最良値(中央値)を、次の 2 つの方法と比べました:

  • ランダム探索: 各ビットを確率 $1/2$ で決めた $x$ を評価することを繰り返します(値は全 $2^{20}$ 通りの分布から計算)。
  • 山登り法: ランダムな $x$ から始め、ランダムな順に 1 ビットずつ反転して評価し、$f$ が小さくなればそこへ移ります。 どのビットを反転しても小さくならない点(局所最適解)に着いたら、新しいランダムな $x$ からやり直します。 評価済みの点は評価し直さず、500 回実行した中央値です。
評価回数 FMQA ランダム探索 山登り法
50 $-285.32$ $-209.88$ $-290.96$
100 $-293.61$ $-235.38$ $-294.11$
200 $-297.93$ $-255.48$ $-296.06$
300 $-298.55$ $-264.01$ $-296.67$
300 回以内に最小値の 99% 以内に入った割合 $90\%$ $1.4\%$ $49\%$
  • FMQA はランダム探索よりずっと小さい値に届きます。
  • 山登り法は、評価回数が少ないうちは FMQA より良い値を出しますが、$-296$ 付近で止まりがちです。 この $f$ には局所最適解が $357$ 個あり、その多くは $-290$ 前後の値です。 そこから最小値の近くへ行くには、3 つの和を同時に谷底へ合わせる必要があり、何ビットも同時に変えなければなりません。
  • FMQA は谷の形を FM で学習し、その最小点として何ビットも離れた点へ一度に移れるので、評価 100〜200 回で山登り法を追い越します。

この例の QUBO は変数が 20 個と小さく、ソルバーにとっては簡単な問題です。 結果を左右しているのは主に学習の部分です。 変数が数百以上に増えたり、$x$ に制約があったりすると、QUBO を正しく解く部分の役割が大きくなります。


Back to top

Page last modified: 2026.10.07.

© 2026 中野浩嗣, 広島大学