机器学习进入量化选股已经十几年,但真正用机器学习跑出稳定超额收益的团队并不多。原因不是模型不够强,而是金融数据的信噪比太低:一个有效因子的 IC 只有 0.03,而同样的模型在图像、文本任务上可以轻易把准确率做到 95% 以上。在这个信噪比下,复杂模型的额外容量大部分用来拟合噪声。
真正难的地方在于模型越复杂,越需要纪律来约束它。金融数据没有「独立同分布」的假设,样本高度重叠、时序自相关、分布随时间漂移。直接把 Kaggle 的那套流程搬过来,几乎必然过拟合。
本文聚焦机器学习建模特有的方法论:特征、标签、时序交叉验证、PBO、再训练。线性因子的构造与检验在 因子研究与有效性检验 中讨论;回测偏差的通用陷阱属于研究方法论,本文只在与模型直接相关处引用。
目录
- 从线性因子到机器学习
- 特征工程与截面处理
- 标签构造与持有期设计
- 时序交叉验证与 purging
- 模型族选择与正则化
- 过拟合度量与 PBO
- 模型衰减与再训练节奏
- 特征重要性与可解释性
- 从模型输出到组合权重
1. 从线性因子到机器学习
线性多因子模型假设「收益 = Σ 因子暴露 × 因子收益 + 噪声」,它的优势是稳定、可解释、参数少。机器学习模型的吸引力在于捕捉线性和交互项:
线性: r = β₁x₁ + β₂x₂
非线性:r = f(x₁, x₂) # 例如「小市值 + 高动量」才有效,单独无效
但在低信噪比下,非线性模型的边际收益常常被它的过拟合风险吃掉。一个实用的判断顺序是:
- 先用线性模型建立基线,测出样本外 IC。
- 只有当非线性模型在严格样本外显著优于线性基线时,才采用。
- 如果提升小于 0.005 IC,直接放弃——这点提升不足以覆盖复杂度的维护成本。
线性模型是一个很强的基线,尤其在因子已经做过中性化和正交化之后。很多「机器学习有效」的报告,其实是没把线性基线调好。
2. 特征工程与截面处理
量化的特征是截面数据:每个时点有几千只股票,每只有几十个特征。处理方式与时间序列任务不同:
| 处理 | 目的 | 方法 |
|---|---|---|
| 去极值 | 抑制异常值 | MAD、分位数截断 |
| 截面标准化 | 统一量纲 | 截面 z-score 或排序 |
| 中性化 | 剥离行业市值 | 对行业哑变量与对数市值回归取残差 |
| 缺失填充 | 处理停牌/新股 | 行业中位数,或加缺失指示位 |
截面标准化必须逐时点做,不能用全样本统计量,否则就是前视偏差:
def cs_standardize(df, col, date_col='date'):
# 每个交易日单独做截面 z-score,绝不能用全样本均值
g = df.groupby(date_col)[col]
return (df[col] - g.transform('mean')) / g.transform('std')
按信息类型,特征可以分三类,它们的衰减速度差别很大:
| 类型 | 例子 | 衰减速度 | 常见处理 |
|---|---|---|---|
| 量价 | 动量、波动率、换手 | 快(天~周) | 滚动窗口 + 截面排序 |
| 基本面 | 估值、成长、质量 | 慢(月~季) | 公告日对齐 + 中性化 |
| 另类 | 舆情、供应链、卫星 | 中 | 事件对齐 + 去极值 |
特征设计上有一条重要原则:优先用「截面排序」而不是「原始值」。原始 PE 的分布随市场整体估值漂移,而 PE 的截面分位数是平稳的。模型的输入分布稳定,样本外的表现才稳定。
特征必须用公告日而非报告期对齐。财报的「报告期」是 2026Q3,但它在 10 月底才公告,如果用报告期索引,等于让模型在 7 月就看到了 9 月的数据——这是最典型的前视偏差。所有基本面特征都必须用公告日期索引,并在公告后延迟若干天(如 1~2 天)才可用,以覆盖数据供应商的入库延迟。
特征数量要克制。经验上特征数应远小于有效样本数:如果每个调仓期有 3000 只股票、10 年 2500 个交易日,看起来样本很多,但由于标签重叠(见下节),有效独立样本可能只有几百个。特征超过 50 个时,过拟合风险急剧上升。
3. 标签构造与持有期设计
标签是机器学习 Alpha 建模里最被低估的环节。最朴素的标签是「未来 N 日收益」,但它有几个问题:
def forward_return(close, horizon=5):
# 未来 5 日收益,注意 shift 方向:shift(-horizon) 用到了未来
return close.shift(-horizon) / close - 1
标签重叠(label overlapping) 是核心问题:如果持有期是 5 天,那么第 t 天的标签和第 t+1 天的标签共享了 4 天的收益,它们高度相关。这会让模型误以为有大量独立样本,严重高估置信度。
Lopez de Prado 的三重障碍法(Triple Barrier) 用「止盈线 / 止损线 / 时间上限」三个障碍定义标签,更贴近真实交易:
def triple_barrier(close, vol, horizon=5, pt=2.0, sl=2.0):
# 上障碍 = 止盈,下障碍 = 止损,垂直障碍 = 时间上限
labels = []
for i in range(len(close) - horizon):
upper = close[i] * (1 + pt * vol[i]) # 止盈价
lower = close[i] * (1 - sl * vol[i]) # 止损价
window = close[i+1 : i+1+horizon]
hit_up = (window >= upper).idxmax() if (window >= upper).any() else None
hit_dn = (window <= lower).idxmax() if (window <= lower).any() else None
# 哪个障碍先触发决定标签,都没触发则为 0(持有到期)
labels.append(0 if hit_up is None and hit_dn is None else
1 if hit_up is not None and (hit_dn is None or hit_up < hit_dn) else -1)
return labels
持有期(horizon)的选择直接由成本决定。持有期越短,样本越多、信号越强,但换手越高。用成本反推:
最小持有期 ≈ 往返成本 / 单日 alpha
往返成本 20 bp、单日 alpha 5 bp → 至少持有 4 天
先算成本,再定持有期,而不是先定持有期再看收益——顺序反了,策略必然被成本吃掉。
4. 时序交叉验证与 purging
标准 K-Fold 交叉验证在金融数据上完全错误。它随机划分样本,导致训练集里包含测试集时间点「之前和之后」的数据,而这些数据的标签与测试集重叠——等于用未来的信息预测过去。
正确做法是时序交叉验证,并且要做两件事:
| 技术 | 作用 | 做法 |
|---|---|---|
| Purging | 剔除训练集中与测试标签重叠的样本 | 去掉测试区间前后各 horizon 天的训练样本 |
| Embargo | 防止测试集信息泄漏到后续训练 | 测试集之后再加一段禁用的训练样本 |
def purged_splits(dates, n_splits=5, horizon=5, embargo=5):
"""返回 (train_idx, test_idx),train 已剔除与 test 标签重叠的部分"""
n = len(dates)
fold = n // (n_splits + 1)
for k in range(1, n_splits + 1):
test_start, test_end = k * fold, (k + 1) * fold
# 训练集 = 测试集之前的部分,去掉与测试标签重叠的 horizon 天
train_end = test_start - horizon
train_idx = list(range(0, max(train_end, 0)))
# embargo:测试集之后一小段也不参与本轮训练
test_idx = list(range(test_start, min(test_end, n)))
yield train_idx, test_idx
Combinatorial Purged CV(CPCV) 是更彻底的方案:把数据切成 N 段,每次取若干段做测试,遍历所有组合,得到多条「样本外」路径。它比单次 Walk-forward 提供更多的独立评估,代价是计算量。CPCV 的评估结果天然适合用于后面的 PBO 计算。
5. 模型族选择与正则化
模型族的选择要与信噪比匹配:
| 模型 | 容量 | 过拟合风险 | 适用 |
|---|---|---|---|
| 线性 + L1/L2 | 低 | 低 | 基线,首选 |
| 决策树 / GBDT | 中 | 中 | 捕捉交互 |
| 随机森林 | 中 | 中 | 稳健 |
| 深度神经网络 | 高 | 高 | 样本极多时 |
GBDT(如 LightGBM)是实践中较常用的中间选择,它捕捉非线性但比深度网络更不容易过拟合,训练也快:
import lightgbm as lgb
params = {
'objective': 'regression',
'num_leaves': 31, # 小叶子数,强正则
'max_depth': 5, # 浅树,防过拟合
'learning_rate': 0.02, # 低学习率 + 多轮
'min_child_samples': 500, # 每叶最少样本,关键正则
'subsample': 0.7,
'colsample_bytree': 0.7,
'lambda_l2': 1.0,
}
model = lgb.train(params, dtrain, num_boost_round=500)
关键正则参数是 min_child_samples(每叶最小样本数):金融数据里把它设大(几百到几千)能显著抑制过拟合。很多过拟合的模型都是默认参数(叶子太细)跑出来的。
集成多个弱模型往往比调一个强模型更稳:把线性、GBDT、随机森林的预测做截面排序后平均,样本外 IC 通常比任何单模型都高,且方差更小。
def ensemble_alpha(models, X):
# 各模型预测先转成截面分位,再平均,避免量纲与分布不一致
ranks = [pd.Series(m.predict(X)).rank(pct=True) for m in models]
return pd.concat(ranks, axis=1).mean(axis=1) # 集成分数
集成的关键是模型之间要有差异性:三个 GBDT 用同一套特征和同一套参数,集成几乎没有收益;一个线性、一个树模型、一个带不同特征子集的模型,才有互补价值。集成前应该检查模型预测之间的相关性,超过 0.95 的模型应该合并。
6. 过拟合度量与 PBO
回测出来的 Sharpe 是样本内的,它天然高估。判断一个策略(或模型配置)是否过拟合,要用概率化的度量。
PBO(Probability of Backtest Overfitting) 的定义是:在多个候选配置中,样本内表现最好的那个,在样本外表现落到中位数以下的概率。PBO 越高,说明你选出来的「最优配置」越可能只是运气。
计算 PBO 的标准方法是 CSCV(Combinatorial Symmetric Cross-Validation):把收益序列切成 S 段,枚举所有「一半做训练、一半做测试」的组合,统计「训练集最优」在测试集的排名:
from itertools import combinations
import numpy as np
def compute_pbo(returns_matrix, n_splits=16):
# returns_matrix: T×N,N 个候选配置的收益序列
S = n_splits
splits = np.array_split(np.arange(len(returns_matrix)), S)
logits = []
for train_combo in combinations(range(S), S // 2):
test_combo = [i for i in range(S) if i not in train_combo]
tr = np.concatenate([splits[i] for i in train_combo])
te = np.concatenate([splits[i] for i in test_combo])
best = np.argmax(sharpe(returns_matrix[tr]).mean(axis=0)) # 训练集最优
rank = rank_of(sharpe(returns_matrix[te]).mean(axis=0), best)
logits.append(np.log(rank / (1 - rank))) # 排名 → logit
return (np.array(logits) <= 0).mean() # 样本外排名落入后 50% 的比例
PBO 的经验阈值:低于 0.5 是基本要求,低于 0.3 才比较放心。如果 PBO 超过 0.5,说明你的「最优配置」比随机选还差。
与 PBO 配套的是 Deflated Sharpe Ratio(DSR):它把「试了多少次」作为惩罚项,把样本内 Sharpe 折算成考虑了多重检验的显著性。DSR 的完整推导属于回测方法论,这里只强调结论:报告 Sharpe 时必须同时报告「试了多少个配置」,否则这个 Sharpe 没有意义。
7. 模型衰减与再训练节奏
模型上线后性能会衰减,原因有三:市场结构变化、因子拥挤、过拟合的均值回归。
def monitor_decay(realized_ic, window=60):
rolling = realized_ic.rolling(window).mean()
baseline = realized_ic[:250].mean() # 上线初期的 IC 作为基线
if rolling.iloc[-1] < baseline * 0.5:
return 'RETRAIN' # 衰减过半,触发再训练
if rolling.iloc[-1] < 0:
return 'HALT' # IC 转负,暂停策略
return 'OK'
再训练的节奏要平衡「跟踪市场」与「引入噪声」:
| 频率 | 优点 | 缺点 |
|---|---|---|
| 每日 | 最快适应 | 引入大量噪声,不稳定 |
| 每月 | 平衡 | 常见选择 |
| 每季 | 稳定 | 对结构变化反应慢 |
| 事件触发 | 有针对性 | 阈值难设 |
实践中常用月度重训 + 事件触发:每月用滚动窗口(如最近 3~5 年)重训一次,同时在 IC 衰减超过阈值时提前触发。重训后不要立刻全量切换,应该新旧模型并行一段时间,比较实盘 IC 再决定切换。
重训窗口长度本身是个参数:窗口太短,模型对近期行情敏感但不稳定;窗口太长,包含已经失效的旧市场结构。经验上窗口应该覆盖至少一个完整的牛熊周期(如 3~5 年),并做窗口长度的敏感性测试——如果模型表现对窗口长度极度敏感,说明它在拟合特定时段,不可信。
在线学习(online learning) 在金融上要谨慎:市场数据的分布漂移不是平稳的,增量学习容易把近期的异常当成新常态。用滚动窗口重训比在线更新更可控。
8. 特征重要性与可解释性
特征重要性是判断模型「学到了什么」的关键工具,但常用的重要性度量有陷阱:
| 方法 | 原理 | 陷阱 |
|---|---|---|
| MDI(分裂增益) | 树的分裂贡献 | 高基数特征被高估 |
| MDA(置换重要性) | 打乱特征看性能下降 | 相关特征互相稀释 |
| SHAP | 博弈论分摊 | 计算贵,相关特征归因不稳 |
from sklearn.inspection import permutation_importance
def mda(model, X_test, y_test, n_repeats=10):
# 置换重要性:打乱某列后看预测误差上升多少
r = permutation_importance(model, X_test, y_test,
n_repeats=n_repeats, random_state=0)
return dict(zip(X_test.columns, r.importances_mean))
相关特征会稀释重要性:如果 x₁ 和 x₂ 高度相关,打乱 x₁ 时模型可以用 x₂ 补偿,x₁ 的重要性被低估。解法是把相关特征分组,按组做置换,而不是单列。
更重要的判断是特征的稳定性:一个特征在训练窗口里重要,在下个窗口里消失,说明它是噪声。应该统计特征重要性在不同训练窗口间的排名相关性,只有排名稳定的特征才值得保留。
9. 从模型输出到组合权重
模型的输出是「预测收益」或「排序分数」,它还不是组合权重。从预测到权重需要几步变换:
def prediction_to_weights(pred, prev_w, cov, max_weight=0.03):
# 1. 截面标准化,抑制极端预测
z = (pred - pred.mean()) / pred.std()
# 2. 截断,防止个别股票权重过大
z = z.clip(-3, 3)
# 3. 转成预期收益尺度
mu = z * 0.005
# 4. 交给组合优化(见组合优化与风险模型)
return optimize(mu, cov, prev_w, max_weight)
模型输出必须先做截面标准化和截断。机器学习模型的预测分布往往有厚尾,直接当预期收益喂给优化器,会让组合在少数几只「模型最看好」的股票上重仓——而这些恰恰可能是模型过拟合最严重的样本。
模型的输出还应该与线性因子组合而不是替代:把 ML 预测和线性因子合成后的分数做加权,通常比纯 ML 更稳。ML 的边际价值在于捕捉线性因子遗漏的交互,而不是推翻线性因子。
最终这一步的优化与约束,在 组合优化与风险模型 中有完整讨论。建模的产出不是权重,而是「一组经过样本外验证的预测」,权重生成是另一个独立环节。
权衡取舍
| 维度 | 线性模型 | GBDT | 深度网络 |
|---|---|---|---|
| 样本需求 | 低 | 中 | 极高 |
| 过拟合风险 | 低 | 中 | 高 |
| 捕捉非线性 | 无 | 有 | 强 |
| 可解释性 | 高 | 中 | 低 |
| 训练成本 | 低 | 中 | 高 |
模型复杂度必须与「有效样本数」匹配,而不是与「总样本数」匹配。因为标签重叠,10 年的日频数据有效独立样本可能只有几百个,这决定了模型容量应该很低。实践中线性 + 少量 GBDT 集成是性价比最高的组合,深度网络只有在拥有另类数据(文本、图像)带来的高维、低相关特征时才有优势。
再训练节奏上也有取舍:重训太频繁会追逐噪声,太稀疏会错过结构变化。折中做法是固定月度重训 + 监控 IC 触发,并把「新旧模型并行期」作为标准流程,用实盘数据而非回测决定是否切换。
常见坑清单
- 用标准 K-Fold 做时序验证:随机划分导致标签重叠泄漏,样本外虚高。
- 全样本截面标准化:用了未来均值,改用逐日截面标准化。
- 忽略标签重叠:把高度相关的样本当独立样本,高估显著性。
- 持有期短于成本回收期:先定持有期再看收益,必然被成本吃掉。
- min_child_samples 用默认值:叶子过细,模型拟合噪声。
- 只报 Sharpe 不报尝试次数:多重检验未校正,Sharpe 无意义。
- 不做 PBO 检验:无法知道「最优配置」是不是运气。
- 用 MDI 看重要性:高基数特征被高估,改用分组置换。
- 每日重训:追逐噪声,模型不稳定。
- 预测直接当预期收益:厚尾未截断,组合在极端预测上重仓。
小结
机器学习 Alpha 建模的核心不是模型,而是纪律:标签要防重叠、验证要防泄漏、模型要控容量、过拟合要量化、再训练要有节奏。在信噪比极低的金融数据上,任何一处纪律的缺失都会被模型的容量放大成样本外的崩塌。
判断一个 ML 策略是否可信,最快的自检是三个问题:它的样本外 IC 是否显著高于同特征的线性基线?PBO 是否低于 0.3?特征重要性在多个训练窗口间是否稳定? 三个问题有一个答不上来,就不该上实盘。
下一步建议阅读 组合优化与风险模型 ,看经过样本外验证的预测如何转化为权重;如果你还没建立线性因子的检验流程,先回到 因子研究与有效性检验 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。