Day 092 — XGBoost② パラメータ — max_depth・eta・subsample の意味

2026-07-12 青 / Phase 4 理論→コーディング max_depth / eta / subsample

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

🔵
Day 091 では xgb.train / xgb.cv の「動かし方」を学びました。今日はその中で当たり前のように使っていた params 辞書の中身——max_depth・eta・subsample——が実際に何をコントロールしているのかを理解します。Kaggleノートブックで必ず目にする3大パラメータです。
用語直感的な意味
max_depth1本の決定木が「質問を何回重ねてよいか」の上限。Phase 2 の決定木の深さと同じ概念
eta(学習率)新しい木を1本追加するたびに、その木の予測を「どれだけ本気で採用するか」の倍率
subsample各木を作る際に訓練データの何%をランダムに使うか。ランダムフォレストのバギングと同じ発想
(関連)colsample_bytreesubsampleの「列版」。各木が使う特徴量の割合をランダムに間引く

🎛️ 3つのツマミ(大きくすると何が起きるか)

max_depth ↑

1本の木がより細かい条件分岐を持てる。訓練データの細部まで記憶しやすくなり、過学習リスク↑

Tabularでは 3〜8 が典型

eta ↓

1本の木の発言力が弱まる分、多くの木を積み上げて慎重に学習。安定するが木の本数が必要

大きめ 0.1〜0.3 で高速探索、小さめ 0.01〜0.05 で本番仕上げ

subsample ↓

各木が見るデータを間引き、木ごとの多様性を生む。ランダムフォレストのバギングと同じ発想で過学習抑制

0.6〜1.0 の範囲で試すのが一般的
⚠️
3つは独立していない。特に etanum_boost_round(何本の木を許すか)は強く連動する——etaを下げたら本数の上限も引き上げないと未学習で終わる。

🎯 問題

Part A: 理論(自分の言葉で説明してみる)

1
eta0.3 から 0.05 に下げると、必要な num_boost_round(木の本数)はどう変化するか。理由も含めて説明する
2
max_depth を際限なく大きくすると何が起きるか。「過学習」という言葉を使って説明する
3
subsample=0.8 がなぜ過学習対策になるのか。ランダムフォレストの「バギング」との共通点から説明する

Part B: コーディング(Day 091の家賃データを使用)

使用データDay091と同じ 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)
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
eta[0.3, 0.1, 0.05]max_depth[2, 4, 6] の全9通りについて xgb.cv(5-fold, early_stopping_rounds=30, num_boost_round=1000)を実行し、各組み合わせの「CVベストラウンド」「CVベストRMSE」を記録する
2
結果を DataFrame にまとめ、CVベストRMSEが最小の組み合わせを表示する
3
eta が小さいほど「CVベストラウンド」がどう変化するか確認する(Part A 設問1の裏付け)
4
最良の eta / max_depth に対し subsample=1.00.8 を比較する

📊 eta を下げると必要な木の本数はどう変わるか(出力例)

模範解答コードを実行した際の出力例(乱数シードにより多少前後する)。1歩の大きさ(eta)が小さいほど、目的地に着くまでの歩数(best_round)が増える。

eta = 0.30
約28本
大股・少ない歩数
eta = 0.10
約84本
中間
eta = 0.05
約169本
小股・多い歩数
💡
本数が増えても、early_stopping_roundsがあるため学習は自動で適切なところで止まる。「小さいetaは遅いが安定」「大きいetaは速いが不安定になりやすい」がこのグラフから読み取れる。

💡 ヒント

ヒント1方向性

3つのパラメータはそれぞれ独立した「ツマミ」だが、影響し合う。etaを小さくする=一歩を小さくする、ということは、同じ目的地(最適な精度)にたどり着くまでの歩数(木の本数)が増えるはず。max_depthは「1本の木の複雑さ」、subsampleは「1本の木が見るデータの量」——どちらも大きい/多いほど1本の木が強力になり、過学習しやすくなる。

ヒント2アプローチ
  • 組み合わせの総当たり: itertools.product(etas, max_depths)
  • ベストRMSE/ラウンド: cv_results["test-rmse-mean"].min() / .idxmin()
  • 結果は辞書のリストに貯めて pd.DataFrame(results_list) にする
  • 並べ替え: sort_values("best_rmse")
ヒント3コード骨格
import itertools
import xgboost as xgb

dtrain = xgb.DMatrix(df[["area_m2", "age_years", "station_dist_min"]], label=df["rent_man_yen"])

etas = [0.3, 0.1, 0.05]
max_depths = [2, 4, 6]

