📚 背景知識(読んでから問題へ)
🎯
Day 060 — Day 059 で学んだ決定木の弱点(過学習)を克服するランダムフォレストを学びます。「多数決」と「ランダム性の導入」がキーワードです。
ランダムフォレスト = 決定木の集合知
1人の専門家の意見より、100人の専門家の多数決のほうが正確です(集合知)。ランダムフォレストは決定木を100本作って多数決します。ただし各木が同じ意見を持つと意味がないので、ランダム性を2つの方法で導入します。
| ランダム性の種類 | 内容 | 効果 |
|---|---|---|
| ブートストラップサンプリング | 各木が異なるデータ(重複あり)で学習 | 木間の相関を低下 |
| 特徴量のランダム選択 | 各分岐で sqrt(n_features) の特徴量のみ使用 | 木間の多様性を確保 |
OOBスコア — 無料のバリデーション
ブートストラップでは全データの約 36.8% が選ばれません(Out-of-Bag)。この未使用データが自動的にバリデーションセットになります。
つまり oob_score=True を設定するだけで、交差検証なしにモデル性能を推定できます。
🌳 バギングの流れ(n_estimators=3 の例)
実際は 100〜500 本の木を作って多数決。各木は異なるデータセットで学習。
元データ
N=891件
全データ
全データ
→
木①
Bootstrap
サンプル①
(N件・重複あり)
サンプル①
(N件・重複あり)
木②
Bootstrap
サンプル②
(N件・重複あり)
サンプル②
(N件・重複あり)
木③
Bootstrap
サンプル③
(N件・重複あり)
サンプル③
(N件・重複あり)
→
多数決
①②③の
予測を集計
→ 最終予測
予測を集計
→ 最終予測
📊 OOBスコアの仕組み
各サンプルについて「そのサンプルを学習に使わなかった木のみ」で予測 → 全体の予測精度 = OOBスコア
OOBスコア vs 5-Fold CV精度の比較
OOBは CVと近い値を1回の学習で推定できるが、厳密には同等ではない。
📉 木の数と分散低減(n_estimators の効果)
n_estimators が増えるほど CV精度は安定するが、ある数以上は頭打ちになる。
観察: 10本は不安定、100本で概ね収束。500本は計算コストが5倍になるが精度向上は微小。
特徴量重要度(RF vs 決定木)
RFは100本の平均なので安定。決定木は1本なので偏りが出やすい。
RandomForest (n=100)
⚙️ 主要パラメータ
n_estimators
木の本数。デフォルト=100。多いほど安定するが計算コスト増。Kaggleでは 500〜1000 が多い。
max_features
各分岐で使う特徴量数。分類='sqrt'(デフォルト)、回帰='1.0'。小さいほど木間の多様性が増す。
oob_score
True にするとOOBスコアを計算。CVの代替として高速評価可能。デフォルト=False。
max_depth
None が標準(制限なし)。バギングで過学習が抑制されるため決定木と違い深くてOK。
min_samples_leaf
葉ノードの最小サンプル数。デフォルト=1。大きくするとメモリ節約・速度向上。
n_jobs
必ず -1 を設定(全CPUコアを並列使用)。大幅な速度向上。デフォルト=1(シングルコア)。
🗂️ データスキーマ(Titanic 疑似データ)
| 列名 | 型 | 値の範囲 | 説明 | RFでの重要度(目安) |
|---|---|---|---|---|
Pclass | int | 1, 2, 3 | 旅客クラス | 中 |
Sex_enc | int | 0 / 1 | 性別のラベルエンコード | 最高 |
Age | float | 1〜80 | 年齢 | 中 |
SibSp | int | 0〜8 | 同乗している兄弟/配偶者数 | 低 |
Fare | float | 0〜∞ | 運賃(Pclassと相関あり) | 中〜高 |
Survived | int | 0 / 1 | 生存(目的変数) | — |
クラス分布
📝 問題
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, classification_report
import warnings
warnings.filterwarnings('ignore')
# Titanic データの簡易作成
np.random.seed(42)
n = 891
data = pd.DataFrame({
'Pclass': np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
'Sex': np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
'Age': np.random.normal(29.7, 14.5, n).clip(1, 80),
'SibSp': np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
'Fare': np.abs(np.random.normal(32, 50, n)),
})
data['Survived'] = (
(data['Sex'] == 'female') * 0.5 +
(data['Pclass'] == 1) * 0.3 +
(data['Age'] < 16) * 0.2 +
np.random.normal(0, 0.15, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)
data['Sex_enc'] = LabelEncoder().fit_transform(data['Sex'])
features = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Fare']
X = data[features].values
y = data['Survived'].values
問1 — 決定木 vs ランダムフォレスト
DecisionTreeClassifier(random_state=42)とRandomForestClassifier(n_estimators=100, random_state=42)をStratifiedKFold(5)で比較せよ- 訓練精度とCV精度の差(過学習度)をそれぞれ計算せよ
- ランダムフォレストが安定している理由を述べよ
問2 — n_estimators のチューニング
n_estimatorsを[10, 50, 100, 200, 500]で変化させながら CV スコアを記録せよ- スコアの変化を観察し、「どこで頭打ちになるか」を確認せよ
- 計算コストと精度のトレードオフを述べよ
問3 — OOBスコアの活用
RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)で学習せよclf.oob_score_を取得し、StratifiedKFold(5)のCV精度と比較せよ- OOBスコアがCVの代替として使える理由を説明せよ
問4 — 特徴量重要度の比較
- ランダムフォレストの
feature_importances_を取得し、重要度順にソートして出力せよ - Day 059 の決定木(
max_depth=5)の特徴量重要度と比較せよ - ランダムフォレストの重要度のほうが安定している理由を述べよ
問5 — max_features の効果
max_featuresを['sqrt', 'log2', None, 0.3, 0.5]で変化させながらCV精度を比較せよmax_features=None(全特徴量使用)と'sqrt'の違いを説明せよ- Kaggleでは通常どの値を使うか述べよ
💡 ヒント
ヒント1(方向性)
- OOBスコア取得には
oob_score=Trueを指定してからfit()した後にclf.oob_score_でアクセス - ランダムフォレストは
fit()で全ての木を学習するためn_estimatorsが大きいほど遅い feature_importances_は全ての木の重要度の平均なので決定木より安定している
ヒント2(アプローチ)
# 決定木 vs ランダムフォレストの比較
cv = StratifiedKFold(5, shuffle=True, random_state=42)
for name, clf in [
("決定木", DecisionTreeClassifier(random_state=42)),
("RF", RandomForestClassifier(n_estimators=100, random_state=42))
]:
clf.fit(X, y)
tr = clf.score(X, y)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f"[{name}] 訓練: {tr:.4f}, CV: {cv_s:.4f}, 差: {tr-cv_s:.4f}")
ヒント3(コード骨格)
# n_estimators チューニング
for n_est in [10, 50, 100, 200, 500]:
clf = RandomForestClassifier(n_estimators=n_est, random_state=42)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f"n_estimators={n_est:4d}: CV={cv_s:.4f}")
# OOBスコア
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X, y)
print(f"OOB Score: {rf_oob.oob_score_:.4f}")
# 特徴量重要度(ランダムフォレスト vs 決定木)
for f, imp in sorted(zip(features, rf_oob.feature_importances_), key=lambda x: -x[1]):
print(f"{f}: {imp:.4f}")
✅ 模範解答
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
import warnings
warnings.filterwarnings('ignore')
# データ準備
np.random.seed(42)
n = 891
data = pd.DataFrame({
'Pclass': np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
'Sex': np.random.choice(['male', 'female'], n, p=[0.65, 0.35]),
'Age': np.random.normal(29.7, 14.5, n).clip(1, 80),
'SibSp': np.random.choice(range(9), n, p=[0.68,0.23,0.04,0.02,0.01,0.01,0.005,0.005,0.01]),
'Fare': np.abs(np.random.normal(32, 50, n)),
})
data['Survived'] = (
(data['Sex'] == 'female') * 0.5 + (data['Pclass'] == 1) * 0.3 +
(data['Age'] < 16) * 0.2 + np.random.normal(0, 0.15, n)
) > 0.4
data['Survived'] = data['Survived'].astype(int)
data['Sex_enc'] = LabelEncoder().fit_transform(data['Sex'])
features = ['Pclass', 'Sex_enc', 'Age', 'SibSp', 'Fare']
X = data[features].values
y = data['Survived'].values
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# ── 問1: 決定木 vs ランダムフォレスト ──
print("=" * 60)
print("問1: 決定木 vs ランダムフォレスト")
print("=" * 60)
for name, clf in [
("DecisionTree(制限なし)", DecisionTreeClassifier(random_state=42)),
("RandomForest(n=100)", RandomForestClassifier(n_estimators=100, random_state=42))
]:
clf.fit(X, y)
tr = clf.score(X, y)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f"[{name}]")
print(f" 訓練精度: {tr:.4f}, CV精度: {cv_s:.4f}, 過学習度: {tr-cv_s:.4f}")
print("\n→ RFは個々の木が過学習しても、多数決で誤差がキャンセルされるため安定する")
# ── 問2: n_estimators のチューニング ──
print("\n" + "=" * 60)
print("問2: n_estimators のチューニング")
print("=" * 60)
for n_est in [10, 50, 100, 200, 500]:
clf = RandomForestClassifier(n_estimators=n_est, random_state=42)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f" n_estimators={n_est:4d}: CV精度 = {cv_s:.4f}")
print("\n→ 100〜200 あたりで頭打ちになる。500 は計算コストが増えるが精度向上は微小")
# ── 問3: OOBスコア ──
print("\n" + "=" * 60)
print("問3: OOBスコア vs CV精度")
print("=" * 60)
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X, y)
oob = rf_oob.oob_score_
cv_s = cross_val_score(rf_oob, X, y, cv=cv, scoring='accuracy').mean()
print(f" OOBスコア: {oob:.4f}")
print(f" CV精度: {cv_s:.4f}")
print(f" 差: {abs(oob - cv_s):.4f}")
print("\n→ OOBはブートストラップで選ばれなかったデータで自動評価するため、")
print(" CV不要で似た推定が得られる(計算コストが低い)")
# ── 問4: 特徴量重要度の比較 ──
print("\n" + "=" * 60)
print("問4: 特徴量重要度(RF vs 決定木)")
print("=" * 60)
print("[RandomForest]")
rf_imp = sorted(zip(features, rf_oob.feature_importances_), key=lambda x: -x[1])
for f, imp in rf_imp:
bar = "█" * int(imp * 40)
print(f" {f:10s}: {imp:.4f} {bar}")
print("\n[DecisionTree max_depth=5]")
dt = DecisionTreeClassifier(max_depth=5, random_state=42)
dt.fit(X, y)
dt_imp = sorted(zip(features, dt.feature_importances_), key=lambda x: -x[1])
for f, imp in dt_imp:
bar = "█" * int(imp * 40)
print(f" {f:10s}: {imp:.4f} {bar}")
print("\n→ RFは100本の木の平均なので重要度が安定(分散が小さい)")
# ── 問5: max_features の効果 ──
print("\n" + "=" * 60)
print("問5: max_features の効果")
print("=" * 60)
for mf in ['sqrt', 'log2', None, 0.3, 0.5]:
clf = RandomForestClassifier(n_estimators=100, max_features=mf, random_state=42)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f" max_features={str(mf):6s}: CV精度 = {cv_s:.4f}")
print("\n→ 'sqrt'がデフォルト。Noneは全特徴量を使い木間の相関が高くなる(多様性低下)")
print(" Kaggleでは'sqrt'か0.3〜0.5が標準的")
🪜 Step-by-Step 解説
1バギングの分散低減を数値で確認
# バギングのシミュレーション(分散低減の確認)
np.random.seed(42)
# 1つの木は高分散(ノイズに過敏)
# n本の平均では分散が 1/n に減少(平均の法則)
n_trees = [1, 10, 50, 100]
for n in n_trees:
# 各木が独立なら Var(平均) = Var(1木) / n
variance_reduction = 1 / n
print(f"木の数 {n:3d}: 分散は単木の {variance_reduction:.3f} 倍")
# → 木が独立なほど効果大(そのためmax_featuresで木を「バラバラ」にする)
2OOBサンプルの割合を計算
# ブートストラップでN件抽出したとき、特定のサンプルが選ばれない確率
# P(選ばれない) = (1 - 1/N)^N → e^(-1) ≈ 0.368 (N→∞)
import numpy as np
N = 891
prob_not_selected = (1 - 1/N) ** N
print(f"OOBサンプルの割合(理論値): {prob_not_selected:.4f}") # ≈ 0.3679
print(f"実際のOOBサンプル数の期待値: {int(N * prob_not_selected)}件")
# 確認: 実際に sklearn でOOBサンプルを確認
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=1, oob_score=True, random_state=42)
rf.fit(X, y)
# 各木でOOBサンプルのインデックスを確認できる(内部属性)
print(f"実際のOOBスコア(1木): {rf.oob_score_:.4f}")
3各木の重要度のばらつきを観察
from sklearn.ensemble import RandomForestClassifier
import numpy as np
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
# 各木の重要度を取得
tree_importances = np.array([tree.feature_importances_ for tree in rf.estimators_])
# shape: (100, 5) = 100本の木 × 5つの特徴量
print("各特徴量の重要度の std(木間のばらつき):")
for f, mean, std in zip(features, tree_importances.mean(axis=0), tree_importances.std(axis=0)):
print(f" {f:10s}: 平均={mean:.4f}, std=±{std:.4f}")
print("\n→ 100本の平均を取ることでstdが低下し安定した重要度が得られる")
4Permutation Importance(より信頼性の高い重要度)
from sklearn.inspection import permutation_importance
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
# Permutation Importance: 特徴量をシャッフルしたときの精度低下
perm_imp = permutation_importance(rf, X, y, n_repeats=10, random_state=42)
print("Permutation Importance(より信頼性が高い):")
for f, mean, std in sorted(
zip(features, perm_imp.importances_mean, perm_imp.importances_std),
key=lambda x: -x[1]
):
print(f" {f:10s}: {mean:.4f} ± {std:.4f}")
# feature_importances_より信頼性が高いが計算コストが大きい
# → 連続値特徴量の重要度が過大評価される問題を修正できる
5Kaggle標準テンプレート
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_predict, StratifiedKFold
from sklearn.metrics import accuracy_score
# Kaggle 標準パターン
rf = RandomForestClassifier(
n_estimators=500, # 多いほど安定(計算コストと相談)
max_features='sqrt', # 各分岐で sqrt(n_features) の特徴量を使用
max_depth=None, # 制限なし(バギングで過学習を抑制)
min_samples_leaf=1, # デフォルト
oob_score=True, # OOBスコアで素早く評価
n_jobs=-1, # 全CPUコアを使用(並列化)
random_state=42
)
rf.fit(X, y)
print(f"OOBスコア: {rf.oob_score_:.4f}") # CVなしで評価
# OOF予測(Kaggle提出前の最終チェック)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof = cross_val_predict(rf, X, y, cv=cv)
print(f"OOF精度: {accuracy_score(y, oof):.4f}")
📐 数学・統計の補足(文系向け)
バギングの分散低減(試験の例え)
100人が「合否」を予測するとします。
- 1人が外れる確率: 40%
- 100人の多数決が外れる確率: 大幅に低下(大数の法則)
数式: Var(平均) = Var(1木) / n(木が完全に独立な場合)
実際には木間に相関があるため: Var(RF) = ρ·σ² + (1-ρ)/n·σ²(ρ=木間の相関)
ρ(木間の相関)が小さいほど RFの分散が低下する
なぜ max_depth=None でも過学習しないのか
| 手法 | max_depth=None の結果 | 理由 |
|---|---|---|
| 決定木(1本) | 過学習(訓練精度 100%) | 全データを完全に記憶 |
| ランダムフォレスト | 適切な汎化 | 各木が異なるデータで学習し、丸暗記がキャンセルされる |
🏆 Kaggleでの実践的な使い方
| 場面 | 使い方 | 備考 |
|---|---|---|
| ベースライン構築 | RFをまず試す | 前処理最小限でもそこそこ動く |
| 特徴量選択 | feature_importances_で上位を選ぶ | EDAの後段として有効 |
| ハイパラ調整 | Optuna + OOBスコア | CV不要で高速チューニング |
| アンサンブル | GBDTとのスタッキング | RF単体より多くの場合高スコア |
| 欠損値処理 | RF は NaN 非対応 | 事前に fillna が必要 |
# Kaggle での RF ベースライン テンプレート
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.metrics import roc_auc_score
rf = RandomForestClassifier(
n_estimators=500,
max_features='sqrt',
oob_score=True,
n_jobs=-1,
random_state=42
)
# OOBで素早く評価
rf.fit(X_train, y_train)
print(f"OOBスコア (AUCではなく精度): {rf.oob_score_:.4f}")
# 特徴量の絞り込み(重要度上位50%のみ使用)
importances = rf.feature_importances_
threshold = np.median(importances)
selected_features = [f for f, imp in zip(feature_names, importances) if imp >= threshold]
print(f"選択された特徴量: {len(selected_features)} / {len(feature_names)}")
🚫 よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| n_estimators を増やし続ける | 「多いほど良い」という思い込み | 100〜500 で収束。以降は計算コストのみ増加 |
| max_depth=None は危険と思う | 決定木の知識を誤適用 | バギングで過学習が抑制されるため制限不要(むしろ None が標準) |
| feature_importances を因果と思う | 相関と因果の混同 | 相関が強い特徴量間で重要度が分散する(多重共線性の影響) |
| oob_score と CV精度が全く同じと思う | 原理を誤解 | 近い値だが乱数・データ分割の違いで差がある |
| n_jobs=-1 を使わない | 並列化を知らない | 全コアを使えば大幅に高速化(4コアで約3倍速) |
🚀 次のステップ
- 発展: Permutation Importance と
feature_importances_の違いを深掘り。n_jobs=-1の速度比較も試す - 次回予告(Day 061): 勾配ブースティング入門 — XGBoost の仕組みと基本パラメータ(ランダムフォレストとの比較)
📋 自己評価(解いた後に記入)
✍️
理解度: [ ] 完全理解 [ ] おおむね理解 [ ] 要復習
自分の回答:
気づき・メモ: