474_文本相似度计算¶
一句话说明¶
文本相似度计算衡量两段文字有多"相像",是搜索引擎、推荐系统、重复检测的核心技术,从词袋到语义向量经历了巨大演进。
核心知识点¶
- 词汇层面:精确字符匹配(Levenshtein)、n-gram重叠(BLEU/ROUGE)
- 语义层面:将文本映射到向量空间,计算余弦相似度
- 对称 vs 非对称:
- 对称:两段文本互换结果不变(语义等价)
- 非对称:query→document(信息检索场景)
- Bi-Encoder vs Cross-Encoder:
- Bi-Encoder:离线预计算向量,ANN快速检索
- Cross-Encoder:两文本一起输入,精度高但慢(适合重排序)
方法对比¶
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Jaccard相似度 | 词集合交集/并集 | 简单快速 | 忽略词序和语义 |
| 编辑距离 | 字符级增删改次数 | 捕捉拼写错误 | 计算慢,语义盲 |
| TF-IDF余弦 | 稀疏向量余弦 | 考虑词频权重 | 同义词无法匹配 |
| Word2Vec均值 | 词向量平均 | 考虑语义 | 句义表示不准 |
| Sentence-BERT | BERT+对比学习 | 语义准确 | 需GPU |
| Cross-Encoder | 两文本拼接输入 | 最高精度 | 无法预计算 |
代码示例¶
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
from sentence_transformers import SentenceTransformer, util
# ---- 1. TF-IDF 余弦相似度 ----
texts = [
"深度学习在图像识别中表现优异",
"卷积神经网络用于图像分类",
"今天天气很好适合出门",
]
tfidf = TfidfVectorizer()
tfidf_matrix = tfidf.fit_transform(texts) # 稀疏矩阵 (3, vocab_size)
# 计算第1条和其他文本的余弦相似度
sim_matrix = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix)
print("TF-IDF相似度:", sim_matrix[0]) # [1.0, 0.4x, 0.0x]
# ---- 2. Sentence-BERT 语义相似度 ----
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
embeddings = model.encode(texts, convert_to_tensor=True)
# util.cos_sim计算余弦相似度矩阵
cosine_scores = util.cos_sim(embeddings, embeddings)
print("\nSentence-BERT相似度矩阵:")
for i in range(len(texts)):
for j in range(len(texts)):
print(f" ({i},{j}): {cosine_scores[i][j]:.4f}")
# ---- 3. 语义搜索(最近邻检索)----
query = "神经网络图像处理"
query_emb = model.encode(query, convert_to_tensor=True)
corpus_emb = model.encode(texts, convert_to_tensor=True)
# 查找最相似的文档
hits = util.semantic_search(query_emb, corpus_emb, top_k=2)
for hit in hits[0]:
idx = hit['corpus_id']
score = hit['score']
print(f"相似度 {score:.4f}: {texts[idx]}")
# ---- 4. 编辑距离(字符级相似)----
def levenshtein_distance(s1, s2):
"""计算两字符串的编辑距离"""
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i # 删除操作
for j in range(n + 1):
dp[0][j] = j # 插入操作
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] # 字符相同,无需操作
else:
dp[i][j] = 1 + min(
dp[i-1][j], # 删除
dp[i][j-1], # 插入
dp[i-1][j-1] # 替换
)
return dp[m][n]
print(levenshtein_distance("深度学习", "深度学院")) # 1(学习→学院)
# ---- 5. BM25 关键词相似度(信息检索经典)----
# pip install rank-bm25
from rank_bm25 import BM25Okapi
# BM25不需要语义,但考虑词频饱和和文档长度归一化
corpus = [text.split() for text in texts] # 简单分词(实际用jieba)
bm25 = BM25Okapi(corpus)
query_tokens = "深度学习 图像".split()
scores = bm25.get_scores(query_tokens) # 每个文档的BM25分数
print("BM25分数:", scores)
# ---- 6. Jaccard相似度 ----
def jaccard_sim(s1, s2):
"""基于字符集合的Jaccard相似度"""
set1, set2 = set(s1), set(s2)
return len(set1 & set2) / len(set1 | set2)
print(jaccard_sim("深度学习", "深度神经")) # 0.5
面试常问点¶
- 余弦相似度和欧式距离的区别?
余弦只看方向(大小不影响),适合文本;欧式看绝对距离,受向量模长影响
Bi-Encoder和Cross-Encoder怎么配合使用?
两阶段:Bi-Encoder快速检索Top-100候选,Cross-Encoder精确重排序Top-10
语义相似和词汇相似不一致怎么处理?
混合排序:λ×语义分 + (1-λ)×BM25分,用交叉验证调λ
大规模相似度搜索(百万文档)怎么实现?
- FAISS(Facebook AI Similarity Search):ANN(近似最近邻)检索,毫秒级
速查表¶
| 场景 | 方法 |
|---|---|
| 精确文本匹配 | 编辑距离 / Jaccard |
| 关键词检索 | TF-IDF / BM25 |
| 语义搜索 | Sentence-BERT + FAISS |
| 相似度重排序 | Cross-Encoder |
| 大规模向量检索 | FAISS / Annoy / Hnswlib |