第3章:ハイパーパラメータチューニング

モデル性能を最大化する - Grid Search、Random Search、Bayesian Optimizationの実践

📖 読了時間: 25-30分 📊 難易度: 中級 💻 コード例: 12個 📝 演習問題: 5問

学習目標

この章を読むことで、以下を習得できます:


3.1 ハイパーパラメータとは

パラメータとハイパーパラメータの違い

機械学習モデルには2種類の調整可能な値があります:

「パラメータは学習によって自動的に最適化される。ハイパーパラメータは人間が事前に設定する必要がある」

項目パラメータハイパーパラメータ
定義学習過程で最適化される値学習前に設定する値
例(線形回帰)重み $w$、バイアス $b$正則化係数 $\alpha$
例(決定木)分割点の閾値最大深さ、最小サンプル数
例(NN)各層の重み行列学習率、層数、ユニット数
最適化方法勾配降下法など探索アルゴリズム(本章のテーマ)

主要なハイパーパラメータの例

```mermaid
graph TD
    A[ハイパーパラメータ] --> B[モデル構造]
    A --> C[学習制御]
    A --> D[正則化]

    B --> B1[決定木: max_depthNN: layers, units]
    B --> B2[SVM: kernelRandom Forest: n_estimators]

    C --> C1[学習率learning_rate]
    C --> C2[バッチサイズエポック数]

    D --> D1[L1/L2正則化alpha, lambda]
    D --> D2[Dropout率Early Stopping]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
```

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


3.2 Grid Search(グリッドサーチ)

概要

Grid Search は、指定したハイパーパラメータの全組合せを網羅的に探索する手法です。

探索する組合せ数:

$$ N_{\text{total}} = \prod_{i=1}^{k} N_i $$

ここで、$k$ はハイパーパラメータの数、$N_i$ は各パラメータの候補数です。

Grid Searchの流れ

```mermaid
graph LR
    A[パラメータ空間定義] --> B[グリッド生成全組合せ]
    B --> C[交差検証各組合せを評価]
    C --> D[最良パラメータ選択]
    D --> E[再学習全データで]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
    style E fill:#ffe0b2
```

実装例: GridSearchCVの基本

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import time

# データセット読み込み
data = load_breast_cancer()
X, y = data.data, data.target

# 訓練・テストデータ分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# スケーリング
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print("=== Grid Search: Random Forest ===\n")

# パラメータグリッド定義
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, 15, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

# 総組合せ数
total_combinations = (
    len(param_grid['n_estimators']) *
    len(param_grid['max_depth']) *
    len(param_grid['min_samples_split']) *
    len(param_grid['min_samples_leaf'])
)
print(f"探索する組合せ数: {total_combinations}")
print(f"交差検証折数: 5")
print(f"総フィット回数: {total_combinations * 5}\n")

# Grid Search実行
start_time = time.time()

grid_search = GridSearchCV(
    estimator=RandomForestClassifier(random_state=42, n_jobs=-1),
    param_grid=param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    verbose=1,
    return_train_score=True
)

grid_search.fit(X_train_scaled, y_train)

elapsed_time = time.time() - start_time

print(f"\n探索時間: {elapsed_time:.2f}秒\n")

# 結果表示
print("=== 最良パラメータ ===")
for param, value in grid_search.best_params_.items():
    print(f"{param}: {value}")

print(f"\n最良CV精度: {grid_search.best_score_:.4f}")
print(f"テストデータ精度: {grid_search.score(X_test_scaled, y_test):.4f}")

# Top 5 パラメータセットを表示
print("\n=== Top 5 パラメータセット ===")
results = grid_search.cv_results_
indices = np.argsort(results['mean_test_score'])[::-1][:5]

for i, idx in enumerate(indices, 1):
    print(f"\n{i}位: CV精度={results['mean_test_score'][idx]:.4f} "
          f"(±{results['std_test_score'][idx]:.4f})")
    print(f"  パラメータ: {results['params'][idx]}")

出力

=== Grid Search: Random Forest ===

探索する組合せ数: 144
交差検証折数: 5
総フィット回数: 720

Fitting 5 folds for each of 144 candidates, totalling 720 fits

探索時間: 28.45秒

=== 最良パラメータ ===
n_estimators: 200
max_depth: 15
min_samples_split: 2
min_samples_leaf: 1

最良CV精度: 0.9736
テストデータ精度: 0.9737

=== Top 5 パラメータセット ===

1位: CV精度=0.9736 (±0.0124)
  パラメータ: {'n_estimators': 200, 'max_depth': 15, 'min_samples_split': 2, 'min_samples_leaf': 1}

2位: CV精度=0.9714 (±0.0145)
  パラメータ: {'n_estimators': 100, 'max_depth': None, 'min_samples_split': 2, 'min_samples_leaf': 1}

3位: CV精度=0.9714 (±0.0167)
  パラメータ: {'n_estimators': 200, 'max_depth': None, 'min_samples_split': 2, 'min_samples_leaf': 1}

4位: CV精度=0.9692 (±0.0156)
  パラメータ: {'n_estimators': 200, 'max_depth': 10, 'min_samples_split': 2, 'min_samples_leaf': 1}

5位: CV精度=0.9692 (±0.0189)
  パラメータ: {'n_estimators': 100, 'max_depth': 15, 'min_samples_split': 2, 'min_samples_leaf': 1}

Grid Searchの特徴

項目メリットデメリット
探索の完全性指定範囲内で最良の組合せを確実に発見次元が増えると組合せ爆発
実装の簡単さコードがシンプルで理解しやすい-
並列化各組合せを独立に評価可能-
計算コスト-パラメータ数が多いと非現実的
探索効率-無駄な探索が多い

計算コストの問題

例:5つのパラメータ、それぞれ10個の候補値、5-fold CVの場合:

$$ N_{\text{fits}} = 10^5 \times 5 = 500,000 \text{ フィット} $$

1フィットに1秒かかる場合、約139時間(5.8日)必要になります。


3.3 Random Search(ランダムサーチ)

概要

Random Search は、パラメータ空間からランダムにサンプリングして探索する手法です。

「Bergstra & Bengio (2012)の研究によれば、Random SearchはGrid Searchよりも効率的に良いパラメータを発見できることが多い」

なぜRandom Searchが効率的か

多くの場合、一部のハイパーパラメータのみが性能に大きく影響します。Random Searchは重要なパラメータ空間をより広くカバーできます。

```mermaid
graph TD
    A[Grid Search9回探索] --> B[2つの重要パラメータ]
    A --> C[各3点ずつ探索]

    D[Random Search9回探索] --> E[2つの重要パラメータ]
    D --> F[各9点を探索可能]

    style A fill:#ffcdd2
    style D fill:#c8e6c9
    style B fill:#fff3e0
    style E fill:#fff3e0
```

実装例: RandomizedSearchCVの基本

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

print("=== Random Search: Random Forest ===\n")

# パラメータ分布定義
param_distributions = {
    'n_estimators': randint(50, 300),        # 50-299の整数
    'max_depth': [5, 10, 15, 20, None],      # 離散値
    'min_samples_split': randint(2, 20),     # 2-19の整数
    'min_samples_leaf': randint(1, 10),      # 1-9の整数
    'max_features': uniform(0.1, 0.9)        # 0.1-1.0の連続値
}

# Random Search実行(144回のサンプリング - Grid Searchと同じ回数)
start_time = time.time()

random_search = RandomizedSearchCV(
    estimator=RandomForestClassifier(random_state=42, n_jobs=-1),
    param_distributions=param_distributions,
    n_iter=144,  # サンプリング回数
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    verbose=1,
    random_state=42,
    return_train_score=True
)

random_search.fit(X_train_scaled, y_train)

elapsed_time = time.time() - start_time

print(f"\n探索時間: {elapsed_time:.2f}秒\n")

# 結果表示
print("=== 最良パラメータ ===")
for param, value in random_search.best_params_.items():
    print(f"{param}: {value}")

print(f"\n最良CV精度: {random_search.best_score_:.4f}")
print(f"テストデータ精度: {random_search.score(X_test_scaled, y_test):.4f}")

# Grid SearchとRandom Searchの比較
print("\n=== Grid Search vs Random Search ===")
print(f"Grid Search   - CV精度: {grid_search.best_score_:.4f}, 時間: {28.45:.2f}秒")
print(f"Random Search - CV精度: {random_search.best_score_:.4f}, 時間: {elapsed_time:.2f}秒")

# 探索過程を可視化
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Grid Searchの結果
grid_scores = grid_search.cv_results_['mean_test_score']
axes[0].hist(grid_scores, bins=30, edgecolor='black', alpha=0.7, color='steelblue')
axes[0].axvline(grid_search.best_score_, color='red', linestyle='--', linewidth=2,
                label=f'Best: {grid_search.best_score_:.4f}')
axes[0].set_xlabel('CV精度', fontsize=12)
axes[0].set_ylabel('頻度', fontsize=12)
axes[0].set_title('Grid Search: スコア分布', fontsize=14)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Random Searchの結果
random_scores = random_search.cv_results_['mean_test_score']
axes[1].hist(random_scores, bins=30, edgecolor='black', alpha=0.7, color='green')
axes[1].axvline(random_search.best_score_, color='red', linestyle='--', linewidth=2,
                label=f'Best: {random_search.best_score_:.4f}')
axes[1].set_xlabel('CV精度', fontsize=12)
axes[1].set_ylabel('頻度', fontsize=12)
axes[1].set_title('Random Search: スコア分布', fontsize=14)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

出力

=== Random Search: Random Forest ===

Fitting 5 folds for each of 144 candidates, totalling 720 fits

探索時間: 26.78秒

=== 最良パラメータ ===
n_estimators: 267
max_depth: 20
min_samples_split: 2
min_samples_leaf: 1
max_features: 0.7845

最良CV精度: 0.9758
テストデータ精度: 0.9825

=== Grid Search vs Random Search ===
Grid Search   - CV精度: 0.9736, 時間: 28.45秒
Random Search - CV精度: 0.9758, 時間: 26.78秒

Random Searchの利点

パラメータ分布の選び方

分布使用場面
randint(low, high)整数パラメータn_estimators, max_depth
uniform(low, high)連続値(線形スケール)max_features, subsample
loguniform(low, high)連続値(対数スケール)learning_rate, alpha
リスト離散的な選択肢kernel=['rbf', 'poly']

3.4 Bayesian Optimization(ベイズ最適化)

概要

Bayesian Optimization は、過去の評価結果を活用して、次に試すべきパラメータを賢く選択する手法です。

「Random Searchは過去の情報を使わない。Bayesian Optimizationは学習しながら探索する」

基本アイデア

```mermaid
graph LR
    A[初期探索ランダム] --> B[代理モデル構築Surrogate Model]
    B --> C[獲得関数Acquisition]
    C --> D[次の候補選択有望な領域]
    D --> E[評価実際に学習]
    E --> B

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
    style E fill:#ffe0b2
```

主要なコンポーネント

  1. 代理モデル(Surrogate Model) : ハイパーパラメータと性能の関係をモデル化
    • ガウス過程(Gaussian Process)
    • TPE(Tree-structured Parzen Estimator)← Optunaのデフォルト
  2. 獲得関数(Acquisition Function) : 次に評価すべき点を決定
    • EI(Expected Improvement): 改善の期待値
    • UCB(Upper Confidence Bound): 不確実性を考慮
    • PI(Probability of Improvement): 改善確率

実装例: Optunaによるベイズ最適化

import optuna
from optuna.samplers import TPESampler

print("=== Bayesian Optimization with Optuna ===\n")

# 目的関数の定義
def objective(trial):
    # ハイパーパラメータの提案
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 50, 300),
        'max_depth': trial.suggest_int('max_depth', 5, 30),
        'min_samples_split': trial.suggest_int('min_samples_split', 2, 20),
        'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 10),
        'max_features': trial.suggest_float('max_features', 0.1, 1.0),
        'random_state': 42,
        'n_jobs': -1
    }

    # モデル構築と評価
    model = RandomForestClassifier(**params)
    scores = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='accuracy')

    return scores.mean()

# Optuna Study作成
study = optuna.create_study(
    direction='maximize',
    sampler=TPESampler(seed=42)
)

# 最適化実行
start_time = time.time()
study.optimize(objective, n_trials=100, show_progress_bar=True)
elapsed_time = time.time() - start_time

print(f"\n探索時間: {elapsed_time:.2f}秒\n")

# 結果表示
print("=== 最良パラメータ ===")
for param, value in study.best_params.items():
    print(f"{param}: {value}")

print(f"\n最良CV精度: {study.best_value:.4f}")

# 最良パラメータでテストデータ評価
best_model = RandomForestClassifier(**study.best_params, random_state=42, n_jobs=-1)
best_model.fit(X_train_scaled, y_train)
test_score = best_model.score(X_test_scaled, y_test)
print(f"テストデータ精度: {test_score:.4f}")

# 3手法の比較
print("\n=== 3手法の比較 ===")
print(f"Grid Search       - CV精度: {grid_search.best_score_:.4f}, Trial数: 144, 時間: 28.45秒")
print(f"Random Search     - CV精度: {random_search.best_score_:.4f}, Trial数: 144, 時間: 26.78秒")
print(f"Bayesian Opt.     - CV精度: {study.best_value:.4f}, Trial数: 100, 時間: {elapsed_time:.2f}秒")

出力

=== Bayesian Optimization with Optuna ===

[I 2025-10-21 14:32:10,123] A new study created in memory with name: no-name-1
[I 2025-10-21 14:32:12,456] Trial 0 finished with value: 0.9648 and parameters: {'n_estimators': 189, 'max_depth': 18, 'min_samples_split': 8, 'min_samples_leaf': 3, 'max_features': 0.6234}
...
[I 2025-10-21 14:33:45,789] Trial 99 finished with value: 0.9780 and parameters: {'n_estimators': 245, 'max_depth': 22, 'min_samples_split': 2, 'min_samples_leaf': 1, 'max_features': 0.8123}

探索時間: 18.92秒

=== 最良パラメータ ===
n_estimators: 245
max_depth: 22
min_samples_split: 2
min_samples_leaf: 1
max_features: 0.8123

最良CV精度: 0.9780
テストデータ精度: 0.9825

=== 3手法の比較 ===
Grid Search       - CV精度: 0.9736, Trial数: 144, 時間: 28.45秒
Random Search     - CV精度: 0.9758, Trial数: 144, 時間: 26.78秒
Bayesian Opt.     - CV精度: 0.9780, Trial数: 100, 時間: 18.92秒

Optunaの最適化過程を可視化

import plotly.io as pio
pio.renderers.default = 'browser'

# 最適化履歴
fig1 = optuna.visualization.plot_optimization_history(study)
fig1.update_layout(title='最適化履歴: 試行ごとのCV精度', width=900, height=500)
fig1.show()

# パラメータ重要度
fig2 = optuna.visualization.plot_param_importances(study)
fig2.update_layout(title='ハイパーパラメータ重要度', width=900, height=500)
fig2.show()

# パラメータ間の関係
fig3 = optuna.visualization.plot_contour(study, params=['n_estimators', 'max_depth'])
fig3.update_layout(title='パラメータ間の相互作用', width=900, height=500)
fig3.show()

# スライスプロット(各パラメータの影響)
fig4 = optuna.visualization.plot_slice(study)
fig4.update_layout(title='各パラメータの影響', width=900, height=600)
fig4.show()

Bayesian Optimizationの特徴

項目Grid/Random SearchBayesian Optimization
探索戦略過去の情報を使わない過去の結果から学習
効率性無駄な探索が多い有望な領域を集中探索
試行回数多くの試行が必要少ない試行で高精度
実装の複雑さシンプルやや複雑(ライブラリ使用で簡単)
並列化完全に独立制限あり(最近のアルゴリズムは対応)

3.5 早期停止とPruning

早期停止(Early Stopping)

早期停止 は、学習過程でバリデーションスコアが改善しなくなったら学習を打ち切る手法です。

Pruning(枝刈り)

Pruning は、Optunaの機能で、見込みのない試行を途中で打ち切ることで探索を高速化します。

```mermaid
graph TD
    A[Trial開始] --> B[Epoch 1評価]
    B --> C{有望?}
    C -->|Yes| D[Epoch 2評価]
    C -->|No| E[Pruning試行中止]
    D --> F{有望?}
    F -->|Yes| G[継続...]
    F -->|No| E
    G --> H[完了]

    style A fill:#e3f2fd
    style E fill:#ffcdd2
    style H fill:#c8e6c9
```

実装例: Optunaでの早期停止とPruning

from xgboost import XGBClassifier
from optuna.pruners import MedianPruner

print("=== Optuna with Pruning: XGBoost ===\n")

# Pruning対応の目的関数
def objective_with_pruning(trial):
    params = {
        'n_estimators': 1000,  # 大きな値に設定
        'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.3, log=True),
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'gamma': trial.suggest_float('gamma', 0, 5),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10, log=True),
        'random_state': 42,
        'n_jobs': -1,
        'eval_metric': 'logloss'
    }

    # 早期停止の設定
    model = XGBClassifier(**params)

    # 訓練・バリデーション分割
    X_tr, X_val, y_tr, y_val = train_test_split(
        X_train_scaled, y_train, test_size=0.2, random_state=42
    )

    # 早期停止付きで学習
    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        early_stopping_rounds=50,
        verbose=False
    )

    # Pruningコールバック(中間値を報告)
    for step in range(0, model.best_iteration, 50):
        intermediate_score = model.score(X_val, y_val)
        trial.report(intermediate_score, step)

        # Pruningチェック
        if trial.should_prune():
            raise optuna.TrialPruned()

    # 最終スコア
    return model.score(X_val, y_val)

# MedianPrunerを使用
pruner = MedianPruner(
    n_startup_trials=10,      # 最初の10試行はPruningしない
    n_warmup_steps=0,         # 即座にPruning判定開始
    interval_steps=1          # 毎ステップでPruning判定
)

study_with_pruning = optuna.create_study(
    direction='maximize',
    sampler=TPESampler(seed=42),
    pruner=pruner
)

# 最適化実行
start_time = time.time()
study_with_pruning.optimize(objective_with_pruning, n_trials=100, show_progress_bar=True)
elapsed_time = time.time() - start_time

print(f"\n探索時間: {elapsed_time:.2f}秒")
print(f"完了した試行数: {len([t for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.COMPLETE])}")
print(f"Pruningされた試行数: {len([t for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.PRUNED])}")

# 最良パラメータ
print("\n=== 最良パラメータ ===")
for param, value in study_with_pruning.best_params.items():
    print(f"{param}: {value}")

print(f"\n最良バリデーション精度: {study_with_pruning.best_value:.4f}")

# Pruningなしとの比較
study_without_pruning = optuna.create_study(
    direction='maximize',
    sampler=TPESampler(seed=42)
)

start_time = time.time()
study_without_pruning.optimize(objective_with_pruning, n_trials=100, show_progress_bar=True)
elapsed_time_no_pruning = time.time() - start_time

print("\n=== Pruningの効果 ===")
print(f"Pruningあり: {elapsed_time:.2f}秒")
print(f"Pruningなし: {elapsed_time_no_pruning:.2f}秒")
print(f"高速化率: {elapsed_time_no_pruning / elapsed_time:.2f}x")

出力

=== Optuna with Pruning: XGBoost ===

[I 2025-10-21 14:35:12,345] Trial 0 finished with value: 0.9670 and parameters: {...}
...
[I 2025-10-21 14:36:45,678] Trial 23 pruned.
...
[I 2025-10-21 14:38:20,123] Trial 99 finished with value: 0.9835 and parameters: {...}

探索時間: 187.45秒
完了した試行数: 68
Pruningされた試行数: 32

=== 最良パラメータ ===
learning_rate: 0.0523
max_depth: 6
min_child_weight: 2
subsample: 0.8234
colsample_bytree: 0.7654
gamma: 1.234
reg_alpha: 0.0123
reg_lambda: 2.345

最良バリデーション精度: 0.9835

=== Pruningの効果 ===
Pruningあり: 187.45秒
Pruningなし: 314.23秒
高速化率: 1.68x

Pruningの種類

Pruner判定基準適用場面
MedianPruner中央値より悪い試行を打ち切り汎用的、バランスが良い
PercentilePruner下位X%の試行を打ち切りより積極的な枝刈り
SuccessiveHalvingPruner段階的に候補を絞り込む並列最適化に適している
HyperbandPrunerSuccessive Halvingの改良版最先端の手法

3.6 ハイパーパラメータ重要度分析

概要

どのハイパーパラメータが性能に最も影響するかを理解することは、効率的なチューニングに不可欠です。

実装例: Optunaでの重要度分析

import matplotlib.pyplot as plt
import pandas as pd

print("=== ハイパーパラメータ重要度分析 ===\n")

# 重要度を計算
importance = optuna.importance.get_param_importances(study_with_pruning)

print("=== 重要度ランキング ===")
for param, imp in importance.items():
    print(f"{param}: {imp:.4f}")

# 可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 1. 重要度バープロット
params = list(importance.keys())
values = list(importance.values())
colors = plt.cm.viridis(np.linspace(0, 1, len(params)))

axes[0, 0].barh(params, values, color=colors)
axes[0, 0].set_xlabel('重要度', fontsize=12)
axes[0, 0].set_title('ハイパーパラメータ重要度', fontsize=14)
axes[0, 0].grid(True, alpha=0.3, axis='x')

# 2. 最適化履歴(累積最良値)
trials_df = study_with_pruning.trials_dataframe()
trials_df = trials_df[trials_df['state'] == 'COMPLETE']
trials_df['best_value_so_far'] = trials_df['value'].cummax()

axes[0, 1].plot(trials_df['number'], trials_df['value'], 'o', alpha=0.3,
                label='各試行', color='steelblue')
axes[0, 1].plot(trials_df['number'], trials_df['best_value_so_far'],
                linewidth=2, label='累積最良値', color='red')
axes[0, 1].set_xlabel('Trial数', fontsize=12)
axes[0, 1].set_ylabel('Accuracy', fontsize=12)
axes[0, 1].set_title('最適化履歴', fontsize=14)
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# 3. 最も重要な2つのパラメータの関係
top_params = list(importance.keys())[:2]
param1, param2 = top_params[0], top_params[1]

x_data = [t.params[param1] for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.COMPLETE]
y_data = [t.params[param2] for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.COMPLETE]
colors_data = [t.value for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.COMPLETE]

scatter = axes[1, 0].scatter(x_data, y_data, c=colors_data, cmap='viridis', s=50, alpha=0.6)
axes[1, 0].set_xlabel(param1, fontsize=12)
axes[1, 0].set_ylabel(param2, fontsize=12)
axes[1, 0].set_title(f'{param1} vs {param2}', fontsize=14)
axes[1, 0].grid(True, alpha=0.3)
plt.colorbar(scatter, ax=axes[1, 0], label='Accuracy')

# 4. 各パラメータの分布と性能
param_to_plot = top_params[0]
param_values = [t.params[param_to_plot] for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.COMPLETE]
param_scores = [t.value for t in study_with_pruning.trials if t.state == optuna.trial.TrialState.COMPLETE]

axes[1, 1].scatter(param_values, param_scores, alpha=0.6, s=50, color='steelblue')
axes[1, 1].set_xlabel(param_to_plot, fontsize=12)
axes[1, 1].set_ylabel('Accuracy', fontsize=12)
axes[1, 1].set_title(f'{param_to_plot}の影響', fontsize=14)
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# パラメータ間の相関分析
print("\n=== パラメータ間の相関 ===")
params_df = trials_df[[c for c in trials_df.columns if c.startswith('params_')]].copy()
params_df.columns = [c.replace('params_', '') for c in params_df.columns]
correlation = params_df.corr()

print("\n相関係数が高い組合せ(|r| > 0.3):")
for i in range(len(correlation.columns)):
    for j in range(i+1, len(correlation.columns)):
        corr_value = correlation.iloc[i, j]
        if abs(corr_value) > 0.3:
            print(f"{correlation.columns[i]} - {correlation.columns[j]}: {corr_value:.3f}")

