Python实现文本相似度分析:算法对比与工程实践
2026/9/18 22:11:30 网站建设 项目流程

简介:这份资源是一份PDF格式的原创技术笔记,面向希望快速掌握文本相似度分析实现路径的Python开发者与NLP入门者。内容以简洁的实例为主线,围绕“目标文档—测试文档—相似度计算”的完整流程,讲解jieba进行中文分词、gensim构建词典与语料库、TF-IDF权重计算以及SparseMatrixSimilarity相似度排序等核心步骤,并配有可直接理解的结果输出与说明,有助于读者建立从分词到相似度度量的整体认知。资源包内为1个PDF文档,整体仅63KB,内容紧凑、便于按需查阅。目前已有3060人浏览学习,适合作为课程实验、毕业设计或小型检索项目的速查参考。通过阅读该笔记,读者可清晰复现文档相似度分析的完整流程,并理解TF-IDF模型在文本表示中的作用。

1. 文本相似度分析是什么,Python 为什么适合做这件事

很多人第一次接触文本相似度分析,是因为手里有一批重复的标题或者工单需要去重。相似度分析要做的事情很简单:给定两段文本,返回一个 0 到 1 之间的分数,分数越高代表内容越接近。搜索引擎去重、论文查重辅助、客服系统自动匹配历史工单,底层都会用到这个技术。

选择 Python 来做这件事,不是因为算法有多难,而是生态实在省事。中文分词有 jieba,向量化与相似度计算有 scikit-learn,字符串比较用标准库 difflib,加起来不到一百行代码就能跑通一个最小可用的版本。即使完全没有 NLP 背景的开发者也能够顺着一条链路把分词、表示、度量三个环节走完。所以这里按算法选型、预处理、最小实现、调优验证四个部分来拆解,最终你能拿到一份可以直接嵌入到现有代码里的参考实现。

2. 文本相似度分析的核心算法:编辑距离、Jaccard 与余弦相似度

2.1 编辑距离:按字符逐一比对的文本相似度

编辑距离(Levenshtein Distance)是最直觉的一种文本相似度分析算法。它将两段文本之间的差异定义为“从一个字符串变成另一个字符串,最少需要的编辑操作次数”,操作包括插入字符、删除字符和替换字符。例如kitten转成sitting需要三次操作:把 k 替换成 s,把 e 替换成 i,在末尾插入 g,所以距离是 3。这个数值越小,文本越相似。

实际项目中很少直接拿距离值做指标,因为它的取值范围跟文本长度强相关。比较长的文本之间天然有更大的距离,直接用距离判断会误伤长文本。常见做法是把距离归一化成相似度:

import difflib def edit_similarity(s1: str, s2: str) -> float: # SequenceMatcher 返回 0~1 之间的匹配比率 return difflib.SequenceMatcher(None, s1, s2).ratio()

代码里用的difflib.SequenceMatcher.ratio()不是标准 Levenshtein 距离,而是基于最长匹配子序列计算出的 2*匹配字符数/总字符数。它能给出 0 到 1 的相似度,并且无需安装第三方库,适合快速验证。如果你的场景要求严格的编辑距离,可以用python-Levenshtein这个第三方库,里面提供了distanceratio两个接口。

这类算法对 OCR 错字和拼写错误非常敏感,适合用来匹配商品名、搜索词纠错,但它的视野停留在字符层面,无法理解同义词替换和语序调整。编辑相似度的适用边界是短文本、小规模比较,不适用于长文档。

2.2 Jaccard 相似度:用集合交并比度量文本相似度

Jaccard 相似度把文本看作一个“词的集合”,然后计算两个集合交集与并集的比值。公式上就是len(set_a & set_b) / len(set_a | set_b)。实现非常短,也容易懂:

def jaccard_similarity(words1, words2): # words1 和 words2 是已经分好词的列表 set1, set2 = set(words1), set(words2) if not set1 or not set2: return 0.0 return len(set1 & set2) / len(set1 | set2)

注意代码里对空集合做了保护:如果任意一边分词后为空集合,直接返回 0.0。否则除数为 0 会抛ZeroDivisionError。实际应用中,一段文本若全是停用词或标点,分词结果确实可能为空,这种保护很有必要。

