Python 文本分析入门:词袋模型 BoW 和 TF-IDF 到底怎么理解?
上一篇主要解决了两个基础问题:
- 为什么中文文本通常要先分词?
- 为什么分词之后还要做停用词过滤?
但文本清洗完成之后,新的问题很快就会出现:
词已经切出来了,接下来怎么让模型"看懂"这些词?
这就是文本分析里下一步要解决的事情:文本表征。
因为对大多数机器学习算法来说,输入通常是数值型特征。
而词语本身不是数值,所以还需要进一步把文本转换成向量形式。常见做法包括词袋模型(BoW)和 TF-IDF,它们都是文本分析中最基础、也最常用的表示方法 [1]。
这篇文章主要回答三个问题:
- 为什么文本还要继续做"向量化"?
- 词袋模型 BoW 是怎么表示文本的?
- TF-IDF 相比普通词频,到底多考虑了什么?
一、为什么分完词之后,还不能直接建模?
先看一个直观问题。
假设一条评论分词之后变成这样:
text
电影 / 好看 / 喜欢 / 演员 / 演技 / 不错
对人来说,这已经足够清楚。
但对模型来说,这仍然不是一个可以直接计算的输入。
因为很多机器学习方法需要的是像下面这样的形式:
text
[0, 1, 3, 0, 2, 0, 1]
也就是说,模型通常并不能直接处理"词",而是需要处理"数字"。
所以文本分析里,分词之后的下一步,不是立刻训练模型,而是先把文本转换成数值特征。
这一步就是文本表征,也可以理解为:把文本变成向量 [1]。
二、文本表征到底在解决什么问题?
文本表征解决的核心问题其实很简单:
怎么把一段文本,变成模型可以处理的数字形式?
如果不做这一步,后面很多工作都没法进行,比如:
- 文本分类
- 文本聚类
- 情感分析
- 文本检索
因为这些任务最终都需要一个"特征矩阵"作为输入。
所以文本表征,本质上是在做一件事:
- 输入:一段自然语言文本
- 输出:一组数值特征
而在入门阶段,最经典的两种方式就是:
- 词袋模型(BoW)
- TF-IDF
三、词袋模型 BoW 是怎么表示文本的?
词袋模型的思路很直接:
不考虑词语顺序,只统计每个词出现了没有、出现了多少次。
也就是说,它把一篇文本看成一个"装词的袋子"。
袋子里有什么词、每个词出现了几次,这些信息会被保留下来;但词和词之间的顺序关系不会保留。
这就是"词袋模型"这个名字的来源。
四、词袋模型为什么叫"词袋"?
我们用一个简单例子来理解。
假设有三条文本:
text
文本1:电影 很 好看
文本2:电影 很 无聊
文本3:剧情 很 一般
先把所有出现过的词放到一起,形成一个总词表:
text
[电影, 很, 好看, 无聊, 剧情, 一般]
接下来,每条文本都可以表示成一个向量:
- 文本1:
[1, 1, 1, 0, 0, 0] - 文本2:
[1, 1, 0, 1, 0, 0] - 文本3:
[0, 1, 0, 0, 1, 1]
这个向量的意思就是:
- 词表里有哪些词
- 某个词在当前文本里有没有出现
- 或者出现了多少次
这就是最典型的 BoW 表示方式 [1]。
五、词袋模型的优点是什么?
词袋模型很经典,不是没有原因的。
1. 思路直观
它很好理解:
就是把文本拆成词,再把词变成计数结果。
2. 实现简单
无论是自己写代码,还是用 sklearn,都很容易实现。
3. 适合做入门
对于刚接触文本分析的人来说,BoW 是理解"文本怎么变成数字"的最好入口之一。
六、词袋模型有什么局限?
词袋模型虽然简单,但也有几个很明显的问题。
1. 向量通常会非常稀疏
如果词表很大,而一篇文本只包含其中很少一部分词,那么向量里就会出现大量的 0,导致矩阵非常稀疏 [1]。
2. 不保留语法信息
BoW 只记录词有没有出现、出现了多少次,但不会告诉你这些词之间是什么关系 [1]。
3. 不保留词序信息
比如下面两句话:
- 我喜欢你
- 你喜欢我
在词袋模型里,它们可能得到非常接近,甚至完全相同的表示,因为词袋模型不考虑单词顺序 [1]。
这也是为什么 BoW 虽然好用,但在表达更复杂语义时会显得不够。
七、为什么只统计词频还不够?
词袋模型已经能把文本变成数字了,那是不是就够了?
很多时候还不够。
因为有些词虽然在某一篇文本里出现很多次,但它在所有文本里都很常见。
这种词未必真的有区分能力。
比如在影评数据里,如果每篇评论里都经常出现"电影"这个词,那么它虽然频繁出现,但不一定能帮助我们区分"好评"和"差评"。
这时候我们就会想到一个更进一步的问题:
一个词的重要性,能不能不仅看它在当前文本里出现了多少次,还看它在整个语料里是不是过于常见?
这就是 TF-IDF 要解决的问题。
八、TF-IDF 到底在多考虑什么?
TF-IDF 的名字看起来有点复杂,但核心思想并不难:
- TF(Term Frequency):某个词在当前文本中出现得是否频繁
- IDF(Inverse Document Frequency):某个词在整个文档集合中是否过于常见 [1]
直观理解就是:
- 一个词在当前文本里出现得越多,通常越重要
- 但如果这个词在所有文本里都很常见,那它的区分价值就会下降 [1]
所以 TF-IDF 本质上是在回答一个更合理的问题:
这个词不仅常出现,而且是不是"只在少数文本里更有代表性"?
九、怎么理解 TF 和 IDF?
可以分开看。
1. TF:词在当前文本里有多常见
如果一个词在当前文本中出现很多次,说明它和这篇文本关系比较强。
例如:
- "好看"在一条影评里反复出现
- "拖沓"在某条差评里多次出现
这些词可能就更值得关注。
2. IDF:词在整个语料里是不是过于普遍
如果一个词几乎在所有文本中都会出现,那么它虽然常见,但区分能力就弱。
比如:
- 电影
- 今天
- 一个
这些词可能在很多文本中都出现,因而不一定能很好地区分类别。
所以 TF-IDF 的思路就是:
- 保留"当前文本里重要"的信息
- 同时削弱"所有文本里都很常见"的词 [1]
十、TF-IDF 相比词袋模型,有什么改进?
它并不是完全推翻词袋模型,而是在词频的基础上做进一步加权。
可以理解成:
- BoW 更关注"出现了几次"
- TF-IDF 更关注"这个词是否真的有区分价值"
因此,在很多任务中,TF-IDF 往往比单纯的词频统计更有效,尤其是在文本分类和信息检索中更常见 [1]。
十一、代码实操:BoW 和 TF-IDF 怎么做?
下面用一组简单文本来对比这两种表示方式。
1. 准备示例文本
python
texts = [
"电影 很 好看 演员 表现 不错",
"剧情 太 拖沓 电影 不 推荐",
"画面 很 漂亮 但是 故事 一般",
"音乐 很 好听 节奏 不错 整体 体验 很好",
"电影 太 无聊 浪费 时间"
]
2. 用词袋模型表示文本
python
from sklearn.feature_extraction.text import CountVectorizer
texts = [
"电影 很 好看 演员 表现 不错",
"剧情 太 拖沓 电影 不 推荐",
"画面 很 漂亮 但是 故事 一般",
"音乐 很 好听 节奏 不错 整体 体验 很好",
"电影 太 无聊 浪费 时间"
]
vectorizer = CountVectorizer()
X_bow = vectorizer.fit_transform(texts)
print("词表:")
print(vectorizer.get_feature_names_out())
print("\nBoW 矩阵:")
print(X_bow.toarray())
这段代码做的事情很直接:
- 先统计所有文本中出现过的词
- 建立词表
- 把每条文本表示成一个计数向量
3. 用 TF-IDF 表示文本
python
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer()
X_tfidf = tfidf_vectorizer.fit_transform(texts)
print("词表:")
print(tfidf_vectorizer.get_feature_names_out())
print("\nTF-IDF 矩阵:")
print(X_tfidf.toarray())
这一步和词袋模型很像,但输出的不再是简单词频,而是加权后的结果。
4. 输出成表格看得更清楚
如果想看得更直观一点,可以转成 DataFrame。
python
import pandas as pd
# BoW 结果
bow_df = pd.DataFrame(
X_bow.toarray(),
columns=vectorizer.get_feature_names_out()
)
print("BoW 表示结果:")
print(bow_df)
# TF-IDF 结果
tfidf_df = pd.DataFrame(
X_tfidf.toarray(),
columns=tfidf_vectorizer.get_feature_names_out()
)
print("\nTF-IDF 表示结果:")
print(tfidf_df.round(3))
这样就能更清楚地看到:
- BoW 输出的是词频计数
- TF-IDF 输出的是加权后的特征值
十二、完整代码
如果想一次性运行,下面这份代码可以直接使用。
python
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
import pandas as pd
# 1. 示例文本
texts = [
"电影 很 好看 演员 表现 不错",
"剧情 太 拖沓 电影 不 推荐",
"画面 很 漂亮 但是 故事 一般",
"音乐 很 好听 节奏 不错 整体 体验 很好",
"电影 太 无聊 浪费 时间"
]
# 2. BoW
bow_vectorizer = CountVectorizer()
X_bow = bow_vectorizer.fit_transform(texts)
print("===== 词袋模型 BoW =====")
print("词表:")
print(bow_vectorizer.get_feature_names_out())
bow_df = pd.DataFrame(
X_bow.toarray(),
columns=bow_vectorizer.get_feature_names_out()
)
print("\nBoW 表示结果:")
print(bow_df)
# 3. TF-IDF
tfidf_vectorizer = TfidfVectorizer()
X_tfidf = tfidf_vectorizer.fit_transform(texts)
print("\n===== TF-IDF =====")
print("词表:")
print(tfidf_vectorizer.get_feature_names_out())
tfidf_df = pd.DataFrame(
X_tfidf.toarray(),
columns=tfidf_vectorizer.get_feature_names_out()
)
print("\nTF-IDF 表示结果:")
print(tfidf_df.round(3))
输出
log
===== 词袋模型 BoW =====
词表:
['一般' '不错' '但是' '体验' '剧情' '好听' '好看' '很好' '拖沓' '推荐' '故事' '整体' '无聊' '时间'
'浪费' '漂亮' '演员' '电影' '画面' '节奏' '表现' '音乐']
BoW 表示结果:
一般 不错 但是 体验 剧情 好听 好看 很好 拖沓 推荐 ... 无聊 时间 浪费 漂亮 演员 电影 画面 \
0 0 1 0 0 0 0 1 0 0 0 ... 0 0 0 0 1 1 0
1 0 0 0 0 1 0 0 0 1 1 ... 0 0 0 0 0 1 0
2 1 0 1 0 0 0 0 0 0 0 ... 0 0 0 1 0 0 1
3 0 1 0 1 0 1 0 1 0 0 ... 0 0 0 0 0 0 0
4 0 0 0 0 0 0 0 0 0 0 ... 1 1 1 0 0 1 0
节奏 表现 音乐
0 0 1 0
1 0 0 0
2 0 0 0
3 1 0 1
4 0 0 0
[5 rows x 22 columns]
===== TF-IDF =====
词表:
['一般' '不错' '但是' '体验' '剧情' '好听' '好看' '很好' '拖沓' '推荐' '故事' '整体' '无聊' '时间'
'浪费' '漂亮' '演员' '电影' '画面' '节奏' '表现' '音乐']
TF-IDF 表示结果:
一般 不错 但是 体验 剧情 好听 好看 很好 拖沓 推荐 ... \
0 0.000 0.398 0.000 0.000 0.000 0.000 0.494 0.000 0.000 0.000 ...
1 0.000 0.000 0.000 0.000 0.538 0.000 0.000 0.000 0.538 0.538 ...
2 0.447 0.000 0.447 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ...
3 0.000 0.313 0.000 0.388 0.000 0.388 0.000 0.388 0.000 0.000 ...
4 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 ...
无聊 时间 浪费 漂亮 演员 电影 画面 节奏 表现 音乐
0 0.000 0.000 0.000 0.000 0.494 0.331 0.000 0.000 0.494 0.000
1 0.000 0.000 0.000 0.000 0.000 0.361 0.000 0.000 0.000 0.000
2 0.000 0.000 0.000 0.447 0.000 0.000 0.447 0.000 0.000 0.000
3 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.388 0.000 0.388
4 0.538 0.538 0.538 0.000 0.000 0.361 0.000 0.000 0.000 0.000
[5 rows x 22 columns]
十三、这段代码在做什么?
这段代码可以分成两部分来理解。
第一部分:CountVectorizer 做 BoW
这里得到的是一个"文档-词项矩阵"。
每一行是一条文本,每一列是一个词,值表示这个词在该文本中出现了多少次。
第二部分:TfidfVectorizer 做 TF-IDF
这里同样会形成一个矩阵,但矩阵中的值不再只是出现次数,而是综合考虑了词在当前文本中的频率,以及它在整个文本集合中的常见程度 [1]。
这也是为什么 TF-IDF 常被看作是比单纯词频更合理的一种加权方式 [1]。
十四、两种方法的结果应该怎么理解?
通常可以从下面几个角度来看。
1. BoW 更适合做最基础的表示
如果你的目标是先把文本转成数字,BoW 是最直接的起点。
2. TF-IDF 更强调区分能力
如果某个词在所有文本里都常见,它在 TF-IDF 中的权重通常会被压低;
如果某个词只在少数文本里出现,但对这些文本很有代表性,它的权重通常会更高 [1]。
3. 两者都不理解真正语义
虽然 TF-IDF 比 BoW 更进一步,但它依然不能真正理解上下文语义。
它们都属于比较基础的表示方法,因此后来才发展出了 Word2Vec、CBOW、Skip-gram 等词嵌入技术 [1]。
十五、练习:试着比较两种表示方式的差异
练习 1
观察同一批文本在 BoW 和 TF-IDF 下的表示结果:
- 哪些词在 BoW 中频率比较高?
- 哪些词在 TF-IDF 中权重更高?
- 为什么会有这种差异?
练习 2
如果有一个词在每条文本里都出现,那么它在 TF-IDF 中的权重通常会有什么变化?
练习 3
下面两句话在词袋模型里为什么可能很接近?
- 我喜欢你
- 你喜欢我
这说明了词袋模型的什么局限?
十六、小结
这篇文章主要解决了三个问题:
- 为什么文本还要继续做向量化?
- 词袋模型 BoW 是怎么表示文本的?
- TF-IDF 比普通词频多考虑了什么?
可以简单概括为:
- BoW:把文本表示成词频向量
- TF-IDF:在词频基础上,进一步考虑词的区分能力 [1]
两种方法都很基础,但也都非常重要。
因为后面的文本分类、文本聚类、信息检索,很多时候都是从这里开始的 [1]。