3、词袋模型与TF-IDF:文本向量化原理

大家好,我是你们这堂课的主讲人。今天咱们聊聊文本向量化。

说白了,计算机不认识中文,也不认识英文。它只认识数字。所以,想让机器理解「研报里提到了茅台」和「研报里提到了风险」,第一步就是把文字变成数字。这个过程,就叫文本向量化。

我个人习惯把文本向量化比作「翻译」。把人类语言翻译成机器语言。今天要讲的词袋模型和TF-IDF,就是两种最经典的翻译方法。

3.1 词袋模型:最朴素的向量化方法

词袋模型,英文叫 Bag of Words,简称 BoW。名字很形象——你把一篇文章拆成一个个词,扔进一个袋子里,然后只数每个词出现了多少次。

核心思想就两点:

  • 忽略语法和词序
  • 只关心词的出现频率

举个例子。假设有两句研报标题:

  • 句子A:「白酒板块持续走强」
  • 句子B:「科技板块出现回调」

我们先构建一个词表(词典):[白酒, 板块, 持续, 走强, 科技, 出现, 回调]

然后,每个句子就可以表示成一个向量,向量的每个位置对应词表中那个词的词频:

句子 白酒 板块 持续 走强 科技 出现 回调
A 1 1 1 1 0 0 0
B 0 1 0 0 1 1 1

你看,句子A变成了 [1,1,1,1,0,0,0],句子B变成了 [0,1,0,0,1,1,1]。

优点:简单、直观、计算快。

缺点:丢失了语义和顺序信息。「猫追老鼠」和「老鼠追猫」在词袋模型里一模一样。

我在项目中遇到过一个问题:用词袋模型处理金融研报时,「风险」和「没有风险」被当成相似的句子。因为「没有」这个词权重不高,而「风险」出现了。嗯,这里要注意,词袋模型对否定词的处理很弱。

3.2 TF-IDF:给词加上权重

词袋模型有个硬伤:它认为所有词一样重要。但实际不是这样。

你想想看,在研报里,「的」、「了」、「在」这些词出现频率极高,但它们几乎不携带信息。而「净利润」、「毛利率」、「ROE」这些词出现次数少,但信息量巨大。

TF-IDF 就是为了解决这个问题而生的。

TF-IDF = TF × IDF

  • TF(词频):这个词在当前文档中出现了多少次。跟词袋模型一样。
  • IDF(逆文档频率):这个词在多少篇文档中出现过。出现得越少,IDF 值越高。

公式长这样:

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

IDF(t) = log( (总文档数) / (包含词 t 的文档数 + 1) )

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

为什么要加1?防止分母为0。这是个小细节,但容易忘。

举个例子。假设我们有1000篇研报,「净利润」出现在其中50篇,「的」出现在全部1000篇。

  • IDF(净利润) = log(1000 / (50+1)) ≈ log(19.6) ≈ 2.98
  • IDF(的) = log(1000 / (1000+1)) ≈ log(0.999) ≈ 0

你看,「净利润」的 IDF 值很高,而「的」几乎为0。这样,TF-IDF 就能自动过滤掉常见但无意义的词。

避坑指南:我曾经在处理季度财报时,发现「同比」这个词的 IDF 值异常低。后来一查,是因为所有财报里都有「同比」这个词。所以,如果你要分析的是同类文档(比如全是研报),IDF 的区分度会下降。这时候可以考虑用其他方法,比如词嵌入。

3.3 使用 Scikit-learn 实现特征提取

理论讲完了,咱们直接上代码。Scikit-learn 提供了现成的工具,几行代码就能搞定。

3.3.1 词袋模型实现

from sklearn.feature_extraction.text import CountVectorizer

# 示例语料
corpus = [
    "白酒板块持续走强",
    "科技板块出现回调",
    "白酒板块出现分化"
]

# 初始化向量化器
vectorizer = CountVectorizer()

# 拟合数据并转换
X = vectorizer.fit_transform(corpus)

# 查看词表
print("词表:", vectorizer.get_feature_names_out())

# 查看向量化结果
print("向量化结果:")
print(X.toarray())

输出结果:

词表: ['出现' '回调' '板块' '持续' '分化' '白酒' '科技' '走强']
向量化结果:
[[0 0 1 1 0 1 0 1]
 [1 1 1 0 0 0 1 0]
 [1 0 1 0 1 1 0 0]]

你看,三句话变成了一个3行8列的矩阵。每一行对应一句话,每一列对应词表里的一个词。

3.3.2 TF-IDF 实现

from sklearn.feature_extraction.text import TfidfVectorizer

# 同样的语料
corpus = [
    "白酒板块持续走强",
    "科技板块出现回调",
    "白酒板块出现分化"
]

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

# 拟合数据并转换
X_tfidf = tfidf_vectorizer.fit_transform(corpus)

# 查看词表
print("词表:", tfidf_vectorizer.get_feature_names_out())

# 查看 TF-IDF 结果
print("TF-IDF 矩阵:")
print(X_tfidf.toarray())

输出结果:

词表: ['出现' '回调' '板块' '持续' '分化' '白酒' '科技' '走强']
TF-IDF 矩阵:
[[0.         0.         0.4472136  0.4472136  0.         0.4472136
  0.         0.4472136 ]
 [0.4472136  0.4472136  0.4472136  0.         0.         0.
  0.4472136  0.        ]
 [0.4472136  0.         0.4472136  0.         0.4472136  0.4472136
  0.         0.        ]]

注意看,词袋模型里「板块」在每句话里都是1,但在 TF-IDF 里,它的值变成了0.447。因为「板块」在三句话里都出现了,IDF 给它降权了。

重要提醒:TF-IDF 的默认实现会对向量做 L2 归一化。所以你会发现所有值都在0到1之间。如果你不需要归一化,可以设置 norm=None

3.3.3 常用参数调优

我个人习惯在实战中调整这几个参数:

参数 作用 推荐值
max_features 限制词表大小,只保留最频繁的N个词 5000-10000
min_df 词至少在多少篇文档中出现过,过滤低频词 2-5
max_df 词最多在多少比例的文档中出现,过滤高频停用词 0.8-0.95
ngram_range 是否考虑词组,比如(1,2)表示考虑单个词和两个词的组合 (1,1) 或 (1,2)

举个例子,如果你处理的是研报标题,我建议用 ngram_range=(1,2)。因为「白酒板块」作为一个词组,比单独看「白酒」和「板块」更有意义。

# 带参数调优的 TF-IDF
tfidf_vectorizer = TfidfVectorizer(
    max_features=5000,
    min_df=2,
    max_df=0.9,
    ngram_range=(1, 2)
)

3.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

文本向量化知识体系 文本向量化 词袋模型 (BoW) TF-IDF 核心:词频统计,忽略顺序 缺点:高频无意义词干扰 核心:TF × IDF 加权 优点:自动过滤停用词 Scikit-learn 实现 CountVectorizer TfidfVectorizer 参数调优

这张图把本章的核心逻辑串起来了。从文本向量化出发,分两条路:词袋模型和TF-IDF。最后都落到Scikit-learn的工具上。

好了,这一章的内容就到这里。记住一句话:词袋模型是基础,TF-IDF是升级。在实际的研报分析中,我90%的情况会用TF-IDF,因为它确实能更好地捕捉关键词的重要性。

如果你在跑代码时遇到问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321