📚 背景知識(読んでから問題へ)
| キーワード | 直感的な説明 |
|---|---|
| Stacking(スタッキング) | 複数のベースモデルの予測確率を新しい特徴量として束ね、メタモデル(多くはシンプルな線形モデル)にもう一段学習させるアンサンブル手法 |
| OOF予測(Out-of-Fold prediction) | K-Foldの各Foldについて、そのFoldを学習に使わなかったモデルで予測を作ること。メタモデルの学習にリークなしで使える予測を作る標準手法 |
| メタモデル / Level-2モデル | ベースモデル(Level-1)の予測を入力として学習する、1段上のモデル |
| アンサンブルの多様性(diversity) | 個々のモデルの精度が高いことよりも、モデル同士が「違う間違い方をする」ことの方がアンサンブルの改善に効くという考え方 |
🗂️ Day136-139 総括(この4日間で何を測ってきたか)
| Day | 手法 | 効果量(delta) | シード間std | delta/std | 判定 |
|---|---|---|---|---|---|
| 136 | Pseudo Labeling(単発・閾値0.9) | 状況により+/- | — | — | 閾値が高すぎると疑似ラベル枯渇、低すぎると誤ラベル混入 |
| 137 | Iterative Pseudo Labeling | 段階により変動 | — | — | confirmation biasで誤りが増幅されることがある |
| 138 | ノイズTTA(scale 0.10-0.30) | +0.0033〜+0.0037 | 0.0024〜0.0045 | 約0.9倍 | 言い切れない |
| 139 | マスキングTTA(ratio 0.05-0.50) | -0.0033〜-0.1100 | 0.0022〜0.0086 | 2.8〜9倍 | 明確に悪化と言い切れる |
今日の前半タスクは、この表を「次にどう使うか」の判断基準に変換すること。「delta が std の何倍あれば行動を変えるべきか」という共通のモノサシを、Pseudo LabelingとTTA両方に当てはめて一枚の意思決定表にする。
🎯 問題
Day136-139と同じ合成分類データ・同じ分割方法(ラベル付き300件・プール1500件・test 600件)、同じベースRandomForest設定を使います。
| 項目 | 値 |
|---|---|
| タスク | 5クラス分類(make_classification, n_samples=2400, n_features=20) |
| ベースモデル4種 | RandomForest / ExtraTrees(各n_estimators=300) / KNeighbors(k=15) / LogisticRegression |
| OOF設定 | StratifiedKFold(n_splits=5, shuffle=True, random_state=0) |
| メタモデル | LogisticRegression(max_iter=2000, random_state=0) |
| 比較対象 | 単体4モデル / leak-free stacking / leakyバージョン / 単純平均アンサンブル(全組み合わせ) |
import numpy as np from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import StratifiedKFold from sklearn.metrics import accuracy_score X, y = make_classification( n_samples=2400, n_features=20, n_informative=12, n_classes=5, n_clusters_per_class=1, random_state=42, flip_y=0.03, ) LABELED_LEN = 300 POOL_LEN = 1500 X_labeled, y_labeled = X[:LABELED_LEN], y[:LABELED_LEN] X_test, y_test = X[LABELED_LEN + POOL_LEN:], y[LABELED_LEN + POOL_LEN:]
💡 ヒント
整理タスクは「効果量がstdの約1倍なら判断保留、3倍を超えたら行動を変える」のような、自分なりの閾値ルールを言語化する練習である。唯一の正解はない。実装タスクは、これまでのTTA実装(拡張サンプルを作りpredict_probaを平均する)とは構造が違うことに注意する。今日は「行ごとに使うモデルを変える」という発想が新しく登場する。
get_oof_probaはskf.split(X, y)の各(train_idx, val_idx)ペアごとにtrain_idxだけでモデルを学習し、val_idxへのpredict_probaを出力配列のval_idx位置に書き込む- 毎Fold
sklearn.base.clone(model)で新しいインスタンスを作る(使い回すと前Foldの学習結果が残る) - メタモデルの入力は
np.hstack([oof_proba["rf"], oof_proba["et"], oof_proba["knn"], oof_proba["logreg"]])のように横連結。5クラス×4モデル=20列 - leakyバージョンの「学習データ上の見かけの精度」は
meta_model.predict(meta_X_train)とy_labeledを比べる
from sklearn.base import clone def get_oof_proba(model, X, y, skf, n_classes): oof = np.zeros((X.shape[0], n_classes)) for train_idx, val_idx in skf.split(X, y): m = clone(model) m.fit(X[train_idx], y[train_idx]) oof[val_idx] = m.predict_proba(X[val_idx]) return oof base_models = { "rf": RandomForestClassifier(n_estimators=300, random_state=0), "et": ExtraTreesClassifier(n_estimators=300, random_state=0), "knn": KNeighborsClassifier(n_neighbors=15), "logreg": LogisticRegression(max_iter=2000, random_state=0), } skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) n_classes = len(np.unique(y_labeled)) oof_proba, test_proba = {}, {} for name, model in base_models.items(): oof_proba[name] = get_oof_proba(model, X_labeled, y_labeled, skf, n_classes) model.fit(X_labeled, y_labeled) test_proba[name] = model.predict_proba(X_test) meta_X_train = np.hstack([oof_proba[n] for n in base_models]) meta_X_test = np.hstack([test_proba[n] for n in base_models]) meta_model = LogisticRegression(max_iter=2000, random_state=0) meta_model.fit(meta_X_train, y_labeled) stack_pred = meta_model.predict(meta_X_test) print(accuracy_score(y_test, stack_pred))
✅ 模範解答
# --- ステップ0: ベースライン --- baseline_model = RandomForestClassifier(n_estimators=300, random_state=0) baseline_model.fit(X_labeled, y_labeled) single_acc = accuracy_score(y_test, baseline_model.predict(X_test)) # 実測: single_acc(RF) = 0.7967 # --- ステップ2: 4ベースモデルを個別に学習・評価 --- base_models = { "rf": RandomForestClassifier(n_estimators=300, random_state=0), "et": ExtraTreesClassifier(n_estimators=300, random_state=0), "knn": KNeighborsClassifier(n_neighbors=15), "logreg": LogisticRegression(max_iter=2000, random_state=0), } test_proba_full = {} for name, model in base_models.items(): model.fit(X_labeled, y_labeled) test_proba_full[name] = model.predict_proba(X_test) # 実測: rf=0.7967 et=0.8117 knn=0.7633 logreg=0.7667 # --- ステップ3: OOF予測 --- def get_oof_proba(model, X, y, skf, n_classes): oof = np.zeros((X.shape[0], n_classes)) for train_idx, val_idx in skf.split(X, y): m = clone(model) m.fit(X[train_idx], y[train_idx]) oof[val_idx] = m.predict_proba(X[val_idx]) return oof skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) oof_proba = {name: get_oof_proba(m, X_labeled, y_labeled, skf, 5) for name, m in base_models.items()} # 実測 OOF acc: rf=0.7767 et=0.8067 knn=0.7500 logreg=0.7400 # → 全モデルでOOF精度がX_labeled全体学習のtest精度よりやや低い(学習に使える行が約240件しかないため) # --- ステップ4: leak-free stacking --- meta_X_train = np.hstack([oof_proba[n] for n in base_models]) meta_X_test = np.hstack([test_proba_full[n] for n in base_models]) meta_model = LogisticRegression(max_iter=2000, random_state=0) meta_model.fit(meta_X_train, y_labeled) stack_acc = accuracy_score(y_test, meta_model.predict(meta_X_test)) # 実測: STACKING (leak-free) acc = 0.8267 delta=+0.0300 # --- ステップ5: leakyバージョン(比較用) --- insample_proba = {} for name, model in base_models.items(): m = clone(model); m.fit(X_labeled, y_labeled) insample_proba[name] = m.predict_proba(X_labeled) # 学習に使った行自身への予測 meta_X_train_leaky = np.hstack([insample_proba[n] for n in base_models]) meta_model_leaky = LogisticRegression(max_iter=2000, random_state=0) meta_model_leaky.fit(meta_X_train_leaky, y_labeled) leaky_train_acc = accuracy_score(y_labeled, meta_model_leaky.predict(meta_X_train_leaky)) leaky_test_acc = accuracy_score(y_test, meta_model_leaky.predict(meta_X_test)) # 実測: 学習データ上の見かけの精度 = 1.0000(完璧) # 実測: STACKING (LEAKY) test acc = 0.8183 delta=+0.0217 # → 学習acc=1.0000なのにtest accはleak-free(0.8267)より低い。見かけと汎化性能が逆転 # --- ステップ6: 単純平均アンサンブル --- avg_proba = np.mean([test_proba_full[n] for n in base_models], axis=0) avg_acc = accuracy_score(y_test, avg_proba.argmax(axis=1)) # 実測: 単純平均アンサンブル(4モデル) acc = 0.8283 delta=+0.0317 # → stacking(+0.0300)よりわずかに単純平均(+0.0317)の方が良い # --- ステップ7: 組み合わせ総当たり --- import itertools for r in range(2, 5): for combo in itertools.combinations(base_models, r): p = np.mean([test_proba_full[n] for n in combo], axis=0) acc = accuracy_score(y_test, p.argmax(axis=1)) print(combo, acc, acc - single_acc) # 実測(抜粋): # rf+logreg acc=0.7783 delta=-0.0183 ← 単体rfより悪化 # et+logreg acc=0.7833 delta=-0.0133 ← これも悪化 # rf+et+knn acc=0.8233 delta=+0.0267 # rf+et+knn+logreg acc=0.8283 delta=+0.0317 ← 全モデル使用が最良
📊 結果の可視化
モデルの組み合わせ別・単純平均アンサンブルのtest acc(全11通り):
■ 赤=単体rf(0.7967)より悪化 / 黄=改善だがごく僅か / 青=改善 / 緑=最良。2モデル構成でrf+logreg・et+logregだけが悪化し、4モデル全部が最良になっている。
| 設定 | test acc | 単一予測比 | 備考 |
|---|---|---|---|
| 単体RF(baseline) | 0.7967 | — | Day136-139と同一条件 |
| 単体ET | 0.8117 | +0.0150 | 4モデル中で単体最良 |
| 単純平均(4モデル) | 0.8283 | +0.0317 | 全構成中の最良 |
| Stacking(leak-free OOF) | 0.8267 | +0.0300 | 単純平均にわずかに劣る |
| Stacking(leaky in-sample) | 0.8183 | +0.0217 | 学習データ上は見かけ1.0000なのに実力は最下位級 |
🪜 Step-by-Step 解説
1なぜOOF精度は、X_labeled全体で学習した場合のtest精度より低いのか
# rf: X_labeled全体で学習→test acc=0.7967 vs OOF acc=0.7767
# et: X_labeled全体で学習→test acc=0.8117 vs OOF acc=0.8067
2なぜleakyバージョンは学習データ上「完璧」なのに、testでは負けるのか
# leaky: 学習データ上の見かけの精度 = 1.0000 test acc = 0.8183
# leak-free: OOFベースの学習acc = 0.8267 test acc = 0.8267(学習acc ≈ test acc)
3なぜ「モデルを増やせば増やすほど良くなる」わけではないのか
# rf単体: 0.7967 logreg単体: 0.7667
# rf+logreg: 0.7783(悪化) et+logreg: 0.7833(悪化)
# rf+et+knn+logreg: 0.8283(4モデルの中では最良)
rf+logreg・et+logregが単体RFより悪化する一方、4モデル全部を使うと最良になる。これはlogregが単体で見せる間違い方が木ベースモデル(rf/et)の間違い方と部分的に重なっており、2モデルだけの平均だと「多数派」が間違った方向に引っ張られやすいためだと考えられる。knnが加わることで、rf・et・logregのどの間違いパターンとも異なる「第4の視点」が入り、平均がようやく安定する。「精度が高いモデルを足す」よりも「違う間違い方をするモデルを足す」ことの方が効くことがある、という多様性の原則を実測の逆転現象として確認できる。🧮 数学・統計の補足(文系向け)
ある生徒(ベースモデル)の実力を測るとき、「その生徒に教えた先生自身が採点する」(リークあり)と、先生は自分が教えた通りの答案に高得点をつけがちです。一方「その生徒を教えていない別クラスの先生が採点する」(OOF)と、生徒が本当に力をつけているかがより正直に測れます。今日のメタモデルは「4人の生徒の答案の傾向から、どの生徒をどれだけ信頼すべきか」を学ぶ役割です。もし担任の採点(リークあり)を見て学んでしまうと、メタモデルは「担任びいきの点数のクセ」を学習してしまい、本番の初見の問題(testデータ)では読みが外れます。
4人で意見を出し合って多数決を取るとき、「似た意見を持つ人ばかり」を集めると、その意見が的外れだった場合に多数決ごと外れます。今日の実測で
rf+logreg(似た傾向を持つ2人)の多数決が単体rfより悪化したのは、これに近い現象です。普段とは違う視点を持つ人(今日で言えばknn)が加わることで多数決全体のバランスが取れ、結果が安定しました。「頭数を増やす」ことより「視点の種類を増やす」ことの方が集団の判断の質を上げるという直感は、機械学習のアンサンブルにもそのまま当てはまります。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☐ 時系列(Time Series)
🔁 OOFパターンはKaggle上位陣の"共通言語"
Stackingに限らず特徴量エンジニアリングにも必須
TargetEncodingなど「学習データに対する予測や統計量を作るとき」は、必ずその行を学習に使っていないモデル/集計で作るという原則は、コンペのあらゆる場面で繰り返し登場する。
⚖️ 「Stackingは必ず単純平均より強い」は思い込み
データ量が少ない局面では平均が有利なことも
今日の実測のようにラベル付きデータやベースモデル数が少ない状況では、パラメータを持たない単純平均の方がロバストに勝つことがある。上位陣もまず単純平均やRank Averagingを試してからStackingに進む段階的アプローチを取ることが多い。
🧊 メタモデルはシンプルに保つのが定石
Level-2を複雑にしすぎない
今日はLogisticRegressionをメタモデルに使った。メタモデルに複雑な木モデルを使うと、少ないOOFデータに対してさらに過学習しやすくなる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| メタモデルの学習には、ベースモデルをX_labeled全体で学習した後のpredict_probaをそのまま使えばよいと思う | 「モデルはもう学習済みだから、そのまま使い回せば効率的」という直感 | 今日の実測のようにこれはリークになり学習データ上の見かけの精度(1.0000)と本当のtest精度(0.8183)が乖離する。必ずOOF予測を使う |
| Stackingは複雑な手法だから単純平均より必ず精度が高いと思い込む | 手法の複雑さと性能の高さを直結させてしまう | 今日の実測ではstacking(+0.0300)が単純平均(+0.0317)にわずかに劣った。データ量が少ないとメタモデルの学習が不安定になり、パラメータのない単純平均の方が有利になることがある |
| アンサンブルに使うモデルは精度が高いものだけを選べばよいと思う | 「弱いモデルを混ぜたら足を引っ張られる」という直感 | 今日の実測では単体で最も弱いlogreg(0.7667)を含む4モデル構成が最良(0.8283)だった一方、rf+logregの2モデル構成は単体rfより悪化した。個々の精度より「間違い方の多様性」が効くことがある |
| 学習データ上の精度が高ければそのモデル(メタモデル)は信頼できると思う | 「訓練精度=モデルの実力」という単純な図式で考えてしまう | leakyバージョンの学習データ上の精度は1.0000だったが、これは汎化性能を何も保証しない。学習accとOOFベースのaccがどれだけ近いかを見る方が汎化性能の手がかりになる |
🚀 次のステップ
- 発展: メタモデルへの入力を「予測確率そのもの」ではなく「予測クラスのOne-Hotエンコーディング」や「予測確率の順位(Rank)」にした場合でも同じ傾向になるか試してみましょう。OOFのFold数を5から10に増やすと、stacking精度や学習acc/test accの一致度がどう変わるかも確認する価値があります
- 次回予告: Phase6テーマ3「高度なアンサンブル」(Day136-140、全5日)が完了しました。次はPhase6テーマ4「コンペ戦略・Gold 3枚達成プラン」(テーマ16-20)に入ります。これまでの技術的な実験(Pseudo Labeling・TTA・Stacking)を離れ、実際のKaggleコンペで「どのタイミングで何を試すか」「提出回数をどう配分するか」といった、より戦略的な意思決定のテーマに移ります
Phase 6 の学習マップ(全20テーマ)
Day136-137でPseudo Labeling、Day138-139でノイズ/マスキングTTA、Day140でStacking(OOF)を実装。Phase6テーマ3が全5日で完了し、次はコンペ戦略フェーズへ。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: