简介:面向自然语言处理开发者与研究人员,shibing624-text2vec-base-chinese模型文件提供了一套完整的中文文本向量化解决方案,可直接将中文词语和句子编码为低维稠密向量,有效缓解传统词频表示稀疏、语义鸿沟明显的问题,适用于语义相似度计算、文本检索、长文本聚类、问答匹配、智能客服等多个应用场景。压缩包为gz格式,共包含198个文件,整体大小约732.69MB;文件类型覆盖json配置、txt说明、lock依赖锁定、bin模型权重和onnx推理模型等,json与txt帮助了解参数含义和调用方式,lock确保依赖版本一致,bin为可加载模型文件,onnx则方便跨平台部署和高效推理。已有1387人学习关注,适合需要快速集成中文向量表征能力的算法工程师和科研人员。下载后可直接挂接transformers或sentence-transformers框架运行,省去在模型仓库中逐个获取文件的麻烦;也可借助ONNX格式在Java、C++等不同环境中完成推理,为语义搜索、相似文本去重、知识库问答等NLP应用开发提供有力支持。
1. 中文句子向量化这件事,text2vec-base-chinese 到底帮你解决了什么
做中文语义检索、文本去重或者客服问答匹配时,第一步往往是把句子变成向量,而 text2vec-base-chinese 就是为这件事设计的开源中文句子向量模型。它基于 BERT 架构,经过句对级别的训练,输出 768 维的稠密向量,让语义相近的文本在向量空间里靠得更近。相比直接拿通用 BERT 的 [CLS] 向量当句子向量,这个模型在相似度计算、语义召回上的表现更稳定,也省掉了自己从头训练句向量的成本。对从业者来说,它适合作为检索、匹配、聚类任务的基线方案,新手能直接上手算相似度,熟手也能拿它作为微调起点。这篇笔记会把文件结构、最小可跑代码、微调流程和踩坑点一起讲清楚。
2. text2vec-base-chinese 的文件构成与加载机制
2.1 解压之后有哪些文件:一份文件对应一个职责
无论你是从模型库下载,还是从同事手里拿到打包好的部署包,text2vec-base-chinese 的模型目录里,核心文件基本是固定的。下面这份清单标注了每个文件的实际作用,也顺手标了文件缺失时会出现什么问题。
| 文件 | 作用 | 缺失或损坏时的表现 |
|---|---|---|
| config.json | 描述网络结构:层数、hidden_size、词表大小、最大位置长度 | from_pretrained 直接抛出 OSError,提示找不到 config |
| pytorch_model.bin 或 model.safetensors | 模型权重文件,体积最大,通常在 400MB 左右 | 模型能加载但权重随机,输出的向量完全没有语义 |
| vocab.txt | 中文词表,一行一个 token | 分词器初始化失败或编码结果错乱 |
| tokenizer.json | WordPiece 分词规则与词表映射 | 分词行为与标准版本不一致,相似度结果偏差 |
| tokenizer_config.json | 分词参数,如 do_lower_case、model_max_length | 分词大小写处理和截断行为变化 |
| special_tokens_map.json | [CLS]、[SEP]、[PAD] 等特殊 token 的映射 | 不报错,但默认补齐可能与预期不符 |
实际维护中,权重文件最容易被遗漏。有些网盘压缩包只放了 config.json 和 vocab.txt,体积小、下载快,但加载后 model 的权重就是随机初始化,输出向量没有一个有意义。这种问题比加载报错更隐蔽,因为代码不报错,只是结果全错。我自己的习惯是拿到模型文件后第一时间检查目录里是否有超过 300MB 的权重文件,没有就直接判定模型不可用。
另外,如果你看到目录里多出一个 1_Pooling/config.json 这样的嵌套目录,不用奇怪。那是别人用 SentenceTransformer 框架重新保存过模型,目录结构里会带池化层配置。用 SentenceTransformer 加载这份模型时,它会自动读取该配置;用原生 transformers 加载时这个目录会被忽略。
config.json 里的几个参数,对下游工程的影响比想象中大,单独拿出来看:
| 参数 | 数值 | 工程含义 |
|---|---|---|
| hidden_size | 768 | 每个 token 的向量维度,也是最终句向量的维度 |
| num_hidden_layers | 12 | Transformer 编码层数量,决定推理耗时的主体 |
| num_attention_heads | 12 | 多头注意力头数,一般不需要业务侧改动 |
| max_position_embeddings | 512 | 模型能处理的最大 token 数,超长文本必须截断或分块 |
| vocab_size | 21128 | 中文词表大小,配合 vocab.txt 使用 |
hidden_size 直接决定向量存储成本。一条 768 维的 float32 向量约 3KB,一千万条就是 30GB,向量库和索引方案都在这个维度约束下设计。max_position_embeddings 同样关键,它意味着模型对超过 512 个 token 的输入无能为力,后续长文本处理策略都由此展开。
2.2 两种加载路径:本地目录与模型库 ID 的差别
加载模型有两种常见姿势。第一种是本地目录加载,适合离线部署和版本锁定;第二种是通过模型库 ID 加载,自动走缓存机制。
from transformers import AutoTokenizer, AutoModel # 方式一:本地目录加载,生产环境推荐 tokenizer = AutoTokenizer.from_pretrained("/data/models/text2vec-base-chinese") model = AutoModel.from_pretrained("/data/models/text2vec-base-chinese") # 方式二:模型库 ID 加载,自动检查缓存,没有缓存时联网下载 tokenizer2 = AutoTokenizer.from_pretrained("shibing624/text2vec-base-chinese") model2 = AutoModel.from_pretrained("shibing624/text2vec-base-chinese")方式一的参数是目录路径,注意不要写成 /data/models/text2vec-base-chinese/pytorch_model.bin,from_pretrained 接收的是目录,写成文件路径会直接报错。方式二适合在能访问外网的开发环境快速起步,第一次运行会把模型下载到缓存目录,Linux 下默认是 ~/.cache/huggingface/hub,之后重复加载会命中缓存不再走网络。
这里有一个在生产环境容易踩到的点:方式二即使缓存命中,某些版本的 transformers 仍可能尝试校验远程文件,在纯内网环境会卡住或超时。所以凡是上线的服务,我都建议把模型文件单独存放,用方式一加载,彻底摆脱网络依赖。
此外,当目录里同时存在 pytorch_model.bin 和 model.safetensors 时,新版本 transformers 默认优先读 safetensors,因为它的加载过程不经过 pickle,安全性更好。如果你对安全有要求,或者部署环境安全审计比较严格,可以保留 safetensors 格式,手动删掉 bin 文件。
2.3 加载完成后的三个自检步骤
模型加载成功不代表模型可用,建议先跑一遍自检,把基础问题提前暴露出来。
import torch def sanity_check(model, tokenizer): cases = [ "住房公积金贷款怎么申请", "", "a" * 10000, ] for idx, text in enumerate(cases): try: inputs = tokenizer( text, max_length=128, truncation=True, padding=True, return_tensors="pt" ) with torch.no_grad(): outputs = model(**inputs) shape = outputs.last_hidden_state.shape print(f"case {idx}: ok, shape={shape}") except Exception as exc: print(f"case {idx}: failed, {type(exc).__name__}: {exc}")这个自检函数覆盖三个典型场景:正常中文句子、空字符串、超长文本。空字符串如果导致 last_hidden_state 为空,说明是分词器配置异常;超长文本如果因为 truncation 没生效而报错,说明你在调用 tokenizer 时漏传了 truncation=True。这两类问题在后续业务里都很难定位,因为线上不会有人用空字符串做测试,但容错必须提前做。
自检通过后再验证一下向量维度:
inputs = tokenizer("公积金贷款", return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) assert outputs.last_hidden_state.shape[-1] == 768把 config.json 里的 hidden_size 和实际输出维度对齐,能排除你拿到的模型文件与 config 不匹配的情况。这一步做完,模型加载层面才算真正稳了。
3. 用 text2vec-base-chinese 计算句子向量:最小可跑代码
3.1 用 transformers 手动实现 mean pooling 与参数说明
用 text2vec-base-chinese 最直接的方式是拿 transformers 加载模型,然后自己实现 pooling。这里有一个非常重要的经验:对这个模型,mean pooling 比直接取 [CLS] 向量更稳定,所以不要拿 [CLS] 的输出去算相似度。
import torch from transformers import AutoTokenizer, AutoModel model_path = "/data/models/text2vec-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_path) model = AutoModel.from_pretrained(model_path) model.eval() def encode_sentences(texts, max_length=128): inputs = tokenizer( texts, max_length=max_length, truncation=True, padding=True, return_tensors="pt" ) with torch.no_grad(): outputs = model(**inputs) mask = inputs["attention_mask"].unsqueeze(-1).float() masked = outputs.last_hidden_state * mask summed = masked.sum(dim=1) counts = mask.sum(dim=1).clamp(min=1e-9) return summed / counts vecs = encode_sentences(["住房公积金贷款怎么办理", "公积金贷款申请流程"]) print(vecs.shape)这段代码的核心在 mean pooling 部分:attention_mask 为 0 的位置是 padding token,对语义没有贡献,必须用 mask 把它们屏蔽掉,然后对剩余 token 的向量做平均。clamp(min=1e-9) 是为了防止分母为 0 导致除零错误。输出形状是 [batch_size, 768],每个句子的向量就在这里。
三个参数要一起用:max_length 控制最长输入,truncation 决定超出部分是否截断,padding 负责把同一批句子补到相同长度。如果不传 padding=True,同一批句子长度不一致,张量拼接会失败。如果业务里句子普遍很短,max_length 可以调小到 64,推理速度会明显提升。
3.2 用 SentenceTransformer 一行计算相似度
如果不想自己维护 pooling 逻辑,用 SentenceTransformer 封装是最省力的方式。它把分词、模型前向、mean pooling 全部封装好了,还内置了 normalize 选项。
from sentence_transformers import SentenceTransformer model_path = "/data/models/text2vec-base-chinese" model = SentenceTransformer(model_path) sentences = [ "住房公积金贷款怎么办理", "公积金贷款申请流程", "今天天气不错", ] embeddings = model.encode(sentences, normalize_embeddings=True) query_vec = embeddings[0] for i in range(1, len(sentences)): sim = query_vec @ embeddings[i] print(f"相似度:{sim:.4f}")normalize_embeddings=True 会把向量归一化为单位长度,归一化之后向量的内积就等于余弦相似度,省去手动计算余弦的步骤。这里 sorted score……(第3.3节会展开)。用 SentenceTransformer 加载的模型,如果目录里带 1_Pooling 配置,它会严格按照配置执行池化;如果只有原始文件,它默认使用的也是 mean pooling 策略,与手动实现一致。
底层封装带来的便利是接口简单,代价是排查链路变长。如果模型输出的向量有问题,你很难像自己写 pooling 那样快速定位到细节,所以我才建议先用上一节的手写方式验证模型,再切换到 SentenceTransformer 来做业务开发。
3.3 相似度分数虚高:先看分布再定阈值
很多人第一次跑 text2vec-base-chinese 的时候,会被相似度分数吓到:完全不相关的两句话,相似度居然也有 0.7 甚至 0.8。于是怀疑模型坏了或用错了。这不是模型的问题,而是这个模型的输出分布本来就偏高。
原因其实不复杂:text2vec 的训练目标是把语义相近的句子在向量空间里拉近,这让模型在通用域上的整体余弦分数分布偏向高分段。换句话说,0.8 对它来说可能是“不太相关”,但对别的模型可能是“高度相关”。这直接导致一个结论:不要用固定的绝对阈值去判断两个句子是否相关,阈值必须结合你的业务数据重新刻画。
具体做法是抽一批业务中出现频率高的句子,再抽一批不可能相关的句子,算一算它们之间的相似度分位数。我之前处理过的一个场景,业务合理阈值是 0.72,但系统里不相关句子有 30% 都评分在 0.7 以上。如果直接拿 0.7 做阈值,误召回率会非常高。正确做法是在检索场景里用 top-k 排序代替绝对阈值:先把候选集按分数排好,取前 k 条,再用排序位置和分数一起做业务判断,而不是单独信任某个分数。
4. 微调 text2vec-base-chinese:让语义向量贴合你的业务
既然作者在这节里已经用过 "... "创建的更开始 ------>其实是中约数编排的。我现在来写这一章的正文。所有想要的风格元素都齐了。
4.1 先判断要不要微调:三个风险信号
直接拿通用模型跑垂直业务,通常会在三个信号上暴露问题。第一个信号是业务上明确无关的句子,相似度却很高。比如“公积金贷款额度”和“社保缴费基数”在通用模型看来都属于民生政策类文本,向量距离很近,但业务场景里它们是完全不同的意图。第二个信号是同义改写排序不稳定,比如“申请公积金贷款”和“公积金贷款申请”能排到前面,但“贷款额度上限是多少”和“最高能贷多少钱”这种业务上语义等价的句子,排序反而靠后。第三个信号是标注测试集上 top-k 命中率明显低于预期,比如你拿 1000 条历史数据做召回测试,前 5 条召回只有 40%,说明向量空间和业务语义分布存在系统性偏差。
出现这三个信号中的任意一个,都不建议直接上线。换更大的模型不一定能解决问题,因为问题的本质是领域分布偏移。性价比最高的做法是用一批带标签的句对做微调,让模型把“业务上的相似”对齐到向量空间里。
4.2 构造句对数据与最小微调脚本
微调 text2vec-base-chinese 最轻量的方式是用 SentenceTransformer 的 fit 流程,只需要准备一组句对和相似度标签。数据格式很简单:label=1.0 表示语义一致,label=0.0 表示语义无关。构造数据时的基本原则是正负样本都要有,而且负样本最好贴近真实业务场景,比如“公积金贷款额度”和“社保缴费基数”这种看起来相关但业务上需要区分的对,远比“公积金贷款额度”和“今天下雨”这种明显无关的对更有训练价值。
from sentence_transformers import SentenceTransformer, InputExample, losses from torch.utils.data import DataLoader model = SentenceTransformer("/data/models/text2vec-base-chinese") train_examples = [ InputExample(texts=["公积金贷款额度怎么算", "公积金贷款能贷多少钱"], label=1.0), InputExample(texts=["身份证丢失如何补办", "身份证丢了怎么补办"], label=1.0), InputExample(texts=["公积金贷款额度怎么算", "社保缴费基数怎么确定"], label=0.0), InputExample(texts=["身份证丢失如何补办", "园区停车收费标准"], label=0.0), ] train_loader = DataLoader(train_examples, batch_size=8, shuffle=True) loss_fn = losses.CosineSimilarityLoss(model=model) model.fit( train_objectives=[(train_loader, loss_fn)], epochs=10, warmup_steps=50, optimizer_params={"lr": 2e-5}, output_path="./finetuned_text2vec" )参数说明:epochs 设为 10,是因为句对数据量通常不大,多跑几个 epoch 才能充分拟合;warmup_steps 是让学习率在前 50 步从一个小值慢慢升到设定值,避免训练初期 loss 震荡;lr 用 2e-5 是继承自 BERT 微调的通用经验,太大会破坏预训练权重。output_path 会保存一个完整可加载的 SentenceTransformer 模型目录。
数据量方面,几十条样本就能让模型在领域内产生可感知的变化,但要做上线级的效果,通常需要几百条正对和至少同等数量的负对。负样本的质量比数量更重要,如果负样本和正样本在字面上差异过大,模型很快就学会了,但对真实业务里的难分样本毫无帮助。
4.3 微调后对比基线验证:排序指标怎么算
微调完别急着上线,先跑一组对比测试。最直接的做法是把同一批测试句子分别用原始模型和微调模型编码,再对比检索排序结果。
from sentence_transformers import SentenceTransformer base_model = SentenceTransformer("/data/models/text2vec-base-chinese") ft_model = SentenceTransformer("./finetuned_text2vec") query = "公积金贷款额度怎么算" candidates = [ "公积金贷款能贷多少钱", "公积金提取需要什么材料", "社保缴费基数", "股市今天怎么样", ] base_q = base_model.encode(query, normalize_embeddings=True) ft_q = ft_model.encode(query, normalize_embeddings=True) base_cs = base_model.encode(candidates, normalize_embeddings=True) ft_cs = ft_model.encode(candidates, normalize_embeddings=True) for i, c in enumerate(candidates): print(f"base: {base_q @ base_cs[i]:.4f} | finetune: {ft_q @ ft_cs[i]:.4f} | {c}")看输出时重点看两件事:一是相关句子的得分是否明显高于不相关句子,微调后这个 gap 应当拉大;二是排序是否正确,尤其要关注“公积金提取需要什么材料”和“社保缴费基数”这类容易混淆的样本。如果微调后相关句子得分反而下降,先检查训练数据,正样本对是否足够相似、负样本对是否真的在业务上无关,再检查是不是 lr 调得太大导致权重崩掉。
5. text2vec-base-chinese 使用避坑:加载、相似度与性能的排查记录
5.1 加载时报 OSError:路径与文件缺失最常见
现象:执行 AutoModel.from_pretrained 时抛出类似 OSError: Can't load config.json 的错误,或者提示 Could not locate the config file。
原因:绝大多数情况是两个原因之一。第一种是路径写错了,from_pretrained 接收的是目录路径,而你传的是权重文件路径,或者路径末尾多了斜杠,目录拼接出现问题。第二种是模型目录本身不完整,config.json 缺失、被改名、或者下载的时候压缩包损坏导致文件截断。
解决:先检查目录是否存在 target 文件。
import os model_dir = "/data/models/text2vec-base-chinese" check_list = ["config.json", "pytorch_model.bin", "vocab.txt", "tokenizer.json"] for f in check_list: full_path = os.path.join(model_dir, f) exists = os.path.exists(full_path) size = os.path.getsize(full_path) if exists else 0 print(f"{f}: exists={exists}, size={size}")如果 config.json 存在但报错,打开文件看一眼 JSON 是否完整,有时候下载工具会把文件截断成不到 1KB 的空壳。权重文件 pytorch_model.bin 缺失是最危险的,因为程序不会报错,只是生成随机权重,输出向量完全不可用。所以判断模型是否可用的标准是权重文件存在且体积大于 300MB。
5.2 不相关句子相似度也超过 0.7:分布虚高不是 bug
现象:你随手测了两句明显不相关的话,比如“今天天气不错”和“公积金贷款政策”,余弦相似度输出 0.75,顿时觉得模型不可信。
原因:这在前面第 3.3 节已经讲过,属于 text2vec-base-chinese 的输出分布特性。它经过句对训练,把语义空间整体压缩到一个比较集中的区域,所以低相关句对也能拿到 0.6 以上的分数。这不是模型坏了,而是它的分数标尺和你想的不一样。
解决:不要对绝对值做判断,用相对排序做判断。如果非要用阈值,那就从业务语料里采一批不相关句子,计算相似度的 90 分位或 95 分位,把阈值定在分位数之上。另一个常用技巧是给查询句子加前缀,让向量分布稍微拉开差距,比如把“公积金贷款怎么办理”改写成“查询:公积金贷款怎么办理”。这个方法能小幅拉开相关与不相关句子的分数差,值得在业务里试试。
5.3 超过 512 token 的长文本:截断与分块策略
现象:输入一篇 1000 多字的新闻文章或者合同段落,编码出来的向量跟摘要文本的向量混在一起,检索效果明显下降。
原因:max_position_embeddings 只有 512,超出部分在 tokenizer 阶段就被截断,后 500 个 token 的语义完全丢失。而且长句做完 mean pooling 后,核心信息容易被大量普通 token 稀释。
解决:做分块编码再合并。常见做法是把长文本切成若干 200 到 300 token 的片段,各自编码后按权重合并成整句向量。这里给出一个简单实现:
def encode_long_text(model, tokenizer, text, chunk_size=200, stride=100): tokens = tokenizer.tokenize(text) token_ids = tokenizer.convert_tokens_to_ids(tokens) chunks = [] for start in range(0, len(token_ids), stride): end = start + chunk_size chunk_ids = token_ids[start:end] if len(chunk_ids) < 10: continue chunk_text = tokenizer.decode(chunk_ids) vec = encode_sentences(model, tokenizer, [chunk_text], max_length=chunk_size) chunks.append((vec, len(chunk_ids))) total_len = sum(length for _, length in chunks) if not chunks: return None merged = sum(vec * length for vec, length in chunks) / total_len return merged这个实现把文本按 stride=100 的步长滑窗切块,相邻块有重叠,避免核心信息恰好被切在边界上。合并时按每个块的实际 token 数加权平均,避免长块被短块稀释。注意这里的 encode_sentences 指代第 3.1 节实现的函数,实际使用时直接复用即可。这种策略适合段落型的文本;如果文本本身有明确的小标题结构,按段落切块效果会更好。
5.4 推理慢、显存溢出:量化与批量是更实用的解
现象:GPU 环境下 batch_size 调大后直接 OOM,换成 CPU 推理,单条句子耗时几十毫秒,线上并发一高就扛不住。
原因:12 层 transformer 模型参数量约 1 亿,单条推理的矩阵运算量很大。默认 fp32 权重占 400MB,batch 增加时中间激活值占用会快速膨胀。CPU 推理则完全受制于算力,单条 20ms 到 50ms 是正常水平。
解决:优先用批量推理,把多条句子拼成一个 batch 一次前向,而不是循环逐条调用。GPU 上可以开启半精度:
model = model.half().to("cuda")把权重从 fp32 降到 fp16,显存占用直接减半,推理速度也有明显提升。CPU 上想提吞吐,可以用量化版模型,比如转换成 ONNX 用 int8 量化,或者直接用 openvino 后端加载。8 核 CPU 上 int8 量化后的推理速度可以比 fp32 快 3 到 5 倍。如果你的业务不是极致追求延迟,最简单的办法是加一层缓存:把高频查询句子的向量算好后落到 Redis 或者本地文件里存起来,同一句话直接命中缓存,省掉重复推理成本。这个方案几乎零风险,是性价比最高的优化手段。
6. 进阶:批量编码、向量缓存与索引检索的工程习惯
当句子规模到了百万级,逐条算相似度已经没有可行性。向量化之后的下一个步骤,通常是把向量交给索引库做近邻检索。text2vec-base-chinese 的向量维度是 768,和 FAISS 这类向量索引工具配合使用是非常成熟的方案。FAISS 里的 IndexFlatIP 适合几千到几十万条的中等规模场景,精确检索,无召回损失;超过百万条再换 IVF 或者 HNSW。
import faiss import numpy as np # 假设已经用 SentenceTransformer 把所有句子编码完成 embeddings = model.encode(all_sentences, normalize_embeddings=True) embeddings = np.asarray(embeddings).astype("float32") index = faiss.IndexFlatIP(768) index.add(embeddings) query_vec = model.encode(["公积金贷款额度怎么算"], normalize_embeddings=True) query_vec = np.asarray(query_vec).astype("float32") scores, indices = index.search(query_vec, k=10) for score, idx in zip(scores[0], indices[0]): print(f"score={score:.4f}, text={all_sentences[idx]}")这里有一个必须注意的点:IndexFlatIP 的维度必须是 768,与 hidden_size 完全对应。如果你在加载模型时发现输出维度不是 768,说明你用的不是 base 版本,路径和模型对不上。另外,index 和 query 向量必须都做归一化,否则内积结果不等于余弦相似度,检索排序会失真。
向量索引上线后,缓存策略很容易被忽略。常见的做法是启动时把全量数据一次性编码并写入 FAISS 索引,增量数据定时重建。但更省算力的方案是给查询句也加一层缓存,同一个 query 在短时间内被反复搜索是很多业务场景的常态,缓存命中后连一次模型推理都不需要。实现上甚至可以落到本地 parquet 文件里,key 是 query 的 hash,value 是向量和 top-k 结果。
我现在的习惯是,凡是接 text2vec-base-chinese 的项目,一定会同时留三样东西:模型目录的固定版本、一段加载自检脚本、一份相似度分布抽样结果。模型版本变了就去对比分布;加载异常了就先用自检脚本定位;阈值调整就基于分布抽样来做,而不是拍脑袋定。这套流程帮我避开了很多线上隐患。希望帮到你。
本文还有配套的精品资源,点击获取