news 2026/6/25 20:49:23

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始理解 TF-IDF:原理、推导与滑动窗口实战

在信息检索与文本挖掘中,如何衡量一个词对一篇文档的重要性?TF-IDF 是最经典、最直观的答案之一。本文将从基本概念出发,推导 TF-IDF 公式,并通过一个真实的编程问题(滑动窗口 + 时间权重 + 余弦相似度)展示其完整应用。

目录

  1. 什么是 TF-IDF?
  2. 公式拆解与推导
    • 词频(TF)
    • 逆文档频率(IDF)
    • TF-IDF 组合
  3. 余弦相似度与文本向量化
  4. 实战问题:滑动窗口内的加权相似度查询
  5. 完整代码实现(Python)
  6. 总结

什么是 TF-IDF?

TF-IDF(Term 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 变体

  1. 对数缩放TF = 1 + log(count(t, d))
    • 为什么:防止某个词在文档中过度频繁出现(如“的”、“是”)主导整个向量。对数函数能压制极端高频词的影响,让权重增长更平缓。
  2. 布尔频度:如果词出现则为 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 ) 的文档数。

公式拆解与设计原理

  1. 分母 ( \text{df}(t) + 1 )
    • +1拉普拉斯平滑(Laplace Smoothing),防止当 ( \text{df}(t) = 0 )(词从未出现)时分母为零。
    • 平滑后,即使某个词在语料库中从未出现,其 IDF 值也不会无穷大,而是 ( \log(N+1) + 1 ),保持数值稳定。
  2. 对数 ( \log )
    • 使用对数是为了压缩 IDF 的取值范围。如果直接使用 ( N / \text{df}(t) ),对于罕见词(df 很小)比值会非常大,可能主导整个 TF-IDF 值。对数函数让 IDF 值增长更平缓,避免罕见词权重过高。
    • 通常使用自然对数(log)或以 2 为底的对数(log2),本文代码使用 Python 的math.log(自然对数)。
  3. 外层 ( +1 )
    • 这是为了保证 IDF 始终 ≥ 1。因为当 ( \text{df}(t) = N )(词出现在所有文档)时,( \log(N/(N+1)) ) 是负数,加 1 后使 IDF ≥ 1,避免 TF-IDF 因 IDF 为负而变成负数(TF 非负)。

平滑形式(本文采用)
IDF ( t ) = log ⁡ ( N + 1 df ( t ) + 1 ) + 1 \text{IDF}(t) = \log\left(\frac{N+1}{\text{df}(t)+1}\right) + 1IDF(t)=log(df(t)+1N+1)+1
这个变体在分子也加了 1,形成对称平滑。好处是:

  • 当词出现在所有文档时(( \text{df}(t) = N )),IDF = ( \log((N+1)/(N+1)) + 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×dqd

  • 分子:向量点积
  • 分母:向量模长的乘积

余弦相似度的值在 ([-1, 1]) 之间,值越大表示方向越一致(越相似)。对于 TF-IDF 向量,所有维度非负,因此相似度 ∈ [0, 1]。


实战问题:滑动窗口内的加权相似度查询

问题背景(改编自牛客网题目)

我们有一个按时间排序的文档序列(编号 0 ~ N-1)。对于每个查询(t, q)

  • 只考虑时间点t之前的最近 K 篇文档(窗口[t-K+1, t])。
  • 在窗口内计算 TF-IDF(IDF 基于窗口内文档计算)。
  • 文档的 TF-IDF 向量再乘上一个时间权重:窗口内第 j 篇(从旧到新,j=0 最旧)权重 (w j = K − j K w_j = \frac{K-j}{K}wj=KKj)。
    → 越新的文档权重越高(最新一篇权重 1,最旧一篇权重 (1/K))。
  • 计算查询与每篇文档的加权余弦相似度,返回 ≥ 0.6 且相似度最高的文档编号;并列时返回窗口中最旧的那一篇。
  • 若无满足阈值的,输出 -1。

这个设定非常贴近实时检索场景:热点事件往往依赖最近的内容,并且时间越近影响越大。

关键步骤

  1. 滑动窗口:根据tK取出文档。
  2. 计算 IDF:遍历窗口内所有文档,统计每个词的文档频率df,代入平滑公式。
  3. 向量化:将查询和每篇文档转为 TF-IDF 向量(仅保留窗口内出现过的词)。
  4. 余弦相似度:计算原始相似度。
  5. 时间加权:乘以对应权重。
  6. 选取最优:记录最高分且 ≥ 0.6 的文档。

完整代码实现(Python)

以下代码完全实现了上述逻辑,并修正了常见错误(如 IDF 公式、向量化调用等)。

fromcollectionsimportCounterimportsysimportmathdefcompute_idf(corpus,K):"""corpus: list of token lists of documents in the window"""df={}total_docs=Kfordocincorpus:uniq=set(doc)forwinuniq:df[w]=df.get(w,0)+1idf={}forwindf:# 平滑 IDF 公式: log((N+1)/(df+1)) + 1idf[w]=math.log((total_docs+1)/(df[w]+1))+1returnidfdefvectorize(tokens,idf):"""将词列表转为 TF-IDF 向量(dict)"""tf=Counter(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):dot=dot_product(a,b)na=norm(a)nb=norm(b)ifna==0ornb==0:return0.0returndot/(na*nb)defmain():data=sys.stdin.read().strip().splitlines()ifnotdata:returnidx=0N=int(data[idx]);idx+=1docs=[]for_inrange(N):docs.append(data[idx].strip().split())idx+=1K=int(data[idx]);idx+=1P=int(data[idx]);idx+=1queries=[]for_inrange(P):parts=data[idx].strip().split()idx+=1t=int(parts[0])q_tokens=parts[1:]queries.append((t,q_tokens))results=[]fort,q_tokensinqueries:start=t-K+1window_docs=docs[start:t+1]# 窗口内文档(旧→新)# 基于窗口计算 IDFidf=compute_idf(window_docs,K)# 查询向量vq=vectorize(q_tokens,idf)best_score=-1.0best_idx=-1forj,doc_tokensinenumerate(window_docs):vd=vectorize(doc_tokens,idf)raw_cos=cosine_sim(vq,vd)weight=(K-j)/K# 旧→新: j=0 → 1, j=K-1 → 1/Kweighted=raw_cos*weightifweighted>=0.6:ifweighted>best_score+1e-9:best_score=weighted best_idx=start+jelifabs(weighted-best_score)<1e-9:# 并列时选窗口内更早的(编号更小)candidate=start+jifbest_idx==-1orcandidate<best_idx:best_idx=candidate 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.
  • 牛客网编程题

本文中的代码已通过题目测试(时间限制内)。你可以直接复制使用,或根据实际需求修改窗口权重策略。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 19:27:34

当贝盒子H5 64G版618首销TOP1!多平台登顶,凭什么这么火?

2026年5月14日&#xff0c;当贝官方发布了618抢先购首日当贝盒子H5 64G版的首销战报。据官方数据显示&#xff0c;这款重磅升级的电视盒子在京东、天猫、抖音三大主流电商平台的电视盒子类目热销榜中&#xff0c;全部拿下TOP1席位&#xff0c;成为今年618大促第一天的现象级爆款…

作者头像 李华
网站建设 2026/6/23 19:27:35

STM32单片机串口通信避坑指南:从CubeMX配置到中断回调函数编写

STM32单片机串口通信避坑指南&#xff1a;从CubeMX配置到中断回调函数编写 在嵌入式开发领域&#xff0c;UART串口通信堪称最基础却又最容易"踩坑"的技术之一。无论是参加蓝桥杯等竞赛的学生&#xff0c;还是工业项目的开发工程师&#xff0c;几乎都会遇到各种串口通…

作者头像 李华
网站建设 2026/6/23 19:27:34

留学生面试反问定生死?2026大厂反向背调与排雷指南

当一场长达 45 分钟的高压技术面试临近尾声&#xff0c;面试官往往会抛出那句经典的结语&#xff1a;“你有什么问题想问我吗&#xff1f;” 在 2026 年的科技求职局中&#xff0c;如果你回答“没有”&#xff0c;或者问出“公司提供免费午餐吗”、“大概什么时候出结果”这类毫…

作者头像 李华
网站建设 2026/6/23 19:27:53

LibSVM在Matlab里的实战:从分类到回归,手把手调参与结果解读

LibSVM在Matlab里的实战&#xff1a;从分类到回归&#xff0c;手把手调参与结果解读 当你第一次在Matlab中成功运行LibSVM时&#xff0c;看到命令行窗口跳出"Accuracy 86.6667%"的那一刻&#xff0c;可能既兴奋又困惑。兴奋的是工具终于跑通了&#xff0c;困惑的是那…

作者头像 李华