Optunaの中身:TPE(Tree-structured Parzen Estimator)の理論とPython実装

OptunaのデフォルトサンプラーTPE(Tree-structured Parzen Estimator)の仕組みを解説。観測を良い群と悪い群に分けて密度比 l(x)/g(x) を最大化するというアイデアをベイズ最適化のEIから導出し、scipyのKDEで約40行のフルスクラッチ実装。Branin関数でランダムサーチ・TPE・multivariate TPE・GP-EIを30シード比較し、breast_cancerデータのHistGradientBoosting実チューニングまで実測検証。

はじめに

ベイズ最適化の基礎 では、ガウス過程(GP)をサロゲートモデルとして獲得関数 EI・UCB・PI を最大化する枠組みを解説しました。一方、実務のハイパーパラメータチューニングで最も使われている Optuna のデフォルトサンプラーは、GPではなく TPE(Tree-structured Parzen Estimator) です。

optuna.create_study() と書くだけで動いてしまうため、TPEの中身を意識する機会は少ないのですが、その発想はGPベースのベイズ最適化と対照的で面白いものです。本記事では、

  1. TPEの核心である密度比 \(l(x)/g(x)\) の最大化を、EIの式変形から導出する
  2. scipy.stats.gaussian_kde約40行のTPEをフルスクラッチ実装し、内部状態を可視化する
  3. Branin関数でランダムサーチ・TPE・multivariate TPE・GP-EIを30シード比較する
  4. 実データ(breast_cancer)のHistGradientBoosting調整でチューニングの費用対効果を実測する

を順に行います。素朴な自作TPEがランダムサーチに勝てないという「負の結果」も含めて、実測値をそのまま示します。

TPEの仕組み:ベイズの定理で獲得関数をひっくり返す

GPベースのベイズ最適化との視点の違い

GPベースのベイズ最適化は「入力 \(x\) を与えると出力 \(y\) の分布を返すモデル \(p(y \mid x)\) 」を学習します。TPEはこれを逆向きにモデル化します。すなわち、観測履歴を評価値で二分し、

\[ p(x \mid y) = \begin{cases} l(x) & (y < y^{*}) \\ g(x) & (y \geq y^{*}) \end{cases} \tag{1} \]

と、「良い評価値を出した入力の分布 \(l(x)\) 」と「悪い評価値を出した入力の分布 \(g(x)\) 」を別々に推定します。しきい値 \(y^{*}\) は評価値の下位 \(\gamma\) 分位点(最小化の場合。Optunaのデフォルトは観測数に応じて変化し、おおむね上位10〜25%)です。

EIの最大化は密度比の最大化と等価

この定式化の見どころは、 ベイズ最適化の基礎 で扱った期待改善量(EI)が、ベイズの定理を通じて密度比に化けることです。EIの定義から出発します。

\[ \mathrm{EI}_{y^{*}}(x) = \int_{-\infty}^{y^{*}} (y^{*} - y)\, p(y \mid x)\, dy \tag{2} \]

ベイズの定理 \(p(y \mid x) = p(x \mid y)\,p(y)/p(x)\) を代入し、\(p(x) = \int p(x \mid y) p(y) dy = \gamma\, l(x) + (1 - \gamma)\, g(x)\) を使って整理すると(導出の詳細は Bergstra et al. (2011))、

\[ \mathrm{EI}_{y^{*}}(x) \propto \left( \gamma + \frac{g(x)}{l(x)} (1 - \gamma) \right)^{-1} \tag{3} \]

が得られます。式 \((3)\) は \(g(x)/l(x)\) の単調減少関数なので、EIを最大化する \(x\) は密度比 \(l(x)/g(x)\) を最大化する \(x\) と同じです。つまりTPEの1試行は、

  1. 観測を上位 \(\gamma\) (良い群)と残り(悪い群)に分ける
  2. それぞれの入力分布 \(l(x)\) ・\(g(x)\) をParzen窓(カーネル密度推定)で推定する
  3. \(l(x)\) から候補点をサンプリングし、\(l(x)/g(x)\) が最大の候補を次に評価する

