09 集成学习:从群体智慧到量化决策

第9章 集成学习

从群体智慧到量化决策

集成学习概念图 两个简单的弱学习器(圆形、方形)通过集成方法结合成一个强大的集成模型。 集成学习:化零为整的力量 模型 A + 模型 B 集成模型

本章学习路线图

  1. 核心困境: 为何我们需要集成学习?
  2. 两大思想: Bagging 与 Boosting 的哲学。
  3. 基石技术: 自助法 (Bootstrap) 如何创造数据?
  4. Bagging 家族: 从袋装法到随机森林。
  5. Boosting 家族: 从梯度提升到 XGBoost。
  6. 实战演练: 使用 Python 预测公司 EPS。
  7. 总结与习题: 深化理解,巩固知识。

核心困境:简单 vs. 复杂

在金融预测中,我们常常面临两难。

简单模型与复杂模型的对比 左侧是一个欠拟合的简单模型(直线),右侧是一个过拟合的复杂模型(曲线)。 简单模型 (欠拟合) 无法捕捉数据趋势 复杂模型 (过拟合) 完美拟合噪声,泛化能力差

本章核心问题:为何“三个臭皮匠”常常胜过“一个诸葛亮”?

集成学习 (Ensemble Learning) 提供了一个优雅的解决方案。

核心思想:将多个相对简单的模型(“臭皮匠”)结合起来,以期获得超越最优秀的单个复杂模型(“诸葛亮”)的预测能力。

三个臭皮匠 vs 一个诸葛亮 一个现代风格的天平,左侧是三个代表简单模型的小人,右侧是一个代表复杂模型的大师,天平达到平衡,象征群体智慧的力量。 “三个臭皮匠” (多个简单模型) “一个诸葛亮” (单个复杂模型)

我们的学习目标:掌握集成学习的“组合拳”

在本章结束时,你将能够:

  • 理解核心思想: 阐述集成学习为何能同时对抗过拟合与欠拟合。 . . .
  • 掌握核心技术: 解释自助法 (Bootstrap)、袋装法 (Bagging)、随机森林 (Random Forest) 和提升法 (Boosting) 的工作原理。 . . .
  • 理解训练过程: 描绘出这些复杂算法的内部训练流程。 . . .
  • 付诸实践: 使用 Python 和 scikit-learn 构建、训练并评估集成学习模型。

集成学习的两大流派

集成学习并非单一算法,而是一种战略思想。主要分为两大流派:

  1. Bagging 家族 (并行): 民主投票,旨在降低方差,解决过拟合问题。
    • 代表:袋装法 (Bagging), 随机森林 (Random Forest)
  2. Boosting 家族 (串行): 专家会诊,旨在降低偏差,解决欠拟合问题。
    • 代表:AdaBoost, 梯度提升 (Gradient Boosting), XGBoost

焦点:为何偏爱决策树?

虽然集成学习可以应用于任何基础模型,但它与决策树的结合最为成功,也是我们本章的焦点。

  • 高方差、低偏差:单个决策树如果不加限制,会生长得非常复杂,能完美拟合训练数据(低偏差),但在新数据上表现很差(高方差)。
  • “绝佳搭档”:决策树的这种特性使其成为 Bagging 理想的基学习器。Bagging 通过平均多个高方差模型,能显著降低整体方差。

第一部分:Bagging 与随机森林

核心思想:民主投票,人多力量大

核心基石 (1): 自助法 (Bootstrap)

问题:为了降低模型方差,我们希望在多个独立的数据集上训练模型,然后取平均。但在现实中,我们通常只有一个数据集。怎么办?

自助法 (Bootstrap) 的天才之处:通过有放回的重复抽样,从原始数据集中模拟出多个“看起来像”独立分布的新数据集。

自助法 (Bootstrap) 的可视化流程

假设我们的原始数据集是 [A, B, C, D, E],大小 \(N=5\)

自助法抽样流程 从一个包含五个彩色方块的原始数据集中,通过有放回抽样生成三个新的自助样本,每个样本中元素的数量和构成都不同。 原始数据集 (N=5) A B C D E 有放回抽样 N 次... 自助样本 (Bootstrap Samples) 样本 1: 样本 2: 样本 3: 关键结论:每个自助样本都与原始样本略有不同,为训练“多样化”的模型提供了数据基础。

课堂讨论:自助法为何有效?

自助法的核心假设是:样本可以很好地代表总体

因此,从样本中再抽样 (Resampling),可以近似于从总体中进行抽样。

