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

【Kaggle挑戦記】TPS Feb 2022:DNA配列データで手元CV0.995超えからの「重複行の罠」

前回のMoAコンペ(マルチラベル分類)を一区切りとし、今回はDNA配列データを扱う過去コンペ「Tabular Playground Series - Feb 2022(TPS Feb 2022)」に参戦しました。まずは手動での特徴量生成(エンジニアリング)を一切行わず、素のデータをそのままLightGBMに投入して不動のベースラインスコアを算出します。

1. コンペの概要とデータ構造

このコンペティションは、ゲノムシーケンス解析の手法をモチーフにした「10クラスの多クラス分類タスク」です。

  • データの中身: ゲノム解読機で抽出された10文字のDNA断片(10-mer)の出現カウントデータです。A0T0G0C10 から A10T0G0C0 まで、全286パターンの比率(標準化された確率)がカラム(特徴量)として並んでいます。
  • ターゲット: 10種類の細菌(大腸菌やピロリ菌、サルモネラ菌など)のいずれか。
  • 評価指標: Accuracy(正解率)

数値の意味としては、単なる割合ではなく「理論上の期待値(ランダムな発生確率)からのズレ」を示しており、プラスならその細菌特有の出現パターン、マイナスなら滅多に出現しないパターンを意味します。

2. 実装したベースラインコード

以前挑戦した「Digit Recognizer(手書き数字認識)」と同じ多クラス分類(multiclass)の枠組みを適用しました。文字列の細菌名を LabelEncoder で数値(0〜9)に変換して学習させ、最終的な予測時に元の細菌名へ復元して提出ファイルを作成します。

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

# 不要なID列の除去と目的変数の分離
X = train.drop(columns=['row_id', 'target'])
y_raw = train['target']
X_test = test.drop(columns=['row_id'])

# ターゲット(文字列の細菌名)を0〜9の数値に変換
le = LabelEncoder()
y = le.fit_transform(y_raw)

# 2. パラメータ設定(10クラスの多クラス分類)
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による5分割交差検証
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof_preds = np.zeros((len(train), 10))
test_preds = np.zeros((len(test), 10))

print("Starting Cross Validation...")
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
    X_train, y_train = X.iloc[train_idx], y[train_idx]
    X_val, y_val = X.iloc[val_idx], y[val_idx]
    
    model = lgb.LGBMClassifier(**params, n_estimators=1000)
    
    model.fit(
        X_train, y_train,
        eval_set=[(X_val, y_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
    print(f" Fold {fold + 1} finished.")

# 4. 手元スコア(Accuracy)の算出
oof_classes = np.argmax(oof_preds, axis=1)
cv_accuracy = accuracy_score(y, oof_classes)

print("\n" + "=" * 30)
print(f"手元での交差検証スコア(Accuracy): {cv_accuracy:.5f}")
print("=" * 30)

# 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_lgb.csv', index=False)
print("Submission file created: submission_tps_feb2022_lgb.csv")

3. 実行結果:衝撃のスコアギャップ

Macのローカル環境でコードを実行し、手元の交差検証スコア(CV)を確認したところ、驚くべき高精度を記録しました。しかし、作成した submission_tps_feb2022_lgb.csv をKaggleにLate Submitした結果は大きく異なるものでした。

Loading data...
Starting Cross Validation...
 Fold 1 finished.
 Fold 2 finished.
 Fold 3 finished.
 Fold 4 finished.
 Fold 5 finished.

==============================
手元での交差検証スコア(Accuracy): 0.99553
==============================
Submission file created: submission_tps_feb2022_lgb.csv
Kaggle Leaderboard 正解率(Accuracy):0.93937 / 0.94192

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

手元でのCVが0.99553(約99.5%)という完璧に近い数値を出していたのに対し、本番のリーダーボード(LB)では0.93937まで急降下するという大きな乖離が発生しました。これこそが、このコンペに潜む「データ重複の罠」です。

  • リークによる過学習: このデータセットには全く同じ値を持つ行(重複データ)が多数含まれています。通常の StratifiedKFold で分割すると、同じデータが「学習用」と「検証用」の両方に分散して入り込んでしまい、モデルが答えを暗記してCVが跳ね上がっていました。
  • 多クラス分類の速度感: 前回のMoA(マルチラベル)と比べ、10クラスの「multiclass」として1回の実行で済むため、20万行の処理もローカル環境で非常にスムーズでした。

「手元のCVを盲信してはいけない」というKaggleの洗礼を綺麗に浴びる形となりましたが、課題(原因)は明確です。次回はこの「重複データの除外」と「サンプルの重み付け(sample_weight)」を導入し、手元の検証環境を正しく修正した上で本番スコアの大幅更新を狙います。


次回は、重複行の処理とサンプルの重み付けを実装し、Kaggle本番スコア0.98オーバーへの到達を目指します。

PR