表征学习的困境
在推荐系统的世界里,无论是经典的协同过滤,还是前沿的深度学习模型,其核心任务之一都可以归结为表征学习 (Representation Learning)。简单来说,就是为系统中的每一个用户(User)和每一个物品(Item)学习一个低维、稠密的向量,我们称之为嵌入向量 (Embedding)。
这个Embedding的质量直接决定了推荐系统的上限。一个理想的Embedding空间应该具有这样的特性:如果用户 对物品 感兴趣,那么在向量空间中,用户 的向量 和物品 的向量 的距离应该非常近。反之,如果用户不感兴趣,它们的距离就应该很远。
传统的监督学习方法,例如在CTR(点击率预估)任务中广泛使用的模型,通常将推荐问题建模为一个二分类或多分类问题。假设我们有一个用户 和一个物品 的交互记录(比如点击了),我们将其视为一个正样本(label=1)。模型的任务就是预测用户 点击物品 的概率。
这通常通过一个预测函数 来实现,例如取两个向量的内积后通过Sigmoid函数:
然后,使用交叉熵损失 (Cross-Entropy Loss) 或对数损失 (Log Loss) 来优化模型参数(包括这些Embedding向量)。对于一个正样本 ,损失函数可以简化为:
为了让模型不仅仅是把所有样本都预测为正,我们还需要负样本。但问题来了:在推荐系统中,我们通常只有“用户与物品发生了交互”这样的显式正反馈,而缺少“用户明确表示不喜欢这个物品”的显式负反馈。用户没有与一个物品交互,可能是因为不喜欢,也可能仅仅是没看到。
因此,我们不得不进行负采样 (Negative Sampling)。最常见的做法是,对于每个正样本 ,我们从用户 从未交互过的物品集合中随机抽取一个或多个物品 作为负样本 。这时,损失函数就变成了成对损失 (Pairwise Loss) 的形式,比如BPR Loss(Bayesian Personalized Ranking Loss),其目标是让正样本的预测得分高于负样本的预测得分。
BPR Loss
BPR Loss(Bayesian Personalized Ranking Loss)是一种常用于推荐系统中排序任务的损失函数,核心思想是基于贝叶斯个性化排序的理论,通过优化“正样本优于负样本”的排序关系来提升推荐效果。
其核心逻辑是:对于用户而言,在推荐场景中,用户实际交互过的物品(正样本)应被模型赋予比未交互过的物品(负样本)更高的评分或排序优先级。BPR Loss通过计算正样本与负样本的预测分数差异,迫使模型学习到这种偏好关系。
具体来说,对于用户 、正样本物品 (用户交互过)和负样本物品 (用户未交互过),模型会分别输出预测分数和 。BPR Loss的目标是最小化如下损失:
其中是sigmoid函数,作用是将分数差映射到(0,1)区间,通过对数函数将“正样本分数高于负样本”的目标转化为可优化的损失——当时,损失较小;反之则损失增大,推动模型调整参数以满足排序偏好。
现在,我们面临一个问题,这也是传统方法的困境:
随机负采样的假设过于宽松,导致模型学习效率低下。
随机采样的负样本绝大多数都是“简单负样本”(Easy Negatives)。想象一下,为一个深度学习爱好者推荐一本言情小说,模型可以轻而易举地判断出这是一个负样本。模型在大量此类样本上进行训练,梯度会变得很小,参数更新缓慢,因为它一直在做一些“它早就知道”的事情。这就像让一个大学生反复去做小学一年级的加减法,进步会非常有限。
真正的挑战在于区分那些“困难负样本”(Hard Negatives)。比如,为一个喜欢《三体》的用户推荐《球状闪电》(同一作者的另一部作品)和《流浪地球》(同一作者的另一部电影原著),这显然是正样本。但如果推荐一部风格类似但质量稍逊的科幻小说,这可能就是一个“困难负样本”。用户可能不会点,但它与正样本在Embedding空间中的距离应该比那本言情小说近得多。
传统方法在处理这些细粒度的相似性关系时显得力不从心。随机负采样策略,本质上是对庞大的未观测样本空间做了一个均匀分布的假设,它忽略了样本之间内在的、复杂的相似性结构。模型无法有效学习到物品之间的细微差别,得到的Embedding空间虽然能区分出差异极大的样本,但对于相似物品的辨识能力却很弱。
总结一下,传统表征学习方法的困境在于:
- 数据稀疏性与隐式反馈:缺乏明确的负反馈,依赖于负采样。
- 采样策略的局限性:随机负采样引入大量简单负样本,模型学习效率低,无法捕捉到样本间细粒度的相似性差异。这本质上是由于采样策略未能准确反映真实世界中复杂的数据分布。
为了解决这个困境,我们需要一种新的学习范式。这种范式不应仅仅满足于“预测用户是否会点击”,而应该更专注于优化Embedding空间本身的结构,使其能更精确地反映样本间的相对相似性。
这,就是对比学习登场的契机。它提供了一种强大的框架,让我们能够通过精心设计的“对比”任务,直接去塑造Embedding空间的形态,从而学习到更具判别力的表征。在下一章,我们将深入探讨对比学习的思想是如何做到这一点的。
对比学习的思想起源与直觉
对比学习(Contrastive Learning)并非诞生于推荐系统,它最初在计算机视觉领域大放异彩,尤其是在自监督学习(Self-Supervised Learning)中展现了惊人的潜力。它的核心思想可以用一句话概括:通过比较(contrast),学习一个好的表征(representation),使得相似的样本在表征空间中更接近,不相似的样本在表征空间中更远离。
为了让你更好地理解这个思想,我们先不谈推荐系统,而是以一个计算机视觉中的具体任务为例来建立直觉。
一个图像识别的例子:理解“相似”与“不相似”
假设我们有一张猫的图片。我们希望通过某种方式,将这张猫的图片转换成一个向量(即它的Embedding),并且这个向量能够代表“猫”这种概念。
传统的监督学习可能会给这张图片打上一个“猫”的标签,然后用分类损失去训练模型。但对比学习不同,它不直接预测标签,而是关注样本之间的关系。
对比学习会这样做:
- 生成“正样本对”: 从这张猫的图片出发,我们对其进行一些数据增强 (Data Augmentation) 操作,例如随机裁剪、旋转、颜色抖动等等。这些操作会生成同一张猫图片的不同“视图”(或称“变种”)。虽然这些视图看起来略有不同,但它们本质上仍然是同一只猫。因此,我们将这些来自同一原始图片的两个不同视图视为一个 “正样本对”。
- 例如,原始图片 A 是一个猫,我们裁剪得到 A1 和 A2。那么 (A1, A2) 就构成了一个正样本对。
- 生成“负样本对”: 同时,我们会随机从数据集中选取其他图片,例如一张狗的图片 B。那么,将猫的图片 A 的某个视图(A1)与狗的图片 B 的某个视图(B1)组合,就构成了一个 “负样本对” (A1, B1)。
现在,对比学习的目标就非常明确了:
- 拉近正样本对: 模型应该学习到,无论猫的图片 A 如何变换(A1, A2),它们在Embedding空间中的向量 和 都应该非常接近。
- 推远负样本对: 同时,猫的图片 A 的向量 和狗的图片 B 的向量 应该相互远离。
这种“拉近正样本,推远负样本”的直觉,正是对比学习的核心。它强迫模型去学习那些能够区分不同个体(甚至是不同类别的个体)的,同时又对同个个体不同变换(或不同上下文)具有鲁棒性的特征。最终得到的Embedding空间,不再仅仅是简单地将图片分类,而是让语义上更相似的图片在空间中聚集,语义上不相似的图片相互远离。
从图像到推荐系统:相似性的迁移
现在,我们把这种思想迁移到推荐系统中。在推荐系统中,我们没有“同一张猫的不同视图”这样的概念。但是,我们有“用户与物品的交互行为”以及“物品之间的关联”。
在推荐场景中,“正样本对” 的定义通常来源于用户与物品的实际交互。例如,如果用户 收藏了物品 ,那么我们就可以认为用户 和物品 之间存在一个“正关系”,即 是一个正样本对。更进一步,如果用户 还收藏了物品 ,并且物品 和物品 在某种意义上是相似的(例如,它们属于同一类别,或经常被同一批用户交互),那么 也可以被视为一个正样本对。
“负样本对” 的定义则来自于未交互或不相关的组合。例如,用户 从未与物品 发生过交互,那么 就可以被视为一个负样本对。或者,两个物品 和 在内容或属性上完全不相关,它们也可以构成负样本对 。
对比学习在推荐系统中的任务,依然是学习用户和物品的Embedding向量 和 。其目标是:
- 如果用户 和物品 之间存在正向交互(比如点击、购买),那么它们的Embedding 和 应该相互吸引,距离较近。
- 如果用户 和物品 之间没有交互(被认为是负样本),那么它们的Embedding 和 应该相互排斥,距离较远。
关键在于“构建”正负样本对:
与图像领域通过数据增强生成正样本不同,在推荐系统中,正样本对的构建方式更加多样,也更具挑战性。常见的策略包括:
- 基于交互: 用户 与物品 的历史交互记录。
- 基于图结构: 在用户-物品二部图上进行随机游走,或通过图卷积网络生成同一节点的两个不同视图。
- 基于数据增强: 对用户或物品的特征进行随机遮蔽 (Masking) 或扰动 (Perturbation),生成它们的变体。
通过这种“比较”的学习方式,模型不再是盲目地预测一个点击概率,而是被引导去理解用户和物品之间深层次的、隐含的相似性。它能够学到更紧凑、更具区分度的Embedding,从而更好地应对数据稀疏性和长尾分布带来的挑战。
这种学习范式,正是我们对抗传统方法困境的有力武器。在下一章中,我们将深入其数学核心,详细推导在推荐系统中广泛应用的对比损失函数——InfoNCE。
InfoNCE损失的详细推导
在第二章中,我们建立了对比学习的直觉:拉近正样本,推远负样本。现在,我们将把这个直觉转化为一个具体的数学目标函数——InfoNCE (Information Noise Contrastive Estimation) 损失。这是在推荐系统以及更广泛的自监督学习领域中,应用最为广泛的对比损失形式之一。
为了理解InfoNCE,我们首先需要明确两个关键概念:相似度度量和正负样本对的定义。
1. 相似度度量
在Embedding空间中,我们如何量化两个向量 和 的相似程度?最常用的方法是余弦相似度 (Cosine Similarity):
其中 是向量点积, 是向量的欧几里得范数(长度)。余弦相似度的值域是 ,1 表示完全相同,-1 表示完全相反,0 表示正交。
在深度学习模型中,通常会将Embedding向量进行L2范数归一化 (L2 Normalization),即 和 。在这种情况下,余弦相似度就简化为简单的向量点积 (Dot Product):
由于点积计算简单且能够有效捕捉向量间的相似性,它成为了对比学习中衡量相似度的事实标准。在接下来的推导中,我们将默认使用点积作为相似度函数。
2. 正负样本对的构建
假设我们现在有一个锚点 (Anchor) 样本的Embedding向量 。我们的目标是学习到这样一个表征空间,使得:
- 与 相关的样本 (称为正样本)的Embedding,在空间中与 尽可能接近。
- 与 不相关的样本 (称为负样本)的Embedding,在空间中与 尽可能远离。
在推荐系统中,一个常见的正样本对构建方式是:
- 锚点 : 可以是一个用户Embedding 或者一个物品Embedding 。
- 正样本 : 如果 是 ,那么 可以是用户 实际交互过的某个物品 。如果 是 ,那么 可以是与物品 有特定关联的另一个物品 (例如,在同一个用户会话中出现的物品)。
- 负样本 : 负样本的选取至关重要。通常是从数据集中随机抽样,或者通过某些策略(如困难负样本挖掘)来获得。对于一个锚点 ,我们会有一批负样本 。
3. InfoNCE损失的数学推导
InfoNCE损失函数的核心思想是,我们将“识别出正样本”的问题,转化成一个多分类问题。想象一下,我们有一个锚点 ,以及一组候选样本:其中一个是我们唯一的正样本 ,其余 个是负样本 。模型的目标是,在这 个候选样本中,正确地识别出哪个是正样本 。
为了做到这一点,我们首先需要计算锚点 与所有候选样本(包括正样本和所有负样本)之间的相似度:
- 与正样本的相似度:
- 与负样本的相似度:
接下来,我们希望将这些相似度分数转换成概率。这里会用到Softmax函数,它的作用是将一组任意实数转换成一个概率分布(所有值都在 之间,并且和为 1)。Softmax 的定义是:
将这个思想应用到我们的相似度分数上,我们可以计算锚点 与正样本 的“相似度概率”:
这个公式是什么意思呢?
- 分子 :表示锚点 与正样本 之间的相似度,经过指数化处理。指数函数的一个特性是,值越大,指数化后的结果增长得越快,这能放大相似度高的样本的影响。
- 分母 :表示锚点 与所有候选样本(包括正样本和所有负样本)的相似度之和,也经过指数化处理。
- 温度系数 (Tau):这是一个非常关键的超参数,通常取一个较小的正数(例如 或 )。它的作用是调节分布的集中程度:
- 当 较小时, 会使得相似度分数的差异被放大,从而使模型更关注那些区分度大的困难负样本,损失函数对错误分类的惩罚更强。
- 当 较大时, 会使得相似度分数的差异被缩小,概率分布变得更加平滑,模型对错误分类的惩罚较弱。
- 形象地说, 控制着模型区分正负样本的“严格程度”。
我们的目标是最大化这个 ,即最大化锚点识别出正样本的概率。在机器学习中,最大化一个概率通常等价于最小化其负对数(这正是交叉熵损失的形式)。
所以,对于一个锚点 及其对应的正样本 和 个负样本 ,InfoNCE损失函数可以定义为:
其中,为了简化表示,我们把正样本 记作 ,而负样本 记作 ,因此分母中的求和是遍历所有 个候选样本(一个正样本和 个负样本)。
这个公式就是InfoNCE损失的完整形式。我们可以将其进一步展开:
这个损失函数的数学含义是什么?
从形式上看,InfoNCE损失与交叉熵损失 (Cross-Entropy Loss) 是完全一致的。它将对比学习任务建模为一个 分类问题:
- 输入: 锚点 和 个候选样本的相似度得分。
- 目标: 正样本 是这 个类别中的“正确”类别。
- Softmax: 将这些相似度分数转换为一个概率分布,表示锚点 认为每个候选样本是正样本的概率。
- 负对数: 最小化目标标签(正样本)的负对数概率,从而最大化正样本的概率。
因此,InfoNCE损失本质上是:它强迫模型将正样本与锚点的相似度推向最大,同时将负样本与锚点的相似度推向最小(因为它们构成了分母的一部分,并且这个部分的贡献越小,整体概率就越大)。 这完美地契合了“拉近正样本,推远负样本”的直觉。
为什么叫”Noise Contrastive Estimation”?
简要地补充一下,NCE 最初的提出是为了估计一个复杂数据分布的密度函数。它通过将数据样本与一些已知噪声分布的样本进行对比,将密度估计问题转化为一个二分类问题(判断一个样本是来自数据分布还是噪声分布)。InfoNCE 是其在自监督学习背景下的一个变体,它将“噪声”扩展到了批量内的所有负样本,并通过一个多分类的Softmax来区分正样本和负样本。
通过这个数学推导,我们可以清晰地看到,InfoNCE损失提供了一个严谨的数学框架,来量化我们所希望的Embedding空间结构。它通过在多分类的背景下优化正样本的概率,有效地解决了传统方法中对细粒度相似性捕获不足的问题。
对比损失与数据分布的关系
而要理解对比损失与数据分布的关系,我们必须回到负采样的本质。
1. 传统负采样的粗糙假设:均匀分布
在传统的推荐系统任务中,尤其是在二分类或成对排序的损失函数中(例如交叉熵或BPR),我们通常从用户未交互的物品中随机采样负样本。这种随机采样的背后,实际上是对负样本空间做了一个非常粗糙且通常不准确的假设:所有未交互的物品都是等概率地、均匀地作为负样本出现。
然而,真实世界的用户-物品交互数据往往呈现出长尾分布 (Long-Tail Distribution)。这意味着:
- 少数热门物品被大多数用户广泛交互。
- 大量长尾物品只有极少数用户交互,甚至从未被交互。
在随机负采样下,模型会高概率地采到那些“热门但用户未交互”的物品,以及“冷门且用户未交互”的物品。
- 对于“热门但用户未交互”的物品,它们可能与正样本的Embedding空间距离较远,模型很容易就能将其识别为负样本(即“简单负样本”)。
- 对于“冷门且用户未交互”的物品,它们通常远离任何热门物品,模型也能轻易将其识别为负样本。
当模型反复看到这些容易区分的负样本时,它学习到的梯度信号会很弱,参数更新效率低下。更重要的是,这种采样方式未能有效指导模型去区分那些 “与正样本相似但实际上是负样本” 的困难情况。例如,如果用户A喜欢看科幻电影《沙丘》,系统给他推荐了一部同类型的科幻电影《星际穿越》,这是正样本。但如果系统给他推荐了一部同样是科幻但口碑很差、他不会感兴趣的《上海堡垒》,这则是一个“困难负样本”——它在类型上和《沙丘》接近,但用户却不感兴趣。模型需要精细地辨别出《星际穿越》和《上海堡垒》之间的微妙差异,而简单的随机负采样很难提供足够的信号来学习这种细粒度区分能力。
所以,传统负采样的症结在于:它对真实数据分布的复杂性估计不足,尤其是在局部相似性和决策边界区域。
2. InfoNCE损失如何处理负样本分布
InfoNCE损失的巧妙之处在于,它将正负样本的区分建模成一个多分类问题。回看其形式:
这个分母中包含了当前批次(Batch)中的所有其他负样本。这意味着,模型不只是学习简单地将正样本 推离某个随机选取的负样本 ,而是学习将 推离所有其他的负样本,并且是相对地推离。
如果一个负样本 与锚点 非常相似(即它是一个“困难负样本”),那么 的值就会比较高,经过指数化后对分母的贡献就会更大。为了最小化损失(即最大化 ),模型就必须更加努力地将正样本 的相似度推得更高,或者将这些困难负样本 的相似度推得更低。
这种机制迫使模型关注那些“有区分度”的负样本,因为它无法通过简单地避开“明显不相关”的样本来获得低损失。这种对批次内所有负样本的“竞争性”对比,隐式地鼓励模型学习到更精细的特征,以便在复杂的Embedding空间中正确区分正负样本。
更正式地说,InfoNCE损失鼓励模型学习一个Embedding空间,使得:
这比简单的二分类损失在单点上进行优化要强大得多。它在每个训练步中,同时对 个样本进行“排序”,从而有效地利用了批次内的所有信息。
3. 困难负样本挖掘
虽然InfoNCE能够更好地利用批次内的负样本,但它仍然受限于我们如何选择这些负样本。如果批次内仍然充斥着大量简单负样本,学习效率仍可能受到影响。这就引出了困难负样本挖掘 (Hard Negative Mining, HNM) 的概念。
困难负样本是指那些与正样本(或锚点)在Embedding空间中距离很近,但实际标签却是负的样本。例如,在一个用户-物品推荐任务中:
- 锚点: 用户
- 正样本: 物品 (用户 实际购买过)
- 简单负样本: 物品 (用户 未购买,且与 毫不相关,如一件衬衫和一艘太空飞船)
- 困难负样本: 物品 (用户 未购买,但与 非常相似,如另一件颜色稍有不同的同款衬衫,或同一作者的另一部用户未读过的书)
挖掘困难负样本的目的是:
- 提高学习效率: 模型在简单负样本上学不到什么,但在困难负样本上会面临挑战,从而产生更大的梯度,促进模型参数的快速更新。
- 增强判别能力: 强制模型学习更精细的特征,以区分那些相似的正负样本。这有助于模型在Embedding空间中构建更清晰的决策边界。
困难负样本挖掘的策略有很多,例如:
- 在线挖掘: 在每个批次训练时,动态地从整个负样本池中选择与锚点相似度最高(或排序分数最高)的样本作为负样本。
- 离线挖掘: 预先计算所有物品的Embedding,然后为每个物品找到其最近的(但在训练集中未被交互的)物品作为困难负样本。
- 伪困难负样本: 例如在协同过滤中,共同交互过某一物品的用户,他们之间的“困难负样本”可能是那些他们都未交互但与该物品相似的物品。
通过主动引入困难负样本,我们实际上是在有意识地调整模型对负样本分布的认知。我们告诉模型:“嘿,别只看那些明显的负样本了,多花点精力去区分那些最容易混淆的样本!”这使得模型能够将精力集中在那些最能提升其判别力的区域。
4. 对比损失与数据分布的联系
综上所述,对比损失,特别是结合了InfoNCE和潜在困难负样本挖掘的策略,与数据分布的关系体现在以下几个层面:
- 从“点对点”到“点对集合”的优化: InfoNCE通过其Softmax形式,将正样本与批次内所有负样本进行对比,这使得模型不仅仅是学习一个独立的预测,而是在一个局部(批次)范围内,学习到一个关于样本相对相似性的分布。它强迫模型学会识别出在所有可能的“噪声”中,哪一个是真正的信号。
- 隐式地关注“信息量大”的负样本: InfoNCE的指数函数特性,使得高相似度的负样本(即困难负样本)对损失的贡献更大。这导致模型在训练时会自动更加关注这些难以区分的样本,从而隐式地进行了一种“困难负样本加权”。
- 主动修正分布偏差: 当我们采用显式的困难负样本挖掘策略时,我们更是主动地引导模型去关注真实数据分布中那些“模糊地带”——即正负样本边界附近的区域。通过对这些区域的强化训练,模型能够学习到更鲁棒、更具判别力的特征。这使得模型不再仅仅擅长区分“猫和狗”,还能区分“不同品种的猫”或者“姿态不同的猫”,这正是推荐系统所需要的细粒度区分能力。
简而言之,对比损失并没有假设一个简单的负样本分布。相反,它通过其数学形式和配合的负采样策略,使得模型能够更智能地利用负样本信息,尤其关注那些具有挑战性的样本。这最终帮助模型学习到更能反映真实世界复杂相似性关系的Embedding,从而有效缓解了数据稀疏性和长尾分布带来的问题。
在下一节中,我们将通过PyTorch代码的实际实现,具体展示InfoNCE损失是如何被编码并应用于训练过程的。这将把我们所有的理论分析转化为实际可操作的代码细节。
PyTorch代码实现与解读
我们将实现一个通用的InfoNCE损失函数。为了让它更具通用性,我们假设输入的Embedding向量已经经过了L2范数归一化 (L2 Normalization)。这是对比学习中常见的做法,因为它使得向量点积直接等同于余弦相似度,并且有助于稳定训练。
首先,导入必要的库:
import torch
import torch.nn.functional as F
import torch.nn as nn接下来,我们定义一个函数来实现InfoNCE损失。
def info_nce_loss(query_embeddings: torch.Tensor,
positive_key_embeddings: torch.Tensor,
negative_key_embeddings: torch.Tensor,
temperature: float = 0.07) -> torch.Tensor:
"""
计算InfoNCE损失函数。
该函数假设所有输入的Embedding都已经过L2范数归一化。
如果未归一化,建议在外部或函数内部进行torch.nn.functional.normalize处理。
Args:
query_embeddings (torch.Tensor): 锚点(Query)的Embedding向量。
形状: (batch_size, embedding_dim)
positive_key_embeddings (torch.Tensor): 正样本(Positive Key)的Embedding向量。
形状: (batch_size, embedding_dim)
negative_key_embeddings (torch.Tensor): 负样本(Negative Keys)的Embedding向量。
形状: (batch_size, num_negative_samples, embedding_dim)
temperature (float): 温度系数 $\tau$,用于调节相似度分布的集中程度。
Returns:
torch.Tensor: 平均InfoNCE损失值。
"""
# 1. 计算锚点与正样本的相似度(点积)
# 由于我们假设Embedding已L2归一化,点积即为余弦相似度。
# torch.sum(query_embeddings * positive_key_embeddings, dim=1)
# 对于批次中的每个样本,计算其query与positive_key的点积
# Shape: (batch_size,)
positive_logits = torch.sum(query_embeddings * positive_key_embeddings, dim=1)
# 2. 计算锚点与所有负样本的相似度(点积)
# 使用 einsum 更高效地处理批次中的每个query与对应的一组负样本的点积
# 'bd' 表示 query_embeddings 的形状 (batch_size, embedding_dim)
# 'bnd' 表示 negative_key_embeddings 的形状 (batch_size, num_negative_samples, embedding_dim)
# 'bn' 表示结果形状 (batch_size, num_negative_samples)
# 对于批次中的每个query,它会与 num_negative_samples 个负样本分别计算点积
# Shape: (batch_size, num_negative_samples)
negative_logits = torch.einsum('bd,bnd->bn', query_embeddings, negative_key_embeddings)
# 3. 将正负样本的相似度拼接起来
# positive_logits 形状是 (batch_size,),我们需要将其扩展一个维度 (batch_size, 1) 才能拼接
# 拼接后,第一个列是正样本的相似度,后面是所有负样本的相似度
# Shape: (batch_size, 1 + num_negative_samples)
all_logits = torch.cat([positive_logits.unsqueeze(1), negative_logits], dim=1)
# 4. 应用温度系数 $\tau$
# 所有相似度分数都被温度系数除以,调节其相对大小。
# 如果温度系数小,相似度差异会被放大;如果大,则差异会被缩小。
# Shape: (batch_size, 1 + num_negative_samples)
scaled_logits = all_logits / temperature
# 5. 计算交叉熵损失
# InfoNCE损失本质上是一个多分类交叉熵损失。
# 我们的目标是让模型在 `all_logits` 中正确地预测出正样本(即索引为 0 的那个)。
# 因此,真实标签(target)对于批次中的每个样本都是 0。
# Shape of targets: (batch_size,)
targets = torch.zeros(query_embeddings.size(0), dtype=torch.long, device=query_embeddings.device)
# F.cross_entropy 内部会先对 scaled_logits 进行 Softmax,然后计算负对数似然。
# 这与 InfoNCE 公式 -log(exp(sim(q,k+)/tau) / sum(exp(sim(q,kj)/tau))) 完全等价。
# 损失是批次中所有样本损失的平均值
loss = F.cross_entropy(scaled_logits, targets)
return loss代码解释与张量形状追踪:
-
positive_logits(正样本相似度):- 输入
query_embeddings形状(batch_size, embedding_dim)。 - 输入
positive_key_embeddings形状(batch_size, embedding_dim)。 query_embeddings * positive_key_embeddings执行的是元素级乘法。结果形状(batch_size, embedding_dim)。torch.sum(..., dim=1)沿着embedding_dim求和,得到每个样本的点积。结果形状(batch_size,)。这表示批次中每个query和其对应的positive_key的相似度。
- 输入
-
negative_logits(负样本相似度):query_embeddings形状(batch_size, embedding_dim)。negative_key_embeddings形状(batch_size, num_negative_samples, embedding_dim)。torch.einsum('bd,bnd->bn', ...)是一个强大的张量操作,这里它相当于:对于批次中的每一个b,取query_embeddings[b, :]与negative_key_embeddings[b, :, :]中的每一个negative_key进行点积。b: 批次维度d: Embedding维度n: 负样本数量维度
- 结果形状
(batch_size, num_negative_samples)。这表示批次中每个query和其对应的所有num_negative_samples个负样本的相似度。
-
all_logits(拼接所有相似度):positive_logits.unsqueeze(1)将(batch_size,)转换为(batch_size, 1),为其增加一个维度,使其可以与negative_logits(batch_size, num_negative_samples)在dim=1上拼接。torch.cat([...], dim=1)将两者沿列方向拼接。- 结果形状
(batch_size, 1 + num_negative_samples)。现在,每行代表一个锚点,其第一个元素是正样本相似度,后面跟着所有负样本相似度。
-
scaled_logits(应用温度系数):all_logits / temperature只是简单的元素级除法。- 形状不变:
(batch_size, 1 + num_negative_samples)。
-
targets(目标标签):torch.zeros(query_embeddings.size(0), dtype=torch.long, device=query_embeddings.device)创建一个长度为batch_size的全零张量。- 形状
(batch_size,)。 F.cross_entropy函数要求targets是一个表示类别索引的长整型张量。在这里,我们希望模型预测第一个(索引为 0)为正样本,所以目标就是 0。
-
loss(最终损失):F.cross_entropy(scaled_logits, targets):scaled_logits是模型的“原始分数”(logits),形状(batch_size, num_classes),这里num_classes就是1 + num_negative_samples。targets是真实的类别索引,形状(batch_size,)。
- 该函数内部会执行 Softmax 并在 Softmax 的输出上计算负对数似然损失。
示例用法
# 设置随机种子以保证结果可复现
torch.manual_seed(42)
# 模拟超参数
batch_size = 4
embedding_dim = 128
num_negative_samples = 32
temperature = 0.07
# 模拟生成随机Embedding
# 注意:在实际应用中,这些Embedding会通过神经网络学习得到。
# 这里我们手动进行L2归一化,确保它们在单位球面上。
# query_embeddings: 用户或物品的锚点Embedding
query_embeddings = torch.randn(batch_size, embedding_dim)
query_embeddings = F.normalize(query_embeddings, p=2, dim=1)
# Shape: (4, 128)
# positive_key_embeddings: 与锚点对应的正样本Embedding
positive_key_embeddings = torch.randn(batch_size, embedding_dim)
positive_key_embeddings = F.normalize(positive_key_embeddings, p=2, dim=1)
# Shape: (4, 128)
# negative_key_embeddings: 与锚点对应的负样本Embedding
# 注意:每个query对应num_negative_samples个负样本
negative_key_embeddings = torch.randn(batch_size, num_negative_samples, embedding_dim)
negative_key_embeddings = F.normalize(negative_key_embeddings, p=2, dim=2) # 在最后一个维度归一化
# Shape: (4, 32, 128)
# 计算InfoNCE损失
calculated_loss = info_nce_loss(query_embeddings,
positive_key_embeddings,
negative_key_embeddings,
temperature)
print(f"计算出的InfoNCE损失: {calculated_loss.item():.4f}")
# 验证损失值:
# 理论上,一个好的模型会使得正样本相似度高,负样本相似度低,导致损失值变小。
# 初始随机化的Embedding,损失值会相对较高。
# 经过训练,模型会优化Embedding,使得损失值下降。我们运行上面的代码,可以看到类似如下的结果:
计算出的InfoNCE损失: 4.4253
这个值是根据随机生成的Embedding计算的,初始时会比较高。在真实的训练过程中,这个损失值会随着模型的优化而逐渐下降。
SGL模型
为了将对比学习的理论和我们刚刚编写的代码与实际的推荐系统应用相结合,我们来看一个具体的例子:《Self-supervised Graph Learning for Recommendation (SGL)》 这篇论文(SIGIR 2021)。SGL模型是一个将图神经网络(Graph Neural Networks, GNN)与对比学习深度融合的推荐模型,它在多个数据集上取得了显著的性能提升。
SGL的提出,正是为了解决传统推荐系统中面临的用户-物品交互数据稀疏性和长尾分布的问题,通过自监督学习的方式来增强用户和物品的Embedding表征能力。
1. SGL的核心思想:图增强与对比学习
SGL的核心思想是:在用户-物品交互图(一个二部图)上,通过结构扰动(Structural Perturbation) 来生成同一个节点(用户或物品)的不同视图,然后利用对比学习来最大化这些不同视图之间的一致性。
具体来说,SGL在用户-物品二部图上引入了三种图数据增强(Graph Augmentation)方式来构造正样本对:
- 节点丢弃 (Node Dropout): 随机移除图中的一些用户或物品节点。
- 边丢弃 (Edge Dropout): 随机移除图中用户与物品之间的交互边。
- 随机游走 (Random Walk): 在图中执行随机游走,从每个节点开始生成其邻居的上下文。
通过这些增强操作,对于同一个原始节点 (可以是用户或物品),我们可以得到其两个不同的、但都源于 的“视图” 和 。例如,如果 是用户 A,那么 可能是经过边丢弃后用户 A 的Embedding,而 可能是经过节点丢弃后用户 A 的Embedding。这两个不同的Embedding都代表着原始用户 A 的信息,因此它们构成了一个正样本对 。
负样本则通常是来自同一个批次(Batch)中的其他节点的视图。例如,对于用户 A 的一个视图 ,批次中其他用户 B、C 的视图 都可以被视为负样本。
2. SGL的对比损失
SGL的损失函数由两部分组成:
- 推荐任务损失 (Recommendation Task Loss): 经典的BPR Loss,用于优化用户对物品的偏好预测。这部分是模型最终推荐性能的基础。
- 自监督对比损失 (Self-supervised Contrastive Loss): 这就是我们刚刚讨论的InfoNCE损失。它用于在图数据增强产生的不同视图之间学习更鲁棒的Embedding。
我们重点关注自监督对比损失部分。对于每个原始节点 ,SGL会生成两个增强视图 和 ,并计算它们的Embedding 和 。然后,对比损失的目标是最大化 和 之间的相似度,同时最小化 与批次中其他节点视图的Embedding之间的相似度。
其自监督损失函数形式与我们前面推导的InfoNCE损失非常相似。对于一个批次中的某个节点 的一个视图 ,其对应的自监督损失为:
其中:
- 是锚点。
- 是正样本(同一原始节点 的另一个视图)。
- 包含批次中所有其他节点的视图 (作为负样本),以及正样本 。
- 是温度系数。
SGL的最终损失函数是推荐任务损失和自监督对比损失的加权和:
其中, 是一个超参数,用于平衡两个损失项的重要性。 和 分别是应用于用户Embedding和物品Embedding的自监督对比损失。
3. SGL如何解决推荐系统问题?
SGL通过引入对比学习,有效地解决了我们前面提到的推荐系统表征学习的困境:
-
缓解数据稀疏性:
- 在稀疏的用户-物品交互图中,某些用户或物品的连接非常少,导致其Embedding学习不足。
- 通过图增强操作(例如随机丢弃边),即使原始交互数据稀疏,模型也能被迫从节点的不同“残缺”视图中提取一致的信息。这使得模型在更广泛的上下文(即使部分信息缺失)中学习到鲁棒的Embedding,从而增强了对稀疏节点的表征能力。
- 对比学习通过最大化不同视图的一致性,使得模型在没有额外标签的情况下也能学习到有效的特征,这对于那些交互数据很少的长尾用户或物品尤为重要。
-
捕捉细粒度相似性:
- 对比学习迫使模型区分正负样本,即使它们在Embedding空间中非常接近(困难负样本)。
- 在SGL中,一个用户/物品的两个不同视图被拉近,而与批次中其他用户/物品的视图被推远。这意味着模型不仅要学习“谁是谁”,更要学习“谁跟谁更像”。
- 这有助于模型学习到更具判别力的特征,例如区分相似类型的电影(如《星际穿越》和《上海堡垒》),而不仅仅是区分电影和书籍。这使得推荐结果更加精准和个性化。
-
增强模型鲁棒性:
- 通过对图结构进行随机扰动,模型被训练去应对输入数据的轻微变化。这种不变性 (Invariance) 或鲁棒性 (Robustness) 的学习使得模型生成的Embedding对噪声和不完整信息更不敏感。
- 例如,即使某个用户的历史行为记录中某个电影的交互被随机丢弃了,模型仍然能通过其他交互和对比学习任务,推断出该用户与该电影的真实关系,从而提升推荐的稳定性。
4. 真实世界应用的启示
SGL以及其他采用对比学习的推荐模型(如LightGCN、GraphCL等),都表明了自监督学习是提升推荐系统性能的强大范式。它允许模型在不依赖额外人工标注数据的情况下,从大量的无标签(或隐式标签)数据中学习到高质量的表征。
在实践中部署这类模型时,你需要关注以下几点:
- 数据增强策略: 如何为用户和物品构建有意义的、能够捕捉内在相似性的“视图”至关重要。这通常需要领域知识和实验。
- 负采样策略: 批次内的负样本足以提供有效的对比信号,但如何结合更高级的负采样(如困难负样本挖掘)可以进一步提升性能。
- 温度系数 的调优: 这是一个敏感的超参数,直接影响模型学习到的Embedding空间的集中程度和区分度。通常需要进行网格搜索或随机搜索来找到最佳值。
通过SGL的例子,我们看到了对比学习如何从一个通用的自监督学习范式,被巧妙地应用到推荐系统的特定挑战中,并取得了显著的成果。