Day 091 — XGBoost① 実装 — DMatrix・早期終了・CVの基本

2026-07-11 青 / Phase 4 コーディング DMatrix / early stopping / xgb.cv

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

🔵
Day 090 で学んだ「勾配ブースティング(木を1本ずつ、残差を埋めるように直列に足していく)」を、今日は XGBoost というライブラリで実際にコードとして動かします。Kaggleの表形式データコンペで長年トップクラスの成績を残してきた定番ツールです。

sklearnの LinearRegression と同じ感覚で使える sklearn APIXGBRegressor)もありますが、今日はより細かく制御できる Native APIxgb.train, xgb.cv)を使います。Kaggleノートブックでは Native API の方がよく使われるため、最初に慣れておくと後々有利です。

用語直感的な意味
DMatrixXGBoost専用の内部データ構造。DataFrameより高速・省メモリに学習できるよう最適化されている
watchlist学習中に「訓練データ」と「検証データ」のスコアを両方モニタリングする仕組み
early stopping(早期終了)検証スコアが指定ラウンド数だけ改善しなくなったら学習を自動で打ち切る仕組み
xgb.cvK-Fold CVを内部で自動実行し、各ラウンドの平均スコアを返す関数。最適な木の本数を安定して見積もれる

🔀 実装フローの全体像

DataFrame → DMatrix → 学習/CV → 評価、の一本道。CVは「最適な木の本数」を確かめる別ルート。

🧱 メインの学習フロー DataFrame DMatrix変換 xgb.train + watchlist early stopping best_iteration 🔁 検証ルート(CV) 同じ dtrain xgb.cv(5-fold) test-rmse-mean 最小 上段: 1回の train/test 分割に基づく early stopping(Step 2) 下段: 5-fold の平均で「最適な木の本数」をより安定して見積もる(Step 3)

🎯 問題

以下は「賃貸物件の家賃予測」のサンプルデータです(Day 090 の家賃の例を拡張したもの)。このデータで XGBoost の回帰モデルを構築してください。

使用データn=200
import numpy as np
import pandas as pd

np.random.seed(42)
n = 200

area_m2 = np.random.normal(45, 15, n).clip(15, 100)          # 専有面積(m2)
age_years = np.random.randint(0, 35, n)                       # 築年数
station_dist_min = np.random.normal(8, 4, n).clip(1, 25)      # 最寄り駅から徒歩(分)

rent_man_yen = (
    3.0
    + area_m2 * 0.25
    - age_years * 0.05
    - station_dist_min * 0.15
    + np.random.normal(0, 1.0, n)
).clip(3.0, None)

df = pd.DataFrame({
    "area_m2": area_m2,
    "age_years": age_years,
    "station_dist_min": station_dist_min,
    "rent_man_yen": rent_man_yen,
})
カラム名意味
area_m2専有面積(平米)float
age_years築年数int
station_dist_min最寄り駅からの徒歩時間(分)float
rent_man_yen家賃(万円・目的変数)float

タスク

1
df を特徴量 X と目的変数 y に分け、train_test_split で訓練80%・テスト20%に分割する
2
訓練・テストをそれぞれ xgb.DMatrix に変換する
3
params 辞書を定義し、xgb.train を watchlist・early_stopping_rounds=20 付きで実行する。best_iteration を確認する
4
xgb.cv で5-fold CVを実行し、CVベースの最適ラウンド数と平均RMES(test-rmse-mean)を求める
5
テストRMSEを計算し、「訓練データの平均値で予測した場合のベースラインRMSE」と比較して改善度を報告する

📊 ベースライン vs XGBoost(RMSE比較イメージ)

数値は模範解答コードを実行した際の出力例。乱数シードにより多少前後する。

ベースライン(平均値予測)
RMSE 約6.9万円
誤差:大きい
XGBoost(early stopping)
約1.1万円
改善率 約85%
💡
「平均値で予測するだけ」と比べてXGBoostがどれだけ誤差を減らせたかを見ることで、モデルの価値を定量的に説明できる。RMSEの絶対値だけを見て「良い/悪い」を判断しないこと。

💡 ヒント

ヒント1方向性

DMatrixは「numpy配列やDataFrameをXGBoost専用の高速フォーマットに変換したもの」というだけで、中身の考え方はsklearnのX, yと同じ。xgb.trainxgb.cvはどちらも同じparams辞書を受け取るが、xgb.trainは1回の学習、xgb.cvはデータをK分割して複数回学習・評価を繰り返す点が違う。early stoppingは「検証スコアの改善が止まったら止める」というシンプルなルール。

