LightGBM 量化实战篇(中):排序学习、Beta 对冲与 Purged 交叉验证

上一篇讨论了金融数据的特殊性质、截面预处理方法和保守的超参数调优策略。完成这些环节后,我们已经知道如何将金融数据正确地处理并喂入 LightGBM。但还有一个关键问题尚未回答:模型的优化目标应该是什么?

回归目标(预测绝对收益率)和排序目标(学习股票的相对强弱)是两种截然不同的思路。选定目标函数后,还需要理解选股得到的相对强弱如何转化为实际收益,以及如何通过严格的交叉验证确保模型没有偷看未来数据。这三个问题构成了本篇的核心内容。


一、排序学习:让模型直接回答"谁更强"

量化选股中存在一个核心认知:我们关注的并非绝对收益预测的精度,而是排序的正确性。假设模型预测 A 涨 3.1%、B 涨 2.9%,而实际 A 涨 1.2%、B 涨 1.0%,绝对收益的预测值与实际值差距显著,但排序关系是正确的。当投资组合仅持有排名前若干只股票时,排序的正确性已经满足需求。因此,目标函数的设计应当与这一目标保持一致。


1. 回归目标的问题

LightGBM 默认支持回归目标,比如最小化均方误差:

$$\mathcal{L} = \sum_i (y_i - \hat{y}_i)^2$$

直接以未来收益率作为标签、训练模型预测绝对收益,会面临多方面的挑战:收益率本身噪声含量高,精确预测具体的涨跌幅极其困难;收益率分布具有显著的厚尾特征,少数暴涨或暴跌的股票会严重拉偏均方误差的优化方向;模型倾向于将优化资源集中在拟合中间区域的普通股票上,而投资者真正关注的恰恰是头部和尾部的标的;此外,投资组合的构建仅依赖相对排序,而非绝对预测值。因此,回归目标并非量化选股的最优选择。


2. LambdaRank:直接学排序

LightGBM 支持排序学习目标(objective = "lambdarank")。其核心思路并非预测具体的收益率数值,而是学习一个排序分数。在每个交易日,股票的因子特征输入模型后,输出一个连续的 score 值:

$$s_{i,t} = f(x_{i,t})$$

这个 score 既不是排名也不是概率,而是一个纯粹用于排序的数值。以下是一个输出示例:

股票 模型分数
A 2.51
B 2.13
C 0.87
D -0.42

按分数从大到小排列,即为模型给出的股票强弱顺序。

训练过程中,模型可以获取真实标签(例如经未来收益率排序后划分的等级)。使用 lambdarank 时,需要传入 group 信息,以告知模型哪些样本属于同一组。在日频选股场景中,每个交易日即为一个 group:

1
group = [4000, 4020, 3980, ...]  # 每天有多少只股票

训练数据必须按照 group 信息进行排序,同一天的股票数据需要连续排列。


3. LambdaRank 的叶子输出是排名吗?

并非如此。叶子节点输出的仍然是连续分数,排名是在预测完成后通过手动排序得到的。模型输出 score,用户按 score 排序即可得到强弱顺序。

为什么不直接输出排名?原因在于排名是离散的,排序操作本身不可导,无法用于计算梯度。因此,排序学习只能通过间接的方式来优化排序质量。


4. Lambda 是什么?

LambdaRank 并非直接最小化预测分数与真实排名之间的数值误差。其核心思想是:如果交换两只股票的排序位置,排序指标(NDCG)会发生多大的变化?

对于一对股票 $i$ 和 $j$,假设真实相关性 $y_i > y_j$,而模型当前的分数为 $s_i$ 和 $s_j$。如果模型将 $j$ 排在了 $i$ 前面,或者两者之间的分数差距不足以体现其真实的强弱关系,那么这对样本就应当受到惩罚。LambdaRank 根据交换这对样本对 NDCG 的影响来构造梯度——排序偏差较大且交换后 NDCG 提升显著的样本对,获得较大的调整力度;排序已经较为准确的,调整力度较小;对最终排序指标影响微乎其微的,模型则不会在其上耗费优化资源。这是一种精巧的梯度加权机制。


5. NDCG 是什么?

NDCG(Normalized Discounted Cumulative Gain,归一化折损累计增益)是排序任务中广泛使用的评价指标。假设模型输出一个排序列表,前 $K$ 个位置的相关性得分分别为 $rel_1, rel_2, …, rel_K$,则 DCG 定义为:

