勾配ブースティングとは?XGBoost/LightGBMの理論をわかりやすく解説

勾配ブースティング(Gradient Boosting)は、複数の弱学習器を逐次的に組み合わせて強力な予測モデルを構築するアンサンブル学習の手法です。Kaggle をはじめとするデータ分析コンペティションでは、テーブルデータにおいて最も高い精度を叩き出すアルゴリズムとして長年トップクラスの地位を占めています。

バギング(ランダムフォレスト等)が複数の学習器を並列に学習させて平均を取るのに対し、ブースティングは前のモデルの誤りを修正するように次のモデルを逐次的に追加していく点が根本的に異なります。この逐次的な誤り修正のメカニズムこそが、ブースティングの高い表現力の源です。

本記事の内容

  • ブースティングの基本的な考え方と AdaBoost
  • 勾配ブースティングの一般的なフレームワーク(疑似残差への回帰)
  • 損失関数の Taylor 展開による最適化の導出
  • XGBoost の正則化・高速化の工夫
  • LightGBM の GOSS・EFB・Leaf-wise 成長
  • Python での実装と 3 つのライブラリの比較

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

ブースティングの考え方

ブースティング(Boosting)は、単体ではそこまで高い精度を持たない弱学習器(weak learner)を逐次的に加算し、最終的に強い学習器を構築する手法です。

モデル全体の予測値を $F_M(\bm{x})$ とすると、$M$ 個の弱学習器 $h_m(\bm{x})$ の加算で次のように表されます。

$$ F_M(\bm{x}) = \sum_{m=1}^{M} \eta \, h_m(\bm{x}) $$

ここで $\eta$ は学習率(shrinkage)で、各弱学習器の寄与を小さくすることで過学習を防ぎます。

バギング(ランダムフォレスト)との違いを整理しておきましょう。

特徴 バギング(ランダムフォレスト) ブースティング(GBDT)
学習方式 並列(独立) 逐次(依存)
各学習器の役割 独立に予測 前のモデルの誤りを修正
主な効果 分散の低減 バイアスの低減
過学習リスク 低い 比較的高い(要正則化)

AdaBoost

ブースティングの先駆けとなった AdaBoost(Adaptive Boosting)について簡単に触れておきます。

AdaBoost は分類問題に特化したブースティング手法で、次のような仕組みで動作します。

  1. 全サンプルに均等な重み $w_i = 1/N$ を割り当てる
  2. 弱学習器 $h_m(\bm{x})$ を重み付きデータで学習する
  3. 誤分類率 $\epsilon_m$ を計算し、学習器の重み $\alpha_m$ を決める

$$ \alpha_m = \frac{1}{2} \ln \frac{1 – \epsilon_m}{\epsilon_m} $$

  1. 誤分類されたサンプルの重みを増やし、正しく分類されたサンプルの重みを減らす

$$ w_i \leftarrow w_i \cdot \exp\bigl(\alpha_m \cdot \mathbb{1}[y_i \neq h_m(\bm{x}_i)]\bigr) $$

  1. 重みを正規化して 2 に戻る

最終的な予測は全弱学習器の重み付き多数決で決まります。

$$ F_M(\bm{x}) = \mathrm{sign}\left(\sum_{m=1}^{M} \alpha_m \, h_m(\bm{x})\right) $$

AdaBoost はシンプルですが、サンプルの重みを調整するという発想に限られます。勾配ブースティングは、これを任意の損失関数に一般化したものです。

勾配ブースティングの一般的フレームワーク

基本アイデア:疑似残差に回帰木をフィット

勾配ブースティングでは、任意の微分可能な損失関数 $L(y, F(\bm{x}))$ を最小化します。核心となるアイデアは、損失関数の負の勾配を「疑似残差」(pseudo residual)として扱い、これに回帰木をフィットさせるということです。

第 $m$ ステップにおける疑似残差は次のように定義されます。

