2. 过拟合检测与预防:交叉验证、学习曲线、正则化在量化策略中的应用
做量化策略最怕什么?不是策略亏钱,而是策略在回测里赚得飞起,一上实盘就崩。说白了,这就是过拟合在作祟。我见过太多人拿着一条完美拟合历史数据的曲线,兴奋地跟我说“找到圣杯了”。嗯,结果往往很惨。
今天我们就来聊聊,怎么在量化策略里检测并预防过拟合。我会结合自己踩过的坑,把交叉验证、学习曲线、正则化这三板斧讲透。
2.1 过拟合:量化策略的隐形杀手
先说说什么是过拟合。你想想看,一个策略在历史数据上表现完美,年化50%,最大回撤只有3%。这听起来是不是很诱人?但问题在于,它把历史数据里的噪声也学进去了。
举个例子。我有个朋友,他写了个策略,专门在每月15号买入,20号卖出。回测下来收益惊人。后来发现,这个规律只在某几年有效,因为那几年每月中旬都有利好政策。政策变了,策略就失效了。这就是典型的过拟合——模型记住了特定时间段的噪声,而不是真正的市场规律。
核心观点:过拟合的策略就像背答案的学生,换一套题就懵了。我们要的是真正理解知识,而不是死记硬背。
2.2 交叉验证:别让数据骗了你
交叉验证是检测过拟合最直接的方法。但量化数据是时间序列,不能像普通机器学习那样随机打乱。你想想看,用未来的数据去训练模型,再用过去的数据去验证,这不就穿越了吗?
我个人习惯用两种方法:
2.2.1 滚动时间窗口交叉验证
把数据按时间顺序切分成多个窗口。比如你有10年数据,可以这样切:
# 滚动时间窗口交叉验证示例
import pandas as pd
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
# 假设我们有10年日频数据
data = pd.DataFrame(index=pd.date_range('2010-01-01', '2020-01-01', freq='D'))
data['return'] = np.random.randn(len(data))
# 设置5折交叉验证
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(data):
train_data = data.iloc[train_idx]
test_data = data.iloc[test_idx]
# 在这里训练和验证你的策略
print(f"训练集: {train_data.index[0]} 到 {train_data.index[-1]}")
print(f"测试集: {test_data.index[0]} 到 {test_data.index[-1]}")
这样做的好处是,每次验证用的都是未来的数据。如果策略在多个时间窗口上表现都稳定,那说明它抓住了真正的规律。
2.2.2 组合交叉验证
还有一种更严格的方法,叫组合交叉验证。把数据分成K段,每次用K-1段训练,剩下1段验证。但注意,训练集必须全部在验证集之前。
我的经验:我曾经用5折滚动交叉验证测试一个趋势跟踪策略。前4折表现都不错,但第5折(最近两年)直接崩了。后来发现是市场风格变了,趋势策略失效了。交叉验证帮我提前发现了这个问题,避免了实盘亏损。
2.3 学习曲线:看你的模型学得怎么样
学习曲线能直观地告诉你,模型是欠拟合还是过拟合。说白了,就是看训练误差和验证误差随着数据量增加的变化趋势。
我一般画两条线:一条是训练集上的表现,一条是验证集上的表现。然后看它们之间的距离。
# 学习曲线绘制示例
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
# 假设我们有一个策略模型
def plot_learning_curve(model, X, y):
train_sizes, train_scores, test_scores = learning_curve(
model, X, y, cv=5,
train_sizes=np.linspace(0.1, 1.0, 10)
)
train_mean = np.mean(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, label='训练集表现')
plt.plot(train_sizes, test_mean, label='验证集表现')
plt.xlabel('训练样本数量')
plt.ylabel('策略表现(如夏普比率)')
plt.legend()
plt.title('学习曲线分析')
plt.show()
怎么看这个图?
- 两条线靠得很近,且都高:模型表现好,没有过拟合。恭喜你!
- 训练线很高,验证线很低:典型的过拟合。模型把训练数据背下来了,但没学会泛化。
- 两条线都很低:欠拟合。模型太简单,连训练数据都没学好。
避坑指南:我曾经犯过一个错误,只看训练集表现就以为策略很好。结果学习曲线一画,验证集表现惨不忍睹。记住,永远不要只看训练集的结果。
2.4 正则化:给模型加点约束
正则化是防止过拟合的利器。它的核心思想很简单:在损失函数里加一个惩罚项,让模型不要学得太复杂。
在量化策略里,我常用两种正则化方法:
2.4.1 L1正则化(Lasso)
L1正则化会让一些特征的权重变成0。说白了,就是帮你做特征选择。哪些因子没用,它直接帮你砍掉。
# L1正则化在因子选择中的应用
from sklearn.linear_model import Lasso
# 假设我们有100个候选因子
X = np.random.randn(1000, 100) # 1000个样本,100个因子
y = np.random.randn(1000) # 目标收益率
# 使用Lasso进行因子选择
lasso = Lasso(alpha=0.01) # alpha控制正则化强度
lasso.fit(X, y)
# 查看哪些因子被选中(权重不为0)
selected_factors = np.where(lasso.coef_ != 0)[0]
print(f"从100个因子中选出了{len(selected_factors)}个有效因子")
2.4.2 L2正则化(Ridge)
L2正则化不会把权重变成0,但会让权重变得很小。它适合那些你觉得所有因子都有用,但不想让任何一个因子权重太大的情况。
# L2正则化在组合优化中的应用
from sklearn.linear_model import Ridge
# 假设我们有10个策略的收益率序列
strategy_returns = np.random.randn(1000, 10)
target_return = np.random.randn(1000)
# 使用Ridge回归分配权重
ridge = Ridge(alpha=0.1)
ridge.fit(strategy_returns, target_return)
# 得到的权重更均衡,不会过度依赖某个策略
weights = ridge.coef_
print(f"策略权重分布: {weights}")
我的建议:正则化参数alpha怎么选?我一般用网格搜索配合交叉验证。从0.001到100,按对数尺度取10个值,看哪个在验证集上表现最好。别偷懒,这个步骤值得花时间。
2.5 知识体系总览
下面这张图总结了过拟合检测与预防的核心逻辑。你可以看到,检测和预防是相辅相成的。
2.6 实战中的避坑指南
说了这么多理论,最后分享几个实战中容易踩的坑。
- 不要用未来数据做交叉验证。我见过有人用随机打乱的数据做交叉验证,结果策略在“未来”数据上表现很好。这完全是作弊。
- 正则化不是万能的。alpha值设太大,模型会欠拟合;设太小,又防不住过拟合。需要耐心调参。
- 学习曲线要结合业务看。有时候验证集表现差,不一定是过拟合,可能是市场结构发生了变化。
我曾经踩过的坑:有一次我写了一个多因子策略,用了100多个因子。回测表现很好,但实盘就是不行。后来用L1正则化一跑,发现真正有效的因子只有5个。其他95个全是噪声。从那以后,我每次做策略都会先跑一遍正则化,看看哪些因子是真正有用的。
好了,关于过拟合检测与预防,今天就聊到这里。记住,量化策略的核心不是拟合历史,而是预测未来。交叉验证、学习曲线、正则化,这三样工具用好了,你的策略才能真正经得起市场的考验。
公众号:蓝海资料掘金营,微信deep3321