三个臭皮匠,赛过诸葛亮
在金融与经济中,预测无处不在
我们依赖各种模型进行关键决策:
核心问题:单一模型 vs. 群体智慧
任何一个单一模型都可能存在缺陷、偏差或错误。
本章要探讨的问题是:
如果我们将许多“还不错”的模型结合起来,能否得到一个“非常强大”的超级模型?
剧透一下:答案是肯定的。这个方法就叫做集成学习 (Ensemble Learning)。
学习路线图
Part 1: 理论依据
为什么“群体智慧”是可靠的?
霍夫丁不等式 (Hoeffding’s Inequality)
霍夫丁不等式从概率论上给出了一个保证:
大量独立随机变量的均值,会以极高的概率收敛于其真实期望值。
换句话说,只要样本够多,样本均值就非常接近总体均值。
这听起来很抽象,我们用一个经典的抛硬币实验来理解。
直观理解:抛硬币实验
- 问题:有一枚可能不均匀的硬币,正面朝上的真实概率是
p
(未知)。我们如何估计 p
?
- 方法:抛
n
次,计算正面朝上的频率 h
(样本均值)。
- 直觉:
n
越大,h
应该越接近真实的 p
。
霍夫丁不等式精确地量化了这种“接近”的程度。
霍夫丁不等式的数学表达
它告诉我们,样本均值 h
与真实期望 p
的差距大于任意小量 ε
的概率,是随着样本数 n
的增加而指数级下降的。
\[
\large{P(|h - p| > \epsilon) \le 2e^{-2n\epsilon^2}}
\]
这个 \(e\) 的负指数项是关键。这意味着我们每增加一次观测(抛一次硬币),犯大错误的概率就会急剧减小。
从抛硬币到集成学习的飞跃
集成学习的错误率呈指数级下降
基于这个类比,霍夫丁不等式的一个推论告诉我们:
如果我们有 T
个独立的、错误率为 ε
的二元分类器(其中 ε < 0.5
,即比随机猜测好),通过简单投票组成的集成模型 H(x)
,其犯错的概率会随着 T
的增加而指数级下降:
\[
\large{P(H(x) \ne y) \le \exp(-2T(0.5 - \epsilon)^2)}
\]
Part 2: 三大核心机制
实践中,我们如何创造出满足那两个条件的“一群臭皮匠”呢?
集成学习的三大流派
Bagging 工作流程图
Bagging 的魔力:降低方差
- 方差 (Variance):模型在不同训练集上预测结果的波动程度。高方差意味着模型对训练数据过于敏感,容易过拟合 (Overfitting)。
- Bagging 为何有效:每个基学习器只看到了部分数据,它们各自的过拟合方向可能不同。通过对这些不同方向的错误进行平均或投票,总体的波动性被有效平滑,从而降低了方差。
- 最成功的应用:随机森林 (Random Forest)。
Boosting 工作流程图
Boosting 的核心:降低偏差
- 偏差 (Bias):模型的预测值与真实值之间的系统性差距。高偏差意味着模型欠拟合 (Underfitting),没有学到数据的基本规律。
- Boosting 为何有效:每一轮新的学习器都被迫去关注上一轮学习器“搞不定”的那些困难样本。这个过程不断地修正模型的系统性错误,逐步减小偏差。
- 著名算法:AdaBoost, Gradient Boosting Machines (GBM), XGBoost。
Stacking 工作流程图
Stacking 的优势:模型融合
- 核心思想: Stacking 不是简单地组合预测,而是训练一个元模型来学习在什么情况下应该更相信哪个基模型。
- 例如: 元模型可能会学到:“如果模型A和模型B的预测很接近,但模型C的预测差很远,那么最终结果应该更倾向于A和B的平均值”。
- 应用场景: 在数据科学竞赛(如 Kaggle)中非常流行,能够将多个高性能模型的优势融合在一起,榨取最后的性能提升。
Part 3: 最受欢迎的“积木”——决策树
为什么我们花这么多时间讨论决策树?因为它是迄今为止最常用、最成功的集成学习基学习器。
决策树:天生的集成学习“好材料”
- 优点:
- 非线性,能捕捉复杂关系。
- 模型可解释性强(单个树)。
- 训练速度相对较快。
- 缺点:
- 非常容易过拟合,单个决策树的性能往往不稳定(高方差)。
绝配:决策树的高方差特性,恰好能被 Bagging (如随机森林) 通过平均来有效抑制!决策树的“弱小”(通过限制深度)又使其成为 Boosting 的完美“原料”。
决策树如何做出决策?
决策树通过一系列“是/否”问题,将复杂的数据集不断划分,直到每个子集都足够“纯净”。
- 根节点 (Root Node): 代表整个数据集。
- 内部节点 (Internal Node): 代表一个特征上的判断(一个问题)。
- 分支 (Branch): 代表这个判断的输出(问题的答案)。
- 叶节点 (Leaf Node): 代表最终的决策类别或预测值。
决策树剖析图
核心问题:如何选择“最好的”分裂?
决策树生长的关键,在于每一步都要选择一个最优的特征来分裂数据。
“最优”的标准是:分裂之后,各个子集的“纯度”最高。
“纯度”越高,意味着不确定性越小,分类越明确。
纯度 (Purity) 的直观理解
度量纯度之一:信息熵
- 信息熵
H(D)
是度量一个数据集 D
不确定性或混乱程度的指标。
- 熵越大,表示数据集越混乱(混合的类别越多)。
- 熵越小,表示数据集越纯净(大部分样本属于同一类别)。
对于一个有 K
个类别的数据集 D
,其信息熵定义为: \[
\large{H(D) = - \sum_{k=1}^{K} p_k \log_2(p_k)}
\] 其中 p_k
是第 k
类样本所占的比例。
熵的数值特性
假设一个数据集中只有两类:正例 (+)
和 反例 (-)
。
完全纯净 |
1.0 |
0.0 |
-1*log2(1) - 0 = 0 |
最高 |
混合 |
0.8 |
0.2 |
-0.8*log2(0.8) - 0.2*log2(0.2) ≈ 0.72 |
较低 |
最混乱 |
0.5 |
0.5 |
-0.5*log2(0.5) - 0.5*log2(0.5) = 1 |
最低 |
当正反例各占一半时,不确定性最大,熵达到最大值1。
案例:计算“场景”属性的信息增益 (1/3)
让我们用一个例子,手动计算属性“场景”的信息增益。
数据集D: 15个样本, 9个“上课”(正例), 6个“自习”(反例)。
1. 计算分裂前的总熵 H(D)
:
\[
\large{H(D) = - \frac{9}{15} \log_2\left(\frac{9}{15}\right) - \frac{6}{15} \log_2\left(\frac{6}{15}\right) \approx 0.971}
\]
案例:计算“场景”属性的信息增益 (2/3)
2. 计算按“场景”分裂后的条件熵 H(D|场景)
:
属性“场景”有3个取值:教室(7个)、宿舍(3个)、户外(5个)。
- 教室 (D1): 5正, 2反. \(\large{H(D_1) = - \frac{5}{7}\log_2(\frac{5}{7}) - \frac{2}{7}\log_2(\frac{2}{7}) \approx 0.863}\)
- 宿舍 (D2): 1正, 2反. \(\large{H(D_2) = - \frac{1}{3}\log_2(\frac{1}{3}) - \frac{2}{3}\log_2(\frac{2}{3}) \approx 0.918}\)
- 户外 (D3): 3正, 2反. \(\large{H(D_3) = - \frac{3}{5}\log_2(\frac{3}{5}) - \frac{2}{5}\log_2(\frac{2}{5}) \approx 0.971}\)
条件熵是加权平均: \[
\large{H(D|\text{场景}) = \frac{7}{15}H(D_1) + \frac{3}{15}H(D_2) + \frac{5}{15}H(D_3) \approx 0.910}
\]
案例:计算“场景”属性的信息增益 (3/3)
3. 计算信息增益 Gain(D, 场景)
:
\[
\large{\text{Gain}(D, \text{场景}) = H(D) - H(D|\text{场景})}
\] \[
\large{\approx 0.971 - 0.910 = 0.061}
\]
ID3 算法会为所有属性(场景、老师、学生等)都计算信息增益,然后选择那个增益最大的属性作为第一个分裂节点。
信息增益的缺陷与改进
- 问题: 信息增益准则对可取值数目较多的属性有所偏好。例如,如果用“学号”作为属性,每个学生一个分支,则每个叶子节点都无比纯净,信息增益会非常大,但这显然是过拟合。
- C4.5 算法的改进: 使用信息增益率 (Gain Ratio) 来校正这种偏好。
- 公式: \[
\large{\text{GainRatio}(D, A) = \frac{\text{Gain}(D, A)}{IV(A)}}
\] 其中
IV(A)
是属性A的固有值 (Intrinsic Value),属性A的取值越多,IV(A)
通常越大,起到了惩罚作用。
度量纯度之二:基尼不纯度
CART (Classification and Regression Tree) 算法使用基尼指数来选择分裂属性。
- 基尼不纯度
Gini(D)
: 从数据集 D
中随机抽取两个样本,其类别标记不一致的概率。
- 基尼指数越小,数据集的纯度越高。
\[
\large{\text{Gini}(D) = \sum_{k=1}^{K} p_k (1 - p_k) = 1 - \sum_{k=1}^{K} p_k^2}
\]
分裂标准2:基尼指数增益
- 思想: 选择一个属性
A
进行分裂,使得分裂后的基尼指数加权和最小。
- 公式: 对于属性
A
的某个划分 a
,分裂后的基尼指数为: \[
\large{\text{GiniIndex}(D|A=a) = \sum_{v=1}^{V} \frac{|D^v|}{|D|} \text{Gini}(D^v)}
\]
- 决策: 选择使
GiniIndex(D|A=a)
最小的属性 A
和划分 a
。
- 优势: 相比于熵的计算,基尼指数不涉及对数运算,计算上更高效。
案例:计算基尼指数 (1/2)
我们用同样的数据计算属性“场景=教室”这个二分划分的基尼指数。
数据集D: 15个样本。
划分:
D1
(“场景=教室”): 7个样本 (5正, 2反)
D2
(“场景!=教室”): 8个样本 (4正, 4反)
1. 计算 D1
和 D2
的基尼指数: \[
\large{\text{Gini}(D_1) = 1 - \left(\frac{5}{7}\right)^2 - \left(\frac{2}{7}\right)^2 \approx 0.408}
\] \[
\large{\text{Gini}(D_2) = 1 - \left(\frac{4}{8}\right)^2 - \left(\frac{4}{8}\right)^2 = 0.5}
\]
案例:计算基尼指数 (2/2)
2. 计算该划分的加权基尼指数:
\[
\large{\text{GiniIndex}(D|\text{场景=教室}) = \frac{7}{15}\text{Gini}(D_1) + \frac{8}{15}\text{Gini}(D_2)}
\] \[
\large{\approx \frac{7}{15}(0.408) + \frac{8}{15}(0.5) \approx 0.190 + 0.267 = 0.457}
\]
CART 算法会遍历所有属性的所有可能二分点,计算它们的加权基尼指数,并选择最小的那个作为最优分裂点。
决策树的现实问题:过拟合
如果不对决策树的生长加以限制,它会一直分裂,直到每个叶子节点只包含一个样本。这时训练集上的错误率为0,但模型会变得极其复杂,对新数据的泛化能力很差。
解决方案:剪枝 (Pruning)
为了防止过拟合,我们需要对决策树进行“剪枝”。
- 预剪枝 (Pre-pruning): 在树的生长过程中,如果一个分裂不能带来泛化性能的提升(例如,在验证集上性能下降),就提前停止分裂。
- 优点: 速度快,生成的树更小。
- 缺点: 可能过于“短视”,错过一些好的分裂组合。
- 后剪枝 (Post-pruning): 先生成一棵完整的决策树,然后自底向上地考察节点。如果剪掉一个子树能提升泛化性能,就把它剪掉。
- 优点: 通常效果更好,更不容易错过好的结构。
- 缺点: 计算开销更大。
Part 4: 强大的集成模型
现在,我们把决策树这个“积木”和 Bagging/Boosting 这两种“搭建方法”结合起来。
随机森林的双重随机性
随机森林为何更强大?“求同存异”
- 特征随机性的作用: 降低了森林中树与树之间的相关性。
- 为何降低相关性很重要: 如果不随机选择特征,森林中的每棵树在顶层节点分裂时,都很可能会选择同一个最强的特征,导致所有树的结构都非常相似。这样的集成,效果就大打折扣。
- 通过强制每棵树在分裂时只能考虑一部分特征,随机森林让每棵树都从不同的“视角”去学习,变得“各有所长”。当它们组合在一起时,就能形成更强大的互补效应,进一步降低整体的方差。
AdaBoost 算法流程详解 (1/4): 初始化
目标: 训练一个强分类器 \(\large{H(x) = sign(\sum \alpha_t h_t(x))}\)
1. 初始化: 所有 N
个训练样本的权重被初始化为相等:
\[
\large{w_{1,n} = 1/N} \quad \text{for } n=1, \dots, N
\]
AdaBoost 算法流程详解 (4/4): 最终组合
3. 最终输出: 将所有 T
个弱学习器按照其权重 α_t
进行加权投票,得到最终的强分类器:
\[
\large{H(x) = \text{sign}\left(\sum_{t=1}^{T} \alpha_t h_t(x)\right)}
\]
实战第1步:数据加载与准备
我们将使用 ucimlrepo
库直接获取数据,并进行训练集和测试集的划分。
# 导入必要的库
import pandas as pd
from sklearn.model_selection import train_test_split
from ucimlrepo import fetch_ucirepo
# --- 从UCI库获取数据 ---
# 这是一个标准化的数据加载方式,保证了数据的可复现性
credit_default = fetch_ucirepo(id=350)
X = credit_default.data.features
y = credit_default.data.targets.squeeze() # 转换成Pandas Series
# --- 数据分割 ---
# 将数据分为训练集(70%)和测试集(30%)
# random_state=42 保证每次分割结果一致
# stratify=y 保证训练集和测试集中违约比例与原始数据一致
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f'训练集维度: {X_train.shape}')
print(f'测试集维度: {X_test.shape}')
训练集维度: (21000, 23)
测试集维度: (9000, 23)
实战第2步:训练基准模型 - 单个决策树
我们先训练一个决策树作为性能基准。为了防止过拟合,我们限制其最大深度为5。
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, roc_auc_score
# --- 训练模型 ---
# max_depth=5 限制树的深度,防止过拟合
dt_clf = DecisionTreeClassifier(max_depth=5, random_state=42)
dt_clf.fit(X_train, y_train)
# --- 评估模型 ---
y_pred_dt = dt_clf.predict(X_test)
y_prob_dt = dt_clf.predict_proba(X_test)[:, 1] # 获取正类的预测概率
acc_dt = accuracy_score(y_test, y_pred_dt)
auc_dt = roc_auc_score(y_test, y_prob_dt)
print(f'单个决策树 (max_depth=5):')
print(f' 准确率 (Accuracy): {acc_dt:.4f}')
print(f' AUC: {auc_dt:.4f}')
单个决策树 (max_depth=5):
准确率 (Accuracy): 0.8164
AUC: 0.7427
实战第3步:训练 Bagging 模型 - 随机森林
现在,让我们看看由100棵决策树组成的“森林”表现如何。n_estimators
就是基学习器的数量 T
。
from sklearn.ensemble import RandomForestClassifier
# --- 训练模型 ---
# n_estimators=100: 构建100棵树
# n_jobs=-1: 使用所有CPU核心并行计算,加快速度
rf_clf = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42, n_jobs=-1)
rf_clf.fit(X_train, y_train)
# --- 评估模型 ---
y_pred_rf = rf_clf.predict(X_test)
y_prob_rf = rf_clf.predict_proba(X_test)[:, 1]
acc_rf = accuracy_score(y_test, y_pred_rf)
auc_rf = roc_auc_score(y_test, y_prob_rf)
print(f'随机森林 (100棵树, max_depth=5):')
print(f' 准确率 (Accuracy): {acc_rf:.4f}')
print(f' AUC: {auc_rf:.4f}')
随机森林 (100棵树, max_depth=5):
准确率 (Accuracy): 0.8118
AUC: 0.7675
观察: 随机森林的准确率和AUC都比单个决策树有所提升。
实战第4步:训练 Boosting 模型 - AdaBoost
最后,我们来试试 AdaBoost。它也是基于决策树,但采用的是串行的、关注错误样本的提升策略。
from sklearn.ensemble import AdaBoostClassifier
# --- 训练模型 ---
# AdaBoost 通常使用比较浅的树(“树桩”),这里用max_depth=1
base_estimator = DecisionTreeClassifier(max_depth=1)
ada_clf = AdaBoostClassifier(
estimator=base_estimator,
n_estimators=100,
random_state=42
)
ada_clf.fit(X_train, y_train)
# --- 评估模型 ---
y_pred_ada = ada_clf.predict(X_test)
y_prob_ada = ada_clf.predict_proba(X_test)[:, 1]
acc_ada = accuracy_score(y_test, y_pred_ada)
auc_ada = roc_auc_score(y_test, y_prob_ada)
print(f'AdaBoost (100个树桩):')
print(f' 准确率 (Accuracy): {acc_ada:.4f}')
print(f' AUC: {auc_ada:.4f}')
AdaBoost (100个树桩):
准确率 (Accuracy): 0.8160
AUC: 0.7704
观察: AdaBoost 的表现也明显优于单个决策树。
实战第5步:结果对比与可视化
语言是苍白的,让我们把结果画出来。AUC (Area Under the Curve) 是一个比准确率更稳健的分类模型评估指标,它衡量了模型区分正负样本的综合能力。
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import seaborn as sns
import pandas as pd
# Fallback font for Chinese characters if needed
# try:
# plt.rcParams['font.sans-serif'] = ['Heiti TC']
# except:
# pass
# 设置绘图风格
sns.set_theme(style="whitegrid", context="talk")
fig, ax = plt.subplots(figsize=(10, 6), dpi=100)
# 数据
results = pd.DataFrame({
'Model': ['单个决策树', '随机森林', 'AdaBoost'],
'AUC': [auc_dt, auc_rf, auc_ada]
}).sort_values('AUC', ascending=True)
colors = ['#86BBD8', '#F26419', '#33658A']
# 绘图
bars = ax.barh(results['Model'], results['AUC'], color=colors, height=0.6)
ax.set_xlim(0.76, 0.785)
ax.set_xlabel('ROC AUC Score', fontsize=14, labelpad=10)
ax.set_title('模型性能对比:信用卡违约预测', fontsize=18, pad=20, weight='bold')
# 在柱状图上显示数值
for bar in bars:
width = bar.get_width()
ax.text(width + 0.0005, bar.get_y() + bar.get_height()/2, f'{width:.4f}',
ha='left', va='center', fontsize=14, weight='bold')
# 美化图表
ax.spines[['top', 'right', 'bottom']].set_visible(False)
ax.xaxis.grid(True, linestyle='--', which='major', color='grey', alpha=0.5)
ax.yaxis.grid(False)
ax.tick_params(axis='y', labelsize=14, length=0)
ax.tick_params(axis='x', labelsize=12)
plt.tight_layout()
plt.show()
结果分析:群体智慧的胜利
从 Figure 1 可以清晰地看到:
- 集成学习模型(随机森林和AdaBoost)的性能显著优于单个决策树。
- 这有力地证明了我们从理论开始的推导:将多个“还不错”的学习器(决策树)通过系统性的方法(Bagging, Boosting)组合起来,确实可以得到一个更强大的模型。
- 在这个特定任务上,随机森林和 AdaBoost 的表现相近,都取得了很好的效果。
集成学习的另一大优势:可解释性
集成模型,特别是基于树的模型,还有一个巨大的优点:它们可以告诉我们,哪些输入特征对于做出最终决策最重要。
这在经济和金融应用中至关重要。我们不仅想预测,更想理解预测背后的驱动因素。
可视化特征重要性
让我们看看随机森林模型认为哪些因素是预测信用卡违约的最重要指标。
import numpy as np
# 获取特征重要性
importances = rf_clf.feature_importances_
feature_names = X.columns
df_importance = pd.DataFrame({'feature': feature_names, 'importance': importances})
df_importance = df_importance.sort_values('importance', ascending=False).head(15)
# 设置绘图风格
fig, ax = plt.subplots(figsize=(10, 7), dpi=100)
# 绘图
sns.barplot(
x='importance',
y='feature',
data=df_importance,
palette='viridis',
ax=ax
)
# 美化图表
ax.set_title('特征重要性分析 (来自随机森林)', fontsize=18, pad=20, weight='bold')
ax.set_xlabel('相对重要性 (Mean Decrease in Impurity)', fontsize=14, labelpad=10)
ax.set_ylabel('')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.tick_params(axis='both', which='major', labelsize=12)
plt.tight_layout()
plt.show()