$$ r_{im} = -\left[\frac{\partial L(y_i, F(\bm{x}_i))}{\partial F(\bm{x}_i)}\right]_{F = F_{m-1}} $$

例えば、損失関数が二乗誤差 $L(y, F) = \frac{1}{2}(y – F)^2$ の場合、疑似残差は単純な残差になります。

$$ r_{im} = -\frac{\partial}{\partial F}\frac{1}{2}(y_i – F)^2 \bigg|_{F=F_{m-1}} = y_i – F_{m-1}(\bm{x}_i) $$

これは直感的にもわかりやすいです。前のモデルが予測しきれなかった「残り」を次のモデルで予測しようとしているわけです。

アルゴリズムの全体像

勾配ブースティングのアルゴリズムを整理すると、以下のようになります。

  1. モデルを定数で初期化する:$F_0(\bm{x}) = \arg\min_{\gamma} \sum_{i=1}^{N} L(y_i, \gamma)$
  2. $m = 1, 2, \dots, M$ に対して以下を繰り返す: – 疑似残差を計算する:$r_{im} = -\left[\frac{\partial L(y_i, F(\bm{x}_i))}{\partial F(\bm{x}_i)}\right]_{F = F_{m-1}}$ – 疑似残差 $\{r_{im}\}$ に対して回帰木 $h_m(\bm{x})$ を学習する – 学習率 $\eta$ を適用してモデルを更新する:$F_m(\bm{x}) = F_{m-1}(\bm{x}) + \eta \, h_m(\bm{x})$
  3. 最終モデル $F_M(\bm{x})$ を出力する

なぜ「勾配」ブースティングなのか

このアルゴリズムは、関数空間における勾配降下法と見なすことができます。

通常の勾配降下法では、パラメータ $\bm{\theta}$ を $\bm{\theta} \leftarrow \bm{\theta} – \eta \nabla_{\bm{\theta}} L$ と更新します。勾配ブースティングでは、パラメータではなく関数 $F$ 自体を更新対象とし、関数空間における勾配の方向(=負の勾配方向に最もよくフィットする木)に沿ってモデルを改善していきます。

$$ F_m = F_{m-1} – \eta \cdot \nabla_F L \approx F_{m-1} + \eta \, h_m $$

つまり $h_m$ は $-\nabla_F L$(疑似残差)の近似であり、勾配降下法の更新ステップに対応しています。

数式の導出:Taylor 展開による 2 次近似

ここでは、XGBoost の論文(Chen & Guestrin, 2016)に基づいて、損失関数の 2 次の Taylor 展開から最適な葉の値を閉じた式で導出します。

目的関数の定義

$m$ 番目の木を追加するときの目的関数を考えます。正則化項 $\Omega(h_m)$ を含めた目的関数は次のとおりです。

$$ \mathcal{L}^{(m)} = \sum_{i=1}^{N} L\bigl(y_i,\; F_{m-1}(\bm{x}_i) + h_m(\bm{x}_i)\bigr) + \Omega(h_m) $$

Taylor 展開

$L\bigl(y_i,\; F_{m-1}(\bm{x}_i) + h_m(\bm{x}_i)\bigr)$ を $h_m(\bm{x}_i)$ について 2 次まで Taylor 展開します。$\hat{y}_i = F_{m-1}(\bm{x}_i)$ とおくと、

$$ L(y_i, \hat{y}_i + h_m(\bm{x}_i)) \approx L(y_i, \hat{y}_i) + g_i \, h_m(\bm{x}_i) + \frac{1}{2} h_i \, h_m(\bm{x}_i)^2 $$

ここで、1 次の勾配 $g_i$ と 2 次の勾配(ヘッセ行列の対角成分)$h_i$ を次のように定義します。

$$ g_i = \frac{\partial L(y_i, \hat{y}_i)}{\partial \hat{y}_i}, \qquad h_i = \frac{\partial^2 L(y_i, \hat{y}_i)}{\partial \hat{y}_i^2} $$

$L(y_i, \hat{y}_i)$ は定数なので無視すると、目的関数は次のように簡略化されます。

$$ \tilde{\mathcal{L}}^{(m)} = \sum_{i=1}^{N} \left[g_i \, h_m(\bm{x}_i) + \frac{1}{2} h_i \, h_m(\bm{x}_i)^2 \right] + \Omega(h_m) $$

正則化項の定義

回帰木 $h_m$ が $T$ 個の葉を持ち、各葉 $j$ の値を $w_j$ とすると、正則化項を次のように定義します。

$$ \Omega(h_m) = \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2 $$

ここで $\gamma$ は葉の数に対するペナルティ、$\lambda$ は葉の値に対する L2 正則化の強さです。

葉ごとの目的関数への整理

葉 $j$ に属するサンプルの集合を $I_j = \{i \mid \bm{x}_i \text{ は葉 } j \text{ に割り当てられる}\}$ とおきます。葉 $j$ に属するサンプルに対しては $h_m(\bm{x}_i) = w_j$ なので、目的関数を葉ごとに整理できます。

$$ \begin{align} \tilde{\mathcal{L}}^{(m)} &= \sum_{i=1}^{N} \left[g_i \, w_{q(\bm{x}_i)} + \frac{1}{2} h_i \, w_{q(\bm{x}_i)}^2 \right] + \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2 \\ &= \sum_{j=1}^{T} \left[\left(\sum_{i \in I_j} g_i\right) w_j + \frac{1}{2}\left(\sum_{i \in I_j} h_i + \lambda\right) w_j^2 \right] + \gamma T \end{align} $$

ここで $q(\bm{x})$ はサンプル $\bm{x}$ を葉のインデックスに写す関数です。簡潔にするため、次の記号を導入します。

$$ G_j = \sum_{i \in I_j} g_i, \qquad H_j = \sum_{i \in I_j} h_i $$

すると目的関数は次のようにきれいに書けます。

$$ \tilde{\mathcal{L}}^{(m)} = \sum_{j=1}^{T} \left[G_j \, w_j + \frac{1}{2}(H_j + \lambda) w_j^2\right] + \gamma T $$

最適な葉の値

各葉 $j$ について $w_j$ で偏微分して 0 とおくと、最適な葉の値 $w_j^*$ が閉じた式で得られます。

$$ \frac{\partial \tilde{\mathcal{L}}^{(m)}}{\partial w_j} = G_j + (H_j + \lambda) w_j = 0 $$

$$ \therefore \quad w_j^* = -\frac{G_j}{H_j + \lambda} $$

この式を目的関数に代入すると、木の構造の良さを評価するスコアが得られます。

$$ \tilde{\mathcal{L}}^{(m)*} = -\frac{1}{2} \sum_{j=1}^{T} \frac{G_j^2}{H_j + \lambda} + \gamma T $$

分割の利得(Gain)

ノードを分割するかどうかの判断には、分割前後での目的関数の改善量(Gain)を使います。あるノードを左 $I_L$ と右 $I_R$ に分割したとき、

$$ \text{Gain} = \frac{1}{2}\left[\frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} – \frac{(G_L + G_R)^2}{H_L + H_R + \lambda}\right] – \gamma $$

Gain が正であれば分割により目的関数が改善されるため分割を行い、負であれば分割を打ち切ります。$\gamma$ が大きいほど分割が抑制され、木が浅くなります(正則化の効果)。

XGBoost の工夫

XGBoost(eXtreme Gradient Boosting)は、上記の勾配ブースティングのフレームワークに加えて、以下の工夫により高い精度と効率を実現しています。

正則化項 $\Omega$

前節で導出したように、XGBoost は目的関数に正則化項 $\Omega(h) = \gamma T + \frac{1}{2}\lambda \sum_j w_j^2$ を明示的に加えます。これにより、木が複雑になりすぎることを防ぎ、過学習を抑えます。通常の GBDT 実装にはこの正則化項がないことが多く、XGBoost の大きな特徴の一つです。

Shrinkage(学習率)

各木の出力に学習率 $\eta$(典型的には 0.01〜0.3)を掛けることで、各ステップでの変更量を小さく抑えます。

$$ F_m(\bm{x}) = F_{m-1}(\bm{x}) + \eta \, h_m(\bm{x}) $$

学習率を小さくすると木の本数を増やす必要がありますが、過学習しにくくなり、汎化性能が向上します。

列サンプリング(Column Subsampling)

ランダムフォレストと同様に、各木(または各分割)で使用する特徴量をランダムにサンプリングします。これにより過学習を防ぎつつ、計算量も削減できます。

Weighted Quantile Sketch

連続値の特徴量に対する最適な分割点を効率的に探索するため、XGBoost は重み付き分位点スケッチを用います。全候補を試す代わりに、2 次の勾配 $h_i$ を重みとした近似分割点を使うことで、精度を保ちながら計算量を大幅に削減します。

Sparsity-Aware Split Finding

欠損値やスパースなデータに対して、欠損値を持つサンプルをデフォルトで左右どちらに振り分けるかを自動的に学習します。これにより、欠損値の事前処理が不要になります。

LightGBM の工夫

LightGBM(Light Gradient Boosting Machine)は、Microsoft Research が開発した GBDT の高速実装です。XGBoost と比べてとくに大規模データでの学習速度に優れています。

GOSS(Gradient-based One-Side Sampling)

勾配ブースティングでは、勾配の絶対値が大きいサンプル(つまり、現在のモデルで大きく外れているサンプル)が情報量の多いサンプルです。GOSS はこの観察に基づき、次のようにサンプルを選択します。

  1. 全サンプルを勾配の絶対値でソートする
  2. 上位 $a \times 100\%$ のサンプル(勾配が大きいもの)は全て残す
  3. 残りのサンプルからランダムに $b \times 100\%$ を選び、重みを $\frac{1-a}{b}$ 倍する

これにより、情報量の少ないサンプルの多くをスキップしつつ、データ分布の偏りを重み補正で軽減します。

EFB(Exclusive Feature Bundling)

高次元のスパースなデータでは、多くの特徴量が互いに排他的(同時に非ゼロにならない)です。EFB はこのような排他的特徴量を束ねて一つの特徴量として扱うことで、実効的な特徴量数を削減し、計算を高速化します。

Leaf-wise(Best-first)成長

従来の GBDT(XGBoost を含む)はLevel-wise(深さ優先)で木を成長させます。つまり、同じ深さの全ノードを分割してから次の深さに進みます。

一方、LightGBM はLeaf-wiseで木を成長させます。全葉ノードの中で最も Gain が大きいものを選んで分割します。

  • Level-wise: 均一な木構造。安定だが無駄な分割が含まれやすい
  • Leaf-wise: 非対称な木構造。同じ葉の数で比較すると損失の低減が大きいが、深い木になりやすく過学習に注意

LightGBM では max_depthnum_leaves で木の成長を制限することで、Leaf-wise 成長の過学習リスクを制御します。

ヒストグラムベースの分割探索

LightGBM は特徴量の値をヒストグラム(ビン)に離散化して分割点を探索します。これにより、分割点の候補数が大幅に減少し、計算量が $O(N \cdot D)$ から $O(B \cdot D)$($B$ はビン数、典型的に 255)に改善されます。XGBoost も後にこの手法を取り入れています(tree_method='hist')。

ハイパーパラメータの指針

勾配ブースティングの性能を引き出すには、適切なハイパーパラメータの設定が重要です。主要なパラメータの指針をまとめます。

パラメータ XGBoost名 LightGBM名 推奨範囲 役割
学習率 learning_rate (eta) learning_rate 0.01〜0.3 小さいほど過学習しにくいが、木の本数が必要
木の本数 n_estimators n_estimators 100〜10000 学習率とセットで調整。早期停止で決定
木の深さ max_depth max_depth 3〜10 深いほど表現力が高いが過学習リスク増
葉の数 num_leaves 15〜255 LightGBM特有。$2^{\text{max\_depth}}$ 以下に設定
L2 正則化 reg_lambda (lambda) reg_lambda 0〜10 葉の値の正則化
L1 正則化 reg_alpha (alpha) reg_alpha 0〜10 葉の値のスパース化
最小分割損失 gamma (min_split_loss) min_split_gain 0〜5 分割に必要な最小 Gain
列サンプリング colsample_bytree feature_fraction 0.5〜1.0 過学習抑制
行サンプリング subsample bagging_fraction 0.5〜1.0 過学習抑制

チューニングの基本戦略

  1. まず learning_rate=0.1, n_estimators=1000 程度で早期停止付きで学習する
  2. 最適な n_estimators が決まったら、max_depthnum_leaves を調整する
  3. 次に colsample_bytreefeature_fraction)と subsamplebagging_fraction)を調整する
  4. 正則化パラメータ lambda, alpha, gamma を調整する
  5. 最後に learning_rate を下げて n_estimators を増やし、最終的な精度を追い込む

Python 実装

scikit-learn の GradientBoostingClassifier、XGBoost、LightGBM の 3 つのライブラリで同じデータセットに対して分類を行い、性能を比較します。

データの準備と共通設定

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, learning_curve
from sklearn.metrics import accuracy_score
import time

# 再現性のためシードを固定
np.random.seed(42)

# 合成データの生成
X, y = make_classification(
    n_samples=5000,
    n_features=20,
    n_informative=10,
    n_redundant=5,
    n_clusters_per_class=3,
    random_state=42
)

# 訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"訓練データ: {X_train.shape[0]} 件")
print(f"テストデータ: {X_test.shape[0]} 件")

3 つのモデルの学習と評価

from sklearn.ensemble import GradientBoostingClassifier
import xgboost as xgb
import lightgbm as lgb

# 共通パラメータ
params = {
    "n_estimators": 500,
    "learning_rate": 0.1,
    "max_depth": 5,
    "random_state": 42,
}

# --- scikit-learn GradientBoosting ---
start = time.time()
clf_sklearn = GradientBoostingClassifier(
    n_estimators=params["n_estimators"],
    learning_rate=params["learning_rate"],
    max_depth=params["max_depth"],
    random_state=params["random_state"],
)
clf_sklearn.fit(X_train, y_train)
time_sklearn = time.time() - start
acc_sklearn = accuracy_score(y_test, clf_sklearn.predict(X_test))

# --- XGBoost ---
start = time.time()
clf_xgb = xgb.XGBClassifier(
    n_estimators=params["n_estimators"],
    learning_rate=params["learning_rate"],
    max_depth=params["max_depth"],
    random_state=params["random_state"],
    eval_metric="logloss",
    verbosity=0,
)
clf_xgb.fit(X_train, y_train)
time_xgb = time.time() - start
acc_xgb = accuracy_score(y_test, clf_xgb.predict(X_test))

# --- LightGBM ---
start = time.time()
clf_lgb = lgb.LGBMClassifier(
    n_estimators=params["n_estimators"],
    learning_rate=params["learning_rate"],
    max_depth=params["max_depth"],
    num_leaves=31,
    random_state=params["random_state"],
    verbosity=-1,
)
clf_lgb.fit(X_train, y_train)
time_lgb = time.time() - start
acc_lgb = accuracy_score(y_test, clf_lgb.predict(X_test))

# 結果の表示
print(f"{'モデル':<25} {'Accuracy':>10} {'学習時間 (秒)':>15}")
print("-" * 55)
print(f"{'sklearn GradientBoosting':<25} {acc_sklearn:>10.4f} {time_sklearn:>15.2f}")
print(f"{'XGBoost':<25} {acc_xgb:>10.4f} {time_xgb:>15.2f}")
print(f"{'LightGBM':<25} {acc_lgb:>10.4f} {time_lgb:>15.2f}")