ヒント2アプローチ
  • DMatrix変換: xgb.DMatrix(X_train, label=y_train)
  • watchlist: [(dtrain, "train"), (dtest, "eval")]
  • 学習: xgb.train(params, dtrain, num_boost_round=500, evals=watchlist, early_stopping_rounds=20, verbose_eval=False)
  • 学習後の本数確認: bst.best_iteration
  • 予測: bst.predict(dtest, iteration_range=(0, bst.best_iteration + 1))
  • CV実行: xgb.cv(params, dtrain, num_boost_round=500, nfold=5, early_stopping_rounds=20, metrics="rmse", as_pandas=True, seed=42)
  • 最適ラウンド: cv_results["test-rmse-mean"].idxmin()
ヒント3コード骨格
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

X = df[["area_m2", "age_years", "station_dist_min"]]
y = df["rent_man_yen"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

dtrain = xgb.DMatrix(___, label=___)
dtest = xgb.DMatrix(___, label=___)

params = {
    "objective": "reg:squarederror",
    "eta": 0.1,
    "max_depth": 4,
    "eval_metric": "rmse",
    "seed": 42,
}

watchlist = [(___, "train"), (___, "eval")]

bst = xgb.train(
    params, dtrain,
    num_boost_round=500,
    evals=watchlist,
    early_stopping_rounds=___,
    verbose_eval=False,
)

print("best_iteration:", bst.best_iteration)

cv_results = xgb.cv(
    params, dtrain,
    num_boost_round=500,
    nfold=___,
    early_stopping_rounds=20,
    metrics="rmse",
    as_pandas=True,
    seed=42,
)
best_round = cv_results["test-rmse-mean"].___()
print("CVベスト:", best_round, cv_results.loc[best_round, "test-rmse-mean"])

模範解答

import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# --- データ準備 ---
np.random.seed(42)
n = 200
area_m2 = np.random.normal(45, 15, n).clip(15, 100)
age_years = np.random.randint(0, 35, n)
station_dist_min = np.random.normal(8, 4, n).clip(1, 25)
rent_man_yen = (
    3.0 + area_m2 * 0.25 - age_years * 0.05 - station_dist_min * 0.15
    + np.random.normal(0, 1.0, n)
).clip(3.0, None)

df = pd.DataFrame({
    "area_m2": area_m2,
    "age_years": age_years,
    "station_dist_min": station_dist_min,
    "rent_man_yen": rent_man_yen,
})

# --- 1. train/testに分割 ---
X = df[["area_m2", "age_years", "station_dist_min"]]
y = df["rent_man_yen"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# --- 2. DMatrixに変換 ---
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

# --- 3. パラメータ定義と学習(watchlist + early stopping) ---
params = {
    "objective": "reg:squarederror",
    "eta": 0.1,
    "max_depth": 4,
    "eval_metric": "rmse",
    "seed": 42,
}
watchlist = [(dtrain, "train"), (dtest, "eval")]

bst = xgb.train(
    params, dtrain,
    num_boost_round=500,
    evals=watchlist,
    early_stopping_rounds=20,
    verbose_eval=False,
)
print("best_iteration:", bst.best_iteration)
print("best_score (eval RMSE):", bst.best_score)

# --- 4. xgb.cvで5-fold CV ---
cv_results = xgb.cv(
    params, dtrain,
    num_boost_round=500,
    nfold=5,
    early_stopping_rounds=20,
    metrics="rmse",
    as_pandas=True,
    seed=42,
)
best_round = cv_results["test-rmse-mean"].idxmin()
print("CVベストラウンド:", best_round)
print("CVベストRMSE:", cv_results.loc[best_round, "test-rmse-mean"])

# --- 5. テストRMSE vs ベースラインRMSE ---
y_pred = bst.predict(dtest, iteration_range=(0, bst.best_iteration + 1))
xgb_rmse = mean_squared_error(y_test, y_pred, squared=False)

baseline_pred = np.full_like(y_test, fill_value=y_train.mean(), dtype=float)
baseline_rmse = mean_squared_error(y_test, baseline_pred, squared=False)

print(f"ベースラインRMSE(平均値予測): {baseline_rmse:.3f} 万円")
print(f"XGBoost RMSE: {xgb_rmse:.3f} 万円")
print(f"改善率: {(1 - xgb_rmse / baseline_rmse) * 100:.1f}%")

# 出力例:
# best_iteration: 34
# best_score (eval RMSE): 1.07...
# CVベストラウンド: 29
# CVベストRMSE: 1.15...
# ベースラインRMSE(平均値予測): 6.9xx 万円
# XGBoost RMSE: 1.0x 万円
# 改善率: 8x.x%

🪜 Step-by-Step 解説

1DMatrixへの変換

dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
🔑
なぜこうするか: XGBoostは内部でヒストグラムベースの分岐探索など独自の最適化を行っており、そのために専用のデータ構造(DMatrix)を要求する。sklearn APIのXGBRegressor.fit(X, y)を使えばこの変換は自動で行われるが、Native APIでは明示的に変換することでメモリ使用量や前処理を細かく制御できる。

2watchlistとearly stoppingで学習を制御する

watchlist = [(dtrain, "train"), (dtest, "eval")]
bst = xgb.train(params, dtrain, num_boost_round=500,
                 evals=watchlist, early_stopping_rounds=20, verbose_eval=False)
🔑
なぜこうするか: Day 090 で「木を増やしすぎると過学習する」と学んだ。num_boost_round=500と多めに設定しても、early_stopping_rounds=20によって「evalスコアが20ラウンド連続で改善しなければ自動で止める」ため、過学習が始まる直前で学習を打ち切れる。bst.best_iterationが実際に使われた木の本数。

3xgb.cvで「何本の木が最適か」を安定して見積もる

cv_results = xgb.cv(params, dtrain, num_boost_round=500, nfold=5,
                     early_stopping_rounds=20, metrics="rmse", as_pandas=True, seed=42)
best_round = cv_results["test-rmse-mean"].idxmin()
🔑
なぜこうするか: Step 2のearly_stopping_roundsは「1回のtrain/test分割」だけを見て止め時を決めているため、たまたまその分割のクセに引っ張られる可能性がある。xgb.cvはデータを5分割して5回学習・評価を繰り返し、各ラウンドの平均スコアを返すため、より信頼できる「最適な木の本数」の見積もりが得られる。Kaggleでは、このCV結果をもとに最終的なnum_boost_roundを決め、全訓練データで再学習する流れがよく使われる。

4ベースラインとの比較で「モデルの価値」を数値化する

baseline_pred = np.full_like(y_test, fill_value=y_train.mean(), dtype=float)
baseline_rmse = mean_squared_error(y_test, baseline_pred, squared=False)
💡
なぜこうするか: RMSEの値単体では「良いのか悪いのか」が判断しにくいため、「何も学習せず、訓練データの平均値で予測した場合」をベースラインとして必ず比較する。これはDay 090の「Step 0(平均で予測)」に相当し、XGBoostがそこからどれだけ改善したかを見ることで、モデルの価値を定量的に説明できる。

🧮 数学・統計の補足(文系向け)

📐
early_stopping_rounds=20 は「20回連続で成績が伸びなかったら、そこで練習をやめる」というルールだと考えてよい。ずっと練習を続けると、そのうち「本番では出ない過去問の細かいクセ」まで覚え始めてしまう(=過学習)。伸びが止まった時点でやめることで、ちょうど良いところで練習を打ち切れる。

身近な例: 模擬試験の伸び止まり

模擬試験の点数が5回連続で伸びなければ、「今のやり方はもう頭打ちだから、この時点の実力を採用しよう」と判断するのに似ている。XGBoostは「評価用データでの点数(eval RMSE)」を模擬試験のスコアとして、それが伸びなくなった時点の木の本数を採用する。

もし数式を見たら

RMSE = √( (1/n) Σ (yᵢ − ŷᵢ)² )

「予測と正解のズレ(誤差)を2乗して平均し、最後にルートを取ったもの」。2乗することで正負の誤差が打ち消し合わないようにし、ルートを取ることで単位を元の家賃(万円)のスケールに戻す。「平均してだいたい何万円ズレているか」の目安になる。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☑ 時系列(Time Series)

Kaggleの多くのTabularコンペでは、まず「素の特徴量 + XGBoostのデフォルトに近いパラメータ + early stopping」でベースラインスコアを素早く作り、そこから特徴量エンジニアリング(Phase 4後半で学ぶAggregation・TargetEncodingなど)を積み重ねてスコアを伸ばしていくのが定石。xgb.cvによるCVベースの木の本数決定は、Public LBに過剰適合しないための基本動作でもある。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
num_boost_roundを大きくすればするほど精度が上がると思う木の本数=モデルの複雑さ、という単純な連想early stoppingがなければ過学習して逆にテストスコアが悪化する。num_boost_roundは「上限」であり、実際に使う本数はbest_iterationで決まる
xgb.trainのearly stoppingとxgb.cvの結果を混同するどちらも「最適な木の本数」を出すため似て見えるxgb.trainは1回の分割に基づく結果、xgb.cvは5分割の平均に基づく結果。CVの方が信頼性が高く、最終的な本数決定にはCV結果を使うのが安全
DMatrix変換を忘れてDataFrameをそのままxgb.trainに渡そうとするsklearn APIとの混同Native API(xgb.train, xgb.cv)は必ずDMatrixが必要。sklearn API(XGBRegressor)なら内部で自動変換される
RMSEの数値だけを見て「良いモデルだ」と判断してしまう絶対値だけでは基準がない必ずベースライン(平均値予測など単純な手法)と比較し、相対的な改善度で評価する

🚀 次のステップ

  • 発展: max_depthetaを変えてCV結果(test-rmse-mean)がどう変化するか試してみる
  • 次回予告: Day 092「XGBoost② パラメータ」max_depthetasubsampleなど主要ハイパーパラメータの意味と役割を理論と実装の両面から学ぶ

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

自分の回答・気づき・メモ: