ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python实现文本相似度分析:算法对比与工程实践

Python实现文本相似度分析:算法对比与工程实践 简介这份资源是一份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这个第三方库里面提供了distance和ratio两个接口。这类算法对 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_pattern是r(?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(i1, 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.667TF-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 调整匹配粒度TfidfVectorizer的ngram_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设置过松第三组偏高说明停用词表太单薄。把这组测试作为回归用例放进代码库每次改动后跑一遍能有效避免预处理调整引起的隐性回归。本文还有配套的精品资源点击获取
返回列表