本ページは ハイパーパラメータチューニング(Hyperparameter Tuning)を 12 のセクションで多角的に解説します。 上のチップは検索・関連語の手がかりです。 以下のリンクで各セクションに直接ジャンプできます:
🍰 まずはやさしく
設定を調整する作業のことです。
AIの性能を最大限に引き出します。
スマホの画質設定を変える感覚です。
効率的な探し方について学びます。
🍰 まずはやさしく
最後の仕上げのような工程です。
AIの正解率をさらに上げるために使います。
部活で道具を細かく調整するのと似ています。
計算時間を減らす工夫について読みます。
ハイパーパラメータチューニングは、 機械学習モデルの精度を 2〜10% 押し上げる仕上げ工程。 アルゴリズム自体の選定・特徴量設計に比べて効果は中程度ですが、 Kaggle 等の競技でメダル圏に届くかどうかを決める 最後の差別化になります。 計算コストとの戦いでもあるため、 賢く探索する手法が重要です。
🍰 まずはやさしく
人間が決める設定値のことです。
データに合う最適な値を探すために使います。
勉強法を自分に合わせて変える感覚です。
設定値の種類と探し方を詳しく読みます。
機械学習モデルには 2 種類のパラメータがあります:
後者は「アルゴリズムの設定値」のようなもの。 学習率が 0.001 と 0.1 では学習の速度・収束性が劇的に違います。 「どの値が良いか」は データとモデルの組合せ次第で、 事前に予測できないため 探索が必要です。
例えば XGBoost には max_depth, learning_rate, n_estimators, subsample, reg_alpha など 10 以上のハイパラがあり、 すべての組合せを試すと 数千〜数万のモデルを学習する羽目になります。 これを効率化するのが本テーマです。
🍰 まずはやさしく
数学的に最適な値を探すことです。
根拠を持って設定を決めるために使います。
買い物で一番コスパの良い店を探す感覚です。
計算式を使った探し方を詳しく読みます。
ハイパラ探索は本質的に「ブラックボックス関数の最大化」問題:
Bayesian Optimization では、 過去の試行から 獲得関数(Expected Improvement, UCB 等)を計算し、 次に試す $\theta$ を決める:
ハイパーパラメータチューニングを数式 / 形式定義で表す:
ハイパラ $\boldsymbol{\lambda}$ で学習したモデルの検証損失期待値を最小化する。 Bayesian Opt はこの目的関数を Gaussian Process でモデル化する。
ハイパーパラメータ調整(HPT)は「ハイパーパラメータ $\lambda \in \Lambda$ を変えると、 検証スコア $V(\lambda)$ が変わる」関数の最適化問題である。 ただし $V(\lambda)$ は分布の期待値であり、 評価には毎回モデル学習が必要なため 高コスト。 探索戦略はこの制約下で「少ない評価で良い $\lambda$ を見つける」設計になる。
📌 読み解きの核:HPT は「コスト高な関数の最適化」。 Grid は全点を律儀に評価(次元の呪い)、 Random は確率的にカバー(同じ予算で広く)、 Bayesian は過去の評価から代理モデル(GP/TPE)を作り「次に試すと得 そうな点」を狙う。 全体は探索(exploration)と活用(exploitation)の trade-offに帰着する。
HP の数と値域はアルゴリズムごとに大きく異なる。 実務でよく使う 5 種類について、 「最低限触るべき HP」と「ログスケール推奨/線形 OK」の使い分けをまとめる。
| アルゴリズム | 主要 HP | 標準探索範囲 | スケール |
|---|---|---|---|
| Ridge / Lasso | $\alpha$ | $10^{-4} \sim 10^{4}$ | log |
| RandomForest | n_estimators, max_depth, min_samples_split, max_features | [50, 500], [2, 30], [2, 20], {sqrt, log2, 0.5} | 線形 / 整数 |
| XGBoost / LightGBM | learning_rate, max_depth, n_estimators, subsample, colsample, reg_alpha, reg_lambda | [0.01, 0.3], [3, 10], [100, 2000], [0.5, 1.0], [0.5, 1.0], [0, 10], [0, 10] | log / 線形混在 |
| SVM (RBF) | C, gamma | $10^{-3} \sim 10^{3}$, $10^{-4} \sim 10^{1}$ | log(両方) |
| NN (MLP) | lr, batch_size, hidden_dim, dropout, weight_decay | [1e-4, 1e-1], {16, 32, 64, 128, 256}, {16, 32, 64, 128}, [0, 0.5], [0, 1e-2] | log / 離散 / 線形 |
💡 探索の優先順位:XGBoost なら lr と n_estimators の組合せ → max_depth → 正則化、 NN なら lr → batch_size → アーキテクチャ の順で効果が大きい。 全 HP を平等に探索するのは無駄。 「効くものから先に」が原則。
上の数式に出てきた記号を 1 つずつ解説します。 数式が出てくる試験問題(統計検定・G 検定・基本情報)では、 各記号の意味を答えられるかが分岐点:
| 記号 | 意味 |
|---|---|
| $\boldsymbol{\lambda}$ | ハイパーパラメータ |
| $\mathcal{D}_{\text{train}}$ | 学習データ |
| $\mathcal{D}_{\text{val}}$ | 検証データ |
| $\mathcal{L}$ | 損失関数 |
| $f_{\boldsymbol{\lambda}}$ | ハイパラ $\boldsymbol{\lambda}$ で学習したモデル |
ハイパーパラメータチューニングを入門レベルで習得した次に進むべき発展テーマ:
Grid/Random search が「探索戦略を持たない」のに対し、 Bayesian Optimization はガウス過程や TPE (Tree-structured Parzen Estimator) で性能関数 $\mathcal{L}(\boldsymbol{\lambda})$ を確率モデル化し、 取得関数 (Expected Improvement など) を最大化する点を次に試す。 optuna・hyperopt・scikit-optimize が代表実装。 SSDSE 47 県のように評価が安価なら Grid で良いが、 LightGBM × 100 万行のような評価コストが高い場合は Bayesian が試行数を 1/5〜1/10 に削減できる。
Successive Halving は弱い候補を早期打ち切りで間引き、 強い候補だけにリソース (epoch・データ量) を集中させる。 Hyperband はこれを複数の budget で組合せ、 Bayesian と並ぶ実用標準になった。 sklearn の HalvingGridSearchCV・HalvingRandomSearchCV で利用可能。 深層学習で 1 試行 30 分かかる場面では、 全パターン回す代わりに「先頭 5 epoch で見込みのない候補を捨てる」だけで全体時間が 1/3〜1/5 に減る。
同じ CV で HP を選び性能を報告すると、 その HP 選定で test に対して上振れた個体を採用している ため楽観バイアスが入る。 Nested CV は外側ループで test スコアを集計し、 内側ループで HP を選ぶ二重構造。 SSDSE 47 県の 5×5 nested CV で外側分散を確認すると「最適 HP は seed 依存で揺れる」事実が見え、 単一の「最良 HP」を信奉するリスクが減る。
数十〜数千試行を回すと、 「どの seed・どの HP・どの metric の組合せだったか」が失われがち。 MLflow で各試行を log し、 optuna.visualization で並列座標プロット・等高線プロットを描くと、 HP 間の相互作用が一目で分かる。 業務適用ではこの記録自体が モデルガバナンスの監査証跡 になり、 後から「なぜこのモデルが選ばれたか」を再現可能にする。
| 戦略 | 評価回数(5 HP × 5 値) | 連続値 HP の扱い | 過去評価の活用 | 並列化 | sklearn 実装 |
|---|---|---|---|---|---|
| GridSearchCV | $5^5=3125$ | 離散化が必要 | なし | 容易 | GridSearchCV |
| RandomizedSearchCV | 指定回数(例 100) | 分布から直接サンプリング | なし | 容易 | RandomizedSearchCV |
| Bayesian(TPE) | 指定回数(例 100) | 分布から直接サンプリング | 代理モデル更新 | 並列で精度低下 | Optuna / Hyperopt |
| Halving | 減少して打ち切り | 離散 | 予算配分 | 容易 | HalvingGridSearchCV |
💡 選び方の指針:HP が 1-2 個+値が少ない → Grid。 HP が 3-5 個+連続値 → RandomizedSearch(同じ予算で広くカバー)。 HP が 5+ 個+評価コストが大 → Bayesian(Optuna 推奨)。 計算機が潤沢で打ち切り戦略を入れたい → HalvingGridSearchCV。
HPT で得た「best CV スコア」を そのまま汎化性能として報告するのは過大評価。 同じ val set で何百回もスコアを覗き見しているため、 偶然良いスコアが出た HP を選んでいるからである。 この「探索による楽観バイアス」を取り除くのが Nested Cross-Validation。
外側 $K_{\text{outer}}$ 分割の各 fold で、 内側 CV で HPT を行い、 選ばれた HP で外側 test を評価する。 学習回数は $K_{\text{outer}} \times K_{\text{inner}} \times |\Lambda|$ と急増する(例: $5 \times 5 \times 7 = 175$ 回)が、 「最終モデルが新規データにどれだけ効くか」の信頼できる推定値が得られる。
📌 実務の使い分け:論文・コンペ最終提出時は Nested CV で報告。 EDA・モデル比較時は単純 CV で十分。 「HPT 後の CV スコア」を Nested CV と混同して報告すると、 本番でスコアが落ちて慌てる典型的失敗。
Optuna が出す HP importance は、 fANOVA(functional ANOVA)に基づく分散分解で計算される。 数学的には「探索した HP のうち、 スコアの分散をどの HP がどれだけ説明したか」を測る。
$\lambda_j$ を固定して残り $\lambda_{-j}$ について平均をとった条件付きスコアの分散が、 全体の分散の何%かを示す。 importance=0.7 なら「$\lambda_j$ がスコアの 70% を支配している」と読める。 これにより「効かない HP は次回から固定」「効く HP は範囲を絞って再探索」の意思決定ができる。
📌 実務の活用:HPT は 1 回で終わらせず、 importance を見て段階的に範囲を絞るのが上級者の手筋。 1st round で広く(20 トライ)→ importance 上位 2-3 個に絞り 2nd round(50 トライ)→ best params 周辺で 3rd round。 同じ予算でも最終スコアが向上する。
例:XGBoost で 3 つのハイパラ(学習率、 深さ、 木の数)を探索する場合の試行回数比較:
| 手法 | 探索方針 | 試行回数の目安 | 長所 |
|---|---|---|---|
| Grid Search | すべての格子点を網羅 | $3 \times 5 \times 4 = 60$ | シンプル、 再現性 |
| Random Search | ランダムに選ぶ | 30〜100 | 連続値に強い、 効率良し |
| Bayesian (Optuna) | 過去結果から賢く選ぶ | 20〜50 | 少試行で良い解、 大規模に強い |
10 次元・各 10 値だと Grid Search は 1010 試行で 不可能。 Random/Bayesian が必須になります。
SSDSE-B-2026 で 勾配ブースティング回帰のハイパラを Optuna で 20 trial 探索し、 RMSE 最小の組合せを発見する。
使用データ:SSDSE-B-2026.csv(独立行政法人 統計センター提供、 47 都道府県 × 100 超の社会経済指標)。 出典
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd import numpy as np 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.rename(columns={df.columns[2]: 'pref'}) X = df[['A1101', 'A1303', 'F3101']].fillna(0).values y = df['A4101'].values # Optuna 風の簡易ループ(実際は optuna.create_study() を使う) best = (None, 1e18) for n in [50, 100, 200]: for d in [2, 3, 4, 5]: for lr in [0.01, 0.05, 0.1]: m = GradientBoostingRegressor(n_estimators=n, max_depth=d, learning_rate=lr, random_state=42) rmse = -cross_val_score(m, X, y, cv=5, scoring='neg_root_mean_squared_error').mean() if rmse < best[1]: best = ({'n':n,'d':d,'lr':lr}, rmse) print(f'最良ハイパラ: {best[0]} RMSE = {best[1]:.2f}') |
▲ 上記コードはそのまま実行可能。 CP932 エンコーディング・skiprows=1(英語ヘッダ行をスキップ)・列名の英数字コード(A1101 = 総人口 など)に注意。
SSDSE-B-2026 47 県を 5-fold CV で評価し、 Ridge 回帰の $\alpha \in \{10^{-3}, 10^{-2}, \ldots, 10^{3}\}$ を GridSearchCV で探索する。 説明変数は「総人口(A1101) / 15歳未満人口(A1301) / 65歳以上人口(A1303)」(いずれも人口規模に連動し多重共線性が強い)、 目的変数は「出生数(A4101)」。
このコードでやること:StandardScaler + Ridge をパイプライン化し、 GridSearchCV で $\alpha$ を 7 候補から選ぶ。 各 $\alpha$ について 5 回学習・5 回評価 → 計 35 回の fit。 最適 $\alpha$ と CV R² を出力する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import pandas as pd from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV 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']] y = df['A4101'] pipe = Pipeline([('sc', StandardScaler()), ('ridge', Ridge())]) grid = {'ridge__alpha': [1e-3, 1e-2, 1e-1, 1, 10, 100, 1000]} gs = GridSearchCV(pipe, grid, cv=5, scoring='r2').fit(X, y) print('best alpha=', gs.best_params_['ridge__alpha']) print('CV R² =', round(gs.best_score_, 4)) print('rank order:', list(gs.cv_results_['mean_test_score'].round(4))) |
📤 実行例:
💬 結果の読み方:CV R² は α=1.0 でピーク(0.9712)。 α が小さすぎる側(0.001)でも R²=0.9701 とほぼ同じだが、 大きい側(100→1000)では正則化が強すぎて R² が 0.96→0.89 と急落する。 「正則化はかけすぎると性能が落ちる」という古典的曲線が SSDSE 47 県でも確認できる。 グリッドの両端が落ちていないと、 探索範囲が不十分の合図。
合成データで同じ評価回数 50 でのカバー範囲を比較する。
| 方法 | n=10 | n=30 | n=50 |
|---|---|---|---|
| グリッド | 0.80 | 0.82 | 0.83 |
| ランダム | 0.78 | 0.84 | 0.87 |
| ベイズ | 0.79 | 0.86 | 0.89 |
1 2 3 4 5 | import numpy as np methods = {'grid':0.83,'random':0.87,'bayes':0.89} best = max(methods.items(), key=lambda x: x[1]) print(f"スコア: {methods}") print(f"最良: {best}") |
💬 手計算 (Step 2) ベイズ最良と Python 出力が完全一致。
最小コードで動かしてみる例:
🎯 このコードでやること: SSDSE-B-2026 の 47 都道府県 × 12 年度 = 564 行から「合計特殊出生率が全国中央値 (1.46) 以上か」を当てる勾配ブースティング分類器を作り、 木の本数・深さ・学習率の 3 つを Optuna のベイズ最適化 (TPE) で 20 試行だけ探索する。 評価は層化 5 分割交差検証の正解率。 1 年度に絞ると 47 行しか無く、 2 クラスに分けたうえで 5 分割すると各 fold のクラス件数が足りないため、 全 12 年度を使って 1 クラス約 282 件を確保している。
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 | import optuna import pandas as pd from sklearn.ensemble import GradientBoostingClassifier from sklearn.model_selection import cross_val_score, StratifiedKFold optuna.logging.set_verbosity(optuna.logging.WARNING) # 47 都道府県 × 12 年度 = 564 行をすべて使う。 # 1 年度に絞ると 47 行しか無く、2 クラスに分けたうえで # StratifiedKFold(n_splits=5) を組もうとすると各 fold のクラス件数が足りない。 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) X = df[['A1101', 'B4101']].assign(高齢化率=df['A1303'] / df['A1101']) y = (df['A4103'] >= df['A4103'].median()).astype(int) # 合計特殊出生率が中央値以上なら 1 def objective(trial): params = { 'n_estimators': trial.suggest_int('n_estimators', 50, 300), 'max_depth': trial.suggest_int('max_depth', 2, 6), 'learning_rate': trial.suggest_float('lr', 1e-3, 1e-1, log=True), } model = GradientBoostingClassifier(**params, random_state=0) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) return cross_val_score(model, X, y, cv=cv, scoring='accuracy').mean() study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=42)) study.optimize(objective, n_trials=20, show_progress_bar=False) print('best params :', study.best_params) print('best accuracy:', round(study.best_value, 4)) |
📤 実行結果:
💬 結果の読み方: 20 試行で正解率 0.814 に到達した。 クラスはほぼ半々 (283 対 281) なので、 でたらめに当てた場合の 0.50 が下限。 そこから +0.31 押し上げられている。 選ばれたのは「木を多め (288 本)・深め (深さ 6)・学習率 0.087」という、 探索範囲の上端寄りの組合せで、 このデータでは表現力を上げるほど当たることを示す。 sampler=TPESampler(seed=42) を指定しているので、 同じ環境なら何度動かしても同じ値になる。
「ハイパーパラメータチューニング」を扱う代表的なライブラリ別実装。 同じ目的でも書き方が違うため、 自分のプロジェクトの依存関係に合わせて選択する:
1 2 3 4 5 6 7 8 | import pandas as pd import numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=0) df = df.rename(columns={df.columns[2]: 'pref'}) print('行数:', len(df), '列数:', df.shape[1]) print(df[['pref', 'A1101', 'A4101', 'A5101', 'F3101']].head()) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # ── この抜粋で使うデータを用意します(英字の項目コードで読み込み)── import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in df.columns[3:]: df[_c] = pd.to_numeric(df[_c], errors='coerce') df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()] from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score, mean_squared_error from sklearn.model_selection import train_test_split import numpy as np X = df[['A1101', 'A1303']].fillna(0).values y = df['A4101'].values X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42) m = LinearRegression().fit(X_tr, y_tr) pred = m.predict(X_te) print(f'R² = {r2_score(y_te, pred):.3f}') print(f'RMSE = {np.sqrt(mean_squared_error(y_te, pred)):.2f}') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # ── この抜粋で使うデータを用意します(英字の項目コードで読み込み)── import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in df.columns[3:]: df[_c] = pd.to_numeric(df[_c], errors='coerce') df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()] from scipy import stats # 例: 2 変数の Pearson 相関 + p 値 r, p = stats.pearsonr(df['A1101'], df['A4101']) print(f'相関係数 r = {r:.3f}, p 値 = {p:.2e}') # 例: 1 標本 t 検定(平均が一定値と異なるか) t, p = stats.ttest_1samp(df['A4101'], popmean=df['A4101'].mean()) print(f't = {t:.3f}, p = {p:.3f}') |
1 2 3 4 5 6 7 8 9 10 11 | import matplotlib.pyplot as plt import seaborn as sns fig, ax = plt.subplots(figsize=(8,5)) sns.scatterplot(data=df, x='A1101', y='A4101', ax=ax) ax.set_xlabel('A1101') ax.set_ylabel('A4101') ax.set_title(f'{len(df)} 都道府県の関係') plt.tight_layout() plt.savefig('out.png', dpi=120) plt.close() |
このコードでやること:RandomForest の 2 つの HP(n_estimators, max_depth)を SSDSE-B-2026 で RandomizedSearchCV(30 トライ)と Optuna(30 トライ)で探索し、 同じ予算で得られる best CV 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 25 26 | import pandas as pd import optuna from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score, RandomizedSearchCV from scipy.stats import randint 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']]; y = df['A4101'] # Randomized: 30 トライ rs = RandomizedSearchCV(RandomForestRegressor(random_state=0), {'n_estimators': randint(50, 500), 'max_depth': randint(2, 15)}, n_iter=30, cv=5, scoring='r2', random_state=42).fit(X, y) # Optuna (TPE): 30 トライ def objective(tr): m = RandomForestRegressor(n_estimators=tr.suggest_int('n_est',50,500), max_depth=tr.suggest_int('d',2,15), random_state=0) return cross_val_score(m, X, y, cv=5, scoring='r2').mean() # TPE は乱数で候補を出すので、種を固定しないと毎回違う答えになる st = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=0)) st.optimize(objective, n_trials=30, show_progress_bar=False) print('Random best R²=', round(rs.best_score_,4), 'params=', rs.best_params_) print('Optuna best R²=', round(st.best_value,4), 'params=', st.best_params) |
📤 実行例:
💬 結果の読み方:同じ 30 トライ予算で、 Optuna(TPE)は過去評価から良い領域を絞り込み、 Random より +0.0012 高い R² に到達した。 ただしこの差は 5-fold CV の fold 間ばらつきに埋もれる大きさで、 47 県 3 特徴という小さな問題では「TPE の方が優れている」と言えるほどの差は出ていない。 面白いのは 両者が選んだ木の本数がほぼ同じ(149 本 / 152 本)である点で、 この問題では n_estimators を 150 前後にすれば深さは 6 でも 10 でもほとんど変わらない、 つまりそもそも探索の余地が小さいことを意味する。 Bayesian 最適化の優位が顕著になるのは、 探索空間が広く・1 回の評価コストが高い実務(XGBoost 10 HP × 数万件)の側である。 なお両者とも種を固定している(random_state=42 と TPESampler(seed=0))ので、 上の値は何度実行しても再現する。
このコードでやること:SSDSE-B-2026 47 県を題材に、 GradientBoostingRegressor の 5 HP(n_estimators, max_depth, learning_rate, subsample, min_samples_split)を Optuna で同時探索する。 suggest_float(log=True) でログスケール、 整数は suggest_int、 離散は suggest_categorical。
📥 探索空間:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd import optuna 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] X = df[['A1101', 'A1301', 'A1303']]; y = df['A4101'] def objective(tr): params = { 'n_estimators': tr.suggest_int('n_estimators', 50, 500), 'max_depth': tr.suggest_int('max_depth', 2, 8), 'learning_rate': tr.suggest_float('learning_rate', 1e-3, 1e-1, log=True), 'subsample': tr.suggest_float('subsample', 0.5, 1.0), 'min_samples_split': tr.suggest_int('min_samples_split', 2, 10)} m = GradientBoostingRegressor(**params, random_state=0) return cross_val_score(m, X, y, cv=5, scoring='r2').mean() st = optuna.create_study(direction='maximize'); st.optimize(objective, n_trials=50) print('best R²=', round(st.best_value, 4), 'params=', st.best_params) |
📤 実行例(50 トライ):
💬 結果の読み方:50 トライで best R²=0.962、 Ridge の 0.971 にはわずかに及ばないが、 SSDSE 47 県という小さなデータでは GBM も十分競争力がある。 lr=0.0428 は log スケール推奨の典型例(線形範囲だと中央 0.05 付近を厚く探索できない)。 importance を見ると lr が最も寄与し、 次に n_estimators・max_depth、 subsample/min_samples_split は影響小、 と分かる。
「ハイパーパラメータチューニング」を実務・試験で扱うときに頻発する典型的なミスです。 各項目を 1 度読んでおけば 9 割の事故が防げます:
random_state 指定。StandardScaler + Ridge をまとめて GridSearchCV に渡すのが定石。2 つのハイパラ(横軸=学習率 η、 縦軸=正則化 λ、 いずれも対数スケールを 0〜1 に正規化)を変えたときの検証スコアの地形を色で表示しています。 明るい(黄色い)ほど高スコア。 グリッド / ランダム / ベイズの各ボタンで試行点を打ち、 同じ試行回数でどれだけ早く「良い設定」に届くかを比べられます。 地図をタップ(クリック)すると、 その設定を自分で 1 回試せます。
※ この地形は説明用の架空データです(なめらかな 2 次元関数+微小ノイズ、 乱数シード固定で毎回同じ結果を再現)。 実在モデルのスコア地形ではありません。 この例では横軸(学習率)の影響が強く、 縦軸(正則化)の影響は弱い設計にしてあります。
グリッド探索は「各軸を 4 分割 × ハイパラ 2 個」で全組合せを試すので、 評価回数は kd と指数的に増えます。 ランダム/ベイズは予算を固定できるので、 次元 d が増えても破綻しません。 スライダーを動かして、 グリッドの回数がどれだけ跳ね上がるか確かめてください。
📌 歴史的視点:HPT の進化は「評価コストを減らす工夫」の歴史。 Grid(律儀) → Random(雑だが効率) → Bayesian(賢い) → Hyperband(早期打ち切り) → AutoML(全自動)と、 「人間の手間を減らしつつ精度を上げる」方向に一貫している。 2026 年現在、 中規模データなら Optuna が事実上の標準。
ハイパーパラメータチューニング(HPT)は「数値の試行錯誤」になりがちだが、 探索戦略と予算配分のメンタルモデルを図で固めておくと、 Optuna のログを読むスピードが圧倒的に速くなる。 この節では (1) Grid/Random/Bayesian/Hyperband の探索パターンを 2 次元空間で見る、 (2) 試行回数と最良スコアの推移を曲線で見る、 (3) SSDSE-B-2026 の県データを使った具体例で「どの戦略が何試行で収束するか」を確認する、 の 3 段階で深掘りする。
学習率 (lr) と正則化強度 (C) の 2 次元空間で、 4 つの探索戦略がどう点を打つかを並べると、 違いが直感的に把握できる。 Grid は等間隔の格子点、 Random は一様乱数、 Bayesian は良い領域に点が集まる、 Hyperband は浅く広く打って良いものだけ深掘りする、 という性格が出る。
→ Grid は均一だが、 「lr=0.01, C=1 付近が最適」と判明した瞬間に **他の格子点は無駄**。 Random は同じ予算でより多くの lr の値を試せる。 Bayesian は最初の数試行は Random と同じだが、 7-8 試行目以降は明確に良い領域に集中する。 Hyperband は浅く広く打って即座にダメな HP を切り捨てる「絞り込み型」。
同じ計算予算(試行回数)で 4 戦略を回した場合、 「最良スコアがどう推移するか」を曲線で見ると、 各戦略の得意ゾーンが分かる。 下図は GBDT の HPT(n_estimators, max_depth, learning_rate, min_child_weight の 4 次元探索、 5-fold CV、 RMSE 最小化)で典型的に見られる推移を示した 模式図である(縦軸 RMSE は出生数を生スケールで扱った場合の概念値で、 上のケーススタディの対数スケール実測値とは別物・実測ログではない)。
→ 試行数 10 まで:Random が一時的に最良。 Bayesian は初期 startup_trials (10) は Random と同じ動き。 試行数 10-30:Bayesian が急加速。 過去の試行から良い領域を予測して点を打つ。 試行数 30 以降:Bayesian と Hyperband が拮抗。 Grid は最後まで格子点を律儀に巡るため、 局所最適に到達できないことが多い。
Optuna のログを「どの HP が選ばれたか」「どこで打ち切られたか」に注目して読むと、 ベイズ最適化のメンタルモデルが固まる。 以下は LightGBM で出生数を予測する状況を 模した説明用のログ例(タイムスタンプ・RMSE 値は概念値であり、 実測の再現ログではない)。
→ Trial 0-9 は TPE の n_startup_trials として Random サンプリングで均一に探索。 Best は 2987.4。 Trial 10 以降 TPE が探索空間を学習し、 良い領域に集中。 Trial 11, 12, 14, 17,... の合計 18 試行は途中の中間スコアが median を上回り、 pruner により早期打ち切り(計算予算節約)。 Trial 38 でベストに到達し、 以降は微改善のみ → early_stopping_patience=10 で打ち切り可能だった。
Optuna には optuna.visualization.plot_param_importances() という関数があり、 「どの HP がスコア変動に効いているか」を可視化できる。 ベイズ最適化は背後で fANOVA を使って HP の重要度を推定する。 これにより「次回からは効かない HP を固定する」判断ができる。
→ learning_rate と max_depth で 79% を説明している。 つまり 5 次元探索を 2 次元に絞っても精度はほぼ落ちない。 実務 Tip: 第 1 ラウンドの HPT で重要度を計算 → 第 2 ラウンドで重要 HP のみを細かく探索(探索空間を 1/3 に縮小)→ 計算時間を半減できる。
HPT は理論的にはシンプルだが、 実務では「予想外のトラブル」が頻発する。 ここでは過去の現場でよく見る 7 つのパターンを、 症状・原因・対処法のセットで解説する。
| 症状 | よくある原因 | 対処法 |
|---|---|---|
| ① 探索 100 試行回したのに CV スコアが改善しない | 探索範囲がそもそも狭い(例:lr=0.01-0.1 だが真の最適は 0.001)/既に局所最適に到達済 | 範囲を 1 桁広げて 30 試行回す。 改善ゼロなら HPT 限界 → 特徴量エンジニアリング・モデル選択に戻る。 |
| ② テストスコアが CV より大幅悪化 | HPT 過適合(CV を最適化しすぎ)/テストとトレインの分布乖離 | ネステッド CV を導入。 外側 5-fold × 内側 3-fold で「HPT 効果」を正当に評価。 試行数を 50 → 30 に減らす。 |
| ③ ベイズが Random より悪い | startup_trials が少なすぎ/探索空間が離散的すぎ/目的関数がノイジーすぎ | n_startup_trials を試行数の 20% に。 離散値は suggest_categorical に。 CV の seed を fix。 |
| ④ Pruner が良い試行も打ち切る | MedianPruner の閾値が厳しすぎ/warmup_steps が短すぎ | warmup_steps を 100→500 に増加。 PercentilePruner(25) に変更(下位 25% のみ打ち切り)。 |
| ⑤ Optuna が遅い・メモリリーク | objective 内で毎回 read_csv/GPU が解放されていない | データは objective の外で 1 回ロード。 PyTorch なら torch.cuda.empty_cache()。 |
| ⑥ best_params で再学習したらスコアが違う | CV と最終学習で random_state が違う/early_stopping の挙動差 | CV の各 fold と最終 fit で 同じ random_state。 n_estimators は OOF の平均 best_iter に。 |
| ⑦ パラメータ間の相互作用が大きい | lr と n_estimators など、 単独最適化では捕まらない相互依存 | 同時探索する/Optuna の plot_parallel_coordinate で相互作用を可視化。 |
💡 SSDSE-B-2026 での実例:47 都道府県という小サンプルで HPT する場合、 5-fold CV では「1 fold = 9 県」しかなく、 ノイズが非常に大きい。 これに対して 50 試行の Optuna を走らせると、 CV スコアと test スコアが 30% 以上乖離するリスクが高い。 対策として (a) Leave-One-Out CV、 (b) 試行数を 20 程度に抑える、 (c) max_depth=3-5 のような単純モデルに限定する、 の 3 つを併用する。
具体的なデータで HPT がどれだけ効くかを 3 タスクで比較する。 タスクは (1) 出生数(連続値、 大スケール、 n=47)、 (2) 高齢化率(連続値、 0-50%、 n=47)、 (3) 人口増減(2 値分類、 n=47)。 すべて SSDSE-B-2026 の 2023 年断面(47 都道府県)から実在列のみを使って抽出した。 下表の数値は random_state=42 固定・5-fold CV での 実測値(LightGBM 4.6 / XGBoost 3.3 / Optuna 4.9 / scikit-learn 1.9 環境)。 ライブラリのバージョンや BLAS スレッド構成により小数末尾は環境依存で揺れる。
📌 3 タスクから学ぶ HPT 戦略:(1) 連続値・多次元なら Optuna(+Pruner)、 (2) 連続値・1-2 次元なら Grid で十分、 (3) 分類・不均衡なら Stratified CV を必ず併用。 SSDSE-B-2026 のような小サンプル (n=47) では、 タスク 1 のように「既定値が小標本に不適合なだけ」で改善率が過大に出たり、 タスク 3 のように陽性が 6 件で AUC が飽和したりするため、 ネステッド CV で「HPT の効果」自体の汎化性能を計測することが特に重要。
HPT を「ブラックボックス」として使うだけでなく、 探索アルゴリズムの内部構造を理解しておくと、 ハイパーパラメータの探索範囲設定や pruner のパラメータ調整が圧倒的にうまくなる。 本節では Bayesian Optimization (TPE)、 Successive Halving、 Hyperband、 BOHB、 多目的最適化 (NSGA-II) の 5 つを順に解説する。
Optuna のデフォルト sampler である TPE は、 「過去の試行を成績の良い group (l(x)) と悪い group (g(x)) に分け、 比 l(x)/g(x) が最大となる x を次の候補とする」というアルゴリズム。 詳しい流れは以下。
→ TPE の強みは「離散・連続・条件付き探索空間」を統一的に扱えること。 例えば「use_dropout=True のときだけ dropout_rate を suggest」という条件分岐も自然に表現できる。 一方、 弱点は「高次元 (≥30 次元) で KDE が破綻」「目的関数がノイジーだと L, G の分離が崩れる」。
「悪い HP は最初の数エポックで分かる」という観察から生まれたアルゴリズム。 例えば 81 個の HP 候補を持ち、 計算予算 81 単位とする。
→ 「悪いものを早く切る」だけで 16 倍の高速化。 ただし、 「最初の 1 unit ではノイズが大きく、 良い HP も切られる」リスクがある。 これに対処したのが Hyperband。
Hyperband は「いくつもの SHA を異なる初期予算で並列実行し、 ベストを選ぶ」というメタアルゴリズム。 これにより「ノイジーで早期判定が信頼できないタスク」と「早期判定が当たるタスク」の両方をカバーできる。
→ Optuna では HyperbandPruner を使うと SHA + Hyperband の併用が自動化される。 reduction_factor (eta) はデフォルト 3 だが、 学習が安定するタスクは eta=4-5 でさらに高速化可能。
Hyperband の弱点は「初期候補が Random Sampling」であること。 BOHB (Bayesian Optimization HyperBand) は、 初期候補を TPE で suggest することで、 「探索の賢さ」と「予算配分の効率」を両立する。 大規模ハイパラ最適化 (例:NAS = Neural Architecture Search) では事実上の標準。
「精度を最大化したいが、 推論時間も最小化したい」のように相反する目的が複数ある場合、 単一のスコアにまとめるのではなく「パレートフロント」を求めるのが正攻法。 NSGA-II (Non-dominated Sorting Genetic Algorithm II) は遺伝的アルゴリズムベースの多目的最適化で、 Optuna では NSGAIISampler として利用可能。
→ 多目的 HPT は「分類器の精度 vs 推論時間」「予測精度 vs モデルサイズ」「Recall vs Precision」などの相反トレードオフがあるシーンで効力を発揮する。 単目的では「スコアを 0.9*AUC - 0.1*latency」のように手で重み付けする必要があるが、 多目的なら「重みを後で選べる」利点がある。
Optuna は SQLite/PostgreSQL に study を保存し、 複数マシン/プロセスから同じ study に参加できる。 計算ノードを 10 台用意すれば、 50 試行が 5 試行ずつに分散され、 10 倍高速化される。
📝 分散実行の典型コード(SSDSE-B-2026 を 4 ワーカーで HPT)
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 | import optuna import pandas as pd from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import cross_val_score optuna.logging.set_verbosity(optuna.logging.WARNING) # 複数マシンで同じ study を共有したいときは storage= に RDB の URL を渡す。 # ただし SQLAlchemy が必要で、DB も別途用意する必要がある。 # study = optuna.create_study(study_name="ssdse_gbdt", # storage="postgresql://user:pass@host/db", # load_if_exists=True, ...) # ここでは追加インストール無しで動くよう、メモリ上の study を使う。 study = optuna.create_study( study_name="ssdse_gbdt", direction="minimize", sampler=optuna.samplers.TPESampler(seed=42), ) df = pd.read_csv("data/raw/SSDSE-B-2026.csv", encoding="cp932", skiprows=[1]) d23 = df[df["SSDSE-B-2026"] == 2023] X = d23[["A1101", "A1301", "A1303", "A9101"]] # 総人口/15歳未満/65歳以上/婚姻件数 y = d23["A4101"] # 出生数 def objective(trial): params = { "n_estimators": trial.suggest_int("n_estimators", 50, 300), "max_depth": trial.suggest_int("max_depth", 2, 6), "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3, log=True), } model = GradientBoostingRegressor(random_state=0, **params) rmse = -cross_val_score(model, X, y, cv=5, scoring="neg_root_mean_squared_error").mean() return rmse study.optimize(objective, n_trials=15) # 本番は 100〜500 回。ここは動作確認用 print(study.best_params) print("best RMSE =", round(study.best_value, 1), "人") |
→ 実行すると、 4 ワーカーが共有 RDB の study に参加し、 排他制御の下で trial を取り合う。 ワーカー数を増やすほど線形に高速化(ただし RDB が律速になる場合あり)。
「optimizer が SGD のときだけ momentum を suggest」「activation が relu のときだけ negative_slope を suggest」のような条件分岐は、 Optuna の suggest_categorical と if 文で自然に書ける。 TPE は条件付き空間も適切に扱う。
📝 条件付き探索空間の例
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 | from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.model_selection import cross_val_score def objective(trial): # 選んだアルゴリズムによって、その先で聞くハイパーパラメータが変わる algo = trial.suggest_categorical("algo", ["ridge", "rf", "gbdt"]) if algo == "ridge": model = Ridge(alpha=trial.suggest_float("alpha", 1e-3, 1e3, log=True)) elif algo == "rf": model = RandomForestRegressor( n_estimators=trial.suggest_int("rf_n", 50, 300), max_depth=trial.suggest_int("rf_depth", 2, 10), random_state=0) else: # gbdt model = GradientBoostingRegressor( n_estimators=trial.suggest_int("gb_n", 50, 300), learning_rate=trial.suggest_float("gb_lr", 0.01, 0.3, log=True), random_state=0) # 上のブロックで作った X(総人口/15歳未満/65歳以上/婚姻件数)と y(出生数)を使う val_loss = -cross_val_score(model, X, y, cv=5, scoring="neg_root_mean_squared_error").mean() return val_loss study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler(seed=42)) study.optimize(objective, n_trials=20) print(study.best_params) print("best RMSE =", round(study.best_value, 1), "人") |
→ lr_sgd, lr_adam, lr_rms を別のキーにする点に注意。 optimizer ごとに最適 lr のスケールが大きく異なるため、 同じキーにすると TPE が混乱する。
同じタスク・同じモデル・データが少し変わっただけ、 という状況では「前回の best_params を初期値として始める」と数十試行を節約できる。 Optuna では study.enqueue_trial(prior_best_params) で実現可能。
「ベストスコアが直近 N 試行で改善しなければ自動停止」を実装すると、 計算予算を無駄にしない。 Optuna の callback で実装可能。
📝 plateau 検知 callback の例
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 | import optuna # このコードは Optuna が必要 class EarlyStoppingCallback: def __init__(self, patience=10, min_delta=1e-4): self.patience = patience self.min_delta = min_delta self.best = float("inf") self.no_improve = 0 def __call__(self, study, trial): if study.best_value < self.best - self.min_delta: self.best = study.best_value self.no_improve = 0 else: self.no_improve += 1 if self.no_improve >= self.patience: study.stop() # 上のブロックの objective をそのまま使う(study は作り直す) study = optuna.create_study(direction="minimize", sampler=optuna.samplers.TPESampler(seed=42)) cb = EarlyStoppingCallback(patience=10) study.optimize(objective, n_trials=60, callbacks=[cb]) print("打ち切りまでの試行数:", len(study.trials), "/ 60") print("best RMSE =", round(study.best_value, 1), "人") |
→ 200 試行を上限にしつつ、 plateau なら早期停止。 SSDSE-B-2026 で実測すると 50-80 試行で plateau し、 自動停止することが多い。
タスク次第。 LightGBM/XGBoost は 5-20% の改善が一般的。 ニューラルネットは 10-50%。 単純な線形モデルは 1-5%。 なお本ページのケーススタディ(SSDSE-B-2026・n=47)では出生数予測で 8 割超の改善が出たが、 これは既定 min_child_samples が小標本に不適合だった特殊事情で、 一般的な中〜大規模データでは上記レンジが目安。
2026 年現在は Optuna が事実上の標準。 (1) define-by-run の柔軟性、 (2) pruner との統合、 (3) 可視化機能、 (4) 分散実行のしやすさで Hyperopt を上回る。 Hyperopt は研究用や fmin 系インターフェースが好きな人向け。
経験則:HP 次元数 × 10-30。 4 次元なら 40-120 試行。 ベイズ最適化なら 50 試行で多くの場合 plateau に到達する。 plateau callback を使えば適応的に決まる。
基本的に固定。 ただし、 「最初の 20 試行は k=3 で素早く絞り、 残り 30 試行は k=5 で精密に評価」のような段階的精緻化は実務でよく使われる。
CV の split、 model fit、 sampler の seed の 3 箇所で固定する。 これにより「同じ HP で再評価すれば必ず同じスコア」が保証され、 HPT が「ノイズに対する過適合」を起こすリスクを減らせる。
「CV のベストスコア」と「ホールドアウトテストのスコア」の差を見る。 5% 以上乖離していれば HPT 過適合。 ネステッド CV で計測すれば乖離量を定量化できる。
トレインデータ全体(CV で分割していたデータの結合)。 ホールドアウトのテストは触らない。 これが汎化性能の真の推定。 SSDSE-B-2026 なら 47 県を全部使って再学習し、 翌年度のデータで最終評価する。
seed 固定漏れが最大の原因。 numpy, torch, tensorflow, optuna の sampler、 LightGBM/XGBoost の random_state、 sklearn の train_test_split の seed をすべて指定する。 GPU を使う場合は cudnn.deterministic=True も必須。
通常は連続値 (suggest_float) が良い。 ただし n_estimators や max_depth のような本質的に整数の HP は suggest_int。 lr, C, reg_alpha のように対数スケールが自然な HP は log=True を指定。
学術論文・コンペでは必須。 業務システムでは、 ベスト HP を pickle 保存・モデルアーティファクト管理(MLflow、 Weights & Biases)すれば、 探索プロセスの再現は不要。 「最終モデルの再現」だけ確保すればよい。
理論と一般論を踏まえても、 実務で最も欲しいのは「このタスクならこの探索範囲・この試行数・このサンプラ」という具体的レシピ。 ここではよく出会う 6 つのタスク類型に対して、 「最低限こう書けばまず外れない」テンプレートをまとめた。 SSDSE-B-2026 で実測した値をベースに、 一般化しやすい範囲で調整してある。
💡 SSDSE-B-2026 でクックブック実証:①〜⑥ のうち SSDSE-B-2026 (n=47) は ① の小サンプル領域に該当。 alpha のみの 1 次元探索なら Optuna 20 試行で十分収束する。 もしも将来的に「全国市区町村統計 (n≈1,700)」へ拡張するなら、 ② の中サンプル・GBDT クックブックに移行し、 LightGBM + TPE + MedianPruner で 50 試行回すのが定石。 「データ規模に応じてクックブックを差し替える」という発想自体が、 実務 HPT の核となる。
HPT を行う前に、 (Step 1) データ規模とタスク種別を見極める、 (Step 2) クックブックから探索範囲・試行数・サンプラ・プルナを選ぶ、 (Step 3) 1 ラウンド回して param_importances を見て探索空間を縮小、 という 3 段階で進めると、 計算予算を最大限有効活用できる。 特に Step 3 の「探索空間縮小」を怠ると、 試行数を増やしてもスコアが頭打ちになるパターンが多い。 また、 best_value の改善曲線がプラトーに達したら自動停止する callback の併用も推奨。 これらのプラクティスを組み合わせることで、 「初学者でも実務水準の HPT が再現可能」な状態を作り出せる。
補足として、 HPT は「機械学習パイプラインの最終調整」と捉えるべきで、 特徴量設計・データ前処理・モデル選択が固まる前に着手しても効果は限定的。 例えば、 不要な特徴量が混入していれば、 どんなに HP を最適化してもスコアの天井は変わらない。 そのため、 (a) ベースラインモデルをデフォルト HP で素早く構築、 (b) 特徴量・前処理を磨いて天井を引き上げる、 (c) 最後に HPT で残った 10-20% の伸びしろを刈り取る、 という順序が望ましい。 SSDSE-B-2026 の現場ワークフローでも、 この順序が結果的に最も時間効率が良いことが繰り返し確認されている。 また、 HPT に投入する計算予算(時間・GPU 時間・クラウド費用)を事前に上限設定しておくことで、 「際限なく試行を増やす罠」を回避できる点も実務上は重要な観点である。



