
在使用TF-IDF方法时,你是否遇到过这样的问题:
这并不是谁算错了。
结论先给出:你选择的 TF-IDF 实现方式,可能并不适合你的应用场景。TF-IDF 并不是一个“唯一算法”,而是一类加权策略的统称。不同实现对 TF、IDF、归一化的处理方式不同,语义也随之发生变化。
下面我们通过一个真实例子来说明这一点。
我们选取 2021–2023 年人民日报语料,每一年作为一篇“文档”,目标是:
为每一年提取最能代表该年度特征的关键词。
我们希望得到“年度主题词”,而不是“全年高频词”。
接下来,我们用三种方式计算 TF-IDF:
TfidfModelTfidfVectorizer接我们先来看看这两年的词频(隐去敏感词汇):

公式完全遵循教材定义:
当一个词在所有年份都出现时:
于是:
核心代码:
# 计算文档频率doc_freq = Counter(word for words_dict in news_df["word_counts"] for word in words_dict)# 预计算IDFdocs_num = len(news_df)idfs = {word: np.log(docs_num / freq) for word, freq in doc_freq.items()}def calculate_tf_idf(words_dict): """计算TF-IDF""" total = sum(words_dict.values()) return {word: (count / total) * idfs[word] for word, count in words_dict.items()}news_df["normal_tf_idf"] = news_df["word_counts"].apply(calculate_tf_idf)得到的 Top 词是:

这些词具有一个共同特征:
它们在当年频繁出现,但在其他年份极少出现。
这正是经典 TF-IDF 想表达的语义:
“区分度”优先于“出现频率”。
优点:
缺点:
Gensim 基于 BoW 表示:
[(term_id, tf), ...]TF-IDF 计算遵循经典公式:
其中:
Gensim 提供了高度可定制化的加权接口,允许用户通过参数调整来精细控制 TF 和 IDF 的计算逻辑:
df2idf),但支持更复杂的变体,例如概率 IDF 或熵权重。normalize=None 禁用归一化,或者使用自定义归一化函数。此外,Gensim 还支持偏移长度归一化(pivot document length normalization),通过引入参考文档长度 $L_0$ 和斜率 $slope$ 参数,可以有效缓解长文档因词汇量大而造成的权重偏差问题。这一特性特别适合处理文档长度差异较大的数据集。
核心代码:
# 1. 分词处理texts = news_df["word_split"].tolist()# 2. 构建词典和词袋模型dictionary = corpora.Dictionary(texts)corpus_bow = [dictionary.doc2bow(text) for text in texts] # 每个文档的(词ID, 词频)列表# 3. 训练TF-IDF模型并计算向量tfidf_model = models.TfidfModel(corpus_bow)corpus_tfidf = tfidf_model[corpus_bow]# 4. 定义转换函数:仅保留TF-IDF值def extract_tfidf(doc_idx): """将gensim计算的TF-IDF结果转换为{词: TF-IDF值}格式""" tfidf_dict = {} # 遍历当前文档的TF-IDF结果 for word_id, tfidf_score in corpus_tfidf[doc_idx]: word = dictionary[word_id] # 词ID转词语 tfidf_dict[word] = tfidf_score # 仅存储TF-IDF值 return tfidf_dict# 5. 应用函数,将结果存入DataFrame的'tfidf'列news_df["gensim_tfidf"] = [extract_tfidf(i) for i in range(len(news_df))]news_dfGensim 得到的 Top 词与手动实现高度一致:

说明:
适用场景:
在绝大多数科研与工业代码中,TF-IDF 通常直接来自:
TfidfVectorizer()这个方法。
其公式依然为词频×逆文档频率,但均有一下细小区别:
TF 描述术语 ( t ) 在文档 ( d ) 中的出现强度。默认采用“自然计数”(natural frequency),即原始词频:
若设置 sublinear_tf=True,则启用对数缩放:
这种非线性变换可以削弱极高频词对结果的支配作用,使模型对极端词频更加鲁棒。
IDF 衡量术语 ( t ) 在整个语料中的“稀有程度”,其计算受 smooth_idf 参数控制。
smooth_idf=False(非默认)时:其中,( n ) 为文档总数,( df(t) ) 为包含术语 ( t ) 的文档数。末尾的 +1 保证即便某词出现在所有文档中(( df(t)=n )),其 IDF 仍不小于 1,而不会被完全抹去。
smooth_idf=True(默认)时:分子与分母同时加 1,相当于引入一个“虚拟文档”包含所有术语,从而避免 ( df(t)=0 ) 导致的除零问题,并提升在小规模或高度稀疏语料上的数值稳定性。
默认采用 L2 归一化,使不同长度的文档在向量空间中具有可比性。可通过参数控制:
norm='l2':L2 归一化(平方之和为 1)norm='l1':L1 归一化(绝对值之和为 1)norm=None:不做归一化需要特别注意两点:
这意味着:
这正体现了 scikit-learn 中 TF-IDF 的“工程化取舍”:在理论纯粹性与数值稳定性、实际可用性之间,优先保证模型在真实语料上的鲁棒与一致表现。
核心代码:
# 导入必要的库import pandas as pdfrom sklearn.feature_extraction.text import TfidfVectorizer# 初始化TF-IDF向量器,重点展示关键参数tfidf_vectorizer = TfidfVectorizer( # IDF计算相关参数 smooth_idf=True, # 平滑IDF,避免除零除 use_idf=True, # 启用IDF计算(默认True) # 归一化参数 norm="l2", # 归一化方式:l2(默认)、l1或None sublinear_tf=False, # 是否应用亚线性TF缩放(log1p(tf)))# 拟合模型并转换文本(计算TF-IDF矩阵)tfidf_matrix = tfidf_vectorizer.fit_transform(news_df["processed_text"])# 获取特征词列表(词汇表)feature_names = tfidf_vectorizer.get_feature_names_out()# 定义函数:将TF-IDF矩阵转换为{词: TF-IDF值}字典def get_tfidf_dict(row_idx): """获取指定行的TF-IDF字典""" # 获取非零元素的索引和分数 row = tfidf_matrix[row_idx] non_zero_indices = row.indices scores = row.data # 构建词与分数的映射 tfidf_dict = { feature_names[idx]: score for idx, score in zip(non_zero_indices, scores) } return tfidf_dict# 将结果存储到news_df的'tfidf'列news_df["sklearn_tfidf"] = [get_tfidf_dict(i) for i in range(len(news_df))]news_dfsklearn 的 Top 词变成:

可以看到,这和之前的词频统计结果几乎没有差异。这已经不再是“年度特征词”,而更像是“年度高频词”。
原因不在于用错了库,而在于:
sklearn 的 TF-IDF 是为“机器学习特征工程”设计的,不是为“文档差异分析”设计的。
它的目标是:
代价是:
核心不是“哪个更好”,而是:
你要的是“统计意义上的区分度”,还是“工程意义上的稳定向量表示”。
当你发现 TF-IDF “筛不出真正代表文档的词”时,问题往往不在算法本身,而在于:
你选用的那一种 TF-IDF,并不匹配你的研究目标。

Reading List
往期推荐
顶刊ISR:社交机器人能促进社交吗?——对微博评论罗伯特的研究