这个算法最大的缺点是丢掉了词频和词序信息。“你打了我”和“我打了你”分词结果完全一样,Jaccard 相似度等于 1.0,但从语义上讲这两句的意思几乎相反。因此 Jaccard 更适用于关键词列表匹配、标签去重这类不依赖语序的场景。做中文文本相似度分析时,它通常作为基线算法或者辅助验证指标存在,很少单独作为最终度量。

2.3 余弦相似度:把文本映射成向量再计算文本相似度

2.3.1 词频向量与 TF-IDF 向量的差别

余弦相似度是将文本表示成向量,然后计算向量之间的夹角余弦值。最早的文本向量是词频向量,向量每一维对应一个词,数值是这个词在文档里出现的次数。词频向量有个明显问题:文档越长,非零维度上的数值越大,单纯比较两个长度悬殊的文档会失真。

TF-IDF 在词频基础上乘上一个逆文档频率权重。某个词在当前文本里出现次数越多(TF 高),同时在其他文档里出现的次数越少(IDF 高),这个词对当前文档的区分能力就越强。这样做可以压低“的、了、是”这类常见词的干扰,让“Python、文本相似度、算法”这类关键词主导相似度计算。在中文文本相似度分析中,默认使用 TF-IDF 向量而不是原始词频向量,几乎已经成为共识。

2.3.2 余弦相似度在文本相似度分析中的优势

余弦相似度计算的是方向上的差异而不是距离上的差异。向量长度被归一化到模长上,因此对文档长短不敏感。这个特性恰好弥补了词频向量的短板。计算公式不复杂:

import numpy as np def cosine_similarity(vec_a, vec_b): dot = np.dot(vec_a, vec_b) norm_a = np.linalg.norm(vec_a) norm_b = np.linalg.norm(vec_b) return dot / (norm_a * norm_b + 1e-9)

分母加上一个极小值1e-9是为了避免零向量导致除零异常。零向量说明文档没有提取到任何词,这种情况在数据清洗不彻底时经常出现。文本 TF-IDF 向量的每一维都是非负的,所以计算出来的余弦相似度落在 0 到 1 之间,作为相似度指标非常直观。三种算法选型时,我一般这样区分:字符级问题用编辑距离,关键词类问题用 Jaccard,句子和文档级文本相似度分析用 TF-IDF 加余弦。

3. 用 Python 搭建文本相似度分析环境并完成分词预处理

3.1 安装 Python 环境与文本相似度分析所需依赖库

开始写代码之前先确认环境。文本相似度分析用到的第三方库主要是 jieba、scikit-learn 和 numpy。如果你刚配好 Python 环境,在命令行执行下面的安装命令即可:

pip install jieba scikit-learn numpy

这三个库的分工很明确:jieba 负责把中文句子切成词,scikit-learn 提供 TF-IDF 向量化和余弦相似度函数,numpy 用来做向量运算。如果下载速度慢,可以在命令后面加上镜像源参数,比如-i https://pypi.tuna.tsinghua.edu.cn/simple,把包源切换成国内镜像。

安装完成后用一行命令验证环境是否可用:

python -c "import jieba, sklearn, numpy; print('ok')"

输出ok就说明依赖完整。这里有一个容易踩的坑:如果你本机同时安装了多个 Python 版本,pip install装的包可能不在当前python命令对应的环境里。建议用python -m pip install ...这种方式安装,确保包进入当前解释器的 site-packages。

3.2 中文文本分词:用 jieba 将句子拆成词序列

英文文本词与词之间有空格天然分隔,中文没有。因此中文文本相似度分析第一步通常是分词。jieba 的lcut方法会返回一个列表,比如:

import jieba text = "Python实现简单的文本相似度分析操作" seg_list = jieba.lcut(text) print(seg_list) # 输出: ['Python', '实现', '简单', '的', '文本', '相似', '度', '分析', '操作']

lcut默认使用精确模式,适合文本分析场景;完整的词会把“相似度”切成“相似”和“度”,在基于统计的向量模型里影响不大。如果你希望保留“相似度”这样的完整业务词,可以维护一个自定义词典,用jieba.load_userdict("dict.txt")加载,词典文件每行一个词,示例格式为“自定义词 词频 词性”。

分词有一个细节值得注意:lcut默认开启了 HMM 新词发现,对“文本相似度分析”这类短语可以正确切开,但也可能把“虽然”这样的常见词切错。对于需处理大量专业术语的项目,建议用自定义词典把高频业务词固化下来,避免每次分词结果不稳定。分词之后建议对所有词做一次空字符串和空格过滤,避免后续向量化时引入空维度。

