halo 的技术博客

返回

你手里有一堆量价特征: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 不升反略降——这恰恰说明不确定性不是现成的信号过滤器,它是一把需要校准的尺子,下文五类陷阱会拆穿

VAE 把每个样本编码成一个高斯分布,瓶颈里既有因子值也有不确定性


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):

logp(x)Eq(zx)[logp(xz)]重建项KL(q(zx)p(z))正则项ELBO\log p(x) \ge \underbrace{\mathbb{E}_{q(z|x)}[\log p(x|z)]}_{\text{重建项}} - \underbrace{\mathrm{KL}\big(q(z|x)\,\|\,p(z)\big)}_{\text{正则项}} \equiv \mathrm{ELBO}

逐块拆:

  • 重建项 E[log p(x|z)]:采样出的 z 能不能把 x 解回来。我们用高斯解码器,等价于 MSE 重建损失。
  • KL 项 KL(q(z|x) || N(0,I)):逼编码器输出的分布靠近标准正态。这一项很关键——它防止编码器”给每个样本一个又尖又窄、彼此离得老远的点”(那样 z 就塌成查表了)。
  • 对对角高斯 q(z|x)=N(μ, diag(σ²)),KL 有闭式解
KL=12j=1m(1+logσj2μj2σj2)\mathrm{KL} = -\frac{1}{2}\sum_{j=1}^{m}\big(1 + \log \sigma_j^2 - \mu_j^2 - \sigma_j^2\big)

所以 VAE 的损失 = 重建 MSE + KL,两样都能直接算,不用采样估计梯度。


3. 重参数化技巧:让采样可反向传播#

VAE 训练时瓶颈 z 是从 N(μ, σ²) 采的,采样操作不可导。重参数化(reparameterization trick)把随机性挪到固定噪声上:

z=μ+σϵ,ϵN(0,I)z = \mu + \sigma \odot \epsilon, \qquad \epsilon \sim N(0, I)

这样 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, out
python

损失与 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, grads
python

实现注记:我们用纯 numpy 手推梯度(不依赖 autograd),是为了让每一步可解释。KL 项对 logvar 的梯度是 0.5*(exp(logvar)-1),对 mu 的梯度是 mu/b——这两个闭式项正是 VAE 能稳定训练的原因。


4. 看训练:重建与 KL 两条损失同步收敛#

我们用 ELBO = 重建 + KL 当总目标。两条分量单独画出来:

VAE 训练中重建损失与 KL 散度同步下降,最终 ELBO 收敛

在我们的合成数据上,最终 ELBO=2.84,其中重建=0.84、KL=2.00,KL 占比 70.6%。这个占比很关键:如果 KL≈0,说明编码器退化成”每个样本给个任意尖峰”(后验塌缩),没学到分布;KL 显著 > 0 说明编码器确实在为每个样本维持一个”有宽度的编码高斯”。


5. 瓶颈编码长什么样:点 + 不确定性的二维投影#

取瓶颈里两个强维度做 2D 投影,颜色编码”该样本的总不确定性 Σσ²”:

VAE 学到的 5 维编码:颜色越亮,样本编码越不确定

注意一个真实现象:同样落在特征空间里,有的点编码很确定(冷色),有的点很散(暖色)。这正是 AE 给不了的视角——VAE 把”哪些样本我其实没把握”显式亮了出来。


6. 不确定性能不能当信号过滤器?一个反直觉的结果#

直觉上,“编码很散的样本,因子值不可信,滤掉它们因子应该更干净”。我们实测一下:把样本按总不确定性 Σσ² 中位数切成两半,分别算 VAE 组合因子(瓶颈 5 维对收益的最小二乘投影)的 |IC|:

编码不确定性本身不是信号过滤器:本例中高不确定子集的 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 反直觉结果均直接打印在脚本输出里。

变分自编码器 VAE 因子挖掘:把不确定性也一起学出来
https://blog.halo26812.eu.org/blog/vae-factor-mining
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