忍者ブログ
統計、機械学習、AIを学んでいきたいと思います。 お役に立てば幸いです。

【Kaggle挑戦記】Titanic 攻略 #16:LightGBMの洗礼。CVスコアの「罠」と過学習の恐怖

前回、最強の刺客 LightGBM を投入し、手元の交差検証(CV)で 0.8418 という驚異的なスコアを叩き出しました。「ついに0.8の大台か?」と期待に胸を膨らませて提出した結果、待っていたのは 0.76315 という非情な現実。自己ベスト(0.78468)から大きく後退する結果となりました。

1. なぜ「手元の高スコア」が「本番の惨敗」を招いたのか?

今回の敗因は、機械学習において最も警戒すべき 過学習(オーバーフィッティング) です。原因をエンジニア的に分析すると、以下の3点に集約されます。

  • モデルが「賢すぎた」: LightGBMは非常に強力なため、約890件という少ない訓練データの「偶然の偏り」まで完璧に学習してしまいました。
  • CVスコアの信憑性: CVスコア 0.84 というのは、訓練データ内での「予行演習」に過ぎません。本番のテストデータとの間に、学習しきれないギャップが存在していました。
  • パラメータの攻めすぎ: max_depth: -1(無制限)や num_leaves: 20 という設定が、少数のデータに対しては複雑すぎた可能性があります。

2. 【実装】光と影を記録した LightGBM コード

CVスコア 0.8418 を出しながらも、本番で 0.76315 に沈んだ「教訓」としてのコードです。

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import GridSearchCV

# 1. データの読み込みと前処理(ベスト布陣を維持)
train_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')

for df in [train_data, test_data]:
    df['Embarked'] = df['Embarked'].fillna('S')
    df['FamilySize'] = df['SibSp'] + df['Parch'] + 1

group_cols = ['Pclass', 'Sex']
train_data['Age'] = train_data['Age'].fillna(train_data.groupby(group_cols)['Age'].transform('median'))
test_data['Age'] = test_data['Age'].fillna(test_data.groupby(group_cols)['Age'].transform('median'))
test_data['Fare'] = test_data['Fare'].fillna(test_data['Fare'].median())

# 2. 特徴量の準備(ダミー変数化)
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize", "Embarked"]
X = pd.get_dummies(train_data[features], drop_first=True)
y = train_data["Survived"]
X_test = pd.get_dummies(test_data[features], drop_first=True)
X, X_test = X.align(X_test, join='left', axis=1, fill_value=0)

# 3. LightGBMの設定(ここでの最適化が本番で裏目に出た)
param_grid = {
    'num_leaves': [10, 20, 31],
    'learning_rate': [0.01, 0.05, 0.1],
    'n_estimators': [100, 500],
    'max_depth': [-1, 3, 5],
    'random_state': [1]
}

gbm = lgb.LGBMClassifier(verbosity=-1)
grid_search = GridSearchCV(gbm, param_grid, cv=5, n_jobs=-1, verbose=0)
grid_search.fit(X, y)

# 結果出力(CVスコアは 0.8418 をマーク)
print(f"LGBM Best Params: {grid_search.best_params_}")
print(f"LGBM Best Score (CV): {grid_search.best_score_:.4f}")

# 予測実行
best_model = grid_search.best_estimator_
predictions = best_model.predict(X_test)
pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions}).to_csv('submission_lgbm_overfit.csv', index=False)

3. 実験結果:期待と現実のギャップ

アルゴリズムを変えたことで、数字に大きな「動き」が出ましたが、今回は悪い方へ転がりました。

  • 自己ベスト(ランダムフォレスト): Score 0.78468
  • 今回(LightGBM): Score 0.76315(CVスコアとの乖離:-0.078)

4. 考察:高すぎるCVスコアを疑え

エンジニア的な視点:
「手元で完璧なモデルが、外の世界で通用するとは限らない」。今回の結果は、AI開発における本質的な難しさを教えてくれました。CVスコアが 0.84 まで跳ね上がった時点で、「学習しすぎではないか?」と疑うべきだったのです。Titanicのような少人数データでは、LightGBMのような強力なモデルを「いかに抑え込むか(正則化)」が次の鍵となります。


スコアダウンは失敗ではなく、モデルの特性を理解するための貴重なデータです。次は、LightGBMをあえて「弱く」する(パラメータを厳しく制限する)か、あるいはランダムフォレストの安定感を見直すか。この 0.02 の差を埋めるための戦いは、さらに深化していきます。


PR