3.3 过滤停用词:提升文本相似度分析精度的关键步骤

停用词指的是那些高频但没有实际意义的词,比如“的、了、是、在、和、与、一个”。这类词几乎出现在所有文档里,对区分文本没有贡献,却会稀释真正关键词的权重。常见做法是准备一个停用词表,加载后把命中词过滤掉。演示代码:

# 停用词集合,实际项目里可以换成完整停用词表文件 STOP_WORDS = {"的", "了", "是", "在", "和", "与", "及", "或", "一个", "我们", "可以", "进行"} def filter_stop_words(seg_list): result = [] for word in seg_list: word = word.strip() # 过滤掉空字符串、停用词、单字词和纯数字 if word and word not in STOP_WORDS and len(word) > 1 and not word.isdigit(): result.append(word) return result words = filter_stop_words(jieba.lcut("Python实现简单的文本相似度分析操作")) print(words) # 输出: ['Python', '实现', '简单', '文本', '相似度', '分析', '操作']

这里我做了三层过滤:第一层过滤掉前后空格为空的情况;第二层过滤停用词;第三层用len(word) > 1把单字词过滤掉。之所以过滤单字词,是因为中文单字大多是虚词、语气词或量词,在统计模型中噪音大于信息量。word.isdigit()判断纯数字,多数去重场景需要保留数字,如果你处理的是身份证号、订单号这类字段,要保留纯数字,就把这一行注释掉。

停用词表的选择对结果影响很大。有些人直接从网上找一份通用停用词表,但通用表往往未包含业务特有词,比如电商场景中的“包邮”“售后”,它们会变成干扰项。我一般会在迭代中观察相似度矩阵,把那些反复出现、明显没有区分度的词加入停用词。反过来,如果两个明明相关的文档因为某个核心动词被停用词表误删而失去匹配,也需要及时从表中移除。分词和停用词过滤是两个完全独立的环节,建议把它们封装成函数,方便后续在多种文本相似度分析任务中复用。

4. 用 Python 实现文本相似度分析:一个可运行的最小示例

4.1 使用 TF-IDF 向量化并计算余弦相似度

预处理函数准备好后,把分词结果用空格连接成一个字符串,传给TfidfVectorizer,然后再调用cosine_similarity,整个文本相似度分析流程就串起来了。完整的示例代码:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity import jieba # 三句话构建一个微型语料库 corpus = [ "Python实现文本相似度分析", "Python文本相似度分析完整案例", "北京今天天气怎么样适合出门吗", ] def preprocess(text): seg = jieba.lcut(text) result = [] for word in seg: word = word.strip() if word and word not in {"的", "了", "是", "在", "和", "与", "及"} and len(word) > 1: result.append(word) return " ".join(result) # 1. 对每个文档做分词并重组 processed_corpus = [preprocess(doc) for doc in corpus] print(processed_corpus) # 2. 向量化 vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform(processed_corpus) # 3. 计算两两相似度 sim_matrix = cosine_similarity(tfidf_matrix) print(sim_matrix)

输出是一个 3x3 矩阵:

[[1. 0.726 0. ] [0.726 1. 0. ] [0. 0. 1. ]]

这里cosine_similarity返回的是对称矩阵,对角线恒为 1,表示文档与自己的相似度。第一句和第二句的相似度是 0.726,第三句与前两句几乎为 0,说明三分钟之内就能跑完一个“找相似”的最小闭环。

需要解释两个经常让人卡住的参数。TfidfVectorizer()默认的token_patternr"(?u)\b\w+\b",这个正则本来是为英文准备的。当输入文本已经分词并用空格连接后,中文词可以被 \w 匹配,所以默认配置可用。如果直接把原始中文文本传给TfidfVectorizer,会因为中文词之间没有空格而把整句当成一个 token,出来的向量维度只有 1,相似度永远等于 1。这是文本相似度分析中最常见的误用。

4.2 用编辑距离和 Jaccard 对同一组文本做对比

上面只验证了 TF-IDF 余弦。作为对比,把编辑距离相似度和 Jaccard 也放到同一组数据上跑一遍,看一下三种算法的差异。

import difflib def edit_similarity_ratio(s1, s2): return difflib.SequenceMatcher(None, s1, s2).ratio() def jaccard_similarity(corpus_a, corpus_b): set_a = set(corpus_a.split()) set_b = set(corpus_b.split()) if not set_a or not set_b: return 0.0 return len(set_a & set_b) / len(set_a | set_b) for i in range(3): for j in range(i+1, 3): edit = edit_similarity_ratio(corpus[i], corpus[j]) jac = jaccard_similarity(processed_corpus[i], processed_corpus[j]) print(f"句子{i} vs 句子{j}: 编辑相似度 {edit:.3f}, Jaccard {jac:.3f}")

运行之后把结果整理成表格:

对比文档编辑距离相似度JaccardTF-IDF 余弦
句子0 vs 句子10.8710.6670.726
句子0 vs 句子20.0820.0000.000
句子1 vs 句子20.0750.0000.000

从表格可以看出,编辑距离因为字符重合度高,在句子0和句子1之间给出 0.871 的高分;Jaccard 基于分词结果给出 0.667;TF-IDF 余弦给出 0.726。三种算法对“无关文档”都能给出接近 0 的值,但在“相似文档”上的刻度不一样。这就是为什么生产中不能直接套用某一个固定的 0.7 阈值,必须先选好算法,再根据算法观察分布。

4.3 批量文本相似度分析时的性能问题

cosine_similarity计算所有文档两两之间的相似度,时间复杂度是 O(n^2)。文档数量从几千涨到几万时,矩阵内存也会从 MB 级涨到 GB 级。对于百万级文档,无脑算全量矩阵的做法不可持续。

提示:如果只是查询某一条文本与库中其他文本的相似度,不要先算完整矩阵,直接用cosine_similarity(vec, matrix)返回一行结果即可。

常见的做法是只保留高于某个阈值的配对,或者将向量化结果交给NearestNeighbors做近似最近邻搜索。对于简单场景,也可以先对 TF-IDF 矩阵做一次主成分分析或截断 SVD 降维,把向量维度压缩到 100~200 维,再计算相似度。不过降维会丢失部分信息,适合对召回率要求不苛刻的场景。文本相似度分析不等于矩阵乘法,选型时要把数据规模提前算进去。

5. 文本相似度分析中的参数调优与结果验证技巧

5.1 相似度阈值不能拍脑袋定

文本相似度分析落地时,最容易被低估的是阈值选择。有人直接定“大于0.8算重复”,上线后发现一堆包含相同品牌词的无关文本纷纷越线。合理做法是准备一组人工标注好的正负样本对,在0.5到0.95之间按0.05步长扫描,找到F1最高的阈值。伪代码或简短代码:

for t in thresholds: pred = sim_scores >= t p = sum(pred & y_true) / sum(pred) r = sum(pred & y_true) / sum(y_true) f1 = 2 * p * r / (p + r)

注意阈值依赖算法和数据分布。换一个数据集必须重新扫描,不能沿用旧值。

5.2 用 ngram_range 调整匹配粒度

TfidfVectorizerngram_range决定向量里是否包含相邻词组合。默认(1,1)只看单个词,“北京今天天气”和“今天北京天气”会被算得很相似,因为词袋相同。如果业务需要识别语序,就把参数改成(1,2),此时“北京今天”和“今天北京”是两个不同特征,顺序颠倒的句子相似度会下降。

但特征维度会随 ngram 组合数爆炸式增长。几万条语料在(1,2)下特征轻松超过十万维,训练耗时和内存占用都会明显增加。我一般的做法是短文本直接用(1,2),长文本先用(1,1)跑通基线,再实验性加上(2,2),观察相似度分布是否符合预期。

5.3 用已知答案的样本快速验证

调试时不要直接看真实数据,先构造几组期望值明确的文本对:

pairs = [ ("Python实现文本相似度分析", "Python实现文本相似度分析", 1.0), ("文本相似度分析Python实现", "Python实现文本相似度分析", 0.8), ("今天天气不错", "明天股市大涨", 0.0), ]

计算每一对的相似度并与期望对比。第一组结果不接近1说明预处理或向量化有Bug;第二组偏低说明ngram设置过松;第三组偏高说明停用词表太单薄。把这组测试作为回归用例放进代码库,每次改动后跑一遍,能有效避免预处理调整引起的隐性回归。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询