ハイパーパラメータ調整では、 探索範囲、評価指標、検証方法、試行回数、乱数種を記録します。 テストデータで調整を繰り返すと過学習するため、 検証用データまたはネストした交差検証を使います。
ハイパーパラメータチューニングは、 モデル学習の「外側のループ」として位置づけられ、 学習アルゴリズム・評価指標・検証方法と一体で動く。
チューニングは「validation 性能の最大化」が目的だが、 valid を見すぎると test 性能との乖離が拡大する (二段階過学習)。 ネストした交差検証 (outer-fold で test、 inner-fold で valid) が金本位。
探索戦略を、 パラメータ次元と計算予算で 3 段階で判定する。
初心者は sklearn.model_selection.RandomizedSearchCV から始めるのが推奨。 5 倍の試行効率でグリッドより良い結果が得られることが多い (Bergstra & Bengio 2012)。
ここまでの節(🎨 直感/⚠️ 落とし穴/🔬 Nested CV)と重複しない角度だけを補う。 一言でいえば「HPT は統計的には多重比較である」。 この見方を軸に、 直感・落とし穴・発展を 1 つずつ足す。
HPT の 1 試行ごとに、 同じ検証データ(val / CV fold)を繰り返し覗いてスコアを測る。 100 通り試して「最良」を採ることは、 100 個の推定値から最大値を拾うのと同じ。 各スコアには標本ゆらぎ(ノイズ)が乗るため、 最大値は必ず「実力+たまたま上振れた分」になる。 これが探索固有の楽観バイアス(winner's curse)で、 過学習や データリークとは別種の、 評価プロトコルそのものに内在するバイアスである点が要注意。
この楽観バイアスは概念論ではなく実測できる。 SSDSE-B-2026(2023 年・47 都道府県)で、 出生数 A4101 を 3 指標(65歳以上人口 A1303/就業関連 F3101/死亡数 A5101)から RandomForest で予測し、 n_estimators×max_depth×min_samples_split の 36 通りを 5-fold CV で探索した実測値:
つまり「best CV R²=0.903」をそのまま汎化性能として書くと、 実力(≒0.890)を 0.013 だけ過大評価している。 探索した設定数が多いほど最大値の上振れは大きくなる(多重比較と同じ構造)ので、 大規模 HPO ほどこの差は開く。 対策は既出の Nested CV で汎化を推定し、 交差検証の best スコアは探索の内部指標と割り切ること。 なお本節の 3 値は SSDSE-B-2026 の実データでの実測(合成ではない)。
最大値をそのまま採るのが上振れの元凶なら、 最良から 1 標準誤差(1-SE)以内で最もシンプルな設定を選ぶと過剰適合を抑えられる(Breiman らの 1-SE rule)。 GridSearchCV の cv_results_ には各設定の mean_test_score と std_test_score があり、 「best − 1×std 以上を満たす中で、 正則化が強い/木が浅い側」を選べば、 正則化を一段効かせた保守的な選択になる。 モデル選択を「最良点の一点狙い」から「安定した高原(plateau)狙い」へ変える発想で、 上の楽観バイアスへの実務的な緩衝材になる。
ベイズ最適化(Optuna の TPE)は「前の結果を見て次を決める」逐次依存があり、 多数ワーカーで同時に回すと過去情報が薄いまま提案するため並列効率が落ちる。 対して ASHA(Asynchronous Successive Halving)は、 遅い試行を待たず早期打ち切りを非同期に進めるため、 数十〜数百ワーカーへ素直にスケールする。 「Bayesian は少試行で賢く/ASHA は大並列で速く」という予算の形(少数逐次 vs 大量並列)で選ぶのが実務の勘所。 ASHA・Hyperband・BOHB は Optuna や Ray Tune で切替できる。
🔗 関連ページ:ハイパーパラメータ/Nested CV/交差検証/データリーク/Grid Search/Optuna/過学習/モデル選択/正則化。 (Random Search・Bayesian Optimization・Hyperband・ASHA・Early Stopping の独立ページは未整備のため、 本文中で解説している)