ランダムフォレスト(Random Forest)は、Breiman(2001)により提案された、バギングに「特徴量のランダム選択」を追加したアンサンブル手法です。バギングの記事で導出したように、モデル間の相関 $\rho$ を下げることが分散削減の鍵でした。ランダムフォレストは、各決定木の分岐点で使用する特徴量をランダムに制限することで、この相関 $\rho$ を効果的に低減します。
本記事では、ランダムフォレストの理論的基盤を数式で丁寧に導出します。特に、特徴量ランダム選択が相関を下げるメカニズム、Breimanの汎化誤差上界の定理、変数重要度の2つの指標(MDIとMDA)、そしてProximity行列による異常値検出について解説します。最後にPythonでスクラッチ実装し、scikit-learnとの比較を行います。
本記事の内容
- ランダムフォレスト = バギング + 特徴量ランダム選択
- 特徴量ランダム選択が相関 $\rho$ を下げる数学的説明
- 推奨特徴量数の根拠(分類: $\lfloor\sqrt{p}\rfloor$、回帰: $\lfloor p/3 \rfloor$)
- Breimanの汎化誤差上界の定理
- 変数重要度(MDI: Mean Decrease Impurity, MDA: Mean Decrease Accuracy)
- Proximity行列と異常値検出
- Pythonによるスクラッチ実装とscikit-learnとの比較
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
ランダムフォレストのアルゴリズム
バギング + 特徴量ランダム選択
ランダムフォレストは以下の手順で構築されます。
学習アルゴリズム:
$b = 1, 2, \dots, B$ について:
- 訓練データ $\mathcal{D}$ からブートストラップサンプル $\mathcal{D}_b^*$ を生成($n$ 個の復元抽出)
- $\mathcal{D}_b^*$ を用いて決定木 $T_b$ を学習。ただし、各ノードの分岐点を決める際に: – 全 $p$ 個の特徴量からランダムに $m$ 個を選択($m < p$) - 選択された $m$ 個の特徴量の中から最良の分岐を決定
- 枝刈りなしで最大まで成長させる
予測:
- 回帰: $\hat{f}_{\text{RF}}(\bm{x}) = \frac{1}{B}\sum_{b=1}^{B} T_b(\bm{x})$
- 分類: $\hat{y}_{\text{RF}}(\bm{x}) = \text{majority vote}(T_1(\bm{x}), \dots, T_B(\bm{x}))$
バギングとの違いは、ステップ2の「$m$ 個のランダム選択」のみです。$m = p$ とすれば通常のバギングになります。
ランダム性の2つの源泉
ランダムフォレストには2つのランダム性があります。
- ブートストラップサンプリング: 各木が異なるデータサブセットで学習(バギングと共通)
- 特徴量のランダム選択: 各ノードの分岐で異なる特徴量サブセットを使用(ランダムフォレスト固有)
この2つのランダム性が組み合わさることで、木同士の相関が大幅に低下します。
特徴量ランダム選択が相関を下げるメカニズム
直感的理解
全特徴量を使ったバギングでは、各木は同じデータの異なるサブセットで学習しますが、支配的に重要な特徴量(例: 特徴量 $X_j$)がある場合、ほぼ全ての木でその特徴量が根ノード付近の分岐に使われます。これにより、木の構造が類似し、予測値も強く相関します。
特徴量ランダム選択では、各ノードで $m$ 個しか使えないため、支配的な特徴量が必ずしも利用可能とは限りません。その結果、異なる特徴量で分岐が行われる頻度が増え、木の構造が多様化します。
数学的分析
$p$ 個の特徴量から $m$ 個を選択するとき、特定の特徴量 $X_j$ が選ばれる確率は
$$ P(X_j \text{ が選ばれる}) = 1 – \frac{\binom{p-1}{m}}{\binom{p}{m}} = 1 – \frac{(p-m)}{p} = \frac{m}{p} $$
この計算を確認します。$\binom{p}{m}$ は $p$ 個から $m$ 個を選ぶ組合せ数です。$X_j$ が選ばれない場合は、残り $p-1$ 個から $m$ 個を選ぶので $\binom{p-1}{m}$ 通りです。
$$ \frac{\binom{p-1}{m}}{\binom{p}{m}} = \frac{(p-1)!/(m!(p-1-m)!)}{p!/(m!(p-m)!)} = \frac{(p-1)! \cdot (p-m)!}{p! \cdot (p-1-m)!} = \frac{p-m}{p} $$
したがって、
$$ P(X_j \text{ が選ばれる}) = 1 – \frac{p-m}{p} = \frac{m}{p} $$
$m = \sqrt{p}$ の場合、$P = \sqrt{p}/p = 1/\sqrt{p}$ となり、$p$ が大きいほど各特徴量が選ばれる確率が低くなります。
2つの木の根ノードが同じ特徴量で分岐する確率
最も重要な特徴量 $X_j$ が2つの独立した木の根ノードで同時に使われる確率は次のように計算されます。
バギング($m = p$)の場合: 常に全特徴量が利用可能なので、両方の木で $X_j$ が最良であれば $P \approx 1$(データのばらつきで多少変動)。
ランダムフォレスト($m < p$)の場合: 両方の木で $X_j$ が候補に含まれる確率は $(m/p)^2$ です。$m = \sqrt{p}$、$p = 100$ の場合、
$$ P(X_j \text{ が両方で候補に入る}) = \left(\frac{\sqrt{100}}{100}\right)^2 = \left(\frac{10}{100}\right)^2 = 0.01 $$
この大幅な確率低下が、木同士の構造の多様化、すなわち相関 $\rho$ の低減につながります。
バギング分散公式との関係
バギングの記事で導出した分散公式を再掲します。
$$ \text{Var}[\hat{f}_{\text{bag}}] = \rho \sigma_f^2 + \frac{(1-\rho)\sigma_f^2}{B} $$
特徴量ランダム選択により $\rho$ が低下すると:
- 第1項 $\rho \sigma_f^2$($B$ で削減不可能な項)が小さくなる
- 第2項は $B$ を十分大きくすれば0に近づく
ただし、$m$ を小さくしすぎると各木の予測力($\sigma_f^2$ の中のバイアス成分)が低下するため、$m$ にはトレードオフがあります。
推奨特徴量数の根拠
経験的ガイドライン
Breimanは実験的に以下の値を推奨しています。
| タスク | 推奨 $m$ | 直感的な理由 |
|---|---|---|
| 分類 | $m = \lfloor\sqrt{p}\rfloor$ | 各ノードで少ない特徴量を使うことで多様性を重視 |
| 回帰 | $m = \lfloor p/3 \rfloor$ | 回帰は分類より多くの特徴量が必要な傾向 |
理論的分析
$m$ の選択にはバイアスと分散のトレードオフがあります。
$m$ を小さくする効果: – 木同士の相関 $\rho$ が低下 $\Rightarrow$ 分散が減少 – 各木の最適な分岐を見つけにくくなる $\Rightarrow$ バイアスが増加 – 各木の分散 $\sigma_f^2$ 自体は増加する可能性がある
$m$ を大きくする効果: – 各木がより良い分岐を見つけやすい $\Rightarrow$ バイアスが減少 – 木同士が類似する $\Rightarrow$ 相関 $\rho$ が増加し、分散削減効果が薄れる
$m$ による汎化誤差の変化を定性的に表すと、
$$ \text{Error}(m) \approx \underbrace{\text{Bias}^2(m)}_{\text{$m$↓で増加}} + \underbrace{\rho(m) \sigma_f^2(m)}_{\text{$m$↓で}\ \rho \text{↓}, \sigma_f^2 \text{↑}} + \underbrace{\frac{(1-\rho(m)) \sigma_f^2(m)}{B}}_{\text{$B$大で無視可能}} + \sigma^2 $$
$m = \sqrt{p}$(分類の場合)はこのトレードオフの経験的な最適点であり、多くのデータセットで良好な結果を与えることが知られています。ただし、最適な $m$ はデータの性質に依存するため、交差検証で選択することが推奨されます。
Breimanの汎化誤差上界
分類問題の設定
入力 $\bm{x}$ とラベル $y$ の同時分布に対して、ランダムフォレスト分類器を $h(\bm{x}, \Theta)$ とします。ここで $\Theta$ はランダムフォレストの構築時のランダム性(ブートストラップ+特徴量選択)を表す確率変数です。
ランダムフォレストの最終予測は多数決で決まります。
$$ H(\bm{x}) = \arg\max_j \sum_{b=1}^{B} \mathbf{1}[h(\bm{x}, \Theta_b) = j] $$
マージン関数
クラス $j$ に対するマージン関数を以下のように定義します。
$$ \text{mg}(\bm{x}, y) = P_{\Theta}(h(\bm{x}, \Theta) = y) – \max_{j \neq y} P_{\Theta}(h(\bm{x}, \Theta) = j) $$
マージンは、正しいクラスへの投票率と、最も多い誤クラスへの投票率の差です。$\text{mg} > 0$ であれば正しく分類されます。
汎化誤差
汎化誤差(誤分類率)は、
$$ PE^* = P_{\bm{x}, y}(\text{mg}(\bm{x}, y) < 0) $$
強度(Strength)
ランダムフォレストの強度 $s$ を、マージンの期待値として定義します。
$$ s = E_{\bm{x}, y}[\text{mg}(\bm{x}, y)] $$
$s > 0$ であれば、平均的に正しく分類する力があることを意味します。
相関の定義
個々の木の予測に基づく raw margin function を定義します。
$$ \text{rmg}(\Theta, \bm{x}, y) = \mathbf{1}[h(\bm{x}, \Theta) = y] – \max_{j \neq y} \mathbf{1}[h(\bm{x}, \Theta) = j] $$
$\hat{j}(\bm{x}, y) = \arg\max_{j \neq y} P_{\Theta}(h(\bm{x}, \Theta) = j)$ として、
$$ \text{rmg}(\Theta, \bm{x}, y) = \mathbf{1}[h(\bm{x}, \Theta) = y] – \mathbf{1}[h(\bm{x}, \Theta) = \hat{j}(\bm{x}, y)] $$
この関数の $(\bm{x}, y)$ にわたる分散を $\sigma^2(\Theta)$ とし、平均相関を $\bar{\rho}$ とします。
$$ \bar{\rho} = \frac{E_{\Theta, \Theta’}[\text{Cov}_{\bm{x},y}[\text{rmg}(\Theta, \bm{x}, y), \text{rmg}(\Theta’, \bm{x}, y)]]}{E_{\Theta}[\sigma^2(\Theta)]} $$
ここで $\Theta, \Theta’$ は独立で同一の分布に従います。
Breimanの定理
以上の定義のもとで、ランダムフォレストの汎化誤差は以下の上界を満たします。
$$ \boxed{PE^* \leq \frac{\bar{\rho} (1 – s^2)}{s^2}} $$
この不等式はChebyshevの不等式から導出されます。
導出の概略:
Chebyshevの不等式から、$s > 0$ のとき、
$$ P(\text{mg}(\bm{x}, y) < 0) \leq P(\text{mg}(\bm{x}, y) - s < -s) \leq \frac{\text{Var}[\text{mg}(\bm{x}, y)]}{s^2} $$
マージンの分散は、各木の予測の相関に依存します。Breimanは以下を示しました。
$$ \text{Var}_{\bm{x},y}[\text{mg}(\bm{x}, y)] \leq \bar{\rho} E_{\Theta}[\sigma^2(\Theta)] $$
さらに、$E_{\Theta}[\sigma^2(\Theta)] \leq 1 – s^2$ が成り立ちます。これは $\text{rmg}$ の値が $\{-1, 0, 1\}$ に制限されることから導かれます。
以上を合わせて、
$$ PE^* \leq \frac{\bar{\rho} (1 – s^2)}{s^2} $$
定理の含意
この上界は2つの量で決まります。
- 平均相関 $\bar{\rho}$: 小さいほど上界が改善。特徴量ランダム選択は $\bar{\rho}$ を下げます。
- 強度 $s$: 大きいほど上界が改善。各木の精度が高いほど $s$ は大きくなります。
$m$ を小さくすると $\bar{\rho}$ は減少しますが、$s$ も減少する可能性があります。最適な $m$ は $\bar{\rho}/s^2$ を最小化する値です。
変数重要度
MDI(Mean Decrease Impurity)
Gini重要度とも呼ばれます。特徴量 $X_j$ の重要度を、全ての木の全ノードにおける不純度の減少量で定義します。
ノード $\nu$ で特徴量 $X_j$ により分岐する場合の不純度減少量は、
$$ \Delta I(\nu) = I(\nu) – \frac{n_{\text{left}}}{n_{\nu}} I(\nu_{\text{left}}) – \frac{n_{\text{right}}}{n_{\nu}} I(\nu_{\text{right}}) $$
ここで $I$ は不純度(分類ならGini不純度、回帰ならMSE)、$n_{\nu}$ はノード $\nu$ のサンプル数です。
Gini不純度は以下のように定義されます。
$$ I_G(\nu) = \sum_{k=1}^{K} \hat{p}_k (1 – \hat{p}_k) = 1 – \sum_{k=1}^{K} \hat{p}_k^2 $$
$\hat{p}_k$ はノード $\nu$ におけるクラス $k$ の割合です。
特徴量 $X_j$ のMDI重要度は、
$$ \text{MDI}(X_j) = \frac{1}{B} \sum_{b=1}^{B} \sum_{\nu \in T_b : v(s_{\nu}) = j} \frac{n_{\nu}}{n} \Delta I(\nu) $$
ここで $v(s_{\nu}) = j$ はノード $\nu$ の分岐が特徴量 $X_j$ を使用することを意味します。$n_{\nu}/n$ は各ノードの重みです。
MDIの特徴: – 計算コストが低い(学習時に同時に計算可能) – 高カーディナリティの特徴量(多くの値を取る変数)に偏りがある – カテゴリ変数では注意が必要
MDA(Mean Decrease Accuracy)
Permutation重要度とも呼ばれます。特徴量 $X_j$ の値をランダムにシャッフル(置換)したときの精度低下で重要度を定義します。
アルゴリズム:
- OOBサンプルでの正確度 $\text{Acc}_{\text{OOB}}$ を計算
- 特徴量 $X_j$ のOOB値をサンプル間でランダムに置換(permute)
- 置換後のOOBサンプルで正確度 $\text{Acc}_{\text{OOB}}^{(j)}$ を計算
- 重要度: $\text{MDA}(X_j) = \text{Acc}_{\text{OOB}} – \text{Acc}_{\text{OOB}}^{(j)}$
各木についてOOBデータで計算し、全木にわたって平均を取ります。
$$ \text{MDA}(X_j) = \frac{1}{B} \sum_{b=1}^{B} \frac{1}{|\text{OOB}_b|} \sum_{i \in \text{OOB}_b} \left[\mathbf{1}(y_i = T_b(\bm{x}_i)) – \mathbf{1}(y_i = T_b(\bm{x}_i^{(j)}))\right] $$
ここで $\bm{x}_i^{(j)}$ はサンプル $i$ の特徴量 $X_j$ を置換したものです。
MDAの特徴: – モデルに依存しない(permutation testの考え方) – カーディナリティの偏りが少ない – 相関の強い特徴量がある場合、重要度が分散する可能性がある – 計算コストはMDIより高い
Proximity行列と異常値検出
Proximity行列
ランダムフォレストのProximity行列 $\bm{P} \in \mathbb{R}^{n \times n}$ は、2つのサンプルが同じ葉ノードに落ちる頻度で定義されます。
$$ P_{ij} = \frac{1}{B} \sum_{b=1}^{B} \mathbf{1}[\text{leaf}_b(\bm{x}_i) = \text{leaf}_b(\bm{x}_j)] $$
$P_{ij} \in [0, 1]$ であり、$P_{ij}$ が大きいほど2つのサンプルが「類似」していることを意味します。$P_{ii} = 1$ です。
異常値スコア
Proximity行列を使って異常値を検出できます。サンプル $i$ (クラス $k$)の異常値スコアは、
$$ \text{outlier}(i) = \frac{n_k}{\sum_{j: y_j = k} P_{ij}^2} $$
ここで $n_k$ はクラス $k$ のサンプル数です。直感的には、他の同クラスサンプルとのProximityが小さい(= どの木でも同じ葉ノードに入らない)サンプルほど、異常値スコアが高くなります。
Pythonでの実装
スクラッチ実装
scikit-learnのDecisionTreeClassifierを基本学習器として使い、ランダムフォレストをスクラッチ実装します。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.datasets import make_classification, make_regression
from sklearn.ensemble import RandomForestClassifier as SklearnRF
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, mean_squared_error
# ----------------------------
# ランダムフォレスト分類器(スクラッチ実装)
# ----------------------------
class RandomForestClassifierScratch:
"""ランダムフォレスト分類器(DecisionTreeClassifierベース)"""
def __init__(self, n_estimators=100, max_features='sqrt',
max_depth=None, random_state=None):
self.n_estimators = n_estimators
self.max_features = max_features
self.max_depth = max_depth
self.random_state = random_state
self.estimators = []
self.feature_indices = [] # 各木で使用する特徴量インデックス
self.oob_indices = []
self.feature_importances_mdi = None
def _get_max_features(self, n_features):
"""max_featuresの数値を取得"""
if self.max_features == 'sqrt':
return max(1, int(np.sqrt(n_features)))
elif self.max_features == 'log2':
return max(1, int(np.log2(n_features)))
elif isinstance(self.max_features, int):
return self.max_features
elif isinstance(self.max_features, float):
return max(1, int(self.max_features * n_features))
else:
return n_features
def fit(self, X, y):
"""ランダムフォレストの学習"""
n_samples, n_features = X.shape
self.n_features_ = n_features
self.classes_ = np.unique(y)
m = self._get_max_features(n_features)
rng = np.random.RandomState(self.random_state)
self.estimators = []
self.feature_indices = []
self.oob_indices = []
# MDI重要度の蓄積
importances = np.zeros(n_features)
for b in range(self.n_estimators):
# ブートストラップサンプリング
bootstrap_idx = rng.choice(n_samples, size=n_samples, replace=True)
oob_idx = np.setdiff1d(np.arange(n_samples), np.unique(bootstrap_idx))
self.oob_indices.append(oob_idx)
X_boot = X[bootstrap_idx]
y_boot = y[bootstrap_idx]
# 決定木の学習(max_features で特徴量をランダム選択)
tree = DecisionTreeClassifier(
max_depth=self.max_depth,
max_features=m,
random_state=rng.randint(0, 100000)
)
tree.fit(X_boot, y_boot)
self.estimators.append(tree)
# MDI重要度の加算
importances += tree.feature_importances_
# MDI重要度の正規化
self.feature_importances_mdi = importances / self.n_estimators
return self
def predict(self, X):
"""多数決による予測"""
predictions = np.array([tree.predict(X) for tree in self.estimators])
# 各サンプルで最も多いクラスを選択
result = np.zeros(X.shape[0], dtype=self.classes_.dtype)
for i in range(X.shape[0]):
values, counts = np.unique(predictions[:, i], return_counts=True)
result[i] = values[np.argmax(counts)]
return result
def predict_proba(self, X):
"""クラス確率の推定"""
n_classes = len(self.classes_)
proba = np.zeros((X.shape[0], n_classes))
for tree in self.estimators:
tree_proba = tree.predict_proba(X)
# クラスの対応付け
for j, c in enumerate(tree.classes_):
idx = np.where(self.classes_ == c)[0][0]
proba[:, idx] += tree_proba[:, j]
proba /= self.n_estimators
return proba
def oob_score(self, X, y):
"""OOBスコア(正確度)"""
n_samples = X.shape[0]
n_classes = len(self.classes_)
oob_votes = np.zeros((n_samples, n_classes))
for b, tree in enumerate(self.estimators):
oob_idx = self.oob_indices[b]
if len(oob_idx) > 0:
tree_proba = tree.predict_proba(X[oob_idx])
for j, c in enumerate(tree.classes_):
idx = np.where(self.classes_ == c)[0][0]
oob_votes[oob_idx, idx] += tree_proba[:, j]
valid = np.sum(oob_votes, axis=1) > 0
oob_pred = self.classes_[np.argmax(oob_votes[valid], axis=1)]
return np.mean(oob_pred == y[valid])
def permutation_importance(self, X, y):
"""MDA(Permutation重要度)の計算"""
base_score = self.oob_score(X, y)
importances = np.zeros(self.n_features_)
for j in range(self.n_features_):
X_permuted = X.copy()
perm_idx = np.random.permutation(X.shape[0])
X_permuted[:, j] = X[perm_idx, j]
perm_score = self.oob_score(X_permuted, y)
importances[j] = base_score - perm_score
return importances
def proximity_matrix(self, X):
"""Proximity行列の計算"""
n_samples = X.shape[0]
prox = np.zeros((n_samples, n_samples))
for tree in self.estimators:
leaves = tree.apply(X)
for i in range(n_samples):
for j in range(i, n_samples):
if leaves[i] == leaves[j]:
prox[i, j] += 1
prox[j, i] += 1
prox /= self.n_estimators
return prox
# ----------------------------
# データ生成
# ----------------------------
np.random.seed(42)
# 分類タスク
X_cls, y_cls = make_classification(
n_samples=500, n_features=20, n_informative=10,
n_redundant=5, n_classes=2, random_state=42
)
X_train_cls, X_test_cls, y_train_cls, y_test_cls = train_test_split(
X_cls, y_cls, test_size=0.3, random_state=42
)
# ----------------------------
# 実験1: スクラッチ実装 vs scikit-learn の比較
# ----------------------------
print("=== スクラッチ実装 vs scikit-learn ===")
# スクラッチ実装
rf_scratch = RandomForestClassifierScratch(
n_estimators=100, max_features='sqrt', random_state=42
)
rf_scratch.fit(X_train_cls, y_train_cls)
y_pred_scratch = rf_scratch.predict(X_test_cls)
acc_scratch = accuracy_score(y_test_cls, y_pred_scratch)
# scikit-learn
rf_sklearn = SklearnRF(
n_estimators=100, max_features='sqrt', random_state=42
)
rf_sklearn.fit(X_train_cls, y_train_cls)
y_pred_sklearn = rf_sklearn.predict(X_test_cls)
acc_sklearn = accuracy_score(y_test_cls, y_pred_sklearn)
print(f"スクラッチ実装 正確度: {acc_scratch:.4f}")
print(f"scikit-learn 正確度: {acc_sklearn:.4f}")
# OOBスコア
oob_scratch = rf_scratch.oob_score(X_train_cls, y_train_cls)
print(f"\nスクラッチ OOBスコア: {oob_scratch:.4f}")
print(f"scikit-learn OOBスコア: {rf_sklearn.oob_score_:.4f}"
if hasattr(rf_sklearn, 'oob_score_') else "")
# ----------------------------
# 実験2: 特徴量数mの影響(相関 vs 精度のトレードオフ)
# ----------------------------
print("\n=== 特徴量数 m の影響 ===")
m_values = [1, 2, 3, 4, 5, 7, 10, 15, 20]
accuracies = []
oob_scores_list = []
for m in m_values:
rf = RandomForestClassifierScratch(
n_estimators=100, max_features=m, random_state=42
)
rf.fit(X_train_cls, y_train_cls)
acc = accuracy_score(y_test_cls, rf.predict(X_test_cls))
oob = rf.oob_score(X_train_cls, y_train_cls)
accuracies.append(acc)
oob_scores_list.append(oob)
print(f"m = {m:2d} Test Acc: {acc:.4f} OOB Score: {oob:.4f}")
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(m_values, accuracies, 'bo-', linewidth=2, markersize=8, label='Test Accuracy')
ax.plot(m_values, oob_scores_list, 'rs--', linewidth=2, markersize=8, label='OOB Score')
ax.axvline(x=int(np.sqrt(20)), color='green', linestyle=':', linewidth=2,
label=f'$\\sqrt{{p}}$ = {int(np.sqrt(20))}')
ax.set_xlabel('Number of Features (m)', fontsize=12)
ax.set_ylabel('Score', fontsize=12)
ax.set_title('Effect of max_features on Random Forest Performance', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ----------------------------
# 実験3: 変数重要度の可視化
# ----------------------------
# MDI重要度
mdi_importance = rf_scratch.feature_importances_mdi
# scikit-learnの重要度と比較
sklearn_importance = rf_sklearn.feature_importances_
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# MDI重要度(スクラッチ)
sorted_idx_scratch = np.argsort(mdi_importance)
ax1.barh(range(len(mdi_importance)), mdi_importance[sorted_idx_scratch], color='steelblue')
ax1.set_yticks(range(len(mdi_importance)))
ax1.set_yticklabels([f'Feature {i}' for i in sorted_idx_scratch])
ax1.set_xlabel('MDI Importance')
ax1.set_title('Feature Importance (Scratch Implementation)')
ax1.grid(True, alpha=0.3, axis='x')
# scikit-learnの重要度
sorted_idx_sklearn = np.argsort(sklearn_importance)
ax2.barh(range(len(sklearn_importance)), sklearn_importance[sorted_idx_sklearn], color='coral')
ax2.set_yticks(range(len(sklearn_importance)))
ax2.set_yticklabels([f'Feature {i}' for i in sorted_idx_sklearn])
ax2.set_xlabel('MDI Importance')
ax2.set_title('Feature Importance (scikit-learn)')
ax2.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
# ----------------------------
# 実験4: 推定器数 B と性能の関係
# ----------------------------
B_values = [1, 5, 10, 25, 50, 100, 200, 300]
test_accs = []
oob_accs = []
for B in B_values:
rf = RandomForestClassifierScratch(
n_estimators=B, max_features='sqrt', random_state=42
)
rf.fit(X_train_cls, y_train_cls)
test_accs.append(accuracy_score(y_test_cls, rf.predict(X_test_cls)))
oob_accs.append(rf.oob_score(X_train_cls, y_train_cls))
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(B_values, test_accs, 'bo-', linewidth=2, markersize=8, label='Test Accuracy')
ax.plot(B_values, oob_accs, 'rs--', linewidth=2, markersize=8, label='OOB Score')
ax.set_xlabel('Number of Trees (B)', fontsize=12)
ax.set_ylabel('Accuracy', fontsize=12)
ax.set_title('Random Forest: Accuracy vs Number of Trees', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ----------------------------
# 実験5: Proximity行列の可視化(小規模データ)
# ----------------------------
# 小規模データで Proximity を計算
X_small, y_small = make_classification(
n_samples=50, n_features=5, n_informative=3,
n_classes=2, random_state=42
)
rf_small = RandomForestClassifierScratch(
n_estimators=100, max_features='sqrt', random_state=42
)
rf_small.fit(X_small, y_small)
prox = rf_small.proximity_matrix(X_small)
# ソートしてクラスごとにまとめる
sort_idx = np.argsort(y_small)
prox_sorted = prox[sort_idx][:, sort_idx]
fig, ax = plt.subplots(figsize=(8, 7))
im = ax.imshow(prox_sorted, cmap='YlOrRd', vmin=0, vmax=1)
plt.colorbar(im, ax=ax, label='Proximity')
ax.set_xlabel('Sample Index (sorted by class)')
ax.set_ylabel('Sample Index (sorted by class)')
ax.set_title('Proximity Matrix (sorted by class label)')
# クラス境界線
n_class0 = np.sum(y_small == 0)
ax.axhline(y=n_class0 - 0.5, color='blue', linewidth=2)
ax.axvline(x=n_class0 - 0.5, color='blue', linewidth=2)
plt.tight_layout()
plt.show()
# 異常値スコアの計算
print("\n=== 異常値スコア(上位5件)===")
for k in np.unique(y_small):
class_mask = y_small == k
class_indices = np.where(class_mask)[0]
n_k = len(class_indices)
outlier_scores = np.zeros(n_k)
for idx, i in enumerate(class_indices):
prox_sum = np.sum(prox[i, class_indices] ** 2)
if prox_sum > 0:
outlier_scores[idx] = n_k / prox_sum
top_outliers = np.argsort(outlier_scores)[-3:][::-1]
print(f"\nクラス {k} の異常値スコア上位:")
for rank, oi in enumerate(top_outliers):
print(f" {rank+1}. サンプル {class_indices[oi]}: "
f"スコア = {outlier_scores[oi]:.4f}")
上記のコードを実行すると、以下のことが確認できます。
- スクラッチ実装とscikit-learnの性能比較: 同じハイパーパラメータで同等の精度が得られることを確認できます
- 特徴量数 $m$ の影響: $m = \sqrt{p}$ 付近で良好な性能が得られ、理論的なガイドラインと整合します
- 変数重要度の可視化: 情報的な特徴量(Feature 0〜9)が高い重要度を持つことが確認できます
- 推定器数 $B$ と性能: $B$ を増やすと性能が単調に向上し、100本程度で収束する傾向が見られます
- Proximity行列: 同クラスのサンプル間のProximityが高く、異クラス間は低いことがブロック構造として可視化されます
まとめ
本記事では、ランダムフォレストの理論について解説しました。
- ランダムフォレストはバギングに特徴量ランダム選択を追加したアンサンブル手法で、木同士の相関 $\rho$ を低減することでバギングよりさらに分散を削減します
- 各ノードで特徴量が選ばれる確率は $m/p$ であり、$m$ を小さくするほど多様性が増しますが、各木の精度は低下するトレードオフがあります
- 推奨特徴量数は分類で $m = \lfloor\sqrt{p}\rfloor$、回帰で $m = \lfloor p/3 \rfloor$ です
- Breimanの定理により、汎化誤差は $PE^* \leq \bar{\rho}(1 – s^2)/s^2$ で上界が与えられます
- MDI(Gini重要度)とMDA(Permutation重要度)の2つの変数重要度指標があり、それぞれ特徴と注意点が異なります
- Proximity行列を用いた異常値検出も可能です
次のステップとして、以下の記事も参考にしてください。