BERT实战:用chinese-bert-wwm-ext计算中文句子相似度 简介面向自然语言处理初学者及有文本相似度计算需求的开发者本资源基于 PyTorch Transformers 库直接加载 BERT 中文预训练模型chinese-bert-wwm-ext实现两个句子的语义相似度计算可应用于问答匹配、文档检索、情感分析等场景。压缩包为 RAR 格式共 4 个文件包括 1 个可直接运行的 Python 脚本、1 个词表文件、1 个模型配置文件以及 1 个预训练权重文件整体约 365.84MB涵盖运行所需的全部关键文件txt 说明文件还能帮助快速了解使用步骤。目前已有 1641 人浏览学习适合希望快速上手 BERT 文本匹配实践的开发者。通过这份资源读者能获得完整的加载与推理流程掌握 BertTokenizer 分词编码、BertModel 提取句向量及 cosine_similarity 计算相似度的具体用法由于模型权重已随包附带还能直接复用或二次修改显著降低环境配置与模型下载成本也可作为后续模型微调的基础代码。1. 直接用预训练 BERT 算句子相似度别把词向量求和当语义表示做 NLP 相关项目时计算两个句子的相似度是绕不开的基础操作。无论是问答系统里给用户返回最相近的已有问题还是文本去重、相似文档检索甚至舆情分析里判断两条消息是否在说同一件事都需要一个可靠且能落地的相似度算法。如果你只是用 TF-IDF 或 Word2Vec 词向量平均翻车是早晚的事——它们的表示是静态的遇到同义词、指代、语序调整就抓瞎。BERT 这类预训练模型能把整句话编码成带上下文的向量效果明显好一个档次。这篇实战笔记的目标很直接用 torch 和 transformers 库加载中文 BERT 预训练模型chinese-bert-wwm-ext把两个句子变成向量算余弦相似度。资源包里已经备好了可以直接跑的.py脚本和完整的chinese-bert-wwm-ext模型文件pytorch_model.bin、config.json、vocab.txt适合两种人一是刚入门 NLP 想拿现成模型做语义匹配的新手二是已经跑通流程但想看看坑在哪儿、参数怎么调的开发者。这篇会把代码拆开讲清楚顺带把那些不跑一遍绝对踩不出来的坑也一并说出来。2. 环境准备与模型加载torch、transformers 与 chinese-bert-wwm-ext 文件清单2.1 安装 torch 和 transformers 的正确姿势与版本匹配拿到代码包第一步不是打开.py文件而是把环境装对。这个项目的基础依赖就两个torch和transformers。很多人在这一步就卡住了尤其是看到类似ERROR: Could not find a version that satisfies the requirement torch这种报错时第一反应往往是怀疑自己拼写错了。真实原因通常有两个Python 版本太老或太新以及没走对安装源。常见做法是先确认 Python 版本再决定怎么装。以 Python 3.9 为例直接pip install torch transformers如果你的机器有 NVIDIA 显卡且想用 GPU 加速先装 CUDA 版 torch 再装 transformers 是更稳的顺序。CPU 环境跑 BERT 不是不行只是句子对计算时会明显慢一些但作为学习和验证完全够用。代码逻辑说明这一步只解决“能 import torch 和 transformers”的问题。transformers库是 Hugging Face 出的它本身不依赖哪个特定版本的 torch但如果你之后要加载模型、跑 forwardtorch 的版本不能太离谱。我一般习惯用torch1.10低于这个版本某些张量操作行为会有差异。参数说明pip install没有额外参数时安装的是当前 Python 版本下最新的可用版本。如果你是在公司内网环境记得用pip install torch transformers -i https://pypi.tuna.tsinghua.edu.cn/simple换成国内镜像源不然下载速度很感人。2.2 资源包里的模型文件pytorch_model.bin、config.json、vocab.txt 各是什么拿到的解压包里chinese-bert-wwm-ext文件夹下面有三个文件pytorch_model.bin、config.json、vocab.txt。这三个文件缺一不可少一个from_pretrained都会报错。pytorch_model.bin是模型权重几百 MB是核心中的核心。它存的是 BERT 每一层 Transformer 的权重参数加载后模型才有“记忆”。config.json是模型配置包括层数num_hidden_layers默认 12、隐藏层维度hidden_size默认 768、注意力头数num_attention_heads默认 12等。from_pretrained读权重前会先读这个文件来确定模型结构。vocab.txt是词表BERT 中文模型用的是字粒度文本里每个字都会查这个词表转成 ID。chinese-bert-wwm-ext的词表大概两万多个 token标点和特殊符号也在里面。实际加载时代码里写的是from transformers import BertTokenizer, BertModel model_path ./chinese-bert-wwm-ext tokenizer BertTokenizer.from_pretrained(model_path) model BertModel.from_pretrained(model_path)逻辑说明from_pretrained做的事很简单——传入本地路径时它会去这个目录下面找对应的文件。BertTokenizer负责把中文句子拆成字 token 并映射成整数 IDBertModel负责把 token ID 序列变成向量。注意model_path必须是文件夹路径不能直接指向.bin文件很多新手在这里翻车。参数说明chinese-bert-wwm-ext全称是 BERT with Whole Word Masking是哈工大讯飞联合发布的中文预训练模型。和原始 BERT 不同的是它在预训练时做的是整词掩码而不是对单个字掩码这在中文本上效果更好。如果你的任务是英文换成bert-base-uncased就行代码不用改只要换路径。2.3 本地加载与在线加载的差异没有网络也能跑资源包里直接放了模型文件最大的好处是断网也能跑。如果你用BertModel.from_pretrained(bert-base-chinese)这种写法transformers 会先去 Hugging Face 的模型仓库检查本地没有缓存就现场下载。问题在于国内网络访问 Hugging Face 经常超时而且模型文件几百 MB下到一半断掉就只能重来。所以拿到这个资源包直接用本地路径加载就好。还有一个常见问题是缓存目录即使你之前在线下载过模型默认缓存路径在~/.cache/huggingface换了一台机器就没了。本地路径没有这个烦恼只要保证chinese-bert-wwm-ext文件夹和.py脚本的相对路径一致就行。我自己的习惯是模型文件放项目根目录下的model/文件夹代码里用相对路径这样整个项目拷走就能直接跑不依赖网络。3. tokenizer 编码与模型推理从句子对到句向量的关键一步3.1 encode_plus 的输入构造add_special_tokens 和 return_tensors 为什么必须设代码里句子编码用的是tokenizer.encode_plus(sentence1, sentence2, add_special_tokensTrue, return_tensorspt)。这一步是整条链路最容易出错的地方很多刚接触 transformers 的人会直接用tokenizer.tokenize()然后自己拼[CLS]和[SEP]这样做不是不行但很容易踩到索引错位的坑。先说encode_plus干了什么。它会自动完成分词、转 ID、加上特殊 token 并且返回一个包含input_ids、token_type_ids、attention_mask的字典。两个句子传入时它会自动拼成[CLS] 句子1 [SEP] 句子2 [SEP]的形式token_type_ids会用 0 和 1 区分两个句子attention_mask会标记非 padding 位置。这些信息模型推理时全都要用。inputs tokenizer.encode_plus( sentence1, sentence2, add_special_tokensTrue, return_tensorspt, max_length128, paddingmax_length, truncationTrue )代码逻辑说明add_special_tokensTrue会在开头加[CLS]、在句间和末尾加[SEP]这两个位置在取向量时要用到所以是必选。return_tensorspt返回 PyTorch 张量而不是 Python 列表这样可以直接传给模型省去手动转张量的步骤。max_length128限制输入的最大长度超过的部分会被截断防止 GPU 显存爆炸。参数说明truncationTrue表示超长部分直接截掉paddingmax_length表示不足 128 的部分用[PAD]填充。注意 padding 策略直接影响 attention_mask——填充位置在计算时会被忽略如果你不填充attention_mask里全是 1模型依然能算但批量推理时因为长度不齐会报错。这里有个非常隐蔽的坑encode_plus返回的input_ids已经是一个整体张量了也就是[CLS] 句子1 [SEP] 句子2 [SEP]的 ID 序列。但在后面提取向量时不能直接用这个整体拿到两个句子各自的表示得靠token_type_ids来拆分或者干脆不拆——后面细说。3.2 模型的 forward 与 outputs.last_hidden_state 的维度解构模型加载好、输入也准备好了接下来是推理with torch.no_grad(): outputs model(**inputs) last_hidden outputs.last_hidden_state注意这行注释很重要model(**inputs)其实是在做批量推理inputs里有input_ids、token_type_ids、attention_mask三个键**是解包操作把它们作为参数传给模型的forward函数。outputs.last_hidden_state的 shape 是(batch_size, seq_length, hidden_size)。按照上面max_length128的设置当前 batch 大小为 1所以 shape 是(1, 128, 768)。第一维是 batch第二维是序列长度每个 token 一个向量第三维是隐藏层维度 768对应模型配置里hidden_size。那么问题来了怎么从(1, 128, 768)这个整体里分离出sentence1_embedding和sentence2_embedding代码里的写法是sentence1_embedding, sentence2_embedding last_hidden[0]这行代码是网上很多教程里的写法但它有两个潜在问题。第一last_hidden[0]拿到的是序列维度上所有 token 的向量shape 是(128, 768)直接解包成两个变量得到的是第一行和第二行也就是[CLS]向量和第一个 token 的向量——这显然不是句子的语义表示。第二如果代码运行不报错那说明解包成功但拿到的sentence1_embedding其实只是第一个 token 的向量完全没有语义。正确的做法是取[CLS]token 对应的向量[CLS]的特殊设计让它能汇总整个序列的信息sentence1_embedding last_hidden[0, 0, :] # [CLS] 位置 sentence2_embedding last_hidden[0, token_len_s1 1, :] # 句子2 的 [SEP] 前一个位置这个细节直接影响相似度计算结果的可靠性。如果你不管三七二十一把整个序列平均了或者像之前那样取前两行算出来的相似度数值可能看起来正常但用于实际任务时效果会很差。拿我自己的项目经历说第一次用 BERT 做相似句子挖掘时就是复制了网上那种解包写法结果去重准确率惨不忍睹排查了半天才发现问题出在向量取错了位置。4. 相似度计算与阈值判定余弦相似度的实现与欧氏距离对比4.1 余弦相似度在语义表示下为什么比欧氏距离更常用拿到两个句子的向量后计算相似度的常用方法有三种余弦相似度、欧氏距离、曼哈顿距离。实际项目里 95% 的情况用余弦相似度原因在于 BERT 输出的向量表示其绝对值大小和语义关系不大更关键的是方向。两个语义相近的句子其向量在多维空间中的夹角很小余弦相似度直接度量夹角余弦值而欧氏距离受向量模长影响大同样的夹角如果模长不同距离会差很多。代码实现用 PyTorch 自带的函数from torch.nn.functional import cosine_similarity similarity cosine_similarity(sentence1_embedding, sentence2_embedding, dim0).item()代码逻辑说明cosine_similarity需要两个一维向量作为输入dim0表示在第 0 维上计算。返回的是一个标量 Tensor.item()把它转成 Python float方便打印判断。公式是(A · B) / (||A|| * ||B||)值的范围是 [-1, 1]越接近 1 表示越相似。如果你偏要用欧氏距离也不是不行但要注意先对向量做 L2 归一化否则结果受向量长度干扰。经验值是归一化后欧氏距离小于 0.5 可以看作比较相似但这个阈值没有普适性不同的文本域差异很大。余弦相似度的好处是阈值相对直观常见场景下 0.8 以上算很相似0.7 到 0.8 算一般相似——但这个数值还是得跑几批自己的数据再定没有银弹。4.2 判断相似与否的阈值怎么定没有银弹只有调实际使用时不要一上来就定similarity 0.8就判断为相似这个阈值在不同的业务场景里偏差非常大。比如做新闻标题去重标题本身就短词面重复率高0.85 可能还太松做开放域问答的匹配问句表达方式差异大0.75 可能就已经很准了。一个务实的做法是拿一批人工标注过的数据跑一遍画出相似度分布然后根据业务能接受的误判率选阈值。如果你连标注数据都没有可以先按 0.8 跑一波把相似度在 0.75 到 0.85 之间的样本捞出来人工看几眼再决定往高调还是往低调。4.3 批量计算两个句子对的相似度列表写法的内存警告很多时候你要算的不止一对句子而是几百对。这时不要写循环一点点算而是把所有句子对一次性喂给模型batch_inputs tokenizer( [s1 for s1 in sentence1_list], [s2 for s2 in sentence2_list], paddingTrue, truncationTrue, max_length128, return_tensorspt ) with torch.no_grad(): outputs model(**batch_inputs) cls_embeddings outputs.last_hidden_state[:, 0, :] # 取每个序列的 [CLS]代码逻辑说明tokenizer直接接受两个列表自动按 batch 编码。outputs.last_hidden_state[:, 0, :]取的是 batch 维度上每个样本的第 0 个位置也就是每个序列的[CLS]向量shape 变成(batch_size, 768)。之后similarities cosine_similarity(cls_embeddings[:, None, :], cls_embeddings[None, :, :], dim-1)这一步做的是所有句子两两之间的相似度矩阵shape 是(batch_size, batch_size)。内存占用随句子数的平方增长1000 条句子就有 100 万个浮点数大约 8MB还行但如果句子数是 10000矩阵就到 800MB这时候就要考虑分块计算了。5. 避坑指南BERT 文本相似度最常见的问题与排查方法5.1 现象import torch 报错 “Could not find a version that satisfies the requirement torch”很多人拿到.py文件后先装依赖但pip install torch直接报上述错误。原因几乎都是 Python 版本不匹配。比如 Python 3.12 刚发布时torch 的预编译包还没有适配这个版本pip 找不到对应 wheel 就会报这个错。解决方法是换 Python 版本推荐 3.9 或 3.10F 上 torch 和 transformers 的兼容性都最好。如果你不愿意换版本可以到 PyTorch 官网找适合当前 Python 版本的安装命令有时候需要指定--index-url用官方源而不是默认 PyPI。还有更玄学的情况公司内网 pip 源没有同步最新 torch 包换回默认源或者国内镜像源通常能解决。5.2 现象加载本地模型时报 “OSError: Cant load model”模型文件明明就在那个路径下但from_pretrained报错说找不到。排查路径是不是写对了——很多人把路径写成./model/chinese-bert-wwm-ext/pytorch_model.bin但from_pretrained要的是文件夹路径不是权重文件路径。另外一个容易被忽略的坑文件夹里面必须同时有config.json和vocab.txt如果你是手动从网盘下载的可能只下载了pytorch_model.bin其他文件漏了。解决方式很简单把这三个文件放在同一个目录下路径参数传这个目录。注意文件名也不要随便改BertModel.from_pretrained内部是按固定文件名去找的改名就找不到了。5.3 现象模型跑起来了但相似度总是很高趋近于 1或者总是 0.99 以上这个现象出现时先别怀疑模型有问题多半是向量取错了位置。尤其是代码里sentence1_embedding, sentence2_embedding last_hidden[0]这种写法拿到的根本不是句子向量。如果你确实想用最后隐藏层表示句子应该取[CLS]那一个 token 的向量也就是last_hidden[:, 0, :]。如果你想要更鲁棒可以取所有 token 向量的平均池化看看也就是last_hidden.mean(dim1)。我自己的习惯是[CLS]向量优先因为预训练时的下一句预测任务脸让[CLS]专门用来做句子级表示。5.4 现象中文句子分词后被拆成英文单词一样的字节对BERT 的 token 化chinese-bert-wwm-ext 的字表是中文原生的正常情况下不会出现拆成 byte-level 的情况。如果你用的是bert-base-uncased加载中文句子就会看到中文被切成类似##的奇怪 token相似度结果没有参考意义。解决方法是中文任务必须用中文预训练模型资源包里给的chinese-bert-wwm-ext就是对的。还有一种情况你的vocab.txt里没有某个生僻字tokenizer 会把它映射到[UNK]如果生僻字太多语义表示质量下降。这种情况要么换更大的词表比如chinese-roberta-wwm-ext-large要么在预处理时做繁简转换。5.5 现象输入长度不一样导致批量推理报错代码里tokenizer处理单个句子对时没问题但一旦批量处理多个句子对长度不齐会导致张量拼接失败。这就是padding参数存在的意义paddingTrue会自动把短序列补齐到 batch 内最长序列的长度。truncationTrue则防止超长句子无限扩张。两个建议一是一定要设max_length否则超长输入会把显存吃满二是批量推理时别混用超短和超长的句子padding 到最长长度会让短句子也占同样多的计算量白白浪费。6. 把 CLS 向量换成平均池化一个提升语句相似度鲁棒性的实测技巧项目里默认写法是取[CLS]token 的最后一层向量这个做法本身没什么问题但在一些实际场景下[CLS]向量会偏向高频词导致相似度计算结果虚高。比如两个句子说的是完全不同的话题但它们都包含“的”和“是”这类高频词[CLS]向量受这些词的影响算出来的相似度可能会误导你。这个时候第二个选择——平均池化——往往表现更稳。具体做法是拿到last_hidden_state之后对所有 token 的向量在序列维度上求平均但要排除[PAD]token不然 padding 位置的全 0 向量会把平均值拉低造成相似度系统性偏移。正确的平均池化实现def mean_pooling(model_output, attention_mask): token_embeddings model_output.last_hidden_state # (batch, seq_len, hidden) input_mask_expanded attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings torch.sum(token_embeddings * input_mask_expanded, dim1) sum_mask torch.clamp(input_mask_expanded.sum(dim1), min1e-9) return sum_embeddings / sum_mask代码逻辑说明attention_mask里 padding 位置是 0非 padding 位置是 1。unsqueeze(-1).expand()把 mask 从(batch, seq_len)扩展成和token_embeddings一样的(batch, seq_len, hidden)然后做逐元素乘法padding 位置乘 0 后等于被剔除。最后除以有效的 token 数即得平均向量。torch.clamp防止出现除 0 的极端情况。如果你手头有标注好的相似句子数据集可以分别用[CLS]和平均池化各跑一遍看看哪个效果更好。我周围的实践经验是短文本几个词到一句话场景[CLS]稍好长文本一段话场景平均池化更稳。这也解释了为什么 Sentence-BERT 这类专门做句子相似度的模型在末尾加了一个池化层——它不赌某一个 token 能代表整句话。最后一个实操建议无论用哪种池化方式算相似度前养成先把向量 L2 归一化的习惯。归一化对余弦相似度结果没有影响但如果你之后想把向量存进向量数据库做 ANN 检索很多索引库要求向量必须是归一化的。代码就一行sentence1_embedding torch.nn.functional.normalize(sentence1_embedding, p2, dim0) sentence2_embedding torch.nn.functional.normalize(sentence2_embedding, p2, dim0)从那以后我每次做句子相似度的项目都会强制先跑一遍三种池化方式的对比再决定在业务里用哪个。具体到这个资源包先把默认脚本跑通再换平均池化跑一遍你就能明显感受到哪个更适合你的数据和场景。希望这篇笔记能帮你少走几步弯路。当初我自己拿到这个压缩包的时候第一件事就是把.py文件打开从头读了一遍把模型路径改成绝对路径然后跑通再慢慢调细节。建议你也按这个顺序来不要一上来就改模型参数。环境、模型、代码、相似度计算这四个环节任何一个出错都不奇怪但按照上面的步骤走问题基本都能快速定位。不同模型、不同池化策略、不同阈值对结果的影响差距很大多试几组你会有收获的。希望帮到你。本文还有配套的精品资源点击获取