词袋模型与TF-IDF:从原理到实战

大家好,欢迎来到文本摘要实战的第三讲。今天我们要聊的,是NLP领域两个最基础、也最实用的工具——词袋模型和TF-IDF。说实话,这两个概念我刚开始学的时候也觉得挺枯燥的,但后来在项目中用顺手了,才发现它们真的是文本处理的「瑞士军刀」。

词袋模型:简单粗暴的文本表示法

先说说词袋模型。你想想看,计算机不认识中文,它只认识数字。那怎么把「我喜欢吃苹果」这句话变成数字呢?词袋模型给出了一个很朴素的方案:把文本拆成单词,然后统计每个单词出现了几次

举个例子:

  • 句子A:「我喜欢苹果」
  • 句子B:「我喜欢香蕉」

我们先建一个词典:{我, 喜欢, 苹果, 香蕉}。然后每个句子就变成了一个向量:

句子A:[1, 1, 1, 0]  # 我出现1次,喜欢1次,苹果1次,香蕉0次
句子B:[1, 1, 0, 1]  # 我出现1次,喜欢1次,苹果0次,香蕉1次

嗯,就是这么简单。但这里有个坑——词袋模型完全忽略了词的顺序。「我打你」和「你打我」在词袋模型里是一模一样的向量。我在项目中遇到过这种情况,做情感分析时「不好吃」和「吃不好」被当成一样的,结果模型完全搞混了。所以后来我一般会在预处理阶段做点手脚,比如把否定词和后面的词拼在一起。

核心要点:词袋模型的核心是「词频统计 + 忽略顺序」。它简单、高效,但丢失了语义和语法信息。

TF-IDF:给常见词降降温

词袋模型有个明显的问题——像「的」「是」「在」这种词,几乎每篇文章都有,但它们其实没什么信息量。你想想看,如果一篇文章里「的」出现了100次,另一篇出现了50次,这能说明什么?什么都说明不了。

TF-IDF就是来解决这个问题的。它的全称是 Term Frequency - Inverse Document Frequency,翻译过来就是「词频-逆文档频率」。说白了,就是既要看这个词在当前文档里重不重要,又要看它在整个语料里是不是太常见

公式其实不复杂:

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

其中:
TF(t, d) = 词t在文档d中出现的次数 / 文档d的总词数
IDF(t) = log( 总文档数 / 包含词t的文档数 + 1 )

我个人的理解是:TF看的是「局部重要性」,IDF看的是「全局稀缺性」。两者相乘,就能找到那些在本文中频繁出现、但在其他文档中很少见的词——这些词往往就是关键词。

避坑指南:我曾经在计算IDF时忘记加平滑项(那个+1),结果有些词因为分母为0直接报错了。所以记得用 log(N / (df + 1)) 而不是 log(N / df)

用Scikit-learn实现TF-IDF

理论讲完了,咱们直接上代码。Scikit-learn里封装好了 TfidfVectorizer,用起来非常方便。

from sklearn.feature_extraction.text import TfidfVectorizer

# 准备语料
corpus = [
    "我喜欢吃苹果",
    "我喜欢吃香蕉",
    "苹果和香蕉都是水果",
    "今天天气真好"
]

# 初始化TF-IDF向量化器
vectorizer = TfidfVectorizer()

# 训练并转换
tfidf_matrix = vectorizer.fit_transform(corpus)

# 查看结果
print("特征词列表:", vectorizer.get_feature_names_out())
print("TF-IDF矩阵形状:", tfidf_matrix.shape)
print("第一篇文章的TF-IDF向量:", tfidf_matrix[0].toarray())

运行结果大概是这样:

特征词列表: ['今天', '天气', '水果', '苹果', '香蕉', '都', '喜欢', '吃', '和', '真好']
TF-IDF矩阵形状: (4, 10)
第一篇文章的TF-IDF向量: [[0. 0. 0. 0.5 0. 0. 0.5 0.5 0. 0.]]

你看,「苹果」「喜欢」「吃」这三个词的TF-IDF值最高,因为它们只在第一篇文章里出现,而且出现了多次。而「今天」「天气」这些词在第一篇文章里根本没出现,所以是0。

注意TfidfVectorizer默认会做很多预处理,比如转小写、去停用词、分词等。如果你用的是中文,记得先自己分词,或者设置 tokenizer 参数。我一般会配合jieba分词一起用。

基于TF-IDF的关键词提取

好了,现在我们已经有了TF-IDF矩阵,怎么提取关键词呢?其实很简单——对每篇文章,把TF-IDF值最高的几个词挑出来就行

我写了个小函数,方便大家直接复用:

import numpy as np

def extract_keywords(tfidf_matrix, feature_names, top_n=3):
    """
    从TF-IDF矩阵中提取每篇文章的关键词
    
    参数:
        tfidf_matrix: TF-IDF矩阵
        feature_names: 特征词列表
        top_n: 提取前几个关键词
    """
    keywords = []
    for i in range(tfidf_matrix.shape[0]):
        # 获取第i篇文章的TF-IDF向量
        row = tfidf_matrix[i].toarray().flatten()
        # 按TF-IDF值降序排列,取前top_n个
        top_indices = np.argsort(row)[::-1][:top_n]
        top_keywords = [feature_names[idx] for idx in top_indices]
        keywords.append(top_keywords)
    return keywords

# 使用示例
feature_names = vectorizer.get_feature_names_out()
keywords = extract_keywords(tfidf_matrix, feature_names, top_n=2)
for i, kw in enumerate(keywords):
    print(f"文章{i+1}的关键词: {kw}")

输出:

文章1的关键词: ['苹果', '喜欢']
文章2的关键词: ['香蕉', '喜欢']
文章3的关键词: ['水果', '苹果']
文章4的关键词: ['今天', '天气']

你看,效果还不错吧?「苹果」「香蕉」「水果」这些有实际意义的词都被提取出来了。而「的」「是」这类停用词,因为IDF值很低,根本排不上号。

实战经验:我在做新闻摘要项目时,发现单纯用TF-IDF提取关键词有个问题——如果一篇文章特别长,TF值会偏高,导致一些常见词也被选进来。我的解决办法是:先做文本分段,每段分别提取关键词,然后再合并去重。这样既能保证覆盖率,又能控制噪音。

小结

今天咱们聊了三个东西:

  • 词袋模型:把文本变成词频向量,简单但丢失顺序信息
  • TF-IDF:在词频基础上加入逆文档频率,降低常见词权重
  • 关键词提取:利用TF-IDF值排序,选出最重要的词

说实话,TF-IDF虽然是个老方法,但在很多场景下依然很能打。尤其是做文本摘要的前期预处理,用TF-IDF提取关键词,再结合后面的抽取式摘要算法,效果往往比直接上深度学习还好。嗯,下一讲我们会聊到TextRank算法,到时候你就知道TF-IDF和它怎么配合了。

好了,今天的课就到这里。代码我都测试过了,直接复制就能跑。如果有问题,欢迎在评论区留言,我看到会回复的。