halo 的技术博客

返回

你的模型在回测里夏普 2.0,上线三个月夏普变成 0.3。是过拟合?是策略衰减?还是——训练时看到的世界,和实盘正在面对的世界,已经不是同一个了?

第三种情况有一个专门的名字:分布漂移(distribution shift)。它的隐蔽之处在于,你的交叉验证、你的样本外测试、你的所有回测指标可能都很漂亮,因为它们都是在「历史那一片同质的分布」里切来切去。而实盘面对的是一个悄悄挪动了的分布。

有一个来自 Kaggle 竞赛圈、被严重低估的诊断工具,能直接量化「训练集和实盘数据到底像不像」——它叫对抗验证(Adversarial Validation)。本文讲清楚它的原理、代码,以及在量化里怎么用。

对抗验证原理示意

一、核心思想:让分类器去区分「训练集」和「测试集」#

先说结论,这个方法聪明得让人拍案:

把「你的训练数据」和「你要预测的新数据」混在一起,给训练数据打标签 0、新数据打标签 1,然后训练一个分类器去区分它们。如果分类器分不出来(AUC ≈ 0.5),说明两批数据同分布,模型可以放心迁移;如果分类器轻松分辨(AUC ≈ 1.0),说明两批数据分布已经漂移,你回测里的一切都要打问号。

逻辑链条是这样的:

  1. 如果训练集和测试集来自同一个分布,那么「一个样本属于训练集还是测试集」这件事本身是无法从特征预测的——它是纯随机的。分类器最多蒙对一半,AUC = 0.5。
  2. 如果分类器能显著超过 0.5,说明特征里藏着「能区分新旧数据」的信息,也就是说特征的分布本身变了

这个方法的美妙在于,它不需要你预先假设「哪个特征漂移了、漂移多少」。你把所有特征丢给分类器,它自己会告诉你答案——甚至告诉你是哪些特征在漂移。

二、最小可运行实现#

用法只有一行:

auc_mean, auc_std = adversarial_validation(features_2020_2024, features_2025)
print(f"对抗验证 AUC = {auc_mean:.3f} ± {auc_std:.3f}")
python

结果的解读表:

AUC 区间含义行动
0.50 ~ 0.60分布高度一致放心上线,回测可信
0.60 ~ 0.75存在中度漂移排查哪些特征漂移,考虑重训
0.75 ~ 0.90显著漂移回测可信度大打折扣,需处理
> 0.90严重漂移训练集几乎无参考价值,别上线

三、进阶:定位是「哪个特征」在漂移#

AUC 告诉你「漂移了」,但真正有用的是「是哪个特征漂移了」。因为不同来源的漂移,处理方式完全不同:

  • 成交量、波动率这类会随市场周期自然变化的特征漂移?——正常,可以用滚动标准化缓解。
  • 某个基本面因子因为会计准则变更、数据供应商换源而漂移?——这是数据污染,得修数据管道。
  • 你自己构造的某个衍生特征因为代码 bug 在新时期算错了?——这是最该抓出来的。

对抗分类器的特征重要性,直接就是「漂移贡献度排行榜」:

排在最前面的特征,就是让新旧数据「看起来不一样」的元凶。如果一个特征的漂移贡献极高,一个务实的做法是:直接把它从模型里剔除。 一个在训练和实盘间分布完全不同的特征,它在训练集学到的关系大概率无法迁移,留着只会误导模型。

四、完整演示:制造漂移并检测#

下面用合成数据完整走一遍:造两批数据,其中一批故意让部分特征漂移,看对抗验证能否抓出来。

典型输出:

整体 AUC = 0.887 ± 0.009

漂移贡献排行:
volatility    0.512
volume        0.446
pe_ratio      0.018
momentum      0.013
sentiment     0.011
plaintext

对抗验证精准地抓住了:整体 AUC 0.887 说明存在严重漂移,而漂移几乎全部来自 volatilityvolume 两个特征——正是我们故意动过手脚的那两个。pe_ratiomomentumsentiment 的贡献接近于零,因为它们确实没变。

特征漂移贡献度排行

五、量化里的三个实战用法#

1. 上线前的「体检」。 在把模型推上实盘前,用最近一段实盘期的特征(哪怕还没有 label)对训练集跑一次对抗验证。AUC 太高就别上——你的回测建立在一个已经消失的世界上。

2. 挑选「稳健」的验证集。 这是对抗验证最巧妙的用法。传统交叉验证是随机切分,但如果你的目标是「让模型在最像未来的数据上表现好」,那就应该用对抗分类器给每个训练样本打分——分数越高(越像测试集)的样本,越应该进验证集。这样你的验证集就成了「训练集里最接近未来分布的那部分」,验证指标更能预示实盘表现。

3. 特征筛选的守门员。 在把一个新因子加进模型前,单独看它的对抗验证贡献。如果它自己就能把新旧数据区分得很开,说明这个因子极不稳定,加进去很可能是「训练集里的过拟合,实盘里的噪声」。

六、局限与诚实的边界#

对抗验证很强,但它不是万能药,几个必须说清的边界:

1. 它只检测特征(X)的漂移,不检测「特征与收益的关系(X→y)」的漂移。 这是最大的误区。有可能特征分布完全没变(AUC = 0.5),但市场逻辑变了——同样的动量信号在牛市赚钱、熊市亏钱。这种「概念漂移」(concept drift)对抗验证抓不到,你需要的是滚动的 IC 监控、样本外的 label 表现追踪。

2. AUC 高不代表模型一定会失效。 有些漂移是无害的。比如成交量整体抬升,但你的模型用的是「成交量的相对排名」而非绝对值,那这个漂移对模型无影响。所以 AUC 只是警报,不是判决——它提示你去排查,而不是替你下结论。

3. 时间序列的特殊性。 金融数据天然非平稳,任何相隔较远的两段时间跑对抗验证,AUC 都会偏高,这是正常的市场演化。所以别追求 AUC = 0.5 的理想值,要建立自己的「基线」:用历史上「模型表现稳定」的相邻时期跑对抗验证,得到一个可接受的 AUC 范围,超过这个范围才拉警报。

4. 别用它做「数据窥探」。 如果你反复用测试集跑对抗验证、然后不断调整训练集去「骗过」分类器,你其实是在用测试集的信息污染训练流程——这又是一种变相的过拟合。对抗验证应该是一次性的诊断,不是可以反复优化的目标函数。

结语#

回测夏普 2.0 上线变 0.3,这件事发生时,大多数人的第一反应是「我过拟合了」,然后回去加正则、砍参数。但很多时候,真正的病因不在模型,而在数据——你训练的世界,已经不是你交易的世界。

对抗验证给你一把简单的尺子,去量化这个「世界的距离」。它不复杂,一个随机森林 + AUC 就能跑起来,但它能回答一个所有量化人都该反复问自己的问题:我凭什么相信,历史会以我训练时的样子重演?

对抗验证:用一个分类器检测训练集与实盘的分布漂移
https://blog.halo26812.eu.org/blog/adversarial-validation-drift
Author halo
Published at 2026年7月26日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