$$DCG@K = \sum_{i=1}^{K} \frac{2^{rel_i} - 1}{\log_2(i+1)}$$

部分实现中采用简化形式,直接使用 $rel_i$ 而非 $2^{rel_i}-1$,具体取决于实现细节。$rel_i$ 可以来自未来收益的分位数划分、分箱后的收益等级,或经处理后的收益排名。

NDCG 是 DCG 的归一化版本:

$$NDCG@K = \frac{DCG@K}{IDCG@K}$$

其中 $IDCG@K$ 是理想排序下的 DCG 值。

NDCG 的设计逻辑包含两个要点:排序越靠前的位置获得越高的权重;将优质股票错误地排到后面会受到更重的惩罚。这与量化选股的实际需求高度一致——当投资组合仅持有前 100 只股票时,模型将第 1 至第 100 名的排序做对,远比将第 3000 名的位置排对更为重要。因此,采用 lambdarank + ndcg 的组合比回归目标更贴合真实的投资决策需求。


6. 标签怎么构造?

排序学习需要构造训练标签,常见的做法有以下几种。

未来收益分箱是最为常用的方法:将每个交易日所有股票的未来 N 日收益率划分为若干组(16、32、64 组均可),收益率越高对应的标签值越大。实现代码如下:

1
2
3
df["label"] = df.groupby("date")["future_return"].transform(
    lambda s: pd.qcut(s, q=32, labels=False)
)

也可以直接将未来收益排名转换为整数排名作为标签,但由于排名数值可能很大,直接作为 relevance label 不一定兼容所有排序损失函数的实现,分箱方式更为稳健。此外,还有一种做法是构造二值标签——仅保留前 10% 或 20% 的股票作为正样本,其余作为负样本。这种方法更接近分类问题,但会损失大量中间排序信息。综合考虑,分箱是目前最常用的选择


7. LightGBM 排序训练示例

简化版的训练框架:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import lightgbm as lgb

train_data = lgb.Dataset(
    X_train,
    label=y_train,
    group=group_train
)

valid_data = lgb.Dataset(
    X_valid,
    label=y_valid,
    group=group_valid,
    reference=train_data
)

params = {
    "objective": "lambdarank",
    "metric": "ndcg",
    "eval_at": [50, 100],
    "learning_rate": 0.03,
    "max_depth": 5,
    "num_leaves": 31,
    "min_data_in_leaf": 300,
    "bagging_fraction": 0.8,
    "bagging_freq": 1,
    "feature_fraction": 0.8,
    "lambda_l2": 1.0,
    "verbosity": -1
}

model = lgb.train(
    params,
    train_data,
    num_boost_round=3000,
    valid_sets=[valid_data],
    callbacks=[
        lgb.early_stopping(100),
        lgb.log_evaluation(100)
    ]
)

需要注意几个要点:X_train 必须按照日期 group 排好序;group_train 告诉模型每个交易日包含多少只股票;验证集同样必须按时间切分,不能随机打乱;eval_at 应当与实际持仓数量相对应(如前 50、前 100、前 200 等)。


二、Beta 与 Alpha:按排名选股到底赚什么钱?

A 股市场存在一个现实问题:同涨同跌现象较为显著,Beta 暴露对组合收益的影响很大。按排名选出前 100 只股票后,如果大盘整体下跌,这些股票同样难以幸免,那么这种选股方式的意义何在?

如果仅采用单边做多策略,确实无法规避 Beta 的影响。但在量化投资中,许多选股模型并非单纯做多,而是通过对冲手段剥离 Beta 暴露、仅保留 Alpha 收益。


1. 收益的拆分

一个股票组合的收益可以拆成:

$$R_p = \alpha_p + \beta_p R_m + \varepsilon_p$$

其中 $R_p$ 为组合收益,$R_m$ 为市场收益,$\beta_p$ 为市场风险暴露系数,$\alpha_p$ 为超额收益(Alpha),$\varepsilon_p$ 为残差噪声。当组合完全暴露于市场时,大盘下跌将直接拖累组合收益。若要获取纯粹的 Alpha,就需要通过某种方式将 Beta 暴露对冲掉。


2. 市场中性策略:做多强股,做空股指期货

