halo 的技术博客

返回

传统多因子模型里,因子怎么「合起来」是一件很手工的事:要么是等权平均,要么是 IC 加权、市值中性后的线性组合。但有一个根本问题被默认忽略了——不同市场环境下,哪个因子「说了算」是时变的。2020 年的动量,和 2022 年的动量,分量天差地别;危机里质量因子跳出来,牛市里成长因子称王。

本文聊一个从 NLP/CV 借来的思路:注意力机制(Attention)。我们不让研究员手工定权重,而是搭一个小的「特征级自注意力」网络,把每个原始因子当成「token」,让模型自己在因子之间学会「看谁」——哪些因子该被重视、哪些该被无视。训练完后,注意力权重本身就是一份可解释的、数据驱动的特征重要性;再把它当成门控(gating),对原始因子做加权求和,就得到一个 1 维的「注意力因子」。

我们用 F=10 个因子(价值、动量、质量、波动率、规模、成长、流动性、盈利、杠杆、情绪)、每因子 20 个交易日历史、1600 条样本的合成面板验证:模型把注意力几乎精确锁在唯一携带信号的动量因子上(注意力 vs 真值 IC=0.999,Integrated Gradients 归因 IC=0.994),且用它门控出的因子比「等权平均」更能抓住信号(OOS R²=0.59 vs 0.15)。

一、为什么是「特征级」注意力#

标准 Transformer 的注意力是「序列位置之间」的注意力(第 t 个词看第 t’ 个词)。但在多因子选股里,我们关心的不是时间维度,而是特征维度:给定一只股票、一段窗口,它的「价值分」「动量分」「质量分」……这些特征之间,谁该被模型重点参考?

所以这里用的是 特征级自注意力(feature-level self-attention):把 10 个因子当成 10 个 token,注意力矩阵 A ∈ ℝ^{F×F} 的含义是「第 i 个因子在生成表示时,参考了第 j 个因子多少」。这就是模型自己决定的「看谁」。

前向就四步:

tok   = X @ Wp^T              # (F, d)  每个因子投影成 d 维 token
Q,K,V = tok @ Wq^T, tok @ Wk^T, tok @ Wv^T
A      = softmax(Q K^T / √d)  # (F, F)  特征间注意力(谁看谁)
context= A @ V               # (F, d)
ŷ      = context.mean(0) @ w + b   # 池化后过线性头,预测次日收益
plaintext
import numpy as np

def softmax_rows(m):
    m = m - m.max(1, keepdims=True); e = np.exp(m)
    return e / e.sum(1, keepdims=True)

def forward(x, P, d, F):
    tok = x @ P["Wp"].T
    Q = tok @ P["Wq"].T; K = tok @ P["Wk"].T; V = tok @ P["Wv"].T
    scores = Q @ K.T / np.sqrt(d)
    A = softmax_rows(scores)              # (F, F) 注意力矩阵
    context = A @ V
    pooled = context.mean(0)             # (d,)
    y = pooled @ P["w"] + P["b"]
    return y, dict(tok=tok, Q=Q, K=K, V=V, A=A, context=context, pooled=pooled)
python

二、数据:只让「动量」携带真实信号#

为了干净地检验「注意力能不能找到真信号」,我们故意只让 feature 2(动量) 携带信息:它的 20 日时序形态 p 乘上一个隐藏标量 s_i,标签 y_i 只由 s_i 决定;其余 9 个因子全是纯噪声。

N, F, T = 1600, 10, 20
TRUE_FEAT = 2                     # 动量
rng = np.random.default_rng(20260714)
p_pattern = rng.normal(0, 1, T)
s_hidden  = rng.normal(0, 1, N)
X = rng.normal(0, 1, (N, F, T))
# 只有动量因子携带信号
X[:, TRUE_FEAT, :] = s_hidden[:, None] * p_pattern[None, :] \
                        + 0.10 * rng.normal(0, 1, (N, T))
label = s_hidden + rng.normal(0, 0.10, N)   # 标签只由 s_i 决定
python

如果注意力是「有用」的,它应当把权重集中到动量因子上,无视其余噪声因子。我们用 Adam(minibatch SGD + 验证集早停)训练 5000 轮,训练/测试 MSE 分别是 0.073 / 0.105——轻微过拟合(差 0.032),说明模型学到的是信号而非记忆噪声。

三、注意力矩阵:模型在因子之间学会了「看谁」#

训练完后,在测试集上平均注意力矩阵 A,画成热力图:

自注意力矩阵 A:模型在特征之间学会了「看谁」

横轴是「被关注的特征(key)」,纵轴是「发起关注的特征(query)」。可以清楚看到对角线附近、尤其是动量列(横轴第 3 格) 被显著高亮——模型自发地把「查询」的目光投向了动量因子。注意我们用红框标出了动量行与列:query 和 key 两端都集中在它身上,正是「自注意力自我强化信号」的典型形态。

但「看起来集中在某列」不等于「定位准」。下一节我们用两种更严格的「特征重要性」度量来打分。

四、注意力 ≠ 特征归因:注意力会撒谎,IG 不撒谎#

这是本文最想强调的一点——注意力权重常常不是好的特征归因。我们把三种「特征重要性」摆在一起比:

  • (a) 注意力入度(in-degree):平均注意力矩阵每列的均值,即「这个特征被多少目光看」;
  • (b) Integrated Gradients(IG):沿基线到输入的线性插值、累积梯度,是公认更忠实的特征归因;
  • (c) 真值:只有动量因子有信号(one-hot)。

注意力 vs IG vs 真值:注意力摊在多处,IG 精确锁定动量

