推荐系统(十一)—— 对比学习与自监督学习
对比学习在推荐系统里到底怎么用?这一篇把 InfoNCE、温度系数、SimCLR/MoCo 的负样本来源、SGL 的图增广、CL4SRec 的序列增广、XSimGCL 的极简噪声扰动讲清楚,配 PyTorch 实现与原理图。
经典推荐模型只学一种信号:用户有没有点、看完没、买没买。这种监督信号很宝贵,但也极其稀疏——大多数用户接触不到目录里 1% 的物品,大多数物品也接触不到 0.1% 的用户,新上线的用户和物品则干脆没有任何记录。直接拿这种稀疏标签去优化,几乎注定会在头部过拟合、在尾部沉默。
对比学习换了一笔交易:它不再问"这个样本的标签是什么",改问"哪两个样本应该长得像、哪两个应该长得不一样"。这个问题的答案可以从数据本身免费得到——把同一个用户/物品/序列以两种不同方式扰动一下,扰动出的两个版本就是一对正样本,同一个 batch 里其他样本则全是负样本。模型由此学习到几何结构:相似的东西在 embedding 空间里靠近,不相似的远离。等几何结构学好了,下游有监督的推荐头只要轻轻一推就够了。
本文先把对比学习这台机器的核心零件拆开讲清楚——InfoNCE、温度系数、增广策略——再分别介绍推荐里真正用得上的四类做法:SimCLR 风格的 batch 内对比、MoCo 风格的队列对比、SGL 的图增广、CL4SRec 的序列增广。最后我们看一个有点反直觉的结论:XSimGCL 把所有花哨的增广都丢掉,只往 embedding 上加一点噪声,效果反而更好。
你将学到什么
- 为什么对比学习处理稀疏性、冷启动、流行度偏置的思路和"再多收点数据"完全不同
- InfoNCE 细讲:损失从哪来、温度系数 $\tau$ 为什么如此关键、它对梯度的形状意味着什么
- SimCLR vs MoCo:batch 内负样本 vs 动量编码器 + 队列负样本,分别什么时候用
- SGL(Wu 等,SIGIR 2021):节点丢弃、边丢弃、随机游走子图三种图视图
- CL4SRec(Xie 等,ICDE 2022):crop / mask / reorder 三种序列增广
- SimGCL / XSimGCL(Yu 等,2022/2023):为什么一个简单的 embedding 噪声扰动能打平精心设计的图增广
- 每一块都有可读的 PyTorch 实现
前置知识
为什么推荐系统需要对比学习
重新审视稀疏性
稀疏性不是简单的"标签太少",它是结构性的:
- 冷启动:新用户/新物品的交互数为零,任何依赖它们做特征的模型(矩阵分解、双塔、GNN)都无法把它们放到 embedding 空间里有意义的位置。
- 头部过拟合:长尾点击分布下,损失被几个热门物品主导。一个把热门物品背下来的模型在训练集上看起来很好,��尾部物品却毫无判别力。
- 流行度偏置:即便候选池里有多样性,最终的打分头依然会把热门物品推到前面——因为这就是训练损失奖励的方向。整个系统会塌缩到"什么都推同样几个东西"。
对比学习换来的是什么
对比学习用一笔不同的交易:放弃昂贵的标签,换来便宜得多的"扰动一致性"信号。设想把一个用户的行为子图随机丢掉 20% 的边再编码一次,再用不同的丢边方式编码第二次。两个视图,同一个用户。规则很简单:这两个 embedding 应该几乎一模一样,而 batch 里其他用户的 embedding 应该和它们都不一样。
这一个目标同时给你三样东西:
- 一份免费的训练信号,量可以无限大(任何用户都能扰动任意多次)
- 一种表示先验:模型学到的特征是那些在扰动下还能保持的——根据定义就是鲁棒的特征,恰好是冷启动和长尾物品最需要的
- 一个对抗塌缩的正则项:损失明确地把不同用户在 embedding 空间里推开,不让它们都挤到热门物品周围

