直接用BERT预训练模型计算句子相似度,无需微调
2026/9/24 22:46:04 网站建设 项目流程

简介:面向自然语言处理初学者与开发者,这份下载包提供了一整套基于 PyTorch Transformers 加载 BERT 预训练模型计算句子相似度的可直接运行方案。资源以文本相似度计算为主线,涵盖问答匹配、文档检索、语义分析等常见场景,通过调用中文 BERT(chinese-bert-wwm-ext)模型,演示了从分词编码、模型推理到余弦相似度计算的核心流程。压缩包共 4 个文件,包括 Python 脚本、文本说明、config.json 配置和 pytorch_model.bin 预训练权重文件,整体约 365.84 MB;其中 py 脚本为可直接运行的示例代码,txt 说明梳理实现思路,config.json 与 bin 分别对应模型配置和预训练权重。运行脚本时,BertTokenizer 完成句子分词与特殊标记添加,BertModel 获取 last_hidden_state 中的句子向量,再通过 cosine_similarity 计算相似度,输出数值越接近 1 代表语义越相近。目前已有 1641 人学习下载,对希望快速上手 BERT 语义匹配任务的读者具有直接参考价值。

1. 算相似度先别急着训练:直接加载 BERT 预训练模型就够了

句子相似度这个需求,几乎每个做文本的团队都会遇到:搜索召回、重复问题合并、客服问答匹配、论文去重。很多人第一反应是“得先标注数据、微调模型”,但如果你只是先验证效果、跑通基线,直接用 torch 和 transformers 加载一个 BERT 预训练模型,不做任何训练也能算出可用的语义相似度。这个方案的核心就三步:加载 tokenizer 和模型,把句子过一遍得到向量,再算余弦相似度。写起来不过三十行代码,CPU 也能跑。适合想快速搭起语义匹配基线、或者需要离线批量算相似度的从业者。

这里有一个反直觉的点:BERT 明明是个语言模型,为什么不做任何微调,直接拿它输出的向量算相似度,效果就比字符重叠、TF-IDF 好一大截?因为它经过预训练后,每一层 Transformer 编码出的向量已经包含了上下文语义信息,句子向量之间的空间距离,本身就是一种相似度度量。本文沿着这条路径,从环境安装、模型加载、向量计算到踩坑排查,完整走一遍。

2. 装环境与加载 bert-base-chinese:先让模型在本地吐出一个向量

2.1 安装 torch 与 transformers:先解决本地跑起来的问题

这个方案的技术栈只有三个:torch 负责张量计算,transformers 提供模型和分词器,BERT 预训练模型作为权重。先装软件包。

pip install torch transformers

如果你有 NVIDIA GPU 并且想用 GPU 加速,一般做法是先去官网选对应的 CUDA 版本再装 torch,然后把 transformers 一起装掉。我通常直接用默认源写这行命令,它能自动拉取当前 Python 版本匹配的 torch 和 transformers。但如果你装过 PyTorch 的 CPU 版,后面想切换 GPU 版,最简单的办法是建一个新的虚拟环境,不要原地反复覆盖,省得出现torch.cuda.is_available()永远为 False 的玄学问题。

关于版本,不用刻意追求最新。torch 2.x 系列配 transformers 4.x 系列是目前最常见组合,transformers 的from_pretrained接口从 3.0 以后就非常稳定。如果你看到error: could not find a version that satisfies the requirement torch,别急,这个错误大概率不是版本号的问题,而是你的 pip 源里没有对应平台和 Python 版本的 wheel,具体排查我放到第 5 章的“坑”里面讲。

装完后先跑一个最基本的检查:

import torch import transformers print("torch:", torch.__version__) print("transformers:", transformers.__version__) print("cuda available:", torch.cuda.is_available())

这里cuda available输出 False 不影响跑通本文的代码,只是速度会慢一些。如果你有 GPU 但输出 False,先查驱动和 CUDA 工具包,再决定要不要重装 torch。

2.2 用 from_pretrained 加载 bert-base-chinese:第一次下载与本地缓存

这里我们用 BERT 的中文预训练权重bert-base-chinese。它是 Google 发布的 12 层 Transformer,隐藏层维度 768,参数量大约 1.1 亿,对中文句子相似度来说是一个非常稳妥的基线模型。加载代码极短:

from transformers import AutoTokenizer, AutoModel model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModel.from_pretrained(model_name) print("tokenizer 类型:", type(tokenizer)) print("model 类型:", type(model))

第一次执行这代码时,transformers 会去 Hugging Face 下载模型权重和词典文件,保存到本地缓存目录。第二次再跑,就直接从缓存读取了,不会重复下载。AutoTokenizer会根据你传的模型名自动选择对应的分词器类,AutoModel同理,这也是相比直接写BertTokenizerBertModel更省心的原因:换 RoBERTa、换多语言模型,代码不用改。

这里需要理解一个关键概念:分词器负责把句子变成 token id,模型负责把 token id 变成向量。两者必须来自同一个预训练模型,不能拿bert-base-chinese的 tokenizer 去配其他模型的权重,否则词典对不上,出来的向量就是乱套的。

2.3 确认模型在 CPU/GPU 上运行:device 迁移与推理开关

模型加载进来后默认在 CPU 上。如果你的机器有 GPU,我建议显式把模型搬过去:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) model.eval()

这三行是后面所有代码的地基。model.to(device)把权重搬到 GPU 显存里;model.eval()把模型切到推理模式,关掉 Dropout 和 BatchNorm 的训练行为。

我见过不少人忘了调model.eval(),结果同一个句子对每次算出来的相似度都不太一样,其实就是 Dropout 在搞鬼。这一点在 BERT 上尤其隐蔽,因为from_pretrained加载出来的模型默认处于训练模式,和 PyTorch 里新建一个网络默认状态一样。除非你显示调用model.eval(),否则 Dropout 层会在前向传播时随机丢弃一部分神经元,输出向量每次都有细微差别。

到这一步,模型已经可以向前计算了。接下来是判断一次前向传播的输出长什么样,方便理解后续代码:

sent = "今天天气怎么样" inputs = tokenizer(sent, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) # outputs 是一个 ModelOutput 对象 last_hidden = outputs.last_hidden_state print("inputs 的形状:", inputs["input_ids"].shape) print("last_hidden_state 形状:", last_hidden.shape)

last_hidden_state的形状是[batch_size, sequence_length, hidden_size],对上面的单条短句来说就是[1, 11, 768]sequence_length是 token 数量(包含[CLS][SEP]),768是每个 token 的向量维度。inputs是一个 dict,里面通常包含input_idstoken_type_idsattention_mask,这三种输入的含义分别是:token 在词表中的编号、句子标识、哪些位置是真实文本哪些是 padding。这三个字段直接作为model(**inputs)的关键字参数传入,就能得到输出。

3. 用 transformers 把句子变成向量:CLS 池化与 mean pooling 的代码实现

3.1 从词向量到句向量:CLS 池化与 mean pooling 的实现

模型输出的是每个 token 的向量,但相似度计算需要的是一个句子一个向量,因此要做“池化”。最常见的两种做法是取[CLS]位置的向量,以及对所有 token 向量做平均。

import torch def get_cls_pooling(model_output): """取 [CLS] 位置的向量,BERT 输出第 0 个 token 对应 [CLS]""" return model_output.last_hidden_state[:, 0, :] def get_mean_pooling(model_output, attention_mask): """ 对所有 token 向量做加权平均,权重用 attention_mask 这样 padding 位置不会把均值拉低 """ last_hidden = model_output.last_hidden_state # [batch, seq_len, hidden] mask = attention_mask.unsqueeze(-1).float() # [batch, seq_len, 1] masked_hidden = last_hidden * mask # padding 位置被乘 0 summed = masked_hidden.sum(dim=1) # 按序列方向求和 counts = mask.sum(dim=1) # 每个句子真实 token 数 return summed / counts # 求平均

这里要理解attention_mask的作用:句子被 padding 后,后面补的[PAD]token 本身也有向量,但它们不表示任何语义。如果直接对所有 token 平均,那些无意义的[PAD]向量会把句向量拉偏。所以用 mask 把 padding 位置乘 0,再把真实位置加起来除以真实 token 数,得到一个不含 padding 干扰的均值向量。

[CLS]池化呢?BERT 预训练时[CLS]位置的输出被当作整个序列的聚合表示用于分类任务,所以它天然带有句子级的语义。不过在相似度场景里,[CLS]的表现并不总是比 mean pooling 好,因为预训练任务对它优化的方向和句向量相似度不完全一致。我一般会把两种都跑一遍,拿一个小验证集看一下效果再定。

3.2 完整的句子相似度最小可运行代码

把前两节的内容拼起来,就是一个完整的句子相似度计算函数:

