Embedding技术:CoSENT句嵌入模型理论介绍和实践

前言

CoSENT是一种句嵌入模型,它被认为比Sentence-BERT更有效,本篇对CoSENT做理论简述,并结合领域文本训练句嵌入做语义检索,最终对比CoSENT和Sentence-BERT两者的效果差异。

有监督句嵌入模型简述

句嵌入是将句子表征为向量的过程,基于句向量可以进一步完成文本匹配,文本聚类等下游场景任务。句嵌入分为无监督和有监督**两大类,在前文Embedding技术:Sentence-BERT句嵌入模型介绍和实践提到的Sentence-BERT是一种有监督句嵌入方案,它通过人工标注的三元组数据(句子1,句子2,是否相似),微调BERT使得相似语义的文本表征距离更小,而无监督的方案不需要人工标注,它依据文本的上下文关系来构造出预测任务,句嵌入是该任务的中间产物,这类方法包括Word2Vec词嵌入池化、Doc2Vec、Sentence2Vec、Skip-Thought Vectors等。

Skip-Thought Vectors无监督句嵌入模型

本篇介绍另一种有监督句嵌入模型CoSENT(Cosine Sentence),它将cosine余弦相似度的排序损失引入到Sentence-BERT的训练环节,使得训练过程更加契合应用场景,同时加快模型在训练阶段的收敛,在众多数据集上表现比Sentence-BERT更好。


快速开始用CoSENT生成句嵌入

在HuggingFace模型仓库中下载shibing624/text2vec-base-chinese预训练模型,它是以macbert作为模型基座,通过CoSENT损失函数策略微调得到的文本向量化模型,可以实现对输入文本做Embedding表征。

text2vec预训练模型

CoSENT也是BERT模型微调的结果,因此使用BERT的模型API导入CoSENT模型和词表

>>> from transformers import BertTokenizer, BertModel
>>> embedding_model_name = "./text2vec-base-chinese"
>>> embedding_model_length = 512
>>> tokenizer = BertTokenizer.from_pretrained(embedding_model_name)
>>> model = BertModel.from_pretrained(embedding_model_name)

输入四个样例句子,对它们进行分词编码预处理

>>> sentences = ['我不知道过年火车票能不能抢到', '过年假期你准备去哪里玩', '我准备春节请假两天提前回家,但是好没有抢到票', '这个假期太短了,我作业还没有做完']
>>> encoded_input = tokenizer(sentences, padding=True, truncation=True, return_tensors='pt')

输出层需要使用BERT最后一层block的非Padding位置所有词Embedding的均值池化作为句嵌入,定义mean_pooling函数来实现该操作

>>> def mean_pooling(model_output, attention_mask):
        token_embeddings = model_output[0]  # First element of model_output contains all token embeddings
        input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
        return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)

最后我们使用CoSENT对句子进行推理,生成[4, 768]的矩阵,代表每个句子表征为768维的向量

>>> with torch.no_grad():
        model_output = model(**encoded_input)
>>> sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask']).cpu().numpy()
>>> sentence_embeddings.shape
(4, 768)
>>> sentence_embeddings
array([[-0.7421524 ,  1.8644766 , -0.26227337, ..., -1.9969096 , -0.6115613 ,  0.08333459],
       [-0.03595918,  1.1413909 ,  0.6439935 , ...,  0.22788872, 0.69812274, -0.23767757],
       [-0.6670484 , -0.44258702,  0.11655644, ..., -1.0349655 , -0.42188278,  0.10289162],
       [-0.02075486,  0.17156008,  1.0694983 , ..., -0.51148343, -1.1184878 ,  0.15443501]], dtype=float32)

我们以第一句“我不知道过年火车票能不能抢到”为目标,分别计算它和其他三个句子的余弦相似度,来初步验证CoSENT做文本匹配的有效性