上图基本上把对比学习的全部直觉讲完了。蓝色锚点是某个用户,两个绿色点是同一用户的两个增广视图,把它们拉到一起;琥珀色点是同一 batch 里其他用户,把它们推开。对每个用户、每个 batch 都做这件事,整个 embedding 空间就会被塑造成"语义相似 = embedding 相似"的几何结构。
InfoNCE:真正在干活的损失函数
几乎所有对比式推荐模型用的都是 InfoNCE 损失(van den Oord 等,2018)的某种变体。给定锚点 $x$、正样本 $x^+$、一组负样本 $\{x_i^-\}$,编码器 $f$,相似度 $\mathrm{sim}(\cdot,\cdot) = z\cdot z'$(在 $\ell_2$ 归一化后的 embedding 上做点积):
$$ \mathcal{L}_{\text{InfoNCE}} = -\log \frac{\exp\!\big(\mathrm{sim}(f(x), f(x^+)) / \tau\big)}{\exp\!\big(\mathrm{sim}(f(x), f(x^+)) / \tau\big) + \sum_{i} \exp\!\big(\mathrm{sim}(f(x), f(x_i^-)) / \tau\big)} $$这其实就是一个 $(N+1)$ 类分类器的交叉熵,正确类别是"那个正样本"。分子让正样本的概率变大;分母强迫模型把正样本排在所有负样本之前。这种排序压力,就是阻止"所有 embedding 塌缩到同一个向量"这个平凡解的关键。
温度系数:被严重低估的旋钮
温度 $\tau$ 控制 softmax 区分正负的锐利程度。它不是一个装饰性超参,它会改变损失到底在优化什么。

从右边那张图能直接读出两点结论:
- 小 $\tau$(0.05):梯度在判决边界附近极其陡峭。模型几乎把全部容量都花在最难的负样本上——那些与正样本相似度几乎一样高的样本。这有利于学细粒度区分,但当那些"困难负样本"其实是被误标的(比如缺失非随机的点击),训练就会不稳定。
- 大 $\tau$(1.0):梯度在所有负样本上摊薄,连容易的也分到一份。优化平滑,但聚类松散。
- 推荐场景的甜点区大致在 $\tau \in [0.1, 0.2]$——足够锐利能学到有用对比,又足够柔和不被噪声带跑。
一个简单的脑模型:$1/\tau$ 是"放大倍数"。把 $\tau$ 减半,每对相似度的差距就被放大一倍,梯度推近-正样本和近-负样本分开的力度也翻一倍。
为什么离不开负样本
如果只优化分子——把正样本拉到一起——那所有 embedding 塌缩到同一个常向量,损失就会愉快地降到零。负样本不是配菜,是承重的约束。这也解释了为什么在 SimCLR 风格的训练里 batch size 影响这么大:batch 256 时每个锚点有 510 个负样本,batch 4096 时有 8190 个。负样本越多,分母越密,对比信号越强。
SimCLR vs MoCo:负样本从哪儿来
视觉自监督的两大范式被原封不动地搬进了推荐:

SimCLR(Chen 等,ICML 2020)走极简路线:一个编码器、一个投影头、每个样本两次增广,同一 minibatch 里其他视图全是负样本。代价显而易见——负样本数量被 GPU 显存限制住了。
MoCo(He 等,CVPR 2020)把两边解耦:query 编码器用 SGD 正常更新,动量 key 编码器以指数滑动平均跟随 $\xi \leftarrow m\xi + (1-m)\theta$,再维护一个 FIFO 队列保存最近 $K$ 个 key 的编码(典型 $K=65{,}536$)。负样本来自队列,$K$ 与 batch size 完全解耦。
放到推荐里,SimCLR 模式更常见:CTR/CVR 训练本身 batch 就大;编码器通常是 GNN,前向开销主要在图遍历而非 embedding 查表;而维护一个上百万的用户 key 队列其实没什么意义,因为 embedding 表本身一直在被更新,过两步队列里的旧 key 就和新 query 不在同一个空间了。MoCo 风格的队列在长序列检索模型里更常见——那种场景编码器很重,确实需要把它的开销摊到大量锚点上。
SimCLR 损失的参考实现
| |
这十几行里有三处不那么显然的细节值得提一下:
- embedding 进来之前必须已经归一化。归一化后的点积就是余弦相似度,温度 $\tau$ 也才有上面那种几何意义。
- 对角线用 $-\infty$ 屏蔽,而不是 0。
cross_entropy工作在 log-softmax 空间,$-\infty$ 在分母里直接消失;用 0 会留一个 $e^0=1$ 的项,悄悄改变梯度。 - 损失是对称的:$2B$ 行各贡献一个交叉熵。前 $B$ 行把第二组视图当目标,后 $B$ 行把第一组视图当目标。
SGL:在用户-物品图上做对比学习
SGL(Wu 等,SIGIR 2021,Self-supervised Graph Learning for Recommendation)是把对比学习真正带进推荐主流的工作。思路是在 LightGCN 主干上挂一个 InfoNCE 头,把两个被随机扰动过的用户-物品图当作每个节点的一对正样本视图。
三种扰动图的方式

