Embedding技术:Sentence-BERT句嵌入模型介绍和实践
前言
Sentence-BERT是一种句嵌入表征模型,常用于文本语义相似度的匹配,本篇对Sentence-BERT做理论介绍,并结合领域文本数据进行实践,训练句嵌入实现语义检索。
Embedding技术和句嵌入简述
Embedding是将某个实体转换为由数字序列形成的向量,使得计算机能够该实体进行理解,从而完成各种算法任务,Embedding技术广泛应用于自然语言处理、图像识别、推荐系统等场景。在NLP和大模型领域,文本经过分词编码和Embedding处理成数值信息灌入语言模型,通过海量语料的训练使得模型具备类似人类一样的语义理解和生成能力。
自然语言通过Embedding进行语义表征
对文本中的每个分词进行Embedding称为词嵌入,对一整句或者一段文本进行Embedding称为句嵌入,句嵌入在文本推荐、查询改写、智能问答、知识库检索等领域有广泛的应用。这些嵌入向量作为模型的中间产物,如果对其本身进行向量聚类和相似度匹配,也可以挖掘出语义的关系远近,一般的,通过对文本做Embedding向量化配合余弦相似度来进行语义比对,余弦相似度越大,实体在语义空间的夹角越小,语义越相似。
句嵌入在大模型问答中的应用
Sentence-BERT快速开始
Sentence-BERT是一种句嵌入模型,输入一段文本,输出整段文本的向量表征。在HuggingFace仓库中下载预训练模型sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2进行调用,快速开始使用Sentence-BERT输出句向量。
>>> from transformers import AutoTokenizer, AutoModel
>>> import torch
>>> tokenizer = AutoTokenizer.from_pretrained('./sentence-transformers')
>>> model = AutoModel.from_pretrained('./sentence-transformers')
>>> sentences = ['中午我想吃清蒸鲈鱼', '天气预报说明天下雨', '食堂的餐饭不好吃', '我做了红烧鱼作为中午的饭菜']
>>> encoded_input = tokenizer(sentences, padding=True, truncation=True, return_tensors='pt')
>>> with torch.no_grad():
model_output = model(**encoded_input)
>>> sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask']).cpu().numpy()
以上代码对四句话进行句嵌入表征,分别是“中午我想吃清蒸鲈鱼”,“天气预报说明天下雨”, “食堂的餐饭不好吃”,“我做了红烧鱼作为中午的饭菜”,Sentence-BERT通过对最后一层输出的所有非Padding位置的词向量做均值池化获得句子向量,每个句子表征为384维。
>>>> sentence_embeddings.shape
Out[20]: (4, 384)
>>> sentence_embeddings
Out[21]:
array([[ 0.10295737, 0.01503621, 0.22508383, ..., -0.35927626, -0.0098072 ],
[ 0.11987129, 0.02635192, 0.04973981, ..., -0.32242486, 0.02670684],
[ 0.39152732, -0.32321697, 0.17835289, ..., -0.15338285, -0.11770304],
[ 0.01825497, 0.14122407, 0.26426324, ..., -0.20018129, 0.07183265]], dtype=float32)
进一步用余弦相似度计算第一句和其他三个句子之间的语义相关程度
>>> 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])
Out[22]: 0.33636028
>>> compute_sim_score(sentence_embeddings[0], sentence_embeddings[2])
Out[23]: 0.39042273
>>> compute_sim_score(sentence_embeddings[0], sentence_embeddings[3])
Out[24]: 0.726213
汇总相似度得分表格如下,“中午我想吃清蒸鲈鱼”和“我做了红烧鱼作为中午的饭菜”的语义相关程度最高,该结论也符合人类的感知,说明Sentence-BERT句嵌入具有一定的有效性。
目标句子 | 候选句子 | 相似度 |
---|---|---|
中午我想吃清蒸鲈鱼 | 天气预报说明天下雨 | 0.3363 |
中午我想吃清蒸鲈鱼 | 食堂的餐饭不好吃 | 0.3904 |
中午我想吃清蒸鲈鱼 | 我做了红烧鱼作为中午的饭菜 | 0.7262 |
Sentence-BERT原理综述
Sentence-BERT是2019由论文《Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks》提出的一种有监督的句嵌入算法,它本质上是基于BERT预训练模型的输出作为句嵌入,额外的,它引入孪生网络的思想将一对句子的表征和人工标注的相似度做比对,从而实现对BERT的微调,使得BERT输出的句嵌入更加契合语义匹配的场景。
给定一段文本输入给BERT,BERT输出为[batch_size, seq_len, emb_size]的矩阵,它由每个位置的token Embedding构成,在文本分类等下游任务中,一般将[CLS]位置的Embedding或者所有token Embedding的均值池化作为整段文本的信息表征。这种表征方式无法适配语义检索场景,因为BERT的预训练是基于自然文本,侧重于学习词和句子的上下文关联,而上下文关联并不代表语义相似。
BERT表征句向量
另一方面,BERT自身可以完成文本匹配的下游任务,输入一对句子,拼接为[CLS]+Sentence 1+[SEP]+Sentence 2+[SEP],做二分类预测两个句子语义是否相近,网络结构如下图所示。这种方式端到端地预测两个句子的匹配程度,但是每次都需要将目标句子和候选所有句子输入到BERT中进行分类预测,推理成本极高,不适合大规模的语义检索场景。
通过BERT推理一对句子的相似度
综上所述作者提出Sentence-BERT(SBERT),通过孪生BERT网络以及人工标注的语义相似三元组数据对BERT做微调,在部署阶段推理出句嵌入,后续再使用余弦相似度进行语义搜索。
Sentence-BERT分为训练和预测两个阶段,训练阶段基于标注的三元组句子对有监督微调BERT,而预测阶段直接基于微调后的BERT生成句嵌入,训练阶段的网络结构如下
Sentence-BERT训练网络结构
作者参考了孪生网络的思想引入了两个参数完全一样的BERT,令BERT输出的embedding维度为d,将句子A和句子B分别输入其中,通过池化得到两个句嵌入u,v,进一步作者对u,v向量做逐位相减再取绝对值,最终生成三组向量将它们拼接为长度3d,输入全连接层做二分类Softmax交叉熵预测。
对于池化操作的选择作者分别尝试了均值池化,最大池化,和直接使用[CLS]位置三种方式,在实验中均值池化效果最好。
在最后一层全连接之前的向量拼接方式选取上,作者尝试了u,v向量的多种element-wise组合方法,包括逐位置相乘,逐位相减等,在实验中u,v和两者逐位相减拼接的效果最好。
论文中池化方式和拼接方式的实验结果
在损失函数的构造上,根据标注数据的类型不同,作者分别采用了二分类交叉熵,MSE回归指标,以及对比学习中的Triplet loss三元组损失,不论采用哪种损失最终的目的都是使得句嵌入的距离能够和人工标注的语义相似关系对齐。样本类型包括
1.是非类型:格式为“(句子1, 句子2, 是否相似)”,采用二分类交叉熵损失
2.打分类型:格式为“(句子1, 句子2, 相似程度)”,采用回归指标MSE作为损失,拟合人工标注的打分
3.三元组句子类型:格式为“(基准句子1, 正例句子2, 负例句子3)”,采用Triplet loss三元组损失,模型学习的目标是使得基准句子和正例句子的语义距离更近,和负例句子的语义距离更远
Triplet loss示意图
Sentence-BERT的预测阶段直接使用微调后的BERT根据相同的池化方式输出句嵌入即可,相当于把训练阶段的孪生网络中的BERT单独摘出来,Sentence-BERT只负责输出句嵌入u,v,后续的相似度检索交给下游的余弦相似度任务单独实现。预测阶段的网络结构如下。
预测阶段Sentence-BERT网络结构
孪生神经网络和对比学习
Sentence-BERT的理论涉及到孪生神经网络和对比学习这两个概念,具体而言,Sentence-BERT采用了孪生神经网络的结构,在损失优化环节借鉴了对比学习的策略。孪生网络和对比学习这两种方法常常一起使用,本节对这两个概念做简要说明介绍。
孪生神经网络(Siamese Networks),它由两个权重共享的任意神经网络拼接而成,两个样本分别输入,输出其嵌入高维空间的表征,从而比较两个样本的相似程度。孪生神经网络于1994年被首次提出,用于验证手写平板电脑签名,一对孪生网络分别提取两个签名的特征表示,从而量化两个特征向量之间的距离,若手写签名与以前存储签名间的距离小于预设阈值则被接受,否则将被视为伪造签名。同理,孪生神经网络也常用于人脸比对
孪生神经网络做人脸比对
对比学习(Contrastive Learning),对比学习的目标是学习一个编码器,使得相似实体的编码在特征空间中尽可能接近,不相似的实体编码结果在特征空间中尽可能的远。对比学习基于代理任务预先生成相似样本和不相似样本,从而提供了一个监督信号结合目标函数去训练模型。其中代理任务通常是人为设定的一些相似规则,数据增强是代理任务的实现常见手段,目标函数一般是基于向量距离的计算,比如Triplet loss中采用基准分别和正例负例的距离之差的最大值作为优化目标,公式中Sa,Sp,Sn分别代表基准向量,正例向量,负例向量,||…||代表两个向量的距离度量方式
Triplet loss公式
Sentence-BERT句嵌入语义相似检索实践
本节将基于预训练bert-base-chinese模型,在领域文本上从头训练一个Sentence-BERT模型,完成训练和预测两个流程,并且基于预测的向量结果完成文本相似检索。
数据预览
采用公开的ATEC文本匹配数据集,内容包含10万多条客服问句匹配样本,格式为三元组形式(问句1,问句2,是否相似),数据样例如下
打不开花呗 为什么花呗打不开 1
花呗收钱就是用支付宝帐号收嘛 我用手机花呗收钱 0
花呗买东西,商家不发货怎么退款 花呗已经分期的商品 退款怎么办 0
数据处理
Sentence-BERT网络结构比较简单,只需要将问句1和问句2分别经过BERT的分词编码,再输入给BERT拿到表征,拼接后输入全连接做二分类预测即可,BERT表征的方式本例采用[CLS]位置,也可以使用其他方式,比如非Padding位置的均值池化等。
在数据处理环节,需要将单条样本的问句1,问句2上下堆叠成两条样本,目的是统一输入同一个BERT网络,如果分开输入,在train状态下由于有Dropout的存在,就算是相同参数的BERT输出也不一样,此时就不满足孪生神经网络的要求,因此需要将原始一对问句进行堆叠,比如一个批次处理32条原始三元组样本,则实际灌入模型的是64条二元组,堆叠函数如下
def collate_fn(data):
s, labels = [], [] # 二元组
for d in data: # 三元组(s1, s2, label)
s.append(d[0])
s.append(d[1])
labels.append(d[2]) # y值也需要复制一次
labels.append(d[2])
s_token = TOKENIZER.batch_encode_plus(s, truncation=True, max_length=PRE_TRAIN_CONFIG.max_position_embeddings,
return_tensors="pt", padding=True)
labels = torch.LongTensor(labels)
return s_token, labels
网络搭建
在网络层只有两个模块预训练BERT和Linear,BERT拿到最后一层的[CLS]位置表征,由于前面有堆叠操作,此处再取出所有偶数位还原出所有句子1,取出所有奇数位还原出句子2,两者拼接上相减绝对值之后一齐输入给Linear。
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 get_cosine_score(self, s1, s2):
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
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 = self.get_cosine_score(s1_emb, s2_emb)
concat = torch.concat([s1_emb, s2_emb, torch.abs(s1_emb - s2_emb)], dim=1)
output = self.linear(concat)
return output, cosine_score
损失函数采用交叉熵,前向传播部分同时输出该批次样本的每对句子的余弦相似度cosine_score,它和标签y值进行皮尔逊相关系数计算,定义10次验证集皮尔逊相关系数不上升作为早停条件。皮尔逊相关系数越大越好,说明余弦相似度和真实的是否相似的情况趋势越趋同。
model = SentenceBert().to(DEVICE)
criterion = nn.CrossEntropyLoss(reduction="mean")
optimizer = torch.optim.AdamW(model.parameters(), lr=0.00003)
for step, (s, labels) in enumerate(train_loader):
s, labels = s.to(DEVICE), labels.to(DEVICE)[::2] # labels需要折叠,取偶数位即可
model.train()
...
output, cosine_score = model(s)
loss = criterion(output, labels)
...
# 计算皮尔逊相关系数作为早停条件
corrcoef = compute_corrcoef(cosine_score.detach().cpu().numpy(), labels.detach().cpu().numpy())
print("epoch: {}, step: {}, loss: {}, corrcoef:{}".format(epoch + 1, step, loss.item(), corrcoef))
if step % 200 == 0 or step == len(train_loader):
# 验证集早停逻辑
loss_val, corrcoef_val = eval_metrics(model, val_loader)
...
模型评价指标
在测试阶段,依旧采用测试集预测每对样本的余弦相似度和真实y值的皮尔逊相关系数作为评价指标,最终相关系数为0.4592。
# TODO 测试
model2 = SentenceBert().to(DEVICE)
model2.load_state_dict(torch.load("./model/sbert_{}.bin".format(data)))
loss_test, corrcoef_test = eval_metrics(model2, test_loader)
# 0.41255660838512037 0.4555726951427768
print(loss_test, corrcoef_test)
模型预测向量
在预测流程中,只需要将微调之后BERT从Sentence-BERT网络中摘出来即可,后续的向量预测都仅仅需要该BERT模型,首先只对整个网络中的pre_train BERT进行保存
s_bert = model2.pre_train
torch.save(s_bert.state_dict(), "./model/sbert_ATEC/pytorch_model.bin")
预测的时候通过HuggingFace的BERT模型API进行导入
from transformers import BertModel, BertTokenizer, BertConfig
PRE_TRAIN_PATH = "model/sbert_ATEC"
TOKENIZER = BertTokenizer.from_pretrained(PRE_TRAIN_PATH)
PRE_TRAIN_CONFIG = BertConfig.from_pretrained(PRE_TRAIN_PATH)
PRE_TRAIN = BertModel.from_pretrained(PRE_TRAIN_PATH)
将样本中所有句子1和句子2全部按照批次灌入BERT中进行[CLS]位置的向量预测,代码如下
cut = list(range(0, len(total), batch_size))
for i in range(len(cut)):
start, end = cut[i], len(total) if i == len(cut) - 1 else cut[i + 1]
batch_text = total[start:end]
text_token = TOKENIZER.batch_encode_plus(batch_text, truncation=True, padding=True,
max_length=PRE_TRAIN_CONFIG.max_position_embeddings,
return_tensors="pt")
embs = PRE_TRAIN(**text_token)[0][:, 0, :]
embs_norm = (embs / torch.norm(embs, dim=1, keepdim=True)).detach().cpu().numpy().tolist()
total_emb.extend(embs_norm)
pickle.dump((total, total_emb), open("./model/sbert_ATEC/emb.bin", "wb"))
每个句子都会生成一个768维度的向量,预览其中1条如下
句子:蚂蚁借呗用了了多久能恢复
向量:[-0.017775828018784523, 0.06854370981454849, -0.009083558805286884, 0.007142649497836828,...]
文本匹配检索
最终我们想输入任意文本,在候选的所有句子中找到和它最相似的文本,本例采用Numpy直接计算余弦相似度,整个过程包含对输入文本的分词编码,输入文本的BERT向量输出,输入文本向量和所有候选向量比对三个过程,我们取最相似的top3句子以及相似度得分。
def search_top_n(input_text, candidate_text, candidate_emb, top_n=3):
text_token = TOKENIZER.batch_encode_plus([input_text], truncation=True, padding=True,
max_length=PRE_TRAIN_CONFIG.max_position_embeddings,
return_tensors="pt")
embs = PRE_TRAIN(**text_token)[0][:, 0, :].detach().cpu().numpy()
# TODO 输入文本向量标准化
embs = embs / np.linalg.norm(embs, axis=1)
# TODO 计算余弦相似度
scores = np.dot(embs, np.array(candidate_emb).T)
scores[np.isneginf(scores)] = 0
top_score = np.sort(scores, axis=1)[:, -3:]
top_index = np.argsort(scores, axis=1)[:, -3:]
res = []
for s, i in zip(top_score, top_index):
one = []
for n in range(top_n):
one.append({"text": candidate_text[i[n]], "score": s[n]})
res.append(one)
return res
测试1:输入文本为 “如何关闭支付宝免密支付”, 运行输出如下,候选的三个句子和输入语义完全相同。
>>> input_text = "没网的时候支付宝能够支付吗"
>>> search_top_n(input_text, total, total_emb, top_n=3)
[[{'text': '怎样去消花呗的免密支付', 'score': 0.9739149930056332},
{'text': '怎么关闭花呗免密支付', 'score': 0.9834292467296013},
{'text': '怎样关闭花呗的免密支付', 'score': 0.9878402150756829}]]
测试2:输入文本为 “没网的时候支付宝能够支付吗”,运行输出如下,候选的三个句子和输入语义相似,但是主体存在略微差异和不明确。
>>> input_text = "没网的时候支付宝能够支付吗"
>>> search_top_n(input_text, total, total_emb, top_n=3)
[[{'text': '手机没网,花呗会自动扣款吗', 'score': 0.874686905948586},
{'text': '不用手机支付宝,花呗能自动还款吧', 'score': 0.8775933520615644},
{'text': '我没有手机支付宝 是不是就没办法给花呗还款了', 'score': 0.8905988043804666}]]
测试3:输入文本为 “支付宝能炒股吗”,运行输出如下,效果可以语义基本相同。
>>> input_text = "支付宝能炒股吗"
>>> search_top_n(input_text, total, total_emb, top_n=3)
[[{'text': '借呗可以用来买股票吗', 'score': 0.900523830153816},
{'text': '蚂蚁借呗能拿来买股票吗', 'score': 0.9067184565541515},
{'text': '借呗可以炒股吗', 'score': 0.9342895273812792}]]
从实践结果来看,Sentence-BERT输出的句嵌入能够很好的完成文本向量化和文本相似匹配任务,全文完毕。
最后的最后
感谢你们的阅读和喜欢,我收藏了很多技术干货,可以共享给喜欢我文章的朋友们,如果你肯花时间沉下心去学习,它们一定能帮到你。
因为这个行业不同于其他行业,知识体系实在是过于庞大,知识更新也非常快。作为一个普通人,无法全部学完,所以我们在提升技术的时候,首先需要明确一个目标,然后制定好完整的计划,同时找到好的学习方法,这样才能更快的提升自己。
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。
四、AI大模型商业化落地方案
五、面试资料
我们学习AI大模型必然是想找到高薪的工作,下面这些面试题都是总结当前最新、最热、最高频的面试题,并且每道题都有详细的答案,面试前刷完这套面试题资料,小小offer,不在话下。
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
更多推荐
所有评论(0)