Day 140 — 高度なアンサンブル⑤ — テーマ群総括 と Stacking基礎(OOFはなぜリークを防げるのか)

2026-08-30 金 / Phase 6(Grandmaster、テーマ3群 の 5日目・最終日) コーディング Stacking / OOF予測 / アンサンブルの多様性

📚 背景知識(読んでから問題へ)

🏁
Phase6テーマ3「高度なアンサンブル」の5日目・最終日です。前半でDay136-139の4日間を振り返って「どの手法をどんな条件で使うべきか」の判断基準表を作り、後半で新しい手法Stacking(メタモデルによる予測の統合)に着手します。
🧩
今日の核心 — OOF(Out-of-Fold)予測: Stackingのメタモデルを「ベースモデルがすでに正解を見た行」で学習すると、間接的に正解が漏れ込むデータリークになる。K-Foldで「その行を学習に使っていないモデル」の予測だけを集めるOOF予測を使うことで、メタモデルは本番のtestデータに対する状況と同じ厳しさの情報から学習できる。
キーワード直感的な説明
Stacking(スタッキング)複数のベースモデルの予測確率を新しい特徴量として束ね、メタモデル(多くはシンプルな線形モデル)にもう一段学習させるアンサンブル手法
OOF予測(Out-of-Fold prediction)K-Foldの各Foldについて、そのFoldを学習に使わなかったモデルで予測を作ること。メタモデルの学習にリークなしで使える予測を作る標準手法
メタモデル / Level-2モデルベースモデル(Level-1)の予測を入力として学習する、1段上のモデル
アンサンブルの多様性(diversity)個々のモデルの精度が高いことよりも、モデル同士が「違う間違い方をする」ことの方がアンサンブルの改善に効くという考え方

🗂️ Day136-139 総括(この4日間で何を測ってきたか)

Day手法効果量(delta)シード間stddelta/std判定
136Pseudo Labeling(単発・閾値0.9)状況により+/-——閾値が高すぎると疑似ラベル枯渇、低すぎると誤ラベル混入
137Iterative Pseudo Labeling段階により変動——confirmation biasで誤りが増幅されることがある
138ノイズTTA(scale 0.10-0.30)+0.0033〜+0.00370.0024〜0.0045約0.9倍言い切れない
139マスキングTTA(ratio 0.05-0.50)-0.0033〜-0.11000.0022〜0.00862.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:]
A1
整理タスク: 「delta/stdが何倍を超えたら採用/不採用を決めてよいか」という判断ラインを決め、3手法それぞれについて採用条件・見送り条件を1行で書く
B2
ベースモデル4種を個別評価: X_labeledで学習し単体test精度を記録
B3
OOF予測関数の実装: get_oof_proba(model, X, y, skf, n_classes)
B4
leak-free stacking: OOF予測でメタモデルを学習しtest精度を測る
B5
leakyバージョン: OOFを使わず学習データ自身へのpredict_probaでメタモデルを学習。見かけの学習精度とtest精度の両方をleak-freeと比較
B6
単純平均アンサンブル: 4モデル等重み平均のtest精度をstackingと比較
B7
組み合わせ総当たり: 2〜4モデルの全組み合わせで単純平均を計算し「増やせば必ず良くなるか」を確認
B8
(考察): Stackingは単純平均より優れていたか、leakyバージョンはどんな失敗の仕方をしたかを150字程度で結論づける

💡 ヒント

ヒント1方向性

整理タスクは「効果量がstdの約1倍なら判断保留、3倍を超えたら行動を変える」のような、自分なりの閾値ルールを言語化する練習である。唯一の正解はない。実装タスクは、これまでのTTA実装(拡張サンプルを作りpredict_probaを平均する)とは構造が違うことに注意する。今日は「行ごとに使うモデルを変える」という発想が新しく登場する。

ヒント2アプローチ
  • get_oof_probaはskf.split(X, y)の各(train_idx, val_idx)ペアごとにtrain_idxだけでモデルを学習し、val_idxへのpredict_probaを出力配列のval_idx位置に書き込む
  • 毎Foldsklearn.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を比べる
ヒント3コード骨格
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   ← 全モデル使用が最良
🧭
タスクB-8: 考察 — Stacking(leak-free、delta +0.0300)は単純平均アンサンブル(delta +0.0317)をわずかに下回った。原因として最も可能性が高いのは、メタモデルの学習に使えるラベル付きデータが300件(OOFなので各Foldの学習は約240件)しかなく、20次元(5クラス×4モデル)の入力から最適な重み付けを学習するには心もとない量だという点である。単純平均は学習パラメータが0個であるため、この程度のデータ量ではむしろロバストに働いた。一方leakyバージョンは、学習データ上の見かけの精度が1.0000という「完璧」な値になったにもかかわらず、test精度はleak-freeバージョンより低かった(0.8183 < 0.8267)。これはリークのあるメタモデルが「ベースモデルの汎化能力」ではなく「ベースモデルがその行を暗記できたかどうか」という、testデータには存在しない情報に適合してしまったためだと考えられる。

