K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

スタッキング

上級

02.4.2

スタッキング

最終更新 2026-11-01 読了時間 6 分
まとめ
スタッキングは、ロジスティック回帰・決定木・SVM のような性質の異なるモデル(ベース学習器)\(h_1, \dots, h_K\) にまず同じ問題を解かせ、その \(K\) 個の予測を新しい特徴量として、2 段目のメタ学習器 \(g\) が最終予測を学習する手法です。ベース学習器が自分の訓練データを予測した値をそのままメタ学習器に渡すと答えが漏れるので、交差検証と同じ要領で「自分を含まない Fold で学習したモデルの予測」(Out-of-Fold 予測)を使ってメタ特徴を作ります。scikit-learn では StackingClassifier(estimators, final_estimator) がこの手順を内部で行います。このページでは深さ 3〜6 の決定木 4 本を重ねた例で、似たモデルばかりでは ROC-AUC がランダムフォレストの 0.86 に対して 0.74 と伸びないことを確かめ、「異なる種類のモデルを混ぜる」というスタッキングの要点を図で示します。

スタッキングは Out-of-Fold 予測を使うので、交差検証 の「Fold を 1 つ外して学習し、外した Fold を予測する」流れを先に押さえてください。あわせて、ロジスティック回帰・決定木・SVM など複数の分類器の基本的な性質の違いを知っていると、どのモデルを 1 段目に並べるべきかが読めるようになります。

直感 #

病院で、内科・外科・放射線科の 3 人の医師がそれぞれ診断を出し、最後に総合診療医がその 3 つの診断を見て結論を出す。スタッキングはこの「診断の診断」です。1 段目の 3 人がベース学習器、総合診療医がメタ学習器で、メタ学習器は「放射線科の意見が外科と食い違うときは放射線科を信じる」といった重み付けをデータから学びます。ここで 1 つ落とし穴があります。総合診療医の訓練に「3 人が答えを知っている患者の診断」を使うと、3 人は正解を丸写しできるので、総合診療医は「全員の意見を信じればよい」と間違って学んでしまいます。そこで 1 段目のモデルには自分が学習に使っていないデータだけを予測させ(Out-of-Fold 予測)、その予測で 2 段目を訓練します。

式で見ると 2 段目は何を学ぶか #

2層アーキテクチャ #

スタッキングは 2 層のモデル構造です。1 段目は上の例の 3 人の医師、2 段目が総合診療医に当たります:

Level-0(ベース学習器): $K$ 個の多様なモデル $h_1, h_2, \dots, h_K$ を訓練データで学習する。

Level-1(メタ学習器): ベース学習器の予測を特徴量として受け取り、最終予測を行うモデル $g$ を学習する。

$$\hat{y} = g\!\left(h_1(x),\; h_2(x),\; \dots,\; h_K(x)\right)$$

Out-of-Fold 予測(リーク防止) #

メタ学習器の訓練データを作る際、ベース学習器が自身の訓練データをそのまま予測するとリーク(情報漏洩)が起きます。深さ 6 の決定木なら訓練データの正解率は 90% を超えるので、メタ学習器は「この木を信じればよい」と学び、テストデータで裏切られます。これを防ぐために Out-of-Fold (OOF) 予測を使います。まず訓練データを $J$ 個の Fold に分割します。次に Fold $j$ を除いた残りでベース学習器 $h_k$ を学習し、除いた Fold $j$ のデータを予測してメタ特徴 $z_{ij}^{(k)}$ とします。これを $j = 1, \dots, J$ で繰り返し、最後に全 Fold の予測を結合してメタ特徴行列 $Z \in \mathbb{R}^{N \times K}$ を構成します。$N$ 件のどのサンプルも、自分を学習に使っていないモデルで予測されている点が要です。

$$Z = \begin{pmatrix} z_{11} & z_{12} & \cdots & z_{1K} \\ z_{21} & z_{22} & \cdots & z_{2K} \\ \vdots & \vdots & \ddots & \vdots \\ z_{N1} & z_{N2} & \cdots & z_{NK} \end{pmatrix}$$

メタ学習器はこの $Z$ と元のラベル $y$ を使って学習します: $g: Z \to y$。$Z$ の各行は「$K$ 個のモデルがこのサンプルをどう予測したか」で、下のコードでは $K = 4$ 本の決定木の予測ラベルが 4 列になります。scikit-learn の StackingClassifier は既定で cv=5 の 5-Fold でこの $Z$ を作り、その後ベース学習器を全訓練データで学習し直して予測時に使います。