>>> def compute_sim_score(v1, v2):
        return v1.dot(v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

>>> compute_sim_score(sentence_embeddings[0], sentence_embeddings[1])  # 0.49395
>>> compute_sim_score(sentence_embeddings[0], sentence_embeddings[2])  # 0.6530
>>> compute_sim_score(sentence_embeddings[0], sentence_embeddings[3])  # 0.4531

结果汇总为表格如下,句嵌入的相似结果和实际的语义情况相符,说明CoSENT和text2vec预训练模型有一定的效果

目标句子候选句子余弦相似度
我不知道过年火车票能不能抢到过年假期你准备去哪里玩0.4940
我不知道过年火车票能不能抢到我准备春节请假两天提前回家,但是好没有抢到票0.6530
我不知道过年火车票能不能抢到这个假期太短了,我作业还没有做完0.4531

CoSENT的目标函数

CoSENT是Sentence-BERT的改进版本,两者的模型基座相同,在损失函数部分,CoSENT使用余弦相似度排序损失,替换了Sentence-BERT的分类交叉熵损失。
一般有监督句嵌入的样本为三元组(句子1,句子2,是否相似),Sentence-BERT的训练过程以是否相似作为分类任务来微调BERT,而预测阶段将BERT单独从模型中剥离出来,拿到BERT的表征作为句嵌入,在应用层使用余弦相似度作为文本匹配的依据,很明显Sentence-BERT的训练阶段和预测阶段目标不一致,可能出现训练中交叉熵损失还在下降,但是实际余弦相似度却没有提升的情况。

Sentence-BERT训练和预测阶段的流程图

由于应用层采用余弦相似度,因此作者想让训练和预测统一,但是Sentence-BERT直接使用余弦相似度或者它的变体作为损失函数效果并不好,原因是余弦相似度的值映射到是否相似的标签上不合适,导致样本矛盾,模型难以收敛
在样本中出现的正样本标记为1,它们都是语义相同的样本,负样本标记为0,它们都是语义有差异,但是字面很相似的样本,举例如下

正样本:什么时候可以降低花呗额度    花呗怎么降低额度    1
负样本:花呗里面没有看到    花呗也没有看到钱    0

其中负样本的句子1和句子2存在字和词的高度重叠,这种样本对模型来说属于“有一定难度的样本”,它们确实语义不同,但是由于本身重叠很高,余弦相似度自然也比较高(比如等于0.7),因此直接计算它们的余弦相似度并且往标签0去学对模型来说过头了,负样本还远达不到0的水平,真正0水平的样本应该是句子1和句子2完全驴头不对马嘴,而给到样本聚焦在一个比较小和比较难的样本空间,隔绝了大量的容易样本。本质上的原因是语义不相似,不代表余弦相似度就应该低。
如果不改变这种小样本空间,相应的好坏分割点的阈值应该被提高,比如以0.8作为阈值,因为给到的不论正样本还是负样本大体都是相似的。作者的创新点在于直接抛弃阈值,以排序的思想解决小样本空间问题,既然不能用绝对的是否来衡量,那么相似度的大小排序总应该还是存在的吧,即所有正样本对的余弦相似度应该尽可能的比所有负样本对的余弦相似度更高。
令一个批次下有三对样本,分别是正样本V1,V2,负样本V3,V4,V5,V6,<.,.>代表向量的余弦相似度,则期望<V1,V2>比其他两个都要大,列入笛卡尔积的所有正负样本的比较如下,其中?位置代表同类样本的比较,可以忽视,在损失函数中不起作用

相似度对比<V1,V2><V3,V4>V5,V6
<V1,V2>等于大于大于
<V3,V4>小于等于?
<V5,V6>小于?等于

为了能实现所有正样本都能比负样本余弦相似度更高的目的,作者提出CoSENT的目标函数如下

CoSENT目标函数

这个式子是一个LogSumExp形式,LogSumExp可以看成是max的光滑近似,我们把log括号后面的1替换为e的0次幂,实际上就是某样本减去它自身,因此上式可以转化为,其中s代表余弦相似度

LogSumExp的光滑近似

公式右侧是一个笛卡尔积组合,罗列了所有负例和正例相减的情形,若所有正样本的余弦相似度都比负样本要高,则公式右侧的值为0,此时损失近似为0,而如果存在有正样本的余弦相似度比负样本要低,则取负样本和正样本得分差距最大的那个作为最终的损失。通过这种方式期望模型给任意正样本的余弦相似度得分都能够比负样本来的大,实现所有正样本都排在负样本前面的效果,这种方式绕过了阈值,直接从排序角度来对目标进行约束


CoSENT模型搭建和语义检索实践

本例参考前文Embedding技术:Sentence-BERT句嵌入模型介绍和实践,采用同样的数据集和模型基座bert-base-chinese,实现最终模型效果的对比。
为了使得在train状态下BERT的输出结果一致,将一对样本的句子1和句子2进行上下堆叠,在损失计算之前再分别取偶数位和奇数位分别拿到句子1和句子2的表征,模型层代码如下

def get_cosine_score(s1: torch.Tensor, s2: torch.Tensor):
    s1_norm = s1 / torch.norm(s1, dim=1, keepdim=True)
    s2_norm = s2 / torch.norm(s2, dim=1, keepdim=True)
    cosine_score = (s1_norm * s2_norm).sum(dim=1)
    return cosine_score


class SentenceBert(nn.Module):
    def __init__(self):
        super(SentenceBert, self).__init__()
        self.pre_train = PRE_TRAIN
        self.linear = nn.Linear(PRE_TRAIN_CONFIG.hidden_size * 3, 2)
        nn.init.xavier_normal_(self.linear.weight.data)

    def forward(self, s):
        s_emb = self.pre_train(**s)['last_hidden_state'][:, 0, :]
        s1_emb, s2_emb = s_emb[::2], s_emb[1::2]
        cosine_score = get_cosine_score(s1_emb, s2_emb)
        return s1_emb, s2_emb, cosine_score

其中在前向传播中计算两向量的余弦相似度cosine_score用于和真实标签计算Spearman相关系数,而Spearman相关系数作为早停条件,如果连续10次验证集不上升则停止训练。
CoSENT的核心在于目标函数,它针对一个批次下的所有样本对,句子1和句子2的余弦相似度进行两两交叉组合相减,通过标签y挑选保留下所有负例-正例的情况,其他全部改为负无穷大,使得e的次幂接近为0对求和结果无效,在logsumexp中加入一项0,作为负例-正例的天花板

def cosent_loss(s1_emb, s2_emb, labels):
    # TODO [batch_size/2, 1] < [1, batch_size/2] => [batch_size/2, batch_size/2],
    # TODO 0<1,为1的时候都是负样本-正样本,
    labels = (labels[:, None] < labels[None, :]).to(float)
    cosine_score = get_cosine_score(s1_emb, s2_emb) * 20
    # TODO [batch_size/2, 1] - [1, batch_size/2] => [batch_size/2, batch_size/2], 该批次下每一对的余弦相似度和自己以及其他对的差
    cosine_diff = cosine_score[:, None] - cosine_score[None, :]
    # TODO 将正样本-其他,或者自身-自身这种情况踢出,置为负无穷大即可,只允许负样本-正样本
    cosine_diff = (cosine_diff - (1 - labels) * 1e12).reshape(-1)
    # TODO 补充上自身和自身相减
    cosine_diff = torch.concat([torch.tensor([0.0]).to(DEVICE), cosine_diff], dim=0)
    return torch.logsumexp(cosine_diff, dim=0)

模型训练过程将一个批次下所有句子1的表征,和句子2的表征传入cosent_loss即可进行损失迭代

for step, (s, labels) in enumerate(train_loader):
    s, labels = s.to(DEVICE), labels.to(DEVICE)[::2]
    model.train()
    optimizer.zero_grad()
    s1_emb, s2_emb, cosine_score = model(s)
    loss = cosent_loss(s1_emb, s2_emb, labels)
    loss.backward()
    optimizer.step()
    ...   

训练集早停,以及测试集测评日志如下,在ATEC文本匹配数据集上测试集的Spearman相关系数有0.4973

epoch: 6, step: 622, loss: 5.033726978888658, corrcoef:0.6161128974200909
epoch: 6, step: 623, loss: 5.7895638147161375, corrcoef:0.7360637834284756
100%|██████████| 313/313 [00:31<00:00,  9.78it/s]
[evaluation] loss: 6.528893296747316 corrcoef: 0.4973212744783885
本轮Spearman相关系数比之前最大Spearman相关系数下降:0.006715430800930733, 当前最大Spearman相关系数: 0.5040367052793192

early stop...

[test] loss: 2113085378242445, corrcoef: 0.4973765080838994

笔者分别在蚂蚁金服ATEC和微众银行BQ两个问句数据上做了Sentence-BERT和CoSENT的测试,对比结果如下

算法/数据集合ATEC数据集BQ数据集
Sentence-BERT0.45920.7006
CoSENT-BERT0.49740.7129

CoSENT在两个数据上想比于Sentence-BERT都有明显的提升,说明CoSENT训练得到的句嵌入在文本匹配场景表现地更好,这种以余弦相似度的排序作为训练目标的策略更加有效。

最后的最后

感谢你们的阅读和喜欢,我收藏了很多技术干货,可以共享给喜欢我文章的朋友们,如果你肯花时间沉下心去学习,它们一定能帮到你。

因为这个行业不同于其他行业,知识体系实在是过于庞大,知识更新也非常快。作为一个普通人,无法全部学完,所以我们在提升技术的时候,首先需要明确一个目标,然后制定好完整的计划,同时找到好的学习方法,这样才能更快的提升自己。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

img

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

四、AI大模型商业化落地方案

img

五、面试资料

我们学习AI大模型必然是想找到高薪的工作,下面这些面试题都是总结当前最新、最热、最高频的面试题,并且每道题都有详细的答案,面试前刷完这套面试题资料,小小offer,不在话下。
在这里插入图片描述

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值