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

【Kaggle挑戦記】TPS Feb 2022:1.1万行の「重複データの罠」を破り、本番LBスコアを更新!

前回の検証では、手動での特徴量生成を行わずに素のデータをLightGBMへ投入した結果、手元CVが0.99553(約99.5%)という異次元の高精度を記録した一方で、Kaggle本番のLeaderboard(LB)では0.93937まで急降下するという大きなスコアギャップに直面しました。

今回は、この乖離の原因である「データ重複によるリーク(過学習)」を解消し、正しく検証環境を再構築してLBスコアの更新に挑みました。

1. なぜ手元スコアと本番スコアが大きくズレたのか?

分析を進めたところ、全20万行ある train.csv の中に、全く同じ特徴量(286列の数値)を持つ「重複行」が約1.1万行(全体の約5%以上)も存在していることが判明しました。

  • 丸暗記による過学習: 通常の StratifiedKFold で分割すると、全く同じデータが「学習用」と「検証用」の両方に分散して入り込んでしまいます。結果としてモデルがデータを暗記してしまい、手元のCVだけが異常に高く(0.99553)評価されていました。
  • 本番データでの失速: Kaggleのテストデータには未知のデータが含まれるため、暗記に頼ったモデルは本番環境(LB 0.93937)で威力を発揮できませんでした。

2. 対策:重複行の一元化と sample_weight の導入

単に重複行を削除(drop_duplicates)するだけでは、「そのデータがどれだけ高頻度で出現するか」という重要な確率情報(重み)が失われてしまいます。

そこで、groupby().size() を活用して重複行を1行に集約しつつ、出現回数を sample_weight(サンプルの重み)として LightGBM に与えて学習させるアプローチ(sample2.py)へコードを全修正しました。

import pandas as pd
import numpy as np
from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score
import lightgbm as lgb
import warnings

warnings.filterwarnings("ignore")

# 1. データの読み込みと重複処理
print("Loading data...")
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

X_raw = train.drop(columns=['row_id'])
X_test = test.drop(columns=['row_id'])

# 特徴量とターゲットが全く同じ行を集計して重み(sample_weight)を作成
features = [col for col in X_raw.columns if col != 'target']
train_dedup = X_raw.groupby(features + ['target']).size().reset_index(name='sample_weight')

X = train_dedup[features]
y_raw = train_dedup['target']
sample_weight = train_dedup['sample_weight']

print(f"元のデータ数: {len(train)} 行")
print(f"重複除去後のデータ数: {len(X)} 行")  # 123,993 行へ圧縮

# ターゲットの数値変換
le = LabelEncoder()
y = le.fit_transform(y_raw)

# 2. 多クラス分類パラメータ
params = {
    'objective': 'multiclass',
    'num_class': 10,
    'metric': 'multi_logloss',
    'verbosity': -1,
    'boosting_type': 'gbdt',
    'learning_rate': 0.1,
    'random_state': 42
}

# 3. Stratified K-Fold(sample_weight付き)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof_preds = np.zeros((len(X), 10))
test_preds = np.zeros((len(test), 10))

for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
    X_train, y_train, w_train = X.iloc[train_idx], y[train_idx], sample_weight.iloc[train_idx]
    X_val, y_val, w_val = X.iloc[val_idx], y[val_idx], sample_weight.iloc[val_idx]
    
    model = lgb.LGBMClassifier(**params, n_estimators=1000)
    
    # fit時に sample_weight を適用
    model.fit(
        X_train, y_train,
        sample_weight=w_train,
        eval_set=[(X_val, y_val)],
        eval_sample_weight=[w_val],
        callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]
    )
    
    oof_preds[val_idx] = model.predict_proba(X_val)
    test_preds += model.predict_proba(X_test) / skf.n_splits

# 4. 正しい重み付き評価(Accuracy)の算出
oof_classes = np.argmax(oof_preds, axis=1)
cv_accuracy = accuracy_score(y, oof_classes, sample_weight=sample_weight)
print(f"修正後の手元交差検証スコア(Accuracy): {cv_accuracy:.5f}")

# 5. 提出用ファイルの作成
test_classes_num = np.argmax(test_preds, axis=1)
test_classes_str = le.inverse_transform(test_classes_num)

submission = pd.DataFrame({
    'row_id': test['row_id'],
    'target': test_classes_str
})
submission.to_csv('submission_tps_feb2022_dedup_lgb.csv', index=False)

3. 実行結果とスコア比較

重複一元化を行ったモデルの実行ログおよびKaggleへ再提出(Late Submit)した結果の推移です。

Loading data...
Processing duplicates...
元のデータ数: 200000 行
重複除去後のデータ数: 123993 行

Starting Cross Validation with sample_weight...
 Fold 1 finished.
 Fold 2 finished.
 Fold 3 finished.
 Fold 4 finished.
 Fold 5 finished.

========================================
修正後の手元交差検証スコア(Accuracy): 0.95766
========================================

【実験結果の比較表】

手法手元CV (Accuracy)Kaggle Public LB評価
1. 生データ (無加工) 0.99553 0.94192 (0.93937) 重複データによるリークでCVが歪んでいた
2. 重複除去 + sample_weight 0.95766 0.94317 (Private: 0.94024) CVが健全化し、本番スコアも着実に向上!

4. 考察とエンジニアとしての気づき

手元のCVスコアは前回の「丸暗記による異常値(0.99553)」から0.95766へと下がり、現実的で信頼できる検証値へと正常化されました。そして何より、Kaggle本番(Public LB)のスコアが 0.94192 → 0.94317 へとしっかり向上したことが大きな成果です。

  • 検証環境(CV)の重要性: 「CVスコアが高ければ良い」わけではなく、適切な前処理によって本番環境(テストデータ)の挙動を正しく反映する検証環境を作ることが最優先であると再認識しました。
  • 重み付けの有効性: 単純な drop_duplicates でデータ量を減らすだけでなく、出現回数を sample_weight としてモデルに伝えることで、情報量を維持したまま過学習を防止できました。

手元検証の基盤が整ったため、いよいよ次回はこのコンペの「真のキーパーツ」であるドメイン知識(GCD:最大公約数を用いた実験スケールの復元)を取り入れ、本番スコアの上位帯(0.98以上)への到達を目指します。


次回は、GCD(最大公約数)特徴量の計算・追加によるモデルの飛躍的な精度向上に挑戦します。

PR