論文一覧に戻る 📚 用語解説(ジャストインタイム型データサイエンス教育)
Optuna
Optuna — Hyperparameter Optimization Framework
Preferred Networks 製の ハイパーパラメータ自動最適化フレームワーク
TPE(ベイズ最適化)や CMA-ES を内蔵し、 数行で書ける枝刈り (pruning)並列実行 が標準装備。
ハイパーパラメータ最適化 ベイズ最適化 TPE Pythonライブラリ
💡 30秒結論 📍 文脈 🎨 直感 📐 数式・定義 🔬 記号の読み解き 🧮 SSDSE-B で計算 🐍 Python 実装 ⚠️ 落とし穴 🌐 関連手法 🔗 関連用語 📚 グループ教材 🗺 概念マップ

💡 30 秒で分かる結論

📍 あなたが今見ているもの

機械学習の論文や Kaggle 解法では、 次のような記述をよく見かけます:

Optuna v3.4 を用い、 LightGBM の 7 個のハイパーパラメータを TPE サンプラー + MedianPruner200 trial 探索した。
その結果、 5-fold CV の RMSE が 1.842 → 1.471 へと 20.1% 改善 した。

この Optuna は、 機械学習モデルの「最適なハイパーパラメータの組み合わせ」を効率的に探索するフレームワークです。 「学習率は 0.01? 0.1?」「木の深さは 5? 8?」を 人手で当てずっぽうに変える のではなく、 過去の試行結果から学習して次の候補を提案 する。 これにより、 同じ計算予算で得られるモデル性能が大きく向上 します。 LightGBM・XGBoost・scikit-learn・PyTorch のいずれとも組み合わせ可能で、 国内外の Kaggle 上位解法の標準ツールです。

🎨 直感で掴む — 「賢い宝探し」のアルゴリズム

あなたは広い遊園地で宝箱を探しているとします。 1 つの場所を掘るのに 1 分かかり、 1 時間しかありません。 どう動くべきでしょうか?

戦略動きたとえ
グリッドサーチ 遊園地を 10×10 の網目に区切り、 全マス順番に掘る 「全部試す」。 確実だが時間が足りない
ランダムサーチ サイコロを振って 60 マスをランダムに選んで掘る 「運任せ」。 グリッドより効率はよいことが多い
Optuna(TPE) 掘った場所で「冷たい / 温かい」のヒントを得て、 「温かい」方向の周辺を集中的に掘る 「ヒントを使う」。 限られた試行で高確率に最良点に近づく

TPE のキモ:良かった群と悪かった群を分けてモデル化

Optuna のデフォルトサンプラー TPE (Tree-structured Parzen Estimator) は、 これまでの試行 (パラメータ → 目的関数値) を次のように扱います:

つまり Optuna は:
  1. 最初はランダムに 10〜20 trial 探索(warm-up)。 探索空間を「軽く眺める」。
  2. その後は 過去の良かった試行 の周辺を集中的にサンプリング。
  3. 同時に 悪かった領域からは離れる 方向に動く。
人間の「コツを掴んで作業効率を上げる」プロセスに極めて近いのです。

📐 数式 — TPE の定式化

ハイパーパラメータ空間を $\mathcal{X}$、 目的関数を $f: \mathcal{X} \to \mathbb{R}$ とする。 目標は最小化問題:

【ハイパーパラメータ最適化の一般形】
$$x^{*} = \arg\min_{x \in \mathcal{X}} f(x)$$
$f(x)$ は 1 回の評価に 数分〜数時間 かかる「ブラックボックス関数」。 微分も使えない。

TPE の中核:2 つの密度比

過去の試行集合を $D = \{(x_1, y_1), \dots, (x_n, y_n)\}$、 値のしきい値(上位 $\gamma$ 分位点、 デフォルト $\gamma \approx 0.25$)を $y^{*}$ とする:

【条件付き密度の分割】
$$p(x \mid y) = \begin{cases} \ell(x) & \text{if } y < y^{*} \quad (\text{良かった群}) \\ g(x) & \text{if } y \ge y^{*} \quad (\text{悪かった群}) \end{cases}$$
$\ell(x), g(x)$ はそれぞれ Parzen 窓(カーネル密度推定)で求める。
【期待改善 (Expected Improvement) と TPE の獲得関数】
$$\mathrm{EI}_{y^{*}}(x) \propto \frac{\ell(x)}{g(x)}$$
これを 最大化する $x$ を次の試行に採用。 比率なので、 「良かった分布で大、 悪かった分布で小」を狙う。

枝刈り (Pruning) の判定式

【MedianPruner】
$$\text{step } s \text{ で打ち切り} \iff y_{\text{current}}(s) > \mathrm{median}\bigl\{y_{i}(s) \mid i \in \text{過去 trial}\bigr\}$$
学習途中の中間値が「過去 trial の中央値より悪い」なら打ち切り。

🔬 数式を「言葉」で読み解く

