
简介面向中文自然语言处理初学者与Python开发者这份单文件PDF通过一个可运行的完整案例讲解如何借助jieba分词与gensim库完成文本相似度分析。资源从语料准备、中文分词、词袋模型构建讲到TF-IDF建模与基于余弦相似度的文档排序覆盖了从原始文本到相似度结果输出的完整链路。包体仅63KB便于作为速查笔记或实验参考目前已有3060人学习。案例中以8个目标文档和1个测试文档进行对比详细演示Dictionary、doc2bow、TfidfModel、Similarity等核心接口的用法并展示了每个词的TF-IDF权重与最终相似度排序帮助读者直观理解权重计算和向量空间模型的实际效果同时特意指出未剔除停用词会对结果产生干扰提醒实战中应补充去停用词、词形还原等预处理步骤。对于希望快速入门文本相似度分析、希望读懂同类项目代码的读者是内容紧凑且可对照运行的实用参考资料。1. 文本相似度分析到底在算什么先把话放这儿文本相似度分析不是把两串字符拿过来比相等而是把“接近”这件事变成可计算的距离。数据清洗、评论去重、搜索召回、论文查重、客服工单聚类底层都在用它。很多人以为这是 NLP 里一个又深又重的方向实际上用好 Python 的 jieba 和 scikit-learn不碰任何深度学习框架就能搭出一条可用的相似度计算链路。这篇文章要做的是把分词、清洗、向量化、相似度计算到 TopN 筛选的完整操作一步步讲透并标出哪些参数值得调、哪些坑必须躲。适合 Python 入门到进阶之间的读者尤其是手上有爬虫数据、正想做文本去重或聚类的开发者。环境不用复杂按平时写脚本的方式配好 Python 解释器就行vscode 还是 pycharm 都无所谓关键是装的库能被同一个解释器识别到。2. 相似度算法选型Jaccard、余弦与编辑距离的适用边界文本相似度没有唯一正确的算法先选算法再写代码。因为不同算法对“相似”的定义完全不同——有的只看字符重合有的看词频权重有的看语义距离。这一章先把三种最常见算法的计算方式和使用边界讲清楚后面章节再落到 Python 实现。2.1 Jaccard 相似度集合运算为什么适合短文本与关键词去重Jaccard 相似度把文本切成词的集合用交集大小除以并集大小公式是|A ∩ B| / |A ∪ B|。这个算法不看词序、不看词频只关心重合的词占多大比例。它的优点是实现成本极低计算速度非常快缺点是两段文本如果用了同义词但词面完全不同相似度会直接掉到 0。实际操作里Jaccard 很适合商品标题关键词比对、标签系统去重、短信验证码这类短文本场景。比如两条文本“Python 爬虫入门”和“爬虫入门 Python”分词后得到相同集合Jaccard 值就是 1.0。但如果应用场景换成一篇文章的段落比对仅靠词集合就太粗糙了需要引入词频和权重。算法计算对象是否考虑词序典型场景耗时特点Jaccard词或 n-gram 集合否短文本、关键词去重低编辑距离字符序列是拼写纠错、OCR 结果比对随文本长度快速上升余弦相似度TF-IDF 向量否依赖 n-gram搜索召回、正文查重中适合批处理词向量词嵌入聚合部分语义匹配、问答高需预训练模型2.2 编辑距离与 Levenshtein适合错字修正而非整体相似编辑距离衡量的是把一个字符串变成另一个字符串所需的最少编辑操作次数操作包括插入、删除和替换。它天然考虑字符顺序所以对“Python”和“Pythonn”这类拼写差异非常敏感。Levenshtein 距离是它的经典实现通常用动态规划求解。但这个算法有两个明显的短板。一是时间复杂度接近 O(m×n)两条几百字的文本做一次计算就非常慢二是它对“同一件事的不同说法”束手无策。比如“这台手机续航不行”和“电池掉电太快”字符层面几乎是零重合编辑距离会给出一个很大的值。因此在文本相似度分析里编辑距离更适合做辅助手段比如先通过编辑距离判定是否属于同一商品的不同 SKU 描述再用余弦相似度做整体判断。2.3 余弦相似度与 TF-IDF为什么它是文本相似度分析的默认基线如果要给“文本相似度分析”选一个默认方案我会选 TF-IDF 向量化加余弦相似度。所谓向量化是把每条文本表示成一个向量每个维度对应一个词。TF-IDF 给每个词赋权重TF 是词在该文本中的出现频率IDF 是逆文档频率用来压制那些在大量文档里都出现的高频词。余弦相似度计算的是两个向量的夹角余弦值取值在 -1 到 1 之间文本相似度场景通常落在 0 到 1。相比 Jaccard它充分利用了词频信息相比编辑距离它对文本长度不敏感也不会因为个别字符差异导致相似度崩坏。后面第 4 章的完整实现就是围绕这个组合展开的这也是搜索系统和推荐系统在召回阶段的常用算法之一。3. 文本预处理与中文分词的实操细节文本相似度分析里预处理对结果的影响不亚于算法本身。中文没有天然空格分词结果的每一个边界都直接影响后续相似度计算。这一章讲三段可复用的预处理代码分词、清洗、停用词过滤。3.1 jieba 分词精确模式与全模式输出的差异中文文本必须先分词。jieba 是目前最常用的 Python 中文分词库安装命令是pip install jieba安装前确认你的 Python 环境里没有重复装到别的解释器上。分词模式常用两种精确模式cut_allFalse和全模式cut_allTrue。import jieba text 自然语言处理在文本相似度分析中应用广泛 # 精确模式返回最合理的分词结果适合相似度计算 print(list(jieba.cut(text, cut_allFalse))) # 全模式把所有可能的词都切出来词之间有重叠适合做召回扩展 print(list(jieba.cut(text, cut_allTrue)))精确模式下输出接近[自然语言, 处理, 文本, 相似度, 分析, 中, 应用, 广泛]全模式会额外切出“自然”“语言”“处理在”等重叠片段。相似度计算建议只用精确模式因为全模式的重复片段会放大某些词在向量里的权重。jieba 还支持jieba.enable_paddle()调用 Paddle 模型对长句的分词效果更好但首次运行要加载模型批处理小规模语料时收益不明显我一般只在语义匹配场景才开。3.2 清洗规则把 URL、数字、标点排除在相似度之外原始文本里经常混着 URL、数字、特殊符号。这些内容不参与语义表达却会干扰向量计算。尤其是两条文本都带同一串 URL 时相似度会被虚高。编程里我一般按下面的顺序清洗先统一小写再去掉 URL再处理数字和符号最后压缩空白。import re def clean_text(text): text text.lower() # 去掉 http/https 开头的链接 text re.sub(rhttps?://\S|www\.\S, , text) # 数字单独出现的场景保留价值低替换成空格 text re.sub(r\d, , text) # 只保留中文、英文字母和数字其他符号全部换空格 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) # 多个空格合并成一个 text re.sub(r\s, , text).strip() return text这里的关键参数是第二和第三个正则。https?://\S匹配以 http 或 https 开头的链接\S吃到第一个空白为止[^\u4e00-\u9fa5a-zA-Z0-9]是取反字符集意思是非中文、非英文、非数字的字符全部替换成空格。全角符号如果频繁出现可以用unicodedata.normalize(NFKC, text)先做规范化把全角转半角再走上面的流程。3.3 停用词表哪些词该删删除后再分词的差异停用词指“的、了、在、是、和”这类高频但语义贡献极低的虚词。文本相似度分析对停用词过滤的敏感度比文本分类更高因为这些词在两条文本里同时出现会显著抬高重合度。实务上先分词再去停用词不要反过来做。原因是一个词是否该保留取决于它在句子里的上下文提前删词会破坏切分边界。def load_stop_words(path): # 每行一个停用词编码用 utf-8 with open(path, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) stop_words load_stop_words(stopwords.txt) def filter_stop_words(words, stop_words): # 去掉空字符串和纯空白词 return [w for w in words if w not in stop_words and w.strip()]停用词表建议自己维护不要直接套用某个 NLP 项目的超大词表因为领域不同有些词比如“数据”“系统”在通用词典里不是停用词但在你的语料里可能高频且无区分度。更精细的做法是加载 jieba 词性标注后只保留名词、动词、形容词把副词、介词、语气词整体丢弃。这一步能显著提升后面相似度结果的可解释性。4. 用 TfidfVectorizer 计算余弦相似度的 Python 实现这一章给出完整可运行的 Python 实现。核心链路是原始文本 → 清洗 → 分词 → 去停用词 → TF-IDF 向量化 → 余弦相似度矩阵 → 输出 TopN 相似句对。sklearn 和 numpy 负责主要计算pip install scikit-learn numpy即可不需要其他重量级依赖。4.1 语料组织与 TfidfVectorizer 的 6 个常用参数sklearn 的TfidfVectorizer不负责分词所以传给它的每条文本必须是已经分词并用空格连接好的字符串。这也是新手最容易犯错的地方直接把原始中文文本丢进去得到的是一个按单字切分的向量空间。正确姿势如下。from sklearn.feature_extraction.text import TfidfVectorizer corpus [ 自然语言处理是人工智能的重要方向, 自然语言处理在人机对话中很关键, Python 写爬虫时需要处理文本编码, 物理引擎的碰撞检测依赖向量运算, ] # 模拟第 3 章的预处理链路 corpus_seg [ .join(filter_stop_words(jieba.cut(clean_text(t)), stop_words)) for t in corpus] vectorizer TfidfVectorizer( ngram_range(1, 2), max_features5000, sublinear_tfTrue, min_df1 ) tfidf_matrix vectorizer.fit_transform(corpus_seg)这 6 个参数是实际调参中最常用的组合逐个说明ngram_range(1, 2)表示向量维度同时包含单个词和相邻两个词构成的短语对中文这种复合词较多的语言有帮助代价是特征维度会明显变多。max_features5000限制特征总数按词频保留前 5000 个防止小语料造出几十万维稀疏向量。sublinear_tfTrue对词频做1 log(tf)变换避免某个词在长文档里重复出现导致权重虚高。min_df1表示只出现一次的词也保留如果语料较大可调到 2。stop_words参数可以直接传停用词集合但我一般在上游显式过滤因为显式过滤后还能复用分词结果。4.2 用 cosine_similarity 构建相似度矩阵与 TopN 提取向量化完成后调用cosine_similarity计算两两相似度。它接受矩阵后返回一个 n×n 的对称矩阵第 i 行第 j 列的值就是第 i 条和第 j 条文本的相似度。import numpy as np from sklearn.metrics.pairwise import cosine_similarity sim_matrix cosine_similarity(tfidf_matrix) n len(corpus) results [] for i in range(n): for j in range(i 1, n): results.append((i, j, sim_matrix[i][j])) top sorted(results, keylambda x: x[2], reverseTrue)[:3] for a, b, s in top: print(fdoc {a} - doc {b}: {s:.4f})逻辑说明上半角遍历只用i j对角线上的自身相似度直接用cosine_similarity计算后不需要再用因为 text 与自己的余铉必然是 1.0。sorted按第三位相似度值降序取前 3 就是最相似的三对。时间复杂度是 O(n²)语料只有几百条时完全没有压力如果到万级就要考虑第 6 章的粗筛方案。4.3 一个完整的相似度分析脚本把上面代码整理成函数输入一个文本列表输出相似度最高的若干对。import re import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def clean_text(text): text text.lower() text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r\d, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return re.sub(r\s, , text).strip() def preprocess(text, stop_words): words jieba.cut(clean_text(text)) return .join(w for w in words if w not in stop_words and w.strip()) def topn_similar(texts, stop_words, topn5, max_features5000): corpus [preprocess(t, stop_words) for t in texts] vec TfidfVectorizer(ngram_range(1, 2), max_featuresmax_features, sublinear_tfTrue) tfidf vec.fit_transform(corpus) sim cosine_similarity(tfidf) pairs [] for i in range(len(texts)): for j in range(i 1, len(texts)): pairs.append((i, j, sim[i][j])) return sorted(pairs, keylambda x: x[2], reverseTrue)[:topn] if __name__ __main__: texts [ 自然语言处理是人工智能的重要方向, 自然语言处理在人机对话中很关键, Python 写爬虫数据时要处理文本编码, ] for i, j, s in topn_similar(texts, stop_words{的, 了, 在, 是}): print(f文本{i} 与文本{j} 相似度为 {s:.4f})脚本中preprocess把清洗、分词、去停用词收敛到了一个函数里方便复用。注意topn不能大于总对数否则取不满结果实际项目里可以先判断len(pairs) topn再截断避免越界。5. 参数调优与高频排错从零向量到 OOV 的五个坑算法跑通只是第一步。真实语料里会遇到文本被清空、词表外词汇、相似度全部为 NaN 等问题。这一章梳理最常见的五类异常及其排查思路并给出一个场景化参数速查表。5.1 全零向量与 NaN当文本被清空后余弦值如何崩坏清洗和停用词过滤不当时某些短文本可能被清理成空字符串。空文本分词后得到空列表再拼接成空字符串进入TfidfVectorizer该行就会变成全零向量。全零向量与任何向量做余弦相似度都会产生 NaN因为公式分母里的向量模长是 0。处理方案有两种。一是预处理阶段记录日志统计每次清理后的分词结果数量这能快速定位是哪一条文档出了问题。二是对确实为空的文本做占位处理比如统一替换成EMPTY标记在筛选结果时再把这些行过滤掉。def safe_preprocess(text, stop_words): seg .join(...) return seg if seg.strip() else EMPTY5.2 中文词向量 OOV 与语料规模不匹配如果相似度需求从字面匹配升级到语义匹配很多人会立刻换 Word2Vec觉得 TF-IDF 过于“笨”。但在领域语料规模不大时预训练词向量会遇到大量词表外词汇OOV比如“熔断机制”“私域运营”这类领域新词在通用词表里根本没有 embedding。此时词向量方案的效果可能反而不如 TF-IDF。建议的落地顺序是先用 TF-IDF 加余弦相似度做基线确认相似度分布是否合理只有基线明显漏掉同义改写文本时再引入词向量且要对 OOV 做单独处理比如回退到单字向量或直接丢弃该词。短文本、领域词多的场景TF-IDF 往往更稳。5.3 高频词干扰与 n-gram 参数取舍max_features只按词频排序保留前 N 个特征但高频词不一定是有区分度的词。比如“数据”“分析”在技术文档里可能每条都出现如果不做过滤它们会拉近所有包含这些词的文档。处理办法是提高min_df或使用自定义停用词表。这里整理一份参数速查表你可以按场景直接套。场景分词模式ngram_range首选算法注意点商品标题去重精确模式(1, 2)Jaccard 或余弦先过滤品牌词和虚词长文本查重精确模式(1, 1)TF-IDF 余弦按段落分块后再比对评论聚类精确模式(1, 2)余弦控制 max_features 防稀疏语义匹配paddle 模式(1, 1)词向量语料小不要强行上预训练模型最后补充一个低频坑TfidfVectorizer的默认token_pattern是针对英文设计的如果你没先用空格连接中文词它会按单个汉字切分导致结果完全不可用。遇到相似度矩阵里所有非对角线值都很低时第一件事检查vectorizer.get_feature_names_out()输出的是词还是字。6. 工程化落地大规模文本去重场景的粗筛与精排最后讲一个实际项目里一定会遇到的问题数据量过万后O(n²) 的全量相似度计算会变得不可接受。工程上的通用做法是粗筛加精排——先用廉价方法选出候选对再用余弦相似度精算排序。6.1 粗筛层用倒排索引大幅缩小候选集粗筛的核心思路是两条完全不共享任何词的文本相似度不可能高。先对每条文本的分词结果建倒排索引词作为 key文本 ID 列表作为 value再对出现在同一个词下的文本 ID 做两两组合生成候选对。只有候选对才进入余弦相似度计算。from collections import defaultdict def build_candidate_pairs(doc_words, max_skip100): # doc_words: list of list每条文本的分词结果 inverted defaultdict(list) for doc_id, words in enumerate(doc_words): for w in set(words): inverted[w].append(doc_id) candidates set() for doc_ids in inverted.values(): for i in range(len(doc_ids)): for j in range(i 1, len(doc_ids)): if doc_ids[j] - doc_ids[i] max_skip: candidates.add((doc_ids[i], doc_ids[j])) return candidates这里的max_skip控制候选对的滑动窗口大小。文档 ID 是按原始顺序递增的如果两个 ID 相差过大说明它们所在位置跨度很大先跳过真正需要计算的是相邻区域内共享同一词的文档。实际应用中倒排索引通常能把候选集压缩到全量组合的 1% 以下。6.2 精排阈值怎么标定小批量抽样画 Precision 曲线粗筛之后精排层返回的分数需要定一个阈值高于阈值才能判定为相似。不要拍脑袋填 0.8。我一般会在 Top 相似结果里随机抽 50 对做人工标注标出“是否算相似”然后按不同阈值计算精确率选择精确率掉到 95% 以下的临界值作为阈值。这种方式成本低且能适应不同语料的主观尺度。阈值确定后把相似句对落库即可。无论是存到 MySQL 里的(id_a, id_b, score)表还是落到 Parquet 供后续聚类使用最终效果都取决于你前面每一步的清洗与参数选择。文本相似度分析的价值不在算法炫技而在把名词变成可调、可验、可靠的加工过程。本文还有配套的精品资源点击获取