机器学习做量化,最容易犯的一个错是:把宝押在单个「最强模型」上。你调参调到深夜,挑出 AUC 最高的那个 XGBoost,兴冲冲上实盘,结果它第一个月就垫底。问题不在模型,在于「单个模型」本身就是一个高方差的赌注。
集成学习(ensemble)在 Kaggle 上是常识,在量化里却常常被低估。本文聊的是其中最朴素、也最被低估的一种——集成模型平均(model averaging):把 M 个「弱预测」直接取平均,得到集成信号 S_M = (1/M)·Σ s_k。
它的核心结论有一个解析保证,不是玄学:
Var(S_M − μ*) = (σ_η² / M) · (1 + (M − 1)·ρ)plaintext其中 ρ 是各模型误差之间的相关系数。它说清楚了一件事:只有当模型误差「相互独立」(ρ 小) 时,加模型才持续降噪;如果 M 个模型共享同一份误差(ρ=1),平均也无济于事。这就是「有效分散化」在预测层面的量化表达——和投资组合分散化是同一个灵魂。
我们用 10 只股票、252 天、带「真 alpha + 共享/独立噪声」的合成面板,把这件事跑成看得见的数据:单模型信号 IC 0.134,40 个模型集成后 IC 升到 0.229;横截面多空组合的年化 Sharpe 从单模型均值 1.60 拉到 3.12(≈oracle 用真 alpha 的 1/3,是单模型的 1.96 倍)。
一、数据机制:真 alpha + 带噪信号#
设每只股票有一个「真 alpha」μ*_i(已对截面去均值),日收益
r_{t,i} = μ*_i + 特质噪声plaintext用横截面多空(dollar-neutral)组合 w ∝ (S − 均值) 来隔离 alpha、剔除市场暴露。
M 个「弱模型」给出带噪信号 s_k = μ* + η_k,其中 η_k 之间相关系数 ρ(共享误差占比)。集成信号 S_M = (1/M)·Σ s_k。
import numpy as np
N = 10 # 股票数
T = 252 # 交易日
SIG_MU = 0.004 # 真 alpha 截面波动(日频,已去均值)
SIG_R = 0.020 # 特质噪声
SIG_ETA = 0.030 # 单模型信号噪声
RHO = 0.30 # 模型误差相关
def make_models(M, rho, seed=20260713):
r = np.random.default_rng(seed)
mu = r.normal(0, SIG_MU, N); mu = mu - mu.mean()
R = mu[None, :] + r.normal(0, SIG_R, (T, N))
zc = r.normal(0, SIG_ETA, (T, N)) # 共享误差成分
signals = []
for k in range(M):
zk = r.normal(0, SIG_ETA, (T, N)) # 独立误差成分
eta = np.sqrt(rho) * zc + np.sqrt(1 - rho) * zk
signals.append(mu[None, :] + eta)
S = np.stack(signals, 0).mean(0) # 集成信号
return mu, R, S, signalspython关键在 eta 的拆法:√(ρ)·zc 是所有模型共享的误差,√(1−ρ)·zk 是模型各自独立的误差。ρ=0 时模型误差完全独立,ρ=1 时所有模型只是同一份噪声的拷贝。
二、为什么平均能降噪:解析推导#
设单模型误差 η_k = μ* 噪声,Var(η_k) = σ_η²,任意两模型误差相关 ρ。集成信号的误差:
S_M − μ* = (1/M)·Σ_k η_k
Var(S_M − μ*) = (1/M²)·Σ_{i,j} Cov(η_i, η_j)
= (1/M²)·[M·σ_η² + M(M−1)·ρσ_η²]
= (σ_η² / M)·(1 + (M−1)·ρ)plaintext分两种极端:
- ρ=0(误差独立):
Var = σ_η²/M,随 M 线性下降,集成本质是「平均掉独立噪声」; - ρ=1(误差全共享):
Var = σ_η²,与 M 无关——加再多模型也只是把同一份噪声复制 M 遍再平均,白搭。
这正是集成学习的命门:集成的增益来自「误差的多样性」(error diversity),而不是「模型的数量」。这也解释了为什么十个几乎一样的 LSTM 集成没用,而树模型(random forest / gradient boosting)集成有用——后者靠 bootstrap 和特征子采样人为制造了误差多样性。

三、回测结果:Sharpe 随 M 收敛向 Oracle#
横截面多空组合的 Sharpe 计算与 IC 度量:
def ls_sharpe(S, R):
w = S - S.mean(1, keepdims=True) # dollar-neutral
w = w / np.abs(w).sum(1, keepdims=True)
ret = (w * R).sum(1)
ann = ret.mean() * 252
vol = ret.std(ddof=1) * np.sqrt(252)
sharpe = ann / vol if vol > 0 else 0.0
return sharpe, ann, vol, ret, w
def ic(sig, target):
def rank(x): return x.argsort().argsort().astype(float)
out = []
for t in range(sig.shape[0]):
rs = rank(sig[t]) - rank(sig[t]).mean()
rt = rank(target[t]) - rank(target[t]).mean()
out.append((rs*rt).sum() / np.sqrt((rs**2).sum()*(rt**2).sum()))
return float(np.mean(out))
mu, R, S, signals = make_models(40, RHO, seed=20260713)
sp_ens, ann_ens, vol_ens, ret_ens, _ = ls_sharpe(S, R)
sp_or = ls_sharpe(mu[None,:].repeat(T,0), R)[0] # oracle 上界
sp_single = np.mean([ls_sharpe(signals[k], R)[0] for k in range(40)])
ic_single, ic_ens = ic(signals[0], mu), ic(S, mu)python主要数字(ρ=0.3, 规范种子):
| 指标 | 单模型均值 | 集成(M=40) | Oracle(真 alpha) |
|---|---|---|---|
| 信号 IC(秩相关) | 0.134 | 0.229 | 1.000 |
| 年化 Sharpe | 1.60 | 3.12 | 10.47 |
| 年化收益 | 0.196 | 0.385 | 1.189 |
集成把 Sharpe 拉到单模型的 1.96 倍,并随 M 上升单调收敛向 Oracle(用真 alpha 的理论上界)。注意 oracle 的 Sharpe 远高于集成,因为集成即使在 M→∞ 也只消除了独立噪声,真实数据里还有固有不可解释方差。

四、噪声被「削平」:日收益分布#
看日收益分布最直观:集成信号的均值更接近 Oracle、方差被削平,单模型信号的噪声尾巴更厚。
fig, ax = plt.subplots(figsize=(10, 5.2))
ax.hist(ret_ens*100, bins=40, color="#C44E52", alpha=0.55, density=True,
label=f"集成(M=40) 日收益 μ={ret_ens.mean()*100:.3f}% σ={ret_ens.std()*100:.3f}%")
ax.hist(ret_or*100, bins=40, color="#8172B3", alpha=0.45, density=True,
label=f"Oracle 日收益 μ={ret_or.mean()*100:.3f}% σ={ret_or.std()*100:.3f}%")
ret_s0 = ls_sharpe(signals[0], R)[3]
ax.hist(ret_s0*100, bins=40, color="#999999", alpha=0.40, density=True,
label=f"单模型 日收益 μ={ret_s0.mean()*100:.3f}% σ={ret_s0.std()*100:.3f}%")
ax.set_xlabel("日收益 (%)"); ax.set_ylabel("密度")
ax.set_title("横截面多空日收益分布:集成把噪声『削平』,均值更接近 Oracle")
ax.legend(fontsize=8); ax.grid(True, color="#DDDDDD", lw=0.6)python
五、权重随 M 收敛到 Oracle#
集成不只是「收益更稳」,它的持仓权重也随 M 收敛向 Oracle 权重。算集成权重与 Oracle 权重的相关系数:
corr_vs_M = []
for m in [1,2,3,5,8,12,20,30,40]:
Sm = np.stack(signals[:m], 0).mean(0)
wm = (Sm - Sm.mean(1, keepdims=True)); wm /= np.abs(wm).sum(1, keepdims=True)
corr_vs_M.append(np.corrcoef(wm.ravel(), w_or.ravel())[0,1])pythonM=40 时权重相关约 0.24(注意:截面权重被 dollar-neutral 归一后相关偏低,但趋势明确——随 M 上升而提高)。这说明集成改善的是信号方向的一致性,而不只是把方差压小。

