📚 背景知識(読んでから問題へ)
🎯
Day 061 — Day 060 のランダムフォレスト(並列バギング)に続き、直列に残差を補正していく勾配ブースティング(XGBoost)を学びます。Kaggle 上位解法の定番モデルです。
ランダムフォレスト vs 勾配ブースティング
| 観点 | ランダムフォレスト | 勾配ブースティング(XGBoost) |
|---|---|---|
| 木の作り方 | 並列(独立) | 直列(前の誤りを補正) |
| 各木の深さ | 深い(制限なし) | 浅い(3〜6 が標準) |
| 学習の仕組み | バギング(分散低減) | ブースティング(バイアス低減) |
| 過学習リスク | 低い | 高い(学習率・木の数で制御) |
| Kaggle順位帯 | ベースライン・上位補助 | 上位解法の定番 |
| 欠損値処理 | 非対応(要 fillna) | 自動処理 |
XGBoost の「X」に込められた改良点
| 改良点 | 内容 | 効果 |
|---|---|---|
| 正則化(L1 / L2) | 木の複雑さにペナルティ | 過学習を抑制 |
| 近似分割アルゴリズム | ヒストグラムで高速化 | 大規模データ対応 |
| 欠損値の自動処理 | NaN の分岐方向を学習 | 前処理の手間軽減 |
| 列サブサンプリング | RF と同様に特徴量をランダム選択 | 多様性・過学習抑制 |
| early_stopping | バリデーション精度が改善しなければ停止 | 最適な n_estimators を自動決定 |
🔁 ブースティングの流れ(反復的な誤差補正)
「前の木が間違えた箇所を次の木が重点的に直す」直列学習
F0
初期予測
(平均値)
MSE 大
(平均値)
MSE 大
→
残差①
正解 − F0
の学習
の学習
→
F1
F0 + η×h1
MSE 中
MSE 中
→
残差②
正解 − F1
の学習
の学習
→
F_N
N回補正後
MSE 小
最終予測
MSE 小
最終予測
early_stopping_rounds: バリデーション残差の最小点を自動検出 → 最適な n_estimators を節約
📐 学習率(learning_rate / eta)の効果
n_estimators=100 固定のとき: 学習率が低すぎると収束不足、高すぎると過学習
経験則: n_estimators × learning_rate ≈ 一定 → lr を下げる分 n_estimators を増やす
Kaggle では lr=0.01〜0.05 + n_estimators=500〜1000 + early_stopping が定石
📊 3モデルの過学習比較(訓練精度 vs CV精度)
過学習度 = 訓練精度 − CV精度。値が大きいほど汎化しにくい
XGBoost は正則化により訓練精度を抑えつつ CV 精度を最大化する(過学習度が最小)
⚙️ 主要パラメータ
n_estimators
木の本数。learning_rate とセットで決める。early_stopping 使用時は多め(1000)に設定しておく。
learning_rate (eta)
各木の寄与度を縮小する率(0〜1)。小さいほど慎重に学習。Kaggle は 0.01〜0.05 が定石。
max_depth
1本の木の深さ。XGBoost は3〜6 が標準。RF と違い浅い木を多数使う設計。
subsample
行のサブサンプリング率(0〜1)。RF のブートストラップに相当。0.7〜0.9 が多い。
colsample_bytree
各木で使う列の割合(0〜1)。RF の max_features に相当。0.6〜0.9 が多い。
reg_lambda / reg_alpha
L2 / L1 正則化係数。デフォルトは lambda=1, alpha=0。過学習時に増やす。
min_child_weight
葉ノードに必要な最小サンプル重み合計。大きくすると過学習抑制。
early_stopping_rounds
最重要テクニック。 n ラウンド改善なければ停止。eval_set が必要。Kaggle では必ず設定。
🗂️ データスキーマ(Titanic 疑似データ)
| 列名 | 型 | 値の範囲 | 説明 | XGBoostでの取扱い |
|---|---|---|---|---|
Pclass | int | 1, 2, 3 | 旅客クラス | 数値としてそのまま使用可 |
Sex_enc | int | 0 / 1 | 性別のラベルエンコード | 最重要特徴量 |
Age | float | 1〜80 | 年齢 | NaN あっても自動処理 ✓ |
SibSp | int | 0〜8 | 同乗兄弟/配偶者数 | 低重要度 |
Fare | float | 0〜∞ | 運賃 | 対数変換推奨(外れ値対策) |
Survived | int | 0 / 1 | 生存(目的変数) | — |
XGBoost の欠損値自動処理の仕組み
NaN サンプルを左右どちらに送るか、学習時に損失を最小化する方向を自動決定する
📝 問題
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score
import warnings
warnings.filterwarnings('ignore')
try:
import xgboost as xgb
print(f"XGBoost version: {xgb.__version__}")
except ImportError:
print("xgboost がインストールされていません: pip install xgboost")
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 — RF vs sklearn GradientBoosting vs XGBoost の比較
RandomForestClassifier・GradientBoostingClassifier・XGBClassifier(各 n_estimators=100)を StratifiedKFold(5) で CV 比較せよ- 訓練精度と CV 精度の差(過学習度)を算出せよ
- 3モデルの特徴を一言で説明せよ
問2 — 学習率 (learning_rate) の効果
learning_rateを[0.001, 0.01, 0.05, 0.1, 0.3, 1.0]で変化させ CV スコアを記録せよ(n_estimators=100 固定)- 学習率が低い・高い場合の挙動を説明せよ
- 学習率と n_estimators がトレードオフである理由を説明せよ
問3 — n_estimators × learning_rate の組み合わせ
- 以下の組み合わせで CV スコアを比較せよ: (50, 0.3) / (100, 0.1) / (300, 0.05) / (500, 0.01)
n_estimators × learning_rate = 一定という経験則を確認せよ
問4 — max_depth と min_child_weight の効果
max_depthを[2, 3, 4, 6, 8]で変えながら CV スコアと学習時間を記録せよ- XGBoost で max_depth が深いと何が起こるかを RF と比較して説明せよ
min_child_weightの役割を説明せよ
問5 — 特徴量重要度と欠損値処理
- XGBoost の
feature_importances_(importance_type='gain')を取得せよ - Titanic データに欠損値(NaN)を 20% 注入し、XGBoost が NaN を自動処理することを確認せよ
- RF と XGBoost の欠損値に対する挙動の違いを述べよ
💡 ヒント
ヒント1(方向性)
- XGBoost は
pip install xgboostでインストール。sklearn API に準拠しているためcross_val_scoreがそのまま使える eval_metric='logloss'を指定すると不要な警告が消える- NaN 注入後、XGBoost は
fit(X_nan, y)でエラーなく動作する。RF はValueErrorになる
ヒント2(アプローチ)
import xgboost as xgb
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for name, clf in [
("RF", RandomForestClassifier(n_estimators=100, random_state=42)),
("GBM", GradientBoostingClassifier(n_estimators=100, random_state=42)),
("XGB", xgb.XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss')),
]:
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(コード骨格)
# 学習率の比較
for lr in [0.001, 0.01, 0.05, 0.1, 0.3, 1.0]:
clf = xgb.XGBClassifier(n_estimators=100, learning_rate=lr,
random_state=42, eval_metric='logloss')
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f"lr={lr:.3f}: CV={cv_s:.4f}")
# n×lr トレードオフ
for n_est, lr in [(50, 0.3), (100, 0.1), (300, 0.05), (500, 0.01)]:
clf = xgb.XGBClassifier(n_estimators=n_est, learning_rate=lr,
random_state=42, eval_metric='logloss')
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f"n={n_est}, lr={lr}, n×lr={n_est*lr:.1f}: CV={cv_s:.4f}")
# 欠損値注入
X_nan = X.copy().astype(float)
mask = np.random.rand(*X_nan.shape) < 0.2
X_nan[mask] = np.nan
✅ 模範解答
import numpy as np
import pandas as pd
import time
import xgboost as xgb
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score
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: 3モデル比較 ──
print("=" * 60)
print("問1: RF vs GradientBoosting vs XGBoost")
print("=" * 60)
for name, clf in [
("RandomForest(n=100)", RandomForestClassifier(n_estimators=100, random_state=42)),
("GradientBoosting(n=100)", GradientBoostingClassifier(n_estimators=100, random_state=42)),
("XGBoost(n=100)", xgb.XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss')),
]:
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}")
# ── 問2: 学習率 ──
print("\n" + "=" * 60)
print("問2: learning_rate の効果")
print("=" * 60)
for lr in [0.001, 0.01, 0.05, 0.1, 0.3, 1.0]:
clf = xgb.XGBClassifier(n_estimators=100, learning_rate=lr,
random_state=42, eval_metric='logloss')
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
bar = "█" * int(cv_s * 40)
print(f" lr={lr:.3f}: CV={cv_s:.4f} {bar}")
# ── 問3: n×lr トレードオフ ──
print("\n" + "=" * 60)
print("問3: n_estimators × learning_rate のトレードオフ")
print("=" * 60)
for n_est, lr in [(50, 0.3), (100, 0.1), (300, 0.05), (500, 0.01)]:
clf = xgb.XGBClassifier(n_estimators=n_est, learning_rate=lr,
random_state=42, eval_metric='logloss')
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f" n={n_est:4d}, lr={lr:.2f}, n×lr={n_est*lr:5.1f}: CV={cv_s:.4f}")
# ── 問4: max_depth ──
print("\n" + "=" * 60)
print("問4: max_depth の効果")
print("=" * 60)
for depth in [2, 3, 4, 6, 8]:
clf = xgb.XGBClassifier(n_estimators=100, max_depth=depth,
random_state=42, eval_metric='logloss')
t0 = time.time()
clf.fit(X, y)
tr = clf.score(X, y)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f" depth={depth}: 訓練={tr:.4f}, CV={cv_s:.4f}, 差={tr-cv_s:.4f}, 時間={time.time()-t0:.2f}s")
# ── 問5: 重要度と欠損値 ──
print("\n" + "=" * 60)
print("問5: 特徴量重要度と欠損値")
print("=" * 60)
clf_xgb = xgb.XGBClassifier(n_estimators=100, random_state=42,
eval_metric='logloss', importance_type='gain')
clf_xgb.fit(X, y)
for f, imp in sorted(zip(features, clf_xgb.feature_importances_), key=lambda x: -x[1]):
bar = "█" * int(imp / max(clf_xgb.feature_importances_) * 30)
print(f" {f:10s}: {imp:.4f} {bar}")
np.random.seed(0)
X_nan = X.copy().astype(float)
X_nan[np.random.rand(*X_nan.shape) < 0.2] = np.nan
cv_nan = cross_val_score(
xgb.XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss'),
X_nan, y, cv=cv, scoring='accuracy').mean()
print(f"\n XGBoost (20% NaN): CV={cv_nan:.4f} ← NaN を自動処理")
print(" RandomForest (NaN): ValueError(fillna が必要)")
🪜 Step-by-Step 解説
1勾配ブースティングの動作をシミュレーション
import numpy as np
from sklearn.tree import DecisionTreeRegressor
# 簡単な回帰問題でブースティングの動作を確認
np.random.seed(42)
X_reg = np.linspace(0, 10, 50).reshape(-1, 1)
y_reg = np.sin(X_reg.ravel()) + np.random.normal(0, 0.1, 50)
# 初期予測(全て平均値)
F = np.full(len(y_reg), y_reg.mean())
print(f"初期MSE: {np.mean((y_reg - F) ** 2):.4f}")
lr = 0.3
for i in range(5):
residuals = y_reg - F # 残差 = 正解 - 現在の予測
h = DecisionTreeRegressor(max_depth=2)
h.fit(X_reg, residuals) # 残差を学習
F += lr * h.predict(X_reg) # 学習率分だけ補正
mse = np.mean((y_reg - F) ** 2)
print(f" ブースト{i+1}回目: MSE={mse:.4f}, 残差最大={abs(residuals).max():.4f}")
2early_stopping の使い方(Kaggle 最重要テクニック)
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
clf = xgb.XGBClassifier(
n_estimators=1000, # 多めに設定
learning_rate=0.05,
max_depth=4,
eval_metric='logloss',
early_stopping_rounds=20, # 20ラウンド改善なければ停止
random_state=42,
verbosity=0
)
clf.fit(
X_train, y_train,
eval_set=[(X_val, y_val)], # バリデーションセットを渡す
verbose=False
)
print(f"最適な n_estimators: {clf.best_iteration}")
print(f"バリデーション精度: {accuracy_score(y_val, clf.predict(X_val)):.4f}")
3正則化パラメータの比較
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
print("正則化パラメータの比較:")
for l2, l1 in [(0, 0), (1, 0), (1, 0.1), (10, 1)]:
clf = xgb.XGBClassifier(
n_estimators=100, reg_lambda=l2, reg_alpha=l1,
random_state=42, eval_metric='logloss'
)
cv_s = cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
print(f" L2={l2:2d}, L1={l1}: CV={cv_s:.4f}")
# L2 (Ridge): 葉の重みを全体的に縮小 → 過学習抑制
# L1 (Lasso): 不要特徴量への寄与をゼロに近づける
4Kaggle 標準 XGBoost テンプレート
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import accuracy_score
xgb_params = {
'n_estimators': 1000,
'learning_rate': 0.05,
'max_depth': 4,
'min_child_weight': 1,
'subsample': 0.8, # 行サブサンプリング
'colsample_bytree': 0.8, # 列サブサンプリング(max_features 相当)
'reg_alpha': 0.1,
'reg_lambda': 1.0,
'eval_metric': 'logloss',
'random_state': 42,
'n_jobs': -1,
}
clf = xgb.XGBClassifier(**xgb_params)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof = cross_val_predict(clf, X, y, cv=cv)
print(f"OOF精度: {accuracy_score(y, oof):.4f}")
clf.fit(X, y)
# 特徴量重要度(gain ベース)
for f, imp in sorted(zip(features, clf.feature_importances_), key=lambda x: -x[1]):
print(f" {f}: {imp:.4f}")
📐 数学・統計の補足(文系向け)
「残差を学習する」を試験で例える
| ラウンド | 試験の例え | 機械学習の対応 |
|---|---|---|
| 初期予測 | 「全員が平均点(60点)だろう」と予想 | F₀ = 全サンプルの平均 |
| 残差計算 | A さんは実際 80点 → 20点読み違えた | 残差 = y − F₀(x) |
| 残差学習 | 「20点の読み違い」のパターンを分析 | 弱い木 h₁ で残差を学習 |
| 予測更新 | 次の予測を慎重に(30%だけ)修正 | F₁ = F₀ + η × h₁ |
| 繰り返し | 残りのズレをまた分析・修正 | N 回繰り返す |
学習率 η の役割
η=1.0 は残差を一度で全部補正しようとする(急ぎすぎ)。η=0.05 は少しずつ慎重に補正する。
🏆 Kaggleでの実践的な使い方
| 場面 | 使い方 | コツ |
|---|---|---|
| ベースライン構築 | XGBoost デフォルトで試す | RF よりも精度が出やすい |
| Optuna 自動チューニング | n_estimators, lr, max_depth を探索 | early_stopping と組み合わせると高速 |
| アンサンブル | RF + XGBoost + LightGBM の平均 | モデル間の相関が低いほど効果大 |
| 大規模データ | LightGBM へ移行 | 100万行以上は LightGBM 推奨 |
| 欠損値処理 | XGBoost は NaN 自動処理 | 事前の fillna 不要(逆に情報損失を防ぐ) |
# Optuna で XGBoost をチューニングする例
# (Day 064 で詳しく学ぶ予告)
import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'max_depth': trial.suggest_int('max_depth', 3, 8),
'subsample': trial.suggest_float('subsample', 0.6, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
'eval_metric': 'logloss', 'random_state': 42
}
clf = xgb.XGBClassifier(**params)
return cross_val_score(clf, X, y, cv=cv, scoring='accuracy').mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=30)
print(f"最良スコア: {study.best_value:.4f}")
print(f"最良パラメータ: {study.best_params}")
🚫 よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| learning_rate を小さくするだけでよい | 「小さい方が安全」という思い込み | n_estimators も一緒に増やす必要がある(トレードオフ) |
| max_depth を RF と同じ感覚で設定 | RF の知識を誤適用 | XGBoost は浅い木(3〜6)が標準。深いと過学習しやすい |
| early_stopping を使わない | 知識不足 | eval_set + early_stopping_rounds は Kaggle の定石。必ず使う |
| feature_importances の weight と gain が同じと思う | デフォルトが weight | gain(情報利得の合計)が最も信頼性が高い。importance_type='gain' を指定 |
| XGBoost が常に RF より優れると思う | コンペ優勝解法のイメージ | 小規模データや単純な問題では RF が勝つことも多い。必ず両方試す |
🚀 次のステップ
- 発展:
subsample/colsample_bytreeの過学習抑制効果を実験する。Optuna での自動チューニングを試す - 次回予告(Day 062): LightGBM 入門 — leaf-wise 成長・カテゴリカル特徴量の直接処理・XGBoost との速度/精度比較
📋 自己評価(解いた後に記入)
✍️
理解度: [ ] 完全理解 [ ] おおむね理解 [ ] 要復習
自分の回答:
気づき・メモ: