Day 061 — 勾配ブースティング入門 — XGBoost の仕組みと基本パラメータ

2026-06-11 緑 / Phase 2 理論 XGBoost・GradientBoosting・learning_rate・early_stopping

📚 背景知識(読んでから問題へ)

🎯
Day 061 — Day 060 のランダムフォレスト(並列バギング)に続き、直列に残差を補正していく勾配ブースティング(XGBoost)を学びます。Kaggle 上位解法の定番モデルです。

ランダムフォレスト vs 勾配ブースティング

観点ランダムフォレスト勾配ブースティング(XGBoost)
木の作り方並列(独立)直列(前の誤りを補正)
各木の深さ深い(制限なし)浅い(3〜6 が標準)
学習の仕組みバギング(分散低減)ブースティング(バイアス低減)
過学習リスク低い高い(学習率・木の数で制御)
Kaggle順位帯ベースライン・上位補助上位解法の定番
欠損値処理非対応(要 fillna)自動処理

XGBoost の「X」に込められた改良点

改良点内容効果
正則化(L1 / L2)木の複雑さにペナルティ過学習を抑制
近似分割アルゴリズムヒストグラムで高速化大規模データ対応
欠損値の自動処理NaN の分岐方向を学習前処理の手間軽減
列サブサンプリングRF と同様に特徴量をランダム選択多様性・過学習抑制
early_stoppingバリデーション精度が改善しなければ停止最適な n_estimators を自動決定

🔁 ブースティングの流れ(反復的な誤差補正)

「前の木が間違えた箇所を次の木が重点的に直す」直列学習

F0
初期予測
(平均値)
MSE 大
残差①
正解 − F0
の学習
F1
F0 + η×h1
MSE 中
残差②
正解 − F1
の学習
F_N
N回補正後
MSE 小
最終予測
ブースティングラウンド(n_estimators) 残差(誤差) 0 訓練残差 Val残差 early_stopping 最適点

early_stopping_rounds: バリデーション残差の最小点を自動検出 → 最適な n_estimators を節約

📐 学習率(learning_rate / eta)の効果

n_estimators=100 固定のとき: 学習率が低すぎると収束不足、高すぎると過学習

lr=0.001(極端に低い)
収束不足
CV≈0.60
lr=0.01(低い)
やや収束不足
CV≈0.72
lr=0.05(適切)
良好
CV≈0.84
lr=0.1(標準)
最良付近
CV≈0.85
lr=0.3(高め)
やや過学習
CV≈0.83
lr=1.0(極端に高い)
過学習
CV≈0.65

経験則: n_estimators × learning_rate ≈ 一定 → lr を下げる分 n_estimators を増やす
Kaggle では lr=0.01〜0.05 + n_estimators=500〜1000 + early_stopping が定石

📊 3モデルの過学習比較(訓練精度 vs CV精度)

過学習度 = 訓練精度 − CV精度。値が大きいほど汎化しにくい

精度 0.80 0.85 0.90 0.95 1.00 RandomForest CV≈0.84 訓練≈0.97 GBM(sklearn) CV≈0.85 XGBoost CV≈0.86 訓練≈0.93

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での取扱い
Pclassint1, 2, 3旅客クラス数値としてそのまま使用可
Sex_encint0 / 1性別のラベルエンコード最重要特徴量
Agefloat1〜80年齢NaN あっても自動処理 ✓
SibSpint0〜8同乗兄弟/配偶者数低重要度
Farefloat0〜∞運賃対数変換推奨(外れ値対策)
Survivedint0 / 1生存(目的変数)

XGBoost の欠損値自動処理の仕組み

分岐条件 NaN サンプル 左ノード 右ノード ↑ 精度が高い方向へ自動分岐

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 の比較

  1. RandomForestClassifierGradientBoostingClassifierXGBClassifier(各 n_estimators=100)を StratifiedKFold(5) で CV 比較せよ
  2. 訓練精度と CV 精度の差(過学習度)を算出せよ
  3. 3モデルの特徴を一言で説明せよ

問2 — 学習率 (learning_rate) の効果

  1. learning_rate[0.001, 0.01, 0.05, 0.1, 0.3, 1.0] で変化させ CV スコアを記録せよ(n_estimators=100 固定)
  2. 学習率が低い・高い場合の挙動を説明せよ
  3. 学習率と n_estimators がトレードオフである理由を説明せよ

問3 — n_estimators × learning_rate の組み合わせ

  1. 以下の組み合わせで CV スコアを比較せよ: (50, 0.3) / (100, 0.1) / (300, 0.05) / (500, 0.01)
  2. n_estimators × learning_rate = 一定 という経験則を確認せよ

問4 — max_depth と min_child_weight の効果

  1. max_depth[2, 3, 4, 6, 8] で変えながら CV スコアと学習時間を記録せよ
  2. XGBoost で max_depth が深いと何が起こるかを RF と比較して説明せよ
  3. min_child_weight の役割を説明せよ

問5 — 特徴量重要度と欠損値処理

  1. XGBoost の feature_importances_importance_type='gain')を取得せよ
  2. Titanic データに欠損値(NaN)を 20% 注入し、XGBoost が NaN を自動処理することを確認せよ
  3. 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 は少しずつ慎重に補正する。

ブースティングラウンド 損失 η=1.0(不安定) η=0.3 η=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 との速度/精度比較

📋 自己評価(解いた後に記入)

✍️

理解度: [ ] 完全理解   [ ] おおむね理解   [ ] 要復習

自分の回答:

気づき・メモ: