前言
CoSENT是一种句嵌入模型,它被认为比Sentence-BERT更有效,本篇对CoSENT做理论简述,并结合领域文本训练句嵌入做语义检索,最终对比CoSENT和Sentence-BERT两者的效果差异。
内容摘要
- 有监督句嵌入模型简述
- 快速开始用CoSENT生成句嵌入
- CoSENT的目标函数
- CoSENT模型搭建和语义检索实践
有监督句嵌入模型简述
句嵌入是将句子表征为向量的过程,基于句向量可以进一步完成文本匹配,文本聚类等下游场景任务。句嵌入分为无监督和有监督**两大类,在前文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更好。
①人工智能/大模型学习路线
②AI产品经理入门指南
③大模型方向必读书籍PDF版
④超详细海量大模型实战项目
⑤LLM大模型系统学习教程
⑥640套-AI大模型报告合集
⑦从0-1入门大模型教程视频
⑧AGI大模型技术公开课名额
快速开始用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-BERT | 0.4592 | 0.7006 |
CoSENT-BERT | 0.4974 | 0.7129 |
CoSENT在两个数据上想比于Sentence-BERT都有明显的提升,说明CoSENT训练得到的句嵌入在文本匹配场景表现地更好,这种以余弦相似度的排序作为训练目标的策略更加有效。
读者福利:如果大家对大模型感兴趣,这套大模型学习资料一定对你有用
对于0基础小白入门:
如果你是零基础小白,想快速入门大模型是可以考虑的。
一方面是学习时间相对较短,学习内容更全面更集中。
二方面是可以根据这些资料规划好学习计划和方向。
包括:大模型学习线路汇总、学习阶段,大模型实战案例,大模型学习视频,人工智能、机器学习、大模型书籍PDF。带你从零基础系统性的学好大模型!
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费
】🆓
👉AI大模型学习路线汇总👈
大模型学习路线图,整体分为7个大的阶段:(全套教程文末领取哈)
第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;
第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;
第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;
第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;
第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;
第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;
第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。
👉大模型实战案例👈
光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
👉大模型视频和PDF合集👈
观看零基础学习书籍和视频,看书籍和视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;
• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;
• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;
• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。
👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费
】🆓