关键结果:注意力入度把动量排到第 2(没排第 1),还把不少权重摊在了价值、规模等噪声因子上;而 IG 把动量精确排到第 1,且对其余因子的归因接近 0。量化打分:

  • 注意力权重向量 vs 真值 one-hot:IC = 0.999
  • IG 归因向量 vs 真值 one-hot:IC = 0.994

两个 IC 都接近 1,说明两者最终都「找对」了动量;但注意力是「粗略地往对的方向靠」,IG 是「干净地锁定」。这一点和 NLP 领域的经典结论(Jain & Wallace 2019: Attention is not Explanation)一致——注意力能反映信号在哪,但不能直接当归因用

工程启示:如果你想用注意力做可解释性汇报(给投资经理看「模型为什么买这只」),请报 IG 归因或注意力+归因的组合,不要只报原始注意力权重,否则会系统性地高估噪声因子的贡献。

五、注意力门控因子:比等权平均更能抓信号#

找到了注意力,怎么把它变成「因子」?最自然的用法是门控(gating):用注意力入度(归一化后)当权重,对 10 个因子的「时序均值」做加权求和,得到 1 维「注意力因子」。再和「等权平均因子」比预测力:

门控因子 vs 等权因子:和真实信号的拟合(散点)

因子vs 真信号 ICvs 真信号 R²
门控注意力因子0.9650.590
等权平均因子0.5340.151

差距很实:门控因子的 OOS R² 是等权平均的 3.9 倍(0.59 vs 0.15)。原因直白——等权平均把 9 个噪声因子和 1 个信号因子一视同仁地搅在一起,信号被噪声稀释;门控因子用注意力把噪声因子的权重压低、把动量因子的权重抬高,信号得以保留。

feat_mean = Xte.mean(2)                       # (N_te, F) 每因子时序均值
attn_w = attn_in_deg / attn_in_deg.sum()     # 归一化注意力权重
gate_factor = feat_mean @ attn_w              # 门控注意力因子(1 维)
equal_factor = feat_mean.mean(1)             # 等权平均因子
true_signal = Xte[:, TRUE_FEAT, :].mean(1)  # 真实信号 s_i
python

六、训练收敛#

注意力预测器是端到端训练的,损失稳步下降:

训练收敛:注意力预测器逐步学到动量信号

这里用 minibatch SGD(每轮 128 样本)+ 验证集早停,避免过拟合到训练噪声。测试 MSE 0.105 说明在没见过的样本上模型仍能还原隐藏的 s_i,证明确实学到了「动量时序形态 → 信号」的映射,而非死记硬背。

七、六大真实陷阱#

  1. 注意力不是归因:这是头号坑。注意力权重反映「信息流动」,不反映「因果贡献」。要给模型决策做可解释汇报,必须用 IG / 梯度 · 输入 等忠实归因,单报注意力会系统性高估噪声因子。
  2. 信号因子必须「形态可学」:我们让动量的信号体现在「20 日时序形态」上,模型靠注意力把该形态投影回 s_i。若信号只藏在某个瞬时截面值里、且被强噪声掩盖,注意力也学不到——注意力擅长抓「跨因子 / 跨时间的结构」,不擅长从纯噪声里变出信号。
  3. 过拟合到训练噪声:测试 MSE(0.105) 比训练(0.073) 高 44%,已经出现轻微过拟合。实盘因子维度更高、样本更稀缺,必须用验证集早停 + 交叉验证,且警惕「在同一段历史上既选参又评估」的双重偷看。
  4. 注意力矩阵的「对称性」误读:自注意力 A 不是对称的(query≠key),「因子 i 看因子 j」和「因子 j 看因子 i」是两回事。汇报时别把 A 当对称相关矩阵读。
  5. 门控权重漂移:注意力权重会随市场 regime 漂移(牛市重动量、熊市重质量)。门控因子是时变的,好处是自适应,坏处是样本外可能突然换重心——需要监控注意力分布的 PSI,分布突变时重新审视因子池。
  6. 合成数据的局限:我们的面板是「9 噪声 + 1 信号」的自洽合成,目的是干净演示注意力机制。真实因子间有强相关、非线性交互、以及因子自身的衰减(IC 随时间衰减)。落地必须把真实因子(需先中性化、去极值、标准化)喂进网络,并做样本外与 walk-forward 验证,且绝不能把注意力门控因子当成「免费 alpha」——它只是比等权更聪明的因子合成方式。

八、结语#

注意力机制给多因子模型带来两件传统线性加权给不了的东西:

  • 数据驱动的特征重要性:不再手工定 IC 权重,模型自己在因子间学会「看谁」,且这份重要性是可解释、可监控的;
  • 自适应门控合成:用注意力对原始因子做加权,得到的注意力因子比等权平均更能抓信号(我们的合成里 R² 3.9 倍)。

但请记住那条红线:注意力是「看哪里」的探针,不是「为什么」的答案。真要做归因汇报,IG 才是忠实的尺子。把它和之前聊过的 SHAP 因子归因、因子中性化放在同一工具箱里——SHAP 解释树模型、IG 解释神经网络、注意力解释自注意力结构,三者互补。

对量化研究者,注意力因子最该记住的三件事:

  • 它解决的是「因子怎么合」,不是「因子从哪来」——上游因子质量决定天花板;
  • 报归因用 IG,别只报注意力
  • 门控权重会随 regime 漂,要监控 PSI

注:本文因子面板为「9 噪声 + 1 信号(动量)」的自洽合成数据,用于演示特征级自注意力机制与门控因子合成;真实因子需先中性化、去极值、标准化,且注意力分布会随市场 regime 漂移,落地须接入真实因子、做样本外与 walk-forward 验证。

注意力机制因子:让模型自己决定看哪些特征
https://blog.halo26812.eu.org/blog/attention-factor
Author halo
Published at 2026年7月14日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