8统计学习方法(第2版)

提升方法

集成学习

AdaBoost提升树梯度提升

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

第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))

python
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) 是最终分类器的输出。

指数损失的特点:

  • 对错分样本给予很大的惩罚
  • 对正确分类但置信度低的样本也给予一定惩罚
  • 对噪声和异常值敏感
python
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 棵决策树。

python
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的优势:

  • 可以使用任意可微损失函数
  • 天然支持回归和分类
  • 通过正则化(学习率、子采样)控制过拟合
python
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)

每棵树只在训练数据的一个子集上训练(不放回采样),类似于随机森林的思想。

python
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提升过程可视化

python
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 单棵决策树

python
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的训练和测试准确率。


8/12提升方法
0%