变分自编码器 VAE 因子挖掘:把不确定性也一起学出来
普通自编码器能把 20+ 维量价特征压成 5 维新因子,但它只给你一个"点估计"编码,不知道哪个样本编码得靠谱。变分自编码器 VAE(Kingma & Welling 2014)把编码器变成"一个分布",每个样本得到一个 (μ, σ²) 的编码高斯——于是除了新因子,你还免费拿到"这个因子值有多可信"。本文用纯 numpy 从零实现 VAE(重参数化技巧 + ELBO),在"3 潜因子驱动收益 + 大量噪声特征"的合成数据上,VAE 组合因子与未来收益 |IC|=0.695,并诚实演示"不确定性到底能不能当信号过滤器"这个反直觉结果,附完整代码与五类真实陷阱(中阶)。
你手里有一堆量价特征:20 多个技术指标、估值分位、资金流、波动率结构……它们彼此高度相关,里面混着真信号和一堆噪声。你想把它们压成一个干净的低维因子喂给择时或选股模型。
前面我们聊过普通自编码器(AE)——它把高维特征编码成低维瓶颈 z,再解码回去,靠”瓶颈逼出信息压缩”。但 AE 有个隐藏短板:它只给你一个 z 的点估计。z₁=0.83 这个样本,到底是”编码得很有把握”,还是”编码器其实很晕、随便吐了个数”?AE 一个字都不说。
结论先放这:变分自编码器 VAE(Kingma & Welling 2014)把编码器从”一个函数”升级成”一个分布”——每个样本不再得到一个点 z,而是得到一组参数 (μ, σ²),即”这个样本的编码服从 N(μ, σ²)”。 于是你既拿到了低维因子(取 μ),又免费拿到了每个因子值的不确定性 σ²。
在我们的合成数据(3 个潜因子驱动收益 + 前 8 维信息特征 + 后 16 维纯噪声特征,共 800 样本)上,VAE 学出的 5 维组合因子与未来收益的 |秩相关 IC|=0.695;KL 散度占 ELBO 的 70.6%,说明编码器确实在学一个”有宽度的分布”而不是退化成单点。 但有个反直觉的地方要讲清楚:在我们这个设定里,把”编码很散”的样本滤掉,IC 不升反略降——这恰恰说明不确定性不是现成的信号过滤器,它是一把需要校准的尺子,下文五类陷阱会拆穿。

1. 普通 AE 的盲区:你不知道编码靠不靠谱#
普通 AE 的编码器是 z = encoder(x),一个确定性映射。训练目标是最小化重建误差 ||x - decode(z)||²。瓶颈维度 m < d 强制信息压缩,所以 z 确实能抓主成分。
问题在推断时:来了一个样本 x*,编码器吐出 z*。如果 x* 落在训练分布边缘(比如某只股票某天出现了从没见过的特征组合),编码器的 z* 可能纯粹是外推的幻觉,但你无从分辨——它和”编码得很笃定”的样本长得一模一样。
VEA 的解法是:让编码器输出”分布”而不是”点”。预测 q(z|x) = N(μ(x), diag(σ²(x))),推断时从这里面采样得到 z。这样:
- 训练时,编码器被迫学会”对模糊样本给大 σ²(我不确定)”;
- 推断时,你可以看 σ² 判断”这个因子值信不信得过”。
2. VAE 的数学核心:ELBO#
VAE 的套路是引入隐变量 z,假设数据由 p(x) = ∫ p(x|z)p(z) dz 生成,其中 p(z) = N(0, I) 是标准先验。直接算 p(x) 难,VAE 用编码器 q(z|x) 当后验近似,最大化证据下界(ELBO):
逐块拆:
- 重建项
E[log p(x|z)]:采样出的 z 能不能把 x 解回来。我们用高斯解码器,等价于 MSE 重建损失。 - KL 项
KL(q(z|x) || N(0,I)):逼编码器输出的分布靠近标准正态。这一项很关键——它防止编码器”给每个样本一个又尖又窄、彼此离得老远的点”(那样 z 就塌成查表了)。 - 对对角高斯
q(z|x)=N(μ, diag(σ²)),KL 有闭式解:
所以 VAE 的损失 = 重建 MSE + KL,两样都能直接算,不用采样估计梯度。
3. 重参数化技巧:让采样可反向传播#
VAE 训练时瓶颈 z 是从 N(μ, σ²) 采的,采样操作不可导。重参数化(reparameterization trick)把随机性挪到固定噪声上:
这样 z 对 (μ, σ) 可导,梯度能一路回传到编码器参数。整个前向变成:
import numpy as np
def forward(p, Xb, rng):
# 编码器: 两层 tanh -> (mu, logvar)
e1 = np.tanh(p["We1"] @ Xb.T + p["be1"][:, None]) # (enc_h, b)
mu = p["We2m"] @ e1 + p["be2m"][:, None] # (z, b)
logvar = p["We2l"] @ e1 + p["be2l"][:, None] # (z, b)
eps = rng.standard_normal(mu.shape) # 固定的随机源
z = mu + eps * np.exp(0.5 * logvar) # 重参数化
# 解码器: 两层 -> 重建
d1 = np.tanh(p["Wd1"] @ z + p["bd1"][:, None])
out = p["Wd2"] @ d1 + p["bd2"][:, None] # (d, b)
return e1, mu, logvar, z, outpython损失与 KL:
def elbo_and_grads(p, Xb, rng):
e1, mu, logvar, z, out = forward(p, Xb, rng)
b = Xb.shape[0]
recon = np.mean((out - Xb.T) ** 2) # 重建 MSE
kl = np.mean(np.sum(-0.5 * (1 + logvar - mu**2 - np.exp(logvar)), axis=0))
elbo = recon + kl
# 反向: 解码器 d1 需从 z 重算, 再对 (mu, logvar) 求解析梯度
d1 = np.tanh(p["Wd1"] @ z + p["bd1"][:, None])
d_out = 2 * (out - Xb.T) / b
# ... (完整反向见 gen_vae_factor_mining.py, 含 KL 对 mu/logvar 的解析梯度)
return elbo, recon, kl, gradspython实现注记:我们用纯 numpy 手推梯度(不依赖 autograd),是为了让每一步可解释。KL 项对
logvar的梯度是0.5*(exp(logvar)-1),对mu的梯度是mu/b——这两个闭式项正是 VAE 能稳定训练的原因。
4. 看训练:重建与 KL 两条损失同步收敛#
我们用 ELBO = 重建 + KL 当总目标。两条分量单独画出来:

