Day 064 — Optuna ハイパーパラメータ最適化 — GBDTの自動チューニング(XGBoost / LightGBM / CatBoost)

2026-06-14 緑 / Phase 2 コーディング Optuna・ベイズ最適化・suggest_*・パラメータ重要度

📚 背景知識(読んでから問題へ)

🎯
Day 064 — XGBoost(Day 061)・LightGBM(Day 062)・CatBoost(Day 063)と学んだ3大GBDTを Optuna で自動チューニングするスキルを身につけます。手動で探していたハイパーパラメータを、ベイズ最適化で賢く自動探索します。

なぜハイパーパラメータのチューニングが必要か?

状況デフォルトで学習チューニング後
Titanic(n=891)の典型 CV Accuracy ≈ 0.80 CV Accuracy ≈ 0.83〜0.85
House Prices(回帰)の典型 RMSE ≈ 30,000 RMSE ≈ 22,000(改善率 27%)
コンペ上位との差 LBスコア 0.80(中位) チューニングでTop 20%に入ることも

チューニング手法の比較

手法仕組み問題点Kaggleでの使用
グリッドサーチ 全組み合わせを試す 組み合わせ爆発(4パラメータ×5段階 = 625回) ほぼ使わない
ランダムサーチ ランダムに組み合わせを試す 運任せ・非効率 たまに使う
Optuna(ベイズ最適化) 過去の結果を参考に次の点を選ぶ 効率的・少ない試行数で高精度 上位入賞者の標準ツール

🔄 Optuna の仕組み — study / trial / objective

Optuna の実行フロー

1
study = optuna.create_study(direction='maximize')
最適化セッション(記録帳)を作成。最大化か最小化かを指定。
2
def objective(trial): ...
1回の実験を定義する関数。trial.suggest_*() でパラメータを提案させ、モデルを学習して評価値を返す。
3
study.optimize(objective, n_trials=50)
objective を n_trials 回呼び出す。内部でベイズ最適化アルゴリズム(TPE)が次の試行点を決める。
4
study.best_params / study.best_value
全試行のうち最も良かったパラメータとスコアを取得。

study と trial の関係

study(最適化セッション全体) Trial 1 lr=0.1 depth=6 0.821 評価値 Trial 2 lr=0.03 depth=8 0.835 評価値 Trial N lr=0.05 depth=7 0.848 ★ BEST best_params lr=0.05 depth=7

🎛️ suggest_* メソッド一覧とパラメータ対応

trial.suggest_int(name, low, high)
整数型
木の本数・深さ・葉の最小サンプル数など整数値のパラメータに使用。
例: suggest_int('n_estimators', 100, 1000)
例: suggest_int('max_depth', 3, 10)
例: suggest_int('num_leaves', 20, 300)
trial.suggest_float(name, low, high)
浮動小数点型(線形スケール)
0〜1 の範囲のサンプリング率などに使用。
例: suggest_float('subsample', 0.5, 1.0)
例: suggest_float('colsample_bytree', 0.5, 1.0)
例: suggest_float('reg_alpha', 0.0, 1.0)
trial.suggest_float(name, low, high, log=True)
浮動小数点型(対数スケール)
learning_rate / 正則化係数などの小さい値に使用。
例: suggest_float('learning_rate', 0.01, 0.3, log=True)
例: suggest_float('l2_leaf_reg', 1.0, 30.0, log=True)
なぜlog? → 0.01と0.02の差も0.1と0.2の差も同等に扱うため
trial.suggest_categorical(name, choices)
カテゴリ選択型
離散的な選択肢のパラメータに使用。
例: suggest_categorical('booster', ['gbtree', 'dart'])
例: suggest_categorical('boosting_type', ['Ordered', 'Plain'])
例: suggest_categorical('grow_policy', ['SymmetricTree', 'Depthwise'])

🧠 ベイズ最適化の直感 — 賢く山の頂上を探す

