文本匹配与相似度计算的深度解析
· 12 min read
"在信息的海洋中,找到相似的文本就像在茫茫人海中寻找知己。这不仅需要技巧,更需要对语言深层次的理解。" —— 2017年在广联达优化智能客服系统时的思考
开篇:文本匹配的无处不在
从搜索引擎到推荐系统,从智能客服到论文查重,文本匹配与相似度计算是NLP领域最基础也最核心的技术之一。在广联达设计智能客服系统时,我面临的核心挑战就是:如何让机器准确判断用户问题与知识库中哪个问题最相似?
这个问题看似简单,实则充满了挑战:
- 同义词问题:
电脑vs计算机 - 语序问题:
A和BvsB和A - 句法结构问题:
我把书给他vs他把书给我 - 深层语义问题:
苹果手机多少钱vsiPhone价格
这篇博客将带你深入探索文本匹配技术的演进,从传统方法到深度学习模型。
传统方法:从字符串到向量
1. 编辑距离(Edit Distance)
编辑距离是最直观的相似度度量,它计算一个字符串转换成另一个字符串所需的最少编辑操作次数(插入、删除、替换)。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import jieba
from collections import Counter
class TraditionalSimilarity:
"""传统相似度计算方法"""
def __init__(self):
pass
def edit_distance(self, s1, s2):
"""计算编辑距离"""
if len(s1) < len(s2):
return self.edit_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]
def edit_distance_similarity(self, s1, s2):
"""基于编辑距离的相似度"""
max_len = max(len(s1), len(s2))
if max_len == 0:
return 1.0
return 1 - self.edit_distance(s1, s2) / max_len
def jaccard_similarity(self, s1, s2):
"""Jaccard相似度"""
set1 = set(jieba.lcut(s1))
set2 = set(jieba.lcut(s2))
intersection = len(set1.intersection(set2))
union = len(set1.union(set2))
return intersection / union if union > 0 else 0
def visualize_traditional_methods(self):
"""可视化传统方法的效果"""
test_cases = [
("钢筋怎么计算", "如何计算钢筋用量"), # 语义相似,字面不同
("混凝土强度等级", "混凝土等级强度"), # 语序不同
("软件操作指南", "软件使用手册"), # 同义词
("造价预算", "成本预算"), # 部分同义
("施工安全", "项目管理") # 不相关
]
results = []
for s1, s2 in test_cases:
edit_sim = self.edit_distance_similarity(s1, s2)
jaccard_sim = self.jaccard_similarity(s1, s2)
results.append({
'pair': f"{s1[:4]}... vs {s2[:4]}...",
'edit_similarity': edit_sim,
'jaccard_similarity': jaccard_sim
})
df = pd.DataFrame(results)
fig, ax = plt.subplots(figsize=(12, 7))
df.set_index('pair').plot(kind='bar', ax=ax, alpha=0.7)
ax.set_ylabel('相似度')
ax.set_title('传统相似度方法比较')
ax.tick_params(axis='x', rotation=45)
ax.grid(True, alpha=0.3, axis='y')
# 添加数值标注
for p in ax.patches:
ax.annotate(f"{p.get_height():.2f}",
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='center', xytext=(0, 9),
textcoords='offset points')
plt.tight_layout()
plt.show()
print("=== 传统方法评估 ===")
print("编辑距离:对字面相似度敏感,无法处理同义词和语序问题。")
print("Jaccard相似度:基于词袋模型,能处理语序问题,但无法处理同义词。")
# 运行传统方法演示
traditional_sim = TraditionalSimilarity()
traditional_sim.visualize_traditional_methods()
2. 向量空间模型(Vector Space Model)
这是文本匹配领域的一次飞跃,它将文本表示为向量,从而可以在向量空间中计算距离。
TF-IDF (Term Frequency-Inverse Document Frequency)
TF-IDF是最经典的文本表示方法,它衡量一个词在文档中的重要性。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
class VectorSpaceModel:
"""向量空间模型"""
def __init__(self):
self.vectorizer = TfidfVectorizer(tokenizer=jieba.lcut)
def tfidf_similarity(self, corpus):
"""计算语料库中所有文本对的TF-IDF余弦相似度"""
tfidf_matrix = self.vectorizer.fit_transform(corpus)
# 计算两两之间的余弦相似度
cosine_sim_matrix = cosine_similarity(tfidf_matrix)
return cosine_sim_matrix, self.vectorizer.get_feature_names_out()
def visualize_tfidf_heatmap(self, corpus):
"""可视化TF-IDF相似度热图"""
cosine_sim_matrix, _ = self.tfidf_similarity(corpus)
# 截断语料库标签以便显示
labels = [text[:10] + '...' if len(text) > 10 else text for text in corpus]
plt.figure(figsize=(10, 8))
sns.heatmap(cosine_sim_matrix, annot=True, cmap='viridis', fmt='.2f',
xticklabels=labels, yticklabels=labels)
plt.title('TF-IDF 余弦相似度热图')
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.show()
def analyze_tfidf_vectors(self, corpus):
"""分析TF-IDF向量"""
tfidf_matrix = self.vectorizer.fit_transform(corpus)
feature_names = self.vectorizer.get_feature_names_out()
df_tfidf = pd.DataFrame(tfidf_matrix.toarray(),
columns=feature_names,
index=[f"Doc {i+1}" for i in range(len(corpus))])
print("=== TF-IDF 向量分析 ===")
print(df_tfidf)
# 找出每个文档中最重要的词
print("\n=== 各文档中最重要的TF-IDF词汇 ===")
for i, doc in enumerate(corpus):
top_word_idx = np.argmax(tfidf_matrix[i])
top_word = feature_names[top_word_idx]
top_score = tfidf_matrix[i, top_word_idx]
print(f"文档 {i+1} ('{doc[:10]}...'): '{top_word}' (TF-IDF: {top_score:.3f})")
# 运行向量空间模型演示
vsm = VectorSpaceModel()
corpus = [
"钢筋搭接长度怎么计算",
"如何计算钢筋的搭接长度",
"混凝土强度等级如何选择",
"造价软件的操作指南",
"广联达软件使用手册"
]
vsm.visualize_tfidf_heatmap(corpus)
vsm.analyze_tfidf_vectors(corpus)
词嵌入方法:让机器理解语义
TF-IDF虽然强大,但它无法理解词语之间的语义关系。词嵌入(Word Embedding)技术解决了这个问题。
1. Word2Vec
Word2Vec是Google在2013年提出的经典词嵌入模型,它能将词语映射到一个低维向量空间,使得语义相近的词在空间中也相近。
from gensim.models import Word2Vec
from sklearn.decomposition import PCA
class WordEmbeddingSimilarity:
"""基于词嵌入的相似度计算"""
def __init__(self):
self.model = None
def train_word2vec_model(self, sentences):
"""训练Word2Vec模型"""
# 分词
tokenized_sentences = [jieba.lcut(s) for s in sentences]
# 训练模型
self.model = Word2Vec(sentences=tokenized_sentences,
vector_size=100, # 向量维度
window=5, # 上下文窗口大小
min_count=1, # 最小词频
workers=4) # 训练线程数
print("Word2Vec模型训练完成")
return self.model
def get_sentence_vector(self, sentence):
"""计算句子向量(简单平均法)"""
words = jieba.lcut(sentence)
vectors = [self.model.wv[word] for word in words if word in self.model.wv]
if not vectors:
return np.zeros(self.model.vector_size)
return np.mean(vectors, axis=0)
def word2vec_similarity(self, s1, s2):
"""计算基于Word2Vec的余弦相似度"""
if self.model is None:
raise ValueError("模型未训练")
vec1 = self.get_sentence_vector(s1).reshape(1, -1)
vec2 = self.get_sentence_vector(s2).reshape(1, -1)
return cosine_similarity(vec1, vec2)[0][0]
def visualize_word_embeddings(self, words_to_visualize):
"""可视化词向量"""
if self.model is None:
raise ValueError("模型未训练")
word_vectors = np.array([self.model.wv[word] for word in words_to_visualize if word in self.model.wv])
# 使用PCA降维到2D
pca = PCA(n_components=2)
result = pca.fit_transform(word_vectors)
plt.figure(figsize=(12, 8))
plt.scatter(result[:, 0], result[:, 1])
for i, word in enumerate(words_to_visualize):
if word in self.model.wv:
plt.annotate(word, xy=(result[i, 0], result[i, 1]), fontsize=12)
plt.title('Word2Vec 词向量可视化 (PCA降维)')
plt.xlabel('PCA Component 1')
plt.ylabel('PCA Component 2')
plt.grid(True, alpha=0.3)
plt.show()
def find_similar_words(self, word, topn=5):
"""查找相似词"""
if self.model is None or word not in self.model.wv:
return []
return self.model.wv.most_similar(word, topn=topn)
# 运行词嵌入方法演示
embedding_sim = WordEmbeddingSimilarity()
# 模拟建筑领域语料库
construction_corpus = [
"钢筋搭接长度怎么计算", "如何计算钢筋的搭接长度", "钢筋用量计算方法",
"混凝土强度等级如何选择", "C30混凝土是什么意思", "混凝土配合比设计",
"造价软件的操作指南", "广联达软件使用手册", "如何使用造价软件",
"施工安全注意事项", "项目管理中的安全问题", "建筑工地安全规范"
]
# 训练Word2Vec模型
w2v_model = embedding_sim.train_word2vec_model(construction_corpus)
# 可视化词向量
words_to_show = ['钢筋', '混凝土', '造价', '软件', '安全', '计算', '选择', '操作']
embedding_sim.visualize_word_embeddings(words_to_show)
# 查找相似词
print("\n=== 相似词查找 ===")
similar_words = embedding_sim.find_similar_words('软件')
print(f"'软件'的相似词: {similar_words}")
# 计算句子相似度
s1 = "钢筋怎么计算"
s2 = "如何计算钢筋用量"
similarity = embedding_sim.word2vec_similarity(s1, s2)
print(f"\n句子相似度 ('{s1}' vs '{s2}'): {similarity:.3f}")
深度学习方法:端到端的匹配
随着深度学习的发展,文本匹配进入了新时代。深度学习模型可以直接从原始文本中学习匹配模式,无需手动设计特征。