import torch from transformers import AutoTokenizer, AutoModel device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModel.from_pretrained(model_name).to(device).eval() def encode_sentence(sentence, max_length=128, pool="mean"): """ 把一条句子编码成 768 维向量 pool: 'mean' 用均值池化,'cls' 用 [CLS] 向量 """ inputs = tokenizer( sentence, max_length=max_length, padding="max_length", truncation=True, return_tensors="pt", ) inputs = {k: v.to(device) for k, v in inputs.items()} with torch.no_grad(): outputs = model(**inputs) if pool == "cls": return outputs.last_hidden_state[:, 0, :].squeeze().cpu() else: mask = inputs["attention_mask"].unsqueeze(-1).float() last_hidden = outputs.last_hidden_state masked = last_hidden * mask vec = masked.sum(dim=1) / mask.sum(dim=1) return vec.squeeze().cpu() # 计算两条句子的相似度 sent_a = "今天天气怎么样" sent_b = "明天会下雨吗" vec_a = encode_sentence(sent_a) vec_b = encode_sentence(sent_b) cos_sim = torch.nn.functional.cosine_similarity(vec_a, vec_b, dim=0) print("句子A:", sent_a) print("句子B:", sent_b) print("余弦相似度:", cos_sim.item())

这段代码的流程是:tokenizer 把句子变成模型输入 dict,模型输出最后一层 hidden state,池化函数得到句向量,最后用cosine_similarity计算相似度。注意我把整个编码过程包在with torch.no_grad()里,这是推理代码的基本素养:不计算梯度,内存占用小很多,速度也快。

encode_sentence返回的是.cpu()的张量,原因是在 GPU 上算完的向量,如果后续要存盘或者用 numpy 处理,搬到 CPU 更顺手。如果只是临时比较两个向量,留在 GPU 上也能算,但这会让后续批量处理时很难统一管理显存。

3.3 max_length、padding 与 truncation 三个参数必须显式配置

tokenizer 有一堆参数,但实际项目中必须关注的就这么几个,不配置全的话代码能跑,但结果不可控:

inputs = tokenizer( sentence, max_length=128, padding="max_length", truncation=True, return_tensors="pt", )

第一个是max_length。BERT 的位置编码上限是 512,超过就没法处理。实际场景里,句子相似度任务很少有超过 128 个 token 的句子,我的经验是设成 128 能覆盖绝大多数中文使用场景。第二个是truncation=True,对超过max_length的文本从尾部截断。第三个是padding="max_length",把所有样本补到统一长度,这样 batch 训练和推理时张量形状才对齐。

这里有一个很多人踩过的细节:两个长度不同的句子,如果各自用padding="max_length"编码,得到的向量长度是一致的,但 padding 位置会对 mean pooling 有影响;如果不用 mask 加权平均,而是裸算last_hidden.mean(dim=1),那句子越短,[PAD]占比越高,向量被稀释得越狠。我见过有人拿同样两个句子,换一台机器跑结果就不一样,最后发现是 tokenizer 配置不同导致 padding 长度变化,均值池化结果跟着变了。所以在第 2 章的池化代码里,我刻意传了attention_mask进去,就是为了消除这个影响。

4. 相似度分数怎么算才靠谱:余弦、内积、温度与阈值的选型参数

4.1 余弦相似度 vs 内积 vs 欧氏距离:不同业务下的选择逻辑

向量算出来了,接下来选相似度指标。最常用的是余弦相似度,因为它只关心方向,不关心向量长度。这在句子相似度场景下非常合适:两句语义相近的话,即使一句是短句一句是长句,向量长度有明显差异,余弦相似度依然能给出接近的值。

import torch.nn.functional as F def cosine_sim(a, b): """对两个向量做 L2 归一化后再点积,等价于余弦相似度""" a = F.normalize(a, p=2, dim=-1) b = F.normalize(b, p=2, dim=-1) return (a * b).sum(dim=-1)

这段代码的工程意义在于:先归一化再点积,和直接调cosine_similarity结果一致,但归一化后的向量可以预先算好缓存,后面算相似度就是一次点积。如果你要做批量比对,比如一万条文本两两计算相似度,把每个向量先归一化,再走矩阵乘法,比循环里反复调cosine_similarity快一个数量级。

什么时候用内积?如果你希望向量本身的“长度”也有信息量,比如用max_length截断后,长文本和短文本的差别在向量长度上有体现,内积会放大这种差别。但在 BERT 直接输出向量的场景里,向量长度受 padding 影响较大,不适合作为业务判断依据。所以我建议统一用余弦相似度。欧氏距离也能用,但它的值域和余弦正好相反——距离越小越相似,很多团队为了逻辑统一,最后还是回到余弦。

