📚 背景知識(読んでから問題へ)
料理でいうと「基本の一皿」から「コンテスト用のコース料理」への格上げです。Bronzeは「丁寧に基本を守れば届く」領域ですが、Silverは モデルの多様性・検証の厳密さ・自分の予測を疑う力 が要求されます。
Bronze → Silver で何が変わるか
| 項目 | Phase 3(Bronze・上位40%) | Phase 4(Silver・上位10%) |
|---|---|---|
| モデル | LightGBM 単体 | LightGBM + XGBoost(+ CatBoost)の 多様性 |
| ハイパラ | デフォルト値 or 手動調整 | Optuna で自動探索(Day 096-097) |
| 検証 | Stratified/K-Fold のみ | NestedCV でチューニングと評価を分離(Day 101) |
| 特徴量 | 基本の変換・単純TargetEncoding | Aggregation・Rolling/Lag・LeaveOneOut TE(Day 098-100) |
| 最終予測 | 単一モデルの平均 | Stacking/Blending で複数モデルを合成(Day 102-104) |
| 頑健性 | — | 外れ値対策・不均衡対策(Day 105-106) |
| 解釈 | 特徴量重要度のみ | SHAP でグローバル/ローカル解釈、リーク・バグの発見(Day 107-108) |
🗺️ Phase 4 の19日間マップ
| Day | テーマ |
|---|---|
| 090 | Gradient Boostingとは何か |
| 091-092 | XGBoost(実装・パラメータ) |
| 093-094 | LightGBM(実装・DART/GOSS) |
| 095 | CatBoost(Ordered Boosting) |
| 096-097 | Optuna(基礎・高度な使い方) |
| 098-100 | Aggregation・Rolling/Lag・高度なTargetEncoding |
| 101 | NestedCV |
| 102-104 | アンサンブル(Averaging・Stacking・Blending) |
| 105-106 | 外れ値対策・不均衡データ対策 |
| 107-108 | 特徴量選択の高度手法・SHAP値の解釈 |
| 109 | Phase 4 総復習(本日) |
📐 評価指標: RMSE を視覚的に理解する
RMSE = √( mean( (y_true − y_pred)² ) )
誤差を2乗してから平均し、最後にルートを取って元の単位に戻した「誤差の代表的な大きさ」
なぜ2乗するのか? 5件の予測を例に、「そのままの誤差」と「2乗した誤差」を比べてみます。
🎯 問題
Kaggle Expert(Bronze 2枚 or Silver 1枚は達成済み)として、Playground Series 相当の 回帰コンペ(評価指標 RMSE)に参加し、Silver圏内(上位10%)を目指す設定です。
データスキーマ
| カラム | 型 | 説明 |
|---|---|---|
id | int | 行を一意に識別するID |
num_01 〜 num_18 | float | 数値特徴量18個(一部に外れ値・軽い欠損を含む) |
cat_region, cat_plan, cat_device, cat_channel, cat_segment | category(低カーディナリティ, 4〜10種) | 属性系カテゴリ変数 |
cat_user_hash | category(高カーディナリティ, 約500種) | ユーザー系列を示す疑似ハッシュ |
cat_item_hash | category(高カーディナリティ, 約350種) | アイテム系列を示す疑似ハッシュ |
target | float(train のみ) | 予測対象の連続値。評価指標は RMSE |
train: 50,000行 / test: 20,000行 / 提出形式: id,target
タスク
💡 ヒント
新しいアルゴリズムを覚える回ではありません。「モデルの多様性」「検証の厳密さ」「自分の予測を疑う目(SHAP)」の3つを、既存のBronzeパイプラインに足し算していく回です。Optunaの探索に使うCVと、最終評価・Stacking用のOOFを作るCVは、役割を分けて考えます。
- Optuna:
optuna.create_study(direction='minimize')で、目的関数の中では軽量な3-Foldなど専用のCVでRMSEを評価する(外側5-Foldとは別物) - 外側5-Fold:
KFold(n_splits=5, shuffle=True, random_state=42)で LightGBM・XGBoost それぞれの OOF・test予測を作る - Stacking:
oof_matrix = np.column_stack([oof_lgb, oof_xgb])をRidge()で学習し、test_matrixに適用 - SHAP:
shap.TreeExplainer(model).shap_values(X)→np.abs(shap_values).mean(axis=0)で特徴量ごとの平均インパクトを見る
import optuna import numpy as np from sklearn.model_selection import KFold from sklearn.linear_model import Ridge from sklearn.metrics import mean_squared_error import lightgbm as lgb import xgboost as xgb # --- Optuna: 内側CV専用(チューニングのみ) --- def objective(trial): params = { 'num_leaves': trial.suggest_int('num_leaves', 15, 63), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1, log=True), 'min_child_samples': trial.suggest_int('min_child_samples', 5, 50), } kf_inner = KFold(n_splits=3, shuffle=True, random_state=0) # ★外側とは別のCV scores = [] for tr_idx, va_idx in kf_inner.split(X): model = lgb.LGBMRegressor(**params, n_estimators=500, random_state=42) model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) pred = model.predict(X.iloc[va_idx]) scores.append(mean_squared_error(y.iloc[va_idx], pred, squared=False)) return np.mean(scores) study = optuna.create_study(direction='minimize') study.optimize(objective, n_trials=30) # --- 外側5-Fold: OOF生成・最終評価用(チューニングには使わない) --- kf_outer = KFold(n_splits=5, shuffle=True, random_state=42) oof_lgb = np.zeros(len(X)) # ... LightGBM・XGBoostそれぞれで同様にOOFを作る ... # --- Level-2 Stacking --- oof_matrix = np.column_stack([oof_lgb, oof_xgb]) meta_model = Ridge(alpha=1.0) meta_model.fit(oof_matrix, y)
✅ 模範解答
タスク1: Silverに向けた5つの打ち手(優先度順)
| 優先度 | 打ち手 | なぜスコアが伸びるか |
|---|---|---|
| 1 | モデルの多様性を増やす(LGBM+XGB、余力があればCatBoost) | 異なる木の育て方の誤差を平均すると分散が下がる |
| 2 | NestedCVでチューニングの過大評価を防ぐ | 同じCVでチューニングと評価をすると、パラメータがそのCV分割に過適合しスコアが甘くなる |
| 3 | 高度な特徴量工学(Aggregation・LeaveOneOut TE) | GBDTは特徴量の質に強く依存する。良い特徴量1つはパラメータ調整10回分の価値がある |
| 4 | Stacking(単純平均より一段上) | Ridgeがモデルごとの信頼度に応じた重みを自動学習し、単純平均より誤差を減らせる |
| 5 | SHAPで予測の健全性を検証 | 重要度が高い特徴に想定外のリークやノイズが混ざっていないかを早期発見できる |
タスク2〜5: 統合パイプライン実装
import numpy as np import pandas as pd import optuna from sklearn.model_selection import KFold from sklearn.linear_model import Ridge from sklearn.metrics import mean_squared_error import lightgbm as lgb import xgboost as xgb import shap optuna.logging.set_verbosity(optuna.logging.WARNING) # ====== 0. データ読み込み ====== train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') y = train['target'] X = train.drop(columns=['target', 'id']) X_test = test.drop(columns=['id']) cat_cols = X.select_dtypes(exclude='number').columns.tolist() for c in cat_cols: X[c] = X[c].astype('category') X_test[c] = X_test[c].astype('category') # ====== 1. Optuna(内側CV=3-Fold・チューニング専用) ====== def objective(trial): params = { 'num_leaves': trial.suggest_int('num_leaves', 15, 63), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1, log=True), 'min_child_samples': trial.suggest_int('min_child_samples', 5, 50), 'subsample': trial.suggest_float('subsample', 0.6, 1.0), 'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0), } kf_inner = KFold(n_splits=3, shuffle=True, random_state=0) scores = [] for tr_idx, va_idx in kf_inner.split(X): model = lgb.LGBMRegressor(**params, n_estimators=500, random_state=42) model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) pred = model.predict(X.iloc[va_idx]) scores.append(mean_squared_error(y.iloc[va_idx], pred, squared=False)) return np.mean(scores) study = optuna.create_study(direction='minimize') study.optimize(objective, n_trials=30) best_params = study.best_params print("Best params:", best_params) # ====== 2. 外側5-Fold: LightGBM・XGBoost で OOF + test 予測 ====== kf_outer = KFold(n_splits=5, shuffle=True, random_state=42) oof_lgb, oof_xgb = np.zeros(len(X)), np.zeros(len(X)) test_lgb, test_xgb = np.zeros(len(X_test)), np.zeros(len(X_test)) final_lgb_model = None for fold, (tr_idx, va_idx) in enumerate(kf_outer.split(X)): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] # --- LightGBM(Optunaのベストパラメータを使用) --- model_lgb = lgb.LGBMRegressor(**best_params, n_estimators=1000, random_state=42) model_lgb.fit( X_tr, y_tr, eval_set=[(X_va, y_va)], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)], ) oof_lgb[va_idx] = model_lgb.predict(X_va) test_lgb += model_lgb.predict(X_test) / kf_outer.n_splits final_lgb_model = model_lgb # SHAP用に最後のfoldモデルを保持 # --- XGBoost(手堅い固定パラメータ) --- model_xgb = xgb.XGBRegressor( max_depth=6, learning_rate=0.03, n_estimators=1000, subsample=0.8, colsample_bytree=0.8, enable_categorical=True, random_state=42, ) model_xgb.fit(X_tr, y_tr, eval_set=[(X_va, y_va)], verbose=False) oof_xgb[va_idx] = model_xgb.predict(X_va) test_xgb += model_xgb.predict(X_test) / kf_outer.n_splits print(f"fold{fold} LGBM RMSE={mean_squared_error(y_va, oof_lgb[va_idx], squared=False):.4f} " f"XGB RMSE={mean_squared_error(y_va, oof_xgb[va_idx], squared=False):.4f}") print(f"\nLightGBM OOF RMSE: {mean_squared_error(y, oof_lgb, squared=False):.4f}") print(f"XGBoost OOF RMSE: {mean_squared_error(y, oof_xgb, squared=False):.4f}") # ====== 3. Level-2 Stacking(Ridge回帰) ====== oof_matrix = np.column_stack([oof_lgb, oof_xgb]) test_matrix = np.column_stack([test_lgb, test_xgb]) meta_model = Ridge(alpha=1.0) meta_model.fit(oof_matrix, y) oof_stack = meta_model.predict(oof_matrix) test_stack = meta_model.predict(test_matrix) print(f"\n=== Stacking OOF RMSE: {mean_squared_error(y, oof_stack, squared=False):.4f} ===") print("Meta weights (LGBM, XGB):", meta_model.coef_) # ====== 4. SHAP で最終モデル(LightGBM)を解釈 ====== explainer = shap.TreeExplainer(final_lgb_model) shap_values = explainer.shap_values(X_va) importance = pd.Series(np.abs(shap_values).mean(axis=0), index=X.columns) print("\nSHAP 上位3特徴量:") print(importance.sort_values(ascending=False).head(3)) # ====== 5. 提出ファイル作成 ====== submission = pd.DataFrame({'id': test['id'], 'target': test_stack}) submission.to_csv('submission.csv', index=False) print(submission.head())
タスク6: CV=0.352 / LB=0.361 の判断とSHAPチェック
| # | 判断・チェック項目 | Bronze期との違い |
|---|---|---|
| 1 | 乖離の許容幅を狭める | Bronze期は0.04〜0.05を許容していたが、Silverは上位10%内の僅差が順位を左右するため、乖離の原因(分布ズレ・fold設計)を必ず特定してから提出を続ける |
| 2 | SHAPの上位特徴量にリーク源がないか確認 | 重要度が異常に高い1特徴(例: IDと相関する列、集計時にtargetが混入した列)がないかをSHAP summary plotで毎回確認する |
| 3 | Adversarial Validationで分布ズレを定量化 | train/testを見分ける分類器のAUCが0.5から大きく離れていないかをSilver狙いでは必須チェックにする |
🧩 アンサンブルの効果(参考シミュレーション値)
架空のコンペ設定における典型的な傾向を示す参考値。RMSEは小さいほど良い指標なので、バーが短いほど高性能を意味する。
単体モデル → 単純平均 → Stackingの順に誤差が小さくなる。誤差パターンが異なる2モデルを組み合わせるほど、分散が下がりRMSEが改善する典型例。
🔍 SHAP解釈: 上位3特徴量(参考シミュレーション値)
🪜 Step-by-Step 解説
1Optunaの内側CVを外側CVと分離する(NestedCV)
kf_inner = KFold(n_splits=3, shuffle=True, random_state=0) # Optuna専用 kf_outer = KFold(n_splits=5, shuffle=True, random_state=42) # OOF・評価専用
random_stateも分けることで、内側と外側の分割が完全に独立します。2LightGBMとXGBoostで別々にOOFを作る
oof_lgb[va_idx] = model_lgb.predict(X_va) oof_xgb[va_idx] = model_xgb.predict(X_va)
3Ridge回帰でLevel-2 Stacking
meta_model = Ridge(alpha=1.0)
meta_model.fit(oof_matrix, y)
4SHAPで最終モデルの妥当性を確認
importance = pd.Series(np.abs(shap_values).mean(axis=0), index=X.columns)
5最終提出は「説明できるCV」を選ぶ
🧮 数学・統計の補足(文系向け)
最終予測 = w1×LightGBMの予測 + w2×XGBoostの予測(+ 切片)。2つのOOF予測が似すぎている(相関が高い)場合に単純な最小二乗法だと係数が不安定になりやすく、Ridgeは係数の二乗和にペナルティを与えることでこれを抑えます。🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☑ 時系列(Time Series)
🧱 モデルを増やす
Phase 5でNNを追加
ニューラルネットワーク(PyTorch)をStackingに追加すると、GBDT系だけでは拾えない非線形パターンをさらに補える。
🧬 特徴量の型を変える
マルチモーダルへ
NLP特徴量・画像特徴量をTabular特徴量と混ぜるマルチモーダルコンペで、今日の骨格がそのまま使える。
🪜 Stackingを多層化
Level-3へ
Level-2をLightGBMにする、Level-3を追加するなど、上位陣は3層構成のStackingも使う。
Silverメダル圏内に入るための実務チェック
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 同系統のGBDTを3つスタッキングすれば精度が上がる | 「モデルを増やせば良い」という思い込み | 誤差パターンが似ていると平均しても分散はあまり減らない。系統の違うモデルを混ぜる方が効果的 |
| Optunaのチューニングと最終CVを同じ分割で行う | 実装を簡単にしたい | パラメータがそのfold分割に過適合し、CVスコアが甘くなる(NestedCVで回避) |
| StackingのRidgeを外側CVなしで一発学習する | OOFなら安全だと過信 | ベースモデルが少数(2〜3個)なら実務上は大きな問題になりにくいが、モデル数が増えるほどRidge自体もCVで検証すべき |
| SHAPは重要度ランキングを見るだけで終わる | 「重要度=正しい」と思い込む | 上位特徴量が本当に意味のある特徴か、リークやノイズでないかを毎回疑う |
| Public LBが良くなるまでStackingの重みを手動調整する | LBが正義だと思う | Meta modelの重みはOOFに基づいて自動学習させ、LB直接最適化(LB probing)は避ける |
🚀 次のステップ
- 発展: このパイプラインに CatBoost を3本目のベースモデルとして追加し、Stackingの重みがどう変化するか確認する。また
optunaのn_trialsを増やし、探索の安定性を比較する - 次回予告: Phase 5(紫帯)突入 — 「ディープラーニング基礎(ニューラルネットワークの直感)」(Day 110)。GBDTが苦手とする画像・テキスト・複雑な非線形パターンに、なぜニューラルネットワークが強いのかを文系向けに解き明かす
Kaggleランクの道のり
Phase 3でBronze圏内(上位40%)の型を習得し、Phase 4でモデル多様性・NestedCV・Stacking・SHAPを積み上げてSilver圏内(上位10%)を狙えるところまで到達。Phase 5ではさらにGold(上位1%前後)を目指す武器を増やしていく。
🎉 Phase 4 完走! GBDT・Optuna・高度な特徴量工学・アンサンブル・SHAP解釈まで、Kaggle Expert〜Silver到達に必要な武器がそろいました。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: