跳到正文

交叉验证与诚实评估

随机切分会把量化策略的表现高估三到四倍。这篇讲怎么把那三四倍挤出去,以及挤完之后还剩下什么。

第 4 篇 · 共 8 篇 约 14 分钟

交叉验证:策略们死去的地方

大多数「前景光明」的 ML 结果死在交叉验证里。训练准确率很高,验证准确率平庸,测试准确率是碰运气。研究死掉,数据集进抽屉,研究生毕业。这是正常结果。

这不是悲观主义,这是机器学习研究的经验现实。通过严格交叉验证的策略是稀有的,这也是为什么它们的作者私藏着。

本文讲的是怎么老老实实做交叉验证,尤其是对金融时间序列。这里有个贯穿全文的模式值得先说:下面每一层方法——walk-forward、purging、embargo、CPCV、PBO、Deflated Sharpe——都会让你的表现估计变小。每一层都对应一种你之前在骗自己的具体方式。如果你加了一层之后数字没掉,八成是这层没装对。

标准 K 折,以及它为什么在别的领域好使

教科书版本很简单:把数据切成 kk 个等大的折;对每一折 ii,在剩下的 k−1k-1 折上训练、在第 ii 折上评估、记下指标;最后把 kk 个指标平均,这就是你的交叉验证表现估计。

k=5k = 5 或 k=10k = 10 时,它给出模型在同分布新数据上表现的稳健估计,是学术 ML 的主力。它之所以稳,是因为每个样本恰好当一次测试样本、当 k−1k-1 次训练样本;训练集之间大量重叠,让每折的指标彼此相关但平均值稳定。在独立同分布的数据下,kk-fold CV 近似期望测试误差。

注意”独立同分布”这五个字。金融数据两条都不满足,而这就是下一节的全部内容。

为什么它在量化里会骗你

在量化金融里,标准 kk-fold CV 比无用更糟——它主动误导。三个理由,一个比一个隐蔽。

最明显的是随机切分抹掉了时间顺序。随机切分会把一部分 2023 年的样本放进训练集、另一部分 2023 年的样本放进测试集,于是模型在训练时见过 2023 年的 pattern,又在同一批 pattern 上被评估。部署时它面对的不是这个局面——它面对的是从没见过的未来。

第二层是未来信息泄漏,而且它躲得过”按日期排序”这道防线。就算你先排序再做随机折,自相关的特征和重叠的窗口意味着模型实际上还是看到了未来:昨天的滞后特征跟今天的特征长得几乎一样。模型学到的 pattern 跟测试集高度相关,这是构造上的必然,不是运气不好。

第三层是时间序列自身的聚类结构。资产收益、波动率 regime、因子收益都在时间上扎堆,随机切分因此制造出”漏水”的折——模型在训练和测试里看到的是相邻的日子,它的测试预测本质上是插值而不是外推。一个靠插值得到的乐观估计跟部署没有任何关系。

后果有多大?拿 20 年美股数据训一个收益预测模型,同一个模型、同一批数据,朴素随机 5 折会给出 Sharpe 估计 2.0(过拟合加泄漏),walk-forward 给出 0.6(现实),purged walk-forward 给出 0.5(更接近真实部署)。随机 CV 和 walk-forward 之间三到四倍的差距是典型量级。如果你一直在用随机 CV 评估量化策略,你把表现高估了三到四倍。这不是个小效果。

(Hastie, Tibshirani, Friedman, The Elements of Statistical Learning, 第 2 版, 第 7 章。ISBN 978-0387848570。)

(López de Prado, M., Advances in Financial Machine Learning, 第 11–12 章。ISBN 978-1119482086。)

先确认一下你的数据到底平不平稳

上面第三条理由背后有个更基本的问题:很多 ML 方法隐式假设平稳性,而非平稳序列的统计性质(均值、方差、自相关)会随时间变。所以在你信任何时间序列 CV 结果之前,先检验一下。

常用的三个检验分两个方向。增广 Dickey-Fuller(ADF)检验单位根,零假设是序列非平稳,所以你要拒绝零假设才能支持平稳性。KPSS 正好反过来,零假设是序列平稳,所以你要不能拒绝才支持平稳性。Phillips-Perron 跟 ADF 类似,对自相关更鲁棒。两个方向的检验一起用是好习惯,因为它们在样本量不够时都容易给不出结论。

