15、机器学习预警模型(三):样本不平衡处理

流动性黑洞这东西,说白了就是市场里的「黑天鹅」。一年到头可能就出现一两次,甚至几年才碰上一回。你想想看,用机器学习去预测这种罕见事件,第一个跳出来的问题就是——样本严重不平衡。

正常行情的数据成千上万条,黑洞样本就那么几个。模型学来学去,最后干脆摆烂:反正全预测成「正常」,准确率也能到99.9%。这能行吗?肯定不行。我们真正要抓的是那0.1%的灾难。

这一章,我就把我在实战中踩过的坑、试过的方法,一次性讲清楚。SMOTE、代价敏感学习、异常检测,这三板斧怎么用,什么时候用,咱们一个一个拆。

15.1 样本不平衡到底有多「要命」?

先给你看个真实数据。我做过一个美股流动性崩塌的预警模型,训练集里正常样本有50万条,黑洞事件只有127条。比例大概是4000:1。

你猜模型学成了什么样?它把所有样本都判成「正常」,准确率99.97%。但我要的是这个吗?我要的是那127次崩塌能提前预警哪怕一半。结果呢,一次都没抓住。

为什么会这样?因为传统的分类模型,比如逻辑回归、随机森林,它们的目标函数是「整体准确率最大化」。少数类样本权重太低,模型根本不在乎。

嗯,这里要注意:不平衡比例超过100:1,常规方法基本失效。流动性黑洞这种极端事件,比例经常在1000:1以上,必须上特殊手段。

核心问题:模型在多数类上「躺赢」,在少数类上「装死」。我们需要强制模型关注那些罕见的崩塌信号。

15.2 方法一:SMOTE——人工合成少数类样本

SMOTE,全称是Synthetic Minority Oversampling Technique。名字挺长,思路其实很简单:既然少数类样本太少,那我就「造」一些出来。

怎么造?不是简单复制。SMOTE的做法是:在少数类样本之间插值。比如你有两个崩塌样本A和B,SMOTE会在A和B的连线上随机生成一个新样本。这样既增加了数量,又保留了数据的分布特征。

我在项目中遇到过一个问题:直接用SMOTE对原始时间序列做插值,结果生成了很多「不可能出现」的行情数据。比如价格跳跃、成交量突变。后来我学乖了——对特征空间做SMOTE,而不是对原始序列做

下面是我常用的实现方式:

from imblearn.over_sampling import SMOTE
import pandas as pd

# 假设 X 是特征矩阵,y 是标签(1=黑洞,0=正常)
smote = SMOTE(sampling_strategy=0.1,  # 让少数类达到多数类的10%
              k_neighbors=5,          # 用5个近邻来插值
              random_state=42)

X_resampled, y_resampled = smote.fit_resample(X, y)

print(f"原始样本分布: {y.value_counts().to_dict()}")
print(f"SMOTE后分布: {pd.Series(y_resampled).value_counts().to_dict()}")

个人经验:SMOTE的sampling_strategy别设太高。我一般控制在0.1到0.5之间。太高会引入过多噪声,模型反而学偏。另外,k_neighbors设3-5就够了,太大容易生成「四不像」样本。

15.3 方法二:代价敏感学习——让模型「心疼」少数类

SMOTE是从数据层面动手。代价敏感学习呢,是从损失函数层面动手。

思路很简单:模型预测错一个黑洞样本,代价是预测错正常样本的100倍。这样模型在训练时就会「掂量掂量」——宁可多报几个假警报,也不能漏掉真崩塌。

我习惯在XGBoost和LightGBM里直接用scale_pos_weight参数。这个参数会自动调整正负样本的权重比例。

import xgboost as xgb

# 计算正负样本比例
neg_count = (y_train == 0).sum()
pos_count = (y_train == 1).sum()
scale = neg_count / pos_count  # 比如4000:1,scale=4000

model = xgb.XGBClassifier(
    scale_pos_weight=scale,
    max_depth=4,
    learning_rate=0.05,
    n_estimators=200
)

model.fit(X_train, y_train)

这里有个坑,我曾经踩过:scale_pos_weight设得太大,模型会疯狂报假警报。比如设成4000,模型可能把10%的正常行情都判成黑洞。这在实际交易中根本没法用——谁会听一个天天喊「狼来了」的模型?

