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

【Kaggle挑戦記】Titanic 攻略 #18:Title再投入の罠。CV 0.8440 からの「急転下落」

前回、LightGBMに Title(敬称) を再投入し、手元の検証(CV)では 0.8440 という過去最高レベルの数字を叩き出しました。「今度こそ0.8突破か?」と震える手で提出。しかし、返ってきた結果は 0.76076。期待とは裏腹に、前回(0.77511)よりもスコアを下げる「急転直下」の結末となりました。

1. 敗因分析:Titleという「劇薬」

CVスコアが上がり、本番スコアが下がる。これは典型的な 過学習(オーバーフィッティング) の再発です。なぜあんなに制限をかけたのに失敗したのか、エンジニア的に振り返ります。

  • 「敬称」のヒントが強すぎた: Titleには生存率に直結する情報(性別や年齢層)が凝縮されています。LightGBMがこれに依存しすぎてしまい、他の特徴量との組み合わせによる「本質的な予測」を疎かにしてしまった可能性があります。
  • CVスコアの「甘い誘惑」: CV 0.8440 という数字は、訓練データ内の特定のパターンを覚え込むことで得られた「虚像」でした。データの少ないTitanicでは、特徴量を1つ増やすだけで、モデルは簡単に「カンニング(暗記)」を始めてしまいます。
  • 汎化性能の喪失: パラメータ探索で min_child_samples: 20 が選ばれ、制約が緩んだことも、ノイズを拾う原因になったと考えられます。

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

CVスコアに一喜一憂し、過学習を招いてしまった「失敗の記録」としてのコードです。

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')

# 2. Title(敬称)の抽出(SyntaxWarning対策済み)
for df in [train_data, test_data]:
    df['Title'] = df['Name'].str.extract(r' ([A-Za-z]+)\.', expand=False)
    df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
    df['Title'] = df['Title'].replace('Mlle', 'Miss')
    df['Title'] = df['Title'].replace('Ms', 'Miss')
    df['Title'] = df['Title'].replace('Mme', 'Mrs')

# 3. 前処理(ベスト構成を維持)
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())

# 4. 特徴量選択(ここでTitleを追加したのが裏目に…)
features = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize", "Embarked", "Title"]
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)

# 5. パラメータ設定
param_grid = {
    'num_leaves': [7, 10, 15],
    'learning_rate': [0.01, 0.05],
    'n_estimators': [100, 200, 300],
    'max_depth': [3, 4], 
    'min_child_samples': [20, 40],
    '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)

print(f"Best CV Score: {grid_search.best_score_:.4f}") # 0.8440

# 6. 予測・出力
best_model = grid_search.best_estimator_
predictions = best_model.predict(X_test)
pd.DataFrame({'PassengerId': test_data.PassengerId, 'Survived': predictions}).to_csv('submission_lgbm_title.csv', index=False)

3. 実験結果:0.77511 → 0.76076 への後退

アルゴリズムや特徴量を変えるたびに、スコアは大きく変動します。今回はその「負の側面」が強く出てしまいました。

  • 前回: Score 0.77511 (LightGBM 制限付き)
  • 今回: Score 0.76076 (Title追加)
  • 分析: CVスコア 0.844 との乖離は 0.08 以上。明らかな過学習です。

4. 考察:0.8の壁は「暗記」では越えられない

エンジニア的な視点:
Titleという強力な特徴量を入れたことで、モデルは「考える」ことをやめ、「覚える」ことに走ってしまいました。Titanic攻略において、0.8という数字がなぜ高い壁なのか。それは、強力なモデルや特徴量を「いかに使わずに、本質だけを捉えさせるか」という、引き算の思考が求められるからだと痛感しました。


スコアが下がったことは敗北ではありません。この強力なTitleを「どう手なずけるか」、あるいは「Titleに頼らない別の道を探すか」。この試行錯誤こそが、データサイエンスの醍醐味です。次こそ、真の汎化性能を求めて!

PR