梯度提升原理 #
什么是 Boosting? #
Boosting 是一种集成学习方法,通过串行训练多个弱学习器,每个学习器都试图纠正前一个学习器的错误,最终组合成强学习器。
Boosting vs Bagging #
text
┌─────────────────────────────────────────────────────────────┐
│ 集成学习方法对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Boosting(提升) Bagging(装袋) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 弱学习器1 │ │ 学习器1 │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ ↓ │ │
│ ┌─────────────────┐ ┌────────┴────────┐ │
│ │ 弱学习器2 │ │ 学习器2 学习器3 │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ ↓ │ │
│ ┌─────────────────┐ ┌────────┴────────┐ │
│ │ 弱学习器3 │ │ 投票/平均 │ │
│ └────────┬────────┘ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ 最终模型 │ │
│ └─────────────────┘ │
│ │
│ 串行训练,纠正错误 并行训练,减少方差 │
│ 降低偏差 降低方差 │
│ │
└─────────────────────────────────────────────────────────────┘
Boosting 的核心思想 #
text
初始模型 → 计算残差 → 训练新模型拟合残差 → 更新模型 → 重复
梯度提升算法 #
基本原理 #
梯度提升(Gradient Boosting)将 Boosting 思想与梯度下降结合,使用损失函数的负梯度作为残差的近似值。
text
┌─────────────────────────────────────────────────────────────┐
│ 梯度提升核心思想 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 初始化模型 F₀(x) │
│ │
│ 2. For m = 1 to M: │
│ a) 计算负梯度(伪残差): │
│ rᵢₘ = -∂L(yᵢ, Fₘ₋₁(xᵢ)) / ∂Fₘ₋₁(xᵢ) │
│ │
│ b) 拟合一个弱学习器 hₘ(x) 到残差 │
│ │
│ c) 线性搜索找到最优步长 γₘ │
│ │
│ d) 更新模型: Fₘ(x) = Fₘ₋₁(x) + γₘhₘ(x) │
│ │
│ 3. 输出最终模型 Fₘ(x) │
│ │
└─────────────────────────────────────────────────────────────┘
数学推导 #
1. 目标函数 #
text
最小化损失函数:
min Σ L(yᵢ, F(xᵢ))
2. 梯度下降 #
在函数空间中进行梯度下降:
text
Fₘ(x) = Fₘ₋₁(x) + η · (-∂L/∂F)
其中:
- η 是学习率
- -∂L/∂F 是负梯度方向
3. 负梯度计算 #
对于不同损失函数,负梯度不同:
| 损失函数 | 表达式 | 负梯度 |
|---|---|---|
| 平方损失 | L = ½(y-F)² | y - F(残差) |
| 绝对损失 | L = |y-F| | sign(y-F) |
| Log损失 | L = log(1+exp(-yF)) | y/(1+exp(yF)) |
Python 实现 #
python
import numpy as np
from sklearn.tree import DecisionTreeRegressor
class SimpleGradientBoosting:
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.trees = []
def fit(self, X, y):
n_samples = X.shape[0]
# 初始化:使用均值
self.F0 = np.mean(y)
F = np.full(n_samples, self.F0)
for i in range(self.n_estimators):
# 计算负梯度(对于平方损失,就是残差)
residual = y - F
# 拟合残差
tree = DecisionTreeRegressor(max_depth=self.max_depth)
tree.fit(X, residual)
# 更新预测
update = tree.predict(X)
F += self.learning_rate * update
self.trees.append(tree)
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
# 测试
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=1000, 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)
model = SimpleGradientBoosting(n_estimators=100, learning_rate=0.1)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"MSE: {mean_squared_error(y_test, y_pred):.4f}")
梯度提升决策树(GBDT) #
GBDT 流程 #
text
┌─────────────────────────────────────────────────────────────┐
│ GBDT 训练流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 初始化 │
│ F₀(x) = argmin_c Σ L(yᵢ, c) │
│ (对于平方损失,c = mean(y)) │
│ │
│ Step 2: For m = 1 to M: │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ a) 计算负梯度(伪残差) │ │
│ │ rᵢₘ = -∂L(yᵢ, Fₘ₋₁(xᵢ))/∂Fₘ₋₁(xᵢ) │ │
│ │ │ │
│ │ b) 拟合回归树到残差 │ │
│ │ 将数据划分为 Rⱼₘ 个区域 │ │
│ │ │ │
│ │ c) 计算每个区域的最优值 │ │
│ │ γⱼₘ = argmin_γ Σ L(yᵢ, Fₘ₋₁(xᵢ) + γ) │ │
│ │ │ │
│ │ d) 更新模型 │ │
│ │ Fₘ(x) = Fₘ₋₁(x) + η · Σ γⱼₘ·I(x ∈ Rⱼₘ) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Step 3: 输出 Fₘ(x) │
│ │
└─────────────────────────────────────────────────────────────┘
GBDT 分类 #
对于分类问题,使用对数损失:
python
import numpy as np
def binary_logistic_gradient(y, F):
"""
二分类对数损失的负梯度
L = log(1 + exp(-y*F))
负梯度 = y / (1 + exp(y*F))
"""
return y / (1 + np.exp(y * F))
def softmax_gradient(y, F, K):
"""
多分类 Softmax 损失的负梯度
"""
prob = np.exp(F) / np.sum(np.exp(F), axis=1, keepdims=True)
return y - prob
GBDT 回归 #
对于回归问题,常用平方损失:
python
def squared_error_gradient(y, F):
"""
平方损失的负梯度
L = 0.5 * (y - F)^2
负梯度 = y - F
"""
return y - F
def absolute_error_gradient(y, F):
"""
绝对损失的负梯度
L = |y - F|
负梯度 = sign(y - F)
"""
return np.sign(y - F)
XGBoost 的改进 #
XGBoost 在传统 GBDT 基础上做了多项改进:
1. 正则化目标函数 #
text
Obj = Σ L(yᵢ, ŷᵢ) + Σ Ω(fₖ)
其中:
Ω(f) = γT + ½λ||w||²
- T: 叶子节点数量
- w: 叶子节点权重
- γ: 叶子节点数惩罚
- λ: L2 正则化权重
2. 二阶泰勒展开 #
XGBoost 使用二阶泰勒展开近似损失函数:
text
L(y, ŷ) ≈ L(y, ŷ⁽ᵗ⁻¹⁾) + gᵢfₜ(xᵢ) + ½hᵢfₜ²(xᵢ)
其中:
- gᵢ = ∂L/∂ŷ⁽ᵗ⁻¹⁾ (一阶导数)
- hᵢ = ∂²L/∂ŷ²⁽ᵗ⁻¹⁾ (二阶导数)
python
def compute_gradients(y, pred, objective='regression'):
"""
计算一阶和二阶梯度
"""
if objective == 'regression':
g = pred - y
h = np.ones_like(y)
elif objective == 'binary':
prob = 1 / (1 + np.exp(-pred))
g = prob - y
h = prob * (1 - prob)
elif objective == 'multiclass':
prob = np.exp(pred) / np.sum(np.exp(pred), axis=1, keepdims=True)
g = prob - y
h = prob * (1 - prob)
return g, h
3. 最优分裂点 #
XGBoost 使用二阶信息找到最优分裂点:
text
增益 = ½ [ (Σgᵢ)²/(Σhᵢ+λ) + (Σgⱼ)²/(Σhⱼ+λ) - (Σgᵢ+Σgⱼ)²/(Σhᵢ+Σhⱼ+λ) ] - γ
python
def calculate_split_gain(G_left, H_left, G_right, H_right, gamma, lambda_):
"""
计算分裂增益
"""
gain_left = G_left**2 / (H_left + lambda_)
gain_right = G_right**2 / (H_right + lambda_)
gain_total = (G_left + G_right)**2 / (H_left + H_right + lambda_)
return 0.5 * (gain_left + gain_right - gain_total) - gamma
4. 稀疏感知 #
XGBoost 自动学习缺失值的最优分裂方向:
python
def find_best_split_with_missing(X, g, h, feature_idx, gamma=0, lambda_=1):
"""
考虑缺失值的分裂
"""
# 非缺失值
mask = ~np.isnan(X[:, feature_idx])
X_valid = X[mask, feature_idx]
g_valid = g[mask]
h_valid = h[mask]
# 缺失值
g_missing = g[~mask]
h_missing = h[~mask]
# 尝试将缺失值分到左子树
gain_left = find_split(X_valid, g_valid, h_valid, gamma, lambda_)
# 尝试将缺失值分到右子树
gain_right = find_split(X_valid, g_valid, h_valid, gamma, lambda_)
# 选择最优方向
return max(gain_left, gain_right)
学习率的作用 #
学习率(η)控制每棵树的贡献:
python
import matplotlib.pyplot as plt
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=1000, n_features=10, noise=0.1, random_state=42)
learning_rates = [0.01, 0.1, 0.5, 1.0]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for ax, lr in zip(axes.flat, learning_rates):
model = GradientBoostingRegressor(
n_estimators=100,
learning_rate=lr,
max_depth=3,
random_state=42
)
model.fit(X, y)
train_score = model.train_score_
ax.plot(train_score)
ax.set_xlabel('Iterations')
ax.set_ylabel('Training Loss')
ax.set_title(f'Learning Rate = {lr}')
ax.grid(True)
plt.tight_layout()
plt.show()
学习率与迭代次数的关系 #
text
┌─────────────────────────────────────────────────────────────┐
│ 学习率 vs 迭代次数 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 学习率小 (η=0.01) │
│ - 需要更多迭代次数 │
│ - 收敛慢,但更稳定 │
│ - 泛化性能通常更好 │
│ │
│ 学习率大 (η=0.3) │
│ - 需要较少迭代次数 │
│ - 收敛快,但可能过拟合 │
│ - 需要更早停止 │
│ │
│ 经验法则: │
│ η × n_estimators ≈ 常数 │
│ │
└─────────────────────────────────────────────────────────────┘
梯度提升的优缺点 #
优点 #
text
1. 高准确率
- 通过纠正错误不断改进
- 在许多任务上表现优异
2. 灵活性
- 可使用不同的损失函数
- 可与其他模型结合
3. 特征重要性
- 自动计算特征重要性
- 便于特征选择
4. 处理缺失值
- 自动处理缺失值
- 无需额外处理
缺点 #
text
1. 训练时间
- 串行训练,不能并行
- 大数据集训练较慢
2. 过拟合风险
- 需要仔细调参
- 需要早停策略
3. 超参数多
- 需要调参经验
- 调参时间较长
4. 可解释性
- 虽然比神经网络好
- 但不如单棵决策树
梯度提升 vs 其他方法 #
| 特性 | 梯度提升 | 随机森林 | AdaBoost |
|---|---|---|---|
| 集成方式 | Boosting | Bagging | Boosting |
| 基学习器 | 任意 | 决策树 | 决策树 |
| 权重更新 | 梯度下降 | 无 | 样本权重 |
| 并行性 | 串行 | 并行 | 串行 |
| 过拟合 | 需控制 | 较少 | 需控制 |
| 异常值 | 敏感 | 较稳健 | 敏感 |
下一步 #
现在你已经理解了梯度提升原理,接下来学习 决策树基础 了解 XGBoost 的基本构建块!
最后更新:2026-04-04