前言

Sentence-BERT是一种句嵌入表征模型,常用于文本语义相似度的匹配,本篇对Sentence-BERT做理论介绍,并结合领域文本数据进行实践,训练句嵌入实现语义检索。


内容摘要
  • 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输出的句嵌入能够很好的完成文本向量化和文本相似匹配任务,全文完毕。

如何系统的去学习大模型LLM ?

作为一名热心肠的互联网老兵,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的 AI大模型资料 包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来

😝有需要的小伙伴,可以V扫描下方二维码免费领取🆓

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

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

img

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

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

img

三、AI大模型经典PDF籍

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

img

在这里插入图片描述

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

img

阶段1:AI大模型时代的基础理解

  • 目标:了解AI大模型的基本概念、发展历程和核心原理。
  • 内容
    • L1.1 人工智能简述与大模型起源
    • L1.2 大模型与通用人工智能
    • L1.3 GPT模型的发展历程
    • L1.4 模型工程
    • L1.4.1 知识大模型
    • L1.4.2 生产大模型
    • L1.4.3 模型工程方法论
    • L1.4.4 模型工程实践
    • L1.5 GPT应用案例

阶段2:AI大模型API应用开发工程

  • 目标:掌握AI大模型API的使用和开发,以及相关的编程技能。
  • 内容
    • L2.1 API接口
    • L2.1.1 OpenAI API接口
    • L2.1.2 Python接口接入
    • L2.1.3 BOT工具类框架
    • L2.1.4 代码示例
    • L2.2 Prompt框架
    • L2.2.1 什么是Prompt
    • L2.2.2 Prompt框架应用现状
    • L2.2.3 基于GPTAS的Prompt框架
    • L2.2.4 Prompt框架与Thought
    • L2.2.5 Prompt框架与提示词
    • L2.3 流水线工程
    • L2.3.1 流水线工程的概念
    • L2.3.2 流水线工程的优点
    • L2.3.3 流水线工程的应用
    • L2.4 总结与展望

阶段3:AI大模型应用架构实践

  • 目标:深入理解AI大模型的应用架构,并能够进行私有化部署。
  • 内容
    • L3.1 Agent模型框架
    • L3.1.1 Agent模型框架的设计理念
    • L3.1.2 Agent模型框架的核心组件
    • L3.1.3 Agent模型框架的实现细节
    • L3.2 MetaGPT
    • L3.2.1 MetaGPT的基本概念
    • L3.2.2 MetaGPT的工作原理
    • L3.2.3 MetaGPT的应用场景
    • L3.3 ChatGLM
    • L3.3.1 ChatGLM的特点
    • L3.3.2 ChatGLM的开发环境
    • L3.3.3 ChatGLM的使用示例
    • L3.4 LLAMA
    • L3.4.1 LLAMA的特点
    • L3.4.2 LLAMA的开发环境
    • L3.4.3 LLAMA的使用示例
    • L3.5 其他大模型介绍

阶段4:AI大模型私有化部署

  • 目标:掌握多种AI大模型的私有化部署,包括多模态和特定领域模型。
  • 内容
    • L4.1 模型私有化部署概述
    • L4.2 模型私有化部署的关键技术
    • L4.3 模型私有化部署的实施步骤
    • L4.4 模型私有化部署的应用场景

学习计划:

  • 阶段1:1-2个月,建立AI大模型的基础知识体系。
  • 阶段2:2-3个月,专注于API应用开发能力的提升。
  • 阶段3:3-4个月,深入实践AI大模型的应用架构和私有化部署。
  • 阶段4:4-5个月,专注于高级模型的应用和部署。
这份完整版的大模型 LLM 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

😝有需要的小伙伴,可以Vx扫描下方二维码免费领取🆓

GitHub 加速计划 / be / bert
8
2
下载
TensorFlow code and pre-trained models for BERT
最近提交(Master分支:4 个月前 )
eedf5716 Add links to 24 smaller BERT models. 4 年前
8028c045 - 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