对资产收益,ADF 通常能拒绝零假设——收益大致是平稳的。对价格本身、波动率水平、以及大多数宏观变量的水平序列,ADF 通常拒绝不了,这些序列得当作非平稳来处理。

这对 CV 的含义很直接:只要你的特征或目标里有非平稳的成分,walk-forward 就不是可选项而是必需品。随机 kk-fold 会给你一个乐观有偏的估计,因为随机切分把不同 regime 混在一起做了平均。

修法第一层:walk-forward

对时间序列诚实的版本是 walk-forward 验证:训练窗口随时间往前推,每次预测下一个时期。

折 1:train [2000–2010], 预测 2011
折 2:train [2000–2011], 预测 2012
折 3:train [2000–2012], 预测 2013
...
折 k:train [2000–2019], 预测 2020

每一折只用过去的数据训练、只用未来的数据评估。没有未来泄漏,每折的测试集是真正被 holdout 的。

三个变体对应三种关于数据的信念。扩展窗口让训练集随时间增长,适合你相信模型能从更多数据受益、底层分布也还算稳定的时候。滚动窗口把训练集固定成最近 NN 个时期,适合你相信近期数据更相关、底层分布在漂移的时候。阻塞时间序列切分(Hyndman & Athanasopoulos)跟 walk-forward 类似,但在 train 和 test 之间放一个显式的 gap block。扩展窗口是稳妥的默认;只有当你有具体理由相信老数据已经 stale 时才换成滚动窗口。

修法第二层:purge 和 embargo

walk-forward 挡住了时间顺序上的泄漏,挡不住标签重叠造成的泄漏。López de Prado (2018) 的两个补丁就是干这个的。

Purging 是在训练集和测试集之间挖一个 gap。如果你的目标是 5 日前瞻收益、测试折在时间 TT 结束,那任何标签跟测试折标签重叠的训练样本都必须去掉;否则模型见过一个输入,而它的标签跟某个测试输入的标签是相关的。purge 窗口的大小等于标签 horizon:预测 5 日前瞻收益,train 和 test 之间至少 purge 5 天。

听起来简单,实现有微妙之处,因为该被 purge 的不只是显然重叠的标签。还有三类容易漏掉的:跟测试标签重叠的特征——如果某个特征本身就是一个前瞻聚合(比如在时间 TT 算的 5 日滚动波动率),而测试标签是从 T+1T+1 开始的 5 日收益,那这个特征跟测试标签相关,得 purge 掉;样本权重——如果样本带权重(比如按 inverse volatility),要确认权重的计算没有跨过 train-test 边界;横截面标签——在面板设置里,股票 ii 在时间 tt 的标签可能通过市场因子跟股票 jj 在同一时刻的标签相关,所以要横截面地 purge,不只是沿时间维度。Arithmion SDK 的 purged_cv() 自动处理基本的时间 purge,复杂的横截面设置需要人工审查。

Embargo 是 purge 的反方向补丁:在测试集之后再从训练集里扣掉一段。原因是测试集里用到的特征可能跟后续的标签相关,造成一条绕道的泄漏路径。举个具体的:测试折是某个月的最后 5 天、特征滞后 1 天,那你的测试预测依赖 T−4T-4 到 TT 的特征;这些特征又跟 T−4T-4 到 T+4T+4 的标签相关(因为标签跨 5 天);于是如果你拿 T+1T+1 到 T+4T+4 的数据去训下一个模型,你就用了跟测试标签相关的数据。embargo 定的就是这个 gap 的大小,典型取数据集大小的 1% 到 5%,具体值取决于你特征的自相关结构。

修法第三层:CPCV,把一条路径变成一堆

对 20 年日度数据、5 日前瞻收益,walk-forward 给你 20 个年度样本外估计。这不算多——20 个数你连个像样的置信区间都算不出来。

Combinatorial Purged CV(López de Prado, AFML §12.4)的想法是不要只走一条 walk-forward 路径,而是生成很多条:取所有 kk 个测试块穿过时间的组合(每个周围都 purge),产生 (Nk)\binom{N}{k} 个不同切分。这里有个容易被写错的细节——真正能跨时间 trace 出来的有效路径数量是 φ[N,k]=kN(Nk)\varphi[N,k] = \frac{k}{N}\binom{N}{k},不是全部的二项式计数。在这些路径上平均指标,你拿到的就不是一个点估计而是一个分布,置信区间也就有了。

代价是算力。N=20N = 20 年、k=5k = 5 个测试块时有 (205)=15,504\binom{20}{5} = 15{,}504 个切分,其中 φ[20,5]=3,876\varphi[20,5] = 3{,}876 条是有效路径。把块切细一点更夸张:S=16S = 16 块、k=8k = 8 个测试块,有 (168)=12,870\binom{16}{8} = 12{,}870 个切分和 φ[16,8]=816(168)=6,435\varphi[16,8] = \frac{8}{16}\binom{16}{8} = 6{,}435 条有效回测路径,每条路径一次模型拟合。对一个快的梯度提升模型这是几小时,对神经网络是几周。

所以实践中大部分团队跑子样本:随机抽 200 条路径,在每条上跑模型,平均指标。这给你一个稳健估计而不用付完整的组合成本。

三个数据集,和那条唯一的硬规则

每一次诚实的 ML 评估都要用三个不相交的数据集。Train 用来拟合模型参数,模型看这些数据并据此更新权重。Validation 用来调超参——早停、正则化强度、架构选择——模型不在它上面训练,但你用它做决策。Test 用来做最终评估,在最后碰恰好一次,绝不参与模型开发中的任何决策。

由此得到这个领域里唯一一条不容商量的规则:如果你碰测试集超过一次,它就不再是测试集了。它是你训练过程的一部分,你报的表现往上偏了。

这条比听起来难守得多。每一次你看了测试表现然后改了模型的什么东西,你就碰了它。每一次你因为测试表现不好而换个随机种子,你碰了。每一次你因为测试表现而加或减了一个特征,你碰了。这里需要的纪律是真的难,而且没人会替你监督。

测什么

指标要跟着任务走。做回归你会看 MSE(重罚大误差)、MAE(对异常值更鲁棒)、以及 R2R^2(解释的方差比例)。做分类你会看 accuracy(正确的比例)、precision 和 recall(类别不平衡时有用)、F1(两者的调和平均)、AUC(ROC 曲线下面积,跟阈值无关)、以及 log-loss(正当的评分规则,重罚自信的错误)。

量化有自己的一套,而且它们才是你最后真正要报的东西。Sharpe ratio 是年化收益除以年化波动率;信息系数(IC)是预测和已实现收益的秩相关;胜率是方向正确的预测比例;最大回撤是最差的峰谷损失;Calmar ratio 是收益除以最大回撤。另外两个专门用来对付「你试了很多次」这件事:Deflated Sharpe Ratio(DSR)是按多重检验调整后的显著性(AFML §14.7.3),Probability of Backtest Overfitting(PBO)通过 Combinatorially Symmetric CV / CSCV 计算(AFML §11.6)。这两个下面单独讲。

对信号评估,IC 和秩相关是最诚实的指标。它们测的是你的预测跟结果是否同向排序,而这正好是一个交易信号真正需要的东西。MSE 和 R2R^2 在收益数据上常常误导,因为分布有肥尾——几个尾部样本就能主宰整个数字。

嵌套交叉验证

当你既要调超参、又想要无偏评估时,你需要嵌套 CV:外层估计泛化,内层调超参,两者分开。

外层:
  对每个外折(测试集):
    在 outer-train 上的内层:
      对每个内折(验证集):
        在 inner-train 上训练,在 inner-validation 上评估
        选最好的超参
      用最好的超参在 outer-train 上重训
    在 outer-test 上评估

代价是 kouter×kinnerk_{\text{outer}} \times k_{\text{inner}} 次模型拟合。kouter=kinner=5k_{\text{outer}} = k_{\text{inner}} = 5 是 25 次,k=10k = 10 是 100 次。boosting 和线性模型做得起,神经网络很贵。

嵌套 CV 是无偏评估的金标准。如果你要报一个会被部署的模型,跑嵌套 CV,同时报内层 CV 表现(训练估计)和外层 CV 表现(部署估计)——两者之间的差距直接告诉你超参调优过拟合了多少。这个差距本身就是一个诊断量,别只报其中一个。

