本章介绍了文本数字化的两种经典方法:词袋(Bag-of-Words)和 TF-IDF。核心概念包括词袋模型、n-gram 和 TF-IDF。词袋模型将文档视为词的集合,仅统计词频,忽略了词序;n-gram 通过将连续 n 个词视为一个整体,保留部分语序,但特征数量会随 n 增大而增加。TF-IDF 通过计算词频(TF)和逆文档频率(IDF)的乘积,赋予罕见且有区分力的词更高权重,解决了词袋模型中常见词权重过高的问题。读者将学会使用这些方法将文本转化为数字特征,并应用于文本分类任务,例如使用 scikit-learn 的工具进行新闻分类。尽管这些方法简单有效,但存在忽略语义和产生稀疏矩阵的问题,这些将在后续章节中解决。
词袋与 TF-IDF
把文本变成数字,最经典的两招:词袋 (Bag-of-Words) 和 TF-IDF。这一章讲清它们是什么、怎么用、什么时候会失效。
词袋:把句子变成"词频向量"
核心思想:把文档看成"装词的袋子",只关心每个词出现几次,不关心顺序。
from sklearn.feature_extraction.text import CountVectorizer
corpus = [
"我 爱 机器学习",
"机器学习 是 一门 好 学科",
"深度学习 是 机器学习 的 子领域"
]
vec = CountVectorizer(token_pattern=r"(?u)\b\w+\b")
X = vec.fit_transform(corpus)
print(vec.get_feature_names_out())
# ['一门' '子领域' '学科' '我' '是' '机器学习' '深度学习' '的' '好']
print(X.toarray())
# [[0 0 0 1 0 1 0 0 0] <- "我 爱 机器学习"
# [1 0 1 0 1 1 0 0 1] <- "机器学习 是 一门 好 学科"
# [0 1 0 0 1 1 1 1 0]] <- "深度学习 是 机器学习 的 子领域"
每行是一个文档,每列是一个词,数值是出现次数。句子变成了数字矩阵。
n-gram:稍微保留一点语序
n-gram 把连续的 n 个词当成一个"超级词":
# ngram_range=(1, 2) 表示同时使用 1-gram 和 2-gram
vec = CountVectorizer(token_pattern=r"(?u)\b\w+\b", ngram_range=(1, 2))
X = vec.fit_transform(corpus)
print(vec.get_feature_names_out())
# ['一门' '一门 好' '学科' '我' '我 爱' '是' '是 机器学习' '机器学习' ...]
- 1-gram: 单个词 ("机器学习")
- 2-gram: 两个连续词 ("机器学习 是")
- 3-gram: 三个连续词
n 越大,特征越多,数据稀疏问题越严重。一般用 bigram 就够,trigram 谨慎用。
TF-IDF:不是所有词都同等重要
词袋的问题:"是"、"的"、"了"在每篇文档都出现,信息量为零。
TF-IDF 解决这个:给罕见的、有区分力的词更高权重。
公式:
- TF (Term Frequency): 词 t 在文档 d 中出现的次数 / 文档 d 的总词数
- IDF (Inverse Document Frequency): log(语料库总文档数 / 包含词 t 的文档数)
- TF-IDF = TF × IDF
直觉:一个词在当前文档出现多 (TF 高) 但在所有文档都出现少 (IDF 高),才是有信息量的词。
from sklearn.feature_extraction.text import TfidfVectorizer
vec = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b")
X = vec.fit_transform(corpus)
print(X.toarray())
# 每个数 = 该词在该文档的 TF-IDF 权重
# "的"、"是" 这种常见词权重会接近 0
# "子领域"、"深度学习" 这种独特的词权重高
实战:用 TF-IDF 给新闻分类
我们用 sklearn 自带的 20 类新闻数据集,跑一个完整分类流程:
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 1. 加载数据
data = fetch_20newsgroups(subset='all', categories=['sci.med', 'rec.autos'], remove=('headers','footers','quotes'))
X_train, X_test, y_train, y_test = train_test_split(data.data, data.target, test_size=0.2, random_state=42)
# 2. TF-IDF 向量化
vec = TfidfVectorizer(max_features=5000, stop_words='english', token_pattern=r"(?u)\b\w+\b")
X_train_vec = vec.fit_transform(X_train)
X_test_vec = vec.transform(X_test)
# 3. 训练逻辑回归
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train_vec, y_train)
# 4. 评估
y_pred = clf.predict(X_test_vec)
print(f"准确率: {accuracy_score(y_test, y_pred):.2%}")
# 通常 90%+
stop_words='english' 自动去常见英文停用词,max_features=5000 限制特征数避免维度爆炸。
TF-IDF 的局限
虽然好用,但有两个大问题:
- 不考虑语义: "好" 和 "棒" 是近义词,但 TF-IDF 觉得它们没关系。需要 Word2Vec / BERT 等词向量。
- 稀疏矩阵: 一篇 100 词的文档,词表可能有 10 万,矩阵是 99.9% 零。深度学习模型处理这种稀疏数据效率不高。
下一章 Word2Vec 词向量 解决第一个问题。
小结
- 词袋 (BoW): 文档 = 词频向量,简单但丢语序
- n-gram: 把连续 n 个词当 1 个,稍微保留语序,特征数会爆炸
- TF-IDF: 词频 × 逆文档频率,降权通用词,加权有区分力的词
- sklearn:
CountVectorizer和TfidfVectorizer,配LogisticRegression就能跑出 90% 准确率 - 局限: 不懂语义, 矩阵稀疏, 深度学习时代已被词向量取代
练习思考
- 给你 3 篇短新闻, 手工计算一个词 (比如"中国")的 TF、IDF、TF-IDF。
TfidfVectorizer的min_df和max_df参数分别控制什么? 为什么要设置?- 试着用
CountVectorizer(ngram_range=(2, 2))跑同一个新闻分类,准确率会变吗?为什么?
章末小测验
检验你对《词袋与 TF-IDF》的掌握程度。
TF-IDF 中的 IDF 是什么的缩写?
词袋 (Bag-of-Words) 模型最大的局限是?