GBDT(Gradient Boosting Decision Tree/勾配ブースティング決定木)は、浅い決定木を順に追加し、それまでのモデル全体の予測に対する損失を減らしていくアンサンブル学習の手法です。2025年1月にNatureへ載ったTabPFNの論文も、要旨で「gradient-boosted decision trees … have dominated tabular data for the past 20 years」と書いています。この記事では仕組みを式の意味から説明したうえで、代表的な4実装(XGBoost・LightGBM・CatBoost・scikit-learnのHistGradientBoosting)を同一データで動かした学習時間とAUC、欠損値とカテゴリ変数の扱いの差、そして古い解説記事のコードが現行版で動かなくなっている箇所までを、実行したコードと出力で示します。
まとめ:GBDTの要点と実装選択の結論
- GBDTは、損失関数の負の勾配(二乗誤差なら残差)を当てにいく浅い決定木を、学習率を掛けながら1本ずつ足していく加法モデルです。
- 実装はXGBoost 3.4.1・LightGBM 4.7.0・CatBoost 1.2.10・scikit-learn 1.9.1のHistGradientBoostingの4系統で、同一データの実測では学習時間1.17秒から19.73秒、AUCは0.9884から0.9921に収まりました。今回のAUC差だけでは実務上の優劣は決められません。選定では必要な精度を満たすかを先に確かめ、そのうえでカテゴリ列の扱いと学習時間を見ます。
- 迷ったらLightGBMかXGBoostから始め、カテゴリ列が多ければCatBoost、追加インストールを避けたいならscikit-learnのHistGradientBoostingという順で検討します。scikit-learnの旧
GradientBoostingClassifierは今回の人工データでは161.59秒かかりAUCも最下位でした。500行では3シード平均のAUCが0.9423対0.9444とHistGradientBoostingとほぼ並びますが、2,000行では0.9479対0.9649と離れます。少量データでも優位には立ちません。 - 数値の欠損はXGBoost・LightGBM・HistGradientBoosting・CatBoostがそのまま学習でき、補完は不要です。旧
GradientBoostingClassifierだけがValueErrorで止まります。 - 古い解説記事のうち、廃止された引数やGPU指定を使うコードは、現行版に合わせた修正が必要です。
fit(early_stopping_rounds=...)はXGBoost・LightGBMともTypeErrorになり、GPU指定もtree_method="gpu_hist"からdevice="cuda"へ変わりました。 - 葉が定数の通常のGBDTは、学習範囲外にも予測値を返しますが、学習した傾きをそのまま延長できません。時点だけを特徴量にした上昇トレンドの例では、範囲外の予測が一定になります。
以下の実測は、2026年9月23日にmacOS 26.6.2(Intel Core i9-9880H・2.30GHz・16論理コア・メモリ16GB)のPython 3.13.14で1回ずつ計測したものです。AUCは乱数シードを固定しているので再実行しても一致しますが、学習時間は同じ機材でも同時に動いている処理の影響で1.5倍から2倍ぶれます。時間は桁の比較として読み、各自の環境で測り直してください。
GBDTの学習手順:残差の方向へ浅い木を1本ずつ足す加法モデル
GBDTは、1本で当てにいく手法ではありません。まず全体の平均のような粗い予測から始め、その予測が外した分(残差)を説明する小さな決定木を作って足す。足した結果でまた残差を計算し、次の木を作る。これを数百回繰り返して作った木の総和が最終的な予測になります。式で書けば、m回目の予測器は直前の予測器に新しい木を加えたF_m(x) = F_(m-1)(x) + v * h_m(x)で、vが学習率、h_mがm本目の木です。
「勾配」と付くのは、木が当てにいく対象が単純な引き算の残差ではなく、損失関数を現在の予測で微分した負の勾配だからです。損失を二乗誤差の半分と定義すると、負の勾配は残差(実測値マイナス予測値)と一致します。二乗誤差をそのまま使う定義では残差の2倍になり、この定数倍は更新幅に吸収できます。この一般化を与えたのがJerome H. Friedmanの論文「Greedy function approximation: A gradient boosting machine」(The Annals of Statistics 29巻5号、2001年、DOI 10.1214/aos/1013203451)で、損失関数を差し替えるだけで同じ枠組みを使い回せるようになりました。要旨に挙がっているのは回帰の二乗誤差・絶対偏差・Huber損失と、分類の多クラスロジスティック尤度です。現在のXGBoost・LightGBM・CatBoostはいずれもこの枠組みの実装です。
ランダムフォレストとの違いは、木の作り方そのものにあります。ランダムフォレストは深い木を互いに独立に作って平均するバギングで、木の間に順序がありません。GBDTは、それまでのモデル全体の予測に基づいて次の木を学習する逐次的な手法です。木の深さや葉数は実装とデータに応じて制限します。1本ずつ弱い木を足して損失を削っていく構造上、学習率と本数の組み合わせを誤ると過学習します。
学習率と木の本数のトレードオフ
学習率を下げるほど1本あたりの寄与が小さくなり、同じ精度に到達するのに必要な木の本数が増えます。XGBoostの既定値は学習率0.3・木100本ですが、これは「まず動かす」ための値です。50,000行・50列の人工データでこの2つだけを振ると、次のようになりました。
| 学習率 | 木50本 | 木100本 | 木300本 | 木1000本 |
|---|---|---|---|---|
| 0.3(既定) | AUC 0.9889 / 1.0秒 | AUC 0.9902 / 1.7秒 | AUC 0.9918 / 4.6秒 | AUC 0.9922 / 12.9秒 |
| 0.1 | AUC 0.9826 / 1.0秒 | AUC 0.9879 / 1.8秒 | AUC 0.9909 / 4.5秒 | AUC 0.9922 / 14.5秒 |
| 0.05 | AUC 0.9736 / 1.1秒 | AUC 0.9826 / 1.9秒 | AUC 0.9893 / 4.6秒 | AUC 0.9916 / 14.1秒 |
学習率0.3で木100本のとき0.9902だったAUCは、学習率0.05に下げると0.9826まで落ち、同じ水準に戻すには木を300本以上に増やす必要がありました。到達点はどの学習率でも0.992前後で頭打ちになり、そのぶん学習時間は12秒から14秒へ伸びています。学習率を下げれば精度が上がるのではなく、下げたぶん本数で買い戻す関係です。
探索の順序は、学習率を0.05から0.1に固定してから木の本数を伸ばし、頭打ちになった時点でmax_depthやnum_leavesへ移る、が実務的です。本数の打ち切りは手で決めずに後述のearly stoppingへ任せます。探索方法そのものの選び方はハイパーパラメータ探索とは?手法の選び方と打ち切り設計を実装目線で解説にまとめています。
主要4実装の設計差:バージョン・既定値・木の伸ばし方
主要4実装のバージョンと既定値(2026年9月)
ここではGBDTの代表的な4実装を比較します。PyPIの最新版と、各ライブラリが公表している既定値は次のとおりです(バージョンは2026年9月23日時点)。XGBoostはGitHubに3.4.2のパッチリリース(2026年9月15日)がありますが、PyPIに出ているのは3.4.1までです。
| 実装 | 最新版 | 木の伸ばし方 | 既定の木の本数 | 既定の学習率 | 木の深さ・葉の既定 |
|---|---|---|---|---|---|
| XGBoost | 3.4.1 | level-wise(depthwise) | 100 | 0.3 | max_depth=6 |
| LightGBM | 4.7.0 | leaf-wise | 100 | 0.1 | num_leaves=31 |
| CatBoost | 1.2.10 | 対称木(SymmetricTree) | 1000 | 条件を満たすと自動・他は0.03 | depth=6 |
| scikit-learn HistGradientBoosting | 1.9.1 | 葉数上限つき(LightGBM系) | 100(max_iter) | 0.1 | max_leaf_nodes=31 |
数字がそろっていないのは設計思想の差です。CatBoostの既定の反復回数は1000です。Logloss・MultiClass・RMSEでは、葉の推定や正則化に関する指定条件を満たすと、データの性質と反復回数から学習率を自動設定し、それ以外の既定値は0.03です。実際、20列の人工データで学習後のget_all_params()を読むと、学習率は200行で0.00518、2,000行で0.01385、30,000行で0.04402、50,000行で0.05475と変わりました。他実装と条件をそろえて比べるときは明示的に指定してください。XGBoostの既定の学習率は0.3ですが、適切な値はデータと反復回数によって変わります。既定値を基準に、検証データで学習率と反復回数を調整してください。scikit-learnのHistGradientBoostingは公式ドキュメントが「inspired by LightGBM」と明記しているとおりLightGBM系の設計で、反復回数の上限はmax_iterで指定します。二値分類では1反復につき1本、多クラス分類ではクラス数分の木を学習するため、反復回数と木の総数は区別が必要です。early stoppingなどの既定動作もLightGBMとは異なります。すでにscikit-learnを導入済みの環境なら、HistGradientBoostingは別のGBDTライブラリを追加せずに使え、Scikit-learnとは?読み方・できること・使い方を実行例で解説で扱った標準のAPIのまま使えます。
leaf-wiseとlevel-wise:木の伸ばし方の違い
木を1段ずつ均等に深くするのがlevel-wise、損失が最も減る葉だけを選んで伸ばすのがleaf-wiseです。LightGBM公式は「LightGBM grows trees leaf-wise (best-first). It will choose the leaf with max delta loss to grow.」と説明し、葉の数を固定したときleaf-wiseのほうが損失が下がりやすいとしています。同じページには「Leaf-wise may cause over-fitting when #data is small」という注意と、「However, trees still grow leaf-wise even when max_depth is specified」という但し書きも並びます。つまりLightGBMでmax_depthを指定しても伸ばし方はleaf-wiseのままで、過学習を抑える主役はnum_leaves(既定31、上限131072)です。
CatBoostは、各深さで同じ分割条件を使ってその段の葉を分割する対称木を既定にしています。同じ深さでは使える分割構造が制限されますが、規則的な構造は推論を高速化しやすくします。CatBoost公式は「Symmetric trees have a very good prediction speed (roughly 10 times faster than non-symmetric trees) and give better quality in many cases」と説明しています。学習済みモデルのパラメータを確認するとgrow_policyがSymmetricTreeであることが読み取れます。この設計差と実装ごとの使い分けはCatBoostとは?特徴・XGBoost・LightGBMとの違いとPython実装を解説で詳しく扱っています。
同一データでの学習時間とAUC
同じデータを各実装の既定の学習設定で処理し、初期設定のまま使った場合の学習時間とAUCを比較します。木の本数や学習率が異なるため、設計差だけを切り分ける実験ではありません。50,000行・50列(情報を持つ特徴量15列、その線形結合による冗長特徴量10列、ノイズ特徴量25列)の人工データを25%だけテストに残し、4実装に旧GradientBoostingClassifierとランダムフォレストを加えた6つを、乱数シードと一部の並列数・ログ設定を指定し、木の本数や学習率などは各実装の既定値で学習させました。
import time
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from sklearn.ensemble import (GradientBoostingClassifier,
HistGradientBoostingClassifier,
RandomForestClassifier)
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from catboost import CatBoostClassifier
X, y = make_classification(n_samples=50000, n_features=50, n_informative=15,
n_redundant=10, random_state=0)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25, random_state=0)
models = {
"GradientBoosting(旧)": GradientBoostingClassifier(random_state=0),
"HistGradientBoosting": HistGradientBoostingClassifier(random_state=0),
"XGBoost": XGBClassifier(random_state=0, n_jobs=-1),
"LightGBM": LGBMClassifier(random_state=0, n_jobs=-1, verbose=-1),
"CatBoost": CatBoostClassifier(random_seed=0, verbose=0),
"RandomForest(参考)": RandomForestClassifier(random_state=0, n_jobs=-1),
}
for name, model in models.items():
started = time.perf_counter()
model.fit(X_tr, y_tr)
elapsed = time.perf_counter() - started
auc = roc_auc_score(y_te, model.predict_proba(X_te)[:, 1])
print(f"{name:20s} fit={elapsed:6.2f}s AUC={auc:.4f}")
GradientBoosting(旧) fit=161.59s AUC=0.9608
HistGradientBoosting fit= 2.21s AUC=0.9884
XGBoost fit= 1.56s AUC=0.9902
LightGBM fit= 1.17s AUC=0.9886
CatBoost fit= 19.73s AUC=0.9921
RandomForest(参考) fit= 7.88s AUC=0.9885
ROC AUCは、陽性と陰性をスコアで順位付けする性能の指標です。今回の4実装では0.9884から0.9921で、最大差は0.0037でした。一方で学習時間は1.17秒から19.73秒まで17倍近く開きました。CatBoostの学習時間には反復回数の差が含まれます。同じデータでiterations=100にそろえて測り直すと2.49秒・AUC 0.9903まで縮みました。注目すべきは旧GradientBoostingClassifierの161.59秒で、最速のLightGBMの138倍です。しかも精度はAUC 0.9608と最下位でした。今回の50,000行の人工データでは、学習時間とAUCの両面でヒストグラム法を使う実装を優先する結果になりました。
実装選びの結論を言えば、まずLightGBMかXGBoostで回し、カテゴリ列が主役ならCatBoost、環境に追加できるライブラリが限られるならHistGradientBoostingです。この差はデータの形(行数・列数・カテゴリの水準数)で入れ替わるので、候補2つを同じ交差検証の分割で比較し、精度のばらつきと学習時間を確認して選びます。選択後の性能は、モデル選択に使っていないテストデータで評価してください。
前処理が減る範囲:欠損値とカテゴリ変数のネイティブ対応
欠損値:補完なしで学習できる実装とできない実装
GBDTは欠損値を埋めずに学習できる、とよく紹介されます。正確には「実装によって違う」が答えです。scikit-learnの古いGradientBoostingClassifierだけは数値の欠損を受け付けず、XGBoost・LightGBM・HistGradientBoostingに加えてCatBoostもそのまま学習します。1割を欠損に置き換えたデータで確かめます(掲載コードは差が出る4つに絞っています)。
import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier
from lightgbm import LGBMClassifier
from xgboost import XGBClassifier
X, y = make_classification(n_samples=5000, n_features=20, random_state=0)
rng = np.random.default_rng(0)
X[rng.random(X.shape) < 0.10] = np.nan # 1割を欠損にする
for name, model in [("GradientBoosting", GradientBoostingClassifier(random_state=0)),
("HistGradientBoosting", HistGradientBoostingClassifier(random_state=0)),
("XGBoost", XGBClassifier(random_state=0)),
("LightGBM", LGBMClassifier(random_state=0, verbose=-1))]:
try:
model.fit(X, y)
print(f"{name:20s} OK")
except ValueError as e:
print(f"{name:20s} ValueError: {str(e).splitlines()[0][:60]}")
GradientBoosting ValueError: Input X contains NaN.
HistGradientBoosting OK
XGBoost OK
LightGBM OK
XGBoostとLightGBMは、欠損値を「どちらの枝へ送ると損失が下がるか」を学習時に決めて分岐の既定方向として保存します。scikit-learnのHistGradientBoostingも同じ考え方で、公式ドキュメントは「built-in support for missing values, which avoids the need for an imputer」と書いています。この既定方向は分岐ごとに学習されます。2割を欠損にしたデータでXGBoostの木10本をJSONにダンプすると、65個の分岐のうち38個が欠損を左の枝へ、27個が右の枝へ送る設定になっていました。ただし、補完すると精度が落ちるとは限りません。欠損の出方をラベルに依存させた人工データで平均補完と比べても、AUCは0.9979と0.9980で差が出ませんでした。今回の人工データでは精度差がほぼなく、ネイティブ対応によって補完処理と補完方法の選択を省けました。精度への影響は実データの欠損の出方しだいで変わります。
カテゴリ変数:ネイティブ指定とone-hotの実測差
カテゴリ変数も、実装側が直接扱えます。LightGBM公式は「It is common to represent categorical features with one-hot encoding, but this approach is suboptimal for tree learners」と述べ、水準数が多い列でone-hotが不利になると説明しています。200水準のカテゴリ1列と数値1列から作った3万行のデータで、実際に比べました。
import time
import numpy as np
import pandas as pd
from catboost import CatBoostClassifier
from lightgbm import LGBMClassifier
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from xgboost import XGBClassifier
rng = np.random.default_rng(0)
n = 30000
code = rng.integers(0, 200, n) # 200水準のカテゴリ
effect = rng.normal(0, 1, 200)
num = rng.normal(0, 1, n)
prob = 1 / (1 + np.exp(-(effect[code] + 0.8 * num)))
y = (rng.random(n) < prob).astype(int)
df = pd.DataFrame({"cat": pd.Categorical(code), "num": num})
X_tr, X_te, y_tr, y_te = train_test_split(df, y, test_size=0.25, random_state=0)
onehot = pd.get_dummies(df, columns=["cat"])
O_tr, O_te, _, _ = train_test_split(onehot, y, test_size=0.25, random_state=0)
def run(label, model, A, B, **fit_kw):
started = time.perf_counter()
model.fit(A, y_tr, **fit_kw)
elapsed = time.perf_counter() - started
auc = roc_auc_score(y_te, model.predict_proba(B)[:, 1])
print(f"{label:32s} cols={A.shape[1]:3d} fit={elapsed:6.2f}s AUC={auc:.4f}")
run("LightGBM category dtype", LGBMClassifier(verbose=-1, random_state=0), X_tr, X_te)
run("LightGBM one-hot", LGBMClassifier(verbose=-1, random_state=0), O_tr, O_te)
run("XGBoost category dtype", XGBClassifier(random_state=0), X_tr, X_te)
run("XGBoost one-hot", XGBClassifier(random_state=0), O_tr, O_te)
run("HistGB categorical_features", HistGradientBoostingClassifier(
categorical_features="from_dtype", random_state=0), X_tr, X_te)
run("CatBoost cat_features", CatBoostClassifier(verbose=0, random_seed=0),
X_tr.assign(cat=X_tr["cat"].astype(str)), X_te.assign(cat=X_te["cat"].astype(str)),
cat_features=["cat"])
LightGBM category dtype cols= 2 fit= 0.25s AUC=0.7747
LightGBM one-hot cols=201 fit= 0.31s AUC=0.7808
XGBoost category dtype cols= 2 fit= 0.37s AUC=0.7392
XGBoost one-hot cols=201 fit= 0.90s AUC=0.7718
HistGB categorical_features cols= 2 fit= 0.58s AUC=0.7781
CatBoost cat_features cols= 2 fit= 21.57s AUC=0.7845
今回の条件では、ネイティブ指定のAUCがone-hotを上回るとは限りませんでした。LightGBMはネイティブ指定0.7747に対しone-hot 0.7808で、精度だけ見ればone-hotがわずかに上回ります。XGBoostも同じ傾向です。ネイティブ指定の効きどころは精度ではなく、列が2列のまま増えないこと(one-hotでは201列)と、学習時間が短くなることにありました。水準を1000に増やして同じ比較をすると、ネイティブ指定がAUC 0.7414・0.45秒・2列、one-hotがAUC 0.7260・0.85秒・1001列となり、精度でも時間でもネイティブが上回りました。水準数が増えるほどone-hotの列数増加が効いてきます。
唯一、精度でも上回ったのがCatBoostの0.7845です。ただし掲載した計測では学習に21.57秒かかり、LightGBMのネイティブ指定の0.25秒に対して約86倍でした。今回の人工データでは、学習時間を許容してAUCを優先する場合にCatBoostが候補になります。実データでは反復回数や学習率をそろえたうえで判断します。
指定方法や制約は実装ごとに違います。HistGradientBoostingでは各カテゴリ特徴量の水準数をmax_bins(既定255)以下にする必要があります。LightGBMはpandasのcategory型をそのまま渡せば自動で認識し、CatBoostはcat_featuresに列名を渡します。scikit-learnはcategorical_featuresの既定値がfrom_dtypeなので、DataFrameの列がcategory型ならそのままカテゴリとして扱われます(公式ドキュメントは「all columns with a categorical dtype will be treated as categorical features」と説明しています)。XGBoostはenable_categorical=Trueが必要だと書かれた記事が多いのですが、3.4.1ではget_params()が返す既定値自体がTrueで、指定なしでもcategory型を受け付けました。2.1.4では既定値がFalseで、同じコードが「DataFrame.dtypes for data must be int, float, bool or category」というValueErrorになります。バージョンで挙動が変わる箇所なので、手元の版を確認してください。カテゴリ列の作り方や水準のまとめ方は特徴量エンジニアリングとは?変換・選択・自動化の工程と実装判断【2026年版】を参照してください。
古い解説記事のコードが止まる箇所:early stoppingとGPU指定の現行API
XGBoost・LightGBMのearly stopping:fit引数廃止後の指定方法
古いGBDTの解説コードを現行版で使う際は、early stoppingの指定方法を確認してください。XGBoostは2.1.0のリリースノートで「For the Python package, eval_metric, early_stopping_rounds, and callbacks from now removed from the fit method in the sklearn interface. They were deprecated in 1.6.」と告知しており、現行版ではコンストラクタ引数で指定します。LightGBMも4.0.0でtrain()・cv()の同名引数を削除しており、fit()に渡しても受け付けません。こちらはcallbacksにlgb.early_stopping()を渡します。古い書き方をそのまま実行するとどうなるかを含めて動かします。
import lightgbm as lgb
from lightgbm import LGBMClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from xgboost import XGBClassifier
X, y = make_classification(n_samples=20000, n_features=30, random_state=0)
X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.2, random_state=0)
# 2023年ごろまでの書き方(現行版では例外になる)
try:
XGBClassifier().fit(X_tr, y_tr, eval_set=[(X_va, y_va)], early_stopping_rounds=20)
except TypeError as e:
print("XGBoost old style ->", type(e).__name__, str(e)[:62])
# XGBoost 3.x:打ち切りはコンストラクタ引数で指定する
xgb_model = XGBClassifier(n_estimators=2000, early_stopping_rounds=20, eval_metric="logloss")
xgb_model.fit(X_tr, y_tr, eval_set=[(X_va, y_va)], verbose=False)
print("XGBoost best_iteration =", xgb_model.best_iteration)
# LightGBM 4.7:打ち切りは callbacks、検証データは eval_X / eval_y で渡す
lgb_model = LGBMClassifier(n_estimators=2000, verbose=-1)
lgb_model.fit(X_tr, y_tr, eval_X=X_va, eval_y=y_va,
callbacks=[lgb.early_stopping(20, verbose=False)])
print("LightGBM best_iteration_ =", lgb_model.best_iteration_)
XGBoost old style -> TypeError XGBClassifier.fit() got an unexpected keyword argument 'early_
XGBoost best_iteration = 15
LightGBM best_iteration_ = 55
木を2000本まで許しても、検証データの改善が止まった時点で打ち切られ、XGBoostはbest_iterationが15、LightGBMはbest_iteration_が55でした。本数を手で決める必要はありません。ただし打ち切り後のモデルの中身は実装で違います。同じ条件でget_booster().num_boosted_rounds()を見るとXGBoostは36本(0始まりの最良反復番号15に対応する16本と、猶予20本の合計)を保持したままで、LightGBMはbooster_.num_trees()が55本ちょうどに切り詰められていました。保存された木の本数と推論で使う本数は区別してください。XGBoostのscikit-learn互換APIのpredict・predict_probaは通常、best_iterationまでを使うため、保持された36本すべてが既定の推論に使われるわけではありません。
LightGBM側はさらに1段進んでいて、4.7.0では検証データの渡し方も変わりました。eval_setを使うとLGBMDeprecationWarning: The argument 'eval_set' is deprecated, use 'eval_X' and 'eval_y' instead. という非推奨警告が出ます(4.7.0のリリースノートも「scikit-learn fit() methods: add eval_X, eval_y, deprecate eval_set」と記載)。検証データが1組ならeval_X=X_va, eval_y=y_vaと配列のまま渡し、複数組ならそれぞれタプルにします。リストで包むとTypeError: Wrong type(list) for label.になるので、eval_setの[(X, y)]という書き癖をそのまま持ち込まないでください。
紛らわしいのは、XGBoostのネイティブAPIxgboost.train()には今もearly_stopping_rounds引数が残っている点です。公式のPython APIリファレンスにも記載があります。「ネイティブAPIの記法をscikit-learn互換APIにそのまま書いてしまう」のが、現行版でTypeErrorに当たる典型的な経路です。なお打ち切りの判定に使う検証データは、学習データからランダムに分けるだけでは時系列や群構造で漏れが起きます。分割の選び方は交差検証とは?分割手法の選び方とデータリーク対策を実装目線で解説【2026年版】にまとめました。
XGBoostのGPU指定:deviceパラメータへの移行
もう1つの陳腐化がGPUの指定方法です。XGBoostの現行ドキュメントのGPU Supportページは「To enable GPU acceleration, specify the device parameter as cuda」と案内し、複数GPU環境ではcuda:1のように序数まで書けるとしています。tree_method="gpu_hist"という書き方はこのページに登場しません。あわせてtree_methodの既定値はautoで、公式パラメータ表は「auto: Same as the hist tree method」と定義しています。ヒストグラム法が既定になったので、tree_method="hist"を明示していた記述も現在は不要です。
GBDTを採用しない方がよい場面:範囲外の予測と滑らかな線形関係
定数葉のGBDTにおけるトレンド外挿の限界
葉に定数を持つ通常のGBDTは、学習範囲外にも予測値を返しますが、線形モデルのように傾きを延長することはできません。決定木は入力空間を区切って各領域に定数を割り当てるだけなので、最も端の領域より外はすべて同じ値を返します。上昇トレンドのある売上や単調に増える指標を、日付や通し番号を特徴量にして学習させると、将来の予測が水平線になります。
import numpy as np
from lightgbm import LGBMRegressor
from sklearn.linear_model import LinearRegression
rng = np.random.default_rng(0)
x = np.linspace(0, 10, 2000).reshape(-1, 1)
y = 2 * x.ravel() + rng.normal(0, 0.5, x.shape[0]) # 真の関係は y = 2x
gbdt = LGBMRegressor(verbose=-1).fit(x, y)
linear = LinearRegression().fit(x, y)
for q in (9.5, 10.0, 12.0, 15.0, 20.0):
point = np.array([[q]])
print(f"x={q:5.1f} true={2*q:6.2f} GBDT={gbdt.predict(point)[0]:7.3f} "
f"linear={linear.predict(point)[0]:7.3f}")
x= 9.5 true= 19.00 GBDT= 18.997 linear= 18.979
x= 10.0 true= 20.00 GBDT= 19.935 linear= 19.978
x= 12.0 true= 24.00 GBDT= 19.935 linear= 23.975
x= 15.0 true= 30.00 GBDT= 19.935 linear= 29.971
x= 20.0 true= 40.00 GBDT= 19.935 linear= 39.963
学習データは0から10までしかありません。x=12でも20でも、GBDTの予測は19.935で止まったままです。線形回帰は同じデータから傾き2を学ぶので、範囲外でも真値に追随します。
逃げ道が1つだけあります。LightGBMのlinear_tree(既定false)を有効にすると、公式の説明どおり「the model at each leaf is linear instead of constant」となり、葉が定数でなくなります。同じデータで試すと、x=12の予測が19.935から23.020へ、x=20が19.935から35.074へ動きました。真値の24.00・40.00には届きませんが、傾きは延長されています。
この頭打ちは精度の問題ではなく構造の問題なので、葉が定数のままでは木を増やしても学習率を下げても直りません。トレンドがあるデータでは、差分や比率に変換してから学習させる、トレンド成分を線形モデルで抜いてから残りをGBDTに任せる、といった前処理が要ります。時系列の需要予測では、トレンドを外挿できないことが誤差の原因になり得ます。季節性、分布変化、特徴量、データ分割も併せて点検してください。
線形の人工データにおけるリッジ回帰との誤差比較
もう1つ、真の関係が素直な線形和のときもGBDTは不利です。木は入力を階段状に区切って各区画に定数を返すため、直線や平面を近似するには段差を大量に刻むしかありません。20列すべてが効く線形回帰データを1回だけ生成し、回帰係数もテストデータも固定したまま、学習に使う行数だけを変えてリッジ回帰と比べました。
import numpy as np
from lightgbm import LGBMRegressor
from sklearn.datasets import make_regression
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split
# 係数もテストデータも固定するため、最大サイズを1回だけ作って行を間引く
X, y = make_regression(n_samples=60000, n_features=20, n_informative=20,
noise=1.0, random_state=0)
X_pool, X_te, y_pool, y_te = train_test_split(X, y, test_size=10000, random_state=0)
for n in (200, 1000, 5000, 50000):
X_tr, y_tr = X_pool[:n], y_pool[:n]
ridge = Ridge().fit(X_tr, y_tr)
gbdt = LGBMRegressor(verbose=-1, random_state=0).fit(X_tr, y_tr)
print(f"n={n:6d} Ridge MAE={mean_absolute_error(y_te, ridge.predict(X_te)):8.3f} "
f"LightGBM MAE={mean_absolute_error(y_te, gbdt.predict(X_te)):8.3f}")
n= 200 Ridge MAE= 1.502 LightGBM MAE= 100.827
n= 1000 Ridge MAE= 0.823 LightGBM MAE= 83.206
n= 5000 Ridge MAE= 0.798 LightGBM MAE= 58.773
n= 50000 Ridge MAE= 0.796 LightGBM MAE= 42.577
行数を200から50,000へ250倍にすると、LightGBMのMAEは100.827から42.577まで下がりました。それでもリッジ回帰の0.796には遠く、53倍の開きが残ります。センサーの校正式や料金計算のように、生成過程が線形だと分かっている問題では、GBDTを第一候補にする理由がありません。GBDTが効くのは、交互作用や非線形なしきい値が効いていて、その形が事前に分からない表形式データのほうです。線形の骨格が分かっているなら、線形モデルの残差だけをGBDTに学習させるハイブリッドも選べます。
1万行以下の小規模データで先行するTabPFN
もっとも、GBDTが表形式データで常に最良とは限らなくなってきました。2025年1月8日にNatureへ掲載されたTabPFN(Tabular Prior-data Fitted Network)の論文は、要旨で「outperforms all previous methods on datasets with up to 10,000 samples by a wide margin, using substantially less training time」と述べ、分類の設定で「In 2.8 s, TabPFN outperforms an ensemble of the strongest baselines tuned for 4 h」と報告しています。行数が1万以下の案件では、GBDTだけで決めずに比較対象として当ててみる価値があります。ただし本文の評価対象は「最大1万サンプル・500特徴量」のデータセットで、しかも「Although for some datasets CatBoost outperforms TabPFN」と、データセット単位ではCatBoostが上回る例も認めています。1万行を超える規模の比較は示されていません。
よくある質問
GBDTとXGBoostの違いは何ですか?
GBDTはアルゴリズムの名前、XGBoostはそれを実装したライブラリの名前です。両者は比較対象ではなく包含関係にあります。XGBoostはGBDTにL1・L2正則化を組み込み、ヒストグラム法や欠損値の自動分岐を加えた実装で、同じ位置にLightGBM・CatBoost・scikit-learnのHistGradientBoostingが並びます。LightGBMにいたっては、ブースティング方式を選ぶパラメータboosting_typeの既定値が文字列gbdtそのものです。「GBDTとXGBoostのどちらを使うか」ではなく「GBDTをどの実装で回すか」が実際の選択になります。
GBDTは回帰にも使えますか?
使えます。むしろ勾配ブースティングは回帰から出発した手法で、Friedmanの原論文も要旨で回帰向けの二乗誤差・絶対偏差・Huber損失を先に挙げ、そのあとに分類向けの多クラスロジスティック尤度を挙げています。実装側もクラス名が分かれているだけで、XGBRegressor・LGBMRegressor・CatBoostRegressor・HistGradientBoostingRegressorと分類器と同じAPIで使えます。対応する実装では分位点回帰やランキング学習も可能です。ランキングではLGBMRankerなどの専用クラスを使い、クエリ単位のグループ情報も渡します。着順予測のような順序付きの問題では競馬予想AIの特徴量一覧とLightGBM(ランキング学習)による着順予測の実装で扱ったランキング目的関数を使います。
LGBMとは何ですか?GBDTとどう関係しますか?
LGBMはLightGBMの略記で、Microsoftが公開しているGBDTの実装です。Pythonのクラス名がLGBMClassifier・LGBMRegressorであるため、この略記が定着しました。GBDTのなかでは木を葉単位で伸ばすleaf-wise方式を採り、大きなデータで学習時間が短くなりやすい設計です。パラメータの詳細やnum_leavesの決め方はLightGBMとは?勾配ブースティングの仕組みと特徴・XGBoostとの違いを実装例つきで解説にまとめています。
GBDTとランダムフォレストはどちらを選ぶべきですか?
精度が最優先なら、まずGBDTです。ランダムフォレストは深い木を独立に並べて平均を取るバギングで、木の間に依存関係がないぶん並列化しやすく、過学習にも鈍感です。一方GBDTは、それまでのモデル全体の予測に対する損失を減らす方向へ木を追加します。同じ木の本数でどちらの誤差が小さくなるかは、データと設定によって変わります。本記事の実測でも、同じ50,000行のデータで既定値のまま学習させたランダムフォレストはAUC 0.9885・7.88秒、XGBoostはAUC 0.9902・1.56秒でした。調整時間を抑えたい場合はランダムフォレストも比較候補です。学習データが数百件の場合も、GBDTと同じ分割で評価し、精度のばらつきと過学習の程度を比較してください。
GBDTの学習をGPUで速くできますか?
できます。GPUによる高速化の程度は、データの規模、使用機器、学習設定によって変わります。XGBoostはdeviceパラメータにcudaを指定するとCUDA対応GPUで学習・予測・評価が動き、複数GPUがある環境ではcuda:1のように序数まで指定できます。LightGBMはdevice_type(既定cpu)にgpuかcudaを指定します。CatBoostもGPU学習に対応しますが、公式は浮動小数点の加算順序が固定されないため結果が非決定的になると注記しています。ただしCPU側もヒストグラム法とマルチスレッドで十分速く、数万行規模では本記事の実測どおり数秒で終わるため、GPU導入の判断は行数や列数だけでは決まりません。使用するCPU・GPUと学習条件をそろえ、データ転送を含む所要時間を比較してください。