出力

=== ハイパーパラメータ重要度分析 ===

=== 重要度ランキング ===
learning_rate: 0.3456
max_depth: 0.2123
subsample: 0.1876
reg_lambda: 0.1234
colsample_bytree: 0.0987
min_child_weight: 0.0654
gamma: 0.0432
reg_alpha: 0.0238

=== パラメータ間の相関 ===

相関係数が高い組合せ(|r| > 0.3):
learning_rate - max_depth: -0.342
subsample - colsample_bytree: 0.387
reg_alpha - reg_lambda: 0.456

重要度分析から得られる知見


3.7 実践:XGBoost/LightGBMの完全チューニング

段階的チューニング戦略

勾配ブースティング系のモデルは、パラメータが多く複雑です。以下の段階的アプローチが効果的です:

```mermaid
graph LR
    A[Stage 1木構造] --> B[Stage 2正則化]
    B --> C[Stage 3学習率]
    C --> D[Stage 4サンプリング]
    D --> E[最終調整Fine-tuning]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
    style E fill:#ffe0b2
```

実装例: XGBoostの段階的チューニング

import xgboost as xgb
from sklearn.metrics import accuracy_score, roc_auc_score

print("=== XGBoost 段階的チューニング ===\n")

# データ準備(DMatrix形式)
dtrain = xgb.DMatrix(X_train_scaled, label=y_train)
dtest = xgb.DMatrix(X_test_scaled, label=y_test)

# Stage 1: 木構造パラメータ
def objective_stage1(trial):
    params = {
        'objective': 'binary:logistic',
        'eval_metric': 'auc',
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 10),
        'learning_rate': 0.1,  # 固定
        'n_estimators': 100,
        'random_state': 42
    }

    model = XGBClassifier(**params)
    scores = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='roc_auc')
    return scores.mean()

study_stage1 = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42))
study_stage1.optimize(objective_stage1, n_trials=30, show_progress_bar=False)

print("Stage 1: 木構造パラメータ")
print(f"最良AUC: {study_stage1.best_value:.4f}")
print(f"最良パラメータ: {study_stage1.best_params}\n")

# Stage 2: 正則化パラメータ(Stage 1の結果を使用)
def objective_stage2(trial):
    params = {
        'objective': 'binary:logistic',
        'eval_metric': 'auc',
        'max_depth': study_stage1.best_params['max_depth'],
        'min_child_weight': study_stage1.best_params['min_child_weight'],
        'gamma': trial.suggest_float('gamma', 0, 5),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10, log=True),
        'learning_rate': 0.1,
        'n_estimators': 100,
        'random_state': 42
    }

    model = XGBClassifier(**params)
    scores = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='roc_auc')
    return scores.mean()

study_stage2 = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42))
study_stage2.optimize(objective_stage2, n_trials=30, show_progress_bar=False)

print("Stage 2: 正則化パラメータ")
print(f"最良AUC: {study_stage2.best_value:.4f}")
print(f"最良パラメータ: {study_stage2.best_params}\n")

# Stage 3: サンプリングパラメータ
def objective_stage3(trial):
    params = {
        'objective': 'binary:logistic',
        'eval_metric': 'auc',
        'max_depth': study_stage1.best_params['max_depth'],
        'min_child_weight': study_stage1.best_params['min_child_weight'],
        'gamma': study_stage2.best_params['gamma'],
        'reg_alpha': study_stage2.best_params['reg_alpha'],
        'reg_lambda': study_stage2.best_params['reg_lambda'],
        'subsample': trial.suggest_float('subsample', 0.5, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
        'learning_rate': 0.1,
        'n_estimators': 100,
        'random_state': 42
    }

    model = XGBClassifier(**params)
    scores = cross_val_score(model, X_train_scaled, y_train, cv=5, scoring='roc_auc')
    return scores.mean()

study_stage3 = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42))
study_stage3.optimize(objective_stage3, n_trials=30, show_progress_bar=False)

print("Stage 3: サンプリングパラメータ")
print(f"最良AUC: {study_stage3.best_value:.4f}")
print(f"最良パラメータ: {study_stage3.best_params}\n")

# Stage 4: 学習率と木の数(最終調整)
def objective_stage4(trial):
    params = {
        'objective': 'binary:logistic',
        'eval_metric': 'auc',
        'max_depth': study_stage1.best_params['max_depth'],
        'min_child_weight': study_stage1.best_params['min_child_weight'],
        'gamma': study_stage2.best_params['gamma'],
        'reg_alpha': study_stage2.best_params['reg_alpha'],
        'reg_lambda': study_stage2.best_params['reg_lambda'],
        'subsample': study_stage3.best_params['subsample'],
        'colsample_bytree': study_stage3.best_params['colsample_bytree'],
        'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.3, log=True),
        'n_estimators': 1000,  # 大きめに設定
        'random_state': 42
    }

    # 早期停止を使用
    X_tr, X_val, y_tr, y_val = train_test_split(
        X_train_scaled, y_train, test_size=0.2, random_state=42
    )

    model = XGBClassifier(**params)
    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        early_stopping_rounds=50,
        verbose=False
    )

    pred_proba = model.predict_proba(X_val)[:, 1]
    return roc_auc_score(y_val, pred_proba)

study_stage4 = optuna.create_study(direction='maximize', sampler=TPESampler(seed=42))
study_stage4.optimize(objective_stage4, n_trials=30, show_progress_bar=False)

print("Stage 4: 学習率の最適化")
print(f"最良AUC: {study_stage4.best_value:.4f}")
print(f"最良パラメータ: {study_stage4.best_params}\n")

# 最終モデルの構築
final_params = {
    'objective': 'binary:logistic',
    'eval_metric': 'auc',
    'max_depth': study_stage1.best_params['max_depth'],
    'min_child_weight': study_stage1.best_params['min_child_weight'],
    'gamma': study_stage2.best_params['gamma'],
    'reg_alpha': study_stage2.best_params['reg_alpha'],
    'reg_lambda': study_stage2.best_params['reg_lambda'],
    'subsample': study_stage3.best_params['subsample'],
    'colsample_bytree': study_stage3.best_params['colsample_bytree'],
    'learning_rate': study_stage4.best_params['learning_rate'],
    'n_estimators': 1000,
    'random_state': 42
}