同程度の精度であっても、学習時間に大きな差が出ることが確認できるはずです。一般に、LightGBM が最も高速で、次いで XGBoost、scikit-learn の順になります。

学習曲線の可視化

訓練データのサイズを変えたときのスコアの変化を学習曲線として描画し、3 つのモデルの振る舞いを比較します。

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

models = [
    ("sklearn GB", clf_sklearn),
    ("XGBoost", clf_xgb),
    ("LightGBM", clf_lgb),
]

train_sizes = np.linspace(0.1, 1.0, 10)

for ax, (name, model) in zip(axes, models):
    train_sizes_abs, train_scores, val_scores = learning_curve(
        model, X_train, y_train,
        train_sizes=train_sizes,
        cv=5,
        scoring="accuracy",
        n_jobs=-1,
    )

    train_mean = train_scores.mean(axis=1)
    train_std = train_scores.std(axis=1)
    val_mean = val_scores.mean(axis=1)
    val_std = val_scores.std(axis=1)

    ax.fill_between(train_sizes_abs, train_mean - train_std,
                    train_mean + train_std, alpha=0.1, color="blue")
    ax.fill_between(train_sizes_abs, val_mean - val_std,
                    val_mean + val_std, alpha=0.1, color="orange")
    ax.plot(train_sizes_abs, train_mean, "o-", color="blue", label="Training")
    ax.plot(train_sizes_abs, val_mean, "o-", color="orange", label="Validation")

    ax.set_title(name, fontsize=14)
    ax.set_xlabel("Training examples")
    ax.set_ylabel("Accuracy")
    ax.legend(loc="lower right")
    ax.set_ylim(0.7, 1.02)
    ax.grid(True, alpha=0.3)

plt.suptitle("Learning Curves Comparison", fontsize=16, y=1.02)
plt.tight_layout()
plt.show()

学習曲線を見ると、以下のような傾向がわかります。

  • Training スコアと Validation スコアの乖離が大きい場合、過学習が起きている
  • 両スコアが収束しているが低い場合、過少学習(バイアスが大きい)
  • データを増やしても Validation スコアが向上しない場合、モデルの表現力は十分である

特徴量重要度の比較

fig, axes = plt.subplots(1, 3, figsize=(18, 6))

for ax, (name, model) in zip(axes, models):
    importances = model.feature_importances_
    indices = np.argsort(importances)[::-1][:10]

    ax.barh(range(10), importances[indices][::-1], color="steelblue")
    ax.set_yticks(range(10))
    ax.set_yticklabels([f"Feature {i}" for i in indices[::-1]])
    ax.set_xlabel("Importance")
    ax.set_title(f"{name} - Feature Importance", fontsize=13)

plt.tight_layout()
plt.show()

特徴量重要度のランキングは 3 つのモデルでおおむね一致しますが、計算方法の違い(Gain ベース、Split 回数ベースなど)により多少の差異が生じます。

まとめ

本記事では、勾配ブースティングの理論を基礎から解説し、XGBoost と LightGBM の主要な工夫を整理しました。

  • ブースティング は弱学習器を逐次的に加算し、前のモデルの誤りを修正していくアンサンブル手法である
  • 勾配ブースティング は損失関数の負の勾配(疑似残差)に回帰木をフィットすることで、任意の微分可能な損失関数を最小化する
  • 損失関数の 2 次 Taylor 展開 により、最適な葉の値 $w_j^* = -G_j / (H_j + \lambda)$ が閉じた式で求まる
  • XGBoost は正則化項 $\Omega$、shrinkage、列サンプリング等により高い汎化性能を実現する
  • LightGBM は GOSS、EFB、Leaf-wise 成長、ヒストグラムベースの分割探索により大規模データでの高速学習を実現する
  • ハイパーパラメータは learning_raten_estimators のバランスを基本とし、早期停止と組み合わせてチューニングするのが定石である

次のステップとして、以下の記事も参考にしてください。