论文:https://arxiv.org/pdf/2104.08821.pdf
代码:https://github.com/princeton-nlp/SimCSE
demo:https://gradio.app/g/AK391/SimCSE
参考博客1:https://zhuanlan.zhihu.com/p/368353121
参考博客2:https://zhuanlan.zhihu.com/p/377612458
参考博客3:https://www.sohu.com/a/469789611_121119001
Abstract
本文将对比学习的思想引入了sentence embedding。我们首先描述一种无监督的方法,该方法输入一个语句,并在一个对比目标中预测自己,学习过程中仅将标准dropout用作噪声。这种简单的方法出奇地有效。dropout是一种数据增强,如果将其删除会导致表示崩溃。(相关说明)然后,我们从通过自然语言推理(NLI)数据集学习句子嵌入的工作中汲取灵感,并通过使用“蕴含”对作为肯定词,而“矛盾”对作为硬否定词,将NLI数据集的标注对纳入对比学习。我们证明,从理论上说,对比学习目标“拉平”了句子嵌入空间的奇异值分布,将预先训练的嵌入的各向异性空间调整为更均匀。我们的无监督SimCSE实质上提高了均匀性,同时避免了由于dropout噪声而导致的退化对齐,大大提高了表示的表达能力。我们还证明了NLI训练信息可以进一步改善正对之间的对齐方式,产生更好的句子嵌入。
Background: Contrastive Learning
对比学习的目的是,减少同类距离,增大不同类之间的距离,借此获得一个文本或者图片更好的表示向量;
对比学习的训练目标:
本质是一个多分类softamx的交叉熵损失函数。区别:
infoNCE loss 中的 K 是 batch 的大小,是可变的,是第 i 个样本要和 batch 中的每个样本计算相似度,而 batch 里的每一个样本都会如此计算,因此上面公式只是样本 i 的 loss。 CE loss 中的 K 是分类类别数的大小,任务确定时是不变的,i 位置对应标签为 1 的位置。不过实际上,infoNCE loss 就是直接可以用 CE loss 去计算的。
分母这里:假设一个batch有N个句子对,那么就有2N个句子,其中正例是1个,负样本应该是总样本数目2N减去样本本身加上样本的正例,也就是2N-2;
Alignment and uniformity
unsupervised SimCSE
我们的无监督SimCSE可以简单地预测输入句子本身,并且仅将dropout用作噪音(图1a)。
利用模型中的Dropout mask,对每一个句子进行两次前向传播(论文中是从维基百科中随机选取了100w个句子进行训练),得到两个不同的embeddings向量,将同一个句子得到的向量对作为正样本对,对于每一个向量,选取其他句子产生的embeddings向量作为负样本,以此来训练模型。
如何采样负样本?论文采取了我们熟悉的In-batch training即将同一个batch中其他不同源句子产生的dropout 增广embedding作为负样本,即损失函数中的 hj
supervised SimCSE