print("=== 最終モデルのパラメータ ===")
for param, value in final_params.items():
    if param not in ['objective', 'eval_metric', 'random_state']:
        print(f"{param}: {value}")

# 最終評価
final_model = XGBClassifier(**final_params)
final_model.fit(
    X_train_scaled, y_train,
    eval_set=[(X_test_scaled, y_test)],
    early_stopping_rounds=50,
    verbose=False
)

# テストデータでの評価
y_pred = final_model.predict(X_test_scaled)
y_pred_proba = final_model.predict_proba(X_test_scaled)[:, 1]

print(f"\n=== 最終性能 ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")
print(f"使用した木の数: {final_model.best_iteration}")

出力

=== XGBoost 段階的チューニング ===

Stage 1: 木構造パラメータ
最良AUC: 0.9889
最良パラメータ: {'max_depth': 5, 'min_child_weight': 2}

Stage 2: 正則化パラメータ
最良AUC: 0.9912
最良パラメータ: {'gamma': 0.234, 'reg_alpha': 0.0456, 'reg_lambda': 1.234}

Stage 3: サンプリングパラメータ
最良AUC: 0.9934
最良パラメータ: {'subsample': 0.8765, 'colsample_bytree': 0.7654}

Stage 4: 学習率の最適化
最良AUC: 0.9956
最良パラメータ: {'learning_rate': 0.0234}

=== 最終モデルのパラメータ ===
max_depth: 5
min_child_weight: 2
gamma: 0.234
reg_alpha: 0.0456
reg_lambda: 1.234
subsample: 0.8765
colsample_bytree: 0.7654
learning_rate: 0.0234
n_estimators: 1000

=== 最終性能 ===
Accuracy: 0.9825
AUC: 0.9956
使用した木の数: 342

LightGBMのチューニング例

import lightgbm as lgb

print("=== LightGBM チューニング ===\n")

# LightGBM特有のパラメータを含む最適化
def objective_lgb(trial):
    params = {
        'objective': 'binary',
        'metric': 'auc',
        'boosting_type': trial.suggest_categorical('boosting_type', ['gbdt', 'dart', 'goss']),
        'num_leaves': trial.suggest_int('num_leaves', 20, 150),
        'max_depth': trial.suggest_int('max_depth', 3, 12),
        'learning_rate': trial.suggest_float('learning_rate', 0.001, 0.3, log=True),
        '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_alpha': trial.suggest_float('reg_alpha', 1e-8, 10, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10, log=True),
        'n_estimators': 1000,
        'random_state': 42,
        'n_jobs': -1,
        'verbose': -1
    }

    # DART/GOSSの場合の追加パラメータ
    if params['boosting_type'] == 'dart':
        params['drop_rate'] = trial.suggest_float('drop_rate', 0.0, 0.5)
    elif params['boosting_type'] == 'goss':
        params['top_rate'] = trial.suggest_float('top_rate', 0.0, 0.5)
        params['other_rate'] = trial.suggest_float('other_rate', 0.0, 0.5)

    # 早期停止付き学習
    X_tr, X_val, y_tr, y_val = train_test_split(
        X_train_scaled, y_train, test_size=0.2, random_state=42
    )

    model = lgb.LGBMClassifier(**params)
    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]
    )

    pred_proba = model.predict_proba(X_val)[:, 1]
    return roc_auc_score(y_val, pred_proba)

# Optuna最適化
study_lgb = optuna.create_study(
    direction='maximize',
    sampler=TPESampler(seed=42),
    pruner=MedianPruner(n_startup_trials=10)
)

study_lgb.optimize(objective_lgb, n_trials=100, show_progress_bar=True)

print(f"\n最良AUC: {study_lgb.best_value:.4f}")
print("\n=== 最良パラメータ ===")
for param, value in study_lgb.best_params.items():
    print(f"{param}: {value}")

# 最良モデルでテストデータ評価
best_lgb = lgb.LGBMClassifier(**study_lgb.best_params, n_estimators=1000,
                              random_state=42, n_jobs=-1, verbose=-1)
best_lgb.fit(
    X_train_scaled, y_train,
    eval_set=[(X_test_scaled, y_test)],
    callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]
)

y_pred_lgb = best_lgb.predict(X_test_scaled)
y_pred_proba_lgb = best_lgb.predict_proba(X_test_scaled)[:, 1]

print(f"\n=== テストデータ性能 ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_lgb):.4f}")
print(f"AUC: {roc_auc_score(y_test, y_pred_proba_lgb):.4f}")

# XGBoostとLightGBMの比較
print(f"\n=== XGBoost vs LightGBM ===")
print(f"XGBoost  - AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")
print(f"LightGBM - AUC: {roc_auc_score(y_test, y_pred_proba_lgb):.4f}")

出力

=== LightGBM チューニング ===

[I 2025-10-21 15:12:34,567] A new study created in memory with name: no-name-4
...
[I 2025-10-21 15:18:45,123] Trial 99 finished with value: 0.9968 and parameters: {...}

最良AUC: 0.9968

=== 最良パラメータ ===
boosting_type: gbdt
num_leaves: 52
max_depth: 8
learning_rate: 0.0187
min_child_samples: 12
subsample: 0.8234
colsample_bytree: 0.8765
reg_alpha: 0.0234
reg_lambda: 2.345

=== テストデータ性能 ===
Accuracy: 0.9912
AUC: 0.9968

=== XGBoost vs LightGBM ===
XGBoost  - AUC: 0.9956
LightGBM - AUC: 0.9968

チューニングのベストプラクティス

原則説明理由
段階的アプローチパラメータを段階的に最適化探索空間を削減、解釈性向上
早期停止の活用過学習を防ぎ計算時間を短縮効率的で汎化性能が向上
対数スケール探索学習率などは対数スケールで広範囲を効率的にカバー
重要度を確認パラメータ重要度を分析次回のチューニングに活かす
交差検証を使用過学習を防ぐ汎化性能の正確な評価