- 边丢弃(Edge Dropout, ED):每条边以概率 $1-p$ 独立保留。便宜、保持结构、用得最多。
- 节点丢弃(Node Dropout, ND):每个节点(连同它的全部边)以概率 $p$ 被丢掉。增广更强,但可能让小度节点训练不稳。
- 随机游走子图(Random Walk, RW):从每个锚点出发做长度 $L$ 的随机游走,把走过的子图作为视图。
SGL 原论文的消融显示,边丢弃在大多数数据集上稳定地匹配甚至略胜另外两种,而且实现最简单。没有特别理由的话直接用 ED。
完整的 SGL 训练步
| |
几个不那么显然的实现细节:
- 三次前向传播,不是两次。对比视图是从原图独立丢边出来的,BPR 用的是干净的原图。如果让 BPR 共用某个被丢过边的视图,监督信号会被偶然存活下来的边带偏。
- 对比损失在节点层面计算,作用对象是用户和物品 embedding 的拼接。这样二部图的两边都能拿到自监督信号。
- 损失权重 $\lambda$ 很关键。太小($<10^{-2}$)对比信号几乎消失;太大($>1$)BPR 抓不住推荐目标。SGL 论文在 $\{0.005, 0.05, 0.1, 0.5, 1.0\}$ 上扫了一遍,结论是 $0.1$ 是个稳健默认值——从这里起步。
CL4SRec:序列推荐里的对比学习
对于基于序列的推荐器(SASRec、BERT4Rec、GRU4Rec……),同样的问题变成"怎么给同一条行为序列造两个视图"。CL4SRec(Xie 等,ICDE 2022)提出的三种增广已经成为事实上的默认。
![同一条行为序列上的三种增广:crop 截取连续片段,mask 把若干位置替换为 [M],reorder 打乱一段连续区间](./11-%e5%af%b9%e6%af%94%e5%ad%a6%e4%b9%a0%e4%b8%8e%e8%87%aa%e7%9b%91%e7%9d%a3%e5%ad%a6%e4%b9%a0/fig5_cl4srec_augmentations.png)
- Crop:保留长度为 $\eta L$ 的连续子序列。保住了局部顺序,让模型对"晚一点开始"或"早一点结束"具有不变性。
- Mask:随机把 $\gamma$ 比例的位置换成特殊 token
[M]。和 BERT 的 masked language modelling 一样的思路——编码器必须从上下文反推被遮挡的物品。 - Reorder:把一段长度 $\beta L$ 的连续区间打乱。教会模型一个 session 内精确位置没那么重要,物品的集合本身才更稳定。
CL4SRec 的做法是每个视图随机抽一种增广,于是 (视图 A, 视图 B) 就有九种组合。这种随机混合本身也是一种正则——编码器没法对单一增广策略过拟合。
| |
编码器用你已有的任何序列模型即可(Transformer、GRU 等)。把最后一层隐藏状态池化、投影、归一化,丢进上面写好的 info_nce,再作为辅助损失加到原本的 next-item 预测上就行。
XSimGCL:连增广本身都不重要
Yu 等(2022/2023)的一项颇为反直觉的实证结果值得单独一节。他们问:SGL 的提升究竟有多少来自图增广本身、有多少来自对比损失?答案是:几乎全来自损失。把图丢边换成在传播得到的 embedding 上加��点点均匀噪声,效果就能持平甚至超过 SGL,而且省掉了图扰动的全部实现成本。
具体技巧(SimGCL / XSimGCL,后者是精简版)大致是:
- 正常跑 LightGCN 的传播。
- 在每一层往 embedding 上加一个小噪声 $\Delta$,方向取单位球面上的随机向量再缩放到很小的 $\epsilon$(比如 0.1),并且强制噪声与原向量同向(避免翻号)。
- 用不同的噪声样本再跑一遍传播,得到第二个视图。完全不动图。
| |
更深的洞察来自 SimGCL 的分析:对比损失同时在做两件事——
- 对齐(alignment):把正样本对拉近(分子在做的事)。
- 均匀(uniformity):把所有 embedding 在单位超球面上推得越均匀越好(分母在做的事)。
而 uniformity 才是打破流行度偏置的关键正则项,它是主导效应。一旦有了 uniformity,第二个视图具体怎么造(图丢边、embedding 噪声、还是别的合理扰动)几乎都无所谓。
这些方法到底有多大提升

上图的具体数值是 SGL(Wu 等,2021)和 SimGCL/XSimGCL(Yu 等,2022/2023)原论文在 Yelp2018、Amazon-Book、Alibaba-iFashion、Gowalla 等数据集上报告幅度的示意。结论一致:给同一个主干加上对比辅助损失,Recall@20 和 NDCG@20 通常能提升 5%–20%,而且增益几乎集中在尾部物品和冷启动用户身上——正是你最在意的地方。
这种增益在 embedding 空间里是什么样:

训练前,物品 embedding 几乎只沿着"流行度"这一根主轴变化,语义结构都被埋住了。训练后,物品按兴趣类别分裂成紧凑、彼此分开的小簇。下游打分头的活就好干多了——一个近似线性的边界基本就够。
常见问题
既然能多收数据,为什么还要用对比学习?
冷用户拿不到"更多数据"——他们就是因为没数据才叫冷。尾部物品也拿不到"更多数据"——尾部本身就是因为几乎没人看才在尾部。对比学习造出的训练信号不需要新交互,只需要把已有交互扰动一下。这和"再多收点点击"是完全不同的事情。
怎么在图增广和 embedding 噪声之间选?
默认用 embedding 噪声(XSimGCL):更快、更简单,最近的文献基本一致认为它能持平或超过图增广。真正需要图增广的场景是:你想顺便正则化 GNN 的结构性归纳偏置,或者你对哪些边/节点最关键有可靠的先验。
温度系数怎么定?
从 $\tau = 0.2$ 起步。如果你的"困难负样本"基本上是真正的负样本(比如有可靠的 dwell-time 信号),把 $\tau$ 降到 0.05–0.1 来加强对比;如果负样本噪声大(比如在百万级目录里随机抽 batch 用户做负样本),保持 $\tau \geq 0.2$ 以免被假负样本带偏。在小网格上扫一下就行,损失对 $\tau$ 是平滑的。
投影头还要不要?
SimCLR / SGL 这类在 GNN 上做对比的,要——而且训练完就丢掉。投影头让编码器的中间表示保持通用,把对比度量的特化任务交给投影头。XSimGCL 是个例外,它直接对传播后的 GNN embedding 做对比,不用投影头照样工作。
对比损失和推荐损失怎么组合?
$$\mathcal{L}_{\text{total}} = \mathcal{L}_{\text{rec}} + \lambda \cdot \mathcal{L}_{\text{CL}}$$从 $\lambda = 0.1$ 起步。线上效果敏感时再在 $\{0.01, 0.05, 0.1, 0.5, 1.0\}$ 里扫一下。$\lambda$ 的敏感度比 $\tau$ 低很多,别在这上面浪费调参预算。
这套方法对隐式反馈管不管用?
管用,而且比对显式反馈更适合。整套框架默认每个交互都是正标签,让 InfoNCE 的结构隐式处理负样本。SGL、SimGCL、XSimGCL、CL4SRec 全是为隐式反馈(点击、播放、购买)设计的。
数据少到什么程度还能用?
对比学习恰恰在监督信号最稀缺的时候帮助最大。SGL 在 Yelp2018(稀疏度约 99.87%)上的提升比稠密数据集上大得多。低于 ~10K 交互时,随机增广的方差会主导,可能需要更强的先验(跨域迁移、内容特征);超过 ~100K 交互时,应该能稳定看到改进。
怎么评估对比式推荐器?
标准 top-K 指标(Recall@K、NDCG@K、HR@K)做基线对齐,然后额外关注:
- 冷启动切片:按交互数把用户/物品分桶,分桶报告指标。
- 长尾覆盖:推荐列表里长尾物品的占比(通常长尾定义为按流行度排序的后 80%)。
- embedding 诊断:t-SNE/UMAP 可视化;按 Wang & Isola(2020)算 uniformity($\log \mathbb{E}\,e^{-2\|z_i - z_j\|^2}$)和 alignment($\mathbb{E}\,\|z - z^+\|^2$)。高 uniformity + 低 alignment 才算健康。
总结
对比学习解决了一个加再多数据也解决不了的问题:它给模型提供了一种从稀疏交互中学几何的方法,方法是要求模型在扰动下保持一致。2024 年的工程配方已经稳定下来:
- 挑一种增广,或者干脆不用增广(XSimGCL 噪声)。
- 套一个 InfoNCE,$\tau \approx 0.2$。
- 作为辅助损失加上去,权重 $\lambda \approx 0.1$,挂在你已有的有监督目标上。
基于图的推荐器,从 XSimGCL 起步——能跑通的最简方案就是它。如果想要一个更可解释的基线,SGL + 边丢弃依然是个稳健选择。序列推荐器,CL4SRec 那三种增广是默认菜单。所有场景下,最大的收益都集中在冷启动和长尾。
系列导航
本文是推荐系统 16 篇系列的第 11 篇。
| 上一篇 | 下一篇 | |
|---|---|---|
| 第十篇:深度兴趣网络与注意力机制 | 所有文章 | 第十二篇:大语言模型与推荐系统 |