37 Python 时序和文本:词袋模型 BoW 和 TF-IDF 到底怎么理解?

Python 文本分析入门:词袋模型 BoW 和 TF-IDF 到底怎么理解?

上一篇主要解决了两个基础问题:

  1. 为什么中文文本通常要先分词?
  2. 为什么分词之后还要做停用词过滤?

但文本清洗完成之后,新的问题很快就会出现:

词已经切出来了,接下来怎么让模型"看懂"这些词?

这就是文本分析里下一步要解决的事情:文本表征

因为对大多数机器学习算法来说,输入通常是数值型特征。

而词语本身不是数值,所以还需要进一步把文本转换成向量形式。常见做法包括词袋模型(BoW)和 TF-IDF,它们都是文本分析中最基础、也最常用的表示方法 [1]。

这篇文章主要回答三个问题:

  1. 为什么文本还要继续做"向量化"?
  2. 词袋模型 BoW 是怎么表示文本的?
  3. TF-IDF 相比普通词频,到底多考虑了什么?

一、为什么分完词之后,还不能直接建模?

先看一个直观问题。

假设一条评论分词之后变成这样:

text 复制代码
电影 / 好看 / 喜欢 / 演员 / 演技 / 不错

对人来说,这已经足够清楚。

但对模型来说,这仍然不是一个可以直接计算的输入。

因为很多机器学习方法需要的是像下面这样的形式:

text 复制代码
[0, 1, 3, 0, 2, 0, 1]

也就是说,模型通常并不能直接处理"词",而是需要处理"数字"。

所以文本分析里,分词之后的下一步,不是立刻训练模型,而是先把文本转换成数值特征。

这一步就是文本表征,也可以理解为:把文本变成向量 [1]。


二、文本表征到底在解决什么问题?

文本表征解决的核心问题其实很简单:

怎么把一段文本,变成模型可以处理的数字形式?

如果不做这一步,后面很多工作都没法进行,比如:

  • 文本分类
  • 文本聚类
  • 情感分析
  • 文本检索

因为这些任务最终都需要一个"特征矩阵"作为输入。

所以文本表征,本质上是在做一件事:

  • 输入:一段自然语言文本
  • 输出:一组数值特征

而在入门阶段,最经典的两种方式就是:

  1. 词袋模型(BoW)
  2. 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())

这段代码做的事情很直接:

  1. 先统计所有文本中出现过的词
  2. 建立词表
  3. 把每条文本表示成一个计数向量

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

下面两句话在词袋模型里为什么可能很接近?

  • 我喜欢你
  • 你喜欢我

这说明了词袋模型的什么局限?


十六、小结

这篇文章主要解决了三个问题:

  1. 为什么文本还要继续做向量化?
  2. 词袋模型 BoW 是怎么表示文本的?
  3. TF-IDF 比普通词频多考虑了什么?

可以简单概括为:

  • BoW:把文本表示成词频向量
  • TF-IDF:在词频基础上,进一步考虑词的区分能力 [1]

两种方法都很基础,但也都非常重要。

因为后面的文本分类、文本聚类、信息检索,很多时候都是从这里开始的 [1]。


相关推荐
2401_873544922 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
剑穗挂着新流苏3122 小时前
202_深度学习的动力源泉:矩阵微积分与自动求导 (Autograd)
人工智能·pytorch·python·深度学习·神经网络
qq_148115372 小时前
Python上下文管理器(with语句)的原理与实践
jvm·数据库·python
qwehjk20082 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python
兰.lan2 小时前
【黑马ai测试】HTTP协议-抓包工具定位-弱网测试-缺陷介绍
网络·python·网络协议·http
每天吃的很好的Ruby3 小时前
报错ValueError: sampler option is mutually exclusive with shuffle
人工智能·pytorch·python
清水白石0083 小时前
Python 性能优化全景解析:当 Big O 骗了你——深挖常数开销、内存与解释器黑盒
开发语言·python·性能优化
oi..3 小时前
python Get/Post请求练习
开发语言·经验分享·笔记·python·程序人生·安全·网络安全
速易达网络3 小时前
python地图商城可视化系统
python