机器学习 Alpha 建模

机器学习 Alpha 建模的工程方法论:从线性因子到非线性模型的取舍、特征工程与截面标准化、标签构造与三重障碍法、时序交叉验证与 purging embargo、正则化与模型族选择、PBO 过拟合度量、模型衰减与再训练节奏,以及从预测到组合的衔接。

机器学习进入量化选股已经十几年,但真正用机器学习跑出稳定超额收益的团队并不多。原因不是模型不够强,而是金融数据的信噪比太低:一个有效因子的 IC 只有 0.03,而同样的模型在图像、文本任务上可以轻易把准确率做到 95% 以上。在这个信噪比下,复杂模型的额外容量大部分用来拟合噪声。

真正难的地方在于模型越复杂,越需要纪律来约束它。金融数据没有「独立同分布」的假设,样本高度重叠、时序自相关、分布随时间漂移。直接把 Kaggle 的那套流程搬过来,几乎必然过拟合。

本文聚焦机器学习建模特有的方法论:特征、标签、时序交叉验证、PBO、再训练。线性因子的构造与检验在 因子研究与有效性检验 中讨论;回测偏差的通用陷阱属于研究方法论,本文只在与模型直接相关处引用。

目录

  1. 从线性因子到机器学习
  2. 特征工程与截面处理
  3. 标签构造与持有期设计
  4. 时序交叉验证与 purging
  5. 模型族选择与正则化
  6. 过拟合度量与 PBO
  7. 模型衰减与再训练节奏
  8. 特征重要性与可解释性
  9. 从模型输出到组合权重

1. 从线性因子到机器学习

线性多因子模型假设「收益 = Σ 因子暴露 × 因子收益 + 噪声」,它的优势是稳定、可解释、参数少。机器学习模型的吸引力在于捕捉线性和交互项:

线性:  r = β₁x₁ + β₂x₂
非线性:r = f(x₁, x₂)   # 例如「小市值 + 高动量」才有效,单独无效

但在低信噪比下,非线性模型的边际收益常常被它的过拟合风险吃掉。一个实用的判断顺序是:

  1. 先用线性模型建立基线,测出样本外 IC。
  2. 只有当非线性模型在严格样本外显著优于线性基线时,才采用。
  3. 如果提升小于 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 触发,并把「新旧模型并行期」作为标准流程,用实盘数据而非回测决定是否切换。

常见坑清单

  1. 用标准 K-Fold 做时序验证:随机划分导致标签重叠泄漏,样本外虚高。
  2. 全样本截面标准化:用了未来均值,改用逐日截面标准化。
  3. 忽略标签重叠:把高度相关的样本当独立样本,高估显著性。
  4. 持有期短于成本回收期:先定持有期再看收益,必然被成本吃掉。
  5. min_child_samples 用默认值:叶子过细,模型拟合噪声。
  6. 只报 Sharpe 不报尝试次数:多重检验未校正,Sharpe 无意义。
  7. 不做 PBO 检验:无法知道「最优配置」是不是运气。
  8. 用 MDI 看重要性:高基数特征被高估,改用分组置换。
  9. 每日重训:追逐噪声,模型不稳定。
  10. 预测直接当预期收益:厚尾未截断,组合在极端预测上重仓。

小结

机器学习 Alpha 建模的核心不是模型,而是纪律:标签要防重叠、验证要防泄漏、模型要控容量、过拟合要量化、再训练要有节奏。在信噪比极低的金融数据上,任何一处纪律的缺失都会被模型的容量放大成样本外的崩塌。

判断一个 ML 策略是否可信,最快的自检是三个问题:它的样本外 IC 是否显著高于同特征的线性基线?PBO 是否低于 0.3?特征重要性在多个训练窗口间是否稳定? 三个问题有一个答不上来,就不该上实盘。

下一步建议阅读 组合优化与风险模型 ,看经过样本外验证的预测如何转化为权重;如果你还没建立线性因子的检验流程,先回到 因子研究与有效性检验 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「量化交易」更多文章

  1. 风险模型与因子归因
  2. 回测偏差与过拟合防范
  3. 市场微结构与流动性