results = []
for eta, depth in itertools.product(___, ___):
    params = {
        "objective": "reg:squarederror",
        "eta": eta,
        "max_depth": depth,
        "eval_metric": "rmse",
        "seed": 42,
    }
    cv_results = xgb.cv(
        params, dtrain,
        num_boost_round=___,
        nfold=5,
        early_stopping_rounds=___,
        metrics="rmse",
        as_pandas=True,
        seed=42,
    )
    best_round = cv_results["test-rmse-mean"].___()
    best_rmse = cv_results["test-rmse-mean"].___()
    results.append({"eta": eta, "max_depth": depth, "best_round": best_round, "best_rmse": best_rmse})

results_df = pd.DataFrame(results).sort_values(___)
print(results_df)

模範解答

Part A: 理論の模範解答

設問模範解答
1. eta を下げると本数は?1本あたりの補強が弱まる分、同じだけ誤差を減らすのにより多くの木が必要になる(best_iteration増加)。一方、たまたまのクセに引きずられにくく最終精度は安定しやすい
2. max_depth を大きくすると?1本の木が訓練データの細部(ノイズ含む)まで記憶できるようになり、訓練スコアは良化するがテストスコアは悪化する=過学習が起きやすい
3. subsample=0.8 が過学習対策になる理由ランダムフォレストのバギングと同じ発想。各木が訓練データの80%だけをランダムに見ることで、木ごとの多様性が生まれ、1本が全データの細部を覚えすぎるのを防ぐ

Part B: コーディングの模範解答

import numpy as np
import pandas as pd
import xgboost as xgb
import itertools

# --- データ準備(Day 091と同じ) ---
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,
})

dtrain = xgb.DMatrix(df[["area_m2", "age_years", "station_dist_min"]], label=df["rent_man_yen"])

# --- 1&2. eta x max_depth のグリッド探索 ---
etas = [0.3, 0.1, 0.05]
max_depths = [2, 4, 6]

results = []
for eta, depth in itertools.product(etas, max_depths):
    params = {
        "objective": "reg:squarederror",
        "eta": eta,
        "max_depth": depth,
        "eval_metric": "rmse",
        "seed": 42,
    }
    cv_results = xgb.cv(
        params, dtrain,
        num_boost_round=1000,
        nfold=5,
        early_stopping_rounds=30,
        metrics="rmse",
        as_pandas=True,
        seed=42,
    )
    best_round = cv_results["test-rmse-mean"].idxmin()
    best_rmse = cv_results["test-rmse-mean"].min()
    results.append({"eta": eta, "max_depth": depth, "best_round": best_round, "best_rmse": best_rmse})

results_df = pd.DataFrame(results).sort_values("best_rmse").reset_index(drop=True)
print(results_df)

best_row = results_df.iloc[0]
print(f"\nベスト組み合わせ: eta={best_row['eta']}, max_depth={best_row['max_depth']}, "
      f"best_round={best_row['best_round']}, best_rmse={best_row['best_rmse']:.4f}")

# --- 3. eta と best_round の関係を確認 ---
print("\neta別の平均best_round:")
print(results_df.groupby("eta")["best_round"].mean().sort_index(ascending=False))

# --- 4. subsampleの比較 ---
for subsample in [1.0, 0.8]:
    params = {
        "objective": "reg:squarederror",
        "eta": best_row["eta"],
        "max_depth": int(best_row["max_depth"]),
        "subsample": subsample,
        "eval_metric": "rmse",
        "seed": 42,
    }
    cv_results = xgb.cv(
        params, dtrain,
        num_boost_round=1000,
        nfold=5,
        early_stopping_rounds=30,
        metrics="rmse",
        as_pandas=True,
        seed=42,
    )
    print(f"subsample={subsample}: best_rmse={cv_results['test-rmse-mean'].min():.4f}, "
          f"best_round={cv_results['test-rmse-mean'].idxmin()}")

# 出力例:
#      eta  max_depth  best_round  best_rmse
# 0   0.05          2         210     1.0812
# 1   0.10          2         103     1.0839
# 2   0.05          4          95     1.0921
# ...(eta=0.3, max_depth=6 は過学習気味でRMSEが相対的に悪化しやすい)
#
# eta別の平均best_round:
# eta
# 0.30     28.0
# 0.10     84.3
# 0.05    168.7
#
# subsample=1.0: best_rmse=1.0812, best_round=210
# subsample=0.8: best_rmse=1.07xx, best_round=2xx

🪜 Step-by-Step 解説

1eta x max_depth の全組み合わせをループする

for eta, depth in itertools.product(etas, max_depths):
    ...