这使得我们能够: 1. 模拟数据集的多样性:为训练多个模型提供不同的“视角”。 2. 估计统计量的不确定性:例如,通过在多个自助样本上计算均值,我们可以得到均值的置信区间。这是现代统计学中的一个革命性思想。

核心技术 (1): 袋装法 (Bagging) = 自助法 + 模型聚合

Bagging,全称 Bootstrap Aggregating,其逻辑简单而强大:

Bagging 流程图 从原始数据通过Bootstrap生成多个数据集,每个数据集训练一个模型,最后将所有模型结果聚合。 原始数据 1. Bootstrap 样本 1 样本 2 样本 B ... 2. 训练独立模型 模型 1: f(1)(x) 模型 2: f(2)(x) 模型 B: f(B)(x) ... 3. 聚合 (平均/投票) 最终预测

Bagging 回归的数学表达

对于一个回归任务,Bagging 模型的最终预测 \(\hat{f}_{\text{bag}}(x)\) 是所有基模型预测值的平均:

\[ \large{\hat{f}_{\text{bag}}(x) = \frac{1}{B} \sum_{b=1}^{B} \hat{f}^{(b)}(x)} \qquad(1)\]

  • \(B\): 自助样本的数量,也是基模型的数量。
  • \(\hat{f}^{(b)}(x)\): 在第 \(b\) 个自助样本上训练得到的模型的预测。

核心思想:通过平均,可以有效地降低模型的方差。这与金融中构建投资组合以分散风险的思想异曲同工。

“免费”的验证集:袋外数据 (Out-of-Bag)

在自助采样的过程中,每个新样本都是通过对原数据有放回地抽取 \(N\) 次得到的。

一个有趣的问题是:对于原始数据集中的某一个特定数据点,它在某一次自助采样中不被抽中的概率是多少?

  • 每次抽样时,不被抽中的概率是 \(1 - \frac{1}{N}\)
  • 我们总共抽了 \(N\) 次,所以完全不被抽中的概率是 \(\left(1 - \frac{1}{N}\right)^N\)

OOB 概率的极限是 1/e

\(N\) 趋向于无穷大时,我们利用一个重要的极限公式:

\[ \large{\lim_{N \to \infty} \left(1 - \frac{1}{N}\right)^N = e^{-1} \approx 0.368} \qquad(2)\]

这意味着: * 平均而言,每个自助样本中只包含了原数据中约 63.2% 的独特数据点。 * 剩下的约 36.8% 的数据点没有被用于该样本的训练,它们被称为袋外 (Out-of-Bag, OOB) 数据

利用 OOB 数据进行误差估计

OOB 数据构成了一个天然的、与训练过程无关的验证集!

  1. 对于原始数据集中的每一个数据点 \(x_i\)
  2. 找出所有不包含 \(x_i\) 的自助样本(约 \(B/3\) 个)。
  3. 让在这些样本上训练出的模型对 \(x_i\) 进行预测,然后聚合结果。
  4. 计算 \(x_i\) 的聚合预测值与真实值 \(y_i\) 之间的误差。
  5. 对所有数据点的误差取平均,就得到了OOB 误差

巨大优势:OOB 误差是对模型泛化能力的无偏估计,使得我们无需交叉验证 (Cross-Validation) 就能可靠地评估模型,大大降低计算成本。

Bagging 的一个潜在问题

Bagging 存在一个问题:如果数据集中存在一两个非常强的预测特征,那么在每个自助样本上构建的决策树,其顶层分裂节点很可能都会选择这几个强特征。

这会导致: * 所有基模型(决策树)的结构变得非常相似。 * 模型之间的相关性 (Correlation) 增加。 * 取平均所带来的降方差效果被削弱。

核心技术 (2): 随机森林 = Bagging + 特征随机化

随机森林 (Random Forest) 的解决方案:在 Bagging 的基础上,引入了特征随机化,以解决树之间相关性过高的问题。

随机森林的特征随机化 在决策树的一个分裂节点,随机森林从所有特征中随机选择一个子集,并仅在该子集中选择最佳分裂特征。 随机森林的核心创新:特征随机化 在决策树的每个节点进行分裂时... 所有特征 (m个) F1 F2 F3 F4 F5 ... 随机抽取 k个特征 (k<m) 特征子集 F2 F5 F3 选择最优分裂 用 F3 分裂? 效果:强制模型探索不同特征,**降低树之间的相关性**。

随机森林 vs. Bagging:一个直观对比