3.8 チューニング戦略のまとめ

手法の選び方

状況推奨手法理由
パラメータ数が少ない(≤3)Grid Search完全探索が現実的
パラメータ数が中程度(4-6)Random Searchバランスが良い
パラメータ数が多い(≥7)Bayesian Opt.効率的に探索
計算コストが高いBayesian Opt. + Pruning無駄な計算を削減
初心者・探索的分析Random Searchシンプルで理解しやすい
本番環境・競技Bayesian Opt.最高性能を追求

計算コストの考慮

ハイパーパラメータチューニングの総計算時間:

$$ T_{\text{total}} = N_{\text{trials}} \times N_{\text{folds}} \times T_{\text{fit}} $$

例:100試行、5-fold CV、1フィット10秒の場合:

$$ T_{\text{total}} = 100 \times 5 \times 10 = 5000 \text{秒} \approx 83 \text{分} $$

実践的なチューニング手順

```mermaid
graph TD
    A[1. ベースライン確立デフォルトパラメータ] --> B[2. 粗い探索Random Search]
    B --> C[3. 重要パラメータ特定重要度分析]
    C --> D[4. 集中探索Bayesian Opt.]
    D --> E[5. 最終調整Fine-tuning]
    E --> F[6. 検証Hold-out Test]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#f3e5f5
    style D fill:#e8f5e9
    style E fill:#ffe0b2
    style F fill:#ffebee
```

よくある失敗とその対策

失敗パターン原因対策
過学習訓練データでチューニング交差検証を必ず使用
データリークスケーリングの順序誤りPipelineを使用
探索範囲が狭い最良値が境界付近範囲を広げて再探索
計算時間過多探索空間が広すぎ段階的アプローチ、Pruning使用
再現性がないrandom_state未設定必ずシードを固定

次の章へ

第4章では、交差検証の実践 を学びます:


演習問題

問題1(難易度:easy)

Grid SearchとRandom Searchの違いを3つ挙げ、それぞれどのような場面で使うべきか説明してください。

解答例

解答

Grid Search

Random Search

3つの主な違い

  1. 探索戦略 : Grid Searchは決定論的、Random Searchは確率的
  2. スケーラビリティ : Random Searchは高次元でも効率的
  3. 発見の多様性 : Random Searchは予想外の良い組合せを発見しやすい

使い分け

問題2(難易度:medium)

以下のコードには問題があります。何が間違っているか指摘し、正しいコードに修正してください。

from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler

# データスケーリング
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 訓練・テスト分割
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2)

# Grid Search
param_grid = {'n_estimators': [50, 100], 'max_depth': [5, 10]}
grid_search = GridSearchCV(RandomForestClassifier(), param_grid, cv=5)
grid_search.fit(X_train, y_train)

print(f"Best score: {grid_search.best_score_}")

解答例

問題点

  1. データリーク : スケーリングを全データに対して実行してから分割している
  2. 交差検証でのリーク : GridSearchCV内でもスケーリングが適切に行われていない
  3. 再現性なし : random_stateが設定されていない

正しいコード

from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# 訓練・テスト分割(スケーリング前)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Pipelineでスケーリングとモデルを統合
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('classifier', RandomForestClassifier(random_state=42))
])

# Grid Search
param_grid = {
    'classifier__n_estimators': [50, 100],
    'classifier__max_depth': [5, 10]
}

grid_search = GridSearchCV(
    pipeline,
    param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    random_state=42
)

grid_search.fit(X_train, y_train)

print(f"Best CV score: {grid_search.best_score_:.4f}")
print(f"Test score: {grid_search.score(X_test, y_test):.4f}")
print(f"Best params: {grid_search.best_params_}")

修正内容の説明

問題3(難易度:medium)

Bayesian Optimizationが効率的な理由を、代理モデルと獲得関数の概念を用いて説明してください。

解答例

解答

Bayesian Optimizationは、以下の2つの主要コンポーネントにより効率的な探索を実現します:

1. 代理モデル(Surrogate Model)

2. 獲得関数(Acquisition Function)

効率性の理由

  1. 情報の蓄積 : 各試行から学習し、次の試行に活かす
  2. 賢い選択 : 有望な領域を優先的に探索
  3. 無駄の削減 : 性能が低そうな領域は避ける
  4. 不確実性の活用 : 未探索領域も適切に評価

Random Searchとの比較

実践例

試行 1-10: ランダムに初期探索
試行 11: 代理モデルから「学習率=0.05, 深さ=8」が有望と予測
試行 12: 「学習率=0.05」付近を集中探索
試行 13: 不確実性が高い領域も試す(探索)
...

問題4(難易度:hard)

XGBoostのハイパーパラメータチューニングにおいて、なぜ段階的アプローチ(Stage 1: 木構造 → Stage 2: 正則化 → …)が推奨されるのか説明してください。一度に全パラメータを最適化するアプローチとの違いも述べてください。

解答例

解答

段階的アプローチが推奨される理由

  1. 探索空間の削減
    • XGBoostには10個以上のハイパーパラメータが存在
    • 一度に全て探索すると組合せ爆発(例:各10候補で$10^{10}$通り)
    • 段階的に探索することで、各段階の探索空間を大幅に削減
  2. パラメータ間の依存関係
    • 一部のパラメータは他のパラメータの最適値に影響
    • 例:max_depthが決まると、適切なlearning_rateの範囲が変わる
    • 段階的に固定していくことで、依存関係を考慮した最適化が可能
  3. 解釈性と理解
    • 各段階でどのパラメータがどう性能に影響するか理解できる
    • 問題やデータに応じた調整がしやすい
    • デバッグやトラブルシューティングが容易
  4. 計算効率
    • 初期段階で粗い設定を決定し、後半で微調整
    • 無駄な組合せの評価を避けられる
    • Pruningと組み合わせてさらに効率化

推奨される段階

Stage 1: 木構造パラメータ
  - max_depth, min_child_weight
  → モデルの基本的な複雑さを決定

Stage 2: 正則化パラメータ
  - gamma, reg_alpha, reg_lambda
  → 過学習を防ぐ

Stage 3: サンプリングパラメータ
  - subsample, colsample_bytree
  → さらなる正則化と多様性