4.2 温度系数与阈值选择:相似度分数怎么变成业务判断

余弦相似度的输出范围是 -1 到 1,但 BERT 直接输出的句向量算出来的余弦值通常集中在 0.7 到 0.95 之间,分布非常窄。这意味着你很难直接拿 0.8 作为阈值去判断“相似还是不相似”,可能两个字面完全不相关的句子算出来也有 0.7 多的相似度。

处理这个问题的常见做法是引入温度系数,把分数拉开:

temperature = 0.05 # 小于 1 的温度能放大分数差异 def scaled_similarity(a, b, temperature=temperature): a = F.normalize(a, p=2, dim=-1) b = F.normalize(b, p=2, dim=-1) return (a * b).sum(dim=-1) / temperature

温度系数的作用是把向量间的微小差异放大。当temperature=1.0时就是原始余弦值;当temperature取 0.05 时,原本 0.85 和 0.86 的两对句子,分数差距能明显拉开。这个技巧最早流行于对比学习,但在相似度阈值筛选时同样好用。

阈值怎么定?我建议不要拍脑袋,跑一批真实数据,把相似度分数画直方图,或者按配对样本看分布重叠区。如果你有少量标注数据,可以做一个简单的网格搜索:

import numpy as np # sims 是 N 个句子对的相似度分数,labels 是 0/1 人工标注 def best_threshold(sims, labels): best_acc = 0 best_t = 0.5 for t in np.arange(0.5, 0.99, 0.005): preds = (sims > t).astype(int) acc = (preds == labels).mean() if acc > best_acc: best_acc = acc best_t = t return best_t, best_acc

这里labels表示句子对是否语义相似,人工标几十对就够初步定阈值。注意这个搜索是在验证集上做的,真正上线前最好再拿一批没参与调参的数据复核一遍,否则容易过拟合到标注集上。

4.3 batch 输入与梯度开关:批量计算时别把显存打满

实际工作中很少只算两三条句子,更多时候是对一个列表做两两比较。批量编码时如果不注意,显存很容易爆掉。下面的代码展示怎么批量跑并且输出一个相似度矩阵:

import torch import torch.nn.functional as F def batch_encode(sentences, batch_size=8): """批量编码句子,返回归一化后的句向量矩阵 [n, 768]""" all_vecs = [] for i in range(0, len(sentences), batch_size): batch = sentences[i: i + batch_size] inputs = tokenizer( batch, max_length=128, padding="max_length", truncation=True, return_tensors="pt", ) inputs = {k: v.to(device) for k, v in inputs.items()} with torch.no_grad(): outputs = model(**inputs) last_hidden = outputs.last_hidden_state mask = inputs["attention_mask"].unsqueeze(-1).float() vec = (last_hidden * mask).sum(dim=1) / mask.sum(dim=1) all_vecs.append(F.normalize(vec, p=2, dim=-1).cpu()) return torch.cat(all_vecs, dim=0) sentences = [ "今天天气怎么样", "明天会下雨吗", "现在几点钟了", "北京的天气如何", ] vecs = batch_encode(sentences, batch_size=2) sim_matrix = vecs @ vecs.T # 归一化后点积即余弦相似度矩阵 print(sim_matrix.numpy().round(3))

我解释一下这个代码的逻辑:batch_encodebatch_size切片处理,每批过一遍模型,出来的向量做 L2 归一化后拼到一起。最后一步vecs @ vecs.T是矩阵乘法,等价于把所有句子两两之间的余弦相似度一次性算完。vecs的形状是[n, 768],乘上它的转置[768, n],得到[n, n]的相似度矩阵,[i, j]位置就是第 i 句和第 j 句的相似度。

注意batch_size对显存的影响:BERT base 模型在 128 token 长度下,单个样本大约占 1GB 显存(包括中间激活),8 是一个相对安全的初始值。如果你的 GPU 显存较小,比如 4GB,就把batch_size降到 4 或 2。如果显存足够了但还是想要更快一点,可以把max_length从 128 降到 64,很多短文本场景在这个长度下精度损失很小,但速度能提升一截。

这也是为什么我不建议在推理时叠加 flash-attention 之类的加速库:对句子相似度这种短文本、小 batch 的场景,显存和速度瓶颈主要在数据搬移和前向传播本身,优化注意力实现带来的收益有限,反而引入额外的安装兼容性问题。这个方向真正值得投入的是第 6 章要讲的向量缓存和批量召回。

