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 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的核心逻辑串起来了。从文本向量化出发,分两条路:词袋模型和TF-IDF。最后都落到Scikit-learn的工具上。
好了,这一章的内容就到这里。记住一句话:词袋模型是基础,TF-IDF是升级。在实际的研报分析中,我90%的情况会用TF-IDF,因为它确实能更好地捕捉关键词的重要性。
如果你在跑代码时遇到问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321