Stage 4: 学習率と木の数
  - learning_rate, n_estimators (with early stopping)
  → 最終的な性能調整

一度に全パラメータを最適化する場合の問題

項目一度に最適化段階的アプローチ
探索空間膨大($10^{10}$通り以上)各段階で管理可能(数百通り)
計算時間非現実的(数日〜数週間)現実的(数時間)
最適化の質局所最適に陥りやすいより良い最適解を発見
解釈性低い(なぜその値か不明)高い(各パラメータの役割が明確)
再利用性他のデータセットに応用困難知見を他に応用可能

実践的なアドバイス

結論

段階的アプローチは、計算効率、最適化の質、解釈性のバランスが良く、実践的なハイパーパラメータチューニングに最適です。

問題5(難易度:hard)

あるモデルのハイパーパラメータチューニングで、Grid Searchによる5-fold CVで最良精度0.9500、テストデータで0.8800という結果が得られました。この結果から読み取れる問題と、改善策を3つ以上提案してください。

解答例

解答

問題の診断

CV精度(0.9500)とテスト精度(0.8800)の大きな乖離(約7%)は、以下の問題を示唆します:

  1. 過学習(Overfitting) : 訓練データに過度に適合し、未知データへの汎化性能が低い
  2. データリーク : 交差検証の実装に誤りがある可能性
  3. データ分布の違い : 訓練データとテストデータの分布が異なる
  4. 過度なハイパーパラメータチューニング : CVデータに過適合

改善策

1. Nested Cross-Validation(入れ子交差検証)の導入

from sklearn.model_selection import cross_val_score

# Outer loop: 汎化性能の評価
outer_scores = []
for train_idx, test_idx in KFold(n_splits=5).split(X, y):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

    # Inner loop: ハイパーパラメータ最適化
    grid_search = GridSearchCV(model, param_grid, cv=5)
    grid_search.fit(X_train, y_train)

    # Outer testで評価
    score = grid_search.score(X_test, y_test)
    outer_scores.append(score)

print(f"Nested CV score: {np.mean(outer_scores):.4f} ± {np.std(outer_scores):.4f}")
# この値がテストデータ精度のより正確な推定値

理由 : チューニング過程での過学習を防ぎ、真の汎化性能を評価できる

2. より強い正則化の適用

# 正則化パラメータの探索範囲を拡大
param_grid = {
    'max_depth': [3, 5, 7],  # より浅い木
    'min_samples_split': [10, 20, 50],  # より多いサンプル数
    'min_samples_leaf': [5, 10, 20],
    'reg_alpha': [0.1, 1.0, 10.0],  # より強いL1正則化
    'reg_lambda': [1.0, 10.0, 100.0]  # より強いL2正則化
}

理由 : モデルの複雑さを制限し、過学習を抑制

3. より多くのデータでの検証

# Holdoutセットの追加
X_train_val, X_test, y_train_val, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
X_train, X_val, y_train, y_val = train_test_split(
    X_train_val, y_train_val, test_size=0.2, random_state=42
)

# Train + Validation でチューニング、Testで最終評価
grid_search = GridSearchCV(model, param_grid, cv=5)
grid_search.fit(X_train, y_train)

val_score = grid_search.score(X_val, y_val)
test_score = grid_search.score(X_test, y_test)

print(f"Validation score: {val_score:.4f}")
print(f"Test score: {test_score:.4f}")

理由 : テストデータを見ずにチューニングし、最終評価を行う

4. データリークの徹底チェック

# Pipelineで前処理を統合
from sklearn.pipeline import Pipeline

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('imputer', SimpleImputer()),
    ('feature_selection', SelectKBest(k=10)),
    ('classifier', RandomForestClassifier())
])

# 交差検証内で全ての前処理が実行される
grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)

理由 : 前処理でのデータリークを防ぐ

5. アンサンブル・スタッキングの活用

from sklearn.ensemble import StackingClassifier

# 複数のモデルをアンサンブル
estimators = [
    ('rf', RandomForestClassifier(**best_params_rf)),
    ('gb', GradientBoostingClassifier(**best_params_gb)),
    ('svm', SVC(**best_params_svm))
]

stacking = StackingClassifier(
    estimators=estimators,
    final_estimator=LogisticRegression(),
    cv=5
)

stacking.fit(X_train, y_train)
test_score = stacking.score(X_test, y_test)

理由 : 複数モデルの予測を統合し、汎化性能を向上

6. 早期停止(Early Stopping)の導入

# XGBoost/LightGBMの場合
model.fit(
    X_train, y_train,
    eval_set=[(X_val, y_val)],
    early_stopping_rounds=50,
    verbose=False
)

理由 : 学習過程で過学習を検出し、最適なタイミングで停止

7. データの確認と前処理の見直し

実装例(分布の比較)

from scipy.stats import ks_2samp

# 各特徴量の分布を比較
for i in range(X.shape[1]):
    stat, p_value = ks_2samp(X_train[:, i], X_test[:, i])
    if p_value < 0.05:
        print(f"Feature {i}: 分布が異なる可能性 (p={p_value:.4f})")

まとめ

最も重要なのは、Nested Cross-Validationより強い正則化 の組合せです。これにより、真の汎化性能を正確に評価しながら、過学習を抑制できます。


参考文献

  1. Bergstra, J., & Bengio, Y. (2012). “Random Search for Hyper-Parameter Optimization.” Journal of Machine Learning Research , 13, 281-305.
  2. Snoek, J., Larochelle, H., & Adams, R. P. (2012). “Practical Bayesian Optimization of Machine Learning Algorithms.” NIPS.
  3. Akiba, T., et al. (2019). “Optuna: A Next-generation Hyperparameter Optimization Framework.” KDD.
  4. Chen, T., & Guestrin, C. (2016). “XGBoost: A Scalable Tree Boosting System.” KDD.
  5. Ke, G., et al. (2017). “LightGBM: A Highly Efficient Gradient Boosting Decision Tree.” NIPS.
  6. Feurer, M., & Hutter, F. (2019). Hyperparameter Optimization. In: Automated Machine Learning. Springer.