第8章:集成学习(Ensemble Learning)
一、导读
1.1 本章学习目标
第8章介绍集成学习——通过组合多个学习器来获得更好性能的方法。"三个臭皮匠,顶个诸葛亮",集成学习是机器学习中最实用的技术之一,在各类竞赛和实际应用中屡获佳绩。
本章的核心学习目标包括:
- 理解集成学习的基本思想:为什么组合多个弱学习器能得到强学习器?
- 掌握 Bagging 方法:并行训练、投票决策的代表方法
- 掌握 Boosting 方法:串行训练、关注错分样本的 AdaBoost
- 理解随机森林:Bagging + 随机特征选择的强大方法
- 理解集成学习的理论:为什么集成有效?多样性为何重要?
- 了解主流集成方法:XGBoost、LightGBM 等
1.2 为什么本章如此重要
集成学习是机器学习实践中最重要的技术之一。在 Kaggle 等数据科学竞赛中,获胜方案几乎都使用了集成学习。XGBoost、LightGBM 等梯度提升方法在结构化数据上长期占据主导地位。
集成学习的核心思想——"组合多个模型"——不仅是一种技术手段,更是一种思维方式。它告诉我们:多样性是智慧的来源,多个不完美的决策者组合起来可能比单个完美的决策者更好。
1.3 核心问题引导
为什么组合多个"弱"学习器能得到一个"强"学习器?
Bagging 和 Boosting 的本质区别是什么?
随机森林为什么比普通 Bagging 更好?
AdaBoost 如何调整样本权重?为什么这样做有效?
集成学习中"多样性"为什么重要?如何保证多样性?
集成学习能无限提升性能吗?理论极限是什么?
二、核心概念详解
2.1 集成学习的基本思想
2.1.1 为什么集成有效
集成学习的核心思想:组合多个学习器,通过投票或平均得到最终预测。
理论分析表明:如果每个基学习器的错误率小于 0.5,且它们之间相互独立,那么通过多数投票,集成学习器的错误率可以任意低。
import numpy as np
def ensemble_theory_demo():
"""集成学习理论演示"""
print("=== 集成学习理论 ===")
# 假设每个基学习器错误率 0.3,相互独立
n_estimators = [1, 3, 5, 11, 21, 51, 101]
base_error = 0.3
print(f"基学习器错误率: {base_error}")
print(f"\n{'集成数量':<10} {'集成错误率':<12} {'提升倍数':<10}")
print("-" * 35)
from math import comb
for T in n_estimators:
# 多数投票错误率:超过一半的学习器犯错
ensemble_error = sum(comb(T, k) * base_error**k * (1-base_error)**(T-k)
for k in range(T//2 + 1, T + 1))
improvement = base_error / ensemble_error if ensemble_error > 0 else float('inf')
print(f"{T:<10} {ensemble_error:<12.6f} {improvement:<10.1f}x")
print("\n关键条件:基学习器相互独立")
print("实际中,基学习器不可能完全独立")
print("因此需要追求'多样性'——基学习器好而不同")
ensemble_theory_demo()2.1.2 多样性的重要性
集成学习的效果取决于两个因素:
基学习器的准确性:每个学习器至少要"比随机好"
基学习器的多样性:学习器之间的差异性越大,集成效果越好
def diversity_demo():
"""多样性演示"""
print("=== 多样性分析 ===")
# 场景1:两个完全相同的学习器
print("\n场景1:两个相同的学习器")
print(" 学习器A准确率: 80%")
print(" 学习器B准确率: 80%")
print(" A和B完全一致")
print(" 集成准确率: 80%(没有提升!)")
# 场景2:两个互补的学习器
print("\n场景2:两个互补的学习器")
print(" 学习器A准确率: 80%")
print(" 学习器B准确率: 80%")
print(" A和B错误不重叠")
print(" 集成准确率: ~96%(显著提升!)")
print("\n结论:")
print("多样性是集成学习的关键")
print("好而不同的学习器组合 > 多个相同的好学习器")
diversity_demo()2.2 Bagging
2.2.1 基本思想
Bagging(Bootstrap Aggregating)的核心思想:
从训练集中有放回地采样,生成多个子数据集
在每个子数据集上训练一个基学习器
分类任务用投票,回归任务用平均
class BaggingClassifier:
"""Bagging分类器"""
def __init__(self, base_estimator, n_estimators=10, random_state=42):
self.base_estimator = base_estimator
self.n_estimators = n_estimators
self.random_state = random_state
self.estimators_ = []
def fit(self, X, y):
np.random.seed(self.random_state)
n_samples = X.shape[0]
for i in range(self.n_estimators):
# 有放回采样
indices = np.random.choice(n_samples, n_samples, replace=True)
X_boot = X[indices]
y_boot = y[indices]
# 训练基学习器
estimator = self.base_estimator()
estimator.fit(X_boot, y_boot)
self.estimators_.append(estimator)
return self
def predict(self, X):
# 多数投票
predictions = np.array([est.predict(X) for est in self.estimators_])
from collections import Counter
return np.array([Counter(predictions[:, i]).most_common(1)[0][0]
for i in range(X.shape[0])])
def score(self, X, y):
return np.mean(self.predict(X) == y)
# 示例
np.random.seed(42)
X = np.random.randn(200, 5)
w = np.random.randn(5)
y = (X @ w > 0).astype(int)
# 划分
indices = np.random.permutation(200)
X_train, X_test = X[indices[:160]], X[indices[160:]]
y_train, y_test = y[indices[:160]], y[indices[160:]]
# 单棵决策树
from collections import Counter
class SimpleDecisionTree:
def __init__(self, max_depth=3):
self.max_depth = max_depth
def fit(self, X, y):
self.X_train = X
self.y_train = y
def predict(self, X):
preds = []
for x in X:
dists = np.sqrt(((self.X_train - x) ** 2).sum(axis=1))
nearest = np.argsort(dists)[:5]
pred = Counter(self.y_train[nearest]).most_common(1)[0][0]
preds.append(pred)
return np.array(preds)
single_tree = SimpleDecisionTree(max_depth=3)
single_tree.fit(X_train, y_train)
single_acc = np.mean(single_tree.predict(X_test) == y_test)
# Bagging
bagging = BaggingClassifier(SimpleDecisionTree, n_estimators=20)
bagging.fit(X_train, y_train)
bagging_acc = bagging.score(X_test, y_test)
print("=== Bagging 效果 ===")
print(f"单棵决策树准确率: {single_acc:.2%}")
print(f"Bagging(20棵树)准确率: {bagging_acc:.2%}")
print(f"提升: {bagging_acc - single_acc:.2%}")2.3 随机森林
2.3.1 基本思想
随机森林 = Bagging + 随机特征选择。在每个节点的分裂时,不是从所有特征中选择最优特征,而是从随机选择的特征子集中选择。
class RandomForest:
"""随机森林"""
def __init__(self, n_estimators=10, max_features='sqrt', random_state=42):
self.n_estimators = n_estimators
self.max_features = max_features
self.random_state = random_state
self.estimators_ = []
self.feature_indices_ = []
def fit(self, X, y):
np.random.seed(self.random_state)
n_samples, n_features = X.shape
# 确定每次使用的特征数
if self.max_features == 'sqrt':
n_feat = int(np.sqrt(n_features))
elif self.max_features == 'log2':
n_feat = int(np.log2(n_features))
else:
n_feat = n_features
for i in range(self.n_estimators):
# 有放回采样
indices = np.random.choice(n_samples, n_samples, replace=True)
X_boot = X[indices]
y_boot = y[indices]
# 随机选择特征
feat_idx = np.random.choice(n_features, n_feat, replace=False)
X_feat = X_boot[:, feat_idx]
# 训练基学习器
estimator = SimpleDecisionTree()
estimator.fit(X_feat, y_boot)
self.estimators_.append(estimator)
self.feature_indices_.append(feat_idx)
return self
def predict(self, X):
predictions = []
for est, feat_idx in zip(self.estimators_, self.feature_indices_):
X_feat = X[:, feat_idx]
pred = est.predict(X_feat)
predictions.append(pred)
predictions = np.array(predictions)
return np.array([Counter(predictions[:, i]).most_common(1)[0][0]
for i in range(X.shape[0])])
# 示例
rf = RandomForest(n_estimators=30, max_features='sqrt')
rf.fit(X_train, y_train)
rf_acc = np.mean(rf.predict(X_test) == y_test)
print(f"\n=== 随机森林 ===")
print(f"随机森林(30棵树)准确率: {rf_acc:.2%}")2.3.2 特征重要性
def feature_importance_demo():
"""特征重要性演示"""
print("=== 随机森林特征重要性 ===")
print("\n随机森林可以评估每个特征的重要性:")
print("方法1:基于不纯度减少(Gini重要性)")
print(" 统计每个特征在所有树中带来的不纯度减少量")
print("方法2:基于排列重要性")
print(" 打乱某个特征的值,观察性能下降程度")
print()
print("特征重要性的应用:")
print(" - 特征选择:选择重要的特征")
print(" - 理解数据:了解哪些特征对预测最重要")
print(" - 降维:去除不重要的特征")
feature_importance_demo()2.4 Boosting
2.4.1 AdaBoost
AdaBoost(Adaptive Boosting)的核心思想:
训练第一个基学习器(在均匀分布的样本上)
根据学习器的表现调整样本权重:错分的样本权重增加
训练下一个基学习器(在调整后的分布上)
重复,直到达到指定的学习器数量
最终预测:加权投票
class AdaBoost:
"""AdaBoost分类器"""
def __init__(self, n_estimators=10):
self.n_estimators = n_estimators
self.estimators_ = []
self.alphas_ = []
def fit(self, X, y):
n_samples = X.shape[0]
sample_weight = np.ones(n_samples) / n_samples
for _ in range(self.n_estimators):
# 训练基学习器(加权采样)
indices = np.random.choice(n_samples, n_samples,
replace=True, p=sample_weight)
X_boot = X[indices]
y_boot = y[indices]
estimator = SimpleDecisionTree()
estimator.fit(X_boot, y_boot)
# 计算误差
pred = estimator.predict(X)
error = np.sum(sample_weight * (pred != y))
if error >= 0.5:
break
# 计算学习器权重
alpha = 0.5 * np.log((1 - error) / (error + 1e-10))
# 更新样本权重
sample_weight *= np.exp(-alpha * y * (2 * pred - 1))
sample_weight /= sample_weight.sum()
self.estimators_.append(estimator)
self.alphas_.append(alpha)
return self
def predict(self, X):
predictions = np.zeros(X.shape[0])
for est, alpha in zip(self.estimators_, self.alphas_):
pred = est.predict(X)
predictions += alpha * (2 * pred - 1)
return (predictions > 0).astype(int)
# 示例
ada = AdaBoost(n_estimators=20)
ada.fit(X_train, y_train)
ada_acc = np.mean(ada.predict(X_test) == y_test)
print("=== AdaBoost ===")
print(f"AdaBoost(20个学习器)准确率: {ada_acc:.2%}")2.4.2 梯度提升
梯度提升(Gradient Boosting)是另一种 Boosting 方法。它不是调整样本权重,而是拟合残差。
def gradient_boosting_concept():
"""梯度提升概念"""
print("=== 梯度提升 ===")
print("\n基本思想:")
print("1. 初始化模型为常数(如均值)")
print("2. 计算残差(负梯度)")
print("3. 训练一个基学习器拟合残差")
print("4. 更新模型:F = F + learning_rate * h")
print("5. 重复步骤2-4")
print()
print("与AdaBoost的区别:")
print(" AdaBoost:调整样本权重")
print(" 梯度提升:拟合残差(负梯度)")
print()
print("优势:")
print(" - 可以优化任意可微损失函数")
print(" - 通常比AdaBoost表现更好")
print(" - 是XGBoost、LightGBM的基础")
gradient_boosting_concept()2.5 集成学习的理论
def ensemble_theory():
"""集成学习理论分析"""
print("=== 集成学习理论 ===")
print("\n1. 偏差-方差分解视角:")
print(" 集成主要降低方差(Bagging)")
print(" 集成主要降低偏差(Boosting)")
print()
print("2. 多样性度量:")
print(" - 不合度量:两个学习器同时犯错的频率")
print(" - 相关系数:两个学习器预测的相关性")
print(" - Q统计量:衡量两个学习器的一致性")
print()
print("3. 多样性-准确性困境:")
print(" 多样性高 → 基学习器可能不够准确")
print(" 准确性高 → 基学习器可能过于相似")
print(" 需要平衡二者")
print()
print("4. 集成学习的极限:")
print(" 即使基学习器错误率接近0.5")
print(" 只要满足独立性假设")
print(" 集成错误率可以趋近于0")
ensemble_theory()三、实战案例
3.1 案例一:Bagging vs Boosting
def bagging_vs_boosting():
"""Bagging与Boosting对比"""
np.random.seed(42)
# 生成数据
n = 500
X = np.random.randn(n, 10)
w = np.random.randn(10)
y = (X @ w > 0).astype(int)
# 划分
indices = np.random.permutation(n)
X_train, X_test = X[indices[:400]], X[indices[400:]]
y_train, y_test = y[indices[:400]], y[indices[400:]]
methods = {
'单棵决策树': SimpleDecisionTree(),
'Bagging(10)': BaggingClassifier(SimpleDecisionTree, n_estimators=10),
'Bagging(30)': BaggingClassifier(SimpleDecisionTree, n_estimators=30),
'AdaBoost(10)': AdaBoost(n_estimators=10),
'AdaBoost(30)': AdaBoost(n_estimators=30),
}
print("=== Bagging vs Boosting ===")
print(f"{'方法':<20} {'训练准确率':<12} {'测试准确率':<12}")
print("-" * 48)
for name, model in methods.items():
model.fit(X_train, y_train)
train_acc = np.mean(model.predict(X_train) == y_train)
test_acc = np.mean(model.predict(X_test) == y_test)
print(f"{name:<20} {train_acc:<12.2%} {test_acc:<12.2%}")
print("\n观察:")
print("- Bagging主要降低方差,对过拟合有帮助")
print("- Boosting主要降低偏差,可以提升准确性")
print("- 两者都能显著提升单棵树的性能")
bagging_vs_boosting()3.2 案例二:集成数量 vs 性能
def n_estimators_analysis():
"""集成数量分析"""
np.random.seed(42)
# 生成数据
n = 300
X = np.random.randn(n, 5)
w = np.random.randn(5)
y = (X @ w > 0).astype(int)
indices = np.random.permutation(n)
X_train, X_test = X[indices[:240]], X[indices[240:]]
y_train, y_test = y[indices[:240]], y[indices[240:]]
print("=== 集成数量 vs 性能 ===")
print(f"{'数量':<8} {'Bagging':<12} {'AdaBoost':<12}")
print("-" * 35)
for n_est in [1, 5, 10, 20, 50, 100]:
bag = BaggingClassifier(SimpleDecisionTree, n_estimators=n_est)
bag.fit(X_train, y_train)
bag_acc = bag.score(X_test, y_test)
ada = AdaBoost(n_estimators=n_est)
ada.fit(X_train, y_train)
ada_acc = np.mean(ada.predict(X_test) == y_test)
print(f"{n_est:<8} {bag_acc:<12.2%} {ada_acc:<12.2%}")
print("\n观察:")
print("- 增加集成数量通常能提升性能")
print("- 但提升会逐渐饱和")
print("- Bagging不太容易过拟合")
print("- Boosting过多可能导致过拟合")
n_estimators_analysis()3.3 案例三:多样性分析
def diversity_analysis():
"""多样性分析"""
np.random.seed(42)
print("=== 多样性分析 ===")
# 生成两个学习器的预测
n = 100
y_true = np.random.randint(0, 2, n)
# 场景1:高多样性
pred1_high = (y_true + np.random.rand(n) > 0.7).astype(int)
pred2_high = (y_true + np.random.rand(n) > 0.8).astype(int)
# 场景2:低多样性
pred1_low = (y_true + np.random.rand(n) > 0.7).astype(int)
pred2_low = pred1_low.copy() # 几乎相同
def disagreement(pred1, pred2):
return np.mean(pred1 != pred2)
def ensemble_acc(pred1, pred2, y_true):
ensemble = ((pred1 + pred2) >= 1).astype(int)
return np.mean(ensemble == y_true)
print(f"\n高多样性场景:")
print(f" 学习器1准确率: {np.mean(pred1_high == y_true):.2%}")
print(f" 学习器2准确率: {np.mean(pred2_high == y_true):.2%}")
print(f" 不合度: {disagreement(pred1_high, pred2_high):.2%}")
print(f" 集成准确率: {ensemble_acc(pred1_high, pred2_high, y_true):.2%}")
print(f"\n低多样性场景:")
print(f" 学习器1准确率: {np.mean(pred1_low == y_true):.2%}")
print(f" 学习器2准确率: {np.mean(pred2_low == y_true):.2%}")
print(f" 不合度: {disagreement(pred1_low, pred2_low):.2%}")
print(f" 集成准确率: {ensemble_acc(pred1_low, pred2_low, y_true):.2%}")
print("\n结论:高多样性带来更大的集成提升")
diversity_analysis()四、本章小结
第8章系统介绍了集成学习的核心内容:
基本思想:组合多个学习器,通过投票或平均得到最终预测。关键在于"好而不同"。
Bagging:并行训练,有放回采样。代表方法:随机森林。主要降低方差。
Boosting:串行训练,关注错分样本。代表方法:AdaBoost、梯度提升。主要降低偏差。
随机森林:Bagging + 随机特征选择。具有特征重要性评估能力。
多样性:集成学习的关键。基学习器之间的差异性越大,集成效果越好。
集成学习是实践中最强大的技术之一。随机森林和梯度提升方法(XGBoost、LightGBM)在结构化数据上表现卓越。
五、思考与练习
思考题
Bagging 和 Boosting 的根本区别是什么?它们分别降低偏差还是方差?
随机森林为什么比普通 Bagging 更好?随机特征选择如何增加多样性?
AdaBoost 中,为什么错分样本的权重会增加?这样做有什么理论依据?
集成学习能无限提升性能吗?理论极限是什么?
在实际应用中,如何选择使用 Bagging 还是 Boosting?
编程练习
实现随机森林,比较不同 max_features 参数对性能的影响。
实现梯度提升回归树(GBRT),在波士顿房价数据集上测试。
设计实验,分析基学习器的多样性与集成性能之间的关系。