资讯动态

余弦相似度算法在文本相似度匹配中的原理与Python实现

发布时间:2026/9/15 2:43:54 来源:尧图企业网站定制
简介一个基于Python实现的文本相似度计算小项目通过简洁代码演示余弦相似度算法在中文文本比较上的应用适合自然语言处理入门者、算法学习者以及需要快速实现文本匹配功能的前后端开发者使用。项目共4个文件1个可直接运行的Python主程序、1个Markdown格式的Readme说明、1个License许可证以及1个Git忽略文件压缩包整体约3KB结构紧凑。目前已有4207人学习下载。主程序演示了文本相似度计算的完整链路先通过nltk对输入文本进行分词再使用sklearn的CountVectorizer构造词袋向量、TfidfVectorizer构造TF-IDF向量最后借助numpy计算两个向量的点积与范数从而得到余弦相似度。代码同时给出两种向量化方式的比对结果注释简洁便于理解分词、词表构建、向量空间表示等关键概念。通过阅读源码与说明读者可以快速掌握余弦相似度的原理与实现方式并将该脚本直接复用于文档去重、信息检索、推荐系统、智能问答等常见场景。1. 余弦相似度算法在文本相似度匹配中的第一个坑向量长度做文本去重时我碰到过一个很直观的现象两段摘要明显是同一主题按字符重合度看却不到一半用 Jaccard 也低得离谱。后来换成余弦相似度算法情况立刻变了——它计算的是两个词频向量的夹角余弦只关心方向不关心向量模长。也就是说一篇 50 字的短句和一篇 5000 字的长文只要词频方向一致相似度可以是 1。这个特性正是它在文本相似度场景里比编辑距离、Jaccard 更常用的原因。适合刚接触 Python 文本处理的人作为入门案例也适合在搜索引擎、推荐系统的召回阶段做初筛。2. 文本向量化分词、词表与词袋模型的距离2.1 分词决定了余弦相似度算法的上限余弦相似度算法本身不直接吃字符串它吃数值向量。所以第一步是把文本拆成可以计数的单元。中文可以用 jieba英文可以直接用 nltk.word_tokenize。这里有一个常见的误解分词不是越细越好。拆成单字会丢掉词组信息“北京”和“首都”在单字向量里完全不相关拆成整句又得不到稳定维度。我一般在简单实现里先把标点、换行过滤掉再用 jieba 的精确模式切词import jieba import re def tokenize(text): # 去掉非中英文和数字的字符避免标点干扰词频统计 text re.sub(r[^\w\u4e00-\u9fa5], , text) return [w for w in jieba.cut(text) if w.strip()]这段代码核心是两件事正则清理字符jieba 按词典切分。re.sub里的\w对英文和数字有效\u4e00-\u9fa5覆盖常用汉字过滤掉标点和特殊符号否则“。”会被当成一个词进入词表白白增加维度。分词之后的结果是一个词列表后续构建词表时需要对所有文档同时统计不能只对单条文本统计否则两个文本的向量维度对不上。2.2 CountVectorizer 与 TfidfVectorizer 的取舍向量化的常见做法有两种词袋模型BoW和 TF-IDF。BoW 统计词频实现简单但“的”“了”这类高频停用词会占据很大的权重TF-IDF 用词频乘逆文档频率降低跨文档都出现的词的权重让“余弦”“相似度”这类有区分度的词主导结果。下面这段代码把两条文本同时向量化from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer texts [ Python 余弦相似度算法计算文本相似度, 使用Python的余弦相似度分析文本 ] bow_vec CountVectorizer(tokenizertokenize) bow_matrix bow_vec.fit_transform(texts) tfidf_vec TfidfVectorizer(tokenizertokenize) tfidf_matrix tfidf_vec.fit_transform(texts) print(bow_vec.get_feature_names_out())这里的fit_transform分两步fit建立词表transform把每条文本映射成向量。bow_matrix是一个稀疏矩阵行对应文本列对应词表中的词。直接打印会看到(0, 2) 1这样的坐标形式这是 scipy 稀疏矩阵的存储方式不是 bug。后面的余弦相似度计算需要先.toarray()转成密集数组或者用能处理稀疏矩阵的sklearn.metrics.pairwise.cosine_similarity。实际项目中我通常优先选 TF-IDF因为它对高频无意义词的抑制是自动的不需要手工维护太细的停用词表。提示CountVectorizer默认的token_pattern只匹配字母数字中文会被整个丢掉所以必须显式传入tokenizer参数。维度CountVectorizerTfidfVectorizer权重含义词频词频 × 逆文档频率跨文档常见词权重偏高自动压低适用场景关键词出现次数本身有意义文本相似度、检索排序输出矩阵稀疏矩阵稀疏矩阵如果拿两个独立训练的向量去算相似度维度不同直接报错。工程里常见做法是拿全量语料先fit一次词表再把新来的文本只做transform。这个“先 fit 后 transform”的顺序经常被忽略。新手容易对每条文本单独调用fit_transform结果是每条文本的向量维度完全不同np.dot会抛出 shape mismatch。所以要保存一个tfidf_vec对象新文本进来时调用tfidf_vec.transform([new_text])词表保持训练时的样子。3. Python 实现余弦相似度核心算法从 numpy 到源码解析3.1 用 numpy 手写余弦相似度计算给定两个向量 A 和 B余弦相似度公式是(A·B) / (|A| × |B|)。使用 numpy 可以这样写import numpy as np def cosine_similarity(vec_a, vec_b): dot_product np.dot(vec_a, vec_b) norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) return dot_product / (norm_a * norm_b)np.dot计算逐个元素乘积再求和np.linalg.norm计算向量欧几里得范数也就是向量的长度。这个函数的隐含假设是norm_a和norm_b都不为 0。如果某个文本全被过滤成空字符串对应的向量全零np.linalg.norm返回 0分母会变成 0得到nan。在实际的simpe_text_cosine_similarity.py这类简单实现中通常会在函数入口判断一下if norm_a 0 or norm_b 0: return 0.0返回 0 表示其中一个文本没有有效特征和任何文本都不相似。这在代码逻辑上说得通但如果你面对的是一批短文本空向量比例可能很高需要留意 0 值的数量否则最终相似度矩阵会有一大片 0看不出真实差异。3.2 稀疏矩阵与密集数组的转换陷阱CountVectorizer返回的bow_matrix是scipy.sparse.csr_matrix直接传入np.dot时numpy 可能会把它当成对象数组处理结果不是期望的数值。常见做法是.toarray()转成二维数组再取第一行。下面是完整示例from sklearn.metrics.pairwise import cosine_similarity vec1 tfidf_matrix.toarray()[0] vec2 tfidf_matrix.toarray()[1] print(cosine_similarity([vec1], [vec2])[0][0])这里我用了 sklearn 内置的余弦相似度函数它内部对稀疏矩阵做了优化不会真的转成 dense。cosine_similarity接收二维数组每行是一个文本向量返回值是二维相似度矩阵。很多人在这段代码上踩坑直接传vec1和vec2两个一维数组返回值变成标量或者形状不一致。必须用[vec1]包一层让它变成1 × n的矩阵。这两种写法效果一样但性能差别明显。几万条文本做两两相似度时.toarray()会把稀疏矩阵膨胀成巨大的 dense 矩阵内存直接翻车。所以生产代码里我会直接用cosine_similarity(tfidf_matrix)它内部用稀疏矩阵乘法实现只计算非零位置的乘积速度能快一个数量级。在py3-cosine-similiarity-master这类小项目中通常把分词、向量化、相似度计算拆成三个函数。Readme.md里会写调用流程.gitignore排除虚拟环境。一个可复用的做法是把cosine_similarity封装成脚本入口接收两个文本路径或者命令行参数python simpe_text_cosine_similarity.py 文本A 文本B脚本内部依次调用tokenize、TfidfVectorizer、cosine_similarity最后输出0.87这样的浮点数。这个实现的关键不是函数多复杂而是数据格式在每一步都要对齐分词返回列表向量化返回稀疏矩阵相似度函数接收二维矩阵。只要这一步不统一后续所有封装都会报错。4. 停用词、词干提取与相似度阈值的工程调优4.1 预处理对余弦相似度算法结果的影响在基础实现跑通后相似度数字往往不符合直觉。比如“我用Python写爬虫”和“我爬虫用Python写”词频完全一样余弦相似度是 1这正确。但“Python写爬虫”和“用Python做数据分析”可能只有 0.2因为共享词太少。这时候第一反应不是调算法而是检查预处理。停用词表能显著改变结果stop_words {的, 了, 和, 与, 在, 是, 对, 中, 就, 都} def tokenize_with_stopwords(text): text re.sub(r[^\w\u4e00-\u9fa5], , text) return [w for w in jieba.cut(text) if w.strip() and w not in stop_words]加上停用词之后无意义的“的”“了”不再进入词表向量维度缩小和主题相关的词权重相对变大。但要小心停用词不一定都是虚词在特定领域“计算”“使用”这种高频词可能也不该参与相似度。工程上我用的是先看词频 Top 30再人工决定哪些词加入停用表。盲目套用网上停用词表会把领域关键词删掉。词干提取对英文有意义对中文一般不用。中文不存在running到run的形态变化但英文文本可以加PorterStemmer。如果语料是中英混合我通常会单独处理中文走 jieba英文走word_tokenize加SnowballStemmer最后把两个词列表拼起来。代码里要小心nltk.download(punkt)这个网络初始化步骤在离线环境会直接报错需要提前把 punkt 包放进 nltk_data。4.2 相似度阈值怎么定才不算瞎调阈值是文本相似度项目里最容易被问到的参数。0.8 以上才算相似不一定。取决于向量化方式和文本长度。文本越长非零维度越多余弦值普遍偏小。我做过一个短标题去重任务TF-IDF 向量的相似度分布集中在 0.10.40.5 以上明显是同一商品的变体。另一个长文档分类任务同类文档相似度也只有 0.4。所以正确的做法是先跑一批样本把相似度分布画出来再根据任务选择阈值。下面这个表格是常见的经验范围场景向量化方式经验阈值短文本去重TF-IDF0.8 以上新闻标题聚类BoW0.6 以上长文档相似TF-IDF0.4 以上推荐召回TF-IDF0.3 以上可入候选这个表不是标准答案它只是说明不同场景下同一套余弦相似度算法输出的分布完全不同。调阈值时至少要看一个批次的正负样本分布而不是拍脑袋定 0.8。如果分布图显示两个类别都堆在 0.3 附近就说明特征工程有问题改阈值没有意义。4.3 相似度计算失败时优先排查的三个点遇到nan先查零向量遇到维度不一致查 fit 和 transform 是否分开。遇到相似度全部为 0 则查分词是否成功特别是中文文本没有传tokenizer参数时CountVectorizer默认正则匹配不出中文词表为空结果自然全零。这三个点覆盖了我在这个简单实现里遇到的绝大多数问题。排查时在分词函数后加一行print(tokens)比反复计算相似度直观得多。5. 余弦相似度算法批量比较文本的向量化进阶写法5.1 一次调用算出所有文本两两相似度当文本数量从 2 条变成 2000 条仍然用双重循环调用cosine_similarity会非常慢而且代码难看。正确做法是直接把整个tfidf_matrix传给cosine_similarity一次得到 N×N 矩阵from sklearn.metrics.pairwise import cosine_similarity import numpy as np # tfidf_matrix 的形状是 (n_samples, n_features) sim_matrix cosine_similarity(tfidf_matrix) print(sim_matrix.shape) # 把对角线置 0方便找最大相似度 np.fill_diagonal(sim_matrix, 0) for i, row in enumerate(sim_matrix): j np.argmax(row) print(fsample {i} 最相似的是 sample {j}相似度 {row[j]:.3f})这里cosine_similarity会自动处理稀疏矩阵内部用矩阵乘法计算避免了 Python 层循环。np.fill_diagonal把每个文本和自身的相似度 1 清零否则argmax永远指向自己。输出时用 f-string 格式化相似度保留三位小数方便快速检查阈值。这个写法的计算量级是 O(n²)2000 条文本生成 400 万个数值内存大约 32MB尚可接受。如果文本量上到几万sim_matrix就会膨胀到几十 GB这时候要用 minhash 或 SimHash 做候选召回再用余弦相似度精排。这是另一个话题但你要清楚余弦相似度算法的边界它能处理中等规模两两比较不适合海量全连接。5.2 用 Top-K 相似结果验证阈值是否合理批量算完相似度后我通常会看每个样本的最相似 Top3 分布。如果绝大多数样本的最相似相似度都低于 0.2说明语料本身区分度差或者预处理过于激进。反之如果所有样本最相似度都接近 1说明文本几乎重复任务可能不需要相似度模型。随着文本数据积累这个验证步骤应该固化成脚本的一部分每次换语料都跑一遍。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价