5. BERT 加载与相似度计算的 5 个常见坑:从安装 torch 失败到结果全为 1

5.1 安装 torch 报 “could not find a version that satisfies the requirement torch”

这是新环境里最常见的报错,完整报错是ERROR: Could not find a version that satisfies the requirement torch,后面通常还跟着一行说找不到匹配版本。现象是 pip install torch 直接失败,怀疑是不是版本名写错了。

原因一般有两个:一是 pip 默认源里没有当前平台和 Python 版本的 torch wheel。比如 Python 3.12 刚发布时,PyTorch 官方只支持到 3.11,你拿 3.12 装旧版 torch 就会找不到匹配版本。二是网络原因,默认源连接失败导致拉取列表不完整。

解决思路是先确认自己的 Python 版本,然后用官方源安装或在命令里指定平台。最常见做法是去 PyTorch 官网复制一条带 CUDA 参数的安装命令,它生成的链接指向官方准确版本。如果你在公司内网,配置镜像源的优先级是:先换 pip 源,再考虑版本问题。

注意:安装失败时不要反复重试同一命令,先跑python --versionpip --version确认环境,再决定是换源还是换版本。

5.2 模型下载卡在进度条或 SSL 报错,transformers 一直加载不了

现象是首次执行from_pretrained时,看到下载进度条一直在 0% 或者几 KB 就停住,最后抛一个 SSL 证书错误。这是因为bert-base-chinese的权重文件总大小大约 400MB,下载连接不稳定就会中断。

我一般在公司服务器上第一次加载模型时,都会先设置一个临时的镜像环境变量,把模型文件的下载指向国内镜像地址。如果镜像也不好使,就先把权重包手动下载好,放到本地路径,然后用本地路径加载:

# 手动下载并解压后,模型目录里包含 pytorch_model.bin / config.json / vocab.txt model = AutoModel.from_pretrained("/data/models/bert-base-chinese") tokenizer = AutoTokenizer.from_pretrained("/data/models/bert-base-chinese")

这是最不依赖网络稳定性的一种做法。权重文件下载一次之后,整个目录可以打成压缩包分发到其他机器,后续全部走本地路径,既不需要缓存目录,也避免了每台机器重复下载。from_pretrained对本地路径和模型名的处理是一样的,你传给它一个目录路径,它就直接从目录读文件,不会再触发下载逻辑。

另外注意一点:手动下载时文件名必须是pytorch_model.bin,不要下载成 TensorFlow 格式的bert_model.ckpt,否则加载时会报缺少键名或者 key 不匹配。拿不准的话,直接看目录里有没有config.json,这是 transformers 识别一个模型目录是否合法的硬指标。

5.3 相似度结果全部接近 1,或者同一对句子每次结果都不一样

这两个现象放在一起说,因为它们经常同时出现。第一批跑出来的结果,任意两个句子的相似度都在 0.98 以上,看起来“什么都是相似的”;如果重新跑一遍,数值又有变化。

第一个现象的原因是忘了做attention_mask加权平均。前面代码里我提到了这一点,如果你简化成last_hidden.mean(dim=1),并且句子长度差异大,padding 占大部分位置,均值池化结果就会被[PAD]向量主导,所有句子都趋向同一个方向,相似度自然接近 1。第二个现象的原因是模型还在训练模式,Dropout 没有关闭,每次前向传播输出的向量有随机扰动。

解决办法也很直接:池化时带上attention_mask,推理前显式调用model.eval(),再把前向传播包进torch.no_grad()。这三个动作缺一个,结果都有隐患。我建议把它们写进一个初始化函数,不要每次写代码时临场决定。

5.4 中文分词结果和预期不符:未登录词与繁体字

现象是“今天天气怎么样”分词变成一堆[UNK],或者繁体句子和简体句子算出来的相似度异常低。bert-base-chinese用的是字级别的分词,理论上不会出现[UNK],但如果文本里有罕见 Unicode 字符、emoji 或者生僻字,词表覆盖不到时也会映射到[UNK]

另一个容易忽略的是繁简体。bert-base-chinese的训练数据以简体为主,对繁体支持一般。如果你做的是台湾用户或香港用户的文本相似度,直接用这个模型可能效果偏差。常见做法有两种:一是代码层先繁转简再进模型,用opencc转一下;二是换用对繁体覆盖更好的中文预训练模型,比如一些社区开源的全词掩码版本。