典型的市场中性策略做法是:买入模型排名靠前的股票组合,同时在期货市场做空对应的指数期货(如中证 500、中证 1000 等)。假设大盘下跌 5%,多头组合因选股较强仅下跌 1%,而空头期货端获利约 5%,粗略计算如下:

$$组合收益 \approx -1% + 5% = 4%$$

上述计算忽略了交易成本、基差变化、保证金成本、跟踪误差等因素,但基本思路是清晰的。通过多空对冲,模型的盈利不再依赖于大盘上涨,而是取决于所选股票能否跑赢市场基准。


3. Rank 选股与市场涨跌的关系

横截面 Rank 关注的核心是相对强弱。当市场整体下跌时,尽管 4000 只股票普遍下跌,但模型选出的 100 只股票跌幅更小、甚至部分标的仍在上涨,这表明它们具有相对强势。若不进行对冲,单边持有仍可能面临亏损;但通过空头对冲,这种相对强势可以转化为 Alpha 收益。

因此,Rank 本身并不保证绝对盈利,它帮助模型识别的是相对强弱关系。要将这种相对强弱转化为稳定的收益,还需要依赖组合构建、风险中性化、交易成本控制、对冲工具选择、仓位管理以及回撤控制等一系列配套措施。


4. A 股做对冲要注意什么?

A 股市场的对冲环境与成熟市场存在差异,有几个需要提前了解的问题。

其一,贴水问题。 长期做空股指期货时,贴水构成一笔持续性的成本支出,即便存在 Alpha 信号,也可能被对冲成本所侵蚀。

其二,指数风格匹配问题。 沪深 300、中证 500、中证 1000 之间的风格差异显著。若多头组合偏向小市值标的,却使用大盘指数进行对冲,就会引入风格错配风险。

其三,Rank 无法消除所有风险暴露。 排序能够削弱市场整体涨跌的影响,但无法处理行业暴露、市值暴露、波动率暴露和流动性暴露等问题。一个成熟的市场中性策略通常还需要借助组合优化器或风险模型来约束各类风险敞口:行业中性、市值中性、Beta 中性、风格因子暴露限制、个股权重上限、换手率约束等,缺一不可。

在学习阶段,优先理解 Rank 和对冲的基本思想即可。当进入实盘或严格回测阶段时,上述风控细节必须逐一落实。


三、交叉验证:金融数据不能随机打乱

交叉验证是评估模型泛化能力的标准方法,在一般机器学习任务中被广泛使用。但在金融时间序列数据上,简单地采用随机 K-Fold 划分,数据泄露的风险将显著增加。


1. 普通 K-Fold 是什么?

K-Fold 的基本原理是将数据划分为 K 份,每份依次轮流作为验证集。以 5-Fold 为例:将数据随机打乱后切分为 5 份,每轮使用其中 4 份进行训练、1 份用于验证,最终取各轮结果的平均值。在图像分类等任务中,由于样本之间不存在时间依赖关系,随机打乱是可行的。但股票数据的特性决定了这一方法并不适用。


2. 为什么金融数据不能随机 K-Fold?

股票收益率、波动率、成交量、资金流等指标与其前后若干交易日的数据存在显著的自相关性。随机打乱后可能出现如下情况:验证集包含 2020 年 1 月 6 日的数据,而训练集中却出现了 1 月 7 日的数据——这意味着模型在训练过程中"看到"了未来信息。其结果是验证集指标异常优异,回测收益看起来很漂亮,但一旦投入实盘便立即失效。

这种现象在业界常被概括为:回测收益虚高,实盘泛化能力不足


3. 标签重叠:更隐蔽的泄露来源

除时间自相关之外,还存在一个更为隐蔽的泄露来源:标签重叠。假设模型的预测目标是未来 5 日收益率,1 月 2 日的标签为 $P_{1月9日}/P_{1月2日}-1$,1 月 3 日的标签为 $P_{1月10日}/P_{1月3日}-1$。这两个标签在 1 月 3 日至 1 月 9 日这一时间段内共享了价格信息。如果训练集中包含 1 月 3 日的样本,而验证集中包含 1 月 2 日的样本,那么训练集的标签已经部分包含了验证集未来区间内的市场信息。无论是多日收益率标签、滚动波动率标签,还是未来 N 日收益分类标签,都不可避免地存在这一问题。


4. Purged K-Fold:净化交叉验证