📊 結果の可視化

test精度の比較(単体モデル / アンサンブル手法) 0.75 .7967rf単体 .8117et単体 .7633knn単体 .7667logreg単体 .8283単純平均 .8267stacking(leak-free) .8183stacking(leaky) 見かけ学習acc1.0000↑ → leakyのtest acc(.8183)はleak-free(.8267)を下回るのに、学習データ上は1.0000という「完璧」な見かけになる

モデルの組み合わせ別・単純平均アンサンブルのtest acc(全11通り):

rf+et
.8050 (+.0083)
rf+knn
.8100 (+.0133)
rf+logreg
.7783 (-.0183)
et+knn
.8100 (+.0133)
et+logreg
.7833 (-.0133)
knn+logreg
.8017 (+.0050)
rf+et+knn
.8233 (+.0267)
rf+et+logreg
.7933 (-.0033)
rf+knn+logreg
.8100 (+.0133)
et+knn+logreg
.8133 (+.0167)
rf+et+knn+logreg
.8283 (+.0317)

■ 赤=単体rf(0.7967)より悪化 / 黄=改善だがごく僅か / 青=改善 / 緑=最良。2モデル構成でrf+logreg・et+logregだけが悪化し、4モデル全部が最良になっている。

設定test acc単一予測比備考
単体RF(baseline)0.7967—Day136-139と同一条件
単体ET0.8117+0.01504モデル中で単体最良
単純平均(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
📐
なぜこうするか: OOFの各Foldは学習に使えるデータが約240件(300件の4/5)しかなく、本番の300件全部を使った学習より条件が厳しい。この「学習データが少ない状態での予測」は、実は本番のtestデータに対する予測条件(X_labeled全体で学習してから未知のX_testを予測する)とほぼ同じ厳しさであり、だからこそOOF予測はメタモデルの学習に使うのに適している。

2なぜleakyバージョンは学習データ上「完璧」なのに、testでは負けるのか

# leaky:     学習データ上の見かけの精度 = 1.0000    test acc = 0.8183
# leak-free: OOFベースの学習acc = 0.8267           test acc = 0.8267(学習acc ≈ test acc)
🎲
なぜこうするか: leakyバージョンのメタモデルへの入力は、木ベースモデルが「まさにその行を学習に使った直後の」予測確率である。決定木ベースのモデルは学習データに対してほぼ完璧に近い予測をしがちなので、メタモデルは「rfの予測確率がほぼ1.0に近い列を見れば正解が分かる」という、testデータでは絶対に再現されない near-tautology(ほぼ同語反復)を学習してしまう。leak-freeバージョンでは学習acc(0.8267)とtest acc(0.8267)がほぼ一致しており、これはメタモデルが学習中に見ていた精度が本番の精度をちゃんと予言できている健全な状態を示す。

3なぜ「モデルを増やせば増やすほど良くなる」わけではないのか

# rf単体: 0.7967   logreg単体: 0.7667
# rf+logreg: 0.7783(悪化)   et+logreg: 0.7833(悪化)
# rf+et+knn+logreg: 0.8283(4モデルの中では最良)
🔍
なぜこうするか: 2モデルの組み合わせではrf+logreg・et+logregが単体RFより悪化する一方、4モデル全部を使うと最良になる。これはlogregが単体で見せる間違い方が木ベースモデル(rf/et)の間違い方と部分的に重なっており、2モデルだけの平均だと「多数派」が間違った方向に引っ張られやすいためだと考えられる。knnが加わることで、rf・et・logregのどの間違いパターンとも異なる「第4の視点」が入り、平均がようやく安定する。「精度が高いモデルを足す」よりも「違う間違い方をするモデルを足す」ことの方が効くことがある、という多様性の原則を実測の逆転現象として確認できる。

🧮 数学・統計の補足(文系向け)

🏫
「OOF vs リーク」を、模擬試験の採点者選びの例えで理解する:

ある生徒(ベースモデル)の実力を測るとき、「その生徒に教えた先生自身が採点する」(リークあり)と、先生は自分が教えた通りの答案に高得点をつけがちです。一方「その生徒を教えていない別クラスの先生が採点する」(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テーマ)

1-5
SOTA手法追跡・論文実装(完了)
6-10
カスタムアーキテクチャ設計(完了)
11-15
高度なアンサンブル(本日5/5・完了。Pseudo Labeling→TTA→Stacking)
16-20
コンペ戦略・Gold 3枚達成プラン(次回開始)

Day136-137でPseudo Labeling、Day138-139でノイズ/マスキングTTA、Day140でStacking(OOF)を実装。Phase6テーマ3が全5日で完了し、次はコンペ戦略フェーズへ。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: