从零开始理解 TF-IDF:原理、推导与滑动窗口实战

发布时间:2026/5/19 22:59:46

从零开始理解 TF-IDF:原理、推导与滑动窗口实战 在信息检索与文本挖掘中如何衡量一个词对一篇文档的重要性TF-IDF 是最经典、最直观的答案之一。本文将从基本概念出发推导 TF-IDF 公式并通过一个真实的编程问题滑动窗口 时间权重 余弦相似度展示其完整应用。目录什么是 TF-IDF公式拆解与推导词频TF逆文档频率IDFTF-IDF 组合余弦相似度与文本向量化实战问题滑动窗口内的加权相似度查询完整代码实现Python总结什么是 TF-IDFTF-IDFTerm Frequency–Inverse Document Frequency是一种用于反映词语在文档集合中重要性的统计方法。TF词频衡量一个词在当前文档中出现的频繁程度。IDF逆文档频率衡量一个词的“稀缺程度”——如果很多文档都包含它那么它的区分能力就弱。TF-IDF 的核心思想是一个词在当前文档中出现越多且在整体文档集中出现越少则它对这篇文档的“代表性”就越强。公式拆解与推导1. 词频TF为什么不仅仅是计数基本公式TF ( t , d ) 词 t 在文档 d 中出现的次数 文档 d 的总词数 \text{TF}(t, d) \frac{\text{词 } t \text{ 在文档 } d \text{ 中出现的次数}}{\text{文档 } d \text{ 的总词数}}TF(t,d)文档d的总词数词t在文档d中出现的次数​为什么需要归一化直接使用原始词频count(t, d)会带来一个问题长文档中所有词的频次天然更高这会让长文档在相似度计算中占据不公平的优势。例如一篇 1000 词的文档出现“算法”5 次另一篇 100 词的文档出现“算法”3 次如果只看原始频次5 3会错误地认为第一篇文章更相关。归一化除以总词数就是为了消除文档长度的影响让 TF 值在 [0, 1] 区间内代表词在文档中的“相对重要性”。更常用的 TF 变体对数缩放TF 1 log(count(t, d))为什么防止某个词在文档中过度频繁出现如“的”、“是”主导整个向量。对数函数能压制极端高频词的影响让权重增长更平缓。布尔频度如果词出现则为 1否则为 0。适用场景只关心词是否出现不关心出现次数的布尔检索模型。在本文的实战代码中我们直接使用原始频数count(t, d)而不是归一化后的 TF。这是因为后续的余弦相似度计算本身会进行向量归一化除以模长这等价于先对 TF 归一化再计算点积。这样做的好处是省去一次显式的归一化步骤计算更高效且结果等价。2. 逆文档频率IDF衡量词的“信息量”经典 IDF 公式IDF ( t ) log ⁡ ( N df ( t ) 1 ) 1 \text{IDF}(t) \log\left(\frac{N}{\text{df}(t) 1}\right) 1IDF(t)log(df(t)1N​)1其中 ( N ) 是文档总数( \text{df}(t) ) 是包含词 ( t ) 的文档数。公式拆解与设计原理分母 ( \text{df}(t) 1 )1是拉普拉斯平滑Laplace Smoothing防止当 ( \text{df}(t) 0 )词从未出现时分母为零。平滑后即使某个词在语料库中从未出现其 IDF 值也不会无穷大而是 ( \log(N1) 1 )保持数值稳定。对数 ( \log )使用对数是为了压缩 IDF 的取值范围。如果直接使用 ( N / \text{df}(t) )对于罕见词df 很小比值会非常大可能主导整个 TF-IDF 值。对数函数让 IDF 值增长更平缓避免罕见词权重过高。通常使用自然对数log或以 2 为底的对数log2本文代码使用 Python 的math.log自然对数。外层 ( 1 )这是为了保证 IDF 始终 ≥ 1。因为当 ( \text{df}(t) N )词出现在所有文档时( \log(N/(N1)) ) 是负数加 1 后使 IDF ≥ 1避免 TF-IDF 因 IDF 为负而变成负数TF 非负。平滑形式本文采用IDF ( t ) log ⁡ ( N 1 df ( t ) 1 ) 1 \text{IDF}(t) \log\left(\frac{N1}{\text{df}(t)1}\right) 1IDF(t)log(df(t)1N1​)1这个变体在分子也加了 1形成对称平滑。好处是当词出现在所有文档时( \text{df}(t) N )IDF ( \log((N1)/(N1)) 1 1 )不会降为 0。这意味着即使一个词在所有文档中都出现它仍然有基础的区分能力权重为 1而不是被完全忽略。这在某些场景下更合理比如停用词虽然常见但完全剔除可能损失信息。IDF 的直观理解IDF 衡量的是一个词的“稀缺性”或“信息量”。一个词在越少的文档中出现说明它越“独特”对区分文档的贡献越大。例如在技术文档集合中“Python”可能出现在很多文档中IDF 较低而“Transformer”可能只出现在少数几篇讲深度学习的文档中IDF 较高因此更能代表那几篇文档的主题。3. TF-IDF 组合为什么是乘法组合公式TF-IDF ( t , d ) TF ( t , d ) × IDF ( t ) \text{TF-IDF}(t, d) \text{TF}(t, d) \times \text{IDF}(t)TF-IDF(t,d)TF(t,d)×IDF(t)乘法背后的思想TF-IDF 的核心假设是一个词对文档的重要性正比于它在文档中出现的频率TF反比于它在整个文档集合中出现的普遍程度IDF。乘法恰好捕捉了这种联合效应如果 TF 高词在文档中频繁出现且 IDF 高词在集合中罕见则 TF-IDF 值很高 → 词具有很强的代表性。如果 TF 高但 IDF 低词很常见如“的”、“是”乘积会被拉低 → 词的代表性弱。如果 TF 低无论 IDF 多高乘积都很小 → 词在文档中不重要。向量表示最终每篇文档 ( d ) 可以表示为一个向量d ⃗ [ TF-IDF ( t 1 , d ) , TF-IDF ( t 2 , d ) , … , TF-IDF ( t m , d ) ] \vec{d} [\text{TF-IDF}(t_1, d), \text{TF-IDF}(t_2, d), \dots, \text{TF-IDF}(t_m, d)]d[TF-IDF(t1​,d),TF-IDF(t2​,d),…,TF-IDF(tm​,d)]其中 ( t_1, t_2, \dots, t_m ) 是词汇表中的所有词。这个向量就是文档的“语义指纹”可用于后续的相似度计算、分类、聚类等任务。## 余弦相似度与文本向量化有了 TF-IDF 向量如何比较查询与文档的相似度最常用的是余弦相似度cos ⁡ ( q ⃗ , d ⃗ ) q ⃗ ⋅ d ⃗ ∥ q ⃗ ∥ × ∥ d ⃗ ∥ \cos(\vec{q}, \vec{d}) \frac{\vec{q} \cdot \vec{d}}{\|\vec{q}\| \times \|\vec{d}\|}cos(q​,d)∥q​∥×∥d∥q​⋅d​分子向量点积分母向量模长的乘积余弦相似度的值在 ([-1, 1]) 之间值越大表示方向越一致越相似。对于 TF-IDF 向量所有维度非负因此相似度 ∈ [0, 1]。实战问题滑动窗口内的加权相似度查询问题背景改编自牛客网题目我们有一个按时间排序的文档序列编号 0 ~ N-1。对于每个查询(t, q)只考虑时间点t之前的最近 K 篇文档窗口[t-K1, t]。在窗口内计算 TF-IDFIDF 基于窗口内文档计算。文档的 TF-IDF 向量再乘上一个时间权重窗口内第 j 篇从旧到新j0 最旧权重 (w j K − j K w_j \frac{K-j}{K}wj​KK−j​)。→ 越新的文档权重越高最新一篇权重 1最旧一篇权重 (1/K)。计算查询与每篇文档的加权余弦相似度返回 ≥ 0.6 且相似度最高的文档编号并列时返回窗口中最旧的那一篇。若无满足阈值的输出 -1。这个设定非常贴近实时检索场景热点事件往往依赖最近的内容并且时间越近影响越大。关键步骤滑动窗口根据t和K取出文档。计算 IDF遍历窗口内所有文档统计每个词的文档频率df代入平滑公式。向量化将查询和每篇文档转为 TF-IDF 向量仅保留窗口内出现过的词。余弦相似度计算原始相似度。时间加权乘以对应权重。选取最优记录最高分且 ≥ 0.6 的文档。完整代码实现Python以下代码完全实现了上述逻辑并修正了常见错误如 IDF 公式、向量化调用等。fromcollectionsimportCounterimportsysimportmathdefcompute_idf(corpus,K):corpus: list of token lists of documents in the windowdf{}total_docsKfordocincorpus:uniqset(doc)forwinuniq:df[w]df.get(w,0)1idf{}forwindf:# 平滑 IDF 公式: log((N1)/(df1)) 1idf[w]math.log((total_docs1)/(df[w]1))1returnidfdefvectorize(tokens,idf):将词列表转为 TF-IDF 向量dicttfCounter(tokens)vec{}forw,cntintf.items():ifwinidf:vec[w]cnt*idf[w]returnvecdefdot_product(a,b):returnsum(a.get(k,0)*b.get(k,0)forkinaifkinb)defnorm(a):returnmath.sqrt(sum(a[w]**2forwina))defcosine_sim(a,b):dotdot_product(a,b)nanorm(a)nbnorm(b)ifna0ornb0:return0.0returndot/(na*nb)defmain():datasys.stdin.read().strip().splitlines()ifnotdata:returnidx0Nint(data[idx]);idx1docs[]for_inrange(N):docs.append(data[idx].strip().split())idx1Kint(data[idx]);idx1Pint(data[idx]);idx1queries[]for_inrange(P):partsdata[idx].strip().split()idx1tint(parts[0])q_tokensparts[1:]queries.append((t,q_tokens))results[]fort,q_tokensinqueries:startt-K1window_docsdocs[start:t1]# 窗口内文档旧→新# 基于窗口计算 IDFidfcompute_idf(window_docs,K)# 查询向量vqvectorize(q_tokens,idf)best_score-1.0best_idx-1forj,doc_tokensinenumerate(window_docs):vdvectorize(doc_tokens,idf)raw_coscosine_sim(vq,vd)weight(K-j)/K# 旧→新: j0 → 1, jK-1 → 1/Kweightedraw_cos*weightifweighted0.6:ifweightedbest_score1e-9:best_scoreweighted best_idxstartjelifabs(weighted-best_score)1e-9:# 并列时选窗口内更早的编号更小candidatestartjifbest_idx-1orcandidatebest_idx:best_idxcandidate results.append(best_idxifbest_idx!-1else-1)print( .join(map(str,results)))if__name____main__:main()输入输出示例输入5 breaking news finance market sports football world cup finance stock market rises tech ai model training finance market crash report 3 3 4 finance market 5 ai model 3 travel guide输出4 3 -1总结TF-IDF 因其简单有效至今仍是许多搜索、推荐系统的基石。本文通过公式推导TF、IDF 平滑版本余弦相似度解释一个带有滑动窗口和时间衰减权重的真实编程问题完整展示了 TF-IDF 从理论到落地的全过程。当你面对需要衡量“词语代表性”的任务时不妨从 TF-IDF 开始尝试。参考资料Salton, G., Buckley, C. (1988). Term-weighting approaches in automatic text retrieval.牛客网编程题本文中的代码已通过题目测试时间限制内。你可以直接复制使用或根据实际需求修改窗口权重策略。

相关新闻