🔑
なぜこうするか: パラメータは単独では良し悪しを判断できない。eta=0.05が良いかどうかはmax_depthにも依存するため、複数のパラメータを同時に動かして「組み合わせ」で評価するのがKaggleでの基本姿勢(本格的なチューニングではOptunaを使うが、まずは総当たりの感覚を掴む)。

2各組み合わせで xgb.cv を実行し、結果を貯める

best_round = cv_results["test-rmse-mean"].idxmin()
best_rmse = cv_results["test-rmse-mean"].min()
results.append({...})
🔑
なぜこうするか: Day 091 で学んだ通り、xgb.cvは5-foldの平均スコアを返すため、1回のtrain/test分割よりも信頼できる評価が得られる。結果を辞書のリストに貯めてDataFrame化することで、9通りの結果を一覧で比較できる。

3eta と best_round の関係を確認する

results_df.groupby("eta")["best_round"].mean().sort_index(ascending=False)
🔑
なぜこうするか: Part A 設問1で理論的に説明した「etaが小さいほど木の本数が必要」という関係を、実際の数値で裏付ける。eta=0.3では数十本、eta=0.05ではその数倍の本数が必要になるはず。

4subsample を変えて過学習抑制効果を見る

for subsample in [1.0, 0.8]:
    ...
💡
なぜこうするか: subsample単体の効果を見るために、他のパラメータ(最良のeta, max_depth)は固定してsubsampleだけを変える。1つの変数だけを動かして純粋な影響を観察する「対照実験」の考え方。

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

📐
etaは「階段を一段ずつ上るときの一歩の大きさ」だと考えてよい。eta=0.3は大股で一気に上る(速いが着地がズレやすい)、eta=0.05は小股で慎重に上る(時間はかかるが正確に目的の高さに到達しやすい)。max_depthは「1人の専門家が答えられる質問の深さ」——深すぎる専門家は「今回のテストに出た特殊な問題」まで覚えてしまう。

身近な例: 抽選で選ばれる80人

subsample=0.8は、100人のクラスから毎回ランダムに80人だけを選んでテストの傾向分析をするようなもの。同じ80人を使わず毎回メンバーを入れ替えることで、「たまたまその80人に多かった特徴」に分析結果が引きずられにくくなる。これが複数の木に渡って行われることで、モデル全体としてより汎用的な判断ができるようになる。

もし数式を見たら

F_m(x) = F_(m-1)(x) + η · h_m(x)

「$m$本目の木を足した後の予測 = $m-1$本目までの予測 + $\eta$(eta)倍した新しい木$h_m$の予測」という意味。$\eta$が1に近いほど新しい木の意見をそのまま採用し、0に近いほど「ちょっとだけ参考にする」形になる。これが「学習率」と呼ばれる理由——木1本あたりの影響力を調整する数値だから。

🏆 Kaggleでの実践的な使い方

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

Kaggle上位者の典型的な流れは「まずetaをやや大きめ(0.1前後)にして高速に探索 → 有望な特徴量・構造が見えてきたらetaを小さく(0.01〜0.03)してnum_boost_roundを増やし、early_stopping_roundsで自動的に止める」という2段階。ラフスケッチで方向性を決めてから清書は丁寧に描く、のと同じ発想。max_depthはTabularコンペでは3〜8程度が多く使われ、subsample/colsample_bytreeは0.6〜0.9あたりがよく試されるレンジ。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
etaを小さくすればするほど精度が上がると信じ、num_boost_roundを増やさずに使う「小さい=慎重=良い」という単純な連想etaを小さくしたらnum_boost_round(とearly_stopping_roundsの許容幅)も増やさないと、木の本数が足りず未学習で終わる
max_depthを大きくすれば表現力が上がって常に良いと思う訓練スコアが上がるのを見て「良くなった」と誤認する訓練スコアの向上とテストスコアの向上は別物。max_depthが大きすぎるとテストスコアはむしろ悪化する(過学習)
subsampleを下げれば下げるほど良いと思う「ランダム性=汎化性能向上」という過度な一般化データが少なすぎる場合、下げすぎると各木が学習に使えるデータが不足し逆に精度が落ちることがある。0.6〜1.0の範囲で試すのが一般的
パラメータを1つずつ順番に最適化しようとする直列に考える方が理解しやすいためパラメータ同士は相互作用する(特にetaと本数)ため、単独最適化では真の最適解を見逃しやすい。グリッドサーチやOptuna(Day 097-098)で同時に探索するのが望ましい

🚀 次のステップ

  • 発展: colsample_bytree(列のサブサンプリング)も加えた3パラメータのグリッド探索を試してみる
  • 次回予告: Day 093「LightGBM① 実装」 — XGBoostとの違いと、なぜLightGBMの方が高速なのかを理論と実装の両面から学ぶ

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

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