Marcos López de Prado 在金融机器学习领域提出了 Purged K-Fold 交叉验证方法。其核心思路是:在切分训练集和验证集时,将那些标签时间窗口与验证集存在重叠的训练样本一并剔除,相当于在训练集与验证集之间建立一道信息隔离屏障。

基本原理

具体而言,假设样本 $i$ 在时间 $t_i$ 生成特征,其标签覆盖的时间区间为 $[t_i, t_i + h]$(其中 $h$ 为预测未来收益的时间跨度)。验证集覆盖的时间区间为 $[t_{val,start}, t_{val,end}]$。只要训练样本 $j$ 的标签区间 $[t_j, t_j + h]$ 与验证集区间存在交集,该样本就不应被纳入训练集。这就是 Purge 操作。


5. Embargo:再加一段隔离期

仅进行 Purge 处理仍然不够。由于金融数据具有自相关性,标签的时间窗口没有直接重叠并不意味着信息完全隔离——训练集末尾的数据可能通过市场的惯性效应对验证集开头产生影响。因此,Purged K-Fold 中通常还会引入一段 Embargo(禁闭期):在验证集前后各留出一段时间窗口,该时间段内的数据既不用于训练,也不用于验证。

例如,验证集覆盖 3 月 1 日至 3 月 31 日,标签长度为 5 天,Embargo 也设为 5 天,则训练集中靠近验证集起始时间的样本需要被清除,验证集结束之后也需留出相应的空白期,以防止信息的间接串扰。Purge 清除的是标签区间明确重叠的部分,而 Embargo 则提供了额外的安全缓冲。


6. 时间序列滚动验证

除 Purged K-Fold 外,量化实践中还常用更为直观的时间序列验证方式。

固定训练窗口,滚动验证

1
2
3
4
5
6
7
8
训练:2016-01-01 到 2018-12-31
验证:2019-01-01 到 2019-03-31

训练:2016-01-01 到 2019-03-31
验证:2019-04-01 到 2019-06-30

训练:2016-01-01 到 2019-06-30
验证:2019-07-01 到 2019-09-30

滚动训练窗口

1
2
3
4
5
6
7
8
训练:2016-01-01 到 2018-12-31
验证:2019-01-01 到 2019-03-31

训练:2017-01-01 到 2019-03-31
验证:2019-04-01 到 2019-06-30

训练:2018-01-01 到 2019-06-30
验证:2019-07-01 到 2019-09-30

滚动窗口可以使模型对近期市场风格的变化更为敏感,但也可能导致训练样本不足。无论采用哪种验证方式,核心原则始终只有一条:训练集不得包含验证集时间点之后的信息,也不得通过标签重叠间接泄露未来数据。


7. 用了 Purged K-Fold 之后分数下降,是坏事吗?

初次使用严格的 Purged K-Fold 后发现指标大幅下降,无需紧张,这是正常现象。此前的高分很可能是由信息泄露所支撑的"虚高"表现,经过清洗后得到的是更为保守、更接近真实泛化能力的估计。只有当一个模型在 Purged K-Fold、Embargo、时间序列滚动验证之后,仍能保持稳定的正 IC、正 Rank IC、合理的分层收益和可接受的换手率,才说明该模型真正具有进一步开发的价值。


小结

本篇讨论了量化 LightGBM 实践中三个紧密关联的问题:目标函数的选择、收益归因与对冲、以及交叉验证的正确做法。

核心要点回顾:排序学习(LambdaRank + NDCG)比回归目标更贴合选股的实际需求,模型输出连续排序分数而非绝对收益预测;Rank 选股获得的是相对强弱信号,通过对冲剥离 Beta 后可转化为 Alpha 收益;交叉验证必须遵循时间序列的先后顺序,Purged K-Fold 和 Embargo 是防止信息泄露的必要手段。

模型训练和验证完成后,还有一系列关键问题需要面对:如何解释模型的决策逻辑、如何应对上线后的概念漂移、以及如何将所有环节串联为一个完整的工作流。这些内容将在本系列最后一篇中展开。


下一篇预告【量化】从零到LGBM | 9 | LGBM的金融应用(下):模型解释、进阶方向与完整工作流——讨论 SHAP 模型解释、概念漂移与增量学习、自定义损失函数、深度学习融合,以及一个完整的八步量化工作流。