だけで実現できます。GPのような \(O(n^3)\) の行列計算は一切登場しません。

名前にある「Tree-structured」は、suggest_int → その値に応じて suggest_float が変わる、といった条件付き(木構造)の探索空間でも、各パラメータの1次元密度をノードごとに独立に持てばそのまま動く、という設計を指しています。カテゴリカル変数も、密度推定をカテゴリ頻度に置き換えるだけで扱えます。

約40行のフルスクラッチ実装

上の3ステップを scipy.stats.gaussian_kde でそのまま書きます。対象は多峰の1次元関数

\[ f(x) = \sin 3x + 0.5x^{2} - 0.7x, \quad x \in [-2, 3] \tag{4} \]

で、20,001点のグリッド評価による大域最適値は \(f(1.483) = -0.904\) です(局所解 \(x \approx -0.5\) が罠として存在します)。

import numpy as np
from scipy.stats import gaussian_kde


def f(x):
    return np.sin(3 * x) + 0.5 * x**2 - 0.7 * x


XLO, XHI = -2.0, 3.0


def tpe_minimize(n_trials=30, n_init=8, gamma=0.25, n_cand=24, seed=0):
    rng = np.random.default_rng(seed)
    xs, ys = [], []
    for i in range(n_trials):
        if i < n_init:  # 最初はランダムに初期化
            x = rng.uniform(XLO, XHI)
        else:
            order = np.argsort(ys)
            n_good = max(2, int(np.ceil(gamma * len(ys))))
            good = np.array(xs)[order[:n_good]]   # 良い観測(上位γ)
            bad = np.array(xs)[order[n_good:]]    # 悪い観測(残り)
            l = gaussian_kde(good, bw_method=0.35)
            g = gaussian_kde(bad, bw_method=0.35)
            cand = l.resample(n_cand, seed=rng.integers(1 << 31)).ravel()
            cand = np.clip(cand, XLO, XHI)
            x = cand[np.argmax(l(cand) / np.maximum(g(cand), 1e-12))]
        xs.append(float(x))
        ys.append(float(f(x)))
    return np.array(xs), np.array(ys)

あるシードの20試行後の内部状態を可視化したものが次の図です。

TPEの内部状態の可視化。上段: 目的関数と観測点(良い観測5点は局所解x≈-0.5付近に集中、悪い観測15点は全域に散在)。中段: 良い観測の密度l(x)がx≈-0.5に鋭いピークを持ち、悪い観測の密度g(x)は平坦。下段: 密度比l(x)/g(x)もx=-0.48で最大となり、次の提案点が局所解のそばに出ている

「良い観測が集まっている場所を高く、悪い観測が集まっている場所を低く評価してサンプリングする」というTPEの動きがそのまま見えます。同時に、この試行が局所解 \(x \approx -0.5\) に張り付いてしまっていることも見て取れます。良い観測が局所解の周りに集中すると \(l(x)\) のピークがそこに立ち、提案もそこに集中する——素朴なTPEの弱点そのものです。

正直な結果:この40行はランダムサーチに勝てない

50シード × 30試行で、この自作TPEとランダムサーチの「見つけた最良値と大域最適値の差」を比較すると次のようになりました。

手法最良値の平均ギャップ(50シード)標準偏差
自作TPE(40行)0.2060.212
ランダムサーチ0.0460.073

ランダムサーチの圧勝です。1次元・区間幅5の探索空間では30回の一様サンプリングだけで大域最適の近傍をほぼ確実に踏めるのに対し、素朴なTPEは局所解に張り付いたシードが平均を悪化させます。候補点を \(l(x)\) からしか引かない実装は、探索(exploration)の仕組みを何も持っていないためです。

実際のOptunaのTPESamplerには、この弱点を埋める工夫が積まれています。

  • 事前分布の混合: \(l(x)\) ・\(g(x)\) に探索空間全体をカバーする事前分布成分を混ぜ、密度がゼロになる領域をなくす(Bergstraの原論文の時点で導入済み)
  • 帯域幅の調整則: 観測数・値域に応じたカーネル帯域幅の自動調整
  • n_startup_trials(デフォルト10): 序盤はランダムサンプリングに委ねる
  • multivariate TPE: パラメータごとに独立な1次元KDEではなく、多変量KDEで相関を捉える(後述の実験で効果を確認します)

