在本文中,我们将介绍EMNLP2021的一篇论文SimCSE。这是一种简单有效的NLP对比学习方法,通过Dropout的方式进行正样本增强,模型能够学习到良好的句向量表示。按照惯例,我们也对该模型在STS-B数据集上进行了实验复现。
论文链接:

https://arxiv.org/abs/2104.08821
实验复现代码:

https://github.com/yangjianxin1/SimCSE
01
引言
对于很多自然语言处理任务来说,学习到一个良好的句向量表示是非常重要的。例如在向量检索,文本语义匹配等任务中,模型将输入的两个句子进行编码得到句向量,然后计算句向量之间的相似度,从而判断两个句子是否匹配。
说到计算两个句子的相似度,最直接的做法便是,将两个句子输入到BERT模型中,使用[CLS]对应的输出或者整个句子序列的输出的平均向量,作为句向量,然后再计算两个句向量的相似度。但是由于BERT模型的MLM与NSP这两个预训练任务的局限性,模型无法很好地学习到句子表征能力。
为什么经过MLM与NSP任务训练之后,BERT无法学习到良好的句向量表示呢?我们简单回顾一下MLM与NSP任务的做法。
  1. MLM任务是遮住某个单词,让模型去预测遮住的单词,在这个训练中,模型并没有显式地对[CLS]向量进行训练,没有告诉模型[CLS]这个向量就是用来编码句子的语义信息的。在MLM任务中[CLS]学习到的并不是句子的语义表征。
  2. NSP任务是给定两个句子,让模型判断两个句子是否为上下文关系,使用[CLS]的输出来进行二分类。在这个任务中,[CLS]是用来编码两个句子之间的关系的,而不是描述某个句子的语义信息。
综上所述,未经过fintune的BERT模型,必然无法得到良好的句子的语义表征。
美团的ConSERT论文研究表明,如果BERT模型不经过微调的话,模型输出的句向量会坍塌到一个非常小的区域内。下图所展示的是在STS数据集中,文本相似度的分布情况,其中横坐标表示人类标注的句子相似度等级,纵坐标表示没有经过finetune的BERT模型预测的句子相似度分布。可以很明显看到模型预测的所有句子对的相似度,几乎都落到了0.6-1.0这个区间,即使含义完全相反的两个句子,模型输出的相似度也非常高。这便是BERT的句子表示的“坍塌”现象。
其中BERT的句向量的坍缩和句子中的高频词有关。当我们使用整个句子序列的输出的平均向量作为句向量时,句子中的高频词将会主导句向量,使得任意两个句向量之间的相似度都非常高。为了验证该想法,美团的ConSERT论文对此也进行了实验。
下图中蓝色线条展示的是去除若干高频词后,BERT模型在STS数据集上的Spearman得分。其中Spearman得分越高,说明模型在数据集上的表现越好。我们可以看到,当计算句向量时,如果去除若干个top-k的高频词,Spearman得分显著提高,句向量的坍塌现象得到了一定程度的缓解。
由此可知,BERT的MLM与NSP预训练任务难以胜任下游的语义匹配任务。为了解决该问题,我们可以使用对比学习的方法对模型进行预训练,从而使模型能够学习到更好的句子语义表示,并且更好地应用到下游任务中。
02
论文解读
对比学习
对比学习起源于计算机视觉任务,它的核心思想是,拉近每个样本与正样本之间的距离,拉远其与负样本之间的距离。其中正样本是语义相似的样本,负样本是语义不相似的样本。这个思想与文本语义匹配、向量检索等任务的思想是相符。
在对比学习中,我们经常使用InfoNCE loss作为损失函数。假设我们有正样本对的集合  ,其中  与  互为正样本,  和  分别表示样本  与  经过模型编码之后的向量。则在规模为N的batch中,第i个样本对应的InfoNCE损失为:
上式中,  表示温度超参数,  表示相似度。可以看到这个损失函数类似于交叉熵的形式,鼓励拉近正样本之间的距离,而在分母部分,鼓励拉远负样本之间的距离。
如何为每个样本构造正样本与负样本是对比学习中的关键问题。负样本的构造往往比较容易,随机采样或者把同一个batch里面的其他样本作为负样本即可,难点在于如何构造正样本。

对于图像来说,对图像进行翻转、裁剪、旋转、扭曲等操作即可很容易地生成正样本。对于NLP来说,往往会采用替换、删除、添加词语的方法来进行正样本构造,但是上述操作非常容易引入噪声,并且改变原有文本的语义。例如对【我爱你】进行替换操作得到【我恨你】,就改变的原来的文本的语义。
为了解决上述问题,SimCSE论文中提出了一种基于Dropout的无监督对比学习方法,同时也对有监督对比学习方法进行了探索。
无监督SimCSE
Dropout是一种用来防止神经网络过拟合的方法,在训练的时候,通过dropout mask的方式,模型中的每个神经元都有一定的概率会失活。所以在训练的每个step中,都相当于在训练一个不同的模型。在推理阶段,模型最终的输出相当于是多个模型的组合输出。
在无监督SimCSE中,作者将同一个样本  ,分别输入模型两次,使用不同的dropout mask得到两个向量  与  。则在规模为N的batch中,第i个样本对应的InfoNCE损失为:
Dropout可以视为一种数据增强的手段,通过dropout mask的方式,模型在编码同一个句子的时候,引入了数据噪声,从而为同一个句子生成不同的句向量,并且不影响其语义信息。其中dropout rate的大小可以视为引入的噪声的强度。

为了验证模型dropout rate对无监督SimCSE的影响,作者在STS-B数据集上进行了消融实验,其中训练数据是作者从维基百科中随机爬取的十万个句子。从下表的实验结果可以看到,当dropout rate设置为0.1的时候,模型在STS-B测试集的效果最好。下表中  表示对于同一个样本,它的dropout mask是一样的,也就是说编码两次得到的向量是一样的,模型几乎学不到东西。
有监督SimCSE
作者还尝试了使用各种人工标注的数据集对模型进行有监督训练,包括QQP、Flickr30k、ParaNMT、NLI数据集。