在我们的合成数据上,最终 ELBO=2.84,其中重建=0.84、KL=2.00,KL 占比 70.6%。这个占比很关键:如果 KL≈0,说明编码器退化成”每个样本给个任意尖峰”(后验塌缩),没学到分布;KL 显著 > 0 说明编码器确实在为每个样本维持一个”有宽度的编码高斯”。
5. 瓶颈编码长什么样:点 + 不确定性的二维投影#
取瓶颈里两个强维度做 2D 投影,颜色编码”该样本的总不确定性 Σσ²”:

注意一个真实现象:同样落在特征空间里,有的点编码很确定(冷色),有的点很散(暖色)。这正是 AE 给不了的视角——VAE 把”哪些样本我其实没把握”显式亮了出来。
6. 不确定性能不能当信号过滤器?一个反直觉的结果#
直觉上,“编码很散的样本,因子值不可信,滤掉它们因子应该更干净”。我们实测一下:把样本按总不确定性 Σσ² 中位数切成两半,分别算 VAE 组合因子(瓶颈 5 维对收益的最小二乘投影)的 |IC|:

| 子集 | 与未来收益 |IC| | |---|---| | VAE 组合因子(全样本) | 0.695 | | 低不确定子集(可信信号) | 0.657 | | 高不确定子集(可疑信号) | 0.728 |
结果反直觉:在我们的设定里,高不确定子集的 IC 反而略高于低不确定子集。 这说明什么?——不确定性衡量的是”编码器对特征的把握度”,不是”因子对收益的预测力”。在我们的合成数据里,噪声特征多、信号特征少,编码器恰恰在”信息特征混合得复杂”的样本上更不确定,而这些样本恰好携带更多信号。所以”滤掉不确定的”反而滤掉了一部分真信号。
这个结论必须讲清楚,否则就是自欺:VAE 的不确定性是一个需要被研究、被校准的独立信息,不是现成的”垃圾过滤器”。它真正的用处是(见陷阱 3):当你要拿这个因子去下注时,知道”这批样本的因子值我不信”,从而降权或回避——这是风险管理视角,不是选股 alpha 视角。
7. 五类真实陷阱(不拆穿就是自欺)#
陷阱 1:后验塌缩(posterior collapse)。 如果解码器太强(容量过大),它能直接从 x 的某些维度重建,绕过 z,于是 KL 被压到 ≈0,z 退化成无效的常数。症状:KL 占比 < 5%、换种子结果乱跳。对策:给解码器降容量、或加 β-VAE 的 β>1 强制用 z。
陷阱 2:KL 权重与”信息 vs 不确定性”的权衡。 KL 太大,编码器被先验 N(0,I) 勒得太紧,z 趋同、因子信息被抹掉(|IC| 掉);KL 太小,又塌缩。没有理论最优,要按”下游因子 IC”反复试。本例 KL 占比 70% 是试出来的甜区附近。
陷阱 3:把不确定性误当 alpha 过滤器(最大坑)。 如第 6 节所示,不确定性 ≠ 噪声标志。它在”特征分布边缘”或”信号结构复杂”处都偏大。正确用法是风险加权(对高不确定样本降低因子暴露),而不是”删掉高不确定样本选股”。
陷阱 4:重建目标对异常值极敏感。 我们用 MSE 重建,等价于假设解码噪声是高斯。金融特征里有厚尾异常值,一个极端样本能把重建损失拉爆,编码器跑去拟合离群点。对策:对特征做稳健标准化(winsorize / rank),或用 Laplacian 重建(L1)。
陷阱 5:瓶颈维度与特征信噪比的匹配。 瓶颈 m 太小,信号塞不下(IC 低);m 太大,噪声特征也被编码进来,z 里混进垃圾。经验:先用 PCA 看”解释方差 90% 要几维”,m 取那个数附近,再按下游 IC 微调。
8. 小结:什么时候用 VAE 挖因子#
VAE 适合:你的特征维度高、彼此冗余、且你不仅想要一个因子,还想知道因子值可不可信——比如因子要进组合优化,你想对”不确信的样本”降权。VAE 用 ELBO 同时把”压缩因子”和”不确定性”一起学出来,这是普通 AE 和 PCA 都给不了的。
不适合:你只想要一条确定性因子线(那 PCA / AE 更直接);或者你的数据量不足以让编码器稳定学出分布(小样本下 KL 极易塌缩,不如先用 AE);或者你指望”过滤不确定性”能直接提 alpha(见陷阱 3,本例打脸)。
一句话:VAE = 把编码器从”映射”升级成”分布”。它用 ELBO(重建 + KL)让你免费拿到每个因子值的不确定性——这把尺子用来管风险是真金,用来当选股过滤器却会翻车。
代码与数据完全可复现:本文所有图由
gen_vae_factor_mining.py生成(纯 numpy,无 torch 依赖)。VAE 组合因子 |IC|=0.695、KL 占比=70.6%、不确定性子集 IC 反直觉结果均直接打印在脚本输出里。