2、TF-IDF算法:词频(TF)计算、逆文档频率(IDF)计算、TF-IDF公式详解、代码实现

2.1 先聊聊TF-IDF到底在干嘛

TF-IDF,说白了就是给每个词打个分。

你想想看,搜索引擎拿到你的查询词,比如「苹果手机」,它得知道哪些文档跟这个词最相关。最简单的想法是:哪个文档里「苹果」和「手机」出现次数多,哪个就更相关。但问题来了——「的」、「是」、「在」这些词几乎每篇文档都有,它们能算数吗?

嗯,这就是TF-IDF要解决的核心问题。

我个人习惯把TF-IDF拆成两半来理解:

  • TF(词频):这个词在当前文档里重不重要?
  • IDF(逆文档频率):这个词在整个文档集合里有没有区分度?

两者一乘,就是这个词对这篇文档的「代表性」有多强。

核心思想:一个词在单篇文档中出现次数越多,同时在整个文档集合中出现次数越少,它就越能代表这篇文档的主题。

2.2 词频(TF)计算

TF的计算方式有好几种,我挑最常用的三种来说。

2.2.1 原始词频

最简单粗暴的方式:

TF(t, d) = 词t在文档d中出现的次数

比如文档「我喜欢苹果,也喜欢香蕉」里,「喜欢」出现了2次,TF就是2。但这样有个问题——长文档天然占便宜。一篇5000字的文章和一篇200字的文章,同样的词出现次数肯定不一样。

2.2.2 归一化词频(我推荐这种)

我在项目中遇到过这种情况:两篇文档长度差10倍,直接用原始词频排序,结果全是长文档排前面。后来我改用归一化:

TF(t, d) = 词t在文档d中出现的次数 / 文档d的总词数

这样就把长度差异抹平了。比如「喜欢」在200字文档里出现2次,TF=0.01;在5000字文档里出现10次,TF=0.002。短文档反而得分更高,这才合理。

2.2.3 对数归一化

还有一种更平滑的方式:

TF(t, d) = log(1 + 词t在文档d中出现的次数)

为什么要加log?因为词频的增长不是线性的。你想想看,一个词出现10次和出现100次,重要性差距并没有10倍那么大。log函数正好能压住这种暴增。

我的建议:做通用搜索时用归一化词频就够了。如果文档长度差异特别大(比如有的几KB,有的几MB),可以考虑对数归一化。

2.3 逆文档频率(IDF)计算

IDF解决的是另一个问题:有些词太常见了,比如「的」、「是」、「在」,它们在几乎所有文档里都出现。这些词对区分文档没什么帮助。

公式长这样:

IDF(t) = log(总文档数N / 包含词t的文档数df(t))

我来拆解一下:

  • N:文档集合的总数
  • df(t):包含词t的文档数量

如果一个词在所有文档里都出现,df(t)=N,IDF=log(1)=0。这个词的权重直接归零——完美!

如果一个词只在极少数文档里出现,df(t)很小,IDF就很大。比如「Transformer」这个词,在AI领域的文档里常见,但在新闻文档里很少见,它的IDF值就会很高。

曾经踩过的坑:我曾经在处理一个百万级文档集时,发现有些生僻词df(t)=0,导致IDF公式里分母为0。后来我加了一个平滑项:IDF(t) = log((N+1)/(df(t)+1)) + 1。这样既避免了除零,又保证了IDF始终为正。

2.4 TF-IDF公式详解

把TF和IDF乘起来,就是TF-IDF:

TF-IDF(t, d) = TF(t, d) × IDF(t)

这个值越大,说明词t对文档d越重要,同时在整个文档集合里越有区分度。

举个例子你就明白了:

文档 「苹果」出现次数 总词数 TF IDF(假设1000篇文档中50篇含「苹果」) TF-IDF
水果介绍 10 200 0.05 log(1000/50)=3.0 0.15
科技新闻 3 300 0.01 log(1000/50)=3.0 0.03
通用文档 1 500 0.002 log(1000/50)=3.0 0.006