3つの探索戦略の比較(横軸: learning_rate、縦軸: CV Accuracy)

真のスコア曲線(未知) グリッドサーチ(全均等) ★ BEST グリッドサーチ(均等だが無駄が多い) ランダムサーチ(散らばって非効率) Optuna(山の近くに集中して効率的) learning_rate → CV Accuracy

📊 対数スケール vs 線形スケール — なぜ log=True が必要か

learning_rate = 0.001〜0.1 の範囲を8点で探索する場合の違い

線形スケール(log=False) 0.001 0.014 0.027 0.040 0.053 0.066 0.079 0.100 0.001〜0.01 の重要な範囲が1点しか探索されない 対数スケール(log=True) 0.001 0.002 0.004 0.008 0.017 0.037 0.079 0.100 各桁(0.001, 0.01, 0.1)を均等に探索できる
💡
経験則: learning_rate / l2_leaf_reg / reg_alpha / min_child_weightlog=True を使う。subsample / colsample_bytree のような 0〜1 の範囲は線形スケールで十分。

チューニング効果の目安(Titanic Accuracy)

デフォルトパラメータ
baseline
≈ 0.800
ランダムサーチ(50試行)
+0.03
≈ 0.820
Optuna ベイズ最適化(30試行)
+0.04〜0.06
≈ 0.835〜0.845
Optuna(100試行 + アンサンブル)
+0.06〜0.10
≈ 0.855+

🗂️ データスキーマ(Titanic 風 n=5,000)

列名値の範囲説明チューニングでの重要度目安
Pclassint1, 2, 3旅客クラス中(GBDTが自動で扱う)
Sexstrmale, female性別(最重要特徴量)
Agefloat1〜80年齢中〜高
SibSpint0〜8同乗兄弟/配偶者数低〜中
Parchint0〜6同乗親/子供数低〜中
Farefloat0〜∞運賃
EmbarkedstrS, C, Q乗船港
Cabin_typestrA,B,C,D,E,Noneキャビン種別低〜中
Survivedint0 / 1生存(目的変数)

📝 問題

セットアップコード(最初に実行)

import numpy as np
import pandas as pd
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import LabelEncoder
import warnings
warnings.filterwarnings('ignore')

try:
    import optuna
    optuna.logging.set_verbosity(optuna.logging.WARNING)
    print("Optuna: OK")
except ImportError:
    print("pip install optuna")

try:
    import xgboost as xgb
    print("XGBoost: OK")
except ImportError:
    print("pip install xgboost")

try:
    import lightgbm as lgb
    print("LightGBM: OK")
except ImportError:
    print("pip install lightgbm")

try:
    from catboost import CatBoostClassifier
    print("CatBoost: OK")
except ImportError:
    print("pip install catboost")

