ベイズ最適化の理論と実装 — ガウス過程で効率的に最適解を探索する

機械学習モデルの学習率や正則化パラメータを調整するとき、各パラメータ設定でモデルを学習して評価する必要があります。ニューラルネットワークの学習は数時間から数日かかることもあり、何百もの組み合わせを試すグリッドサーチは非現実的です。たとえば、5つのハイパーパラメータについてそれぞれ10個の候補値を試すだけで $10^5 = 100,000$ 回の評価が必要になり、1回の学習に1時間かかるとすると約11年もかかってしまいます。

では、「これまでの評価結果から、次にどのパラメータを試すのが最も効率的か」を賢く判断する方法はないでしょうか。ベイズ最適化(Bayesian Optimization)は、まさにこの問いに答えます。

ベイズ最適化のアイデアは、人間のエキスパートが行う試行錯誤のプロセスに似ています。経験豊富なエンジニアは、過去の実験結果を踏まえて「この辺りのパラメータが良さそうだ」という推測を行い、同時に「まだ試していない領域にもっと良い値があるかもしれない」という探索的な判断もします。ベイズ最適化は、この直感的なプロセスを数学的に定式化したものです。

具体的には、ガウス過程で目的関数の「代理モデル」(surrogate model)を構築し、獲得関数(acquisition function)で「次に探索すべき点」を選ぶことで、少ない評価回数で効率的に最適解を見つけます。ブラックボックス関数(内部の数式がわからない関数)の最適化に特に強力です。

ベイズ最適化を理解すると、以下のような場面で活用できます。

  • ハイパーパラメータ最適化: 機械学習モデルの学習率、正則化、アーキテクチャの選択。OptunaやHyperoptなどのライブラリの背後にある理論でもあります
  • 実験計画: 化学実験や材料探索の効率的な条件探索。新素材の開発において、合成条件の最適化に活用されています
  • A/Bテスト: 限られた試行回数でのパラメータ最適化。Webサービスのコンバージョン率の最適化などに応用できます
  • ロボティクス: ロボットの制御パラメータの効率的なチューニング。実機実験が高コストな場面で特に有効です
  • シミュレーション最適化: 計算流体力学(CFD)や有限要素法(FEM)のパラメータ探索など、1回のシミュレーションに長時間を要する場面

本記事の内容

  • ベイズ最適化の全体像と動機
  • ガウス過程による代理モデルの構築
  • 獲得関数(EI, UCB, PI)の理論と比較
  • 最適化ループの実装
  • Pythonでの1次元関数の最適化と可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

ベイズ最適化の全体像

問題設定

ベイズ最適化が対象とするのは、以下のような最適化問題です。

$$ \bm{x}^* = \arg\min_{\bm{x} \in \mathcal{X}} f(\bm{x}) $$

ここで $f$ は

  • ブラックボックス: 解析的な式が不明
  • 評価コストが高い: 1回の評価に数分〜数日
  • 微分不可能: 勾配情報が得られない
  • ノイズを含む: 同じ入力でも出力が多少異なる

典型的な例がニューラルネットワークのハイパーパラメータ最適化で、$f$ はバリデーション損失、$\bm{x}$ は学習率・バッチサイズ等のハイパーパラメータです。

この問題設定で重要なのは、$f$ の勾配 $\nabla f$ が得られないことです。通常の最適化手法(勾配降下法、ニュートン法など)は勾配情報を前提としていますが、ハイパーパラメータ最適化のようなブラックボックス問題では勾配が定義できません。したがって、$f$ の値そのものだけを使って最適化を進める必要があります。

グリッドサーチ・ランダムサーチとの比較

手法 特性 問題点
グリッドサーチ 格子状に全点を評価 次元の呪い。5パラメータ×10値 = 10万回
ランダムサーチ ランダムに点を評価 過去の結果を活用しない
ベイズ最適化 過去の結果から次の点を選択 代理モデルの構築コスト

ベイズ最適化の核心は、「過去の評価結果を使って、次にどこを評価するかを賢く選ぶ」ことです。グリッドサーチやランダムサーチが「闇雲に」点を選ぶのに対し、ベイズ最適化は「これまでの観測結果から目的関数の形状を推定し、最も有望な点を次の候補とする」逐次的なアプローチをとります。

アルゴリズムの概要

ベイズ最適化は以下のループを繰り返します。

  1. 代理モデルの更新: これまでの観測データ $\mathcal{D} = \{(\bm{x}_i, y_i)\}$ にガウス過程をフィット
  2. 獲得関数の最大化: 次に評価すべき点 $\bm{x}_{\text{next}}$ を獲得関数の最大化で決定
  3. 目的関数の評価: $y_{\text{next}} = f(\bm{x}_{\text{next}}) + \epsilon$ を観測
  4. データの追加: $\mathcal{D} \leftarrow \mathcal{D} \cup \{(\bm{x}_{\text{next}}, y_{\text{next}})\}$

このループにおける2つの核心的要素 — 代理モデル(ガウス過程)と獲得関数 — を詳しく見ていきましょう。

ガウス過程による代理モデル

なぜガウス過程か

代理モデルの要件は以下のとおりです。

  1. 不確かさの定量化: 未探索領域での「自信のなさ」を表現できる
  2. 少ないデータでの推定: 10〜100点程度のデータから関数を推定できる
  3. 柔軟な関数クラス: 複雑な形状の関数を表現できる

ガウス過程はこの3つの要件を全て満たします。予測が平均と分散の正規分布で表されるため、不確かさの定量化が自然に得られます。

他の代理モデルの候補としては、ランダムフォレストやニューラルネットワークがありますが、これらは不確かさの推定が容易ではありません。ガウス過程は「不確かさの定量化」がモデルの構造に組み込まれている点で、ベイズ最適化の代理モデルとして最適です。

ただし、ガウス過程の計算量は $O(n^3)$(カーネル行列の逆行列計算)であるため、観測データ数 $n$ が数千を超えると計算が困難になります。これが後述する「高次元での課題」にもつながります。

ガウス過程の予測

観測データ $\mathcal{D}_n = \{(\bm{x}_i, y_i)\}_{i=1}^n$ が与えられたとき、新しい点 $\bm{x}$ でのガウス過程の予測分布は

$$ f(\bm{x}) | \mathcal{D}_n \sim \mathcal{N}(\mu_n(\bm{x}), \sigma_n^2(\bm{x})) $$

予測平均(目的関数の推定値):

$$ \mu_n(\bm{x}) = \bm{k}(\bm{x})^T (\bm{K} + \sigma_\epsilon^2 \bm{I})^{-1} \bm{y} $$

予測分散(不確かさ):

$$ \sigma_n^2(\bm{x}) = k(\bm{x}, \bm{x}) – \bm{k}(\bm{x})^T (\bm{K} + \sigma_\epsilon^2 \bm{I})^{-1} \bm{k}(\bm{x}) $$

ここで $\bm{k}(\bm{x}) = [k(\bm{x}_1, \bm{x}), \ldots, k(\bm{x}_n, \bm{x})]^T$、$K_{ij} = k(\bm{x}_i, \bm{x}_j)$ はカーネル行列、$\sigma_\epsilon^2$ は観測ノイズの分散です。

予測平均 $\mu_n(\bm{x})$ の式を直感的に解釈すると、新しい点 $\bm{x}$ での予測値は、訓練データ $\bm{y}$ の重み付き和です。重みは「新しい点が各訓練データ点にどれだけ近いか」をカーネル関数で測り、訓練データ間の相関も考慮して調整されたものです。

予測分散 $\sigma_n^2(\bm{x})$ は、「事前の不確かさ $k(\bm{x}, \bm{x})$ から、訓練データによって減少した分を引いたもの」と解釈できます。訓練データの近くでは不確かさが小さく(よく知っている領域)、遠い場所では不確かさが大きく(未知の領域)なります。この性質こそが、ベイズ最適化における探索と活用のバランスの鍵になります。

カーネルの選択

ベイズ最適化ではMatern 5/2カーネルが広く使われます。

$$ k_{\text{Matern52}}(r) = \sigma_f^2 \left(1 + \frac{\sqrt{5} r}{\ell} + \frac{5r^2}{3\ell^2}\right)\exp\left(-\frac{\sqrt{5} r}{\ell}\right) $$

ここで $r = \|\bm{x} – \bm{x}’\|$、$\ell$ は長さスケール、$\sigma_f^2$ は出力分散です。

Matern 5/2はRBFカーネルより滑らかさの仮定が弱く、より多くのブラックボックス関数に適合します。RBFカーネルは無限回微分可能な関数を仮定しますが、多くの目的関数(たとえばニューラルネットワークのバリデーション損失)はそれほど滑らかではありません。Matern 5/2は2回微分可能な関数を仮定するため、より現実的なモデリングが可能です。

カーネルのハイパーパラメータ $\ell$(長さスケール)と $\sigma_f^2$(出力分散)は、通常、観測データに対する対数周辺尤度を最大化する方法(Type II Maximum Likelihood)で推定します。長さスケール $\ell$ は「目的関数がどの程度の距離スケールで変化するか」を制御し、小さいほど関数の変化が速い(短い距離で値が大きく変わる)と仮定します。

代理モデルの予測分布が得られたところで、「次にどこを探索するか」を決める獲得関数について説明します。獲得関数は代理モデルの予測平均と予測分散を入力とし、「この点を評価する価値がどれくらいあるか」をスカラー値で返します。

獲得関数

探索と活用のトレードオフ

ベイズ最適化の核心的な課題は、探索(exploration)と活用(exploitation)のトレードオフです。

  • 活用: 現在の推定で最も良さそうな領域($\mu_n$ が小さい領域)を評価する → 局所的な改善
  • 探索: 不確かさが大きい領域($\sigma_n$ が大きい領域)を評価する → 未知の良い領域の発見

獲得関数はこのトレードオフを定量的にバランスする指標です。日常のアナロジーで言えば、宝探しに似ています。既に見つけた宝が埋まっていた場所の近くを掘れば確実にいくらかの宝が見つかるかもしれませんが(活用)、まだ掘っていない場所に大きな宝が隠れている可能性もあります(探索)。獲得関数は「次にどこを掘るべきか」を数学的に判断する指標です。

代表的な獲得関数を3つ紹介します。

Expected Improvement(EI)

最も広く使われる獲得関数です。現在の最良値 $f^* = \min_i y_i$ からの改善量の期待値を最大化します。

$$ \begin{equation} \text{EI}(\bm{x}) = \mathbb{E}[\max(f^* – f(\bm{x}), 0)] \end{equation} $$

ガウス過程の予測分布が正規分布であることを利用すると、解析的に計算できます。

$f(\bm{x}) \sim \mathcal{N}(\mu, \sigma^2)$ とおくと(添字 $n$ を省略)

$$ \text{EI}(\bm{x}) = (f^* – \mu)\Phi(Z) + \sigma \phi(Z) $$

ここで $Z = (f^* – \mu) / \sigma$、$\Phi$ は標準正規分布のCDF、$\phi$ はPDFです。

この式の2項を解釈しましょう。

  • 第1項 $(f^* – \mu)\Phi(Z)$: 活用項。$\mu$ が $f^*$ より小さい(推定値が良い)ほど大きい
  • 第2項 $\sigma \phi(Z)$: 探索項。$\sigma$ が大きい(不確かが大きい)ほど大きい

$\sigma = 0$ のとき(既知の点)は $\text{EI} = 0$ となり、同じ点を再評価しません。これは非常に合理的な性質で、既に観測した点を再び評価するのは無駄だからです。

EI の導出過程をもう少し詳しく見ましょう。$f(\bm{x}) \sim \mathcal{N}(\mu, \sigma^2)$ のとき、改善量 $I = \max(f^* – f(\bm{x}), 0)$ の期待値を計算します。$u = (f(\bm{x}) – \mu) / \sigma$ と変数変換すると

$$ \text{EI} = \int_{-\infty}^{f^*} (f^* – f) \cdot \frac{1}{\sigma}\phi\left(\frac{f – \mu}{\sigma}\right) df $$

この積分を標準正規分布の性質を用いて解くと、上述の閉じた式が得られます。解析的に計算可能であることは、実装の観点からも大きな利点です。

Upper Confidence Bound(UCB)

$$ \begin{equation} \text{UCB}(\bm{x}) = -\mu_n(\bm{x}) + \beta \sigma_n(\bm{x}) \end{equation} $$

ここで $\beta > 0$ は探索と活用のバランスパラメータです。$\beta$ が大きいほど探索を重視します。最小化問題なので $-\mu$ を使っています。

理論的には $\beta_t = 2 \log(t^{d/2+2}\pi^2/3\delta)$($d$ は次元、$\delta$ は信頼度パラメータ、$t$ はイテレーション数)とすることで、後悔(regret)の上界が保証されます(Srinivas et al., 2010)。

Probability of Improvement(PI)

$$ \text{PI}(\bm{x}) = P(f(\bm{x}) < f^*) = \Phi(Z) $$

最もシンプルですが、改善の「量」を考慮しないため、微小な改善でも高い値を返す傾向があります。実用ではEIの方が好まれます。

獲得関数の比較

獲得関数 特性 長所 短所
EI 改善量の期待値 探索と活用を自動バランス $f^*$ 付近に集中しがち
UCB 平均 + $\beta \times$ 標準偏差 $\beta$ で探索度合を制御可能 $\beta$ の設定が必要
PI 改善確率 計算が最もシンプル 微小な改善を過大評価

実用ではEIがデフォルトの選択肢であり、Optuna、Hyperopt、BoTorchなどの主要なライブラリもEIをベースにしています。

なお、EIには発展的なバリエーションも提案されています。たとえば、Log-EIはEIの対数をとることで数値的な安定性を改善し、Knowledge Gradient(KG)は1ステップ先の最適値の改善を考慮することでより長期的な視点を持ちます。実務では標準的なEIで十分なことが多いですが、特に多段階の意思決定が必要な場面ではKGが有効です。

理論を理解したところで、次にPythonでベイズ最適化を実装し、動作を可視化してみましょう。

Pythonでの実装

ガウス過程とベイズ最適化の実装

以下のコードでは、ガウス過程による代理モデルとEI獲得関数を用いたベイズ最適化をスクラッチ実装します。実装のポイントは、ガウス過程の予測分布からEIを計算し、EIが最大の点を次の評価点として選択するループです。

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

def rbf_kernel_bo(x1, x2, length_scale=1.0, signal_var=1.0):
    """RBFカーネル"""
    sq_dist = np.subtract.outer(x1.ravel(), x2.ravel())**2
    return signal_var * np.exp(-0.5 * sq_dist / length_scale**2)

class GaussianProcess:
    """ガウス過程回帰"""
    def __init__(self, length_scale=1.0, signal_var=1.0, noise_var=1e-6):
        self.length_scale = length_scale
        self.signal_var = signal_var
        self.noise_var = noise_var

    def fit(self, X, y):
        self.X_train = X.ravel()
        self.y_train = y.ravel()
        K = rbf_kernel_bo(self.X_train, self.X_train,
                         self.length_scale, self.signal_var)
        self.K_inv = np.linalg.inv(K + self.noise_var * np.eye(len(X)))

    def predict(self, X):
        X = X.ravel()
        K_s = rbf_kernel_bo(self.X_train, X, self.length_scale, self.signal_var)
        K_ss = rbf_kernel_bo(X, X, self.length_scale, self.signal_var)
        mu = K_s.T @ self.K_inv @ self.y_train
        cov = K_ss - K_s.T @ self.K_inv @ K_s
        sigma = np.sqrt(np.maximum(np.diag(cov), 1e-10))
        return mu, sigma

def expected_improvement(mu, sigma, f_best):
    """Expected Improvement"""
    with np.errstate(divide='warn'):
        Z = (f_best - mu) / (sigma + 1e-10)
        ei = (f_best - mu) * norm.cdf(Z) + sigma * norm.pdf(Z)
        ei[sigma < 1e-10] = 0.0
    return ei

def bayesian_optimization(f, bounds, n_init=3, n_iter=15,
                          length_scale=0.3, signal_var=1.0):
    """ベイズ最適化のメインループ"""
    # 初期点のランダムサンプリング
    X_init = np.random.uniform(bounds[0], bounds[1], n_init)
    y_init = np.array([f(x) for x in X_init])

    X_obs = X_init.tolist()
    y_obs = y_init.tolist()
    history = []

    gp = GaussianProcess(length_scale=length_scale, signal_var=signal_var)

    for i in range(n_iter):
        X_arr = np.array(X_obs)
        y_arr = np.array(y_obs)
        gp.fit(X_arr, y_arr)

        # 獲得関数の最大化(候補点のグリッド評価)
        X_cand = np.linspace(bounds[0], bounds[1], 500)
        mu, sigma = gp.predict(X_cand)
        f_best = np.min(y_arr)
        ei = expected_improvement(mu, sigma, f_best)

        x_next = X_cand[np.argmax(ei)]
        y_next = f(x_next)

        history.append({
            'iter': i, 'x_next': x_next, 'y_next': y_next,
            'f_best': f_best, 'X_cand': X_cand, 'mu': mu.copy(),
            'sigma': sigma.copy(), 'ei': ei.copy()
        })

        X_obs.append(x_next)
        y_obs.append(y_next)

    return np.array(X_obs), np.array(y_obs), history

上のコードでは、GaussianProcess クラスがガウス過程回帰の核心部分を担っています。fit メソッドでカーネル行列の逆行列を計算し、predict メソッドで任意の点での予測平均と予測分散を返します。expected_improvement 関数は前節で導出したEIの閉じた式をそのまま実装しており、norm.cdfnorm.pdf でそれぞれ標準正規分布のCDFとPDFを計算しています。

bayesian_optimization 関数がメインのループを担い、初期点のランダムサンプリング後に「GP更新 → EI最大化 → 目的関数評価 → データ追加」を繰り返します。獲得関数の最大化は、ここでは簡易的にグリッド上での評価で行っていますが、実用的なライブラリでは L-BFGS-B などの最適化手法が使われます。

1次元関数の最適化と可視化

それでは、多峰性の目的関数に対してベイズ最適化を実行し、最適化の進行を可視化してみましょう。多峰性関数を選んだ理由は、局所解が複数ある問題でベイズ最適化が探索と活用をどのようにバランスするかを観察するためです。

np.random.seed(42)

# 目的関数(多峰性)
def objective(x):
    return np.sin(3 * x) * x**2 * 0.1 + np.cos(5 * x) * 0.5 - 0.3 * x

bounds = (-2, 4)
X_obs, y_obs, history = bayesian_optimization(
    objective, bounds, n_init=3, n_iter=12, length_scale=0.5
)

# 最適化過程の可視化(4つの時点)
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
x_plot = np.linspace(bounds[0], bounds[1], 500)
y_true = np.array([objective(x) for x in x_plot])

for idx, (ax, step) in enumerate(zip(axes.ravel(), [0, 3, 7, 11])):
    h = history[step]
    n_obs = step + 3  # 初期3点 + step

    # 真の関数
    ax.plot(x_plot, y_true, "k-", linewidth=1, alpha=0.5, label="True $f(x)$")

    # GP予測
    mu = h['mu']
    sigma = h['sigma']
    ax.plot(x_plot, mu, "b-", linewidth=1.5, label="GP mean $\\mu$")
    ax.fill_between(x_plot, mu - 2*sigma, mu + 2*sigma,
                    alpha=0.2, color="blue", label="$\\mu \\pm 2\\sigma$")

    # 観測点
    ax.scatter(X_obs[:n_obs+1], y_obs[:n_obs+1], c="red", s=40,
               zorder=5, label="Observations")

    # 次の評価点
    ax.axvline(h['x_next'], color="green", linestyle="--", linewidth=1.5,
               alpha=0.7, label=f"Next: x={h['x_next']:.2f}")

    # EI(サブプロット)
    ax2 = ax.twinx()
    ei_scaled = h['ei'] / (h['ei'].max() + 1e-10)
    ax2.fill_between(x_plot, 0, ei_scaled, alpha=0.15, color="orange")
    ax2.set_ylabel("EI (scaled)", fontsize=10, color="orange")
    ax2.set_ylim(0, 1.5)
    ax2.tick_params(axis='y', labelcolor='orange')

    ax.set_title(f"Iteration {step+1} (n_obs={n_obs+1})", fontsize=12)
    ax.set_xlabel("$x$", fontsize=11)
    ax.set_ylabel("$f(x)$", fontsize=11)
    ax.grid(True, alpha=0.3)
    if idx == 0:
        ax.legend(fontsize=8, loc="upper left")

plt.tight_layout()
plt.savefig("bayesian_optimization.png", dpi=150, bbox_inches="tight")
plt.show()

# 最適解
best_idx = np.argmin(y_obs)
print(f"Best x: {X_obs[best_idx]:.4f}")
print(f"Best f(x): {y_obs[best_idx]:.4f}")

このグラフは、ベイズ最適化の4つの時点での状態を示しています。

  1. 左上(イテレーション1): 初期3点からGPが関数の大まかな形状を推定しています。不確かさ(青い帯)が観測点の間で大きく、EI(オレンジの領域)は不確かさの大きい探索的な領域にピークがあります

  2. 右上(イテレーション4): 観測点が増え、GPの推定が真の関数に近づいています。EIのピークは、推定値が低い(良い)かつ不確かさが残る領域に移動しています。探索と活用のバランスが機能しています

  3. 左下(イテレーション8): 最適値の付近に観測点が集中し始め、GPの推定がその領域で特に正確になっています。EIの値は全体的に小さくなり、大きな改善の余地が少なくなっていることを示しています

  4. 右下(イテレーション12): 最適値付近の推定が非常に正確になり、EIがほぼゼロに近づいています。ベイズ最適化が収束に向かっていることがわかります。わずか15回の評価(初期3点 + 12イテレーション)で、真の最適値に近い解が得られています

注目すべきは、ベイズ最適化が初期の探索的なフェーズから徐々に活用的なフェーズに移行していく様子です。初期にはEIが広い範囲にわたって高い値を示しますが、後半になるとEIのピークが最適値付近に集中します。これはEIの構造(探索項 $\sigma\phi(Z)$ は不確かさが減少するにつれて小さくなり、活用項 $(f^* – \mu)\Phi(Z)$ が支配的になる)から自然に生じる振る舞いです。

収束の比較

ベイズ最適化の効率性を定量的に評価するために、ランダムサーチとの収束を比較します。20回の独立した試行を行い、平均と標準偏差を可視化します。

np.random.seed(42)

n_total = 15
n_trials = 20

# ベイズ最適化の収束
bo_best_curves = []
for trial in range(n_trials):
    np.random.seed(trial)
    X_obs, y_obs, _ = bayesian_optimization(
        objective, bounds, n_init=3, n_iter=n_total-3, length_scale=0.5
    )
    best_curve = np.minimum.accumulate(y_obs)
    bo_best_curves.append(best_curve)

# ランダムサーチの収束
rs_best_curves = []
for trial in range(n_trials):
    np.random.seed(trial + 1000)
    X_rs = np.random.uniform(bounds[0], bounds[1], n_total)
    y_rs = np.array([objective(x) for x in X_rs])
    best_curve = np.minimum.accumulate(y_rs)
    rs_best_curves.append(best_curve)

bo_best = np.array(bo_best_curves)
rs_best = np.array(rs_best_curves)

fig, ax = plt.subplots(figsize=(8, 5))
evals = np.arange(1, n_total + 1)

ax.plot(evals, bo_best.mean(axis=0), "b-", linewidth=2, label="Bayesian Opt (EI)")
ax.fill_between(evals, bo_best.mean(axis=0) - bo_best.std(axis=0),
                bo_best.mean(axis=0) + bo_best.std(axis=0), alpha=0.2, color="blue")

ax.plot(evals, rs_best.mean(axis=0), "r--", linewidth=2, label="Random Search")
ax.fill_between(evals, rs_best.mean(axis=0) - rs_best.std(axis=0),
                rs_best.mean(axis=0) + rs_best.std(axis=0), alpha=0.2, color="red")

ax.set_xlabel("Number of Evaluations", fontsize=12)
ax.set_ylabel("Best $f(x)$ Found", fontsize=12)
ax.set_title("Convergence: Bayesian Optimization vs Random Search", fontsize=13)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("bo_convergence.png", dpi=150, bbox_inches="tight")
plt.show()

このグラフから、ベイズ最適化の効率性が明確にわかります。ベイズ最適化(青)はランダムサーチ(赤)よりも少ない評価回数で低い値に到達しており、特に5〜10回の評価の段階で差が顕著です。帯の幅(標準偏差)もベイズ最適化の方が小さく、結果の安定性も高いことがわかります。この差は、評価コストが高いタスクほど実用的に重要になります。

多次元への拡張

高次元での課題

ここまでの議論は1次元の例で行いましたが、実際のハイパーパラメータ最適化問題は多次元です。ベイズ最適化は低次元($d \leq 20$ 程度)で最も効果的ですが、次元が増えるといくつかの課題が顕在化します。

  • ガウス過程の計算量: カーネル行列の逆行列計算が $O(n^3)$ かかります。観測データが数千点を超えると計算が困難になります
  • 獲得関数の最適化: 高次元空間での獲得関数の最大化は、それ自体が困難な最適化問題です。$d$ が大きくなると、グリッド評価は非現実的になり、勾配ベースの最適化やランダムサンプリングが必要になります
  • サンプル効率: 高次元空間を適切にカバーするために必要な評価回数が指数的に増加します(次元の呪い)。$d = 20$ でも、空間の大部分は未探索のままになります

対策

高次元問題に対しては、以下のような対策が提案されています。

  • ランダム射影: 高次元空間を低次元に射影してベイズ最適化を適用する方法です。REMBO(Random Embedding Bayesian Optimization)は、目的関数が実質的に低次元の構造を持つ場合に有効です。多くのハイパーパラメータ最適化問題では、全てのパラメータが等しく重要ではないため、この仮定は現実的です
  • 加法分解: $f(\bm{x}) \approx \sum_i f_i(\bm{x}_{S_i})$ と仮定し、低次元の部分問題に分解する方法です。ハイパーパラメータ間の交互作用が限定的な場合に有効です
  • Tree-structured Parzen Estimator(TPE): ガウス過程の代わりにカーネル密度推定を使用する方法で、Optuna や Hyperopt で採用されています。TPEはガウス過程のような $O(n^3)$ の計算を必要とせず、条件付きパラメータ空間やカテゴリカル変数の扱いが容易です

実用的なハイパーパラメータ最適化では、TPEベースの手法(Optuna等)が高次元にもスケールし、広く使われています。一方、低次元かつ評価コストが非常に高い問題(実験計画や材料探索など)では、ガウス過程ベースのベイズ最適化が依然として最良の選択肢です。

まとめ

本記事では、ベイズ最適化の理論と実装について解説しました。

  • ベイズ最適化はガウス過程で目的関数の代理モデルを構築し、獲得関数で次の評価点を効率的に選択する。過去の観測結果を活かして逐次的に最適解を探索するため、グリッドサーチやランダムサーチよりも圧倒的に効率的である
  • Expected Improvement(EI)は探索と活用を自動的にバランスし、解析的に計算可能。EIの2つの項(活用項と探索項)が自然にトレードオフを制御する
  • 少ない評価回数(10〜100回程度)で効率的に最適解を見つけられ、ブラックボックス関数の最適化に特に有効。ニューラルネットワークのハイパーパラメータ最適化や実験計画など、評価コストが高い問題で真価を発揮する
  • 低次元($d \leq 20$)で最も効果的で、高次元にはTPEなどの代替手法がある。問題の性質に応じた手法の選択が重要

次のステップとして、以下の記事も参考にしてください。