与无监督SimCSE一样,作者利用数据集中人工标注的正样本对,使用InfoNCE loss对模型进行训练,实验结果如下表所示。可以看到使用SNLI+MNLI数据集训练的模型效果最好,并且其指标也比无监督SimCSE提高了2.4个点。
除此之外,作者还尝试在NLI数据集上,为模型引入了困难负样本,将模型的输入由  扩展为  ,其中  与  分别表示  的entailment与contradiction。损失函数扩展为:
从上表的实验数据可以看到,为模型添加了难负样本之后,模型的指标从84.9提升到了86.2,说明了在对比学习中添加难负样本的有效性与必要性。
实验结果
对于无监督SimCSE与有监督SimCSE,论文的实验结果如下表,可以看到,在STS任务中,无论是无监督还是有监督的训练方法,都比之前的方法有了较大幅度的提高,这证明了论文方法的有效性。
作者还进行了一些关于池化层、  以及迁移任务的实验,详细内容可以参考原论文。
03
实验复现
按照惯例,笔者尝试对SimCSE进行了复现实验,但由于资源有限,笔者仅在STS-B数据集上进行了实验。
中文数据集的复现结果可以参考苏剑林的复现实验:
https://kexue.fm/archives/8348
实验所使用的训练集与原论文一致,均训练2个epoch,每隔100个step进行一次验证集的评测,并且保存最好的checkpoint。预训练权重使用bert-base-uncased,并且直接将BERT的[CLS]的输出作为句向量,没有添加额外的池化层。
复现的总体效果如下表所示。可以看到,在无监督训练中,当dropout=0.2时,复现效果比原文略高。但在有监督训练中,复现效果与原论文的差距较大,甚至比无监督效果还略差。这个结果比较反常,笔者对代码进行了一轮debug,暂未定位到问题所在,后续会再次对有监督部分的训练代码进行排查。
无监督SimCSE
笔者在无监督任务上,对batch size、dropout进行了对比实验,实验结果如下表所示。发现模型在验证集与测试集的指标差距有大,大概3-5个点的差距,说明训练得到的模型的泛化能力还有待提高。
Batch Size比较
从上表可知,当lr=3e-5,dropout=0.1时,batch size设为256最佳,在测试集上的得分为0.761。在原论文中,作者通过实验证明SimCSE对batch size不敏感,但从复现实验结果看来,batch size越大,效果会更好。
Dropout比较
当lr=3e-5,batch size=64时,可以看到dropout设为0.2效果更好,取0.1或0.3都会变差。笔者认为,从某种意义上说,dropout的大小相当于噪声的强度,取0.1的时候,噪声强度较小,模型编码得到的两个向量太相似,导致模型无法学到足够的语义知识。当dropout取0.3时,噪声强度太大,改变了文本向量的语义信息。而在原论文中作者仅在0-1之间进行了dropout的对比实验,证明了dropout=0.1效果更好。
下图展示了无监督SimCSE在训练过程中验证集的spearman相关系数的变化趋势。可以看到大概在12k步时,模型在验证集上效果最好,保存了最好的checkpoint。大概在12k步之后,模型在验证集上的得分不升反降,说明模型开始过拟合。
无监督SimCSE的损失函数计算方式如下:
defsimcse_unsup_loss(y_pred, device, temp=0.05):"""无监督的损失函数    y_pred (tensor): bert的输出, [batch_size * 2, dim] """# 得到y_pred对应的label, [1, 0, 3, 2, ..., batch_size-1, batch_size-2] y_true = torch.arange(y_pred.shape[0], device=device) y_true = (y_true - y_true % 2 * 2) + 1# batch内两两计算相似度, 得到相似度矩阵(对角矩阵) sim = F.cosine_similarity(y_pred.unsqueeze(1), y_pred.unsqueeze(0), dim=-1)# 将相似度矩阵对角线置为很小的值, 消除自身的影响 sim = sim - torch.eye(y_pred.shape[0], device=device) * 1e12# 相似度矩阵除以温度系数 sim = sim / temp# 计算相似度矩阵与y_true的交叉熵损失# 计算交叉熵,每个case都会计算与其他case的相似度得分,得到一个得分向量,目的是使得该得分向量中正样本的得分最高,负样本的得分最低 loss = F.cross_entropy(sim, y_true)return torch.mean(loss)
有监督SimCSE
下表展示了有监督SimCSE的复现效果,复现效果与原论文的差距较大,甚至比无监督效果还略差。这个结果比较反常,后续会再次对实验代码进行排查。
有监督SimCSE的损失函数计算方式如下
defsimcse_sup_loss(y_pred, device, temp=0.05):""" 有监督损失函数    y_pred (tensor): bert的输出, [batch_size * 3, dim] """ similarities = F.cosine_similarity(y_pred.unsqueeze(0), y_pred.unsqueeze(1), dim=2) row = torch.arange(0, y_pred.shape[0], 3) col = torch.arange(0, y_pred.shape[0]) col = col[col % 3 != 0] similarities = similarities[row, :] similarities = similarities[:, col] similarities = similarities / temp y_true = torch.arange(0, len(col), 2, device=device) loss = F.cross_entropy(similarities, y_true)return loss
04
结语
本文首先介绍了BERT编码的句向量质量不佳的原因,然后介绍了一种简单有效的句向量对比学习方法SimCSE。最后笔者在STS-B数据集上进行了复现实验,验证了无监督SimCSE的有效性,可惜在有监督SimCSE方法上,未能复现论文中的实验效果,还得再debug一下实验代码。
总体来说,SimCSE的效果确实非常惊艳,通过Dropout这种非常简单的方式进行正样本增强,确实让人眼前一亮。也许很多人有过类似的想法,但是却不一定有动手做实验,实验是检验想法的最佳手段。
笔者认为这种通过对数据添加噪声来构造正样本的对比学习方法,还可以进行深挖。例如在CV中,一般会给图片添加高斯噪声来增强模型的泛化性,可以考虑以某种方式为句向量添加高斯噪声来进行正样本增强,这也不会改变句向量的语义信息。其次,在对比学习中也可以通过增大batch size、引入难、难正样本,来增强模型的泛化性。
点赞、在看、关注,炼丹效率加倍
继续阅读
阅读原文