用 sentence-transformers 把"意思相近"算出来:中文语义相似度与近重复检测实战
你有没有遇到过这种场景:用户提交的两千条反馈里,一大半其实在说同一件事,只是换了种说法;或者爬虫抓回来的十万篇文章,标题长得不一样,正文却几乎是一篇。用==或字符串相似度去比,几乎没用——"物流太慢了"和"发货速度让人等得着急"在字符层面毫不相干,意思却一模一样。
这类"意思相近"的问题,靠关键词匹配解决不了,得让模型把文本变成一串数字(向量),再用向量之间的距离衡量语义远近。本文用sentence-transformers走通这条链路,并落地一个能扛住万级文本秒级查重的方案。
为什么字符串匹配不够用
传统做法要么是精确匹配,要么是编辑距离(Levenshtein)这类字符层算法。它们的问题是只看"字长什么样",不看"字是什么意思"。下面三句:
- "这家店的服务态度很差"
- "服务太拉胯了,体验不好"
- "店铺服务质量低下"
字符重合度很低,但语义高度一致。字符算法会把它们判成三个完全不同的东西;而人一眼就知道它们是一类。语义向量的价值,就是把这种"人能秒懂的相近"量化成数字距离。
三行代码上手 sentence-transformers
sentence-transformers把"句向量"这件事封装得很薄,核心就是把句子喂进去、拿回向量。
from sentence_transformers import SentenceTransformer # 中文场景优先选在中文语料上训过的模型 model = SentenceTransformer("uer/sbert-base-chinese-nli") sentences = [ "这家店的服务态度很差", "服务太拉胯了,体验不好", "今天天气不错", ] vectors = model.encode(sentences, normalize_embeddings=True) # 归一化后,余弦相似度等价于点积,越接近 1 越相似 import numpy as np sim_01 = float(np.dot(vectors[0], vectors[1])) sim_02 = float(np.dot(vectors[0], vectors[2])) print(f"差评句之间相似度: {sim_01:.3f}") # 通常 0.7+ print(f"差评与天气句相似度: {sim_02:.3f}") # 通常 0.1 左右跑完你会看到:前两句相似度很高,第三句几乎不相关。这就把"语义相近"变成了可计算的数。注意两点:一是normalize_embeddings=True让向量模长为 1,余弦相似度直接等于点积,省一次除法;二是中文务必选中文模型,用英文通用模型效果会掉一大截。
近重复检测实战:用 FAISS 对万级文本秒级查重
单条比对是O(n^2),文本一多就跑不动。正确做法是把所有向量建进一个近邻索引,查询时一次返回最相近的若干条。Facebook 的faiss就是干这个的。
import faiss import numpy as np from sentence_transformers import SentenceTransformer model = SentenceTransformer("uer/sbert-base-chinese-nli") docs = [ "这家店的服务态度很差,等了半小时没人理", "服务太拉胯了,体验不好,前台爱答不理", "包裹包装很严实,东西没磕碰", "物流包装到位,商品完好无损", # ... 实际场景中这里是上万条文本 ] vecs = model.encode(docs, normalize_embeddings=True).astype("float32") dim = vecs.shape[1] # 内积索引(向量已归一化,内积=余弦相似度) index = faiss.IndexFlatIP(dim) index.add(vecs) # 以第 0 条为查询,找 top-3 近邻 query_vec = vecs[0:1] scores, idxs = index.search(query_vec, k=3) print("最相近的条目:", [docs[i] for i in idxs[0]]) print("对应相似度:", [round(float(s), 3) for s in scores[0]])输出里,第 1 条("服务太拉胯…")会排在最前,相似度明显高于第 2、3 条(都是讲包装的)。上万条文本建索引只需几百毫秒,单条查询在毫秒级,完全能接线上。
落地时几个容易踩的坑
第一,阈值别拍脑袋。余弦相似度 0.7 算近重复还是 0.85 算,取决于业务容忍度。建议先抽一百条人工标一下,画出"相似度—是否真重复"的分布再定线。
第二,长文本要先切。模型有最大长度(通常 512 token),一整篇论文直接丢进去会被截断。先按段落或滑动窗口切,再对得到的多个向量取平均,比硬塞全文稳。
第三,时效性数据要重算。商品名、活动规则会变,向量是静态快照。建索引时记下数据版本,下游每月或每次大更新后批量重建,别让旧向量一直服役。
第四,聚类比两两比对更省事。要的是"哪些是一伙的",直接用faiss配合简单连通域或层次聚类,把相似度超阈值的互相连起来,比打印全量两两相似度实用得多。
总结
语义相似度不是玄学,它就是把"意思相近"翻译成"向量相近"再量距离。sentence-transformers负责把句子变成向量,faiss负责在规模上把查询提速到线上可用。这套组合在文本去重、反馈聚类、内容推荐去重、客服工单归并里都能直接落地。下一步如果你想接"先搜再答"的问答,那就是另一套更大体系的事了——但向量化的基本功,今天这篇已经帮你踩平了大部分坑。