遇到相似度明显不合理的情况,先把两句的 tokenizer 结果打出来看,确认没有异常分词再怀疑模型本身:

tokens = tokenizer.tokenize("今天天气怎么样") print(tokens) # ['今', '天', '天', '气', '怎', '么', '样']

这一步能帮你快速定位是词表问题还是模型计算问题。

5.5 GPU 显存溢出:明明只传了两句话

现象是计算批量相似度时,代码跑到一半报CUDA out of memory,但你以为自己只输出了两条短句。原因通常有两个:一是在batch_encodebatch_size设置太大,BERT 的中间激活值占了大量显存;二是代码里没有释放历史张量,循环里累积了太多旧的计算图。如果你忘了with torch.no_grad(),每个 batch 都会保留一份计算图,显存占用会随着循环线性增长,直到溢出。

解决方式是三层递进:先确保推理代码用no_grad;然后调小batch_size,短文本场景从 8 降到 4 基本能绕开多数显存不足;最后如果你的句子真的非常长,把max_length从 128 改成 64。这三个参数调整对相似度结果的影响很小,但对显存占用是决定性的。建议在任何团队内部测试环境里,先跑一次用 20 条真实句子的批量编码,记录显存占用峰值,再推断全量数据的 batch 大小。

6. 从两句话到批量文本:向量缓存、语义检索与效果验证的进阶玩法

当你的数据量从几条变成几万条,逐次调用encode_sentence就不现实了。这里的核心转变是把算力花一次、结果存下来反复用。句子向量是一个 768 维的浮点数列表,一条句子大约 3KB,一万条也就 30MB,完全可以直接落盘。

import torch # 全量句子编码 all_sentences = [...] # 你的语料 vecs = batch_encode(all_sentences, batch_size=8) # 落盘保存,后续直接加载,不再重新过模型 torch.save(vecs, "sentence_vecs.pt") with open("sentences.txt", "w", encoding="utf-8") as f: f.write("\n".join(all_sentences))

这里我把句向量和原始文本分开存:向量文件用来算相似度,文本文件用来回查内容。后续跑服务时,加载一次向量文件,新来的查询句子只需要算一次向量,和库里所有句子做一次矩阵乘法,就能拿到相似度排序。这就是一个最简的语义检索系统:不依赖任何外部向量数据库,单机就能扛住十万级别的数据量。

如果你想在几万条向量里找“和某条最相似的前 10 条”,完全手写矩阵乘也能跑,但每次查询都是 O(n) 复杂度。当数据量到百万级别,就需要引入向量检索工具了。最常见的做法是直接把归一化后的向量写入一个索引文件,用 FAISS 建索引,它内置了 IVF、HNSW 等近似最近邻算法,检索耗时能降两个数量级。我这里提供思路,不贴完整代码,因为接入 FAISS 后你的向量存储和更新策略会跟着变,它适合数据量确实大到算不过来的阶段。

最后一个必做的步骤是效果验证。很多人跑通了两条句子的相似度,就认为“BERT 直接加载做相似度可行”,但真实业务里的相似和不相似边界比想象中模糊。我一般会取 200 对样本,人工标 0/1,然后算模型分数和人工标签的 Spearman 相关系数,用这个相关系数衡量当前池化方式和阈值好不好用:

from scipy.stats import spearmanr # 人工标注:1 表示语义相似,0 表示不相似 human_labels = [1, 0, 1, 0, 1] model_scores = [0.91, 0.62, 0.88, 0.58, 0.93] corr, p_value = spearmanr(human_labels, model_scores) print(f"Spearman 相关系数: {corr:.3f}")

如果相关系数低于 0.6,说明当前配置下模型对相似和不相识的区分能力不够,优先尝试调池化方式(mean 换 cls)、换更合适的中文预训练模型。相关系数只是衡量排序一致性,最终业务判断还是要回到第 4 章说的阈值选择。我个人的习惯是相关系数过 0.8 之后,就不再纠结模型选型,专心跳阈值和业务指标。

这个方向我从最早的 TF-IDF 一路做到 BERT 向量化,最大的一个教训是:别过早引入向量数据库、微调、蒸馏这些重武器。先用bert-base-chinese把基线跑通,向量存好,拿真实的业务数据验证一轮效果,再决定下一步投入。你在这一步踩过的那些从安装到阈值调整的坑,后面换任何模型都要重踩一遍。希望这篇笔记能帮你绕开其中大部分,直接到业务验证的阶段。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询