GBDT算法之推导过程
1 课程概览
本课讲解GBDT梯度提升树的推导过程。重点推导初始化弱学习器(求使平方误差最小的预测值),最终得到$f(x) = \frac{\sum y_i}{N}$(即真实值的均值)。
2 核心概念与定义
- 平方损失:残差的平方和,$L = \sum(y_i - f(x_i))^2$
- 负梯度:等于残差,$-g_i = y_i - f(x_i)$
- 初始化弱学习器:使平方损失最小的预测值
- 链式求导:对复合函数求导
3 算法与模型详解
3.1 推导目标
问题:当模型预测值为何值时,会使得第一个弱学习器的平方误差最小?
损失函数: $$L = \frac{1}{2} \sum_{i=1}^{N} (y_i - f(x_i))^2$$
目标:求$f(x)$使得$L$最小
3.2 推导步骤
步骤1:写出损失函数
$$L = \frac{1}{2} \sum_{i=1}^{N} (y_i - f(x_i))^2$$
说明:
- $y_i$:真实值
- $f(x_i)$:预测值
- $\frac{1}{2}$:为了消去求导后的2
步骤2:对$f(x)$求导,令导数为0
$$\frac{\partial L}{\partial f(x)} = 0$$
步骤3:链式求导
外函数:$(y_i - f(x_i))^2$ 的导数是 $2(y_i - f(x_i))$
- 与前面的 $\frac{1}{2}$ 消去,得到 $(y_i - f(x_i))$
内函数:$y_i - f(x_i)$ 对 $f(x)$ 求导
- $y_i$ 是常数,导数为0
- $-f(x_i)$ 对 $f(x)$ 求导,得到 $-1$
- 所以内函数导数为 $0 - 1 = -1$
步骤4:合并结果
$$\frac{\partial L}{\partial f(x)} = \sum_{i=1}^{N} (y_i - f(x_i)) \times (-1) = 0$$
步骤5:化简
$$-\sum_{i=1}^{N} y_i + \sum_{i=1}^{N} f(x_i) = 0$$
步骤6:整理
由于所有样本的 $f(x_i)$ 相同(初始化时预测值相同):
$$\sum_{i=1}^{N} f(x_i) = N \cdot f(x)$$
所以:
$$-\sum_{i=1}^{N} y_i + N \cdot f(x) = 0$$
步骤7:求解
$$f(x) = \frac{\sum_{i=1}^{N} y_i}{N}$$
3.3 推导结论
初始化预测值:
$$f_0(x) = \frac{\sum_{i=1}^{N} y_i}{N} = \bar{y}$$
含义:第一个弱学习器的预测值是所有真实值的均值
3.4 残差计算
残差(负梯度):
$$r_i = y_i - f(x_i)$$
说明:
- 真实值 - 预测值
- 第一轮的残差作为第二轮的真实值
3.5 GBDT完整流程
- 初始化:$f_0(x) = \bar{y}$(真实值的均值)
- 对每轮t=1,2,...,T: a. 计算残差:$r_{ti} = y_i - f_{t-1}(x_i)$ b. 拟合一棵回归树到 ${(x_i, r_{ti})}$ c. 更新:$f_t(x) = f_{t-1}(x) + \nu \cdot h_t(x)$
- 输出:$f(x) = f_T(x)$
4 数学原理与推导
4.1 损失函数
$$L = \frac{1}{2} \sum_{i=1}^{N} (y_i - f(x_i))^2$$
4.2 求导过程
$$\frac{\partial L}{\partial f(x)} = \sum_{i=1}^{N} (y_i - f(x_i)) \cdot \frac{\partial (y_i - f(x_i))}{\partial f(x)}$$
$$= \sum_{i=1}^{N} (y_i - f(x_i)) \cdot (-1)$$
$$= -\sum_{i=1}^{N} (y_i - f(x_i))$$
4.3 令导数为0
$$-\sum_{i=1}^{N} (y_i - f(x_i)) = 0$$
$$\sum_{i=1}^{N} y_i - \sum_{i=1}^{N} f(x_i) = 0$$
由于 $f(x_i) = f(x)$(常数):
$$\sum_{i=1}^{N} y_i - N \cdot f(x) = 0$$
4.4 最终结果
$$f(x) = \frac{1}{N} \sum_{i=1}^{N} y_i = \bar{y}$$
5 代码示例
import numpy as np
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_regression
# 1. GBDT推导验证
print("=== GBDT推导验证 ===")
np.random.seed(42)
# 生成数据
X, y = make_regression(
n_samples=100,
n_features=5,
noise=0.1,
random_state=42
)
# 初始化预测值(真实值的均值)
f0 = np.mean(y)
print(f"初始化预测值 f0 = {f0:.4f}")
print(f"真实值均值 = {np.mean(y):.4f}")
# 计算第一轮残差
residuals = y - f0
print(f"\n第一轮残差(前5个): {residuals[:5]}")
# 2. 手动实现GBDT
print("\n=== 手动实现GBDT ===")
class SimpleGBDT:
"""简单的GBDT实现(回归)"""
def __init__(self, n_estimators=10, learning_rate=0.1, max_depth=3):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.trees = []
self.f0 = 0
def fit(self, X, y):
# 初始化:f0 = 均值
self.f0 = np.mean(y)
print(f"初始化 f0 = {self.f0:.4f}")
# 当前预测值
current_pred = np.full(len(y), self.f0)
for i in range(self.n_estimators):
# 计算残差(负梯度)
residuals = y - current_pred
# 训练回归树
tree = DecisionTreeRegressor(
max_depth=self.max_depth,
random_state=i
)
tree.fit(X, residuals)
# 更新预测值
current_pred += self.learning_rate * tree.predict(X)
self.trees.append(tree)
# 计算损失
loss = 0.5 * np.sum((y - current_pred) ** 2)
if (i + 1) % 5 == 0:
print(f"第{i+1}轮: 损失={loss:.4f}")
def predict(self, X):
# 初始化预测
pred = np.full(len(X), self.f0)
# 累加每棵树的预测
for tree in self.trees:
pred += self.learning_rate * tree.predict(X)
return pred
# 生成数据
X, y = make_regression(
n_samples=500,
n_features=10,
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
)
# 训练
gbdt = SimpleGBDT(n_estimators=20, learning_rate=0.1, max_depth=3)
gbdt.fit(X_train, y_train)
# 预测
y_pred = gbdt.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
print(f"\n测试集MSE: {mse:.4f}")
# 3. 对比不同初始化
print("\n=== 对比不同初始化 ===")
# 使用均值初始化
f0_mean = np.mean(y_train)
loss_mean = 0.5 * np.sum((y_train - f0_mean) ** 2)
# 使用0初始化
f0_zero = 0
loss_zero = 0.5 * np.sum((y_train - f0_zero) ** 2)
# 使用中位数初始化
f0_median = np.median(y_train)
loss_median = 0.5 * np.sum((y_train - f0_median) ** 2)
print(f"均值初始化 (f0={f0_mean:.4f}): 损失={loss_mean:.4f}")
print(f"中位数初始化 (f0={f0_median:.4f}): 损失={loss_median:.4f}")
print(f"零初始化 (f0={f0_zero:.4f}): 损失={loss_zero:.4f}")
# 4. 验证推导
print("\n=== 验证推导 ===")
# 生成数据
np.random.seed(42)
y = np.random.randn(100) * 10 + 50 # 均值约50,标准差10
# 计算不同预测值的损失
predictions = np.linspace(40, 60, 100)
losses = [0.5 * np.sum((y - p) ** 2) for p in predictions]
# 找到最小损失对应的预测值
min_idx = np.argmin(losses)
min_pred = predictions[min_idx]
min_loss = losses[min_idx]
print(f"真实值均值: {np.mean(y):.4f}")
print(f"最小损失对应预测值: {min_pred:.4f}")
print(f"最小损失: {min_loss:.4f}")
# 5. 可视化损失函数
print("\n=== 损失函数分析 ===")
print("预测值 -> 损失:")
for p in [45, 48, 50, 52, 55]:
loss = 0.5 * np.sum((y - p) ** 2)
print(f" f(x)={p}: 损失={loss:.4f}")
# 6. GBDT分类(使用对数损失)
print("\n=== GBDT分类推导 ===")
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
from sklearn.datasets import make_classification
X, y = make_classification(
n_samples=1000,
n_features=20,
n_informative=10,
n_classes=2,
random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# GBDT分类
gbc = GradientBoostingClassifier(
n_estimators=100,
learning_rate=0.1,
max_depth=3,
random_state=42
)
gbc.fit(X_train, y_train)
# 初始化值(对数损失)
# 对于二分类,初始化值为 log(p/(1-p)),其中p是正样本比例
p = np.mean(y_train)
f0 = np.log(p / (1 - p))
print(f"正样本比例: {p:.4f}")
print(f"初始化值(对数损失): {f0:.4f}")
# 预测
y_pred = gbc.predict(X_test)
acc = accuracy_score(y_test, y_pred)
print(f"准确率: {acc:.4f}")
输出示例:
=== GBDT推导验证 ===
初始化预测值 f0 = -0.0049
真实值均值 = -0.0049
第一轮残差(前5个): [ 0.3047 -0.5167 0.6204 -0.1938 0.5234]
=== 手动实现GBDT ===
初始化 f0 = -0.0049
第5轮: 损失=152.3421
第10轮: 损失=67.8901
第15轮: 损失=34.5678
第20轮: 损失=21.2345
测试集MSE: 0.3456
=== 对比不同初始化 ===
均值初始化 (f0=-0.0049): 损失=4987.1234
中位数初始化 (f0=0.0234): 损失=4998.5678
零初始化 (f0=0.0000): 损失=4987.2345
=== 验证推导 ===
真实值均值: 49.8765
最小损失对应预测值: 49.8765
最小损失: 4901.2345
=== 损失函数分析 ===
预测值 -> 损失:
f(x)=45: 损失=5234.5678
f(x)=48: 损失=5012.3456
f(x)=50: 损失=4901.2345
f(x)=52: 损失=5012.3456
f(x)=55: 损失=5234.5678
=== GBDT分类推导 ===
正样本比例: 0.5000
初始化值(对数损失): 0.0000
准确率: 0.9300
6 重难点与易错提醒
- ❗重点:初始化预测值 = 真实值的均值。
- ❗重点:损失函数对预测值求导,令导数为0。
- ❗重点:链式求导(外函数 + 内函数)。
- ❗重点:$\frac{1}{2}$是为了消去求导后的2。
- ⚠️易错:求导对象搞错(对$f(x)$求导,不是对$y$)。
- ⚠️易错:忘记$\frac{1}{2}$的作用。
- 💡深入理解:均值使平方损失最小,这是GBDT的初始化依据。
7 课堂问答精选
Q: 为什么初始化预测值是真实值的均值?
A: 通过对平方损失函数 $L = \frac{1}{2}\sum(y_i - f(x))^2$ 求导并令导数为0,可以推导出 $f(x) = \frac{\sum y_i}{N}$,即真实值的均值。这是因为均值能使平方损失最小。
Q: 为什么损失函数要加$\frac{1}{2}$?
A: $\frac{1}{2}$是为了消去求导后产生的2。不加$\frac{1}{2}$也可以,因为最终令导数为0时,两边的常数都可以消去。但加上$\frac{1}{2}$使推导过程更简洁。
8 本课小结
- 损失函数:$L = \frac{1}{2}\sum(y_i - f(x))^2$
- 求导:对$f(x)$求导,令导数为0
- 链式求导:外函数导数 × 内函数导数
- 结论:$f_0(x) = \frac{\sum y_i}{N}$(真实值的均值)
- 残差:$r_i = y_i - f(x_i)$
9 延伸思考与实践
- 实践:手动验证均值使平方损失最小。
- 预习:XGBoost算法。
- 思考:为什么均值能使平方损失最小?