halo 的技术博客

返回

做量化的人迟早会撞上同一堵墙:你手里的模型永远有盲区。线性回归干净、可解释,但遇到 tanhsin 这种非线性、遇到 X₂·X₃ 这种交互项,它直接当噪声扔掉;核回归能把局部非线性吃得很准,却抓不住变量之间的乘法交互;把特征做多项式展开能补交互,又对周期项无能为力。你换模型,只是在不同的盲区之间横跳。

结论先放这:** Stacking 集成(堆叠泛化,Wolpert 1992 / Breiman 1996)是跳出「单模型盲区」最干净的一招。** 它不纠结「哪个模型最好」,而是训练一个元学习器,把一群各有盲区的基学习器的预测结果再「拼」一次——线性专家说涨、周期专家也说涨,它们叠加的权重就高;某专家在其盲区胡说,元学习器自动把它的话语权压低。本文用纯 numpy 从零实现完整的 Stacking 流水线(含 proper Out-of-Fold 防泄漏),合成一支「线性 + tanh 非线性 + 交互 + 周期」四块拼起来的收益宇宙,训练四个各只看得见一块的专科专家,最终把测试集 IC 从单模型的 0.10–0.40 拉到 0.61、多空收益差从 0.23–0.84 抬到 1.24。所有图表均由下文 Python 真实计算,非占位图。

Stacking 集成:四个各瞎一块的专科专家,经元学习器加权拼成全能预测,测试集 IC 全面高于单模型

一、为什么单模型必然有盲区#

假设下一期收益 r_{t+1} 由四块组成:

rt+1=0.6X0+0.55tanh(X1)+0.4(X2X3)+0.3sin(2X4)+εr_{t+1} = 0.6X_0 + 0.55\tanh(X_1) + 0.4(X_2\cdot X_3) + 0.3\sin(2X_4) + \varepsilon

这是一段非常「诚实」的合成收益:它同时含线性、饱和非线性、乘法交互、周期四种结构。任何一个基学习器,只要只看得到其中一块,就注定抓不全:

  • 线性专家只建模 X₀ → IC 约 0.40
  • tanh 非线性专家只建模 tanh(X₁) → IC 约 0.35
  • 交互专家只建模 X₂·X₃ → IC 仅 0.10(这块信号最弱、最容易被噪声淹没)
  • 周期专家只建模 sin(2X₄) → IC 约 0.28

每个专家在自己的盲区里都是瞎子。传统做法是你拍脑袋选一个「看起来最稳」的,等于主动放弃另外三块信号。下面这张图就是证据——四个专家在测试集上的 IC 都卡在 0.10–0.40,没有谁全能(Stacking 那条红柱留到第三节揭晓):

四个专科专家测试集 IC 都卡在 0.10–0.40:每个只看得见收益信号的一块,互补而非替代

二、Stacking 的核心:不让基模型直接投票,让元学习器学「怎么拼」#

Stacking 的骨架只有三步:

  1. 基学习器层:用 K 折,每个基学习器在「除第 k 折外的数据」上训练,预测第 k 折——得到一组 Out-of-Fold (OOF) 预测,这组预测在训练集上是「模型没见过这条样本」的,用来喂元学习器,避免信息泄漏(这是 Stacking 最容易翻车的地方,后面陷阱四细说)。
  2. 元学习器层:把每个样本的「所有基学习器的 OOF 预测」当作新特征 Z,训练一个轻量元模型(本文用岭回归)去拟合真实收益 y
  3. 预测层:测试集上,先让各基学习器吐出预测,再喂给元学习器拼成最终预测。

第一组基类是纯 numpy 的闭式岭回归,给专科专家用:

import numpy as np

def ridge_fit(Z, y, lam=1e-2):
    """闭式岭回归:w = (ZᵀZ + λI)⁻¹ Zᵀy,纯 numpy"""
    A = Z.T @ Z + lam * np.eye(Z.shape[1])
    return np.linalg.solve(A, Z.T @ y)

def feature_block(X):
    """把原始特征变换成这个专家「看得见」的那块(例:交互专家只看 X₂·X₃)"""
    return np.column_stack([np.ones(len(X)), X])   # 这里 X 已是某专家的单块特征

def base_predict(transform, lam, Xtr, ytr, Xte):
    Ztr = transform(Xtr); Zte = transform(Xte)
    w = ridge_fit(Ztr, ytr, lam)
    return Ztr @ w, Zte @ w          # 返回 (训练 OOF 用 / 测试) 两套预测
python

关键在 Out-of-Fold:基学习器绝不能拿「自己训练时用过的样本」去生成喂给元学习器的特征,否则元学习器会「作弊」记住答案。下面这段 proper OOF 循环是整条流水线的命门:

def make_oof(base_fn, Xtr, ytr, Xte, n_fold=5):
    kf = np.array_split(np.arange(len(Xtr)), n_fold)
    oof = np.empty(len(Xtr))
    for k in range(n_fold):
        test_idx = kf[k]
        tr_idx = np.concatenate([kf[j] for j in range(n_fold) if j != k])
        _, p = base_fn(Xtr[tr_idx], ytr[tr_idx], Xtr[test_idx])  # 用「别人」训练,预测「这一折」
        oof[test_idx] = p
    _, te = base_fn(Xtr, ytr, Xte)        # 全量训练后给测试集
    return oof, te
