第8章:提升方法(Boosting)
一、导读
1.1 本章学习目标
第8章介绍提升方法(Boosting)——一种强大的集成学习技术。提升方法的核心思想是:将多个弱学习器组合成一个强学习器。每个弱学习器只需要比随机猜测稍好,但通过巧妙地组合它们,可以得到性能非常好的模型。
本章的核心学习目标包括:
- 理解提升的思想:如何将弱学习器"提升"为强学习器?
- 掌握AdaBoost算法:如何通过调整样本权重来关注错分样本?
- 理解提升树:以决策树为基学习器的提升方法
- 掌握梯度提升树(GBDT):用梯度下降优化任意损失函数
- 了解XGBoost和LightGBM:工程优化的GBDT实现
- 实现AdaBoost算法
1.2 为什么本章如此重要
提升方法是机器学习中最成功的算法之一。在深度学习兴起之前,GBDT及其变体(XGBoost、LightGBM)几乎统治了结构化数据的机器学习竞赛。即使在深度学习时代,GBDT在许多实际应用中仍然是首选方法。
提升方法还揭示了一个深刻的理论结论:弱学习可以转化为强学习。这个结论连接了计算学习理论(PAC学习)和实际算法设计。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
什么是弱学习器?它需要满足什么条件?
AdaBoost是如何调整样本权重的?为什么关注错分样本是有效的?
提升树和普通的决策树集成有什么区别?
梯度提升的核心思想是什么?为什么它可以用任意损失函数?
学习率(shrinkage)在GBDT中起什么作用?
XGBoost相比GBDT做了哪些工程优化?
二、核心概念详解
8.1 提升的思想
提升(Boosting)的核心思想非常直观:
训练一个弱学习器(只需比随机好一点点)
关注被错分的样本
训练下一个弱学习器,更关注之前错分的样本
重复步骤2-3,组合所有弱学习器
关键定理(Schapire, 1990):弱学习器等价于强学习器。只要弱学习器比随机猜测稍好(准确率 > 50%),通过提升可以得到任意精度的强学习器。
8.2 AdaBoost算法
AdaBoost(Adaptive Boosting)是最经典的提升算法。
8.2.1 算法流程
初始化样本权重 D_1 = (1/N, ..., 1/N)
对 m = 1, 2, ..., M:
a. 在加权数据集上训练弱学习器 G_m(x)
b. 计算加权误差 e_m = Σ_i D_m(i) * I(G_m(x_i) ≠ y_i)
c. 计算学习器权重 α_m = (1/2) * log((1-e_m)/e_m)
d. 更新样本权重 D_{m+1}(i) = D_m(i) exp(-α_m y_i * G_m(x_i)) / Z_m
最终分类器 G(x) = sign(Σ_m α_m * G_m(x))
import numpy as np
class DecisionStump:
"""决策树桩:单层决策树(AdaBoost的弱学习器)"""
def __init__(self):
self.feature = None
self.threshold = None
self.polarity = 1
def fit(self, X, y, sample_weights):
n_samples, n_features = X.shape
best_error = float('inf')
for feature in range(n_features):
thresholds = np.unique(X[:, feature])
for threshold in thresholds:
for polarity in [1, -1]:
predictions = np.ones(n_samples)
if polarity == 1:
predictions[X[:, feature] <= threshold] = -1
else:
predictions[X[:, feature] > threshold] = -1
error = np.sum(sample_weights * (predictions != y))
if error < best_error:
best_error = error
self.feature = feature
self.threshold = threshold
self.polarity = polarity
return self
def predict(self, X):
predictions = np.ones(len(X))
if self.polarity == 1:
predictions[X[:, self.feature] <= self.threshold] = -1
else:
predictions[X[:, self.feature] > self.threshold] = -1
return predictions
class AdaBoost:
def __init__(self, n_estimators=50):
self.n_estimators = n_estimators
self.estimators = []
self.alphas = []
def fit(self, X, y):
n_samples = len(y)
weights = np.ones(n_samples) / n_samples
self.estimators = []
self.alphas = []
for m in range(self.n_estimators):
# 训练弱学习器
stump = DecisionStump()
stump.fit(X, y, weights)
predictions = stump.predict(X)
# 计算加权误差
error = np.sum(weights * (predictions != y))
error = np.clip(error, 1e-10, 1 - 1e-10)
# 计算学习器权重
alpha = 0.5 * np.log((1 - error) / error)
# 更新样本权重
weights *= np.exp(-alpha * y * predictions)
weights /= np.sum(weights)
self.estimators.append(stump)
self.alphas.append(alpha)
return self
def predict(self, X):
predictions = np.zeros(len(X))
for alpha, stump in zip(self.alphas, self.estimators):
predictions += alpha * stump.predict(X)
return np.sign(predictions)
def staged_predict(self, X):
"""逐步预测,用于观察提升过程"""
predictions = np.zeros(len(X))
for alpha, stump in zip(self.alphas, self.estimators):
predictions += alpha * stump.predict(X)
yield np.sign(predictions)
# 测试
np.random.seed(42)
X_pos = np.random.randn(50, 2) + np.array([1.5, 1.5])
X_neg = np.random.randn(50, 2) + np.array([-1, -1])
X = np.vstack([X_pos, X_neg])
y = np.array([1] * 50 + [-1] * 50)
# 单个决策树桩
stump = DecisionStump()
stump.fit(X, y, np.ones(len(y)) / len(y))
stump_acc = np.mean(stump.predict(X) == y)
# AdaBoost
adaboost = AdaBoost(n_estimators=50)
adaboost.fit(X, y)
boost_acc = np.mean(adaboost.predict(X) == y)
print(f"单个决策树桩准确率: {stump_acc:.4f}")
print(f"AdaBoost(50个树桩)准确率: {boost_acc:.4f}")
print(f"提升带来的改进: {boost_acc - stump_acc:.4f}")8.3 AdaBoost的损失函数
AdaBoost最小化的损失函数是指数损失:
L(y, f(x)) = exp(-y * f(x))其中 f(x) = Σ_m α_m * G_m(x) 是最终分类器的输出。
指数损失的特点:
- 对错分样本给予很大的惩罚
- 对正确分类但置信度低的样本也给予一定惩罚
- 对噪声和异常值敏感
import numpy as np
def exponential_loss(y, f):
"""指数损失"""
return np.mean(np.exp(-y * f))
# 对比不同损失函数
y = np.array([1, 1, -1, -1])
f_correct = np.array([2, 1, -1, -2]) # 正确分类,高置信度
f_wrong = np.array([0.1, -0.5, 0.5, -0.1]) # 错误分类
print(f"正确分类的指数损失: {exponential_loss(y, f_correct):.4f}")
print(f"错误分类的指数损失: {exponential_loss(y, f_wrong):.4f}")8.4 提升树
提升树(Boosting Tree)以决策树为基学习器。与AdaBoost不同,提升树直接拟合残差:
F_m(x) = F_{m-1}(x) + T(x; θ_m)其中 T(x; θ_m) 是第 m 棵决策树。
import numpy as np
class RegressionTreeStump:
"""回归树桩"""
def __init__(self, max_depth=1):
self.max_depth = max_depth
self.feature = None
self.threshold = None
self.left_value = None
self.right_value = None
def fit(self, X, y):
best_mse = float('inf')
for feature in range(X.shape[1]):
for threshold in np.unique(X[:, feature]):
left_mask = X[:, feature] <= threshold
right_mask = ~left_mask
if np.sum(left_mask) == 0 or np.sum(right_mask) == 0:
continue
left_mean = np.mean(y[left_mask])
right_mean = np.mean(y[right_mask])
predictions = np.where(left_mask, left_mean, right_mean)
mse = np.mean((y - predictions) ** 2)
if mse < best_mse:
best_mse = mse
self.feature = feature
self.threshold = threshold
self.left_value = left_mean
self.right_value = right_mean
return self
def predict(self, X):
return np.where(X[:, self.feature] <= self.threshold,
self.left_value, self.right_value)
class GradientBoostingRegressor:
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=1):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.trees = []
def fit(self, X, y):
# 初始预测:均值
self.initial_prediction = np.mean(y)
current_prediction = np.full(len(y), self.initial_prediction)
self.trees = []
for _ in range(self.n_estimators):
# 计算残差(负梯度)
residuals = y - current_prediction
# 拟合残差
tree = RegressionTreeStump(max_depth=self.max_depth)
tree.fit(X, residuals)
# 更新预测
update = self.learning_rate * tree.predict(X)
current_prediction += update
self.trees.append(tree)
return self
def predict(self, X):
prediction = np.full(len(X), self.initial_prediction)
for tree in self.trees:
prediction += self.learning_rate * tree.predict(X)
return prediction
# 测试
np.random.seed(42)
X = np.sort(np.random.rand(100, 1) * 10, axis=0)
y = np.sin(X).flatten() + np.random.randn(100) * 0.3
gbr = GradientBoostingRegressor(n_estimators=50, learning_rate=0.1)
gbr.fit(X, y)
predictions = gbr.predict(X)
mse = np.mean((y - predictions) ** 2)
print(f"GBDT均方误差: {mse:.4f}")8.5 梯度提升树(GBDT)
GBDT的核心思想是:用梯度下降来优化任意可微损失函数。
对于回归问题(平方损失):
- 残差 = y - F(x) = 负梯度
对于分类问题(对数损失):
- 负梯度 = y - P(Y=1|x)
GBDT的优势:
- 可以使用任意可微损失函数
- 天然支持回归和分类
- 通过正则化(学习率、子采样)控制过拟合
import numpy as np
class GBDTClassifier:
"""GBDT二分类(对数损失)"""
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 _sigmoid(self, z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
def fit(self, X, y):
# 初始预测:log(p/(1-p))
p = np.mean(y)
self.initial_prediction = np.log(p / (1 - p + 1e-10))
F = np.full(len(y), self.initial_prediction)
self.trees = []
for _ in range(self.n_estimators):
# 负梯度 = y - sigmoid(F)
pseudo_residuals = y - self._sigmoid(F)
# 拟合负梯度
tree = RegressionTreeStump()
tree.fit(X, pseudo_residuals)
# 更新
F += self.learning_rate * tree.predict(X)
self.trees.append(tree)
return self
def predict_proba(self, X):
F = np.full(len(X), self.initial_prediction)
for tree in self.trees:
F += self.learning_rate * tree.predict(X)
return self._sigmoid(F)
def predict(self, X):
return (self.predict_proba(X) >= 0.5).astype(int)
# 测试
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
gbdt = GBDTClassifier(n_estimators=50, learning_rate=0.1)
gbdt.fit(X, y)
accuracy = np.mean(gbdt.predict(X) == y)
print(f"GBDT分类准确率: {accuracy:.4f}")8.6 正则化:学习率与子采样
8.6.1 学习率(Shrinkage)
学习率 ν 控制每棵树对最终预测的贡献:
F_m(x) = F_{m-1}(x) + ν * T(x; θ_m)- ν 小(如0.01-0.1):每棵树贡献小,需要更多树,但泛化更好
- ν 大(如1.0):每棵树贡献大,容易过拟合
8.6.2 子采样(Stochastic Gradient Boosting)
每棵树只在训练数据的一个子集上训练(不放回采样),类似于随机森林的思想。
import numpy as np
def effect_of_learning_rate():
"""演示学习率的影响"""
np.random.seed(42)
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
split = 400
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
print(f"{'学习率':>8} {'树数量':>8} {'训练准确率':>12} {'测试准确率':>12}")
print("-" * 42)
for lr in [0.01, 0.05, 0.1, 0.5, 1.0]:
n_trees = int(200 * 0.1 / lr) # 保持总贡献大致相同
gbdt = GBDTClassifier(n_estimators=n_trees, learning_rate=lr)
gbdt.fit(X_train, y_train)
train_acc = np.mean(gbdt.predict(X_train) == y_train)
test_acc = np.mean(gbdt.predict(X_test) == y_test)
print(f"{lr:>8.2f} {n_trees:>8} {train_acc:>12.4f} {test_acc:>12.4f}")
effect_of_learning_rate()三、实战案例
3.1 AdaBoost提升过程可视化
import numpy as np
def visualize_boosting_process():
"""可视化AdaBoost的提升过程"""
np.random.seed(42)
X_pos = np.random.randn(30, 2) + np.array([1.5, 1.5])
X_neg = np.random.randn(30, 2) + np.array([-1, -1])
X = np.vstack([X_pos, X_neg])
y = np.array([1] * 30 + [-1] * 30)
adaboost = AdaBoost(n_estimators=20)
adaboost.fit(X, y)
print("AdaBoost提升过程:")
print(f"{'轮数':>5} {'α':>8} {'训练准确率':>12}")
print("-" * 28)
for i, (alpha, predictions) in enumerate(zip(adaboost.alphas,
adaboost.staged_predict(X))):
if i < 5 or i >= 18: # 只显示前5轮和最后2轮
acc = np.mean(predictions == y)
print(f"{i+1:>5} {alpha:>8.4f} {acc:>12.4f}")
elif i == 5:
print(" ...")
visualize_boosting_process()3.2 GBDT vs 单棵决策树
import numpy as np
def compare_gbdt_vs_tree():
"""对比GBDT和单棵决策树"""
np.random.seed(42)
X, y = make_classification(n_samples=1000, n_features=20,
n_informative=10, random_state=42)
split = 800
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# GBDT
gbdt = GBDTClassifier(n_estimators=100, learning_rate=0.1)
gbdt.fit(X_train, y_train)
gbdt_train = np.mean(gbdt.predict(X_train) == y_train)
gbdt_test = np.mean(gbdt.predict(X_test) == y_test)
# 单棵决策树(用CART)
tree = CARTClassifier(max_depth=5)
tree.fit(X_train, y_train)
tree_train = np.mean(tree.predict(X_train) == y_train)
tree_test = np.mean(tree.predict(X_test) == y_test)
print(f"{'模型':>15} {'训练准确率':>12} {'测试准确率':>12} {'Gap':>8}")
print("-" * 50)
print(f"{'单棵决策树':>15} {tree_train:>12.4f} {tree_test:>12.4f} {tree_train-tree_test:>8.4f}")
print(f"{'GBDT(100棵树)':>15} {gbdt_train:>12.4f} {gbdt_test:>12.4f} {gbdt_train-gbdt_test:>8.4f}")
print(f"\nGBDT的测试准确率更高,且过拟合更少(Gap更小)")
compare_gbdt_vs_tree()四、本章小结
第8章介绍了提升方法:
提升思想:将多个弱学习器组合成强学习器。弱学习器只需比随机稍好。
AdaBoost:通过调整样本权重关注错分样本,通过指数损失优化。
提升树:以决策树为基学习器,直接拟合残差。
GBDT:用梯度下降优化任意可微损失函数,是提升方法的通用框架。
正则化:学习率控制每棵树的贡献,子采样增加随机性。
提升方法的核心启示是:量变引起质变。多个"弱"模型的巧妙组合可以产生"强"模型。
五、思考与练习
思考题
AdaBoost为什么对噪声和异常值敏感?这与指数损失有什么关系?
学习率在GBDT中起什么作用?为什么小学习率+多棵树通常比大学习率+少棵树好?
GBDT和随机森林都是决策树集成,它们有什么本质区别?
为什么GBDT不需要特征标准化?
XGBoost相比GBDT做了哪些优化?
编程练习
从零实现AdaBoost算法,观察提升过程中准确率的变化。
实现GBDT回归,在不同数据上测试效果。
对比不同学习率下GBDT的训练和测试准确率。