$x$(ハイパーパラメータ)
学習率、 木の深さ、 正則化係数などの 調整可能な設定値。 通常 5〜30 次元のベクトル。
$f(x)$(目的関数)
「その $x$ でモデルを学習して検証データで評価した結果」。 RMSE・MAE・log-loss・AUC など。 微分不可能・ノイズあり・高コスト
$y^{*}$(分位点しきい値)
これまでの試行のうち上位 $\gamma$(例:25%)の境界値。 これより良い試行を「良かった群」と定義する。
$\ell(x)$, $g(x)$
それぞれ「良かった群」「悪かった群」の パラメータ空間における分布。 Parzen 窓(カーネル密度推定)で推定。
$\ell(x) / g(x)$
「ここを試したらどれだけ改善が期待できるか」の指標。 これが大きい点を選んで次の試行とする。
trial
Optuna で 1 回の (パラメータ設定 → 評価値計算) のサイクル。 study.optimize(objective, n_trials=100) なら 100 試行。
study
1 つの最適化セッション。 すべての trial の履歴を保持し、 ベスト値・収束曲線・パラメータ重要度などを集約。
pruning
学習エポックの途中で見込みのない trial を打ち切ること。 epoch 数の多い深層学習で特に有効。

🧮 SSDSE-B 都道府県データで実際に最適化してみる

SSDSE-B 2026 の 47 都道府県(年度パネル)を使って、 「翌年の人口総数を予測する LightGBM 回帰モデル」 のハイパーパラメータを Optuna で 100 trial 探索します。

タスク設計

探索空間(7 パラメータ)

パラメータ範囲スケール役割
learning_rate0.005 〜 0.3対数1 本の木の影響度。 小さいと精度↑だが trial 時間↑
num_leaves15 〜 255整数木の複雑さ。 大きすぎると過学習
max_depth3 〜 12整数木の最大深さ。 上限を設けて過学習抑制
min_child_samples5 〜 100整数葉に必要な最低サンプル数
subsample0.5 〜 1.0連続行サンプリング率
colsample_bytree0.5 〜 1.0連続列サンプリング率
reg_lambda1e-3 〜 10.0対数L2 正則化強度

結果の典型例(数値は再現実行で変動)

STEP 1 デフォルト値での RMSE
5-fold CV-RMSE ≈ 1.842(万人単位)
STEP 2 Optuna 100 trial 後のベスト
learning_rate = 0.041, num_leaves = 63, max_depth = 7, min_child_samples = 24,
subsample = 0.81, colsample_bytree = 0.74, reg_lambda = 0.18
CV-RMSE = 1.471(−20.1%)
STEP 3 パラメータ重要度(fANOVA)
最も効いたのは learning_rate (寄与 ≈ 0.34) と num_leaves (0.27)。
subsamplecolsample_bytree は寄与 ≈ 0.05 程度で、 デフォルトでもよかった。

🐍 Python 実装 — SSDSE-B で LightGBM × Optuna

1. 最小構成(10 行で動く)

1
2
3
4
5
6
7
8
9
import optuna

def objective(trial):
    x = trial.suggest_float('x', -10, 10)
    return (x - 2) ** 2

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)
print(study.best_params, study.best_value)

2. SSDSE-B で LightGBM のハイパラを 100 trial 探索

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import pandas as pd
import numpy as np
import optuna
from lightgbm import LGBMRegressor
from sklearn.model_selection import GroupKFold
from sklearn.metrics import mean_squared_error

# データ読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8-sig', header=1)
df = df.sort_values(['地域', '年度']).reset_index(drop=True)
df['人口_翌年'] = df.groupby('地域')['A1101'].shift(-1)
df = df.dropna(subset=['人口_翌年'])

feat_cols = ['A1101', 'A1102', 'A4101', 'A4200', 'A6101', 'B1101']
X = df[feat_cols].values
y = df['人口_翌年'].values
groups = df['地域'].values

def objective(trial):
    params = {
        'learning_rate': trial.suggest_float('learning_rate', 5e-3, 0.3, log=True),
        'num_leaves':    trial.suggest_int('num_leaves', 15, 255),
        'max_depth':     trial.suggest_int('max_depth', 3, 12),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        'subsample':     trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'reg_lambda':    trial.suggest_float('reg_lambda', 1e-3, 10.0, log=True),
        'n_estimators':  800,
        'random_state':  42,
        'verbose': -1,
    }
    cv = GroupKFold(n_splits=5)
    rmses = []
    for tr, va in cv.split(X, y, groups):
        model = LGBMRegressor(**params)
        model.fit(X[tr], y[tr])
        pred = model.predict(X[va])
        rmses.append(np.sqrt(mean_squared_error(y[va], pred)))
    return np.mean(rmses)

study = optuna.create_study(direction='minimize',
                            sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(objective, n_trials=100, show_progress_bar=True)

print('best RMSE :', study.best_value)
print('best params:', study.best_params)

3. 枝刈り (Pruning) で計算を半分にする

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from optuna.integration import LightGBMPruningCallback

def objective_pruned(trial):
    params = {# ...上と同じ...}
    cv = GroupKFold(n_splits=5)
    rmses = []
    for fold, (tr, va) in enumerate(cv.split(X, y, groups)):
        model = LGBMRegressor(**params)
        model.fit(X[tr], y[tr],
                  eval_set=[(X[va], y[va])],
                  callbacks=[LightGBMPruningCallback(trial, 'rmse',
                                                     valid_name='valid_0')])
        pred = model.predict(X[va])
        rmses.append(np.sqrt(mean_squared_error(y[va], pred)))
    return np.mean(rmses)

study = optuna.create_study(direction='minimize',
                            pruner=optuna.pruners.MedianPruner(n_warmup_steps=50))
study.optimize(objective_pruned, n_trials=200)

4. 結果の可視化(収束曲線・重要度・パラレル座標)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import optuna.visualization as vis

# trial 数 vs ベスト値(収束曲線)
vis.plot_optimization_history(study).show()

# どのパラメータが効いたか(fANOVA)
vis.plot_param_importances(study).show()

# パラメータ空間の探索軌跡(パラレル座標)
vis.plot_parallel_coordinate(study).show()

# 2 パラメータの目的値ランドスケープ
vis.plot_contour(study, params=['learning_rate', 'num_leaves']).show()

5. SQLite ストレージで並列実行

1
2
3
4
5
6
7
8
9
# 共通ストレージを指定すれば、複数プロセス・複数マシンで study を共有できる
study = optuna.create_study(
    study_name='ssdse_lgbm_pop',
    storage='sqlite:///optuna_study.db',
    direction='minimize',
    load_if_exists=True,
)
# 別ターミナルでも同じコードを起動すれば、自動で trial が分散される
study.optimize(objective, n_trials=50)

⚠️ 5 つの「Optuna 落とし穴」

① 検証データに対して過学習する (over-tuning)
Optuna は 検証 RMSE を 0.0001 でも下げる方向 に動きます。 trial 数が多すぎる(数千 trial)と、 検証データのノイズに過剰適合 してテスト性能が悪化することがある。
対処:① nested CV(外側 CV でモデル評価、 内側 CV で Optuna)を使う、 ② trial 数を 50〜200 程度に抑える、 ③ ベストではなく上位 5 % の平均パラメータを採用する。
② 探索範囲(suggest の min/max)が不適切
learning_rate の範囲を 0.1 〜 0.5 に狭めてしまうと、 真の最適 0.04 を絶対に見つけられない。
対処:① 初回は 広め+対数スケールlog=True)を使う、 ② ベスト trial が範囲の端に張り付いていたら範囲を広げて再探索、 ③ 文献・公式ドキュメントの推奨範囲を参考にする。
③ シードを固定しないと結果が再現できない
TPE はランダム要素を含むため、 seed を指定しないと毎回違う結果 になります。 論文・コンペでは再現性が命。
対処TPESampler(seed=42)、 モデル側にも random_state=42 を渡す。 numpy/torch のシードも合わせて固定
④ 多目的最適化を忘れる
「精度」だけでなく「推論時間」「モデルサイズ」も気にする場面では、 単目的最適化はミスリーディング。
対処directions=['minimize', 'minimize'] として 多目的最適化(NSGA-II) を使い、 パレート最適解を取得。
⑤ デフォルトサンプラー(TPE)が最強とは限らない
TPE は 独立な離散・連続変数の混合空間に強い が、 パラメータ間の強い相関がある場合は CMA-ES が有利。 高次元では GP-BOSMAC も検討。
対処optuna.samplers.CmaEsSampler()optuna.samplers.RandomSampler() をベンチマーク比較する。

🌐 関連手法・派生

手法/ライブラリ特徴Optuna との関係
グリッドサーチ格子状の探索点を全部試す少数パラメータ・小範囲で有効。 Optuna でも GridSampler 利用可
ランダムサーチ探索空間からランダム抽出多次元では Grid よりよい。 Optuna で RandomSampler
ベイズ最適化 (GP)ガウス過程で目的関数をモデル化scikit-optimize、 GPyOpt が代表。 Optuna も内蔵可
HyperoptTPE の元祖実装Optuna は Hyperopt を進化させた後発
Ray Tune分散環境特化、 多数のサンプラークラウド・GPU 多数なら Ray が強い
scikit-optimizesklearn 互換 BOscikit-learn 標準ワークフローに統合しやすい
HyperBand / BOHB枝刈りに特化Optuna の HyperbandPruner として利用可能

📚 関連グループ教材

🗺 概念マップ — Optuna の位置づけ

階層 概念 関係
最上位数理最適化Optuna はブラックボックス最適化の実装
上位ハイパーパラメータ最適化機械学習特化の自動チューニング
同列Hyperopt, Ray Tune, scikit-optimize同種のフレームワーク
内部アルゴリズムTPE, CMA-ES, NSGA-IIサンプラーとして選択可
前提交差検証、 損失関数、 機械学習モデルこれらと組み合わせて動く
応用AutoML、 NAS(Neural Architecture Search)Optuna は両者の中核要素