在有监督的SimCSE中,我们成功利用了基于自然语言推理(NLI)数据集进行句子嵌入学习,并将受监督的句子对纳入对比学习中(图1b)。与先前将其转换为三分类任务(包含/中立/矛盾)的工作不同(如SBERT,这种方式忽略了正样本与负样本之间的交互),而对比损失则可以让模型学习到更丰富的细粒度语义信息),我们利用了包含对可以自然地用作正例的事实。我们还发现,添加相应的矛盾对作为反例可以进一步提高性能。
构造训练目标:将NLI(SNLI+MNLI)数据集中的entailment作为正样本,conradiction作为负样本,加上原样本premise一起组合为,并将损失函数改进为
这里的可以被看作是hard negatives。
另外,作者分别尝试在不同的语义匹配数据集上训练模型,并在STS-B上测试模型发现NLI训练出来的模型性能是最好的,这是因为NLI数据集的质量本身很高,正样本词汇重合度非常小(39%)且负样本足够困难。
Connection to Anisotropy
近几年不少研究都提到了语言模型生成的语义向量分布存在各向异性的问题,这极大地限制了语义向量的表达能力,缓解这个问题的一种简单方法是加入后处理步骤,比如BERT-flow和BERT-whitening将向量映射到各向同性的分布上,而本文作者证明了对比学习的训练目标可以隐式地压低分布的奇异值,提高uniformity。
FAQ
参考:https://www.sohu.com/a/469789611_121119001
- 对比学习和度量学习的差别是什么?
对比学习和度量学习的思想是一样的,都是去拉近相似的样本,推开不相似的样本。但是对比学习是无监督或者自监督学习方法,而度量学习一般为有监督学习方法。而且对比学习在 loss 设计时,为单正例多负例的形式,因为是无监督,数据是充足的,也就可以找到无穷的负例,但如何构造有效正例才是重点。而度量学习多为二元组或三元组的形式,如常见的 Triplet 形式(anchor,positive,negative),Hard Negative 的挖掘对最终效果有较大的影响。
- 对比学习中一般选择一个 batch 中的所有其他样本作为负例,那如果负例中有很相似的样本怎么办?
在无监督无标注的情况下,这样的伪负例,其实是不可避免的,首先可以想到的方式是去扩大语料库,去加大 batch size,以降低 batch 训练中采样到伪负例的概率,减少它的影响。
另外,神经网络是有一定容错能力的,像伪标签方法就是一个很好的印证,但前提是错误标签数据或伪负例占较小的比例。
- 对比学习的 infoNCE loss 中的温度常数 t 的作用是什么?
温度系数的作用是调节对困难样本的关注程度: 越小的温度系数越关注于将本样本和最相似的困难样本分开,去得到更均匀的表示。然而困难样本往往是与本样本相似程度较高的,很多困难负样本其实是潜在的正样本,过分强迫与困难样本分开会破坏学到的潜在语义结构,因此,温度系数不能过小。
考虑两个极端情况,温度系数趋向于 0 时,对比损失退化为只关注最困难的负样本的损失函数;当温度系数趋向于无穷大时,对比损失对所有负样本都一视同仁,失去了困难样本关注的特性。
为什么在计算相似度时我们需要对句子向量做L2正则?
这样做的目的是将所有的句子向量映射在一个半径为1的超球体上,一方面我们将所有向量统一至单位长度,去除了长度信息是为了让模型的训练更加稳定;另一方面如果模型的表示能力足够好,能够把相似的句子在超球面上聚集到较近区域,那么很容易使用线性分类器把某类和其它类区分开。
- SimCSE 中的 dropout mask 指的是什么,dropout rate 的大小影响的是什么?
一般而言的 mask 是对 token 级别的 mask,比如说 BERT MLM 中的 mask,batch 训练时对 padding 位的 mask 等。
SimCSE 中的 dropout mask,对于 BERT 模型本身,是一种网络模型的随机,是对网络参数 W 的 mask,起到防止过拟合的作用。
而 SimCSE 巧妙的把它作为了一种 noise,起到数据增强的作用,因为同一句话,经过带 dropout 的模型两次,得到的句向量是不一样的,但是因为是相同的句子输入,最后句向量的语义期望是相同的,因此作为正例对,让模型去拉近它们之间的距离。
在实现上,因为一个 batch 中的任意两个样本,经历的 dropout mask 都是不一样的,因此,一个句子过两次 dropout,SimCSE 源码中实际上是在一个 batch 中实现的,即 [a,a,b,b…] 作为一个 batch 去输入。
dropout rate 大小的影响,可以理解为,这个概率会对应有 dropout 的句向量相对无 dropout 句向量,在整个单位超球体中偏移的程度,因为 BERT 是多层的结构,每一层都会有 dropout,这些 noise 的累积,会让句向量在每个维度上都会有偏移的,只是 p 较小的情况下,两个向量在空间中仍较为接近,如论文所说,“keeps a steady alignment”,保证了一个稳定的对齐性。
- SimCSE 无监督模式下的具体实现流程是怎样的,标签生成和 loss 计算如何实现?
前向句子 embedding 计算:
假设初始输入一个句子集 sents = [a,b],每一句要过两次 BERT,因此复制成 sents = [a,a,b,b]。
sents 以 batch 的形式过 BERT 等语言模型得到句向量:batch_emb = [a1,a2,b1,b2]。
batch 标签生成:
标签为 1 的地方是相同句子不同 embedding 对应的位置。
pytorch 中的 CE_loss,要使用一维的数字标签,上面的 one-hot 标签可转换成:[1,0,3,2]。
可以把 label 拆成两个部分:奇数部分 [1,3…] 和偶数部分 [0,2…],交替的每个奇数在偶数前面。因此实际生成的时候,可以分别生成两个部分再 concat 并 reshape 成一维。
pytorch 中 label 的生成代码如下:
构造标签
batch_size = batch_emb.size( 0)
y_true = torch.cat([torch.arange( 1,batch_size,step= 2,dtype=torch.long).unsqueeze( 1),
torch.arange( 0,batch_size,step= 2,dtype=torch.long).unsqueeze( 1)],
dim= 1).reshape([batch_size,])
score 和 loss计算:
batch_emb 会先 norm,再计算任意两个向量之间的点积,得到向量间的余弦相似度,维度是:[batch_size, batch_size]。
但是对角线的位置,也就是自身的余弦相似度,需要 mask 掉,因为它肯定是 1,是不产生 loss 的。
然后,要除以温度系数,再进行 loss 的计算,loss_func 采用 CE loss,注意 CE loss 中是自带 softmax 计算的。
计算score和loss
norm_emb = F.normalize(batch_emb, dim= 1, p= 2)
sim_score = torch.matmul(norm_emb, norm_emb.transpose( 0, 1))
sim_score = sim_score - torch.eye(batch_size) * 1e12
sim_score = sim_score * 20# 温度系数为 0.05,也就是乘以20
loss = loss_func(sim_score, y_true)

