查看全课大纲(第 13 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
朴素贝叶斯文本分类:垃圾邮件
小象实战讲义 · 自然语言处理
在上一节中,我们学习了语言模型如何描述语言的概率规律。本节我们将看到,这种概率化的思想如何直接应用于解决一个经典的文本分类问题——垃圾邮件过滤。通过学习朴素贝叶斯模型,你将掌握如何将贝叶斯定理与“词袋”表示相结合,构建一个简单、高效且可解释的文本分类器,并理解其背后的“朴素”假设与平滑技术。
💡 核心导读
- 贝叶斯框架的通用性:理解如何将语音识别、拼写纠错、机器翻译等任务统一到“噪声信道模型”的概率框架下。
- 朴素贝叶斯分类器:掌握如何利用贝叶斯定理,在“词袋”表示和特征条件独立性假设下,构建垃圾邮件分类器。
- 模型训练与预测:学习如何从标注数据中估计先验概率和条件概率,并用于新邮件的分类决策。
- 平滑技术:理解为何需要平滑(Smoothing)以及拉普拉斯平滑等简单方法如何解决零概率问题。
噪声信道模型:一个统一的概率框架
在自然语言处理中,许多看似不同的任务都可以归结为同一个概率问题:在观察到某种“噪声”证据 x 后,推断其背后最可能的“干净”结果 y。
这个框架被称为噪声信道模型。其数学核心是贝叶斯定理:
其中:
P(y)是语言模型:描述“干净”结果y本身在语言中出现的先验概率。例如,一个正确的单词、一句通顺的句子出现的可能性。P(x|y)是噪声模型:描述从“干净”结果y生成“带噪”观测x的概率。它刻画了信道引入的“噪声”或“变形”规律。P(y|x)是后验概率:给定观测x,我们想要推断出最可能的y,即寻找argmax_y P(y|x)。由于分母P(x)对所有y相同,因此我们只需最大化分子P(x|y)P(y)。
这个框架具有惊人的普适性:
- 语音识别:
x是声学信号,y是可能的单词序列。P(y)是语言模型(哪些词序列更常见),P(x|y)是声学模型(某个单词序列产生特定声音信号的概率)。 - 拼写纠错:
x是拼写错误的词(如 “wrok”),y是候选的正确词(如 “work”)。P(y)是单词的语言模型(“work” 比 “wark” 更常见),P(x|y)是拼写错误模型(把 “work” 错打成 “wrok” 的概率)。 - 机器翻译:
x是源语言句子,y是目标语言句子。P(y)是目标语言的语言模型,P(x|y)是翻译模型。
垃圾邮件分类也可以纳入这个框架:x 是待分类的邮件内容,y 是邮件的类别(垃圾邮件 spam 或正常邮件 ham)。我们的目标是计算 P(y=spam|x) 和 P(y=ham|x),并选择概率更高的类别。
文本表示:词袋模型
在应用概率模型之前,我们需要将非结构化的文本邮件转化为机器可处理的数值特征。最经典的方法是词袋模型。
假设我们有一个包含 V 个词的词典(例如,从所有训练邮件中收集的 50,000 个最常见词)。对于任何一封邮件,我们可以用一个长度为 V 的向量来表示它:
其中,x_i 表示词典中第 i 个词在该邮件中出现的次数(词频)或是否出现(二值,0或1)。在朴素贝叶斯的简单实现中,我们通常使用二值表示。
例如,词典为 ["免费", "购买", "会议", "报告", ...],一封邮件内容为 “免费获取报告”,其向量可能为 [1, 0, 0, 1, ...]。
词袋模型的“朴素”之处在于它完全忽略了词的顺序和句法结构。“免费获取报告”和“报告获取免费”在这种表示下是完全相同的。虽然这丢失了大量语言信息,但对于许多分类任务(如主题分类、垃圾邮件过滤)而言,词的出现与否已经提供了很强的信号。
朴素贝叶斯分类器
现在,我们将贝叶斯定理应用于垃圾邮件分类。我们的目标是计算:
其中 P(Y=c) 是先验概率,即一封邮件属于类别 c 的概率,可以直接从训练数据中估计(例如,垃圾邮件数 / 总邮件数)。
难点在于估计似然概率 P(\mathbf{x} | Y=c)。\mathbf{x} 是一个高维向量,其联合概率 P(x_1, x_2, ..., x_V | Y=c) 难以直接估计,因为可能的组合太多,数据永远不够。
这就是朴素贝叶斯做出其核心“朴素”假设的地方:假设在给定类别 Y 的条件下,特征 x_1, x_2, ..., x_V 是条件独立的。也就是说,一封垃圾邮件中出现“免费”这个词,与它是否出现“点击”这个词,在统计上是独立的(当然,现实中这并不完全成立)。
基于这个假设,联合似然可以分解为每个特征条件概率的乘积:
本节统一采用多项式事件模型(Multinomial):把一封邮件的所有词看作从一个类条件词分布里独立抽出的样本,于是似然只对文档里实际出现的词连乘(同一个词出现几次就乘几次),未出现的词不进入乘积:
这样口径是自洽的:概率由类内词频估计,似然也按词频连乘。另一种常见选择是伯努利事件模型(Bernoulli)——它对每个词都判断”出现/不出现”两次,似然里会多出 因子、参数也按”含该词的文档数”估计。两套口径各有适用面,但不能在推导里用一套、估计里用另一套,否则分类分数就不对应任何一个模型。本节后面所有例子都走多项式口径。
在实际计算中,我们通常使用对数概率来将连乘转换为连加,避免数值下溢:
定义未归一化对数评分 score_c = log P(Y=c) + Σ log P(x_i|Y=c);其中 −log P(x) 对所有类别是同一个常数,不影响比较。因此只需比较各类的 score_c,选择值更大的类别。
参数估计与平滑
模型的参数包括:
- 先验概率
P(Y=c):N_c / N,其中N_c是类别c的邮件数,N是总邮件数。 - 条件概率
P(w_i | Y=c):在类别c的邮件中,词w_i出现的概率。多项式事件模型按类内词频估计: 其中count(w_i, c)是词w_i在所有类别c邮件中出现的总次数,分母是该类所有词的总次数。
然而,这里存在一个严重问题:零概率问题。如果某个词 w_j 从未在垃圾邮件中出现过(count(w_j, spam)=0),那么对于任何包含 w_j 的新邮件,根据连乘公式,P(\mathbf{x} | Y=spam) 就会变成 0,无论其他词多么像垃圾邮件。这显然是不合理的。
解决方案是平滑。最常用的方法是拉普拉斯平滑(加一平滑)。它将估计修正为:
这里,我们给每个词的计数都加了 1,同时分母也相应增加了词典大小 V。这样,即使一个词从未出现,其概率也是一个很小的正值 1/(total words in c + V),避免了零概率。
需要澄清一个常见误读:V 不是平滑强度,而是为了让加过 1 的 V 个词的概率重新归一化才必须加的项(V 个词各得一份虚拟计数,分母就得补上 V)。真正的平滑强度是”加的那个数”——加 1 是拉普拉斯,加 就是 Lidstone,平滑越强,已见词的概率被压得越狠。
实战:手算朴素贝叶斯
让我们通过一个极简的例子,手动演练一遍朴素贝叶斯分类器的训练和预测过程。
import numpy as np
# 1. 定义极简词典和训练数据
vocab = ["免费", "赢取", "会议", "报告"] # 词典 V=4
# 训练数据:每行是一个文档的词袋表示(二值),最后是标签 (1=spam, 0=ham)
train_data = [
([1, 1, 0, 0], 1), # 文档1: "免费 赢取" -> 垃圾邮件
([1, 0, 0, 1], 1), # 文档2: "免费 报告" -> 垃圾邮件
([0, 0, 1, 1], 0), # 文档3: "会议 报告" -> 正常邮件
([0, 0, 1, 0], 0), # 文档4: "会议" -> 正常邮件
]
# 2. 训练:估计先验概率和条件概率(使用拉普拉斯平滑)
V = len(vocab)
# 统计各类别文档数
N_spam = sum(1 for _, label in train_data if label == 1)
N_ham = sum(1 for _, label in train_data if label == 0)
N_total = len(train_data)
# 先验概率
prior_spam = N_spam / N_total
prior_ham = N_ham / N_total
print(f"先验概率: P(spam)={prior_spam:.3f}, P(ham)={prior_ham:.3f}")
# 统计各类别中每个词出现的总次数
count_spam = np.zeros(V)
count_ham = np.zeros(V)
total_words_spam = 0
total_words_ham = 0
for doc_vec, label in train_data:
if label == 1: # spam
count_spam += doc_vec
total_words_spam += sum(doc_vec)
else: # ham
count_ham += doc_vec
total_words_ham += sum(doc_vec)
# 应用拉普拉斯平滑计算条件概率
cond_prob_spam = (count_spam + 1) / (total_words_spam + V)
cond_prob_ham = (count_ham + 1) / (total_words_ham + V)
print("\n条件概率 P(word | class) (拉普拉斯平滑后):")
for i, word in enumerate(vocab):
print(f" {word}: P(|spam)={cond_prob_spam[i]:.3f}, P(|ham)={cond_prob_ham[i]:.3f}")先验概率: P(spam)=0.500, P(ham)=0.500
条件概率 P(word | class) (拉普拉斯平滑后):
免费: P(|spam)=0.375, P(|ham)=0.143
赢取: P(|spam)=0.250, P(|ham)=0.143
会议: P(|spam)=0.125, P(|ham)=0.429
报告: P(|spam)=0.250, P(|ham)=0.286现在,我们使用训练好的模型来预测一个新文档 “免费 会议” (向量表示为 [1, 0, 1, 0])。
import numpy as np
# 复用同一极简训练数据,在本块内独立完成训练与预测(块间不共享变量)
vocab = ["免费", "赢取", "会议", "报告"] # 词典 V=4
train_data = [
([1, 1, 0, 0], 1), # 文档1: "免费 赢取" -> 垃圾邮件
([1, 0, 0, 1], 1), # 文档2: "免费 报告" -> 垃圾邮件
([0, 0, 1, 1], 0), # 文档3: "会议 报告" -> 正常邮件
([0, 0, 1, 0], 0), # 文档4: "会议" -> 正常邮件
]
V = len(vocab)
N_spam = sum(1 for _, label in train_data if label == 1)
N_ham = sum(1 for _, label in train_data if label == 0)
N_total = len(train_data)
prior_spam = N_spam / N_total
prior_ham = N_ham / N_total
count_spam = np.zeros(V)
count_ham = np.zeros(V)
total_words_spam = 0
total_words_ham = 0
for doc_vec, label in train_data:
if label == 1:
count_spam += doc_vec
total_words_spam += sum(doc_vec)
else:
count_ham += doc_vec
total_words_ham += sum(doc_vec)
cond_prob_spam = (count_spam + 1) / (total_words_spam + V)
cond_prob_ham = (count_ham + 1) / (total_words_ham + V)
# 3. 预测新文档
new_doc = [1, 0, 1, 0] # "免费 会议"
# 计算各类的未归一化对数评分 score_c(−log P(doc) 对各类相同,比较时忽略)
score_spam = np.log(prior_spam)
score_ham = np.log(prior_ham)
for i, present in enumerate(new_doc):
if present: # 词出现:多项式口径只把出现过的词计入似然
score_spam += np.log(cond_prob_spam[i])
score_ham += np.log(cond_prob_ham[i])
# 未出现的词不进入乘积(伯努利口径才会乘 (1-p),本节不用)
print(f"新文档的未归一化对数评分 score_c:")
print(f" score(spam) = {score_spam:.4f}")
print(f" score(ham) = {score_ham:.4f}")
if score_spam > score_ham:
print("预测结果: 垃圾邮件 (spam)")
else:
print("预测结果: 正常邮件 (ham)")新文档的未归一化对数评分 score_c:
score(spam) = -3.7534
score(ham) = -3.4864
预测结果: 正常邮件 (ham)分析:“免费”在垃圾邮件里概率更高(0.375 对 0.143),但“会议”在正常邮件里高得多(0.429 对 0.125),后者的一升一降把总分拉了回来——log 0.125 比 log 0.429 低 1.23,而“免费”只赚回 0.97。这个例子正好演示朴素贝叶斯如何把各词的对数似然当成证据加权求和:证据不是投票,是可以互相抵消的加减项。
朴素贝叶斯的特性与局限
优点:
- 简单高效:训练只需扫描一遍数据统计计数,预测速度极快。
- 可解释性强:可以通过检查
P(word|spam) / P(word|ham)的比值(似然比)来理解哪些词是区分垃圾邮件的关键。 - 对无关特征相对鲁棒:即使加入一些与分类无关的词,由于它们在两类中分布相似,对最终决策影响较小。
- 适合增量学习:有新标注数据时,只需更新计数即可。
局限(“朴素”的代价):
- 特征条件独立性假设过强:现实中,“免费”和“点击”在垃圾邮件中经常共现,其相关性被忽略。
- 词袋模型忽略词序和语义:“价格便宜”和“便宜价格”被视作相同,但语义可能不同。
- 对稀有词敏感:尽管有平滑,但训练集中出现次数极少的词,其概率估计可能不可靠。
现代视角: 虽然朴素贝叶斯因其简单性仍在某些场景(如基线模型、需要极快预测的场景)中使用,但在今天的垃圾邮件过滤等任务中,更主流的做法是使用基于词向量(如Word2Vec、FastText)的文本表示,并搭配逻辑回归、支持向量机(SVM)或深度学习模型(如TextCNN、BERT)。这些模型能够捕捉词的语义、上下文以及特征间的复杂交互,从而获得更高的分类精度。朴素贝叶斯是理解概率图模型和文本分类的绝佳起点,其核心思想——基于贝叶斯定理进行概率推断——至今仍在许多前沿模型中闪耀光芒。
📝 动手练一练
概念辨析:在噪声信道模型
P(y|x) ∝ P(x|y)P(y)中,对于垃圾邮件分类任务,P(y)和P(x|y)分别对应什么现实世界的概念?如果我们要构建一个中文拼写纠错器,y和x又分别是什么?手算与代码验证:假设我们只有以下两条训练数据:
- 垃圾邮件:
"特价 优惠" - 正常邮件:
"明天 开会"词典为["特价", "优惠", "明天", "开会"],使用二值词袋表示和拉普拉斯平滑。 a) 请手动计算P(词 | spam)和P(词 | ham)对于词典中每个词的条件概率。 b) 现在要对新邮件"特价 开会"进行分类。请手动计算两类的未归一化对数评分score_spam/score_ham(即 log 后验去掉类别无关的−log P(doc)常数项;只计算出现词的对数条件概率之和,再加上对数先验),并判断它属于哪一类。 c) 编写一个简短的 Python 代码块来验证你的手算结果。
- 垃圾邮件:
👉 点击查看参考答案
概念辨析:
- 垃圾邮件分类:
y:邮件类别(spam 或 ham)。x:邮件文本内容。P(y):垃圾邮件或正常邮件的先验概率(整体分布)。P(x|y):给定邮件类别,生成特定邮件内容的概率。在朴素贝叶斯中,它被分解为邮件中各个词出现概率的乘积。
- 中文拼写纠错:
y:正确的汉字或词序列。x:用户输入的、可能包含拼写错误的汉字或词序列。P(y):正确序列的语言模型概率(即它是否是一个通顺、常见的表达)。P(x|y):从正确序列y误输入为错误序列x的概率模型,例如基于键盘距离、拼音相似度的混淆矩阵。
- 垃圾邮件分类:
手算与代码验证: a) 手动计算条件概率: - 词典大小 V = 4,两封邮件各 2 个词,类内总词数都是 2(多项式口径按词频归一化)。 - 垃圾邮件词频:
特价:1,优惠:1,明天:0,开会:0。类内总词数 = 2。 - 正常邮件词频:特价:0,优惠:0,明天:1,开会:1。类内总词数 = 2。 - 拉普拉斯平滑(分母 = 类内总词数 + V = 6): -P(特价|spam) = (1+1)/(2+4) = 2/6 ≈ 0.333-P(优惠|spam) = (1+1)/(2+4) = 2/6 ≈ 0.333-P(明天|spam) = (0+1)/(2+4) = 1/6 ≈ 0.167-P(开会|spam) = (0+1)/(2+4) = 1/6 ≈ 0.167-P(特价|ham) = (0+1)/(2+4) = 1/6 ≈ 0.167-P(优惠|ham) = (0+1)/(2+4) = 1/6 ≈ 0.167-P(明天|ham) = (1+1)/(2+4) = 2/6 ≈ 0.333-P(开会|ham) = (1+1)/(2+4) = 2/6 ≈ 0.333b) 对新邮件分类: - 新文档向量:
[1, 0, 0, 1](特价, 开会)。 - 先验概率:P(spam)=0.5,P(ham)=0.5。 - 多项式口径只算出现的词: -score_spam = log(0.5) + log(0.333) + log(0.167) ≈ -0.6931 -1.0986 -1.7918 = -3.5835-score_ham = log(0.5) + log(0.167) + log(0.333) ≈ -0.6931 -1.7918 -1.0986 = -3.5835- 两者相等。注意这两个数值是未归一化对数评分:log P(c|doc) = score_c − log P(doc),−log P(doc)对两类相同、不影响比较,但评分本身不是对数后验。这个平局是构造出来的:两封邮件的词数相同、先验相同、两个词在两类中的概率恰好互为镜像,所以对数似然和完全对称。想打破平局,要么补充”词未出现”的信息(那就得换成伯努利口径,乘 ),要么增加训练数据让两类词频不再对称。c) 验证代码:
import numpy as np
# 训练数据
vocab = ["特价", "优惠", "明天", "开会"]
V = len(vocab)
# 统计
count_spam = np.array([1, 1, 0, 0])
count_ham = np.array([0, 0, 1, 1])
total_spam = 2
total_ham = 2
# 平滑后条件概率
cond_spam = (count_spam + 1) / (total_spam + V)
cond_ham = (count_ham + 1) / (total_ham + V)
print("P(word|spam):", [round(float(p), 3) for p in cond_spam])
print("P(word|ham):", [round(float(p), 3) for p in cond_ham])
# 预测
new_doc = np.array([1, 0, 0, 1])
prior = 0.5
score_spam = np.log(prior) + np.log(cond_spam[new_doc == 1]).sum()
score_ham = np.log(prior) + np.log(cond_ham[new_doc == 1]).sum()
print(f"未归一化对数评分 score(spam) = {score_spam:.4f}")
print(f"未归一化对数评分 score(ham) = {score_ham:.4f}")P(word|spam): [0.333, 0.333, 0.167, 0.167]
P(word|ham): [0.167, 0.167, 0.333, 0.333]
未归一化对数评分 score(spam) = -3.5835
未归一化对数评分 score(ham) = -3.5835本章小结
本节我们深入探讨了朴素贝叶斯文本分类器,它是概率图模型在NLP中最直接、最经典的应用之一。
- 噪声信道模型为我们提供了一个强大的统一视角,将语音识别、拼写纠错、机器翻译和文本分类等任务都表述为贝叶斯推断问题。
- 词袋模型是一种简单有效的文本表示方法,它将文档转化为高维稀疏向量,虽然丢失了词序信息,但为概率计算提供了基础。
- 朴素贝叶斯的核心是利用贝叶斯定理,并在“给定类别下特征条件独立”的强假设下,将复杂的联合概率估计分解为单个特征条件概率的乘积,使得模型易于训练和预测。
- 拉普拉斯平滑是解决零概率问题的关键技术,它通过给所有计数加一个小的常数,确保即使未在训练集中出现的词也有非零的概率,保证了模型的鲁棒性。
朴素贝叶斯模型以其简单、高效和可解释性,至今仍是文本分类任务中一个有用的基线模型和教学范例。
📋 行动清单
- 尝试用Python从头实现一个完整的朴素贝叶斯文本分类器,处理一个小型的真实邮件数据集(注意文本预处理,如分词、去除停用词)。
- 思考并验证:如果放弃“朴素”的独立性假设,我们该如何估计
P(x_1, x_2, ..., x_V | Y=c)?这会带来什么计算上的挑战? - 了解
scikit-learn库中的MultinomialNB和BernoulliNB类,比较它们与本节实现的多项式朴素贝叶斯有何异同(重点看两者对”词未出现”和”词频”的处理差异)。
—— 小象教研组