六、鲁棒性:10 个种子#
单次结果可能侥幸。跑 10 个种子确认:
- 集成 Sharpe 均值 2.23 ± 1.92,单模型均值 1.22,Oracle 均值 9.83,纯噪声基准 0.18;
- 集成 > 单模型:8/10 个种子;集成 > 纯噪声:7/10 个种子。
高标准差(±1.92)说明在小样本(10 股票/252 天)下集成增益仍有波动,但「集成不劣于单模型、且方向上优于纯噪声」是稳定的。
七、六大真实陷阱#
集成模型平均最容易被滥用。六个坑:
- 误差不独立(ρ 高):这是头号坑。如果 M 个模型都是「同一份数据 + 同一套特征 + 不同超参」,它们的误差高度相关,平均几乎不降噪。要制造多样性,得在数据子采样、特征子集、模型族、标签定义上真正分叉。
- 等权并非总是最优:本文用等权平均。若模型质量差异大,可加权(按 IC / Sharpe / 时序相关性倒数)。但加权本身要样本外确定,不能偷看未来。
- 过拟合叠加:每个子模型若都已过拟合,集成只会把「一致的过拟合」放大。务必每个子模型都做 purged K-fold / walk-forward 验证。
- 标签泄漏:多模型共享同一份泄漏标签,会集体「看起来很准」。集成放大的是信号也放大噪声——包括泄漏带来的假信号。
- 相关性时变:ρ 不是常数。市场 regime 切换时模型误差相关性会骤升(危机时所有模型一起犯错),此时集成的降噪红利消失,需要做 regime 条件下的相关性监控。
- 成本吞噬:等权集成常提高换手(多个信号方向不一)。实盘必须把交易成本、冲击成本算进 Sharpe 对比,否则纸面增益被摩擦吃掉。
八、结语#
集成模型平均的威力,凝结在一条公式里:Var(S_M−μ*) = (σ_η²/M)·(1+(M−1)ρ)。增益来自误差多样性,不是模型数量。 当模型误差独立(ρ 小),加模型持续降噪、Sharpe 收敛向 Oracle;当误差共享(ρ→1),集成退化成「把同一份噪声复制 M 遍」。
对量化研究者而言,最该记住的不是「集成更好」,而是**「我的模型们到底有多不相关」**。在因子模型、多信号择时、跨模型 alpha 融合里,先用 ρ 把误差相关性量化出来,再去决定要不要集成、怎么集成。它和前面聊过的分散化投资、风险平价是同一套思想在不同层的投影。
实盘落地清单(给研究者的提醒):
- 在数据/特征/模型族/标签四个维度至少一处制造真正分叉,先把子模型误差相关 ρ 压下来;
- 用样本外滚动窗口估计 ρ,危机段 ρ 会飙升,提前设好「集成降噪失效」的降级开关;
- 等权起步,确有样本外证据再上 IC/相关性加权;
- 集成前后都扣真实交易成本对比 Sharpe,别被纸面增益骗了。
注:本文信号为「真 alpha + 共享/独立噪声」的自洽合成数据,用于演示集成机制;真实因子模型中模型误差相关性来自共同数据来源与因子暴露,落地需接入真实多因子/多模型流水线并做样本外与 walk-forward 验证。