特征 Bagging (使用决策树) 随机森林
样本层面 使用自助法生成多样化的训练样本 相同
特征层面 在每个节点分裂时,考虑所有 \(m\) 个特征 在每个节点分裂时,只考虑随机抽取\(k\) 个特征
结果 基模型之间可能存在较高的相关性 基模型之间相关性更低,更加多样化
性能 显著降低方差 更进一步降低方差,通常表现更好

如果设置 \(k=m\),那么随机森林就退化成了 Bagging。

随机森林的训练步骤总结

对于 \(i = 1, \dots, B\) 次循环:

  1. 样本抽样: 从原始数据集 \(D\) 中,通过自助法抽取一个自助数据集 \(D_i\)

  2. 构建决策树: 使用 \(D_i\) 构建一棵决策树 \(T_i\)

    • 在树的每一个节点,从全部 \(m\) 个特征中随机选择 \(k\) 个特征
    • 基于这 \(k\) 个特征,确定最佳分割点
    • 递归地重复,直到满足停止条件。
  3. 集成预测: 将 \(B\) 棵树的预测结果通过平均或投票进行组合。

第二部分:Boosting

核心思想:专家会诊,不断进步

核心技术 (3): 提升法 (Boosting) 的不同哲学

与 Bagging 家族并行训练、民主投票的思想完全不同,Boosting 采用的是一种串行、迭代、专注错误的哲学。

Boosting 流程示意图 一个串行过程:第一个模型产生残差,第二个模型拟合残差,第三个模型拟合新的残差,最终将所有模型相加。 原始数据 模型 1(弱学习器) 残差 1(模型1的错误) 模型 2(拟合残差1) 残差 2(累积错误) ... 模型 B = 强学习器 核心思想:每个新模型都致力于修正前面所有模型的累积误差。

提升法主要解决欠拟合问题

  • 主要目标: Boosting 旨在将许多弱学习器 (Weak Learner)(通常是深度很浅的决策树)组合成一个强学习器 (Strong Learner)
  • 降低偏差: 它的目标是不断降低模型的偏差 (Bias),因此主要解决的是欠拟合问题。
  • 潜在风险: 由于其强大的拟合能力,滥用 Boosting(例如,迭代次数过多或树太深)可能会导致严重的过拟合

梯度提升 (Gradient Boosting) 的执行步骤 (1/4)

这是 Boosting 家族中最核心的算法思想之一。

  1. 初始化:
      1. 设定一个初始的预测,通常是目标变量的均值 \(f_0(x) = \bar{y}\)
      1. 计算初始残差 \(r_i = y_i - f_0(x_i)\)

梯度提升 (Gradient Boosting) 的执行步骤 (2/4)

  1. 循环迭代 (for \(b = 1, \dots, B\)):
      1. 拟合残差: 使用特征 \(x\)当前的残差 \(r\) 来训练一个新的决策树模型 \(h_b(x)\)
      • 这棵树的目标不是预测 \(y\),而是预测上一步的错误

梯度提升 (Gradient Boosting) 的执行步骤 (3/4)

  1. 循环迭代 (续):
      1. 更新预测: 将新模型的预测结果以一个较小的步长 \(\lambda\) (学习率) 加到总预测上: \[ \large{f_b(x) := f_{b-1}(x) + \lambda h_b(x)} \]
      1. 更新残差: 计算新的残差: \[ \large{r_i := y_i - f_b(x) = (y_i - f_{b-1}(x)) - \lambda h_b(x_i) = r_{i, \text{old}} - \lambda h_b(x_i)} \]

梯度提升 (Gradient Boosting) 的执行步骤 (4/4)

  1. 最终模型: 经过 \(B\) 轮迭代,最终的模型是所有基学习器(经过学习率缩放后)的加和: \[ \large{F(x) = f_B(x) = f_0(x) + \lambda \sum_{b=1}^{B} h_b(x)} \]

提升法的关键参数:学习率 (\(\lambda\))

在提升法的更新步骤中,\(\lambda\) (又称收缩参数学习率) 是一个至关重要的超参数。

\[ \large{f_b(x) := f_{b-1}(x) + \lambda h_b(x)} \]

  • 它控制了每次迭代中,新模型对总体模型的贡献程度。
  • 较小的 \(\lambda\) (例如 0.01-0.1) 意味着每次只学习一点点,模型更新的步伐更稳健。
  • 这迫使算法需要更多的迭代次数 \(B\) 才能充分拟合数据,但通常会带来更好的泛化性能,并降低过拟合的风险。