你试过多少次,决定了你该信多少

前面每一层修的都是”数据怎么切”。这一层修的是另一件事:你在这份数据上试了多少次。

Deflated Sharpe Ratio(López de Prado, 2014,完整论述在 AFML §14.7.3)按三件事调整一个观测到的 Sharpe:你试过的策略数、收益分布的偏度和峰度、以及策略之间的相关性。它计算的是在”真技能为零”的零假设下、观测到这么高的 Sharpe 的概率——所以 DSR 输出的是一个概率而不是一个折算过的 Sharpe 数值,别把它当成”打折后的 Sharpe”来读。要点很简单:100 次试验下的一个 Sharpe 2.0 主要是运气,而 DSR 是把这句话量化出来的那个数。Arithmion SDK 暴露 deflated_sharpe_ratio() 来算它。

Probability of Backtest Overfitting(PBO)是同一个问题的另一面:一个通过回测选出来的策略,部署后跑输它样本内基准的概率是多少。计算走 Combinatorially Symmetric Cross-Validation(CSCV):把数据切成 SS 块(SS 通常取偶数),形成全部 (SS/2)\binom{S}{S/2} 个 train/test 划分——在 S/2S/2 块上训练、在剩下的 S/2S/2 块上测试,等半划分,没有 TT 对 S−TS-T 的不对称,也不是 2S−12^S - 1 的枚举。对每个划分,记录按样本内表现会被选为「最佳」的那个策略,然后看它在样本外的排名。PBO 就是这个样本内最佳策略掉到测试集策略中位数以下的概率——具体做法是构建一条把样本内排名跟样本外 logit 排名对起来的校准曲线,再报基于 logit 的跑输概率。它不是简单的「测试 Sharpe 为负的划分比例」,这个近似会给你错的数。

PBO 超过 0.5,你的回测多半会样本外失败。大部分已发表的量化策略 PBO 落在 0.6 到 0.9 之间。这是诚实回测评估里单一最有信息量的诊断;算不出来它,你就是在蒙眼飞。Arithmion SDK 暴露 pbo(returns_matrix, n_blocks=16)。

给估计配一个区间

交叉验证给你的是一个点估计,不是置信区间。Bootstrap 是补上区间最直接的办法:在训练数据的 BB 个不同 bootstrap 样本上各训一次模型,每个模型在固定测试集(或 OOB 样本)上评估,这 BB 个测试指标的分布就是你的区间。B=100B = 100 给你一个粗略的区间,B=1000B = 1000 给你一个稳定的。它对大多数指标(Sharpe、MSE、胜率)都适用,也不需要参数假设。

时间序列要用更讲究的版本:block bootstrap。不抽单个样本,抽连续的 block,这样 block 内部的时间相关结构被保留下来。日度金融数据用 5 到 21 天的 block size。

block bootstrap 配 walk-forward CV,你得到的是 walk-forward Sharpe 估计的置信区间。而这个区间回答的是那个最该问的问题:这个策略是「大概率盈利」,还是「就在噪声里」。

然后还要再打一次折

交叉验证的试金石是:walk-forward 估计跟实盘表现相关吗?

经验上,弱相关。Walk-forward 估计比随机 CV 估计好得多,但在很多情况下它仍然把表现高估 30% 到 60%。原因不难理解——walk-forward 假设未来会像过去,而当过去里包含一个已经不存在的 regime(或者反过来,未来出现了一个过去没有的),它就会高估。

这个结论是清醒剂:即使有了严格的 CV,你也应该预期实盘 Sharpe 大约是 walk-forward Sharpe 的一半。walk-forward 说 1.5,你按 0.7 到 1.0 去做计划。有些团队干脆把 walk-forward 估计按一个固定因子(比如 0.5×)打折当启发式用;这不令人满意,但比不打折实用。正确做法是跟踪实盘表现、随时间重新校准这个折扣。

报告的时候把这些一起给出来

如果你希望你的研究被信任,报 walk-forward Sharpe 而不是样本内 Sharpe,或者两个都报、清楚标注哪个是哪个。同时报出各个 walk-forward 折的 Sharpe 的标准差——如果标准差大于均值,那个结果就是噪声,别再往下解释了。报按试过的策略数调整过的 Deflated Sharpe Ratio,报 PBO(高于 0.5 就说明回测很可能过拟合),有实盘表现就把它连同覆盖的时间段一起报。

还有两条是关于你自己而不是关于模型的:披露你试了多少次,因为试了 50 个策略只报最好的那个,Deflated Sharpe Ratio 会把这件事反映出来;以及披露什么失败了,你试过而没 work 的策略是这幅诚实图景的一部分。

很少有已发表的量化论文把这些全做到。做到的那些通常是对的,做不到的那些通常是错的。

在你信自己报的表现之前,把这张表过一遍:

  • 我是不是看测试表现超过一次了?
  • 我是不是因为测试表现不好改了模型?
  • 我是不是因为测试表现加或减了特征?
  • 我是不是换了不同随机种子直到测试表现变好?
  • 我报的是单次测试运行的结果,还是多次的平均?
  • 我用的是 walk-forward 验证,还是随机 kk-fold?
  • 我有没有 purge train 和 test 之间的 gap?
  • 我有没有按试过的策略数做调整?

前四个任何一个答「是」,你报的表现是过拟合的。后四个任何一个答「否」,你可能在欠验证。大部分已发表的量化 ML 论文违反其中一条或多条,而读者看不出来,因为作者不披露他们试过什么。

(Bishop, Pattern Recognition and Machine Learning, 第 1、3 章。ISBN 978-0387310732。)

Arithmion SDK 里的对应工具

  • walk_forward_cv(model_fn, X, y, n_splits=10)——标准扩展窗口 walk-forward。
  • purged_cv(model_fn, X, y, n_splits=10, embargo_pct=0.01)——带 embargo 的 purged walk-forward。
  • cpcv(model_fn, X, y, n_blocks=16, k_test_blocks=8, n_subsample=200)——Combinatorial Purged CV。
  • deflated_sharpe_ratio(sharpe, n_trials, skewness, kurtosis)——按试验次数调整的显著性。
  • pbo(returns_matrix, n_blocks=16)——Probability of Backtest Overfitting。
  • block_bootstrap_ci(metric_fn, X, y, n_bootstrap=1000, block_size=21)——bootstrap 置信区间。

这些是对的工具。不带它们别上线策略。

过滤器串起来之后

现在把全文的层次串一遍,你会看到一个残酷的漏斗:大多数「前景光明」的策略过不了 walk-forward 验证;过了 walk-forward 的大多数过不了 purged walk-forward;过了 purged walk-forward 的大多数过不了 PBO 分析;而过了 PBO 分析的策略,在实盘交易里还是有相当一部分跑输。每一层筛掉五成到九成的候选。

这就是为什么本文开头说”死掉是正常结果”。也是为什么通过全部过滤器的策略稀有到它们的作者会私藏。

好消息是,这套东西是可以学会的技能,而且它不炫——它不涉及任何聪明的算法,只涉及纪律:不信单个回测,不碰测试集第二次,不对时间序列用随机切分,不报没按多重检验 deflate 过的表现,不在没有外部验证的情况下信自己的结果。算法存在,数据存在,算力存在。稀缺的是愿意放弃那些没通过诚实评估的想法。

纪律才是 edge,其他都是大宗商品。

参考文献

  • Hastie, Tibshirani, Friedman, The Elements of Statistical Learning, 第 2 版, 第 7 章。ISBN 978-0387848570。
  • Bishop, C. M., Pattern Recognition and Machine Learning, 第 1、3 章。ISBN 978-0387310732。
  • López de Prado, M., Advances in Financial Machine Learning。第 7 章(金融中的交叉验证)、§11.6(Combinatorially Symmetric Cross-Validation / CSCV,用于 PBO 估计)、§12.4(Combinatorial Purged Cross-Validation / CPCV)。ISBN 978-1119482086。
  • López de Prado, M. (2014). “The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting and Non-Normality.” 完整推导在 AFML §14.7.3。
  • Aronson, D., Evidence-Based Technical Analysis, 第 9–12 章。ISBN 978-0-470-00874-4。
  • Hyndman, R. J., Athanasopoulos, G., Forecasting: Principles and Practice, 第 3 章。