8机器学习

集成学习

三个臭皮匠

BaggingBoosting随机森林AdaBoost

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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,且它们之间相互独立,那么通过多数投票,集成学习器的错误率可以任意低。

python
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 多样性的重要性

集成学习的效果取决于两个因素:

基学习器的准确性:每个学习器至少要"比随机好"

基学习器的多样性:学习器之间的差异性越大,集成效果越好

python
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)的核心思想:

从训练集中有放回地采样,生成多个子数据集

在每个子数据集上训练一个基学习器

分类任务用投票,回归任务用平均

python
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 + 随机特征选择。在每个节点的分裂时,不是从所有特征中选择最优特征,而是从随机选择的特征子集中选择。

python
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 特征重要性

python
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)的核心思想:

训练第一个基学习器(在均匀分布的样本上)

根据学习器的表现调整样本权重:错分的样本权重增加

训练下一个基学习器(在调整后的分布上)

重复,直到达到指定的学习器数量

最终预测:加权投票

python
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 方法。它不是调整样本权重,而是拟合残差。

python
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 集成学习的理论

python
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

python
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 性能

python
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 案例三:多样性分析

python
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),在波士顿房价数据集上测试。

设计实验,分析基学习器的多样性与集成性能之间的关系。

8/16集成学习
0%