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

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

言語 JA

ランダムフォレスト

入門

02.4.1

ランダムフォレスト

最終更新 2026-11-01 読了時間 6 分
まとめ
ランダムフォレストは、訓練データを重複ありで引き直したブートストラップ標本ごとに決定木を 1 本ずつ作り、分類なら多数決、回帰なら平均で予測を合算する手法です。バギングとの違いは、各ノードの分割で全特徴量ではなく \(\sqrt{p}\) 個程度をランダムに選ぶ点で、これにより木どうしの相関 \(\rho\) が下がり、分散 \(\rho\sigma^2 + (1-\rho)\sigma^2/B\) の両方の項が縮みます。1 本の木は訓練データの偶然に引っぱられてでこぼこな境界を引きますが、50 本の多数決にすると滑らかになり、RandomForestClassifier の例では各木の ROC-AUC が 0.6〜0.7 でも全体は 0.81 に届きます。このページでは木 1 本ずつと森全体の性能比較、2 種類の特徴量重要度、OOB スコアとテスト精度の一致を順に確かめます。

直感 #

クイズ番組で 1 人の回答者に全問任せるより、50 人の観客に多数決させるほうが当たりやすいことがあります。観客 1 人 1 人は知識に偏りがあって間違えますが、偏り方がばらばらなら多数決で打ち消されるからです。ランダムフォレストは決定木でこれをやります。ただし同じデータで同じ木を 50 本作っても全員が同じ答えになるので、2 つの仕掛けで木をわざとばらつかせます。1 つはブートストラップ標本で、訓練データから重複ありで同じ件数を引き直すと、約 37% のデータが入れ替わります。もう 1 つは特徴量のランダム選択で、各ノードの分割に 20 特徴のうち 4〜5 個しか使わせないため、「いつも同じ列で割る」木ばかりにはなりません。この 2 つで木どうしの相関を下げると、多数決の分散が下がります。

何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:1 本ごとの境界はでこぼこでも、多数決にすると滑らかになる。標本を変えるから木がばらつく。

式で見ると分散はどこまで下がるか #

特徴量のランダムサブセット #

ランダムフォレストはバギングに加えて、各ノード分割時に $p$ 個の特徴量から $m$ 個をランダムに選びます。scikit-learn の既定値は分類で $m = \lfloor\sqrt{p}\rfloor$(max_features="sqrt")、回帰で $m = p$(max_features=1.0)で、Breiman の原論文は回帰に $m = \lfloor p/3 \rfloor$ を推奨しています。下のコードは 20 特徴の分類なので、各ノードで候補になる列は $\lfloor\sqrt{20}\rfloor = 4$ 個です。

これにより木同士の相関 $\rho$ が下がり、バギングの分散低減効果が強化されます:

$$\text{Var}(\bar{h}) = \rho \sigma^2 + \frac{1 - \rho}{B}\sigma^2$$

第 2 項は木の本数 $B$ を増やせば消えますが、第 1 項 $\rho\sigma^2$ は $B$ を増やしても残ります。ステップ再生で見た「標本を変えるから木がばらつく」がバギングによる第 2 項の削減で、特徴量のランダム選択はさらに $\rho$ 自体を下げて第 1 項を縮めます。これがランダムフォレストが単純なバギングより安定する理由です。

変数重要度 #

MDI(Mean Decrease in Impurity / Gini Importance):

$$\text{MDI}(j) = \sum_{\text{tree } b} \sum_{\text{node } t \text{ splits on } j} p(t) \cdot \Delta i(t)$$

$p(t)$ はノード $t$ に到達するサンプルの割合、$\Delta i(t)$ は分割による不純度の減少量です。

MDA(Mean Decrease in Accuracy / Permutation Importance):

$$\text{MDA}(j) = \frac{1}{B}\sum_{b=1}^{B} \left[\text{Err}_{b}^{(j\text{-permuted})} - \text{Err}_{b}^{(\text{OOB})}\right]$$