「アルゴリズムの骨格は40行で書けるが、実用性能はエンジニアリングの積み重ねが担っている」——これがTPEを自作してみて得られる一番の学びです。

Braninベンチマーク:TPE vs GP-EI vs ランダム

次元が上がると話が変わります。標準ベンチマークのBranin関数(2次元、大域最適値 \(0.397887\) )で、各60試行 × 30シードの平均収束を比較しました。Optuna 4.9.0 を使用しています。

import optuna

optuna.logging.set_verbosity(optuna.logging.WARNING)


def branin(x1, x2):
    a, b, c = 1.0, 5.1 / (4 * np.pi**2), 5 / np.pi
    r, s, t = 6.0, 10.0, 1 / (8 * np.pi)
    return a * (x2 - b * x1**2 + c * x1 - r) ** 2 + s * (1 - t) * np.cos(x1) + s


def objective(trial):
    x1 = trial.suggest_float("x1", -5, 10)
    x2 = trial.suggest_float("x2", 0, 15)
    return branin(x1, x2)


# サンプラーを差し替えて比較する
samplers = {
    "random": lambda s: optuna.samplers.RandomSampler(seed=s),
    "tpe": lambda s: optuna.samplers.TPESampler(seed=s),
    "tpe_mv": lambda s: optuna.samplers.TPESampler(seed=s, multivariate=True),
}
study = optuna.create_study(sampler=samplers["tpe"](0))
study.optimize(objective, n_trials=60)

比較対象のGP-EIは、 ベイズ最適化の基礎 と同系の sklearn GaussianProcessRegressor(Matérn 5/2カーネル)+ EI最大化の自作実装です。結果は次のとおりです。

Branin関数での収束曲線比較(30シード平均、対数軸)。GP-EIが一貫して最速で60試行後にギャップ0.001未満まで到達。TPE(multivariate=True)がそれに続き、独立TPE、ランダムサーチの順。ランダムサーチは20試行以降ほぼ改善が止まる

手法20試行後のギャップ60試行後のギャップ(±標準偏差)
ランダムサーチ3.201.061 ± 0.987
TPE(デフォルト・独立)2.490.268 ± 0.304
TPE(multivariate=True)1.530.180 ± 0.143
GP-EI(自作)0.270.0009 ± 0.0010

3つの観察ができます。

  1. TPEはランダムサーチに明確に勝つ(ギャップで約4〜6倍の差)。1次元では出なかった優位が、2次元でははっきり現れます。
  2. multivariate=True は無料の改善です。Braninのように入力次元間に相関がある関数では、独立TPEより一貫して速く、ばらつきも半分以下になりました。OptunaではまだデフォルトOFFなので、連続パラメータ主体の問題では明示的に有効化する価値があります。
  3. 低次元・連続・滑らかな問題ではGP-EIが圧勝します。60試行でギャップ0.001未満は、TPE系より2桁以上良い値です。

それでもOptunaがTPEをデフォルトに据えているのは、実務のチューニング問題が「低次元・連続・滑らか」ではないからです。カテゴリカル変数(optimizer の種類など)や条件付きパラメータ(層数によって変わる各層のユニット数)が混ざる木構造の空間はGPでは素直に扱えず、TPEなら自然に扱えます。また、GPは観測数 \(n\) に対して \(O(n^3)\) の再学習を毎試行繰り返すのに対し、TPEはKDEの評価だけなので試行数が増えても軽いままです。

実データでの費用対効果:breast_cancer × HistGradientBoosting

最後に「実際のところ、チューニングでどれだけ上がるのか」を実測します。breast_cancerデータ(569サンプル)で HistGradientBoostingClassifier の4パラメータを調整し、5-fold CVのROC-AUCを最大化します。40試行 × 5シードです。

from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import cross_val_score

