halo 的技术博客

返回

你手上 4000 支股票,每只每月一条 8 维特征。你想训个模型预测「次月 alpha」。但标签(次月真实 alpha)又贵又滞后——等你知道答案,行情早走了。

监督学习卡在这:标签不够,模型学歪。但仔细想,「哪些股票像」这件事,根本不需要标签——同行业、同风格、同风险因子的股票,它们的特征向量天然该离得近。对比学习(Contrastive Learning)就是把这条「什么样和什么样像」的先验,变成可训练的 pretext( pretext task),无标签地先学出一套好表征,再拿去下游任务上用少量标签微调。

结论先放这:在标签稀缺时,对比预训练确实能白捡精度。我们的合成数据(42 支股票 / 3 行业,原始特征里行业信号被强噪声淹没)上,下游「少标注探针」——每个行业只给 1~3 个标注、其余用类原型 + 最近邻分类——对比表征把精度从 0.498 / 0.538 / 0.576 抬到 0.574 / 0.614 / 0.614;而且增益随标注变多而收敛(+0.076 → +0.076 → +0.038):标签越少,预训练越值钱;标签多到一定程度,原始特征自己也能凑出来。这正是工业界「先大规模无标签预训练、再小样本微调」的逻辑根基。

SimCLR / InfoNCE:锚点拉近正样本、推远负样本


1. 对比学习在优化什么:拉近「该像的」,推远「不像的」#

核心思想极简:构造「对」(pair),让正对(该像的)在表征空间靠近、负对(不像的)远离

在金融里,「对」怎么定义?最自然的两种:

  • 同行业 / 同板块 = 正对:两只银行股特征该接近;
  • 跨行业 = 负对:银行股和半导体股该拉远。

SimCLR(Chen et al. 2020)的做法是:同一批样本过两次增强,得到两份视图,同一原始样本的两份视图互为正对,其余全当负对。我们这里没做数据增强(金融特征不好「增强」而不泄露),直接用同行业=正对、跨行业=负对这个结构化先验——本质上就是一种**有监督对比(Supervised Contrastive)**的简化版。


2. InfoNCE 损失:把「分类」变成「对比」#

正对拉近、负对推远,写成损失就是 InfoNCE(Oord et al. 2018):

LInfoNCE=ilogexp(sim(zi,zi+)/τ)jexp(sim(zi,zj)/τ)\mathcal{L}_{\text{InfoNCE}} = -\sum_i \log \frac{\exp(\text{sim}(z_i, z_i^+) / \tau)}{\sum_{j} \exp(\text{sim}(z_i, z_j) / \tau)}

分子是同一样本的正对相似度,分母是「它和所有样本(含负对)」的相似度之和,τ\tau 是温度系数。最小化它 = 让正对相似度在全体里「脱颖而出」。

我们用纯 numpy 两层 MLP 当投影头(encoder 把 8 维原始特征压成 4 维表征),端到端用 InfoNCE 训练。关键是从零实现梯度——InfoNCE 对表征 zz 的梯度有个干净闭式:

def info_nce(z):
    sim = (z @ z.T) / tau
    np.fill_diagonal(sim, -1e9)          # 自己不算
    loss = 0.0
    gz = np.zeros_like(z)
    for i in range(N):
        p = np.exp(sim[i]); p /= p.sum()       # softmax 行归一
        G = p.copy(); G[pos_mask[i]] -= 1.0     # 正对位置减 1
        # ∂L_i/∂z_i = (1/τ²) Σ_j G_ij z_j
        gz[i] += (1.0 / tau ** 2) * (G @ z)
        gz += (1.0 / tau ** 2) * np.outer(G, z[i])
        loss += -np.log(p[pos_mask[i]].sum() + 1e-12)
    return loss, gz
python

然后反向过投影头(tanh 链路)更新权重。注意一个踩了大坑才懂的细节:梯度已经对所有样本求和,更新时不要再除以 N;早期版本多除了一次,学习率被吃掉 1000 多倍,训练肉眼看不出动——损失卡在 48.9 一动不动,调了半天才发现是这行 W2 -= lr * gW2 / N/N


3. 合成数据:标签稀缺 + 噪声淹没行业结构#

我们造一个对监督学习不友好、却恰好凸显对比价值的数据:

  • 42 支股票、3 个行业,每只一条 8 维特征;
  • 行业质心拉得很开,但每只股票叠了强噪声(信噪比约 0.7 : 1.1):特征 = 0.7 × 行业质心 + N(0, 1.1)
  • 于是原始特征里行业结构被噪声严重淹没——这正是真实金融嵌入的样子(量价因子里行业信号本就混在噪声里)。
X = np.empty((N, DIM))
for i in range(N):
    X[i] = 0.7 * cen[sec[i]] + rng.normal(0, 1.1, DIM)
# 注意:原始特征不预先 L2 归一化——归一化会把行业结构洗成无信号的单位向量,
# 让对比任务退化(这是另一个踩过的坑)。
pos_mask = (sec[:, None] == sec[None, :]) & ~np.eye(N, dtype=bool)  # 同行业=正对
python

4. 训练结果:损失收敛,表征把行业「聚成团」#

端到端训练 600 轮,InfoNCE 损失从 48.95 收敛到 36.25(稳定下降,不是假收敛)。把学到的 4 维表征做 PCA 投影到 2 维,和原始特征对比:

左:原始特征 PCA,行业被噪声淹没、混成一团;右:对比表征 PCA,同行业聚团、跨行业分离

  • 左(原始特征):三个行业颜色完全混在一起——噪声把行业结构吃光了;
  • 右(对比表征):同行业明显挤成三个紧实的团、彼此分开。

这就是对比学习的「免费午餐」:它没用过任何标签,只用了「同行业该像」这个结构先验,就把被噪声淹没的行业信息,在表征空间里重新显形。


5. 下游验证:少标注探针,对比表征白捡精度#

光「看着聚团」不够,要量化它有没有用。我们做少标注探针(few-shot probe)

  • 每个行业只给 per ∈ {1, 2, 3}标注样本;
  • 用这些标注算每个行业的类原型(均值向量);
  • 其余样本用「最近原型」分类;
  • 对比「直接用原始特征」vs「用对比表征」两种输入的精度。

每行业标注 1/2/3 个:对比表征(紫)全面高于原始特征(灰),增益随标注变多收敛

每行业标注数原始特征精度对比表征精度增益
1 个0.4980.574+0.076
2 个0.5380.614+0.076
3 个0.5760.614+0.038

这条曲线本身就是结论:标签越稀缺(1 个/行业),对比预训练增益越大;标签多到 3 个/行业,原始特征自己也能凑出 0.576,增益收窄到 +0.038。这恰是「大规模无标签预训练 + 小样本微调」范式成立的理论直觉——预训练的价值,在标签贵的角落最大


6. 五类真实陷阱(不拆穿就是自欺)#

陷阱 1:正/负对定义错了,学出来的是垃圾。 对比学习 90% 的效果取决于「什么算正对」。用「随机两个样本当负对」在金融里会出事——两只同行业的股票可能被当成负对,模型被教去把它们推远。对策:正对用结构化的「同行业 / 同板块 / 同因子暴露」,别用随机增广(金融特征不好做不泄露的增广)。

陷阱 2:温度 τ 是隐形旋钮。 τ 太小→对比过尖锐、只盯着最难负对,训练不稳;τ 太大→所有样本差不多,梯度消失。本文用 τ=0.1,实际要网格扫一下。

陷阱 3:表征空间坍缩(collapse)。 最经典的死法:所有样本被映射到同一个点(或同一方向),相似度全 1,损失看着低实则啥也没学。本文靠「正对只减 1、分母含全部负对」的 InfoNCE 结构天然防坍缩;若上更复杂架构,要加投影头 + 停止梯度 + 负对队列等防坍缩手段。

陷阱 4:原始特征别乱归一化。 我们踩过的坑:把原始特征 L2 归一化后,所有行变成单位向量、行业结构被洗平,对比任务直接退化成「区分噪声方向」,训练损失卡死不动。原始特征保持原尺度,只归一化「学出来的投影 z」。

陷阱 5:分布外行业 / 新标的,表征未必靠谱。 对比预训练学的是「训练样本里的相似结构」。一只全新行业、或结构已漂移的老股票,它的表征可能落在奇怪的位置。生产里要定期用新数据重训投影头,别拿半年前的表征当永恒真理。


7. 小结:对比学习该怎么用#

对比学习不是「又一个深度学习花活」,它是在标签贵、标签少的金融场景里,把「无标签数据」榨出结构的正道:

  • 它适合预训练阶段:海量无标签的量价 / 基本面 / 新闻嵌入,先无监督学出「什么和什么像」的表征;
  • 它最适合下游标签稀缺的迁移:新市场、新标的、少标注的细分任务,拿预训练表征当起点,少样本就能起量;
  • 它的命门是正对定义防坍缩——这俩错了,剩下全是自欺。

一句话:对比学习不预测「它是什么」,只学「它和谁像」——而在金融里,「像谁」往往比「是什么」更早、更便宜、更可学。

代码与数据完全可复现:本文所有图由 gen_contrastive_finance.py 生成(纯 numpy,无 torch/sklearn 依赖)。InfoNCE 损失 48.95→36.25、少标注探针精度 0.498/0.538/0.576 → 0.574/0.614/0.614,均直接打印在脚本输出里。正对用「同行业」结构化先验,原始特征不做 L2 归一化。

对比学习金融表征:不用标签,也能把「同行业」和「跨行业」拉开
https://blog.halo26812.eu.org/blog/contrastive-learning-finance
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