特徴量$j$の値をランダムに置換したときのOOB誤差増加量を測定します。MDI は学習中に計算できて速い反面、連続値や水準数の多いカテゴリ列を過大評価します。MDA はその偏りが少なく、推奨されることが多い手法で、下のコードでは permutation_importance がこれに当たります。


Python で確かめる #

以下では 20 特徴・2500 サンプルの人工データに深さ 3 の木を 50 本持つ RandomForestClassifier を当て、まず森全体の ROC-AUC を出し、次に木を 1 本ずつ取り出して性能を比べ、2 種類の特徴量重要度を描き、最後に OOB スコアがテスト精度の代わりになるかを確かめます。

1
2
import numpy as np
import matplotlib.pyplot as plt

ランダムフォレストを学習 #

20 特徴のうち 10 個だけがラベルに関係するデータを作り、3 分の 1 をテスト用に分けてから、深さ 3 の木 50 本の森を学習します。

ROC-AUCについては、ROC-AUCにプロットの仕方について説明を載せています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

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
)

model = RandomForestClassifier(
    n_estimators=50, max_depth=3, random_state=777, bootstrap=True, oob_score=True
)
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.814573097628059

深さ 3 の木 50 本で ROC-AUC は約 0.81 でした。次の節で、この森に入っている 1 本 1 本の木がどれくらいの性能なのかを取り出して比べます。

ランダムフォレストに含まれる各木の性能を確認する #

model.estimators_ には学習済みの 50 本の DecisionTreeClassifier が入っています。最初の 10 本を 1 本ずつテストデータに当てて ROC-AUC を出し、森全体の 0.81 と棒グラフで並べます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

estimator_scores = []
for i in range(10):
    estimator = model.estimators_[i]
    estimator_pred = estimator.predict(X_test)
    estimator_scores.append(roc_auc_score(y_test, estimator_pred))

plt.figure(figsize=(10, 4))
bar_index = [i for i in range(len(estimator_scores))]
plt.bar(bar_index, estimator_scores)
plt.bar([10], rf_score)
plt.xticks(bar_index + [10], bar_index + ["RF"])
plt.xlabel("木のインデックス")
plt.ylabel("ROC-AUC")
plt.show()

png

1 本 1 本の木は ROC-AUC が 0.6〜0.7 台で、どれも森全体の 0.81 に届きません。深さ 3 で特徴量を 4 列しか見ない木は単独では弱いものの、間違え方がばらばらなので、多数決にすると 1 本では出ない性能になります。これが分散の式の第 2 項 $(1-\rho)\sigma^2/B$ が縮んだ結果です。

特徴の重要度 #

不純度(impurity)に基づいた重要度 #

feature_importances_ は本文の MDI で、各列で分割したときの不純度(ジニ係数)の減少量を 50 本分合計して正規化した値です。fit と同時に計算されるので追加の計算は不要です。

1
2
3
4
5
6
plt.figure(figsize=(10, 4))
feature_index = [i for i in range(n_features)]
plt.bar(feature_index, model.feature_importances_)
plt.xlabel("特徴のインデックス")
plt.ylabel("特徴の重要度")
plt.show()

png

permutation importance #

本文の MDA に当たる方法です。学習済みの森をそのまま使い、特徴量 $j$ の列だけをシャッフルして予測精度がどれだけ落ちるかを測ります。精度がほとんど落ちなければ、その列はモデルが頼っていない列です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.inspection import permutation_importance

p_imp = permutation_importance(
    model, X_train, y_train, n_repeats=10, random_state=77
).importances_mean

plt.figure(figsize=(10, 4))
plt.bar(feature_index, p_imp)
plt.xlabel("特徴のインデックス")
plt.ylabel("特徴の重要度")
plt.show()

png

ランダムフォレストに含まれる各木を出力する #

多数決の中身を見るために、森の最初の 10 本を Graphviz で描きます。50 本のうち 10 本でも、ルートで使われる列や閾値が木ごとに違うことが確認できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from sklearn.tree import export_graphviz
from subprocess import call
from IPython.display import Image
from IPython.display import display