避坑指南:我建议用验证集上的F1-score或Precision-Recall曲线来调scale_pos_weight。别只看召回率。一个召回率90%但精确率只有1%的模型,等于没有模型。

15.4 方法三:异常检测——换个角度看问题

前面两种方法,本质上还是「分类」思维。但流动性黑洞这种事件,其实更适合用「异常检测」来做。

为什么?因为黑洞样本太少,你很难定义「黑洞长什么样」。但你可以定义「正常行情长什么样」。正常行情的数据多得很,模型可以学得很充分。然后,任何偏离正常模式太远的,都标记为异常——也就是潜在的黑洞前兆。

我常用的异常检测方法有两种:

  • 孤立森林(Isolation Forest): 速度快,适合高维数据。核心思想是:异常点更容易被「孤立」,只需要很少的随机切分就能把它分离出来。
  • 单类SVM(One-Class SVM): 适合数据分布比较稳定的场景。它会学出一个「正常区域」的边界,落在边界外的就是异常。
from sklearn.ensemble import IsolationForest

# 只用正常样本训练
X_normal = X_train[y_train == 0]

iso_forest = IsolationForest(
    contamination=0.001,  # 预期异常比例,根据历史黑洞频率来设
    random_state=42,
    n_estimators=100
)

iso_forest.fit(X_normal)

# 对全量数据做预测
# 返回1表示正常,-1表示异常
y_pred = iso_forest.predict(X_test)
anomaly_flags = (y_pred == -1).astype(int)

我的习惯:异常检测通常作为「第一道筛子」。先用孤立森林筛出可疑样本,再用分类模型做二次确认。这样既减少了分类模型处理的数据量,又提高了召回率。我在2020年3月的流动性危机中,用这套组合提前2小时捕捉到了信号。

15.5 三种方法的对比与选择

说了这么多,到底该用哪个?我整理了一张对比表,你一看就明白:

方法 适用场景 优点 缺点 我的推荐指数
SMOTE 少数类样本有一定数量(>50条) 实现简单,能保留数据分布 可能生成噪声,不适合极端不平衡 ⭐⭐⭐
代价敏感学习 不平衡比例明确,业务代价可量化 直接优化目标,调参灵活 权重设不好容易过拟合 ⭐⭐⭐⭐
异常检测 少数类极少(<50条),或定义模糊 不需要少数类样本,适合冷启动 假阳性率高,需要二次过滤 ⭐⭐⭐⭐⭐

我个人习惯是:先用异常检测做初筛,再用代价敏感学习做精调。SMOTE我用的相对少,除非样本量实在不够但又不想放弃分类框架。

15.6 实战中的组合策略

最后,分享一个我在实盘环境中跑通的流程。这个流程帮我抓住了2022年6月的一次流动性骤降事件:

  1. 数据预处理: 提取买卖价差、订单簿斜率、成交量冲击成本等20个特征。不做SMOTE,保持原始分布。
  2. 第一层——异常检测: 用孤立森林对每5分钟的数据窗口打分。异常分数超过阈值的,标记为「可疑窗口」。
  3. 第二层——代价敏感分类: 对可疑窗口内的数据,用XGBoost(scale_pos_weight=500)做二次判断。输出「黑洞概率」。
  4. 决策规则: 黑洞概率超过0.7,且连续3个窗口都触发,才发出预警。这样有效降低了假警报。

嗯,这套流程的核心思想就是:宁可多算几步,也不要漏掉一次。流动性黑洞一旦发生,损失是巨大的。多花点计算资源,值得。

最后说一句:样本不平衡没有「万能药」。SMOTE、代价敏感学习、异常检测,都是工具。关键是你得理解你的数据、你的业务场景。我在不同市场(美股、A股、加密货币)试过,效果差异很大。别照搬,多试,多调。

流动性黑洞预警——样本不平衡处理策略 原始数据 正常:500000 黑洞:127 SMOTE 人工合成少数类样本 适用:少数类样本>50 风险:可能生成噪声 代价敏感学习 调整损失函数权重 适用:代价可量化 优势:直接优化目标 异常检测 孤立森林/单类SVM 适用:少数类极少 优势:无需少数类样本 组合策略:异常检测初筛 + 代价敏感精调 降低假阳性,提高召回率

交易系统化学习资料 微信Strategy888888