人工智能之语言领域
第九章 文本相似度计算
文章目录
- 人工智能之语言领域
- [前言 文本相似度计算](#前言 文本相似度计算)
- [9.1 文本相似度概述](#9.1 文本相似度概述)
- [9.1.1 相似度的核心定义与应用场景](#9.1.1 相似度的核心定义与应用场景)
- [9.1.2 相似度与距离的关系](#9.1.2 相似度与距离的关系)
- [9.2 传统相似度计算方法](#9.2 传统相似度计算方法)
- [9.2.1 基于字符串的方法](#9.2.1 基于字符串的方法)
- [(1)编辑距离(Levenshtein Distance)](#(1)编辑距离(Levenshtein Distance))
- [(2)Jaccard 相似度](#(2)Jaccard 相似度)
- [9.2.2 基于向量的方法:TF-IDF 向量余弦相似度](#9.2.2 基于向量的方法:TF-IDF 向量余弦相似度)
- [9.3 现代相似度计算方法](#9.3 现代相似度计算方法)
- [9.3.1 基于词嵌入的相似度计算](#9.3.1 基于词嵌入的相似度计算)
- [9.3.2 基于预训练模型的句嵌入相似度](#9.3.2 基于预训练模型的句嵌入相似度)
- [9.3.3 双塔模型在相似度计算中的应用](#9.3.3 双塔模型在相似度计算中的应用)
- [9.4 实战案例:智能问答中的问句匹配与文本检索](#9.4 实战案例:智能问答中的问句匹配与文本检索)
- 补充:方法对比总结
- 小结
- 资料关注
前言 文本相似度计算
文本相似度计算是自然语言处理(NLP)中一项基础而关键的技术,旨在量化两段文本在语义或形式上的接近程度。它是智能搜索、问答系统、推荐引擎、抄袭检测等应用的核心支撑。本章将系统介绍文本相似度的定义、传统与现代计算方法,并通过智能问答中的问句匹配实战案例完整演示端到端流程。
9.1 文本相似度概述
9.1.1 相似度的核心定义与应用场景
文本相似度 :衡量两个文本片段在字面形式 或语义内容 上的相近程度,取值通常在 [0, 1] 区间(0=完全不相似,1=完全相同)。
典型应用场景:
| 场景 | 任务 | 相似度作用 |
|---|---|---|
| 智能问答(QA) | 用户问句 vs FAQ库 | 找最匹配的标准问题 |
| 搜索引擎 | 查询 vs 网页 | 排序相关文档 |
| 推荐系统 | 用户历史 vs 候选内容 | 推荐相似商品/文章 |
| 文本去重 | 新闻/论文比对 | 检测重复或抄袭 |
| 对话系统 | 用户输入 vs 对话状态 | 判断意图是否一致 |
🌰 示例:
问句1:"怎么重置我的密码?"
问句2:"忘记密码了怎么办?"
→ 语义高度相似,应匹配同一答案
9.1.2 相似度与距离的关系
距离(Distance) 越小 → 相似度(Similarity) 越大。常见转换关系:
| 距离度量 | 相似度转换 | 特点 |
|---|---|---|
| 余弦距离 | sim = 1 - distance 或直接用 cosine_similarity |
忽略向量长度,关注方向 |
| 欧氏距离 | sim = 1 / (1 + distance) |
受向量尺度影响大 |
| Jaccard 距离 | `sim = 1 - distance = | A∩B |
文本1
向量化
文本2
距离度量
余弦距离
欧氏距离
Jaccard距离
相似度 = cos(θ)
相似度 = 1/(1+d)
相似度 = |A∩B|/|A∪B|
✅ 余弦相似度 是 NLP 中最常用的相似度度量,因其对文本长度不敏感。
9.2 传统相似度计算方法
9.2.1 基于字符串的方法
(1)编辑距离(Levenshtein Distance)
- 定义:将一个字符串转换为另一个所需的最少单字符编辑操作(插入、删除、替换)次数。
- 相似度转换:
sim = 1 - edit_distance / max(len(s1), len(s2))
python
def levenshtein_distance(s1, s2):
if len(s1) < len(s2):
return levenshtein_distance(s2, s1)
if len(s2) == 0:
return len(s1)
previous_row = list(range(len(s2) + 1))
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]
s1, s2 = "kitten", "sitting"
dist = levenshtein_distance(s1, s2)
sim = 1 - dist / max(len(s1), len(s2))
print(f"编辑距离: {dist}, 相似度: {sim:.3f}") # 0.571
⚠️ 缺陷:无法捕捉语义("car" 和 "automobile" 距离大)
(2)Jaccard 相似度
- 将文本视为词集合,计算交集与并集之比。
- 适用于关键词匹配场景。
python
def jaccard_similarity(s1, s2):
set1, set2 = set(s1.split()), set(s2.split())
intersection = set1 & set2
union = set1 | set2
return len(intersection) / len(union) if union else 0
s1 = "如何重置密码"
s2 = "密码忘了怎么重置"
print("Jaccard相似度:", jaccard_similarity(s1, s2)) # 0.4(按字切分更高)
💡 中文建议按字 或分词后处理。
9.2.2 基于向量的方法:TF-IDF 向量余弦相似度
步骤:
- 构建语料库 TF-IDF 矩阵
- 将两文本表示为 TF-IDF 向量
- 计算余弦相似度
python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
corpus = [
"如何重置我的账户密码",
"忘记密码了怎么办",
"怎样登录我的邮箱",
"手机无法开机"
]
# 拟合 TF-IDF
vectorizer = TfidfVectorizer(token_pattern=r'\S+') # 中文需先分词
# 简化:按字切分
corpus_char = [" ".join(list(s)) for s in corpus]
tfidf_matrix = vectorizer.fit_transform(corpus_char)
# 计算相似度
query = " ".join(list("密码忘记了怎么找回"))
query_vec = vectorizer.transform([query])
cos_sim = cosine_similarity(query_vec, tfidf_matrix).flatten()
for i, score in enumerate(cos_sim):
print(f"'{corpus[i]}' 相似度: {score:.3f}")
# 输出:第一句和第二句得分高
✅ 优点:简单高效,适合关键词匹配
❌ 缺点:忽略语序和语义("北京到上海" vs "上海到北京" 被视为不同)
9.3 现代相似度计算方法
9.3.1 基于词嵌入的相似度计算
使用 Word2Vec/GloVe/FastText 获取词向量,再聚合为句向量。
常用聚合策略:
- 平均池化(Mean Pooling):所有词向量求均值
- TF-IDF 加权平均:高频通用词权重低
- SIF(Smooth Inverse Frequency):去除主成分噪声
python
import numpy as np
from gensim.models import Word2Vec
# 假设已训练好 Word2Vec 模型
model = Word2Vec.load("your_w2v_model.bin")
def sentence_embedding(words, model):
vectors = [model.wv[w] for w in words if w in model.wv]
return np.mean(vectors, axis=0) if vectors else np.zeros(model.vector_size)
s1 = ["密码", "重置"]
s2 = ["忘记", "密码", "怎么办"]
emb1 = sentence_embedding(s1, model)
emb2 = sentence_embedding(s2, model)
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity([emb1], [emb2])[0][0]
print("词嵌入相似度:", sim)
⚠️ 仍存在局限:无法处理一词多义("苹果"在不同上下文)
9.3.2 基于预训练模型的句嵌入相似度
Sentence-BERT(SBERT) 是当前 SOTA 方案,专为句子相似度设计。
- 微调 BERT,使语义相近句子的嵌入距离更近
- 支持直接计算余弦相似度
python
from sentence_transformers import SentenceTransformer
# 加载中文 SBERT 模型
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# 或中文专用:'shibing624/text2vec-base-chinese'
sentences = [
"如何重置密码?",
"密码忘了怎么找回?",
"我的手机坏了"
]
embeddings = model.encode(sentences)
sim_matrix = cosine_similarity(embeddings)
print("相似度矩阵:")
print(sim_matrix.round(3))
# 输出:前两句相似度 >0.8,与第三句 <0.2
🌐 中文推荐模型:
shibing624/text2vec-base-chineseWangZeJun/sentence-bert-base-chinese
9.3.3 双塔模型在相似度计算中的应用
双塔架构(Dual Encoder) 是工业级大规模检索的标准方案。
- Query 塔:编码用户查询
- Doc 塔:编码候选文档
- 离线计算:Doc 嵌入可预先计算并索引
- 在线匹配:仅需计算 Query 嵌入,与 Doc 库做 ANN 搜索
用户查询
Query Encoder
文档1
Doc Encoder
文档2
向量索引
ANN 搜索
Top-K 相似文档
优势:
- 在线推理快(O(1) 复杂度)
- 支持十亿级文档检索
实现工具:
- 向量索引:FAISS(Facebook)、Annoy(Spotify)
- 模型训练:使用对比学习(Contrastive Loss)或三元组损失(Triplet Loss)
python
# 使用 FAISS 进行快速相似检索
import faiss
import numpy as np
# 假设 doc_embeddings 是 [N, 768] 的文档嵌入矩阵
index = faiss.IndexFlatIP(768) # 内积(等价于余弦,若向量已归一化)
index.add(doc_embeddings)
# 查询
query_emb = model.encode(["如何重置密码?"])
D, I = index.search(query_emb, k=3) # 返回 Top-3
print("最相似文档ID:", I[0])
print("相似度得分:", D[0]) # 内积值 ∈ [-1, 1]
9.4 实战案例:智能问答中的问句匹配与文本检索
任务目标
构建一个 FAQ 问答系统:
- 输入:用户自然语言问句
- 输出:最匹配的标准问题及答案
完整实现流程
Step 1: 准备 FAQ 数据库
python
faq = {
"如何重置密码?": "请访问登录页面,点击'忘记密码'链接。",
"怎么修改手机号?": "进入个人中心 -> 账户安全 -> 修改手机号。",
"退款流程是什么?": "提交退款申请后,3个工作日内处理。"
}
standard_questions = list(faq.keys())
Step 2: 构建句嵌入索引
python
from sentence_transformers import SentenceTransformer
import faiss
# 加载模型
model = SentenceTransformer('shibing624/text2vec-base-chinese')
# 编码标准问题
doc_embeddings = model.encode(standard_questions)
# 构建 FAISS 索引(L2 距离)
index = faiss.IndexFlatL2(doc_embeddings.shape[1])
index.add(np.array(doc_embeddings, dtype='float32'))
Step 3: 用户查询匹配
python
def get_answer(user_query, top_k=1):
# 编码查询
query_emb = model.encode([user_query])
# 搜索
D, I = index.search(np.array(query_emb, dtype='float32'), top_k)
# 返回最相似问题及答案
best_idx = I[0][0]
similarity = 1 - D[0][0] / 4 # 粗略归一化(L2最大距离≈2)
if similarity > 0.5: # 阈值过滤
question = standard_questions[best_idx]
return faq[question], similarity
else:
return "抱歉,未找到相关问题。", similarity
# 测试
user_input = "密码忘记了怎么办?"
answer, score = get_answer(user_input)
print(f"用户问: {user_input}")
print(f"匹配度: {score:.3f}")
print(f"回答: {answer}")
Step 4: 可视化相似度(可选)
python
import matplotlib.pyplot as plt
scores = []
for q in standard_questions:
emb = model.encode([q])
D, _ = index.search(np.array(emb, dtype='float32'), 1)
scores.append(1 - D[0][0]/4)
plt.bar(standard_questions, scores)
plt.title("各标准问题与用户的匹配度")
plt.xticks(rotation=45)
plt.show()
系统优化建议
- 负采样:训练时加入困难负例(如语义相近但不匹配的问题)
- 多轮交互:若 Top-1 置信度低,提供多个候选让用户选择
- 领域微调:在客服对话日志上继续微调 SBERT
补充:方法对比总结
| 方法 | 语义理解 | 速度 | 适用场景 |
|---|---|---|---|
| 编辑距离 | ❌ | ⚡️ | 拼写纠错、短文本 |
| Jaccard | ❌ | ⚡️ | 关键词匹配 |
| TF-IDF + 余弦 | △ | ⚡️ | 中小规模检索 |
| 词嵌入平均 | ✅ | ⚡️ | 无监督场景 |
| SBERT | ✅✅ | 中 | 高精度匹配 |
| 双塔 + FAISS | ✅✅ | ⚡️⚡️ | 大规模在线服务 |
小结
文本相似度计算从早期的字符串匹配发展到今天的深度语义模型,已成为智能系统不可或缺的组件。TF-IDF 仍是轻量级应用的首选,而 Sentence-BERT + 向量检索 是当前工业界高精度场景的标准方案。在实际项目中,应根据数据规模、延迟要求、语义深度 选择合适技术栈。未来,相似度计算将与大语言模型(LLM) 、多模态嵌入 、个性化表示深度融合,迈向更智能、更精准的语义匹配。
资料关注
咚咚王
《Python 编程:从入门到实践》
《利用 Python 进行数据分析》
《算法导论中文第三版》
《概率论与数理统计(第四版) (盛骤) 》
《程序员的数学》
《线性代数应该这样学第 3 版》
《微积分和数学分析引论》
《(西瓜书)周志华-机器学习》
《TensorFlow 机器学习实战指南》
《Sklearn 与 TensorFlow 机器学习实用指南》
《模式识别(第四版)》
《深度学习 deep learning》伊恩·古德费洛著 花书
《Python 深度学习第二版(中文版)【纯文本】 (登封大数据 (Francois Choliet)) (Z-Library)》
《深入浅出神经网络与深度学习 +(迈克尔·尼尔森(Michael+Nielsen)》
《自然语言处理综论 第 2 版》
《Natural-Language-Processing-with-PyTorch》
《计算机视觉-算法与应用(中文版)》
《Learning OpenCV 4》
《AIGC:智能创作时代》杜雨 +&+ 张孜铭
《AIGC 原理与实践:零基础学大语言模型、扩散模型和多模态模型》
《从零构建大语言模型(中文版)》
《实战 AI 大模型》
《AI 3.0》