for i in range(10):
    try:
        estimator = model.estimators_[i]
        export_graphviz(
            estimator,
            out_file=f"tree{i}.dot",
            feature_names=[f"x{i}" for i in range(n_features)],
            class_names=["A", "B"],
            proportion=True,
            filled=True,
        )

        call(["dot", "-Tpng", f"tree{i}.dot", "-o", f"tree{i}.png", "-Gdpi=500"])
        display(Image(filename=f"tree{i}.png"))
    except KeyboardInterrupt:
        # TODO: jupyter bookビルド時に出力に失敗するため一時的に例外処理を挟む
        pass

png

png

png

png

png

png

png

png

png

png

10 枚の木を見比べると、ルートの分割に使われる列が x3 だったり x7 だったりと木ごとに違います。これは各ノードで 20 列のうち 4 列しか候補にならないためで、もし全列を候補にすると、ほとんどの木が同じ「最も効く列」でルートを割り、似た木ばかりになります。

OOB(out-of-bag)Score #

ブートストラップで引かれなかった約 37% のサンプルは、その木にとって未知のデータです。各サンプルを「自分を使わなかった木」だけで予測して精度を測ったのが OOB スコアで、テストデータを別に用意しなくても汎化性能の目安になります。ここでは乱数シードと木の深さ(3 か 4)を変えた 10 通りの森で、OOB の Accuracy とテストデータの Accuracy が近い値になることを確かめます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from sklearn.metrics import accuracy_score

for i in range(10):
    model_i = RandomForestClassifier(
        n_estimators=50,
        max_depth=3 + i % 2,
        random_state=i,
        bootstrap=True,
        oob_score=True,
    )
    model_i.fit(X_train, y_train)
    y_pred = model_i.predict(X_test)
    oob_score = model_i.oob_score_
    test_score = accuracy_score(y_test, y_pred)
    print(f"OOBでの検証結果={oob_score} テストデータでの検証結果={test_score}")
シード imax_depthOOB での Accuracyテストデータでの Accuracy
030.78690.8121
140.81010.8364
230.78870.8024
340.81610.8315
430.79100.8073
540.81010.8339
630.78150.8133
740.80600.8339
830.78330.7952
940.80840.8388

10 通りすべてで OOB とテストの差は 0.01〜0.03 に収まり、深さ 4 の森のほうが両方とも高い、という傾向も OOB だけで読み取れます。テストデータを割けないほどデータが少ないときは、oob_score=True を交差検証の代わりに使えます。

不純度ベースの特徴量重要度は、カーディナリティの高い特徴量(カテゴリ数が多い変数や連続値)を過大評価する傾向があります。正確な重要度を得るにはpermutation_importanceを併用してください。

n_estimatorsは大きくしても過学習しにくいため、計算リソースが許す限り増やすのが基本です。oob_score=Trueを設定するとホールドアウトなしで汎化性能を推定でき、交差検証の代わりに使えます。

まとめ #

ランダムフォレストは、ブートストラップ標本と各ノードでの特徴量のランダム選択(分類では $\sqrt{p}$ 個)で木どうしの相関 $\rho$ を下げ、多数決の分散 $\rho\sigma^2 + (1-\rho)\sigma^2/B$ を両方の項から縮める手法です。実験では ROC-AUC が 0.6〜0.7 台の木 50 本の多数決が 0.81 に届き、1 本ずつは弱くても集めれば強くなることを確認しました。n_estimators は増やしても過学習しにくいので計算時間が許す限り大きくし、木の深さや max_features で個々の木の強さを調整します。特徴量重要度は学習と同時に出る feature_importances_(MDI)と、列をシャッフルして精度の低下を測る permutation_importance(MDA)の 2 種類があり、連続値や水準の多い列を過大評価しにくい後者を本番の判断に使います。oob_score=True の OOB スコアはテスト精度と 0.01〜0.03 の差で一致したので、データが少ないときは交差検証の代わりになります。