X, y = load_breast_cancer(return_X_y=True)


def objective(trial):
    model = HistGradientBoostingClassifier(
        learning_rate=trial.suggest_float("learning_rate", 0.01, 0.5, log=True),
        max_iter=trial.suggest_int("max_iter", 50, 300),
        max_leaf_nodes=trial.suggest_int("max_leaf_nodes", 7, 63),
        l2_regularization=trial.suggest_float("l2_regularization", 1e-6, 1.0, log=True),
        random_state=0,
    )
    return cross_val_score(model, X, y, cv=5, scoring="roc_auc").mean()


study = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=0))
study.optimize(objective, n_trials=40)

breast_cancerデータでのチューニング曲線(5シード平均)。デフォルト設定のAUC 0.9930に対し、ランダムサーチ・Optuna TPEともに10試行前後で0.9946に到達し、以降はほぼ横ばい。両者の曲線はほとんど重なっている

設定5-fold CV ROC-AUC
デフォルトのHistGB0.99302
ランダムサーチ(40試行後)0.99498 ± 0.00041
Optuna TPE(40試行後)0.99482 ± 0.00026

ここでも正直に書くと、TPEとランダムサーチの差は誤差の範囲でした(むしろランダムの平均がわずかに上)。理由ははっきりしていて、この問題は「デフォルトが既に強く、広い領域のパラメータが同じくらい良い性能を出す」ため、10試行もあればどちらの手法でも天井付近(+0.002)に届いてしまうのです。

これは「チューニングは無意味」という意味ではなく、手法差が出るのは探索空間が広い・評価が高コストで試行数を絞りたい・パラメータ間の相互作用が強い問題だ、という切り分けです。Braninの実験が示したとおり、当たり領域が狭い問題では明確な差がつきます。逆に、今回のような素直な問題でランダムサーチとの差を誇張するベンチマークを見たら、疑ってかかるべきです。

まとめ

項目内容
TPEの核心観測を上位 \(\gamma\) で二分し、EI最大化と等価な密度比 \(l(x)/g(x)\) 最大化で次の点を選ぶ
GPとの違い\(p(y \mid x)\) でなく \(p(x \mid y)\) をモデル化。\(O(n^3)\) 不要、カテゴリカル・条件付き空間も自然に扱える
自作40行の実力1次元・30試行ではランダムサーチに負ける(ギャップ 0.206 vs 0.046)。実用性能は事前分布混合などの工夫が担う
Branin(2次元)ギャップはランダム 1.06 > TPE 0.27 > TPE-mv 0.18 ≫ GP-EI 0.0009。低次元連続ではGPが圧勝
multivariate=True相関のある空間で一貫して改善(ばらつきも半減)。連続パラメータ主体なら有効化を推奨
実データの現実強いデフォルト+簡単な問題では、40試行のTPEとランダムの差は誤差範囲(AUC +0.002 はどちらでも到達)

おすすめ書籍

TPEの構成要素であるParzen窓(カーネル密度推定)の理論は、次の1冊の第2章「確率分布」でノンパラメトリック密度推定として体系的に学べます。

パターン認識と機械学習 上(C.M.ビショップ、丸善出版)

「良い観測・悪い観測の分布を推定して比を取る」というTPEのベイズ的な発想に馴染むには、ベイズ推論そのものの入門書が近道です。

ベイズ推論による機械学習入門(須山敦志、講談社)

関連記事

参考文献

  • Bergstra, J., Bardenet, R., Bengio, Y., & Kégl, B. (2011). “Algorithms for Hyper-Parameter Optimization”. Advances in Neural Information Processing Systems 24 (NIPS 2011).
  • Akiba, T., Sano, S., Yanase, T., Ohta, T., & Koyama, M. (2019). “Optuna: A Next-generation Hyperparameter Optimization Framework”. Proceedings of the 25th ACM SIGKDD.
  • Watanabe, S. (2023). “Tree-Structured Parzen Estimator: Understanding Its Algorithm Components and Their Roles for Better Empirical Performance”. arXiv:2304.11127
  • Optuna documentation — TPESampler