你看,「苹果」在水果文档里的TF-IDF值最高,说明它最能代表这篇文档的主题。科技新闻里虽然也有「苹果」,但出现频率低,得分就低。

关键点:TF-IDF不是万能的。它假设词之间相互独立,不考虑词序和语义。说白了,它就是个基于统计的「词袋模型」。但对于很多搜索场景,它已经够用了。

2.5 代码实现

理论说完了,咱们直接上代码。我习惯用Python实现,因为简洁明了。

2.5.1 从零实现TF-IDF

import math
from collections import Counter

class TFIDF:
    def __init__(self, documents):
        self.documents = documents
        self.N = len(documents)
        self.df = {}  # 文档频率
        self.idf = {}  # 逆文档频率
        self._compute_df()
        self._compute_idf()
    
    def _compute_df(self):
        """计算每个词的文档频率"""
        for doc in self.documents:
            words = set(doc.split())  # 去重,只统计是否出现
            for word in words:
                self.df[word] = self.df.get(word, 0) + 1
    
    def _compute_idf(self):
        """计算每个词的IDF值"""
        for word, df_value in self.df.items():
            # 加1平滑,避免除零
            self.idf[word] = math.log((self.N + 1) / (df_value + 1)) + 1
    
    def compute_tf(self, doc):
        """计算文档的词频"""
        words = doc.split()
        total_words = len(words)
        tf = {}
        for word, count in Counter(words).items():
            tf[word] = count / total_words  # 归一化词频
        return tf
    
    def compute_tfidf(self, doc):
        """计算文档的TF-IDF向量"""
        tf = self.compute_tf(doc)
        tfidf = {}
        for word, tf_value in tf.items():
            if word in self.idf:
                tfidf[word] = tf_value * self.idf[word]
            else:
                tfidf[word] = 0  # 未出现过的词,IDF为0
        return tfidf

# 使用示例
docs = [
    "苹果 是 一种 水果",
    "苹果 公司 发布 了 新 手机",
    "香蕉 和 苹果 都 是 水果"
]

model = TFIDF(docs)
for i, doc in enumerate(docs):
    result = model.compute_tfidf(doc)
    print(f"文档{i+1}的TF-IDF: {result}")

2.5.2 用scikit-learn实现(生产环境推荐)

from sklearn.feature_extraction.text import TfidfVectorizer

docs = [
    "苹果 是 一种 水果",
    "苹果 公司 发布 了 新 手机",
    "香蕉 和 苹果 都 是 水果"
]

# 创建TF-IDF向量化器
vectorizer = TfidfVectorizer()

# 拟合数据并转换
tfidf_matrix = vectorizer.fit_transform(docs)

# 查看结果
print("特征词:", vectorizer.get_feature_names_out())
print("TF-IDF矩阵:\n", tfidf_matrix.toarray())

我的经验:生产环境里我一般直接用scikit-learn的TfidfVectorizer。它内置了平滑、归一化、停用词过滤等功能,比自己手写稳定得多。但理解底层原理很重要——出了问题你能快速定位。

2.6 TF-IDF的局限性

说实话,TF-IDF在2000年代很火,但现在很多场景已经被更先进的算法取代了。我总结几个明显的短板:

  • 忽略语义:「苹果」和「iPhone」在语义上相关,但TF-IDF认为它们是两个完全独立的词
  • 不考虑词序:「苹果手机」和「手机苹果」在TF-IDF眼里是一样的
  • 对长文档有偏:虽然归一化缓解了这个问题,但长文档包含更多词,整体得分还是会偏高

不过话说回来,TF-IDF简单、高效、可解释性强。很多中小型搜索系统,或者作为基线模型,它依然是个不错的选择。我至今还在用TF-IDF做关键词提取和文本相似度计算的快速原型。

一句话总结:TF-IDF用词频和逆文档频率的乘积,衡量一个词对文档的重要性。它不完美,但足够实用。