LESSON

モデルチューニング

「特徴量エンジニアリングで底力が上がった。最後の仕上げはチューニングだ。」

田中VPoEがOptunaのドキュメントを開く。

「グリッドサーチは古い。Optunaでベイズ最適化を使えば、効率的に最適なハイパーパラメータを見つけられる。ただし、過学習には気をつけろ。」

ハイパーパラメータチューニングの戦略

チューニングの手法:
1. Grid Search    → 全組み合わせを試す(小規模向け)
2. Random Search  → ランダムにサンプリング(中規模向け)
3. Bayesian Opt   → 過去の結果から次の候補を選ぶ(効率的)
4. Optuna         → ベイズ最適化 + 枝刈り(推奨)

交差検証の設計

from sklearn.model_selection import StratifiedKFold

# 層化K分割交差検証
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# 各foldの離反率を確認
for fold, (train_idx, val_idx) in enumerate(cv.split(X_train, y_train)):
    fold_rate = y_train.iloc[val_idx].mean()
    print(f"Fold {fold+1}: 離反率 {fold_rate:.3f}")

なぜ交差検証が必要か

手法利点欠点
ホールドアウト高速分割の偶然に依存
K分割交差検証安定した評価計算コストがK倍
層化K分割不均衡データに対応やや複雑
リピートK分割最も安定計算コスト大

Optunaによるハイパーパラメータ最適化

LightGBMのチューニング

import optuna
from lightgbm import LGBMClassifier
from sklearn.model_selection import cross_val_score
import numpy as np

def objective(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        '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.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 50),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),
        'num_leaves': trial.suggest_int('num_leaves', 20, 150),
        'is_unbalance': True,
        'random_state': 42,
        'verbose': -1,
    }

    model = LGBMClassifier(**params)
    scores = cross_val_score(
        model, X_train, y_train,
        cv=cv, scoring='roc_auc', n_jobs=-1
    )
    return scores.mean()

# 最適化の実行
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100, show_progress_bar=True)

print(f"Best AUC-ROC: {study.best_value:.4f}")
print(f"Best params: {study.best_params}")

XGBoostのチューニング

def objective_xgb(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        '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.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'min_child_weight': trial.suggest_int('min_child_weight', 1, 20),
        'gamma': trial.suggest_float('gamma', 1e-8, 5.0, log=True),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),
        'scale_pos_weight': len(y_train[y_train==0]) / len(y_train[y_train==1]),
        'random_state': 42,
        'eval_metric': 'auc',
        'use_label_encoder': False,
    }

    model = XGBClassifier(**params)
    scores = cross_val_score(
        model, X_train, y_train,
        cv=cv, scoring='roc_auc', n_jobs=-1
    )
    return scores.mean()

study_xgb = optuna.create_study(direction='maximize')
study_xgb.optimize(objective_xgb, n_trials=100, show_progress_bar=True)

チューニング結果の分析

# パラメータの重要度
importance = optuna.importance.get_param_importances(study)
print("パラメータ重要度:")
for k, v in importance.items():
    print(f"  {k}: {v:.4f}")

# 最適化の履歴
fig = optuna.visualization.plot_optimization_history(study)
fig.write_image('optuna_history.png')

# パラメータ間の関係
fig = optuna.visualization.plot_param_importances(study)
fig.write_image('optuna_param_importance.png')

最終モデルの構築と評価

# 最良パラメータでモデルを構築
best_model = LGBMClassifier(**study.best_params, is_unbalance=True,
                              random_state=42, verbose=-1)
best_model.fit(X_train, y_train)

# テストデータで最終評価
from sklearn.metrics import roc_auc_score, average_precision_score, classification_report

y_test_proba = best_model.predict_proba(X_test)[:, 1]
y_test_pred = best_model.predict(X_test)

print("=== 最終モデル(テストデータ)===")
print(f"AUC-ROC: {roc_auc_score(y_test, y_test_proba):.4f}")
print(f"PR-AUC:  {average_precision_score(y_test, y_test_proba):.4f}")
print(f"\n{classification_report(y_test, y_test_pred)}")

過学習の検知

# 訓練スコア vs 検証スコア
y_train_proba = best_model.predict_proba(X_train)[:, 1]
train_auc = roc_auc_score(y_train, y_train_proba)
test_auc = roc_auc_score(y_test, y_test_proba)

print(f"訓練 AUC-ROC: {train_auc:.4f}")
print(f"テスト AUC-ROC: {test_auc:.4f}")
print(f"差分: {train_auc - test_auc:.4f}")

# 差分が0.05以上なら過学習の疑い
if train_auc - test_auc > 0.05:
    print("⚠ 過学習の可能性あり。正則化を強化してください。")

モデルの保存

import joblib

# モデルと前処理の保存
joblib.dump(best_model, 'churn_model.pkl')
joblib.dump(scaler, 'scaler.pkl')

# 特徴量リストの保存
with open('feature_names.txt', 'w') as f:
    for name in X_train.columns:
        f.write(f"{name}\n")

print("モデルを保存しました。")

まとめ

項目ポイント
推奨手法Optuna(ベイズ最適化 + 枝刈り)
交差検証層化5分割交差検証
主要パラメータlearning_rate, max_depth, num_leaves
過学習検知訓練/テストのスコア差を確認
最終評価テストデータ(一度だけ使用)

チェックリスト

  • Optunaでハイパーパラメータ最適化を実行できる
  • 交差検証の設計ができる
  • パラメータの重要度を分析できる
  • 過学習を検知し対処できる
  • モデルを保存・読み込みできる

次のステップへ

モデルのチューニングが完了した。次は演習で、ここまでの一連のモデル構築プロセスを実践してみよう。

推定読了時間: 30分