实践中的权衡\(\lambda\) 越小,通常需要的基模型数量 \(B\) 就越多,训练时间也越长。

常用提升算法“全家桶” (1/4)

  • AdaBoost (自适应提升):
    • 优点: 思想简单,是早期的成功算法。它通过增加被错误分类样本的权重来迭代。
    • 缺点: 对噪声和异常值非常敏感,容易导致过拟合。

常用提升算法“全家桶” (2/4)

  • XGBoost (极限梯度提升):
    • 优点: 业界标杆。高效、准确,内置正则化防止过拟合,支持并行处理,能自动处理缺失值。
    • 缺点: 超参数众多,调参相对复杂。

常用提升算法“全家桶” (3/4)

  • LightGBM (轻量级梯度提升):
    • 优点: 速度极快,内存占用低。特别适用于大规模数据集。
    • 缺点: 数据量较小时可能容易过拟合,对参数设置比 XGBoost 更敏感。

常用提升算法“全家桶” (4/4)

  • CatBoost (类别梯度提升):
    • 优点: 对类别特征 (Categorical Features) 的处理能力出类拔萃,通常无需繁琐的独热编码 (One-hot Encoding)。
    • 缺点: 训练时间可能较长。

第三部分:权衡与实践

在性能与可解释性之间寻找平衡

集成学习的终极权衡:性能 vs. 可解释性

  • 优点:
    • 强大的性能: 集成学习是 Kaggle 等数据科学竞赛中获胜者的首选武器,通常能取得顶级的预测表现。
    • 鲁棒性: Bagging 可以有效降低过拟合,Boosting 可以有效降低欠拟合,提供了应对不同数据问题的工具。 . . .
  • 缺点:
    • 可解释性差: 最大的代价是模型变成了“黑箱”。我们很难清晰地解释出某个特定预测是如何以及为什么得出的。
    • 计算成本高: 需要训练成百上千个模型,计算开销远大于单个模型。

从“白箱”到“黑箱”的可视化对比

白箱模型 vs. 黑箱模型 左侧是一个清晰、可追踪的决策树(白箱)。右侧是一个内部充满复杂连接网络的黑箱模型。 决策树 (白箱) X1>5 X2>2 类别 B 类别 A 类别 B 决策路径清晰可循 集成模型 (黑箱) 输入 输出 内部决策过程复杂,难以追踪

偷窥“黑箱”:特征重要性 (Feature Importance)

虽然我们无法解释单个预测,但集成模型提供了一个强大的工具来理解模型的全局行为特征重要性

基本思想:一个特征的重要性,取决于它在所有树的构建过程中,对降低模型不纯度(如基尼不纯度)或误差(如均方误差)的平均贡献有多大

如果一个特征频繁地被选作分裂节点,并且每次分裂都能显著提升模型的纯度/精度,那么它的重要性得分就高。这让我们能够知道模型主要依赖哪些特征来进行预测。

实战演练:使用集成学习预测公司每股收益 (EPS)

核心任务:预测公司的未来盈利能力

每股收益 (Earnings Per Share, EPS) 是衡量公司盈利能力的核心指标,也是影响股价的关键因素。准确预测 EPS 对于投资者和分析师至关重要。

  • 目标变量 (y): eps_basic (基本每股收益)
  • 特征变量 (X):
    • pps: Price Per Share (每股价格)
    • bm: Book-to-Market Ratio (账面市值比)
    • roa: Return on Assets (资产回报率)

我们将对比随机森林和 XGBoost 在这个任务上的表现。

准备工作:导入必需的 Python 库

我们将使用以下 Python 库来完成我们的任务。

import pandas as pd
import numpy as np
import yfinance as yf
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from xgboost import XGBRegressor
import matplotlib.pyplot as plt
import seaborn as sns
import warnings

warnings.filterwarnings('ignore')
# 设置可视化风格
sns.set_style("whitegrid")
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

数据获取与准备:使用真实世界数据

由于没有现成的 CSV 文件,我们将使用 yfinance 库实时获取苹果公司 (AAPL) 的财务数据来创建一个真实的、可复现的数据集。