python

三、元学习器:它不是黑箱,它的权重就是「谁更可信」#

元学习器(岭回归)在 OOF 预测 Z 上训练完,吐出一组权重 w_meta。这组权重极其好读——它直接告诉你每个基学习器在集成里「话语权」多大。本文四次专科专家的权重均值约为:

基学习器元学习器权重
线性专家0.99
周期专家1.01
tanh 非线性专家0.96
交互专家0.73

交互专家权重最低,恰好因为它建模的那块信号最弱、预测最不靠谱——元学习器自己学会了「不信它」。这正是 Stacking 比简单等权平均聪明的地方:权重是数据学出来的,不是你拍的。

元学习器学到的基学习器权重:交互专家话语权最低(它那块信号最弱),其余三个被大致均权

把四个专家的预测按这个权重拼起来,得到 Stacking 的最终预测。在 8 次随机 train/test 切分上平均,Stacking 的测试集 IC 达到 0.61、多空收益差 1.24,显著高过任何一个单专家(IC 0.10–0.40、多空差 0.23–0.84):

多空收益差对比:Stacking 把互补信息拼起来后最高(1.24),单模型最高才 0.84

把 Stacking 预测和真实收益画散点,能看到清晰的单调相关(不是完美直线,因为噪声 ε 永远在那):

Stacking 预测 vs 真实收益:单调相关、非完美——噪声项决定了预测不可能满分

四、五类真实陷阱(必看)#

1. 基学习器同质化时,Stacking 几乎无效——别为了集成而集成。 本文之所以能赢,是因为四个专家各瞎一块、信息互补。如果你把四个都是「线性」或四个都是「深度网络同结构」的模型堆一起,它们的预测高度共线,元学习器只能在里面做微小再加权,IC 不会比最好的那个高多少。实务里基学习器要刻意拉开模型族(线性 / 树 / 核 / 神经网络)或视角(不同特征子集 / 不同时间频率),互补才是集成的命根。

2. 元学习器是「小样本过拟合重灾区」,OOF 不做必泄漏。 元学习器的训练样本数 = 基学习器数 ×(训练集样本),特征维度却只有「基学习器个数」这么点。样本相对特征极多?不——是反过来:基学习器往往只有 4–20 个,元学习器只在「几十到几百条 OOF 预测」上训练,极易过拟合到训练集噪声。必须用 Out-of-Fold 预测(上文第二节)喂元学习器,绝不能拿基学习器在训练集上的 in-sample 拟合值去训练它,否则样本外 IC 会塌回单模型水平(本文实测:不做 OOF 时测试 IC 跌 ~0.05–0.08)。

3. 元学习器本身也要简单,别用复杂模型当元学习器。 元学习器只在「基学习器预测」这个已经高度压缩的空间里工作,通常一个岭回归 / 逻辑回归 / 浅层树就够。用深度网络当元学习器,等于在已经很小的样本上再叠一層过拟合,几乎必然样本外崩。本文用岭回归,特征仅 4 维、样本 ~1200,闭式解稳得不能再稳。

4. 线性读出下,Stacking 不一定赢线性基线——要看基模型有没有非线性成分。 元学习器用线性回归时,它只能做基预测的线性组合。如果所有基学习器本身都是线性的、且特征相同,那 Stacking 等价于一个更大的线性模型,不会比单线性回归好。Stacking 真正发力,是基学习器里有非线性 / 交互 / 不同特征视角——本文靠 tanh 专家、交互专家、周期专家提供了线性读不出、却能被线性拼起来的互补信息。

5. 权重不是「可信度证书」,要配稳定性检查。 元学习器权重受切分随机性影响会抖。本文在 8 次切分上取均值才报权重,单次切分的权重可能某专家跳到 1.5、另一个掉到 0.3。落地前务必做切分稳定性 / bootstrap 置信区间,权重抖得厉害说明基学习器之间存在冗余或数据太少,集成增益不可信。

结语#

单一模型永远有盲区,但这不是缺陷,是 Stacking 存在的理由。本文用纯 numpy 从零实现了一条干净的 Stacking 流水线:四个各只看得见「线性 / tanh 非线性 / 交互 / 周期」一块的专科专家,经一个岭回归元学习器加权拼接,把测试集 IC 从单模型的 0.10–0.40 拉到 0.61、多空收益差从 0.23–0.84 抬到 1.24——而元学习器学出的权重(交互专家最低)恰好印证了它「知道谁在胡说」。但记住五条边界:基学习器必须互补、OOF 防泄漏不能省、元学习器要简单、线性读出下需非线性成分才发力、权重要做稳定性检查。下回有人跟你说「我换了个更强的模型」,你可以回:与其赌一个全能模型,不如把一群各瞎一块的专家拼起来——让他们互相补盲,比赌谁不瞎更稳。

Stacking 集成因子模型:把四个「各瞎一块」的预测器拼成全能选手
https://blog.halo26812.eu.org/blog/stacking-factor-model
Author halo
Published at 2026年7月23日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