# Titanic 風データ生成(n=5000)
np.random.seed(42)
n = 5000
data = pd.DataFrame({
    'Pclass':     np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
    'Sex':        np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
    'Age':        np.random.normal(29.7, 14.5, n).clip(1, 80),
    'SibSp':      np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
    'Parch':      np.random.choice(range(7), n, p=[0.76,0.13,0.09,0.005,0.005,0.005,0.005]),
    'Fare':       np.abs(np.random.normal(32, 50, n)),
    'Embarked':   np.random.choice(['S', 'C', 'Q'], n, p=[0.72, 0.19, 0.09]),
    'Cabin_type': np.random.choice(['A','B','C','D','E','None'], n,
                                   p=[0.05,0.08,0.12,0.1,0.07,0.58]),
})
data['Survived'] = (
    (data['Sex'] == 'female') * 0.55 + (data['Pclass'] == 1) * 0.25 +
    (data['Age'] < 16) * 0.2 + np.random.normal(0, 0.12, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)

cat_cols = ['Sex', 'Embarked', 'Cabin_type']
data_enc = data.copy()
for col in cat_cols:
    data_enc[col] = LabelEncoder().fit_transform(data_enc[col])

features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked', 'Cabin_type']
X_raw = data[features]
X_enc = data_enc[features].values
y     = data['Survived'].values
cv    = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

def manual_cv_score(clf, X, y, cv):
    scores = []
    for tr_idx, val_idx in cv.split(X, y):
        if isinstance(X, pd.DataFrame):
            X_tr, X_val = X.iloc[tr_idx], X.iloc[val_idx]
        else:
            X_tr, X_val = X[tr_idx], X[val_idx]
        y_tr, y_val = y[tr_idx], y[val_idx]
        clf.fit(X_tr, y_tr)
        scores.append(accuracy_score(y_val, clf.predict(X_val)))
    return float(np.mean(scores))

print("データ準備完了:", X_raw.shape)

問1 — XGBoost の Optuna チューニング(基本)

  1. objective(trial) 関数を定義し、以下のパラメータを最適化せよ
    • n_estimators: 100〜1000(整数)
    • max_depth: 3〜10(整数)
    • learning_rate: 0.01〜0.3(対数スケール浮動小数点)
    • subsample: 0.5〜1.0(浮動小数点)
    • colsample_bytree: 0.5〜1.0(浮動小数点)
  2. study.optimize(objective, n_trials=30) で最適化を実行せよ
  3. study.best_paramsstudy.best_value を表示せよ
  4. デフォルトパラメータとの精度差を表示せよ

問2 — LightGBM の Optuna チューニング

  1. LightGBM 向けの objective 関数を定義し、以下を最適化せよ
    • n_estimators: 100〜1000(整数)
    • num_leaves: 20〜300(整数)
    • learning_rate: 0.01〜0.3(対数スケール)
    • min_child_samples: 5〜100(整数)
    • reg_alpha: 0.0〜1.0(浮動小数点)
    • reg_lambda: 0.0〜10.0(浮動小数点)
  2. n_trials=30 で最適化し、best_params と best_value を表示せよ
  3. デフォルトパラメータとの精度差を表示せよ

問3 — CatBoost の Optuna チューニング

  1. CatBoost 向けの objective 関数を定義し、以下を最適化せよ
    • iterations: 100〜500(整数)
    • depth: 4〜10(整数)
    • learning_rate: 0.01〜0.3(対数スケール)
    • l2_leaf_reg: 1.0〜30.0(対数スケール)
  2. cat_features=['Sex','Embarked','Cabin_type'] を使用すること
  3. n_trials=20 で最適化(CatBoostは遅いため少なめ)し、結果を表示せよ

問4 — 最適化の履歴を可視化(matplotlib)

  1. 問1 の study オブジェクトを使い、試行ごとのスコア推移を折れ線グラフで描画せよ
  2. 各試行の累積最良スコア(cumulative best)も一緒にプロットせよ
  3. グラフのタイトルと軸ラベル、凡例を設定せよ

問5 — パラメータ重要度分析

  1. optuna.importance.get_param_importances(study) で問1 のパラメータ重要度を取得せよ
  2. 棒グラフで可視化せよ
  3. どのパラメータが XGBoost のスコアに最も影響を与えるか考察せよ

💡 ヒント

ヒント1(方向性)
  • pip install optuna でインストール。optuna.logging.set_verbosity(optuna.logging.WARNING) でログを抑制すること
  • CatBoost の objective 内では manual_cv_score を使うこと(cross_val_score は cat_features を保持しないため)
  • XGBoost / LightGBM は cross_val_score で直接評価できる
  • direction='maximize' を忘れずに。デフォルトは minimize
ヒント2(アプローチ)
import optuna

def objective_xgb(trial):
    params = {
        'n_estimators':     trial.suggest_int('n_estimators', 100, 1000),
        'max_depth':        trial.suggest_int('max_depth', 3, 10),
        'learning_rate':    trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'subsample':        trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'random_state': 42,
        'eval_metric': 'logloss',
    }
    clf = xgb.XGBClassifier(**params)
    score = cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()
    return score

study_xgb = optuna.create_study(direction='maximize')
study_xgb.optimize(objective_xgb, n_trials=30)
print(f"Best: {study_xgb.best_value:.4f}")
print(study_xgb.best_params)
ヒント3(コード骨格)
# パラメータ重要度
from optuna.importance import get_param_importances

importances = get_param_importances(study_xgb)
print(importances)

# 可視化
import matplotlib.pyplot as plt

# 試行スコア推移
scores = [t.value for t in study_xgb.trials]
best_so_far = [max(scores[:i+1]) for i in range(len(scores))]

plt.figure(figsize=(10, 4))
plt.plot(scores, alpha=0.5, label='各試行スコア')
plt.plot(best_so_far, color='red', label='累積最良スコア')
plt.xlabel('試行番号')
plt.ylabel('CV Accuracy')
plt.title('XGBoost Optuna 最適化 — スコア推移')
plt.legend()
plt.tight_layout()
plt.show()

# 棒グラフ(重要度)
plt.figure(figsize=(8, 4))
plt.barh(list(importances.keys()), list(importances.values()))
plt.xlabel('重要度スコア')
plt.title('パラメータ重要度')
plt.tight_layout()
plt.show()

模範解答

import numpy as np
import pandas as pd
import optuna
import xgboost as xgb
import lightgbm as lgb
from catboost import CatBoostClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score
from optuna.importance import get_param_importances
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
optuna.logging.set_verbosity(optuna.logging.WARNING)

# ── データ準備 ──
np.random.seed(42)
n = 5000
data = pd.DataFrame({
    'Pclass':     np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
    'Sex':        np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
    'Age':        np.random.normal(29.7, 14.5, n).clip(1, 80),
    'SibSp':      np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
    'Parch':      np.random.choice(range(7), n, p=[0.76,0.13,0.09,0.005,0.005,0.005,0.005]),
    'Fare':       np.abs(np.random.normal(32, 50, n)),
    'Embarked':   np.random.choice(['S', 'C', 'Q'], n, p=[0.72, 0.19, 0.09]),
    'Cabin_type': np.random.choice(['A','B','C','D','E','None'], n,
                                   p=[0.05,0.08,0.12,0.1,0.07,0.58]),
})
data['Survived'] = (
    (data['Sex'] == 'female') * 0.55 + (data['Pclass'] == 1) * 0.25 +
    (data['Age'] < 16) * 0.2 + np.random.normal(0, 0.12, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)

cat_cols = ['Sex', 'Embarked', 'Cabin_type']
data_enc = data.copy()
for col in cat_cols:
    data_enc[col] = LabelEncoder().fit_transform(data_enc[col])

features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked', 'Cabin_type']
X_raw = data[features]
X_enc = data_enc[features].values
y     = data['Survived'].values
cv    = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

def manual_cv_score(clf, X, y, cv):
    scores = []
    for tr_idx, val_idx in cv.split(X, y):
        if isinstance(X, pd.DataFrame):
            X_tr, X_val = X.iloc[tr_idx], X.iloc[val_idx]
        else:
            X_tr, X_val = X[tr_idx], X[val_idx]
        y_tr, y_val = y[tr_idx], y[val_idx]
        clf.fit(X_tr, y_tr)
        scores.append(accuracy_score(y_val, clf.predict(X_val)))
    return float(np.mean(scores))

# ── 問1: XGBoost ──
print("=" * 65)
print("問1: XGBoost Optuna チューニング")
print("=" * 65)

default_xgb_score = cross_val_score(
    xgb.XGBClassifier(random_state=42, eval_metric='logloss'),
    X_enc, y, cv=cv, scoring='accuracy'
).mean()
print(f"デフォルト XGBoost CV: {default_xgb_score:.4f}")

def objective_xgb(trial):
    params = {
        'n_estimators':     trial.suggest_int('n_estimators', 100, 1000),
        'max_depth':        trial.suggest_int('max_depth', 3, 10),
        'learning_rate':    trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'subsample':        trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'random_state': 42, 'eval_metric': 'logloss',
    }
    clf = xgb.XGBClassifier(**params)
    return cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()

study_xgb = optuna.create_study(direction='maximize')
study_xgb.optimize(objective_xgb, n_trials=30)
print(f"最適化 XGBoost CV: {study_xgb.best_value:.4f}")
print(f"改善幅: {study_xgb.best_value - default_xgb_score:+.4f}")
print("最良パラメータ:")
for k, v in study_xgb.best_params.items():
    print(f"  {k}: {v}")

# ── 問2: LightGBM ──
print("\n" + "=" * 65)
print("問2: LightGBM Optuna チューニング")
print("=" * 65)

default_lgb_score = cross_val_score(
    lgb.LGBMClassifier(random_state=42, verbose=-1),
    X_enc, y, cv=cv, scoring='accuracy'
).mean()
print(f"デフォルト LightGBM CV: {default_lgb_score:.4f}")

def objective_lgb(trial):
    params = {
        'n_estimators':      trial.suggest_int('n_estimators', 100, 1000),
        'num_leaves':        trial.suggest_int('num_leaves', 20, 300),
        'learning_rate':     trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
        'reg_alpha':         trial.suggest_float('reg_alpha', 0.0, 1.0),
        'reg_lambda':        trial.suggest_float('reg_lambda', 0.0, 10.0),
        'random_state': 42, 'verbose': -1,
    }
    clf = lgb.LGBMClassifier(**params)
    return cross_val_score(clf, X_enc, y, cv=cv, scoring='accuracy').mean()

study_lgb = optuna.create_study(direction='maximize')
study_lgb.optimize(objective_lgb, n_trials=30)
print(f"最適化 LightGBM CV: {study_lgb.best_value:.4f}")
print(f"改善幅: {study_lgb.best_value - default_lgb_score:+.4f}")
print("最良パラメータ:")
for k, v in study_lgb.best_params.items():
    print(f"  {k}: {v}")

# ── 問3: CatBoost ──
print("\n" + "=" * 65)
print("問3: CatBoost Optuna チューニング")
print("=" * 65)

default_cat_score = manual_cv_score(
    CatBoostClassifier(iterations=200,
                       cat_features=['Sex','Embarked','Cabin_type'],
                       random_seed=42, verbose=0),
    X_raw, y, cv
)
print(f"デフォルト CatBoost CV: {default_cat_score:.4f}")

def objective_cat(trial):
    params = {
        'iterations':    trial.suggest_int('iterations', 100, 500),
        'depth':         trial.suggest_int('depth', 4, 10),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'l2_leaf_reg':   trial.suggest_float('l2_leaf_reg', 1.0, 30.0, log=True),
        'cat_features': ['Sex', 'Embarked', 'Cabin_type'],
        'random_seed': 42, 'verbose': 0,
    }
    clf = CatBoostClassifier(**params)
    return manual_cv_score(clf, X_raw, y, cv)

study_cat = optuna.create_study(direction='maximize')
study_cat.optimize(objective_cat, n_trials=20)
print(f"最適化 CatBoost CV: {study_cat.best_value:.4f}")
print(f"改善幅: {study_cat.best_value - default_cat_score:+.4f}")
print("最良パラメータ:")
for k, v in study_cat.best_params.items():
    print(f"  {k}: {v}")

# ── 問4: スコア推移可視化 ──
print("\n" + "=" * 65)
print("問4: スコア推移可視化")
print("=" * 65)

scores = [t.value for t in study_xgb.trials]
best_so_far = [max(scores[:i+1]) for i in range(len(scores))]

plt.figure(figsize=(10, 4))
plt.plot(scores, alpha=0.4, marker='o', markersize=4, label='各試行スコア', color='#38bdf8')
plt.plot(best_so_far, color='#f0c419', linewidth=2, label='累積最良スコア')
plt.axhline(default_xgb_score, color='#ef4444', linestyle='--', linewidth=1,
            label=f'デフォルト ({default_xgb_score:.4f})')
plt.xlabel('試行番号')
plt.ylabel('CV Accuracy (5-Fold)')
plt.title('XGBoost Optuna 最適化 — スコア推移 (n_trials=30)')
plt.legend()
plt.tight_layout()
plt.show()

# ── 問5: パラメータ重要度 ──
print("\n" + "=" * 65)
print("問5: パラメータ重要度分析")
print("=" * 65)

importances = get_param_importances(study_xgb)
print("パラメータ重要度:")
for k, v in importances.items():
    bar = "█" * int(v * 40)
    print(f"  {k:20s}: {v:.4f} {bar}")

plt.figure(figsize=(8, 4))
params_list = list(importances.keys())
values_list = list(importances.values())
colors = ['#22c55e' if v == max(values_list) else '#38bdf8' for v in values_list]
plt.barh(params_list, values_list, color=colors)
plt.xlabel('重要度スコア')
plt.title('XGBoost ハイパーパラメータ重要度 (Optuna)')
plt.tight_layout()
plt.show()

max_param = max(importances, key=importances.get)
print(f"\n考察: 最重要パラメータは '{max_param}'")
print("→ このパラメータのチューニングに注力すると効率的にスコアが改善できる")

🪜 Step-by-Step 解説

1Optuna の基本概念 — study と trial

import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)  # ログ抑制(必須)

# study = 1回の最適化セッション(記録帳)
study = optuna.create_study(direction='maximize')  # スコアを最大化

# trial = 1回の試行(パラメータ1セットの実験)
# objective 関数内で trial.suggest_* を使う

study は「最適化の記録帳」、trial は「今回試す1つの実験」のイメージ。direction='maximize' でスコアを上げる方向に最適化。

2suggest_* の使い分け

def objective(trial):
    # 整数: n_estimators, max_depth, num_leaves など
    n = trial.suggest_int('n_estimators', 100, 1000)

    # 浮動小数点(線形スケール): subsample, colsample_bytree など
    sub = trial.suggest_float('subsample', 0.5, 1.0)

    # 浮動小数点(対数スケール): learning_rate, l2 正則化係数
    # → 0.001 〜 0.1 のような小さい値の探索に使う
    lr = trial.suggest_float('learning_rate', 0.01, 0.3, log=True)

    # カテゴリ選択
    booster = trial.suggest_categorical('booster', ['gbtree', 'dart'])

3CatBoost の注意点 — cross_val_score が使えない問題

# NG: cross_val_score は cat_features を保持しない
score = cross_val_score(
    CatBoostClassifier(cat_features=['Sex', ...], verbose=0),
    X_raw, y, cv=cv
).mean()  # エラーまたは精度が低い

# OK: 手動CVで DataFrame を iloc でスライスする
def manual_cv_score(clf, X, y, cv):
    scores = []
    for tr_idx, val_idx in cv.split(X, y):
        X_tr, X_val = X.iloc[tr_idx], X.iloc[val_idx]
        y_tr, y_val = y[tr_idx], y[val_idx]
        clf.fit(X_tr, y_tr)
        scores.append(accuracy_score(y_val, clf.predict(X_val)))
    return np.mean(scores)

4累積最良スコアの計算

scores = [t.value for t in study.trials]
# t.value = その試行のスコア

best_so_far = [max(scores[:i+1]) for i in range(len(scores))]
# i 番目まで(含む)のスコアの最大値
# → グラフにすると「何回目でベストが更新されたか」が一目でわかる

5パラメータ重要度の読み方

from optuna.importance import get_param_importances

importances = get_param_importances(study)
# {'learning_rate': 0.45, 'max_depth': 0.30, 'n_estimators': 0.15, ...}
# → 合計が1になる相対的な重要度
# → 重要度が高いほど「そのパラメータがスコアに与える影響が大きい」

📐 数学・統計の補足(文系向け)

ベイズ最適化を直感的に理解する

段階ランダムサーチOptuna(ベイズ最適化)
1回目 でたらめに試す でたらめに試す(同じ)
2〜5回目 またでたらめ 1回目の結果を参考に良さそうな場所を絞る
N回後 N回ランダムに試した 賢く良い場所に集中して試した
💡
山探しの例え: ランダムサーチ = 地図なしで山をでたらめに歩く。ベイズ最適化 = 「今いる場所の高さ」を記録しながら、より高い方向へ賢く移動する。

対数スケールが必要な理由

スケール0.001〜0.1 の8点探索例問題点
線形スケール 0.001, 0.014, 0.028, 0.042, 0.055, 0.069, 0.083, 0.100 0.001〜0.01 の重要な低領域が粗い
対数スケール 0.001, 0.002, 0.004, 0.008, 0.017, 0.037, 0.079, 0.100 各桁を均等に探索できる

🏆 Kaggleでの実践的な使い方

場面Optuna の使い方ポイント
コンペ序盤のベースライン n_trials=20〜30 で粗く探索 速度優先。LightGBM が最速
コンペ中盤 n_trials=50〜100 で精密探索 探索範囲を粗い探索の結果で絞る
最終提出前 best_params でアンサンブル XGB + LGB + CatBoost の3モデルを多数決
制限時間がある場合 early_stopping_rounds と組み合わせ 各trial の学習を早期終了して n_trials を増やす
# Kaggle 本番パターン: 二段階チューニング
# Phase 1: 粗い探索(n_trials=20)
study = optuna.create_study(direction='maximize')
study.optimize(objective_xgb, n_trials=20)

# Phase 2: best_params 周辺を精密探索
best_lr = study.best_params['learning_rate']
best_depth = study.best_params['max_depth']

def objective_fine(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 300, 800),
        'max_depth':    trial.suggest_int('max_depth',
                                          max(3, best_depth-2),
                                          min(12, best_depth+2)),
        'learning_rate': trial.suggest_float('learning_rate',
                                              best_lr*0.5, best_lr*2.0, log=True),
        'subsample':        trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'random_state': 42, 'eval_metric': 'logloss',
    }
    return cross_val_score(xgb.XGBClassifier(**params), X_enc, y,
                           cv=cv, scoring='accuracy').mean()

