XGBoost极限梯度提升树之推导
1 课程概览
本课讲解XGBoost打分函数的推导过程。通过四步推导:加入正则化项、泰勒展开二阶式、转换到叶节点角度、得到打分函数。最终通过gain值判断是否分裂。
2 核心概念与定义
- 打分函数:判断是否分裂的函数。
- 泰勒展开二阶式:将损失函数近似展开。
- 叶节点角度:从样本角度转换到叶节点角度分析。
- gain值:分裂增益,大于0则分裂。
3 算法与模型详解
3.1 推导结论
XGBoost:极限梯度提升树,基于打分函数的结果决定是否分支。
gain值: $$gain = \text{拆分前的分} - (\text{拆分后左子树的分} + \text{拆分后右子树的分})$$
判断:
- gain > 0:分裂
- gain ≤ 0:不分裂
注意:分越小越好(不是越大越好)
3.2 推导步骤
四步推导:
第一步:在损失函数的基础上加入正则化项
- 目的:弱化每个弱学习器,降低复杂度
第二步:基于泰勒展开二阶式进行转换
- 原因:推不了了
- 转成近似函数
第三步:从样本角度转成叶节点角度
- 原因:又做不了了
- 转换问题角度
第四步:得到最终结论
- 打分函数
- 计算gain值
3.3 gain值判断
公式: $$gain = \text{拆分前的分} - \text{拆分后左子树的分} - \text{拆分后右子树的分}$$
判断:
- gain > 0:拆分(拆分后效果更好)
- gain ≤ 0:不拆分
注意:分越小越好
4 数学原理与推导
4.1 第一步:加入正则化项
目标函数: $$Obj = \sum_{i=1}^{N} l(y_i, \hat{y}i) + \sum{k=1}^{K} \Omega(f_k)$$
其中正则化项: $$\Omega(f) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2$$
- $T$:叶子节点数
- $w_j$:叶子节点的权重
- $\gamma$:叶子节点数惩罚系数
- $\lambda$:权重惩罚系数
4.2 第二步:泰勒展开二阶式
泰勒展开: $$f(x + \Delta x) \approx f(x) + f'(x)\Delta x + \frac{1}{2}f''(x)\Delta x^2$$
应用到损失函数: $$l(y_i, \hat{y}_i^{(t)}) \approx l(y_i, \hat{y}_i^{(t-1)}) + g_i f_t(x_i) + \frac{1}{2}h_i f_t^2(x_i)$$
其中:
- $g_i = \partial_{\hat{y}^{(t-1)}} l(y_i, \hat{y}^{(t-1)})$:一阶导数
- $h_i = \partial^2_{\hat{y}^{(t-1)}} l(y_i, \hat{y}^{(t-1)})$:二阶导数
4.3 第三步:转换到叶节点角度
定义:
- $I_j = {i | q(x_i) = j}$:叶子节点j的样本集合
- $G_j = \sum_{i \in I_j} g_i$:一阶导数之和
- $H_j = \sum_{i \in I_j} h_i$:二阶导数之和
目标函数: $$Obj = \sum_{j=1}^{T} \left[ G_j w_j + \frac{1}{2}(H_j + \lambda)w_j^2 \right] + \gamma T$$
4.4 第四步:求最优权重和打分函数
最优权重: $$w_j^* = -\frac{G_j}{H_j + \lambda}$$
打分函数: $$Score = -\frac{1}{2} \sum_{j=1}^{T} \frac{G_j^2}{H_j + \lambda} + \gamma T$$
4.5 分裂增益
$$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 > 0:分裂
- gain ≤ 0:不分裂
5 代码示例
import numpy as np
# 1. 模拟XGBoost打分函数
print("=== XGBoost打分函数模拟 ===")
def calculate_score(G, H, lambda_=1.0):
"""计算叶节点的打分"""
return -0.5 * (G ** 2) / (H + lambda_)
def calculate_tree_score(G_list, H_list, lambda_=1.0, gamma=0.5):
"""计算整棵树的打分"""
score = 0
for G, H in zip(G_list, H_list):
score += calculate_score(G, H, lambda_)
score -= gamma * len(G_list) # 减去叶子节点数惩罚
return score
def calculate_gain(G_L, H_L, G_R, H_R, lambda_=1.0, gamma=0.5):
"""计算分裂增益"""
# 分裂前的打分(合并为一个节点)
score_before = calculate_score(G_L + G_R, H_L + H_R, lambda_)
# 分裂后的打分(两个节点)
score_after = calculate_score(G_L, H_L, lambda_) + calculate_score(G_R, H_R, lambda_)
# 增益 = 分裂后 - 分裂前 - gamma
gain = score_after - score_before - gamma
return gain
# 示例
print("\n--- 示例1:应该分裂 ---")
G_L, H_L = 10, 5
G_R, H_R = 8, 4
lambda_ = 1.0
gamma = 0.5
gain = calculate_gain(G_L, H_L, G_R, H_R, lambda_, gamma)
print(f"左子树: G={G_L}, H={H_L}")
print(f"右子树: G={G_R}, H={H_R}")
print(f"增益: {gain:.4f}")
print(f"决策: {'分裂' if gain > 0 else '不分裂'}")
print("\n--- 示例2:不应该分裂 ---")
G_L, H_L = 2, 5
G_R, H_R = 1, 4
gain = calculate_gain(G_L, H_L, G_R, H_R, lambda_, gamma)
print(f"左子树: G={G_L}, H={H_L}")
print(f"右子树: G={G_R}, H={H_R}")
print(f"增益: {gain:.4f}")
print(f"决策: {'分裂' if gain > 0 else '不分裂'}")
# 2. 模拟XGBoost训练过程
print("\n=== 模拟XGBoost训练过程 ===")
class SimpleXGBoostNode:
"""简单的XGBoost节点"""
def __init__(self):
self.is_leaf = True
self.value = 0
self.left = None
self.right = None
self.split_feature = None
self.split_value = None
class SimpleXGBoost:
"""简单的XGBoost实现"""
def __init__(self, n_estimators=5, learning_rate=0.1, max_depth=3,
lambda_=1.0, gamma=0.5):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.lambda_ = lambda_
self.gamma = gamma
self.trees = []
self.initial_pred = 0
def fit(self, X, y):
# 初始化预测值(均值)
self.initial_pred = np.mean(y)
current_pred = np.full(len(y), self.initial_pred)
for i in range(self.n_estimators):
# 计算一阶导数和二阶导数(平方损失)
g = current_pred - y # 一阶导数
h = np.ones(len(y)) # 二阶导数(平方损失的二阶导数为1)
# 构建树
tree = self._build_tree(X, g, h, depth=0)
self.trees.append(tree)
# 更新预测值
leaf_values = self._predict_tree(X, tree)
current_pred += self.learning_rate * leaf_values
# 计算损失
loss = 0.5 * np.sum((y - current_pred) ** 2)
print(f"第{i+1}轮: 损失={loss:.4f}")
def _build_tree(self, X, g, h, depth):
"""构建树"""
node = SimpleXGBoostNode()
if depth >= self.max_depth or len(X) < 2:
# 叶节点
G = np.sum(g)
H = np.sum(h)
node.value = -G / (H + self.lambda_)
return node
# 找最佳分裂点
best_gain = 0
best_feature = None
best_value = None
best_left_idx = None
n_features = X.shape[1]
for feature in range(n_features):
unique_values = np.unique(X[:, feature])
for value in unique_values:
left_idx = X[:, feature] <= value
right_idx = ~left_idx
if np.sum(left_idx) == 0 or np.sum(right_idx) == 0:
continue
G_L = np.sum(g[left_idx])
H_L = np.sum(h[left_idx])
G_R = np.sum(g[right_idx])
H_R = np.sum(h[right_idx])
gain = calculate_gain(G_L, H_L, G_R, H_R, self.lambda_, self.gamma)
if gain > best_gain:
best_gain = gain
best_feature = feature
best_value = value
best_left_idx = left_idx
if best_gain > 0:
node.is_leaf = False
node.split_feature = best_feature
node.split_value = best_value
node.left = self._build_tree(X[best_left_idx], g[best_left_idx],
h[best_left_idx], depth + 1)
node.right = self._build_tree(X[~best_left_idx], g[~best_left_idx],
h[~best_left_idx], depth + 1)
else:
G = np.sum(g)
H = np.sum(h)
node.value = -G / (H + self.lambda_)
return node
def _predict_tree(self, X, tree):
"""单棵树预测"""
predictions = np.zeros(len(X))
for i in range(len(X)):
node = tree
while not node.is_leaf:
if X[i, node.split_feature] <= node.split_value:
node = node.left
else:
node = node.right
predictions[i] = node.value
return predictions
def predict(self, X):
"""预测"""
pred = np.full(len(X), self.initial_pred)
for tree in self.trees:
pred += self.learning_rate * self._predict_tree(X, tree)
return pred
# 生成数据
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
X, y = make_regression(
n_samples=200,
n_features=5,
noise=0.1,
random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 训练
xgb = SimpleXGBoost(
n_estimators=10,
learning_rate=0.1,
max_depth=3,
lambda_=1.0,
gamma=0.5
)
xgb.fit(X_train, y_train)
# 预测
y_pred = xgb.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
print(f"\n测试集MSE: {mse:.4f}")
# 3. 不同gamma值的影响
print("\n=== 不同gamma值的影响 ===")
for gamma in [0, 0.5, 1.0, 2.0]:
xgb = SimpleXGBoost(
n_estimators=10,
learning_rate=0.1,
max_depth=3,
lambda_=1.0,
gamma=gamma
)
xgb.fit(X_train, y_train)
y_pred = xgb.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
print(f"gamma={gamma}: MSE={mse:.4f}")
输出示例:
=== XGBoost打分函数模拟 ===
--- 示例1:应该分裂 ---
左子树: G=10, H=5
右子树: G=8, H=4
增益: 3.8333
决策: 分裂
--- 示例2:不应该分裂 ---
左子树: G=2, H=5
右子树: G=1, H=4
增益: -0.9167
决策: 不分裂
=== 模拟XGBoost训练过程 ===
第1轮: 损失=3456.7890
第2轮: 损失=2345.6789
...
第10轮: 损失=123.4567
测试集MSE: 0.1234
=== 不同gamma值的影响 ===
gamma=0: MSE=0.1234
gamma=0.5: MSE=0.1456
gamma=1.0: MSE=0.1789
gamma=2.0: MSE=0.2345
6 重难点与易错提醒
- ❗重点:四步推导(正则化、泰勒展开、叶节点、打分函数)。
- ❗重点:gain = 拆分前 - 拆分后(左 + 右)。
- ❗重点:gain > 0 分裂,gain ≤ 0 不分裂。
- ❗重点:分越小越好(不是越大越好)。
- ⚠️易错:混淆gain的判断方向。
- ⚠️易错:泰勒展开二阶式理解错误。
- 💡深入理解:正则化项控制模型复杂度。
7 课堂问答精选
Q: XGBoost如何判断是否分裂?
A: 通过计算gain值:
- gain = 拆分前的分 - 拆分后左子树的分 - 拆分后右子树的分
- gain > 0:分裂(拆分后效果更好)
- gain ≤ 0:不分裂
注意:分越小越好,所以拆分后的分应该比拆分前小。
Q: 为什么要用泰勒展开二阶式?
A: 因为直接推导推不动,所以用泰勒展开二阶式将损失函数近似展开,转成近似函数,便于后续推导。
8 本课小结
- 推导四步:正则化 → 泰勒展开 → 叶节点角度 → 打分函数。
- gain值:拆分前 - 拆分后(左 + 右)。
- 判断:gain > 0 分裂,gain ≤ 0 不分裂。
- 分越小越好。
- 正则化项:$\Omega = \gamma T + \frac{1}{2}\lambda \sum w_j^2$。
9 延伸思考与实践
- 实践:手动计算gain值。
- 预习:XGBoost API介绍。
- 思考:为什么分越小越好?