词袋模型
约 1323 字大约 4 分钟
2026-06-28
词袋模型(Bag-of-Words)是一种简单的文本表示方法,也是 NLP 的经典模型。它把文本里的词看作一个个独立的个体,不考虑顺序,只关心每个词出现的频次。核心思想是:把文本表示成一个 "词频向量",不看词序、不看句法,只关注 词是否出现 和 出现了几次。
可以把它想象成一个装词的口袋,所有词零零散散地丢进去,谁在前谁在后不重要,只数每个词有几个。
工作原理
假设有三句话作为语料库(Corpus):
句子1:我喜欢吃苹果
句子2:我不喜欢吃香蕉
句子3:她喜欢吃葡萄分词 —— 把每个句子切成词
["我", "喜欢", "吃", "苹果"] ["我", "不", "喜欢", "吃", "香蕉"] ["她", "喜欢", "吃", "葡萄"]构建词表 —— 提取所有出现过的唯一词汇(去重),按顺序编号
词表 = ["我", "喜欢", "吃", "苹果", "不", "香蕉", "她", "葡萄"]这个词表确定了向量的"维度"。共 8 个词,所以 向量长度就是 8。
构建词频向量 —— 每句话对应一个长度为 8 的向量,每一位记该词出现的次数
词汇位置 我 喜欢 吃 苹果 不 香蕉 她 葡萄 句子1 1 1 1 1 0 0 0 0 句子2 1 1 1 0 1 1 0 0 句子3 0 1 1 0 0 0 1 1 这张表就是 BoW 向量化后的结果,每句话变成一个向量。这一步是里程碑式的进步——句子一旦向量化,就意味着可以拿去做数学计算了。
相比 N-Gram 的改进
| 问题 | N-Gram 的缺陷 | BoW 的解决 |
|---|---|---|
| 维度膨胀 | 组合数非常多(Trigram 以上尤甚) | 只统计词不统计组合,维度更小 |
| 数据稀疏 | 很多词组组合从未出现 | 用词频统计,不会因未见组合归 0,更稳定 |
| 上下文建模复杂 | 有上下文但建模代价大 | 完全不建上下文,计算效率优先 |
| 擅长领域 | 语言建模、文本生成 | 分类、聚类、检索,不用来生成文本 |
由此可见,词袋模型更擅长 文本分类、文本相似度分析、信息检索 / 文档排序。
代码实践
sklearn 的 CountVectorizer 就是现成的词袋工具。先看向量化:
from sklearn.feature_extraction.text import CountVectorizer # 词袋模型
import jieba # 中文分词
# 1. 原始中文语料
corpus = [
"我喜欢吃苹果",
"我不喜欢吃香蕉",
"她喜欢吃葡萄",
]
# 2. jieba 分词后用空格连接(CountVectorizer 默认按空格切)
corpus_cut = [" ".join(jieba.cut(s)) for s in corpus]
print("【分词结果】:")
for i, sent in enumerate(corpus_cut):
print(f"句子{i + 1}:{sent}")
# 3. 创建词袋模型并训练、向量化
# 注意 token_pattern:CountVectorizer 默认只保留长度 ≥2 的词,
# 会把"我""吃""她"等单字丢掉,必须改成 \b\w+\b 才能保留单字
vectorizer = CountVectorizer(token_pattern=r"(?u)\b\w+\b")
X = vectorizer.fit_transform(corpus_cut)
# 4. 查看词表与词频向量
print("\n【词表】:", vectorizer.get_feature_names_out())
print("\n【向量表示】:\n", X.toarray())警告
CountVectorizer 默认的 token_pattern 是 \b\w\w+\b,只保留长度 ≥2 的词,会把 我、吃、她、不 这些单字直接丢掉,导致结果和上面手算的词频表对不上。中文场景下务必改成 token_pattern=r"(?u)\b\w+\b" 才能保留单字。
把句子转换成向量后,就可以通过向量来 计算相似度、进行分类。下面用朴素贝叶斯做一个餐厅评论的情感分类:
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB # 朴素贝叶斯分类器
import jieba
# 1. 语料和标签(1 正面,0 负面)
texts = [
"这家店的菜太难吃了", # 0
"服务态度很差,再也不来了", # 0
"菜品很丰富,味道不错", # 1
"真是一次愉快的用餐体验", # 1
"价格太贵,吃不饱", # 0
"环境很好,服务也周到", # 1
]
labels = [0, 0, 1, 1, 0, 1]
# 2. 分词并向量化(同样要保留单字,否则"贵""差"等会被丢掉)
texts_cut = [" ".join(jieba.cut(t)) for t in texts]
vectorizer = CountVectorizer(token_pattern=r"(?u)\b\w+\b")
X = vectorizer.fit_transform(texts_cut)
# 3. 训练分类器
model = MultinomialNB()
model.fit(X, labels)
# 4. 预测新评论
test_comments = [
"服务不好,东西也难吃", # 期望 0
"饭很好吃,环境也不错", # 期望 1
"太贵了,不推荐", # 期望 0
"下次还会来,真的很满意", # 期望 1
]
test_cut = [" ".join(jieba.cut(c)) for c in test_comments]
X_test = vectorizer.transform(test_cut)
predictions = model.predict(X_test)
print("【情感预测结果】:")
for comment, label in zip(test_comments, predictions):
print(f"'{comment}' → {'正面' if label == 1 else '负面'}")重要
词袋模型把句子变成词频向量,首次让文本能被数学化处理,这是从 N-Gram 到现代 NLP 的关键一跃。但它丢掉了词序和语义,所以更适合分类、检索这类任务,而非文本生成。
它留下的 "语义" 问题,要靠后面的词嵌入(Embedding)来解决。
常见缺陷
- 只看词有没有,不看上下文。
- 词序被忽略:"我打了他" 和 "他打了我" 在词袋里是同一个向量,被当成一个意思。
- 没有语义泛化能力:模型不知道 满意 和 不错 意思相近,也不知道 愉快 和 开心 是近义词。