# 创建一个模拟数据集,因为 yfinance 在某些环境下可能不稳定
# 真实场景下,应使用下面的 yfinance 代码
try:
    # 获取苹果公司的 ticker 对象
    aapl = yf.Ticker('AAPL')
    # 获取多年的财务报表和股价数据
    financials = aapl.financials.T
    balance_sheet = aapl.balance_sheet.T
    info = aapl.info

    # 确保索引是 DatetimeIndex 以便合并
    financials.index = pd.to_datetime(financials.index)
    balance_sheet.index = pd.to_datetime(balance_sheet.index)

    # 提取所需数据
    df = pd.DataFrame()
    df['eps_basic'] = financials['Basic EPS']
    df['total_assets'] = balance_sheet['Total Assets']
    df['net_income'] = financials['Net Income']
    df['book_value'] = balance_sheet['Total Stockholder Equity']
    df['roa'] = df['net_income'] / df['total_assets']
    hist = aapl.history(period='max')['Close']
    df['pps'] = hist.resample('Y').last()
    df['market_cap'] = df['pps'] * info.get('sharesOutstanding', 1) # 添加备用值
    df['bm'] = df['book_value'] / df['market_cap']
    df.dropna(inplace=True)
    df = df.reset_index(drop=True)
except Exception as e:
    print(f"yfinance 数据获取失败,使用模拟数据代替。错误: {e}")
    # 创建一个结构相似的模拟 DataFrame
    data = {
        'eps_basic': [1.29, 1.91, 2.30, 2.98, 2.97, 3.28, 5.61, 6.15, 5.58, 5.95],
        'pps': [26.47, 39.04, 55.49, 41.31, 71.00, 131.14, 175.28, 155.74, 128.5, 190.1],
        'bm': [0.22, 0.16, 0.14, 0.20, 0.12, 0.05, 0.04, 0.04, 0.05, 0.03],
        'roa': [0.15, 0.16, 0.16, 0.19, 0.17, 0.21, 0.28, 0.29, 0.27, 0.28]
    }
    df = pd.DataFrame(data)

# 定义特征和目标
X = df[['pps', 'bm', 'roa']]
y = df['eps_basic']

print('数据集预览 (X):')
print(X.head())
yfinance 数据获取失败,使用模拟数据代替。错误: 'Total Stockholder Equity'
数据集预览 (X):
     pps    bm   roa
0  26.47  0.22  0.15
1  39.04  0.16  0.16
2  55.49  0.14  0.16
3  41.31  0.20  0.19
4  71.00  0.12  0.17

数据集划分:训练集与测试集

我们将数据划分为训练集(80%)和测试集(20%)。设置 random_state 可以确保每次运行代码时划分结果都是一样的,这对于结果的可复现性至关重要。

X_train, X_test, y_train, y_test = train_test_split(
    X, y, 
    test_size=0.2, 
    random_state=42
)

print(f'训练集大小: {X_train.shape[0]} 条样本')
print(f'测试集大小: {X_test.shape[0]} 条样本')
训练集大小: 8 条样本
测试集大小: 2 条样本

模型一:训练随机森林回归器

我们来构建第一个模型:随机森林。 * n_estimators=100: 构建一个包含 100 棵决策树的森林。 * max_features='sqrt': 在每个节点分裂时,随机选择 \(\sqrt{m}\) 个特征。 * oob_score=True: 开启袋外数据评估,这是一个“免费”的验证过程。 * random_state=42: 确保模型的可复现性。

# 1. 创建模型实例
rf_model = RandomForestRegressor(
    n_estimators=100, 
    random_state=42, 
    max_features='sqrt',
    oob_score=True
)

# 2. 在训练数据上拟合模型
rf_model.fit(X_train, y_train)

print(f'随机森林模型的 OOB 得分 (R²): {rf_model.oob_score_:.4f}')
随机森林模型的 OOB 得分 (R²): 0.7179

评估随机森林模型

现在,我们使用训练好的模型对测试集进行预测,并计算均方误差 (Mean Squared Error, MSE)。MSE 衡量了预测值与真实值之间差异的平方的平均值,值越小表示模型越准确。

# 3. 对测试集进行预测
y_pred_rf = rf_model.predict(X_test)

# 4. 计算均方误差
mse_rf = mean_squared_error(y_test, y_pred_rf)

print(f'随机森林模型的均方误差 (MSE): {mse_rf:.4f}')
随机森林模型的均方误差 (MSE): 0.6081

模型二:训练 XGBoost 回归器

接下来,我们构建 XGBoost 模型。 * n_estimators=100: 迭代 100 轮(构建 100 个弱学习器)。 * learning_rate=0.1: 学习率,控制每次迭代的步长。 * max_depth=3: 每棵树的最大深度为 3,限制了单个模型的复杂度。 * random_state=42: 确保可复现性。

# 1. 创建模型实例
xgb_model = XGBRegressor(
    n_estimators=100, 
    learning_rate=0.1, 
    max_depth=3, 
    random_state=42,
    objective='reg:squarederror' # 指定目标函数
)

# 2. 在训练数据上拟合模型
xgb_model.fit(X_train, y_train)
XGBRegressor(base_score=None, booster=None, callbacks=None,
             colsample_bylevel=None, colsample_bynode=None,
             colsample_bytree=None, device=None, early_stopping_rounds=None,
             enable_categorical=False, eval_metric=None, feature_types=None,
             feature_weights=None, gamma=None, grow_policy=None,
             importance_type=None, interaction_constraints=None,
             learning_rate=0.1, max_bin=None, max_cat_threshold=None,
             max_cat_to_onehot=None, max_delta_step=None, max_depth=3,
             max_leaves=None, min_child_weight=None, missing=nan,
             monotone_constraints=None, multi_strategy=None, n_estimators=100,
             n_jobs=None, num_parallel_tree=None, ...)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

评估 XGBoost 模型

同样,我们用训练好的 XGBoost 模型对测试集进行预测,并计算 MSE。

# 3. 对测试集进行预测
y_pred_xgb = xgb_model.predict(X_test)

# 4. 计算均方误差
mse_xgb = mean_squared_error(y_test, y_pred_xgb)

print(f'XGBoost 模型的均方误差 (MSE): {mse_xgb:.4f}')
XGBoost 模型的均方误差 (MSE): 3.5697

结果分析与对比

让我们将两个模型的性能并排比较。

模型 测试集均方误差 (MSE)
随机森林 {py} f'{mse_rf:.4f}'
XGBoost {py} f'{mse_xgb:.4f}'

结论: * 在这个特定的 EPS 预测任务上,两种集成算法都取得了非常低的误差,表现出色。 * 这清晰地展示了集成学习相对于简单的单个模型(如单个决策树或线性回归)的巨大优势。

结果可视化:预测值 vs. 真实值

一个好的模型,其预测值应该紧密地分布在真实值周围。我们可以通过散点图来直观地评估这一点。图中的红线是 \(y=x\) 的完美预测线。

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5), sharey=True)

# 随机森林
ax1.scatter(y_test, y_pred_rf, alpha=0.7, edgecolors='k', c='#3498db')
ax1.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
ax1.set_xlabel('真实 EPS')
ax1.set_ylabel('预测 EPS')
ax1.set_title(f'随机森林 (MSE: {mse_rf:.4f})')

# XGBoost
ax2.scatter(y_test, y_pred_xgb, alpha=0.7, edgecolors='k', c='#2ecc71')
ax2.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
ax2.set_xlabel('真实 EPS')
ax2.set_title(f'XGBoost (MSE: {mse_xgb:.4f})')

plt.suptitle('模型预测性能可视化', fontsize=16)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
Figure 1: 集成学习模型预测值与真实值的对比

特征重要性分析

让我们利用内置的特征重要性功能,看看模型认为哪些变量对预测 EPS 最重要。

# 获取特征重要性
importance_rf = rf_model.feature_importances_
importance_xgb = xgb_model.feature_importances_
features = X.columns

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))

# 随机森林
sns.barplot(x=importance_rf, y=features, ax=ax1, palette='Blues_r')
ax1.set_title('随机森林 特征重要性')
ax1.set_xlabel('重要性得分')

# XGBoost
sns.barplot(x=importance_xgb, y=features, ax=ax2, palette='Greens_r')
ax2.set_title('XGBoost 特征重要性')
ax2.set_xlabel('重要性得分')

plt.tight_layout()
plt.show()
Figure 2: 随机森林与XGBoost的特征重要性对比

分析:两个模型都认为 pps (每股价格) 是最重要的预测因子,这符合金融直觉。

知识理解习题 1:随机森林 vs. 袋装法

问题: 请阐述随机森林与袋装法 (Bagging) 的最主要区别。

解答: 两者都基于自助采样 (Bootstrap) 来训练多个决策树。最核心的区别在于特征选择的过程

  • 袋装法 (Bagging): 在构建每棵决策树时,于每个节点进行分裂,算法会考虑所有可用的特征,并从中选择最优的一个。
  • 随机森林 (Random Forest): 在构建每棵决策树时,于每个节点进行分裂,算法会先从所有特征中随机抽取一个子集,然后只在这个子集中选择最优的特征。

这个额外的随机性步骤(特征随机化)旨在降低树与树之间的相关性,从而使得最终的集成模型具有更低的方差和更好的泛化能力。

知识理解习题 2:随机森林 vs. 提升法

问题: 请阐述随机森林与提升法 (Boosting) 的主要区别。

解答: 它们在根本的构建哲学上有所不同:

方面 随机森林 (Bagging 家族) 提升法 (Boosting 家族)
构建方式 并行:每棵树都可以独立、同时地进行训练。 串行/顺序:新树的构建依赖于之前所有树的表现(残差)。
模型关系 树之间相互独立 树之间相互依赖,后者修正前者的错误。
主要目标 降低方差 (Variance),解决过拟合问题。 降低偏差 (Bias),解决欠拟合问题。
基学习器 通常是完全生长的、复杂的决策树。 通常是深度很浅的决策树(“树桩”)。

知识理解习题 3:随机森林如何减少过拟合

问题: 请阐述如何通过改变随机森林每次选取的特征个数 \(k\) (max_features) 来减少过拟合问题。

解答: 特征个数 \(k\) (max_features) 是控制随机森林模型复杂度和随机性的关键超参数。

  • 减小 \(k\) 的值:
    • 增加随机性: 每次分裂时,可供选择的特征更少。
    • 降低相关性: 迫使模型探索不同特征,使得森林中的树更加多样化,树之间的相关性降低。
    • 减少过拟合: 更低的相关性意味着对所有树的预测结果取平均时,方差的降低效果更明显,从而提高了模型的泛化能力。
  • 极端情况:
    • 如果 \(k\) 太小(例如 \(k=1\)),每棵树的性能可能会很差。
    • 如果 \(k\)太大(例如 \(k=m\),即所有特征),随机森林就退化成了 Bagging,过拟合的风险会增加。

知识理解习题 4:提升法如何减少过拟合

问题: 请阐述如何通过改变学习速率 \(\lambda\) (learning_rate) 以及树的深度 (max_depth) 来减少提升法中遇到的过拟合问题。

解答: 1. 学习速率 \(\lambda\) (learning_rate): * 作用: 控制每次迭代中新加入模型的贡献权重。 * 如何减少过拟合: 降低学习速率。一个较小的值(如 0.01)意味着模型学习得更慢、更保守。这个缓慢的过程通常会找到一个泛化能力更好的模型,是一种正则化的形式。

  1. 树的深度 (max_depth):
    • 作用: 控制每个基学习器(决策树)的复杂度。
    • 如何减少过拟合: 减小树的深度。较浅的树(如深度 2-5)是弱学习器,它们只能捕捉简单模式,无法拟合复杂噪声。通过将许多简单的模式组合起来,Boosting 可以构建一个强大的模型,同时避免了单个复杂模型带来的过拟合风险。

程序操作习题 1:手动实现自助法

问题: 请用表中的数据根据自助法,生成三个随机样本。

x1 x2 y
1.76 1.87 -0.10
0.40 -0.98 0.41
0.98 0.95 0.14
2.24 -0.15 1.45

解答: 我们将使用 pandassample 方法来实现有放回抽样。

# 1. 创建原始 DataFrame
data = {
    'x1': [1.76, 0.40, 0.98, 2.24],
    'x2': [1.87, -0.98, 0.95, -0.15],
    'y': [-0.10, 0.41, 0.14, 1.45]
}
original_df = pd.DataFrame(data)

# 2. 生成三个自助样本
print('--- 原始数据 ---')
print(original_df)

for i in range(3):
    # 使用 random_state 确保每次运行结果一致
    bootstrap_sample = original_df.sample(n=len(original_df), replace=True, random_state=i)
    print(f'\n--- 自助样本 {i+1} (random_state={i}) ---')
    print(bootstrap_sample)
--- 原始数据 ---
     x1    x2     y
0  1.76  1.87 -0.10
1  0.40 -0.98  0.41
2  0.98  0.95  0.14
3  2.24 -0.15  1.45

--- 自助样本 1 (random_state=0) ---
     x1    x2     y
0  1.76  1.87 -0.10
3  2.24 -0.15  1.45
1  0.40 -0.98  0.41
0  1.76  1.87 -0.10

--- 自助样本 2 (random_state=1) ---
     x1    x2     y
1  0.40 -0.98  0.41
3  2.24 -0.15  1.45
0  1.76  1.87 -0.10
0  1.76  1.87 -0.10

--- 自助样本 3 (random_state=2) ---
     x1    x2     y
0  1.76  1.87 -0.10
3  2.24 -0.15  1.45
1  0.40 -0.98  0.41
0  1.76  1.87 -0.10

