勾配ブースティング(Gradient Boosting)は、損失関数の負の勾配(擬似残差)を逐次的にフィッティングすることで、強力な予測モデルを構築するアンサンブル手法です。特に決定木を弱学習器として用いた Gradient Boosted Decision Trees(GBDT)は、Kaggle をはじめとするデータ分析コンペティションで圧倒的な実績を持ち、XGBoost、LightGBM、CatBoost といった高速実装が広く使われています。
AdaBoost が指数損失関数に特化していたのに対し、勾配ブースティングは任意の微分可能な損失関数に適用できる汎用的なフレームワークです。本記事では、関数空間での勾配降下法としての理論的基盤を丁寧に導出し、回帰・分類での具体的な擬似残差、XGBoost や LightGBM の工夫にも触れた上で、Python でのスクラッチ実装を行います。
本記事の内容
- 勾配ブースティングの基本思想(関数空間での勾配降下法)
- 一般的な損失関数での勾配ブースティングアルゴリズムの導出
- 回帰(二乗誤差)・分類(対数損失)の場合の擬似残差の導出
- 学習率(縮小率)による正則化
- XGBoost の工夫(2次近似・正則化項・分割ゲインの導出)
- LightGBM の工夫(GOSS・EFB)の概要
- Python でのスクラッチ実装と scikit-learn との比較
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
勾配ブースティングの基本思想
AdaBoost の限界
AdaBoost は指数損失関数 $\exp(-yf(\bm{x}))$ を最小化するアルゴリズムとして導出されましたが、以下の制約があります。
- 二値分類に特化しており、回帰問題への拡張が自然ではない
- 指数損失は外れ値に敏感で、ノイズの多いデータでは過学習しやすい
- 別の損失関数を使いたい場合、アルゴリズム全体を再導出する必要がある
勾配ブースティングは、これらの制約を解消する汎用的なフレームワークです。
関数空間での最適化
通常の最適化では、パラメータ空間 $\mathbb{R}^d$ でパラメータベクトル $\bm{\theta}$ を更新します。
$$ \bm{\theta}^{(t)} = \bm{\theta}^{(t-1)} – \eta \nabla_{\bm{\theta}} L(\bm{\theta}^{(t-1)}) $$
勾配ブースティングの核心的なアイデアは、この最適化を関数空間で行うことです。予測関数 $f(\bm{x})$ そのものを最適化の対象とし、次のように更新します。
$$ f^{(t)}(\bm{x}) = f^{(t-1)}(\bm{x}) + \eta \cdot h_t(\bm{x}) $$
ここで $h_t(\bm{x})$ は、損失関数の負の勾配(関数空間での勾配降下方向)をフィッティングする弱学習器です。
擬似残差の導入
経験損失関数を
$$ \mathcal{L}(f) = \sum_{i=1}^{N} L(y_i, f(\bm{x}_i)) $$
とします。これを $f(\bm{x}_i)$ の関数として見たとき、各データ点における関数空間での負の勾配は
$$ r_i^{(t)} = -\left[\frac{\partial L(y_i, f(\bm{x}_i))}{\partial f(\bm{x}_i)}\right]_{f = f^{(t-1)}} $$
です。これを擬似残差(pseudo-residual)と呼びます。名前の由来は、二乗誤差損失の場合に実際の残差 $y_i – f(\bm{x}_i)$ と一致することにあります。
勾配ブースティングアルゴリズムの導出
一般的な定式化
入力: 訓練データ $\{(\bm{x}_i, y_i)\}_{i=1}^{N}$、微分可能な損失関数 $L(y, f)$、弱学習器の数 $T$、学習率 $\eta$
Step 0: 初期モデルを定数で初期化します。
$$ f^{(0)}(\bm{x}) = \arg\min_{c} \sum_{i=1}^{N} L(y_i, c) $$
For $t = 1, 2, \dots, T$:
Step 1: 擬似残差を計算します。各 $i = 1, \dots, N$ に対して
$$ r_i^{(t)} = -\left[\frac{\partial L(y_i, f(\bm{x}_i))}{\partial f(\bm{x}_i)}\right]_{f = f^{(t-1)}} $$
Step 2: 擬似残差 $\{r_i^{(t)}\}$ をターゲットとして弱学習器(回帰木)$h_t$ を訓練します。
$$ h_t = \arg\min_{h} \sum_{i=1}^{N} (r_i^{(t)} – h(\bm{x}_i))^2 $$
Step 3: 木の各リーフ $j = 1, \dots, J_t$ について、最適な出力値を計算します。
$$ \gamma_{jt} = \arg\min_{\gamma} \sum_{i \in R_{jt}} L(y_i, f^{(t-1)}(\bm{x}_i) + \gamma) $$
ここで $R_{jt}$ はリーフ $j$ に属するサンプルの集合です。
Step 4: モデルを更新します。
$$ f^{(t)}(\bm{x}) = f^{(t-1)}(\bm{x}) + \eta \sum_{j=1}^{J_t} \gamma_{jt} \mathbb{1}[\bm{x} \in R_{jt}] $$
出力: $f^{(T)}(\bm{x})$
なぜこれが関数空間での勾配降下法なのか
通常の勾配降下法 $\bm{\theta} \leftarrow \bm{\theta} – \eta \nabla L$ のアナロジーで考えましょう。
- パラメータベクトル $\bm{\theta}$ に対応するのが、$N$ 個の関数値 $(f(\bm{x}_1), \dots, f(\bm{x}_N))^T$
- 負の勾配 $-\nabla L$ に対応するのが、擬似残差ベクトル $(r_1, \dots, r_N)^T$
- パラメータ更新 $\bm{\theta} – \eta \nabla L$ に対応するのが、$f \leftarrow f + \eta h_t$
ただし、擬似残差は $N$ 個の訓練データ点でしか定義されないため、任意の $\bm{x}$ での値を得るために弱学習器 $h_t$ でフィッティングして「汎化」させます。
具体的な損失関数での擬似残差
回帰:二乗誤差損失
$$ L(y, f) = \frac{1}{2}(y – f)^2 $$
擬似残差を計算します。
$$ r_i^{(t)} = -\frac{\partial}{\partial f}\left[\frac{1}{2}(y_i – f)^2\right]_{f = f^{(t-1)}(\bm{x}_i)} = y_i – f^{(t-1)}(\bm{x}_i) $$
これはまさに通常の残差です。二乗誤差損失の場合、擬似残差 = 残差 となり、勾配ブースティングは「残差にフィットする木を次々に足す」という直感的な操作と一致します。
初期値は
$$ f^{(0)} = \arg\min_c \sum_{i=1}^{N} \frac{1}{2}(y_i – c)^2 = \bar{y} $$
すなわち、ターゲットの平均値です。
回帰:絶対誤差損失(ロバスト)
$$ L(y, f) = |y – f| $$
擬似残差は
$$ r_i^{(t)} = -\frac{\partial}{\partial f}|y_i – f|_{f = f^{(t-1)}(\bm{x}_i)} = \text{sign}(y_i – f^{(t-1)}(\bm{x}_i)) $$
残差の符号のみを使うため、外れ値に対してロバストです。各リーフでの最適出力値は、そのリーフに属する残差の中央値となります。
分類:対数損失(ロジスティック損失)
二値分類 $y \in \{-1, +1\}$ に対して、対数損失(logistic loss)は
$$ L(y, f) = \log(1 + \exp(-yf)) $$
擬似残差を導出します。
$$ \frac{\partial L}{\partial f} = \frac{-y \exp(-yf)}{1 + \exp(-yf)} = \frac{-y}{1 + \exp(yf)} $$
よって擬似残差は
$$ r_i^{(t)} = -\frac{\partial L}{\partial f}\bigg|_{f = f^{(t-1)}(\bm{x}_i)} = \frac{y_i}{1 + \exp(y_i f^{(t-1)}(\bm{x}_i))} $$
確率 $p_i = P(y_i = +1 | \bm{x}_i) = \sigma(f^{(t-1)}(\bm{x}_i)) = \frac{1}{1 + e^{-f^{(t-1)}(\bm{x}_i)}}$ を用いると、$y_i \in \{0, 1\}$ の表記では
$$ r_i^{(t)} = y_i – p_i $$
と簡潔に書けます。これは観測されたラベルと予測確率の差、すなわち「確率空間での残差」に相当します。
各リーフでの最適出力値 $\gamma_{jt}$ は、対数損失の 2 次近似から
$$ \gamma_{jt} = \frac{\sum_{i \in R_{jt}} r_i^{(t)}}{\sum_{i \in R_{jt}} |r_i^{(t)}|(1 – |r_i^{(t)}|)} $$
と求まります。
学習率(縮小率)による正則化
縮小の効果
学習率 $\eta \in (0, 1]$ は、各弱学習器の寄与を縮小する役割を持ちます。
$$ f^{(t)}(\bm{x}) = f^{(t-1)}(\bm{x}) + \eta \cdot h_t(\bm{x}) $$
$\eta$ を小さくすると
- 各ステップでの更新量が小さくなり、より多くのラウンドが必要になる
- しかし、汎化性能は一般に向上する(過学習を抑制)
- 計算コストと汎化性能のトレードオフが存在する
経験的には $\eta = 0.01 \sim 0.1$ 程度の小さな値が推奨されます。
確率的勾配ブースティング(Stochastic Gradient Boosting)
Friedman (2002) は、各ラウンドでデータのサブサンプル(通常 50〜80%)を使って弱学習器を訓練する手法を提案しました。
$$ h_t = \arg\min_{h} \sum_{i \in S_t} (r_i^{(t)} – h(\bm{x}_i))^2 $$
ここで $S_t \subset \{1, \dots, N\}$ はランダムに選ばれたサブセットです。これはバギングのアイデアと同様に分散を減少させる効果があり、また計算コストの削減にもなります。
XGBoost の理論的工夫
2次近似(Newton ブースティング)
XGBoost(Chen & Guestrin, 2016)の最も重要な理論的工夫は、損失関数の 2 次テイラー展開を用いる点です。
ラウンド $t$ での目的関数を
$$ \mathcal{L}^{(t)} = \sum_{i=1}^{N} L(y_i, f^{(t-1)}(\bm{x}_i) + h_t(\bm{x}_i)) + \Omega(h_t) $$
とします。ここで $\Omega(h_t)$ は正則化項です。$h_t(\bm{x}_i)$ の周りで 2 次まで展開すると
$$ \mathcal{L}^{(t)} \approx \sum_{i=1}^{N} \left[L(y_i, f^{(t-1)}(\bm{x}_i)) + g_i h_t(\bm{x}_i) + \frac{1}{2} h_i h_t(\bm{x}_i)^2\right] + \Omega(h_t) $$
ここで
$$ g_i = \frac{\partial L(y_i, f)}{\partial f}\bigg|_{f = f^{(t-1)}(\bm{x}_i)}, \quad h_i = \frac{\partial^2 L(y_i, f)}{\partial f^2}\bigg|_{f = f^{(t-1)}(\bm{x}_i)} $$
はそれぞれ 1 次勾配と 2 次勾配(ヘシアン)です。定数項を除くと
$$ \tilde{\mathcal{L}}^{(t)} = \sum_{i=1}^{N} \left[g_i h_t(\bm{x}_i) + \frac{1}{2} h_i h_t(\bm{x}_i)^2\right] + \Omega(h_t) $$
正則化項
XGBoost では、木の複雑さに対する正則化を
$$ \Omega(h_t) = \gamma J + \frac{1}{2}\lambda \sum_{j=1}^{J} w_j^2 $$
と定義します。ここで $J$ はリーフの数、$w_j$ はリーフ $j$ の出力値、$\gamma$ はリーフ数に対するペナルティ、$\lambda$ は L2 正則化パラメータです。
最適リーフ値と分割ゲインの導出
木の構造を固定したとき、リーフ $j$ に属するサンプル集合を $I_j$ とすると
$$ \tilde{\mathcal{L}}^{(t)} = \sum_{j=1}^{J} \left[G_j w_j + \frac{1}{2}(H_j + \lambda) w_j^2\right] + \gamma J $$
ここで
$$ G_j = \sum_{i \in I_j} g_i, \quad H_j = \sum_{i \in I_j} h_i $$
これは $w_j$ の二次関数なので、微分して 0 とおくと
$$ \frac{\partial \tilde{\mathcal{L}}^{(t)}}{\partial w_j} = G_j + (H_j + \lambda) w_j = 0 $$
$$ w_j^* = -\frac{G_j}{H_j + \lambda} $$
最適リーフ値を代入すると、最適な目的関数値は
$$ \tilde{\mathcal{L}}^{(t)*} = -\frac{1}{2} \sum_{j=1}^{J} \frac{G_j^2}{H_j + \lambda} + \gamma J $$
分割ゲイン
あるノードを左($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 $$
ここで $G_L = \sum_{i \in I_L} g_i$、$H_L = \sum_{i \in I_L} h_i$ などです。ゲインが正であれば分割を行い、そうでなければ分割を停止します。$\gamma$ はリーフ追加のコストとして、枝刈りの役割を果たします。
LightGBM の工夫
GOSS(Gradient-based One-Side Sampling)
LightGBM(Ke et al., 2017)は大規模データへの対応として、勾配の大きさに基づくサンプリング手法 GOSS を導入しました。
勾配の絶対値が大きいサンプルは情報量が多い(まだ十分に学習できていない)ため、これらは全て保持します。一方、勾配の小さいサンプルからはランダムにサブサンプリングします。
具体的には
- 全サンプルを勾配の絶対値でソートする
- 上位 $a \times 100\%$ のサンプル(集合 $A$)を全て選択
- 残りから $b \times 100\%$ をランダムにサンプリング(集合 $B$)
- $B$ のサンプルの勾配を $\frac{1-a}{b}$ 倍に増幅して、情報量を補正
EFB(Exclusive Feature Bundling)
高次元スパースデータでは、多くの特徴量が互いに排他的(同時に非ゼロにならない)です。EFB はそのような特徴量をバンドル(束ね)て1つの特徴量として扱うことで、特徴量の数を大幅に削減します。
Leaf-wise 成長戦略
従来の GBDT が Level-wise(同じ深さのノードを全て分割)で木を成長させるのに対し、LightGBM は Leaf-wise(最もゲインの大きいリーフを分割)で成長させます。同じリーフ数の木であっても、Leaf-wise の方が損失の減少が大きくなります。
Python でのスクラッチ実装
回帰木ベースの勾配ブースティング
まず、簡単な回帰木(CART)と勾配ブースティング回帰を実装します。
import numpy as np
import matplotlib.pyplot as plt
class RegressionTreeNode:
"""回帰木のノード"""
def __init__(self, value=None, feature_index=None, threshold=None, left=None, right=None):
self.value = value # リーフの予測値
self.feature_index = feature_index # 分割特徴量
self.threshold = threshold # 分割閾値
self.left = left # 左の子ノード
self.right = right # 右の子ノード
class RegressionTree:
"""回帰木(CART)"""
def __init__(self, max_depth=3, min_samples_leaf=5):
self.max_depth = max_depth
self.min_samples_leaf = min_samples_leaf
self.root = None
def _mse(self, y):
"""平均二乗誤差"""
if len(y) == 0:
return 0
return np.var(y) * len(y)
def _best_split(self, X, y):
"""最適な分割を探索"""
n_samples, n_features = X.shape
best_gain = -float('inf')
best_feature = None
best_threshold = None
current_mse = self._mse(y)
for feature_idx in range(n_features):
thresholds = np.unique(X[:, feature_idx])
for threshold in thresholds:
left_mask = X[:, feature_idx] <= threshold
right_mask = ~left_mask
if np.sum(left_mask) < self.min_samples_leaf or np.sum(right_mask) < self.min_samples_leaf:
continue
gain = current_mse - self._mse(y[left_mask]) - self._mse(y[right_mask])
if gain > best_gain:
best_gain = gain
best_feature = feature_idx
best_threshold = threshold
return best_feature, best_threshold, best_gain
def _build_tree(self, X, y, depth):
"""再帰的に木を構築"""
# リーフノードの条件
if depth >= self.max_depth or len(y) < 2 * self.min_samples_leaf:
return RegressionTreeNode(value=np.mean(y))
feature_idx, threshold, gain = self._best_split(X, y)
if feature_idx is None or gain <= 0:
return RegressionTreeNode(value=np.mean(y))
left_mask = X[:, feature_idx] <= threshold
right_mask = ~left_mask
left_child = self._build_tree(X[left_mask], y[left_mask], depth + 1)
right_child = self._build_tree(X[right_mask], y[right_mask], depth + 1)
return RegressionTreeNode(
feature_index=feature_idx,
threshold=threshold,
left=left_child,
right=right_child
)
def fit(self, X, y):
"""学習"""
self.root = self._build_tree(X, y, depth=0)
def _predict_single(self, x, node):
"""1サンプルの予測"""
if node.value is not None:
return node.value
if x[node.feature_index] <= node.threshold:
return self._predict_single(x, node.left)
else:
return self._predict_single(x, node.right)
def predict(self, X):
"""予測"""
return np.array([self._predict_single(x, self.root) for x in X])
class GradientBoostingRegressor:
"""勾配ブースティング回帰のスクラッチ実装"""
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3, min_samples_leaf=5):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.min_samples_leaf = min_samples_leaf
self.trees = []
self.f0 = None # 初期予測値
self.train_losses = []
def fit(self, X, y):
"""学習"""
# 初期値: ターゲットの平均
self.f0 = np.mean(y)
f = np.full(len(y), self.f0)
for t in range(self.n_estimators):
# 擬似残差の計算(二乗誤差損失の場合は残差そのもの)
residuals = y - f
# 回帰木を擬似残差にフィット
tree = RegressionTree(max_depth=self.max_depth, min_samples_leaf=self.min_samples_leaf)
tree.fit(X, residuals)
# モデルの更新
update = tree.predict(X)
f += self.learning_rate * update
self.trees.append(tree)
# 訓練損失の記録
loss = np.mean((y - f) ** 2)
self.train_losses.append(loss)
def predict(self, X):
"""予測"""
f = np.full(X.shape[0], self.f0)
for tree in self.trees:
f += self.learning_rate * tree.predict(X)
return f
学習過程の可視化(回帰問題)
# 合成データの生成
np.random.seed(42)
X_train = np.sort(np.random.uniform(0, 10, 200)).reshape(-1, 1)
y_true = np.sin(X_train.ravel()) + 0.5 * np.sin(3 * X_train.ravel())
y_train = y_true + np.random.normal(0, 0.3, len(y_true))
# 勾配ブースティングの学習
gb = GradientBoostingRegressor(n_estimators=200, learning_rate=0.1, max_depth=3, min_samples_leaf=5)
gb.fit(X_train, y_train)
# --- 段階的な予測の可視化 ---
fig, axes = plt.subplots(2, 3, figsize=(16, 10))
X_test = np.linspace(0, 10, 500).reshape(-1, 1)
rounds_to_show = [1, 5, 10, 30, 100, 200]
for idx, n_trees in enumerate(rounds_to_show):
ax = axes[idx // 3, idx % 3]
# n_trees ラウンドまでの累積予測
f_pred = np.full(X_test.shape[0], gb.f0)
for t in range(min(n_trees, len(gb.trees))):
f_pred += gb.learning_rate * gb.trees[t].predict(X_test)
ax.scatter(X_train, y_train, c='gray', s=10, alpha=0.5, label='Training data')
ax.plot(X_test, np.sin(X_test.ravel()) + 0.5 * np.sin(3 * X_test.ravel()),
'g--', linewidth=2, label='True function')
ax.plot(X_test, f_pred, 'r-', linewidth=2, label='GBDT prediction')
ax.set_title(f'T = {min(n_trees, len(gb.trees))}', fontsize=13)
ax.legend(fontsize=9)
ax.set_ylim(-2.5, 2.5)
ax.grid(True, alpha=0.3)
plt.suptitle('Gradient Boosting Regression: Prediction at Different Rounds', fontsize=15, y=1.02)
plt.tight_layout()
plt.savefig('gbdt_regression_progress.png', dpi=150, bbox_inches='tight')
plt.show()
# --- 訓練損失の推移 ---
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(range(1, len(gb.train_losses) + 1), gb.train_losses, 'b-', linewidth=2)
ax.set_xlabel('Number of estimators', fontsize=12)
ax.set_ylabel('Training MSE', fontsize=12)
ax.set_title('Training Loss vs Number of Estimators', fontsize=14)
ax.set_yscale('log')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('gbdt_training_loss.png', dpi=150, bbox_inches='tight')
plt.show()
scikit-learn との比較
from sklearn.ensemble import GradientBoostingRegressor as SklearnGBR
from sklearn.metrics import mean_squared_error
# scikit-learn のGBDT
sklearn_gb = SklearnGBR(n_estimators=200, learning_rate=0.1, max_depth=3, random_state=42)
sklearn_gb.fit(X_train, y_train)
# テストデータでの予測
X_test = np.linspace(0, 10, 500).reshape(-1, 1)
y_test_true = np.sin(X_test.ravel()) + 0.5 * np.sin(3 * X_test.ravel())
y_pred_scratch = gb.predict(X_test)
y_pred_sklearn = sklearn_gb.predict(X_test)
# 比較の可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].scatter(X_train, y_train, c='gray', s=10, alpha=0.5, label='Training data')
axes[0].plot(X_test, y_test_true, 'g--', linewidth=2, label='True function')
axes[0].plot(X_test, y_pred_scratch, 'r-', linewidth=2, label='Scratch GBDT')
axes[0].set_title('Scratch Implementation', fontsize=14)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)
axes[1].scatter(X_train, y_train, c='gray', s=10, alpha=0.5, label='Training data')
axes[1].plot(X_test, y_test_true, 'g--', linewidth=2, label='True function')
axes[1].plot(X_test, y_pred_sklearn, 'b-', linewidth=2, label='scikit-learn GBDT')
axes[1].set_title('scikit-learn Implementation', fontsize=14)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)
plt.suptitle('Gradient Boosting: Scratch vs scikit-learn', fontsize=15, y=1.02)
plt.tight_layout()
plt.savefig('gbdt_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# MSEの比較
mse_scratch = mean_squared_error(y_test_true, y_pred_scratch)
mse_sklearn = mean_squared_error(y_test_true, y_pred_sklearn)
print(f"Scratch GBDT - MSE: {mse_scratch:.4f}")
print(f"scikit-learn GBDT - MSE: {mse_sklearn:.4f}")
学習率の影響
# 学習率の異なるGBDTの比較
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
learning_rates = [0.01, 0.1, 0.5]
for idx, lr in enumerate(learning_rates):
ax = axes[idx]
gb_lr = GradientBoostingRegressor(n_estimators=200, learning_rate=lr, max_depth=3)
gb_lr.fit(X_train, y_train)
y_pred = gb_lr.predict(X_test)
ax.scatter(X_train, y_train, c='gray', s=10, alpha=0.5)
ax.plot(X_test, y_test_true, 'g--', linewidth=2, label='True')
ax.plot(X_test, y_pred, 'r-', linewidth=2, label=f'GBDT (lr={lr})')
ax.set_title(f'Learning Rate = {lr}', fontsize=13)
ax.legend(fontsize=10)
ax.set_ylim(-2.5, 2.5)
ax.grid(True, alpha=0.3)
plt.suptitle('Effect of Learning Rate on GBDT', fontsize=15, y=1.02)
plt.tight_layout()
plt.savefig('gbdt_learning_rate.png', dpi=150, bbox_inches='tight')
plt.show()
学習率が小さいほど各ステップでの更新が控えめになり、より滑らかで安定した予測になります。一方で、同じラウンド数では学習が不十分になる場合があり、より多くのラウンドが必要です。学習率 $\eta = 0.5$ では振動的な挙動が見られ、過学習のリスクが高まります。
まとめ
本記事では、勾配ブースティング(GBDT)の理論と実装を詳しく解説しました。
- 基本思想: 損失関数の負の勾配(擬似残差)を弱学習器でフィッティングする、関数空間での勾配降下法です
- 一般的な定式化: 任意の微分可能な損失関数に適用でき、回帰(二乗誤差 → 残差)、分類(対数損失 → 観測値と予測確率の差)として具体化されます
- 学習率と正則化: 縮小率 $\eta$、確率的サンプリング、木の深さ制限など、複数の正則化手法で過学習を抑制します
- XGBoost: 損失関数の 2 次テイラー展開により、リーフ値と分割ゲインの閉形式解を導出し、正則化項で木の複雑さを制御します
- LightGBM: GOSS(勾配ベースサンプリング)と EFB(排他的特徴量バンドリング)で大規模データに対応し、Leaf-wise 成長戦略で効率的に木を構築します
次のステップとして、以下の記事も参考にしてください。