手写 TF-IDF 向量:文本匹配实测(Python 实现)
我处理过一份客户咨询记录,想用文本相似度自动归类,却发现直接用字符串匹配根本对不上。用 TF-IDF 向量计算余弦相似度后,原本看似不相关的句子在向量空间里距离很近,这才是检索系统该算的数。本文从零实现 TF-IDF 与余弦相似度,跑通完整流程,给出可复现代码和实测数据。
预处理与词频计算
先把文本转小写、去掉标点、按空格分词。这是最基础的步骤,但很容易被忽略。比如 “The cat sat on the mat” 预处理后变成 ['the', 'cat', 'sat', 'on', 'the', 'mat'],共 6 个词,其中 ‘the’ 出现 2 次,词频 TF(the) = 2/6 = 0.333。
四篇测试文档预处理后的词频如下:
| 文档 1 | the: 0.333, cat: 0.167, sat: 0.167, on: 0.167, mat: 0.167 |
| 文档 2 | the: 0.333, dog: 0.167, ran: 0.167, in: 0.167, park: 0.167 |
| 文档 3 | cats: 0.200, and: 0.200, dogs: 0.200, are: 0.200, pets: 0.200 |
| 文档 4 | mat: 0.167, and: 0.167, rug: 0.167, are: 0.167, floor: 0.167, coverings: 0.167 |
词频只反映词在当前文档中的出现频率,不考虑这个词在整个语料库中的重要性。“the” 在每个文档里都出现,光靠 TF 无法区分它的判别能力。
逆文档频率计算
IDF 的核心思想是:在越多文档里出现的词,越不重要。公式为 IDF(w) = log(N / (df(w) + 1)),其中 N 是文档总数,df(w) 是包含词 w 的文档数,+1 为平滑。
四篇文档共 4 个,部分词的 IDF 值如下:
| on | 1 | log(4/2) = 0.6931 |
| cat | 1 | 0.6931 |
| sat | 1 | 0.6931 |
| park | 1 | 0.6931 |
| in | 1 | 0.6931 |
| dog | 1 | 0.6931 |
| the | 2 | log(4/3) = 0.2877 |
| and | 2 | 0.2877 |
| are | 2 | 0.2877 |
可以看到,只在一个文档里出现的词 IDF 更高,在多个文档里反复出现的词 IDF 更低。这正是 IDF 要做的:降低常见词的权重,提升稀有词的权重。
TF-IDF 向量与余弦相似度
TF-IDF = TF × IDF。将每个词的 TF 乘以其 IDF,得到该词的 TF-IDF 权重。把所有词按固定词汇表排列,就得到文档的 TF-IDF 向量。
余弦相似度衡量两个向量方向的夹角,公式为 cos(θ) = (A·B) / (||A|| × ||B||)。值域 [-1, 1],实际应用中通常为 [0, 1],值越大表示越相似。
实测四篇文档的余弦相似度矩阵如下:
| 文档 1 | 1.0000 | 0.1619 | 0.0000 | 0.0467 |
| 文档 2 | 0.1619 | 1.0000 | 0.0000 | 0.0000 |
| 文档 3 | 0.0000 | 0.0000 | 1.0000 | 0.1005 |
| 文档 4 | 0.0467 | 0.0000 | 0.1005 | 1.0000 |
文档 1 与文档 2 相似度 0.1619,因为它们共享 “the” 但其他词不同;文档 1 与文档 4 相似度 0.0467,因为共享 “mat” 但整体差异大;文档 3 与文档 4 相似度 0.1005,因为共享 “and” 和 “are”。自相似度均为 1.0,符合预期。
查询示例
查询语句 “the cat sat on the mat” 与文档 1 完全相同,余弦相似度 1.0000;与文档 2 相似度 0.1619(共享 “the”);与文档 3 相似度 0.0000(无公共词);与文档 4 相似度 0.0467(共享 “mat”)。结果符合直观预期。
我怎么用它
在实际项目中用这套方法做过文本聚类,踩过几个坑:
停用词必须手动过滤。像 “the”、“a”、“is” 这类词 IDF 低、权重小,但如果数据量大,累积起来会影响距离计算。我加了中文停用词表后,相似度分布更合理。
词干提取不是必须。英文做词干收缩(如 “cats” → “cat”)能合并变体,但中文没有词干概念,分词质量直接影响结果。用简单的空格分词能跑通,但专业分词器效果更好。
向量维度爆炸。当语料库词汇表很大时,向量会很稀疏。我试过用 sklearn 的 TfidfVectorizer 做稀疏矩阵表示,内存占用降到原来的 1/10 以下,计算速度也更快。
相似度阈值需要调。不同语料下,0.5 的相似度可能意味着完全不同,也可能意味着高度相关。我通过人工标注样本,找到业务合适的阈值,而不是用固定值。
可落地动作
先写一个纯 Python 的 TF-IDF 实现,跑通预处理→TF→IDF→TF-IDF→向量→相似度的全流程,确保每一步输出可验证。
换用 sklearn 的 TfidfVectorizer 和 cosine_similarity 做对照,验证自己实现的正确性,差异控制在机器精度范围内。
在真实语料上测试相似度分布,人工标注一批样本,确定业务可用的相似度阈值,而不是直接用默认值。

