跳到正文

用于不平衡类别的 Bagging 分类器

当 90% 的标签是“不交易”时,准确率毫无意义。平衡 Bagging 通过对每个类别抽取等量子样本来修正不平衡。关键在于把参数 $u$ 选对。

第 10 篇 · 共 12 篇 约 11 分钟

Bagging 解决不平衡:为什么准确率会骗人

交易中常见的标签分布:90% 不交易,5% 多头赢家,5% 空头赢家。一个对每条观测都预测“不交易”的分类器能达到 90% 准确率。这个分类器没用。准确率没有意义。scikit-learn、XGBoost 和大多数机器学习库的标准损失函数都是为最大化准确率设计的,如果类别不平衡够严重,它们会找到“永远不交易”这个平凡解。

修正是对训练集做再平衡。三种思路:过采样少数类、欠采样多数类,或在损失函数中使用类别权重。前两种改数据,第三种改损失。在这些方法中,对高不平衡数据最稳健的是平衡 Bagging + 欠采样:从每个类别抽取等量大小的子样本,在每个子样本上训练分类器,对预测取平均。

参考是 AFML 第 4 章 §4.5(Bagging 分类器与唯一性)。这个方法不复杂,但有一个参数(uu,子样本大小)很少被讨论,也经常被选错。

(López de Prado, M., Advances in Financial Machine Learning, 第 4 章 §4.5。ISBN 978-1119482086。)

准确率陷阱

一个简单的例子。你有 10,000 条标签:9,000 条不交易,500 条多头赢,500 条空头赢。一个永远预测“不交易”的模型达到 90% 准确率。这个模型没有预测能力,没有捕捉任何信号。它的 90% 准确率是最差的基线:任何有用的模型都必须超过 90%,但门槛并不严格:一个 95% 的时候预测“不交易”、5% 的时候预测“多头”的模型也可能有 95% 准确率,比平凡基线还差。

实际中,情况更微妙。最大化准确率的模型学会以高置信度预测多数类,忽略少数类。它们不是“永远不交易”,而是“以 0.99 的概率不交易”。在少数模型预测多头的时刻,它的校准可能只是 50% 的正确率,并不比随机好。模型学会的是“有选择地愚蠢”。

问题在损失函数。在 90/5/5 的类别分布上,交叉熵损失由不交易类主导。9,000 条不交易观测的梯度淹没了 500 条多头赢和 500 条空头赢观测的梯度。模型收敛到一种状态:不交易预测得好,少数类被当作噪声。

标准修正:

类别权重:按类别频率的倒数对损失加权。少数类观测得到更高权重。有效,但权重是有噪声的估计,特别是对小的少数类。

过采样:复制少数类观测直到类别平衡。有效,但增加了少数类的过拟合风险。

欠采样:删除多数类观测直到类别平衡。有效,但会丢数据。

Bagging 加欠采样:混合方法。抽取多个平衡子样本,在每个上训练分类器,对预测取平均。把 Bagging 的方差缩减和欠采样的平衡结合在一起。

本文重点说第四种。

(AFML, §4.5。)

Bagging 分类器

算法:

  1. 设 Nmin⁡N_{\min} 是最小类别的大小。设 u≤Nmin⁡u \leq N_{\min} 是欠样本大小。
  2. 对 k=1,2,…,Kk = 1, 2, \ldots, K:
    • 抽取一个平衡子样本:从训练集中无放回地抽取每个类别的 uu 条观测。
    • 在子样本上训练分类器。
    • 记录分类器在完整测试集上的预测。
  3. 对 KK 个分类器预测的概率取平均。选平均概率最高的类别作为最终预测。

关键参数是 uu。如果 u=Nmin⁡u = N_{\min},每个子样本包含所有少数类观测和 Nmin⁡N_{\min} 个多数类观测。如果 u<Nmin⁡u < N_{\min},每个子样本是每个类别的严格子集。

分类器个数 KK 通常是 100–1000。更多分类器减少方差,但计算量线性增加。

一个细节:在第 2 步,“平衡子样本”用无放回抽样。有放回抽样会有重复,无放回抽样则保证观测互不相同。优先使用无放回抽样,因为它保留了训练集的多样性。

(Breiman, L., “Bagging Predictors,” Machine Learning, 24(2), 1996, pp. 123–140。)

选择 uu

欠样本大小 uu 是最重要的参数。三种状态。

状态 1:u=Nmin⁡u = N_{\min}(全少数类,平衡多数类)

每个分类器看到所有少数类观测和多数类观测的平衡样本。这是标准的平衡 Bagging。模型被迫学习少数类,因为两类对损失的贡献相等。

优点:每条少数类观测都被使用。信息保留最多。

缺点:训练集很小(大小 2u2u,其中 uu 是少数类大小)。模型方差很高。Bagging 能缓解但无法消除。

状态 2:u<Nmin⁡u < N_{\min}(子采样的少数类)

每个分类器看到少数类的严格子集和多数类的平衡样本。这是“子采样 Bagging”或“带子采样的平衡 Bagging”。

优点:分类器之间多样性更大。每个分类器看到少数类的不同切片,方差更小。

缺点:部分少数类观测没有任何分类器见过。信息有损失。

状态 3:u>Nmin⁡u > N_{\min}(少数类过采样)

每个分类器看到所有少数类观测(如果 u>Nmin⁡u > N_{\min} 就会有放回)和数量相当的多数类样本。这是“过采样 Bagging”。

优点:每个分类器训练数据更多,方差更小。

缺点:少数类观测被重复。少数类有过拟合风险。

哪种状态最好?取决于少数类的大小。当 Nmin⁡>1000N_{\min} > 1000 时,状态 1 通常够用。当 Nmin⁡<500N_{\min} < 500 时,状态 2(子采样)经常胜出,因为多样性降低了方差。当 Nmin⁡<100N_{\min} < 100 时,状态 3(过采样)是必要的,因为你不可能在少于 100 条观测上训练出可用模型。

一个实务经验法则:u=min⁡(Nmin⁡,Ntotal/2)u = \min(N_{\min}, \sqrt{N_{\text{total}} / 2})。这给出一个适中的欠样本大小,在大多数情况下都有效。

(AFML, §4.5.2。)

为什么 Bagging 对不平衡起作用

Bagging 降低方差,而方差是不平衡数据的主要问题。在一个平衡子样本上拟合的单个分类器方差很高:换一个子样本,得到的分类器不同。跨多个分类器取平均,方差更低,更接近“真实”的决策边界。

形式上,如果每个分类器方差是 σ2σ^2,两两协方差是 ρσ2ρ σ^2,那么 Bagging 预测的方差是:

Varbagged=ρσ2+(1−ρ)σ2K\text{Var}_{\text{bagged}} = \rho \sigma^2 + \frac{(1 - \rho) \sigma^2}{K}

当 K→∞K \to \infty 时,Bagging 的方差趋近 ρσ2ρ σ^2。如果分类器高度相关(ρρ 很大),Bagging 帮助不大。如果它们不相关(ρρ 很小),Bagging 把方差降低 KK 倍。

对平衡 Bagging 来说,分类器在不同的子样本上训练,错误有中等程度的相关性。典型 ρ≈0.3ρ \approx 0.3–0.50.5,所以 Bagging 把方差降低 50%–70%。这是一个有意义的提升。

(Breiman, 1996。)

实现

一个干净的实现:

import numpy as np
from sklearn.base import clone

class BalancedBaggingClassifier:
    def __init__(self, base_estimator, n_estimators=100, u=None, random_state=None):
        self.base_estimator = base_estimator
        self.n_estimators = n_estimators
        self.u = u
        self.random_state = random_state
    
    def fit(self, X, y):
        classes, counts = np.unique(y, return_counts=True)
        self.classes_ = classes
        n_min = counts.min()
        u = self.u if self.u is not None else n_min
        
        self.estimators_ = []
        rng = np.random.default_rng(self.random_state)
        
        for _ in range(self.n_estimators):
            # Draw balanced subsample
            subsample_idx = []
            for c in classes:
                class_idx = np.where(y == c)[0]
                chosen = rng.choice(class_idx, size=min(u, len(class_idx)), replace=False)
                subsample_idx.extend(chosen)
            
            X_sub = X[subsample_idx]
            y_sub = y[subsample_idx]
            
            estimator = clone(self.base_estimator)
            estimator.fit(X_sub, y_sub)
            self.estimators_.append(estimator)
        
        return self
    
    def predict_proba(self, X):
        probs = np.array([est.predict_proba(X) for est in self.estimators_])
        return probs.mean(axis=0)
    
    def predict(self, X):
        return self.classes_[self.predict_proba(X).argmax(axis=1)]

u 参数是欠样本大小。如果不指定,默认等于少数类大小(状态 1)。

base_estimator 的常见选择:限制深度的决策树(max_depth = 5–10)或逻辑回归。深度不设上限的树会在小的平衡子样本上过拟合。限制深度来控制方差。

校准问题

一个细节:Bagging 分类器在平衡数据上训练,但它要在不平衡数据上预测。预测概率没有针对原始类别分布做校准。

例子:原始数据中少数类占 10%,但训练是平衡的。分类器对某条测试观测给出少数类概率 0.7。这是不是说该观测有 70% 的概率属于少数类?

不是。分类器在 50% 都是少数类的数据上训练,它的输出是针对那个分布校准的。要得到针对原始分布校准的概率,套用贝叶斯公式:

P(少数类∣特征)=P平衡(少数类∣特征)⋅πP平衡(少数类∣特征)⋅π+P平衡(多数类∣特征)⋅(1−π)P(\text{少数类} | \text{特征}) = \frac{P_{\text{平衡}}(\text{少数类} | \text{特征}) \cdot \pi}{P_{\text{平衡}}(\text{少数类} | \text{特征}) \cdot \pi + P_{\text{平衡}}(\text{多数类} | \text{特征}) \cdot (1 - \pi)}

其中 π\pi 是原始的少数类占比。当 π=0.05\pi = 0.05、平衡分类器输出 0.7 时:

P=0.7⋅0.050.7⋅0.05+0.3⋅0.95=0.0350.32≈0.11P = \frac{0.7 \cdot 0.05}{0.7 \cdot 0.05 + 0.3 \cdot 0.95} = \frac{0.035}{0.32} \approx 0.11

所以校准后的概率是 0.11,不是 0.7。这是一个有意义的差别。如果用未校准的概率来给仓位,少数类会被严重超配。

始终把 Bagging 分类器的输出针对原始类别分布做校准。最简单的方法:把预测概率除以被预测类别的先验概率。更精细的方法:在 holdout 集上用 Platt 缩放或 isotonic 回归。

(AFML, §4.5.3。)

完整例子

设定:10,000 条日频观测,9,000 条不交易,500 条多头赢,500 条空头赢。特征是 20 天动量、60 天均值回归和波动率。

不用 Bagging:逻辑回归收敛到“始终以 99% 置信预测不交易,偶尔以 50% 置信预测多头”。准确率 90%。模型 P&L 为零,因为它从不交易。

用平衡 Bagging(u=500u = 500、K=100K = 100):

  • 每个分类器在 500 条不交易、500 条多头赢、500 条空头赢上训练。
  • 模型被迫学习类别之间的差异。
  • Bagging 之后,模型在特征看涨时给出 50% 的多头赢概率,看跌时给出 50% 的空头赢概率,中性时给出 50% 的不交易概率。
  • 针对原始分布做校准:特征看涨时多头赢概率大约是 5%(远小于 50% 的原始输出)。

结果:模型一年找出大约 200 个多头赢机会和 200 个空头赢机会(在 10,000 个交易日中)。其中 60% 是正确的(对比平凡基线的 50%)。策略在这些高置信度信号上下注。

在全部 10,000 条观测上准确率是 88%(低于 90% 的基线),但交易准确率(一年约 400 笔交易)是 60%。夏普在 1.5 以上。这就是收益:交易次数变少,但每笔交易的质量变高。

常见陷阱

五种陷阱。

陷阱 1:忘了校准。

如上所述。未校准概率会超配少数类。始终针对原始分布做校准。

陷阱 2:uu 选得太小。

如果 u=50u = 50、少数类有 500 条观测,每个分类器只用了 10% 的少数类观测。模型方差很高,可能错过重要模式。Bagging 能缓解但无法消除。

陷阱 3:uu 选得太大。

如果 u=Nmin⁡=500u = N_{\min} = 500,每个分类器用了所有少数类观测。少数类维度上分类器之间没有多样性。Bagging 只对多数类减少方差。

陷阱 4:基础估计器过于灵活。

深度不设上限的决策树会过拟合每个平衡子样本。把深度限制在 5–10,或者用逻辑回归。

陷阱 5:在不平衡的测试集上算准确率。

在测试集上报告 90% 准确率会产生误导。应该报告精确率、召回率、F1 和交易夏普。准确率指标对不平衡数据没有用。

Bagging 不够用的情况

三种情况。

情况 1:极端不平衡(少数类 <1%)。

如果少数类小于数据的 1%,即使平衡 Bagging 也可能产生不出足够信号。模型无论怎么 Bagging,方差都很大。考虑用异常检测方法(单类 SVM、isolation forest)替代分类。

情况 2:多类不平衡。

如果类别有 3 个以上且不平衡程度不同,平衡 Bagging 调起来更困难。考虑在损失函数中使用类别权重(例如 XGBoost 的 scale_pos_weight)加单个分类器。

情况 3:序列依赖。

Bagging 把观测当作独立同分布。金融数据有序列依赖(自相关、状态结构)。Bagging 处理不了这些。要额外用序列交叉验证方案(净化 k 折、组合净化 CV)。

一个有用的诊断

计算 Bagging 分类器在测试集上的预测,查看预测概率的分布。如果大多数预测接近 0.33(3 类的随机基线),分类器没学到多少。如果分布是双峰的(一些预测接近 0,另一些接近 1),分类器学到了东西。

一种有用的图:少数类预测概率的直方图。如果直方图在 0 附近峰值,分类器没有识别少数类观测。如果在 0.5 或更高处峰值,分类器在做有用的事。

另一个诊断:在少数类上的精确率-召回率曲线。PR 曲线下面积(AUPRC)比 ROC 曲线下面积对不平衡数据更有信息量。

另一个有用的诊断

除直方图和 PR 曲线外,下面几个诊断对评估 Bagging 分类器的行为也很有用。

校准图:画预测概率(x 轴)和观测频率(y 轴)。校准良好的分类器落在对角线上。Bagging 分类器通常没校准好(对少数类过度自信),因为它是在平衡数据上训练的。施加校准调整(Platt 缩放或 isotonic 回归),并验证曲线是否改善。

特征重要性:从每个基模型算特征重要性,跨 Bag 取平均。决策树的特征重要性是总的不纯度下降量。逻辑回归是系数绝对值。平均后的重要性揭示哪些特征在驱动分类器。

测试集上的混淆矩阵:报告混淆矩阵。对 3 类问题(不交易、多头赢、空头赢),这能看出哪些类被混淆。经常把不交易和多头赢混淆的分类器校准得不好;把多头赢和空头赢混淆的分类器可以接受。

按类别分层的指标:对每个类分别计算精确率、召回率和 F1。整体准确率对不平衡数据没有信息。按类别指标揭示分类器对哪些类处理得好。

(AFML, §4.5.6。)

和 Meta-Labeling 的联系

平衡 Bagging 和 meta-labeling 解决不同问题,但互补。

平衡 Bagging 解决:分类器因为类别不平衡而过于频繁预测“不交易”。Bagging 强制分类器学习少数类。

Meta-labeling 解决:分类器不知道每个预测应该有多大的置信度。Meta-model 提供置信度估计。

一个组合流水线:

  1. 训练一个平衡 Bagging 分类器预测方向(-1、0、+1)。
  2. 训练一个 meta-labeling 模型预测 Bagging 分类器是否正确,前提是 Bagging 分类器预测非零。
  3. 用 meta-labeling 输出作为仓位大小。

Bagging 处理不平衡。Meta-labeling 处理置信度。两者一起,构成一个稳健的交易系统。

Bagging 分类器和 meta-model 可以共享特征,也可以使用不同特征。通常,它们共享方向预测特征,但 meta-model 会加上置信度预测特征(波动率、状态等)。

(AFML, 第 4 章, §4.5 小结。)

Bagging 给你带来什么

一个学会少数类而不是默认多数类的分类器。提升通常很显著:一个从“从不交易”变成“选择性交易”的策略,夏普可以从 0 涨到 1 以上。分类器在绝对意义上并没有变“更聪明”;它只是不再忽略少数类。

代价是计算:训练 KK 个分类器而不是一个。K=100K = 100 时,代价是 100 倍。对支持并行训练的现代机器学习库,墙钟时间通常远小于 100 倍。

对大多数应用,这个代价是被提升所证明的。一个忽略少数类的分类器在交易中毫无用处;平衡 Bagging 让它变得有用。

更大的视角

平衡 Bagging 是处理不平衡类别的一种工具。更广泛的原则是:让损失函数的结构反映你在意的事。如果你想识别少数类观测(在交易中你确实应该在意,因为这些才是交易),就不要用被多数类主导的损失函数。

同一精神下的其他工具:

  • Focal loss(Lin 等,2017):对容易观测降权,聚焦困难观测。等价于过采样困难样本。
  • 成本敏感学习:按成本对每个类加权(漏掉少数类 vs 错判多数类)。等价于类别权重,但更明确。
  • SMOTE:通过插值生成合成少数类观测。对很小的少数类有用。

对大多数金融机器学习应用,平衡 Bagging 是最简单、最稳健的方法。SMOTE 和 focal loss 也有用武之地,但它们引入了额外的超参数,可能产生不真实的合成数据。从 Bagging 起步,等需要时再考虑更复杂的方法。

(López de Prado, M., Advances in Financial Machine Learning, 第 4 章 §4.5。ISBN 978-1119482086。)