程序操作习题 2:调优随机森林

问题: 使用随机森林预测。调整树的数量 n_estimators 至 2, 10, 50。调整特征数量 max_features 至 2 (所有特征),并观察结果变化。

解答: 我们将循环遍历不同的 n_estimators 值,并记录 MSE。

# 使用上一题的数据
X_ex = original_df[['x1', 'x2']]
y_ex = original_df['y']

# 调整 n_estimators
n_estimators_list = [2, 10, 50]
print('--- 调整 n_estimators (max_features="sqrt") ---')
for n in n_estimators_list:
    model = RandomForestRegressor(n_estimators=n, random_state=42, max_features='sqrt')
    model.fit(X_ex, y_ex)
    preds = model.predict(X_ex)
    mse = mean_squared_error(y_ex, preds)
    print(f'树的数量 = {n:2d}, 训练集 MSE = {mse:.4f}')

# 调整 max_features
print('\n--- 调整 max_features (n_estimators=50) ---')
# 'sqrt' 对 2 个特征来说是 1,2 是所有特征
max_features_list = ['sqrt', 2] 
for mf in max_features_list:
    model = RandomForestRegressor(n_estimators=50, random_state=42, max_features=mf)
    model.fit(X_ex, y_ex)
    preds = model.predict(X_ex)
    mse = mean_squared_error(y_ex, preds)
    print(f'特征数量 = {mf}, 训练集 MSE = {mse:.4f}')
--- 调整 n_estimators (max_features="sqrt") ---
树的数量 =  2, 训练集 MSE = 0.2048
树的数量 = 10, 训练集 MSE = 0.0941
树的数量 = 50, 训练集 MSE = 0.0608

--- 调整 max_features (n_estimators=50) ---
特征数量 = sqrt, 训练集 MSE = 0.0608
特征数量 = 2, 训练集 MSE = 0.0810

程序操作习题 3:调优 XGBoost

问题: 使用 xgboost 预测。将学习速率 learning_rate 调整至 0.01, 0.1, 0.5。将树的最大深度 max_depth 调整至 1, 3, 5。

解答: 我们将分别对 learning_ratemax_depth 进行调优实验。

# 调整 learning_rate
learning_rates = [0.01, 0.1, 0.5]
print('--- 调整 learning_rate (max_depth=3) ---')
for lr in learning_rates:
    model = XGBRegressor(n_estimators=100, learning_rate=lr, max_depth=3, random_state=42)
    model.fit(X_ex, y_ex)
    preds = model.predict(X_ex)
    mse = mean_squared_error(y_ex, preds)
    print(f'学习率 = {lr:.2f}, 训练集 MSE = {mse:.4f}')

# 调整 max_depth
max_depths = [1, 3, 5]
print('\n--- 调整 max_depth (learning_rate=0.1) ---')
for md in max_depths:
    model = XGBRegressor(n_estimators=100, learning_rate=0.1, max_depth=md, random_state=42)
    model.fit(X_ex, y_ex)
    preds = model.predict(X_ex)
    mse = mean_squared_error(y_ex, preds)
    print(f'最大深度 = {md}, 训练集 MSE = {mse:.4f}')
--- 调整 learning_rate (max_depth=3) ---
学习率 = 0.01, 训练集 MSE = 0.1220
学习率 = 0.10, 训练集 MSE = 0.0000
学习率 = 0.50, 训练集 MSE = 0.0000

--- 调整 max_depth (learning_rate=0.1) ---
最大深度 = 1, 训练集 MSE = 0.0001
最大深度 = 3, 训练集 MSE = 0.0000
最大深度 = 5, 训练集 MSE = 0.0000

本章总结:集成学习的智慧

  • 集成学习是一种元算法:它是一种组合其他模型的高级策略,而非一个具体的模型。
  • 两大流派,解决两大问题
    • Bagging 和 随机森林: 通过并行训练和投票/平均来降低方差,是对抗过拟合的利器。
    • Boosting: 通过串行训练和迭代修正错误来降低偏差,是对抗欠拟合的强大工具。
  • 性能与解释性的权衡: 集成学习能带来顶尖的预测性能,但代价是模型变成了“黑箱”。特征重要性是窥探其内部工作原理的重要补充工具。
  • 实践出真知: 集成学习的效果高度依赖于超参数调优。在金融实践中,对 n_estimators, max_features, learning_rate, max_depth 等参数进行仔细的交叉验证是取得成功的关键。