study2 = optuna.create_study(direction='maximize')
study2.optimize(objective_fine, n_trials=30)
print(f"最終 Best: {study2.best_value:.4f}")

🚫 よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
direction を指定しない デフォルトで動く気がする デフォルトは 'minimize'。accuracy は最大化なので 'maximize' を明示
log=True なしで learning_rate を探索 線形の方が均等に見える 小さい値のレンジには必ず log=True を使う
CatBoost で cross_val_score を使う sklearn の標準 API だから DataFrame の iloc でスライスする手動 CV が必要
n_trials を大きくしすぎる 多いほど良いと思う CV × trials 回学習。コンペ制限時間に注意。30〜50 が現実的
best_value がデフォルトより低い チューニングしたのに… 探索範囲が外れていることが多い。デフォルト値を探索範囲内に含める
ログを抑制しない デフォルト確認不足 optuna.logging.set_verbosity(optuna.logging.WARNING) を最初に呼ぶ

🚀 次のステップ

  • 発展: optuna.integration.OptunaSearchCV で sklearn の GridSearchCV と同じ使い方ができる。early_stopping_rounds との組み合わせも試すこと
  • 次回予告(Day 065): スタッキング(Stacking Ensemble)— ベースモデルのOOF予測をメタモデルで統合する

📋 自己評価(解いた後に記入)

✍️

理解度: [ ] 完全理解   [ ] おおむね理解   [ ] 要復習

自分の回答:

気づき・メモ: