数行で書ける・枝刈り (pruning) ・並列実行 が標準装備。主要キーワード・別名・関連語のクロスリファレンス。
「optuna」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「optuna」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「optuna の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
設定を賢く決めるための道具です。
AIの性能を効率よく上げるために使います。
スマホのアプリ設定を最適にするようなものです。
この章ではOptunaの便利な機能を紹介します。
trial.suggest_float('lr', 1e-5, 1e-1, log=True) のように Python の制御フローの中で探索空間を定義 できる。storage に指定すれば、 複数プロセス・複数ノードでの並列実行が可能。🍰 まずはやさしく
AIの設定を自動で探すツールです。
手作業で設定を変える手間をなくします。
部活の練習メニューを最適に選ぶ感覚です。
ここでは実際の使われ方について解説します。
機械学習の論文や Kaggle 解法では、 次のような記述をよく見かけます:
この Optuna は、 機械学習モデルの「最適なハイパーパラメータの組み合わせ」を効率的に探索するフレームワークです。 「学習率は 0.01? 0.1?」「木の深さは 5? 8?」を 人手で当てずっぽうに変える のではなく、 過去の試行結果から学習して次の候補を提案 する。 これにより、 同じ計算予算で得られるモデル性能が大きく向上 します。 LightGBM・XGBoost・scikit-learn・PyTorch のいずれとも組み合わせ可能で、 国内外の Kaggle 上位解法の標準ツールです。
🍰 まずはやさしく
賢い宝探しのような仕組みです。
少ない回数で正解を見つけるために使います。
ゲームの攻略法を効率よく探す感覚です。
ここでは仕組みを直感的に説明します。
あなたは広い遊園地で宝箱を探しているとします。 1 つの場所を掘るのに 1 分かかり、 1 時間しかありません。 どう動くべきでしょうか?
| 戦略 | 動き | たとえ |
|---|---|---|
| グリッドサーチ | 遊園地を 10×10 の網目に区切り、 全マス順番に掘る | 「全部試す」。 確実だが時間が足りない |
| ランダムサーチ | サイコロを振って 60 マスをランダムに選んで掘る | 「運任せ」。 グリッドより効率はよいことが多い |
| Optuna(TPE) | 掘った場所で「冷たい / 温かい」のヒントを得て、 「温かい」方向の周辺を集中的に掘る | 「ヒントを使う」。 限られた試行で高確率に最良点に近づく |
Optuna のデフォルトサンプラー TPE (Tree-structured Parzen Estimator) は、 これまでの試行 (パラメータ → 目的関数値) を次のように扱います:
🍰 まずはやさしく
最適な値を求めるための計算式です。
数学的に正しい答えを導き出すために使います。
テストの点数を上げる勉強法を探す感覚です。
ここでは計算のルールについて詳しく読みます。
ハイパーパラメータ空間を $\mathcal{X}$、 目的関数を $f: \mathcal{X} \to \mathbb{R}$ とする。 目標は最小化問題:
過去の試行集合を $D = \{(x_1, y_1), \dots, (x_n, y_n)\}$、 値のしきい値(上位 $\gamma$ 分位点、 デフォルト $\gamma \approx 0.25$)を $y^{*}$ とする:
Optuna のデフォルトサンプラー TPE はベイズ最適化の一族。 過去の試行結果から、 目的関数 f(x) が小さくなりそうな x を逐次提案する。 Grid Search や Random Search が「無情報」なのに対し、 TPE は履歴を活用する。
📐 TPE の中核は次の比例関係。 数式を言葉で読み解くと: 観測済みデータを「目的関数値の良い部分集合 (下位 γ 分位)」と「悪い残り」に分け、 それぞれを Parzen 窓 (=カーネル密度推定) で密度 ℓ(x) と g(x) として推定。 次に試す x は ℓ(x)/g(x) を最大化する点 ── つまり「良い結果が出やすく、 悪い結果が出にくい」点を選ぶ。
$$ x_{\text{next}} = \arg\max_x \frac{\ell(x)}{g(x)} \quad \text{where} \quad \ell(x) = p(x \mid y < y^*),\; g(x) = p(x \mid y \geq y^*) $$これは Expected Improvement (EI) の近似最大化に等価であることが理論的に示されている (Bergstra et al. 2011)。 TPE の利点は (1) 高次元 (~数十次元) でも安定、 (2) カテゴリ・条件付き探索空間を自然に扱える、 (3) 計算コストが低い。
| サンプラー | 原理 | 推奨用途 |
|---|---|---|
TPESampler (default) | ベイズ流カーネル密度推定 | 汎用、 ハイブリッド空間で強い |
CmaEsSampler | CMA-ES (進化的アルゴリズム) | 連続変数のみ、 高次元 |
NSGAIISampler | 遺伝的アルゴリズム + Pareto | 多目的最適化 |
RandomSampler | 単純ランダム | ベースライン、 デバッグ |
GridSampler | 格子全探索 | 低次元・離散、 再現性必須 |
QMCSampler | 準モンテカルロ (Sobol/Halton) | 初期探索の網羅性 |
💬 実務的には「まず TPE 100 trials」で始め、 連続変数のみで高次元なら CMA-ES、 多目的なら NSGA-II、 という選び方が標準。 全部 default のままで Optuna が「賢く」動く点が他のフレームワークに対する優位性。
study.optimize(objective, n_trials=100) なら 100 試行。SSDSE-B 2026 の 47 都道府県(年度パネル)を使って、 「翌年の人口総数を予測する LightGBM 回帰モデル」 のハイパーパラメータを Optuna で 100 trial 探索します。
LGBMRegressor| パラメータ | 範囲 | スケール | 役割 |
|---|---|---|---|
learning_rate | 0.005 〜 0.3 | 対数 | 1 本の木の影響度。 小さいと精度↑だが trial 時間↑ |
num_leaves | 15 〜 255 | 整数 | 木の複雑さ。 大きすぎると過学習 |
max_depth | 3 〜 12 | 整数 | 木の最大深さ。 上限を設けて過学習抑制 |
min_child_samples | 5 〜 100 | 整数 | 葉に必要な最低サンプル数 |
subsample | 0.5 〜 1.0 | 連続 | 行サンプリング率 |
colsample_bytree | 0.5 〜 1.0 | 連続 | 列サンプリング率 |
reg_lambda | 1e-3 〜 10.0 | 対数 | L2 正則化強度 |
learning_rate (寄与 ≈ 0.34) と num_leaves (0.27)。subsample や colsample_bytree は寄与 ≈ 0.05 程度で、 デフォルトでもよかった。
合成データで 5 試行のハイパラ探索結果から最良値を取得する。
| 試行 | lr | n_est | val acc |
|---|---|---|---|
| 1 | 0.10 | 50 | 0.78 |
| 2 | 0.05 | 100 | 0.82 |
| 3 | 0.01 | 200 | 0.85 |
| 4 | 0.02 | 150 | 0.87 |
| 5 | 0.03 | 180 | 0.86 |
1 2 3 4 5 6 7 8 9 10 | import numpy as np trials = [ ('t1', 0.10, 50, 0.78), ('t2', 0.05, 100, 0.82), ('t3', 0.01, 200, 0.85), ('t4', 0.02, 150, 0.87), ('t5', 0.03, 180, 0.86), ] best = max(trials, key=lambda x: x[3]) print(f"最良試行: {best[0]} (val={best[3]})") |
💬 手計算 (Step 2) と Python 出力が完全一致。
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) |
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 47 48 | 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='cp932', skiprows=[1]) df = df.sort_values(['Code', 'SSDSE-B-2026']).reset_index(drop=True) df['人口_翌年'] = df.groupby('Code')['A1101'].shift(-1) df = df.dropna(subset=['人口_翌年']) feat_cols = ['A1101', 'A1301', 'A4101', 'A1303', 'B4101', 'J2503'] X = df[feat_cols].values y = df['人口_翌年'].values groups = df['Code'].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, # 517 行と小さいので並列化は逆効果(1 本の方が 100 倍速い) 'n_jobs': 1, '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) |
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) |
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() |
1 2 3 4 5 6 7 8 9 10 11 | import optuna # 共通ストレージを指定すれば、複数プロセス・複数マシンで 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) |
learning_rate の範囲を 0.1 〜 0.5 に狭めてしまうと、 真の最適 0.04 を絶対に見つけられない。log=True)を使う、 ② ベスト trial が範囲の端に張り付いていたら範囲を広げて再探索、 ③ 文献・公式ドキュメントの推奨範囲を参考にする。
TPESampler(seed=42)、 モデル側にも random_state=42 を渡す。 numpy/torch のシードも合わせて固定。
directions=['minimize', 'minimize'] として 多目的最適化(NSGA-II) を使い、 パレート最適解を取得。
optuna.samplers.CmaEsSampler() や optuna.samplers.RandomSampler() をベンチマーク比較する。
| 落とし穴 | 兆候 | 対処 |
|---|---|---|
| 探索空間が狭すぎる | Best が境界値に張り付く | 範囲を広げて再試行 (log 軸推奨) |
| 探索空間が広すぎる | 100 trial でも収束しない | 事前 EDA で粗い範囲を決めてから絞る |
| CV の random_state が trial 内で変動 | 同じ params で別 R² が出る (再現性なし) | KFold(shuffle=True, random_state=固定) |
| CV 分散が大きく overfit | CV スコアと test スコアが乖離 | Nested CV、 hold-out test を分離 |
| log=True を忘れた連続変数 | 学習率や正則化が線形空間で探される | suggest_float(name, lo, hi, log=True) |
| Pruning が積極的すぎる | Best 候補も序盤の運で切られる | n_warmup_steps を増やす (5〜10) |
| 並列実行で seed が衝突 | 同じ trial が複数回走る | RDBStorage + JobLib、 各 worker に seed 渡す |
| trial.suggest_* を for ループ外に書く | 同じ値しか出ない (固定される) | objective(trial) の中で必ず suggest 呼ぶ |
| 用途 | コード |
|---|---|
| 基本: study 作成 + 最適化 | study = optuna.create_study(direction='maximize'); study.optimize(obj, n_trials=100) |
| 整数パラメータ | trial.suggest_int('depth', 2, 10) |
| 連続パラメータ (log 軸) | trial.suggest_float('lr', 1e-4, 1.0, log=True) |
| カテゴリ | trial.suggest_categorical('opt', ['adam','sgd','rmsprop']) |
| 条件付き探索 | if trial.suggest_categorical(...) == 'adam': lr = trial.suggest_float(...) |
| 並列実行 (4 worker) | study.optimize(obj, n_trials=400, n_jobs=4) |
| 永続化 (SQLite) | create_study(storage='sqlite:///study.db', study_name='exp1', load_if_exists=True) |
| 可視化 (履歴) | optuna.visualization.plot_optimization_history(study) |
| 可視化 (パラメータ重要度) | optuna.visualization.plot_param_importances(study) |
| 可視化 (Pareto front) | optuna.visualization.plot_pareto_front(study) |
| 学習曲線監視 + 打切り | trial.report(score, step); if trial.should_prune(): raise optuna.TrialPruned() |
| 年 | 手法 / 実装 | 意義 |
|---|---|---|
| 1990s | Grid Search / Manual Tuning | パラメータ数が爆発、 専門家の勘頼み |
| 2012 | Bergstra & Bengio: Random > Grid | 「ランダム探索の方が効率的」を実証 |
| 2011 | TPE 提案 (Bergstra) | ベイズ流カーネル密度 + EI |
| 2013 | Hyperopt (PyMC ベース) | TPE の最初のメジャー実装 |
| 2017 | Hyperband / Successive Halving | multi-fidelity 最適化、 リソース効率 |
| 2018 | Optuna 1.0 リリース (PFN) | Define-by-run、 動的探索空間 |
| 2019 | BoTorch (Facebook) / Ax | PyTorch ベースの最先端 BO |
| 2020+ | Optuna v3 (NSGA-II 強化) | 多目的・並列・大規模対応 |
💬 Optuna の特色は「Define-by-run API」── 探索空間をモデル定義時ではなく objective 関数の中で動的に決められる。 これは PyTorch 流の autograd と同じ哲学で、 条件付き探索 (「Adam の場合だけ β1 を探す」など) を素直に書ける。 Hyperopt の「Define-and-run」方式に対する大きな優位点。
trial.suggest_* で動的に定義。create_study(directions=[...]) + NSGA-II で Pareto front 直接取得。| 疑問 | 答え |
|---|---|
| Q1. n_trials は何回が目安? | パラメータ次元 D に対し 10D 〜 30D が目安。 D=5 なら 50〜150 trial。 大きすぎても限界効用は逓減。 |
| Q2. seed を固定して再現できる? | TPESampler(seed=X) で再現可。 ただし n_jobs>1 並列実行時は trial 順序が変わり結果が微妙にずれる。 |
| Q3. 探索空間に依存関係 (Adam なら lr、 SGD なら momentum) は? | objective 関数内で if 分岐すれば OK。 Define-by-run の強み。 |
| Q4. Optuna と Hyperopt はどう違う? | 両方 TPE ベースだが Optuna は (1) Define-by-run、 (2) 永続化、 (3) Pruning、 (4) 多目的が豊富。 2020 年以降 Optuna が事実上の標準。 |
| Q5. GPU が必要なモデルで Optuna 並列実行は? | n_jobs= でなく CUDA_VISIBLE_DEVICES=0 ごとに別プロセスを起動し SQLite ストレージで共有。 |
| Q6. 制約付き最適化はできる? | return 値で制約違反を ±∞ や巨大ペナルティで返すか、 NSGA-II の制約モード使用。 |
| Q7. ベストパラメータの再現性は? | 同じ seed・同じデータでも CV 分割や random_state がモデル内で振れると微差が出る。 必ずモデル random_state も固定。 |
| Q8. 過去 study から知識を移植できる? | study.add_trials(previous_study.trials) で履歴を継承可能。 Warm-starting に有効。 |
| Q9. AutoML との関係は? | AutoML (auto-sklearn, FLAML) は内部で Optuna 的なベイズ最適化を使う。 Optuna は「最適化エンジン」、 AutoML は「モデル選択 + 最適化」のラッパー。 |
| Q10. 過学習せず本番投入するには? | 必ず hold-out test を最適化に使わず分離。 Nested CV で汎化を見積もる。 最終モデルは全データで再学習。 |
「コンペ初日にコピーしてすぐ走らせる」用のミニマル雛形。 これに自分のモデルと探索空間を差し替えるだけで動く。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import optuna, pandas as pd from sklearn.model_selection import cross_val_score, KFold from sklearn.ensemble import RandomForestRegressor # ① データロード (SSDSE-B-2026) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026']==2023] X = df[['A1101','A1301','A1303','J2503']].values y = df['A4101'].values # ② objective を define-by-run で書く def objective(trial): m = RandomForestRegressor( n_estimators = trial.suggest_int('n_estimators', 50, 500), max_depth = trial.suggest_int('max_depth', 3, 20), min_samples_split=trial.suggest_int('mss', 2, 10), random_state=0, n_jobs=-1) return cross_val_score(m, X, y, cv=5, scoring='r2').mean() # ③ 実行 study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=42)) study.optimize(objective, n_trials=100) print('Best R²:', study.best_value, 'Best params:', study.best_params) |
💬 これだけで「Random Forest を 100 通り試してベスト構成を返す」が完結。 モデルを XGBRegressor / LGBMRegressor / Neural Net に差し替えれば、 そのまま同じ枠で動く。 さらに本格的に詰めるなら + (1) Pruning、 (2) 永続化、 (3) Nested CV、 (4) 重要度確認 → 探索空間絞り直し、 の 4 段で。
| 階層 | 概念 | 関係 |
|---|---|---|
| 最上位 | 数理最適化 | Optuna はブラックボックス最適化の実装 |
| 上位 | ハイパーパラメータ最適化 | 機械学習特化の自動チューニング |
| 同列 | Hyperopt, Ray Tune, scikit-optimize | 同種のフレームワーク |
| 内部アルゴリズム | TPE, CMA-ES, NSGA-II | サンプラーとして選択可 |
| 前提 | 交差検証、 損失関数、 機械学習モデル | これらと組み合わせて動く |
| 応用 | AutoML、 NAS(Neural Architecture Search) | Optuna は両者の中核要素 |
「Optuna」は単独で完結せず、 前後の手法と組み合わさって価値が発揮される。 入力データの準備 (上流)・同目的の代替手法との比較 (並列)・結果の活用 (下流) という 3 軸で隣接領域を整理する。
この上流・並列・下流の対応を地図化することで、 「Optuna」を中核に据えた分析パイプライン (データ準備 → 手法選択 → 結果の検証と展開) の全体像が見えてくる。
「Optuna(ハイパーパラメータ最適化)」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
ハイパーパラメータ探索は、 特徴量の作り方やデータの質の改善に比べると効きが小さいことが多い。 先に試す順序を間違えないこと。
ハイパーパラメータ 1 個(横軸 x ∈ [0, 1]、 例えば学習率を 0〜1 に正規化したもの)に対する検証スコアの地形(縦軸、 高いほど良い)が隠されています。 「1 試行」ボタンを押すたびに、 3 つの戦略が同じ予算(最大 40 試行)でそれぞれ次の 1 点を選んで評価します。 どこに点が打たれるか、 そして下の「最良スコアの推移」グラフで C(TPE 風)がどれだけ速く高い値に到達するかを観察してください。
注記:スコア地形はガウス関数 3 つの和による合成データ(真の最大値 0.8800 は x = 0.67、 実データではない)。 C は本家 TPE(Parzen estimator + EI 最大化)を 1 次元向けに簡略化した実装(startup を層化サンプリング、 候補 15 個を良い群の分布から生成し l/g 比で選択)。 乱数はシード固定で、 計算はすべてページ内 JavaScript で正確に実行される。
💡 図の上をなぞる(マウス / タッチ)と縦のガイド線で x の値を確認できます(地形表示 ON なら真のスコアも表示)。 現在の試行: 0 / 40 |
| 戦略 | 最良スコア | 最良の x | 真の最大 0.8800 との差 |
|---|---|---|---|
| A グリッド | — | — | — |
| B ランダム | — | — | — |
| C TPE 風 | — | — | — |
観察ポイント:(1) A は左端から順に舐めるだけなので、 序盤は左側の低い山(局所ピーク)しか知らない。 (2) B は運次第でばらつく。 (3) C は 8 試行の層化 startup の後、 良かった試行の周辺に点が密集し始め、 15 試行前後で最良値 0.87 以上に到達することが多い(シードによっては C も局所ピークに留まる回がある ── これが「試行数不足」の危険の実感)。 「地形を表示」を ON にすると、 C の点が右側の真のピーク周辺に集中しているのが一目で分かる。
次は学習曲線の途中打切りのデモ。 10 個の trial(各 10 step の学習曲線、 合成データ:plateau 値と立ち上がり速度が異なる飽和曲線)を 1 つずつ実行します。 各 step で中間スコアを報告し、 「過去 trial の同 step 中央値より悪ければ打切り」(MedianPruner のルールそのまま)。 n_warmup_steps スライダーで「最初の何 step は打ち切らないか」を変えると、 計算節約と「遅咲きの最良 trial を切ってしまう」リスクのトレードオフが正確な計算で確認できます。
観察ポイント:この合成曲線では、 n_warmup_steps=0(最も攻撃的)にすると trial 7(最終値 0.908 で本来の最良)が step 0 で切られてしまい、 完走ベストは 0.871 止まり ── 計算は 68% 節約できるが答えを失う。 n_warmup_steps≥1 なら trial 7 が生き残り、 節約率は下がるが正しい最良値に到達する。 「Pruning が積極的すぎる」落とし穴(上の表参照)はまさにこれ。
グリッドとランダムは何回試しても賢くならない(各試行が独立)。 一方 TPE は、 試行が増えるほど「良かった群 l(x)」と「悪かった群 g(x)」の分布推定が正確になり、 有望領域の絞り込みが指数的に効いてくる。 上のデモで C の点が「最初は散らばり → 途中から密集」に変わる瞬間が、 まさに探索 (exploration) から活用 (exploitation) への重心移動。 実務の LightGBM チューニング(7 パラメータ)でも同じことが 7 次元空間で起きている。
suggest_float の min/max が狭すぎないか・学習率などは log=True か、 を最初に確認(ベストが範囲の端に張り付いたら赤信号)。HyperbandPruner(Successive Halving の改良)、 改善停滞で切る PatientPruner など。 深層学習では epoch 単位の pruning で計算 70% 以上の削減も。SSDSE-B-2026 の 2023 年 47 県データから「総人口・15歳未満・65歳以上・保育所等数」で「出生数 (A4101)」を予測する Gradient Boosting Regressor を作り、 Optuna で 100 trial の最適化を回す。
🎯 このコードでやること: GBR のハイパラ (n_estimators / max_depth / learning_rate / min_samples_split / subsample) を Optuna で探索、 5-fold CV の R² を最大化する。 Grid Search との結果比較も行う。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import optuna import pandas as pd from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import cross_val_score df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026']==2023].copy() X = df[['A1101', 'A1301', 'A1303', 'J2503']].values y = df['A4101'].values def objective(trial): params = { 'n_estimators': trial.suggest_int('n_estimators', 30, 300), 'max_depth': trial.suggest_int('max_depth', 2, 8), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True), 'min_samples_split': trial.suggest_int('min_samples_split', 2, 10), 'subsample': trial.suggest_float('subsample', 0.5, 1.0), } model = GradientBoostingRegressor(**params, random_state=0) return cross_val_score(model, X, y, cv=5, scoring='r2').mean() study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=42)) study.optimize(objective, n_trials=100, show_progress_bar=False) print(f'Best R² = {study.best_value:.4f}\nBest params = {study.best_params}') |
📤 実行結果 (典型例):
💬 読み方: TPE は履歴を学習しながら狭く深く探索するため、 同じ予算 (100 trial) で Random Search や Grid Search よりも良い解を見つける傾向。 Best params は「深さ 7 の木を 165 本」「学習率 0.0357」「サブサンプル 93%」── 学習率を既定の 0.1 から大きく下げ、 その分だけ本数と深さで補う構成に落ち着いた。 改善幅はデフォルト比 +0.0115 と控えめで、 47 県しかないデータでは探索してもそこまで伸びない。 TPESampler(seed=42) で種を固定してあるので、 この値は手元でも再現する。
学習に時間がかかる深層学習や巨大 GBM では、 序盤の数 epoch で「これは見込みなし」と判断して打ち切れば計算予算を大きく節約できる。 Optuna は trial.report() と trial.should_prune() でこれを実装する。 デフォルトの MedianPruner は「現時点までの中央値より悪い trial」を切る。
🎯 このコードでやること: XGBoost の boosting round (n_estimators) を 1 から徐々に増やしながら CV スコアを報告し、 MedianPruner で見込みなし trial を打ち切る。 全 trial 完走した場合と比べてどれくらい速くなるか測る。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import optuna from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import KFold import numpy as np def objective(trial): lr = trial.suggest_float('learning_rate', 0.01, 0.3, log=True) depth = trial.suggest_int('max_depth', 2, 8) scores = [] for step, ne in enumerate([5, 10, 20, 50, 100, 200]): fold_r2 = [] for tr, te in KFold(5, shuffle=True, random_state=0).split(X): m = GradientBoostingRegressor(n_estimators=ne, max_depth=depth, learning_rate=lr, random_state=0) m.fit(X[tr], y[tr]); fold_r2.append(m.score(X[te], y[te])) scores.append(np.mean(fold_r2)) trial.report(scores[-1], step) # 中間結果を報告 if trial.should_prune(): # 中央値より悪ければ打ち切り raise optuna.TrialPruned() return scores[-1] study = optuna.create_study(direction='maximize', pruner=optuna.pruners.MedianPruner(n_warmup_steps=2)) study.optimize(objective, n_trials=50) print(f'Best {study.best_value:.4f}, pruned trials = {sum(1 for t in study.trials if t.state==optuna.trial.TrialState.PRUNED)} / {len(study.trials)}') |
📤 実行結果 (典型例):
💬 読み方: 中央値より悪い trial を 22/50 (44%) 打ち切ったことで CV 計算量が 25% 削減された。 大規模深層学習なら epoch ベースで pruning して 70% 以上削減することも珍しくない。 Pruner には HyperbandPruner (Successive Halving の改良版)、 PatientPruner (改善停滞時打切) などもあり用途で選ぶ。
「精度を最大化しつつ推論時間を最小化したい」という現実の要件は、 単一目的最適化ではトレードオフを 1 点に固定してしまう。 Optuna の create_study(directions=['maximize','minimize']) と NSGA-II でPareto frontを直接得られる。
🎯 このコードでやること: GBR の (n_estimators, max_depth) を変化させて (R², 学習秒数) のペアを最適化。 50 trial 後に Pareto 最適な構成だけを抽出する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import time, optuna def multi_obj(trial): ne = trial.suggest_int('ne', 10, 500) depth = trial.suggest_int('depth', 2, 10) t0 = time.time() model = GradientBoostingRegressor(n_estimators=ne, max_depth=depth, random_state=0) r2 = cross_val_score(model, X, y, cv=5, scoring='r2').mean() return r2, time.time() - t0 study = optuna.create_study(directions=['maximize','minimize'], sampler=optuna.samplers.NSGAIISampler()) study.optimize(multi_obj, n_trials=50) print('Pareto-optimal trials:') for t in study.best_trials: print(f' R²={t.values[0]:.4f}, time={t.values[1]:.3f}s, params={t.params}') |
📤 実行結果 (典型例):
💬 読み方: 5 つの非劣 (Pareto-optimal) 構成が得られた ── 「精度 0.91 で 38ms」 から 「精度 0.96 で 745ms」まで。 ユーザー要件 (リアルタイム推論なら 100ms 以下、 オフライン分析なら精度優先) に応じて中から選べばよい。 単一目的なら最高 R² の構成しか得られず、 「3 倍速くて精度ほぼ同じ」の選択肢を見逃す。
Optuna で「CV R²=0.95」が出ても、 これは 同じ CV 分割で最適化した結果なので将来データへの真の汎化性能ではない (CV overfitting)。 正しい評価には Nested CV: 外ループで test fold を切り出し、 内ループで Optuna 最適化し、 best params を外 fold で評価する。
🎯 このコードでやること: SSDSE-B-2026 の 2023 年 47 県データで、 外 5-fold × 内 Optuna 20 trial の Nested CV を回し、 「最適化後 CV (楽観値)」と「外 fold で評価した真の汎化 R² (現実値)」のギャップを定量化する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import optuna from sklearn.model_selection import KFold optuna.logging.set_verbosity(optuna.logging.WARNING) outer = KFold(5, shuffle=True, random_state=0) opt_scores, test_scores = [], [] for fold, (tr, te) in enumerate(outer.split(X)): X_tr, X_te = X[tr], X[te]; y_tr, y_te = y[tr], y[te] def obj(tr_): m = GradientBoostingRegressor( n_estimators=tr_.suggest_int('n_estimators', 30, 300), max_depth=tr_.suggest_int('max_depth', 2, 6), learning_rate=tr_.suggest_float('learning_rate', 0.02, 0.3, log=True), random_state=0) return cross_val_score(m, X_tr, y_tr, cv=4, scoring='r2').mean() study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=fold)) study.optimize(obj, n_trials=20) best = GradientBoostingRegressor(**study.best_params, random_state=0).fit(X_tr, y_tr) opt_scores.append(study.best_value) # 楽観値 (内 CV) test_scores.append(best.score(X_te, y_te)) # 真の汎化 (外 test) print(f'内 CV (楽観) Mean R² = {np.mean(opt_scores):.4f} ± {np.std(opt_scores):.4f}') print(f'外 test (現実) Mean R² = {np.mean(test_scores):.4f} ± {np.std(test_scores):.4f}') |
📤 実行結果 (典型例):
💬 読み方: 「単 CV で報告した R²=0.957」は実は 4〜5% 上方バイアスがかかっており、 新規データでの本当の R² は 0.91 程度。 さらに外 fold 標準偏差 0.061 と大きく、 47 県の小標本では汎化性能の点推定すら不安定であることが見える。 大規模データなら Nested CV までやらず一段の CV と hold-out test で十分だが、 小標本・コンペでは Nested CV が誠実。
Optuna は study が終わったあと、 各パラメータが目的関数に与えた影響度を計算できる (内部は fANOVA や PED-ANOVA)。 重要度が高いパラメータは絞って探索を深くし、 低いものは固定する ── これが効率的な「2 ラウンド戦法」。
🎯 このコードでやること: 前ブロックで作った study (n_trials=100) からパラメータ重要度を抽出し、 「最も効くパラメータ」を上から並べる。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 | from optuna.importance import get_param_importances, FanovaImportanceEvaluator # fANOVA (Random Forest ベース、 デフォルト) imp_fanova = get_param_importances(study, evaluator=FanovaImportanceEvaluator(seed=42)) print('=== fANOVA Importance ===') for k, v in sorted(imp_fanova.items(), key=lambda kv: -kv[1]): print(f' {k:<20}: {v:.4f}') # PedAnova (Optuna v3 で導入、 より高精度) from optuna.importance import PedAnovaImportanceEvaluator imp_ped = get_param_importances(study, evaluator=PedAnovaImportanceEvaluator()) print('=== PED-ANOVA Importance ===') print(dict((k, round(v,3)) for k,v in sorted(imp_ped.items(), key=lambda x:-x[1]))) |
📤 実行結果 (典型例):
💬 読み方: learning_rate が結果の62% の変動を説明。 次に max_depth (21%)。 min_samples_split はほぼゼロ寄与なので次の最適化では min_samples_split=5 で固定し、 残り 4 パラメータをより細かく探索すべき。 こうした「重要度ガイド付き 2 ラウンド最適化」で予算を 2 倍効率化できる。
Optuna の study はメモリ上に持つことも、 SQLite/MySQL/PostgreSQL に永続化することもできる。 永続化すれば別マシン・別プロセスから同じ study に trial を追加できる。 GPU 4 台で同時に DL モデルを試す、 クラスタで 100 並列で XGBoost を回す、 といった用途に必須。
🎯 このコードでやること: SQLite に study を作成し、 4 並列 (n_jobs=4) で 200 trial を実行。 後で別プロセスから同じ study_name で開き直し、 trial を追加する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import optuna import os if os.path.exists('optuna_birth.db'): os.remove('optuna_birth.db') # 毎回同じ trial 数になるよう作り直す # プロセス A (初回) study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=0), storage='sqlite:///optuna_birth.db', study_name='gbr_birth_rate', direction='maximize', load_if_exists=True) study.optimize(objective, n_trials=50, n_jobs=4) # 4 並列で 50 trial # プロセス B (別ターミナル、 後から追加) study2 = optuna.load_study(storage='sqlite:///optuna_birth.db', study_name='gbr_birth_rate') print(f'現在の trial 数: {len(study2.trials)}') study2.optimize(objective, n_trials=50) # さらに 50 追加 print(f'追加後 trial 数: {len(study2.trials)}, Best: {study2.best_value:.4f}') |
📤 実行結果 (典型例):
💬 読み方: 同じ DB を共有することで、 (1) 中断しても再開できる、 (2) 別マシンから「もう少し探そう」と継続できる、 (3) 並列で安全に書き込める (Optuna が排他制御)。 大規模 study では SQLite から MySQL/PostgreSQL に切り替えると数百ノード並列も実用に。 Kubernetes / Slurm でのジョブ化と相性抜群。
Optuna は Plotly ベースの可視化 API を備えており、 1 行で散布図・等高線図・パラメータ重要度・並列座標プロットなど主要な分析が描ける。 Kaggle や論文用のグラフ作成にも便利。
🎯 このコードでやること: 完了済み study から 5 種類の代表的なプロットを生成し、 HTML や PNG として保存する。 ノートブック上では fig.show() でインタラクティブ表示。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 | import optuna.visualization as vis vis.plot_optimization_history(study).write_html('history.html') # 履歴 vis.plot_param_importances(study).write_html('importance.html') # 重要度 vis.plot_parallel_coordinate(study).write_html('parallel.html') # 並列座標 vis.plot_contour(study, params=['learning_rate', 'max_depth']).write_html('contour.html') vis.plot_slice(study).write_html('slice.html') # スライス # matplotlib backend (PNG 保存に便利) import optuna.visualization.matplotlib as vism ax = vism.plot_param_importances(study) ax.figure.savefig('importance.png', dpi=150) |
📤 出力ファイル:
💬 読み方: 最低限見るべきは (1) optimization_history (TPE が改善し続けているか・もう収束したか)、 (2) param_importances (どこに資源を投入すべきか)、 (3) contour (パラメータ間の交互作用) の 3 枚。 これで「最適化が十分か」「次は探索空間を絞るか広げるか」が判断できる。
ここまでの各章(🎨 直感・📐 数式・🎮 ウィジェット)を一段深く束ね直す。 Optuna の本質は 4 つの部品の組み合わせに分解できる。 それぞれを「なぜそう設計されているか」で読むと、 API の一つひとつが腑に落ちる。
objective(trial) の内側で trial.suggest_* を呼んだ瞬間に次元が生える。 だから「optimizer が Adam のときだけ beta1 を探す」という条件付き・木構造の空間を Python の if でそのまま書ける。 これが Hyperopt の define-and-run に対する決定的な優位点で、 名前の Tree-structured の由来でもある。trial.report(value, step) で報告し、 should_prune() が真なら打ち切る。 「1 試行 = 完走」という暗黙の前提を崩し、 予算を横(試行数)だけでなく縦(各試行の深さ)にも配分する発想。💬 まとめると Optuna は「次にどこを試すか(TPE)」「どこで諦めるか(pruning)」「どう空間を描くか(define-by-run)」「結果をどう貯めるか(study)」の 4 つを、 数行のコードに畳み込んだフレームワーク。 上の各実演で得た本ページの実測値(SSDSE-B-2026 / 2023 年 47 県、 GBR で R² デフォルト 0.9328 → Optuna 100 trial 0.9572)も、 この 4 部品が噛み合った結果である。
Optuna は「与えた目的関数を全力で最大化する道具」であり、 目的関数の設計が誤っていればその誤りごと最適化してしまう。 上の ⚠️ 落とし穴章・表を補い、 特に再現性と汎化に関わる論点を体系化する。
log=True(対数スケール)にする。 ウィジェットで真のピーク x=0.67 が範囲外だったらどの戦略も無力、 という体験がこれに対応。TPESampler(seed=42) に加えモデル側 random_state・numpy/torch のシードまで揃えないと結果が毎回ずれる。 並列(n_jobs>1)実行時は trial 到着順が変わり、 seed を固定しても厳密再現は保証されない点に注意。Pipeline を CV に渡す)。 詳細は データリーク を参照。plot_optimization_history)が平坦なら打ち切り、 予算は特徴量エンジニアリングへ回す方が効く局面が多い。n_warmup_steps=0 にすると本来最良の trial 7(最終 0.908)が step 0 で切られ、 完走ベストが 0.871 止まりになる例を正確な計算で示した。 n_warmup_steps を 1 以上に取り、 曲線の交差が多いタスクほど warmup を厚くする。注記:上記の 4.5% 楽観バイアス・0.871 等の数値は本ページの SSDSE-B-2026 実演(🧮 Nested CV 節・🎮 枝刈りデモ)で得た値。 デモの学習曲線は plateau と立ち上がりが異なる架空の飽和曲線(合成データ)である旨は既存章の注記どおり。
| サンプラー | 原理 | 強い場面 | 弱い場面 |
|---|---|---|---|
| TPE(既定) | Parzen 窓による密度比最大化(ベイズ流) | 混合空間・条件付き・中〜高次元の汎用 | パラメータ間に強い相関があると非効率 |
| CMA-ES | 共分散行列を適応させる進化戦略 | 連続変数のみ・相関の強い滑らかな空間 | 離散・カテゴリ・条件付き空間が苦手 |
| GridSampler | 格子の全探索 | 低次元・離散・完全再現が必須 | 次元が増えると組合せ爆発 |
| RandomSampler | 一様乱択 | ベースライン・デバッグ・完全並列 | 履歴を活かさず効率は頭打ち |
| QMCSampler | 準モンテカルロ(Sobol/Halton) | 初期探索の網羅性・低食い違い | 後半の集中探索は不得手 |
| NSGA-II / NSGA-III | 遺伝的アルゴリズム+Pareto 選択 | 多目的(精度 vs 時間 など) | 単目的では TPE に劣ることが多い |
💬 実務の定石は「まず TPE で 10D〜30D trial」。 連続かつ高次元で滑らかなら CMA-ES、 複数指標を同時に見たいなら NSGA-II、 という切り替え。 いずれも optuna.samplers.*Sampler() を create_study(sampler=...) に渡すだけで差し替わる。
💬 枝刈りは trial.report() + should_prune() を objective に埋めるのが前提。 pruner の攻撃度は n_warmup_steps/n_startup_trials で調整し、 「遅咲きを切る危険」と「計算節約」のトレードオフ(落とし穴 (6))を意識して選ぶ。
create_study(directions=['maximize','minimize']) で Pareto front を直接取得。 本ページの多目的実演では「精度 0.91 で 38ms」から「精度 0.96 で 745ms」まで 5 つの非劣構成が得られ、 単目的なら見逃す「3 倍速で精度ほぼ同」の選択肢が可視化された。storage に指定すれば、 複数プロセス・複数マシンが同一 study に trial を書き込める(Optuna が排他制御)。 GPU 台数ぶんプロセスを分け CUDA_VISIBLE_DEVICES で割り当てるのが定石。 ただし TPE は履歴依存なので並列度を上げすぎると「賢さ」が薄まる(ランダム/ASHA は完全並列に強い)。study.add_trials(prev.trials) で過去知識を warm-start 継承、 enqueue_trial() で「試したい既知の良構成」を先頭に差し込める。 重要度(fANOVA/PED-ANOVA)で効くパラメータを見極め、 効かない軸は固定して効く軸を細探索する「2 ラウンド戦法」が予算効率を大きく上げる。本ページの内容を、 用語集内の実在ページと接続する。 未収録の概念(下段)はリンクせずテキストで示す。
GridSampler でも利用可、 計算量比較の基準。ベイズ最適化(GP-BO)、 ランダムサーチ、 TPE、 CMA-ES、 NSGA-II、 Hyperband/ASHA、 SMAC、 Ray Tune、 scikit-optimize は独立ページ未収録。 定義は本ページ内の「🌐 関連手法・派生」表・「📐 数式」節・上の発展表を参照。