阶段2
|
NLP传统 · 第8课/共30课
📖 第08课:词袋模型
🎯 课程目标
词袋模型(Bag of Words)是最基础的文本表示方法,将文本转化为固定维度向量。
- 理解词袋模型的原理与局限
- 从零实现词袋向量化器
- 掌握N-gram扩展词袋模型
- 理解稀疏性问题及其影响
📐 核心原理
词袋模型的"三不"
- 不考虑顺序:"猫吃鱼"和"鱼吃猫"向量相同
- 不考虑语义:"高兴"和"开心"向量正交
- 不考虑权重:每个词平等对待
💡 N-gram改进:Bigram捕获相邻词关系,部分弥补顺序丢失。代价是特征空间爆炸。
🔬 手动实现词袋模型
手动词袋向量化器
from collections import Counter
import numpy as np
class BoWVectorizer:
def __init__(self):
self.vocab = {}
def fit(self, documents):
word_set = set()
for doc in documents:
word_set.update(doc.split())
self.vocab = {word: idx for idx, word in enumerate(sorted(word_set))}
return self
def transform(self, documents):
matrix = np.zeros((len(documents), len(self.vocab)))
for i, doc in enumerate(documents):
counts = Counter(doc.split())
for word, count in counts.items():
if word in self.vocab:
matrix[i, self.vocab[word]] = count
return matrix
def get_feature_names(self):
return sorted(self.vocab.keys(), key=lambda x: self.vocab[x])
docs = [
"我 喜欢 自然语言 处理",
"自然语言 处理 很 有趣",
"我 喜欢 机器 学习",
"深度 学习 是 机器 学习 的 子领域"
]
bow = BoWVectorizer()
bow.fit(docs)
matrix = bow.transform(docs)
print("=== 词袋模型 ===")
print(f"词表: {bow.get_feature_names()}")
print(f"矩阵形状: {matrix.shape}")
print("\n文档向量:")
for i, doc in enumerate(docs):
print(f" 文档{i+1}: {matrix[i].astype(int)}")
def cosine_sim(v1, v2):
dot = np.dot(v1, v2)
norm = np.linalg.norm(v1) * np.linalg.norm(v2)
return dot / norm if norm > 0 else 0
print("\n=== 文档相似度 ===")
for i in range(len(docs)):
for j in range(i+1, len(docs)):
sim = cosine_sim(matrix[i], matrix[j])
print(f" 文档{i+1}↔文档{j+1}: {sim:.4f}")
=== 词袋模型 ===
词表: ['喜欢', '处理', '子领域', '学习', '很', '我', '是', '有趣', '机器', '深度', '的', '自然语言']
矩阵形状: (4, 12)
文档向量:
文档1: [1 1 0 0 0 1 0 0 0 0 0 1]
文档2: [0 1 0 0 1 0 0 1 0 0 0 1]
文档3: [1 0 0 1 0 1 0 0 1 0 0 0]
文档4: [0 0 1 2 0 0 1 0 1 1 1 0]
=== 文档相似度 ===
文档1↔文档2: 0.5000
文档1↔文档3: 0.5000
文档1↔文档4: 0.0000
文档2↔文档3: 0.0000
文档2↔文档4: 0.0000
文档3↔文档4: 0.5000
✅ 验证通过:代码实机运行结果如上
🔧 N-gram词袋与稀疏性
N-gram扩展与稀疏性分析
from sklearn.feature_extraction.text import CountVectorizer
import jieba
import numpy as np
corpus = [
"自然语言处理很有趣",
"语言处理是重要技术",
"深度学习推动自然语言处理",
"计算机视觉也很重要"
]
corpus_seg = [' '.join(jieba.cut(text)) for text in corpus]
# Unigram
cv1 = CountVectorizer(token_pattern=r'(?u)\b\w+\b')
X1 = cv1.fit_transform(corpus_seg)
print("=== Unigram词袋 ===")
print(f"特征: {cv1.get_feature_names_out()}")
print(f"矩阵:\n{X1.toarray()}")
# Bigram
cv2 = CountVectorizer(ngram_range=(2,2), token_pattern=r'(?u)\b\w+\b')
X2 = cv2.fit_transform(corpus_seg)
print(f"\n=== Bigram词袋 ===")
print(f"特征: {cv2.get_feature_names_out()}")
# 稀疏性分析
total = X1.shape[0] * X1.shape[1]
nonzero = X1.count_nonzero()
print(f"\n=== 稀疏性 ===")
print(f"矩阵大小: {X1.shape}, 非零元素: {nonzero}/{total} ({nonzero/total*100:.1f}%)")
=== Unigram词袋 ===
特征: ['也' '处理' '学习' '很' '技术' '推动' '是' '有趣' '深度' '自然语言' '视觉' '计算机' '语言' '重要']
矩阵:
[[0 1 0 1 0 0 0 1 0 1 0 0 0 0]
[0 1 0 0 1 0 1 0 0 0 0 0 1 1]
[0 1 1 0 0 1 0 0 1 1 0 0 0 0]
[1 0 0 1 0 0 0 0 0 0 1 1 0 1]]
=== Bigram词袋 ===
特征: ['也 很' '处理 很' '处理 是' '学习 推动' '很 有趣' '很 重要' '推动 自然语言' '是 重要' '深度 学习'
'自然语言 处理' '视觉 也' '计算机 视觉' '语言 处理' '重要 技术']
=== 稀疏性 ===
矩阵大小: (4, 14), 非零元素: 19/56 (33.9%)
✅ 验证通过:代码实机运行结果如上
📝 练习
动手练习
- 实现一个带TF-IDF加权的词袋模型
- 对比Unigram、Bigram、Trigram在情感分类任务上的效果
- 实现特征选择(按词频阈值过滤低频/高频词)
👜
🏆 成就解锁:词袋匠人
掌握了最经典的文本向量化方法。
🧪 扩展:从词袋到神经方法
词袋模型的数学视角
词袋模型将文档映射到|V|维空间中的一个点。这个空间的维度等于词表大小,通常上万到几十万。
高维稀疏向量的问题:
- 维度灾难:距离失去区分力,所有点对距离趋于相同
- 计算开销:大矩阵运算慢
- 语义鸿沟:"开心"和"快乐"向量正交
词袋的进化路径
词袋(BoW) → TF-IDF加权 → LSA降维 → Word2Vec稠密 → BERT上下文
稀疏高维 权重优化 矩阵分解 局部语义 全局语义
每一步都在解决上一步的核心问题,但也在引入新的挑战。没有银弹——选择取决于任务和数据。
📚 扩展阅读:从稀疏到稠密
文本表示的演进
文本表示方法的本质区别在于如何编码"词的语义":
- 词袋/TF-IDF:语义=词面匹配,"开心"≠"快乐"
- LSA:通过SVD分解发现潜在语义维度,"开心"≈"快乐"
- Word2Vec/GloVe:通过上下文学习词向量,语义相似=向量相近
- ELMo/BERT:上下文化表示,"苹果"在水果和公司语境下有不同向量
每次演进都解决了上一步的核心问题,但引入了新的复杂性。词袋的简单性在很多场景下仍然是优势。
🔬 扩展实践:从词袋到句向量
词袋 → TF-IDF → LSA → 句向量
词袋模型的关键升级路径:
- 词袋 → TF-IDF:加权,降低常见词影响
- TF-IDF → LSA:SVD降维,发现潜在语义,解决稀疏性
- LSA → Word2Vec平均:用预训练词向量的平均值作为句向量
- W2V平均 → SIF:加权平均(SIF算法),效果显著提升
SIF(Smooth Inverse Frequency)的核心思想:常用词的向量贡献应该被降低权重,类似于TF-IDF对IDF的利用。
🧪 扩展:从词袋到句向量
文本表示的演进路径
- 词袋 → TF-IDF:加权,降低常见词影响
- TF-IDF → LSA:SVD降维,发现潜在语义,解决稀疏性
- LSA → Word2Vec平均:用预训练词向量的平均值作为句向量
- W2V平均 → SIF:加权平均(SIF算法),效果显著提升
SIF(Smooth Inverse Frequency)的核心思想:常用词的向量贡献应该被降低权重。
词袋模型的数学视角
词袋模型将文档映射到|V|维空间中的一个点。高维稀疏向量的问题:
- 维度灾难:距离失去区分力
- 计算开销:大矩阵运算慢
- 语义鸿沟:"开心"和"快乐"向量正交
📚 参考文献
推荐阅读
- Jurafsky & Martin. Speech and Language Processing (3rd ed). 第8课相关章节
- Manning, Raghavan & Schutze. Introduction to Information Retrieval
- Goodfellow, Bengio & Courville. Deep Learning. 自然语言处理章节
在线资源
- NLTK文档: https://www.nltk.org/
- spaCy文档: https://spacy.io/
- gensim文档: https://radimrehurek.com/gensim/
- sklearn文本处理: https://scikit-learn.org/stable/modules/feature_extraction.html
词袋模型 - 关键概念速查
| 概念 | 定义 | 应用 |
| Pipeline | 处理流水线,各步骤串行执行 | 文本处理系统 |
| Tokenization | 将文本切分为词/子词单元 | 分词、子词分割 |
| Embedding | 将离散符号映射到连续向量空间 | 词向量、句向量 |
| Feature Engineering | 从原始数据提取有效数值特征 | 传统ML模型输入 |
| Fine-tuning | 在预训练模型基础上微调 | BERT等模型适配 |
本课要点回顾
- 词袋模型是NLP技术栈的重要组件
- 理解原理比调用API更重要——知道为什么比知道怎么做更有价值
- 代码验证是学习的好方法——动手跑一遍胜过读十遍
- 从简单方法开始,逐步过渡到复杂方法——简单方法的基线作用不可替代
- 实践出真知——用自己的数据跑一遍,发现课本上没写的问题
❓ 常见问题FAQ
- Q: 这个方法的数据需求量是多少?
- A: 传统方法通常需要几百到几千条标注数据。深度学习方法通常需要万级以上。数据量少时优先用传统方法。
- Q: 如何处理中文的特殊情况?
- A: 中文没有天然词边界,分词质量直接影响下游效果。建议:1)使用领域词典优化分词 2)比较不同分词工具的效果 3)对关键任务做分词错误分析。
- Q: 传统方法和深度学习如何选择?
- A: 考虑三个因素:1)数据量——少数据用传统方法 2)可解释性——需要解释选传统方法 3)计算资源——资源受限选传统方法。深度学习在大数据+强算力场景下优势明显。
- Q: 如何评估和调优?
- A: 1)建立基线:用最简单方法确定性能下限 2)错误分析:找出模型最常犯的错误类型 3)针对性改进:根据错误模式选择改进策略 4)避免过拟合:在独立测试集上验证。
- Q: 生产环境部署要注意什么?
- A: 1)延迟要求:简单模型推理更快 2)吞吐量:批处理提升吞吐 3)模型版本:做好版本管理和回滚 4)监控:持续跟踪线上模型性能 5)A/B测试:新模型上线前小流量验证。
🛠️ 实用工具箱
| 工具 | 用途 | 安装 |
| jieba | 中文分词 | pip install jieba |
| gensim | 词向量训练 | pip install gensim |
| scikit-learn | 传统ML | pip install scikit-learn |
| nltk | 英文NLP工具 | pip install nltk |
| spaCy | 工业NLP管线 | pip install spacy |
| hanlp | 中文NLP工具包 | pip install hanlp |
| fasttext | Facebook文本分类 | pip install fasttext |
| textblob | 简单NLP接口 | pip install textblob |