Python で確かめる #

以下では 20 特徴・2500 サンプルの人工データで、まず深さ 5 の木 50 本のランダムフォレストを基準にし、次に深さ 3〜6 の決定木 4 本を StackingClassifier で重ねて ROC-AUC を比べます。その後メタ学習器の木と重要度を描き、「似たモデルを重ねても 1 本の強いモデルに頼るだけになる」ことを目で確認します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import matplotlib.pyplot as plt

from sklearn.datasets import make_classification
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import StackingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from sklearn.tree import export_graphviz
from subprocess import call

実験用のデータを作成 #

20 特徴のうち 10 個だけがラベルに関係し、各クラスが 4 つの塊に分かれる 2500 件のデータを作り、3 分の 1 をテスト用に分けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 特徴が20あるデータを作成
n_features = 20
X, y = make_classification(
    n_samples=2500,
    n_features=n_features,
    n_informative=10,
    n_classes=2,
    n_redundant=0,
    n_clusters_per_class=4,
    random_state=777,
)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=777
)

データを複数の特徴に関してプロットしてみる #

20 特徴のうち 2 つずつを 4 組選んで散布図にし、1 本の直線や単純なルールでは 2 クラスを分けられないデータであることを確認します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
plt.figure(figsize=(10, 10))
plt.subplot(2, 2, 1)
plt.scatter(X[:, 2], X[:, 7], c=y)
plt.xlabel("x2")
plt.ylabel("x7")
plt.subplot(2, 2, 2)
plt.scatter(X[:, 4], X[:, 9], c=y)
plt.xlabel("x4")
plt.ylabel("x9")
plt.subplot(2, 2, 3)
plt.scatter(X[:, 5], X[:, 1], c=y)
plt.xlabel("x5")
plt.ylabel("x1")
plt.subplot(2, 2, 4)
plt.scatter(X[:, 1], X[:, 3], c=y)
plt.xlabel("x1")
plt.ylabel("x3")
plt.show()

png

4 枚どれも 2 色の点が重なり合っていて、1 本の直線で分けられる組はありません。各クラスが 4 つの塊に分かれているので、2 次元の切り口だけでは塊の境目が見えないためです。

スタッキングとランダムフォレストの比較 #

ランダムフォレストで分類してみた場合 #

比較の基準として、深さ 5 の木 50 本のランダムフォレストでテストデータの ROC-AUC を出します。

1
2
3
4
5
model = RandomForestClassifier(n_estimators=50, max_depth=5, random_state=777)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
rf_score = roc_auc_score(y_test, y_pred)
print(f"ROC-AUC = {rf_score}")
ROC-AUC = 0.855797033310609

複数の木でスタッキングした場合 #

1 段目に深さ 3、4、5、6 の決定木を 4 本並べ、2 段目にも深さ 3 の決定木を置いた StackingClassifier を学習します。DecisionTreeClassifier だけを重ねた場合、ランダムフォレストの 0.86 に対してどこまで届くかを見ます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# スタッキング前段に使用するモデル
estimators = [
    ("dt1", DecisionTreeClassifier(max_depth=3, random_state=777)),
    ("dt2", DecisionTreeClassifier(max_depth=4, random_state=777)),
    ("dt3", DecisionTreeClassifier(max_depth=5, random_state=777)),
    ("dt4", DecisionTreeClassifier(max_depth=6, random_state=777)),
]
# スタッキングに含まれるモデル数
n_estimators = len(estimators)
# スタッキング後段に使用するモデル
final_estimator = DecisionTreeClassifier(max_depth=3, random_state=777)
# スタッキングモデルを作成し訓練する
clf = StackingClassifier(estimators=estimators, final_estimator=final_estimator)
clf.fit(X_train, y_train)
# テストデータで評価する
y_pred = clf.predict(X_test)
clf_score = roc_auc_score(y_test, y_pred)

print("ROC-AUC")
print(f"決定木のスタッキング={clf_score}, ランダムフォレスト={rf_score}")
ROC-AUC
決定木のスタッキング=0.7359716965608031, ランダムフォレスト=0.855797033310609

スタッキングは 0.74 で、ランダムフォレストの 0.86 に大きく負けています。1 段目の 4 本が「同じデータ・同じ乱数で深さだけ違う木」なので予測がほぼ同じになり、メタ学習器に新しい情報が渡らないためです。次の 2 つの図でその中身を確認します。

スタッキングに使用した木を可視化してみる #

メタ学習器(2 段目の決定木)を Graphviz で描き、4 本のベース学習器のうちどの予測で分岐しているかを見ます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export_graphviz(
    clf.final_estimator_,
    out_file="tree_final_estimator.dot",
    class_names=["A", "B"],
    feature_names=[e[0] for e in estimators],
    proportion=True,
    filled=True,
)

call(
    [
        "dot",
        "-Tpng",
        "tree_final_estimator.dot",
        "-o",
        f"tree_final_estimator.png",
        "-Gdpi=200",
    ]
)
display(Image(filename="tree_final_estimator.png"))

png

スタッキングで使用した木の特徴重要度を見る #

メタ学習器の feature_importances_ を見ると、4 本のベース学習器のうちどれの予測が最終判断に使われたかが分かります。結果は 4 番目の木 dt4 に集中し、他の 3 本はほとんど使われていません。

1
2
3
4
5
6
7
plt.figure(figsize=(6, 3))
plot_index = [i for i in range(n_estimators)]
plt.bar(plot_index, clf.final_estimator_.feature_importances_)
plt.xticks(plot_index, [e[0] for e in estimators])
plt.xlabel("model name")
plt.ylabel("feature-importance")
plt.show()

png

スタッキング前段の各木の性能を確認する #

最後に 1 段目の 4 本をテストデータに 1 本ずつ当て、どの木が最も強かったかを ROC-AUC で確かめます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# スタッキング前段の木の精度を計測する
scores = []
for clf_estim in clf.estimators_:
    print("====")
    y_pred = clf_estim.predict(X_test)
    scr = roc_auc_score(y_test, y_pred)
    scores.append(scr)
    print(clf_estim)
    print(scr)

n_estimators = len(estimators)
plot_index = [i for i in range(n_estimators)]

# グラフ作成
plt.figure(figsize=(8, 4))
plt.bar(plot_index, scores)
plt.xticks(plot_index, [e[0] for e in estimators])
plt.xlabel("model name")
plt.ylabel("roc-auc")
plt.show()
====
DecisionTreeClassifier(max_depth=3, random_state=777)
0.7660117774277722
====
DecisionTreeClassifier(max_depth=4, random_state=777)
0.7744128916993818
====
DecisionTreeClassifier(max_depth=5, random_state=777)
0.8000158677919086
====
DecisionTreeClassifier(max_depth=6, random_state=777)
0.8084639977432473

png

4 本の ROC-AUC は深さの順に 0.77、0.77、0.80、0.81 で、最も深い dt4 が最も強い木でした。メタ学習器は OOF 予測の上で「dt4 を信じる」と学びました。それでもテストでは dt4 単体(0.81)より低い 0.74 に落ちています。4 本の予測がほぼ同じ情報しか持たないため、2 段目の木は雑音に合わせた分岐を作っただけだった、というのがこの実験の結論です。

使いどころと落とし穴 #

スタッキングが効くのは、1 段目に性質の異なるモデルを並べたときです。たとえばロジスティック回帰(線形の境界)、k 近傍法(局所的な境界)、勾配ブースティング(軸に平行な分割の重ね合わせ)のように、間違え方が違うモデルを混ぜると、メタ学習器が「このモデルはこの領域で当たる」という補完関係を学べます。同じ決定木を深さだけ変えて 4 本並べた上の実験は、その逆の例です。2 段目には過学習しにくいロジスティック回帰を置くのが定番で、StackingClassifier(..., final_estimator=LogisticRegression()) と書けます。学習コストはベース学習器の数 × Fold 数(既定 5)になるので、4 本なら 20 回の学習が走ることも覚えておいてください。

まとめ #

スタッキングは、$K$ 個のベース学習器の予測 $h_1(x), \dots, h_K(x)$ を特徴量にして、メタ学習器 $g$ が最終予測を学ぶ 2 層の手法です。要はリーク対策です。メタ学習器の訓練には、各サンプルを「自分を学習に使っていない Fold のモデル」で予測した Out-of-Fold 予測を使い、StackingClassifier は cv=5 でこれを自動で行います。深さ 3〜6 の決定木 4 本を重ねた実験では、予測が似すぎていてメタ学習器が dt4 だけに頼り、ROC-AUC は 0.74 とランダムフォレストの 0.86 に届きませんでした。効かせる条件は「間違え方の違うモデルを 1 段目に並べる」ことです。同種のモデルを並列に平均したいだけなら、バギング や ランダムフォレスト のほうが簡単で強いです。