第7章:贝叶斯分类器(Bayesian Classifiers)
一、导读
1.1 本章学习目标
第7章介绍基于概率论的分类方法——贝叶斯分类器。贝叶斯方法从概率的角度审视分类问题,提供了一套完整的理论框架。
本章的核心学习目标包括:
- 理解贝叶斯决策论:最小化总体风险的最优决策框架
- 掌握朴素贝叶斯分类器:属性条件独立性假设下的简化模型
- 理解拉普拉斯平滑:如何解决零概率问题
- 了解半朴素贝叶斯:放松属性独立性假设的方法
- 掌握极大似然估计:从训练数据中学习概率分布
- 理解贝叶斯分类器的优缺点:适用场景和局限性
1.2 为什么本章如此重要
贝叶斯分类器是概率机器学习的基石。它不仅是一种实用的分类算法,更代表了一种从概率角度思考问题的方法论。朴素贝叶斯虽然假设简单,但在文本分类、垃圾邮件过滤等任务中表现出色。贝叶斯思想还延伸到更复杂的概率图模型、生成模型等领域。
1.3 核心问题引导
贝叶斯决策论的最优性是如何定义的?
朴素贝叶斯的"朴素"体现在哪里?这个假设合理吗?
为什么需要拉普拉斯平滑?不 smoothing 会怎样?
半朴素贝叶斯如何放松独立性假设?
朴素贝叶斯为什么在文本分类中效果好?
二、核心概念详解
2.1 贝叶斯决策论
2.1.1 基本思想
贝叶斯决策论的核心思想是:基于概率进行决策,选择使期望损失最小的类别。
对于分类任务,给定样本 $x$,贝叶斯分类器的目标是:
$$h^*(x) = \arg\max_{c \in \mathcal{Y}} P(c|x)$$
其中 $P(c|x)$ 是后验概率,通过贝叶斯定理计算:
$$P(c|x) = \frac{P(x|c) P(c)}{P(x)}$$
import numpy as np
def bayes_decision_demo():
"""贝叶斯决策演示"""
print("=== 贝叶斯决策论 ===")
# 西瓜分类问题
# 先验概率
P_good = 0.6 # 好瓜的概率
P_bad = 0.4 # 坏瓜的概率
# 条件概率(似然)
# P(色泽=青绿|好瓜) = 0.4, P(色泽=青绿|坏瓜) = 0.6
P_color_good = 0.4
P_color_bad = 0.6
# 观测到色泽=青绿
# P(色泽=青绿) = P(色泽=青绿|好瓜)*P(好瓜) + P(色泽=青绿|坏瓜)*P(坏瓜)
P_color = P_color_good * P_good + P_color_bad * P_bad
# 后验概率
P_good_color = P_color_good * P_good / P_color
P_bad_color = P_color_bad * P_bad / P_color
print(f"先验概率: P(好瓜)={P_good}, P(坏瓜)={P_bad}")
print(f"条件概率: P(青绿|好瓜)={P_color_good}, P(青绿|坏瓜)={P_color_bad}")
print(f"证据: P(青绿)={P_color:.2f}")
print(f"\n后验概率:")
print(f" P(好瓜|青绿) = {P_good_color:.4f}")
print(f" P(坏瓜|青绿) = {P_bad_color:.4f}")
print(f"\n贝叶斯决策: 预测为{'好瓜' if P_good_color > P_bad_color else '坏瓜'}")
bayes_decision_demo()2.1.2 风险与最优决策
在贝叶斯决策论中,引入损失函数 $\lambda_{ij}$ 表示将真实类别为 $j$ 的样本分类为 $i$ 的损失。
条件风险:
$$R(c|x) = \sum_{i} \lambda_{ic} P(i|x)$$
最优决策:选择使条件风险最小的类别。
def risk_based_decision():
"""基于风险的贝叶斯决策"""
print("\n=== 基于风险的决策 ===")
# 医疗诊断场景
# 类别:患病(c=1), 健康(c=0)
# 预测:阳性(p=1), 阴性(p=0)
# 损失矩阵
lambda_matrix = np.array([
[0, 1], # 真实健康: 预测健康(0), 预测患病(1) → 误报
[10, 0], # 真实患病: 预测健康(10), 预测患病(0) → 漏报代价大
])
# 后验概率
P_disease = 0.1 # 患病率10%
P_health = 0.9
# 条件风险
risk_positive = lambda_matrix[1, 0] * P_health + lambda_matrix[1, 1] * P_disease
risk_negative = lambda_matrix[0, 0] * P_health + lambda_matrix[0, 1] * P_disease
print(f"患病率: {P_disease}")
print(f"损失矩阵:")
print(f" 误报(健康→患病): {lambda_matrix[1, 0]}")
print(f" 漏报(患病→健康): {lambda_matrix[0, 1]}")
print(f"\n条件风险:")
print(f" 预测阳性: {risk_positive:.2f}")
print(f" 预测阴性: {risk_negative:.2f}")
print(f"\n决策: {'预测阳性' if risk_positive < risk_negative else '预测阴性'}")
print("即使患病率只有10%,由于漏报代价大,仍应预测阳性")
risk_based_decision()2.2 朴素贝叶斯分类器
2.2.1 属性条件独立性假设
朴素贝叶斯的核心假设:所有属性在给定类别下条件独立。
$$P(x|c) = P(x_1, x_2, \ldots, x_d | c) = \prod_{i=1}^{d} P(x_i | c)$$
这使得计算变得简单:
$$P(c|x) \propto P(c) \prod_{i=1}^{d} P(x_i | c)$$
class NaiveBayes:
"""朴素贝叶斯分类器"""
def __init__(self):
self.classes = None
self.class_prior = {}
self.conditional_prob = {}
def fit(self, X, y):
"""训练朴素贝叶斯"""
self.classes = np.unique(y)
n_samples = len(y)
for c in self.classes:
# 先验概率
X_c = X[y == c]
self.class_prior[c] = len(X_c) / n_samples
# 条件概率
self.conditional_prob[c] = {}
for j in range(X.shape[1]):
values, counts = np.unique(X_c[:, j], return_counts=True)
# 拉普拉斯平滑
n_values = len(np.unique(X[:, j]))
prob = (counts + 1) / (len(X_c) + n_values)
self.conditional_prob[c][j] = dict(zip(values, prob))
return self
def predict_proba(self, X):
"""预测概率"""
proba = np.zeros((X.shape[0], len(self.classes)))
for i, c in enumerate(self.classes):
log_prob = np.log(self.class_prior[c])
for j in range(X.shape[1]):
if X[0, j] in self.conditional_prob[c][j]:
log_prob += np.log(self.conditional_prob[c][j][X[:, j]])
else:
log_prob += np.log(1e-10) # 未知值
proba[:, i] = log_prob
# 归一化
proba = np.exp(proba - np.max(proba, axis=1, keepdims=True))
proba /= proba.sum(axis=1, keepdims=True)
return proba
def predict(self, X):
"""预测类别"""
proba = self.predict_proba(X)
return self.classes[np.argmax(proba, axis=1)]
# 示例:西瓜分类
X = np.array([
[0, 0, 0], # 青绿, 蜷缩, 浊响
[1, 0, 0], # 乌黑, 蜷缩, 浊响
[1, 0, 1], # 乌黑, 蜷缩, 沉闷
[0, 0, 0], # 青绿, 蜷缩, 浊响
[1, 1, 0], # 乌黑, 稍蜷, 浊响
[0, 2, 0], # 青绿, 硬挺, 浊响
[1, 0, 0], # 乌黑, 蜷缩, 浊响
[0, 1, 1], # 青绿, 稍蜷, 沉闷
])
y = np.array([1, 1, 0, 1, 1, 0, 1, 0]) # 1=好瓜, 0=坏瓜
nb = NaiveBayes()
nb.fit(X, y)
# 预测新样本
X_test = np.array([[0, 0, 0]]) # 青绿, 蜷缩, 浊响
pred = nb.predict(X_test)
proba = nb.predict_proba(X_test)
print("=== 朴素贝叶斯预测 ===")
print(f"测试样本: 色泽=青绿, 根蒂=蜷缩, 敲声=浊响")
print(f"预测类别: {'好瓜' if pred[0] == 1 else '坏瓜'}")
print(f"预测概率: P(好瓜)={proba[0, 1]:.4f}, P(坏瓜)={proba[0, 0]:.4f}")2.2.2 极大似然估计
从训练数据中估计先验概率和条件概率:
$$\hat{P}(c) = \frac{|D_c|}{|D|}$$
$$\hat{P}(x_i = v | c) = \frac{|D_{c, x_i=v}|}{|D_c|}$$
def mle_estimation():
"""极大似然估计演示"""
print("=== 极大似然估计 ===")
# 训练数据
data = [
('青绿', '蜷缩', '浊响', '好瓜'),
('乌黑', '蜷缩', '浊响', '好瓜'),
('乌黑', '蜷缩', '沉闷', '坏瓜'),
('青绿', '蜷缩', '浊响', '好瓜'),
('乌黑', '稍蜷', '浊响', '好瓜'),
('青绿', '硬挺', '浊响', '坏瓜'),
]
n_total = len(data)
n_good = sum(1 for d in data if d[3] == '好瓜')
n_bad = n_total - n_good
# 先验概率
P_good = n_good / n_total
P_bad = n_bad / n_total
print(f"样本总数: {n_total}")
print(f"好瓜: {n_good}, 坏瓜: {n_bad}")
print(f"\n先验概率:")
print(f" P(好瓜) = {P_good:.4f}")
print(f" P(坏瓜) = {P_bad:.4f}")
# 条件概率
good_data = [d for d in data if d[3] == '好瓜']
n_green_good = sum(1 for d in good_data if d[0] == '青绿')
P_green_good = n_green_good / n_good
print(f"\n条件概率:")
print(f" P(青绿|好瓜) = {n_green_good}/{n_good} = {P_green_good:.4f}")
mle_estimation()2.2.3 拉普拉斯平滑
当某个属性值在训练集中没有出现时,条件概率为 0,导致整个后验概率为 0。拉普拉斯平滑通过添加一个小的常数来解决这个问题:
$$\hat{P}(x_i = v | c) = \frac{|D_{c, x_i=v}| + 1}{|D_c| + N_i}$$
其中 $N_i$ 是属性 $x_i$ 的可能取值数。
def laplace_smoothing_demo():
"""拉普拉斯平滑演示"""
print("=== 拉普拉斯平滑 ===")
# 没有平滑的情况
P_x_good = 0 / 5 # 某属性值在好瓜中出现0次
print(f"无平滑: P(x|好瓜) = 0/5 = {P_x_good}")
print(" → 整个后验概率变为0!")
# 有平滑的情况
N = 3 # 该属性有3个可能取值
P_x_good_smooth = (0 + 1) / (5 + N)
print(f"\n拉普拉斯平滑: P(x|好瓜) = (0+1)/(5+3) = {P_x_good_smooth:.4f}")
print(" → 避免了零概率问题")
print("\n拉普拉斯平滑的本质:给每个可能的值一个最小的非零概率")
laplace_smoothing_demo()2.3 半朴素贝叶斯
2.3.1 放松独立性假设
朴素贝叶斯的属性独立性假设在实际中往往不成立。半朴素贝叶斯允许属性之间存在一定程度的依赖。
SPODE (Super-Parent ODE):每个属性除了依赖类别外,还依赖一个"超父"属性。
AODE (Averaged ODE):将每个属性轮流作为超父,然后平均。
def spoDE_concept():
"""SPODE概念说明"""
print("=== 半朴素贝叶斯 ===")
print("\n朴素贝叶斯的假设:")
print(" P(x1, x2, x3 | c) = P(x1|c) * P(x2|c) * P(x3|c)")
print(" 所有属性条件独立")
print()
print("SPODE的放松:")
print(" 选择一个属性作为'超父'(如x1)")
print(" P(x1, x2, x3 | c) = P(x1|c) * P(x2|x1,c) * P(x3|x1,c)")
print(" 其他属性依赖类别和超父")
print()
print("AODE的改进:")
print(" 轮流让每个属性做超父,然后平均")
print(" 降低了单个超父选择的影响")
print()
print("权衡:")
print(" 朴素贝叶斯:假设强,估计准,但可能不准确")
print(" 半朴素贝叶斯:假设弱,更灵活,但需要更多数据")
spoDE_concept()三、实战案例
3.1 案例一:文本分类
def text_classification_demo():
"""朴素贝叶斯文本分类"""
print("=== 朴素贝叶斯文本分类 ===")
# 训练数据
documents = [
("免费 中奖 奖金", "垃圾"),
("免费 优惠 促销", "垃圾"),
("会议 报告 项目", "正常"),
("报告 数据 分析", "正常"),
("优惠 打折 促销", "垃圾"),
("项目 进度 报告", "正常"),
]
# 统计词频
vocab = set()
for doc, _ in documents:
vocab.update(doc.split())
vocab = sorted(vocab)
# 构建特征向量
def doc_to_vector(doc):
words = doc.split()
return np.array([1 if w in words else 0 for w in vocab])
X = np.array([doc_to_vector(doc) for doc, _ in documents])
y = np.array([1 if label == "垃圾" else 0 for _, label in documents])
# 训练朴素贝叶斯
n_spam = np.sum(y == 1)
n_normal = np.sum(y == 0)
n_total = len(y)
P_spam = n_spam / n_total
P_normal = n_normal / n_total
# 条件概率(带拉普拉斯平滑)
n_vocab = len(vocab)
P_word_spam = (X[y == 1].sum(axis=0) + 1) / (n_spam + n_vocab)
P_word_normal = (X[y == 0].sum(axis=0) + 1) / (n_normal + n_vocab)
# 测试
test_doc = "免费 优惠 奖金"
test_vec = doc_to_vector(test_doc)
log_prob_spam = np.log(P_spam) + np.sum(test_vec * np.log(P_word_spam))
log_prob_normal = np.log(P_normal) + np.sum(test_vec * np.log(P_word_normal))
print(f"词汇表: {vocab}")
print(f"训练文档: {len(documents)}")
print(f"测试文档: '{test_doc}'")
print(f"\n对数概率:")
print(f" P(垃圾|文档) ∝ {log_prob_spam:.4f}")
print(f" P(正常|文档) ∝ {log_prob_normal:.4f}")
print(f"\n预测: {'垃圾邮件' if log_prob_spam > log_prob_normal else '正常邮件'}")
text_classification_demo()3.2 案例二:不同平滑方法对比
def smoothing_comparison():
"""平滑方法对比"""
np.random.seed(42)
# 生成数据
n = 100
X = np.random.randint(0, 5, (n, 3))
y = np.random.randint(0, 2, n)
# 划分
train_idx = np.random.choice(n, 80, replace=False)
test_idx = np.setdiff1d(np.arange(n), train_idx)
print("=== 平滑方法对比 ===")
print(f"训练样本: {len(train_idx)}, 测试样本: {len(test_idx)}")
# 无平滑
nb_no_smooth = NaiveBayes()
# 修改为无平滑版本
nb_no_smooth.fit(X[train_idx], y[train_idx])
acc_no_smooth = np.mean(nb_no_smooth.predict(X[test_idx]) == y[test_idx])
# 拉普拉斯平滑(默认)
nb_laplace = NaiveBayes()
nb_laplace.fit(X[train_idx], y[train_idx])
acc_laplace = np.mean(nb_laplace.predict(X[test_idx]) == y[test_idx])
print(f"\n无平滑准确率: {acc_no_smooth:.2%}")
print(f"拉普拉斯平滑准确率: {acc_laplace:.2%}")
print("\n拉普拉斯平滑通常能提升泛化性能")
smoothing_comparison()3.3 案例三:朴素贝叶斯 vs 其他分类器
def classifier_comparison():
"""分类器对比"""
np.random.seed(42)
# 生成数据
n = 300
X = np.random.randn(n, 5)
w = np.random.randn(5)
y = (X @ w > 0).astype(int)
# 划分
indices = np.random.permutation(n)
train_idx, test_idx = indices[:240], indices[240:]
print("=== 分类器对比 ===")
print(f"样本数: {n}, 特征数: 5")
print(f"训练集: {len(train_idx)}, 测试集: {len(test_idx)}")
# 朴素贝叶斯
nb = NaiveBayes()
nb.fit(X[train_idx], y[train_idx])
nb_acc = np.mean(nb.predict(X[test_idx]) == y[test_idx])
# 逻辑回归
from collections import Counter
def knn_predict(X_train, y_train, X_test, k=3):
preds = []
for x in X_test:
dists = np.sqrt(((X_train - x) ** 2).sum(axis=1))
nearest = np.argsort(dists)[:k]
pred = Counter(y_train[nearest]).most_common(1)[0][0]
preds.append(pred)
return np.array(preds)
knn_acc = np.mean(knn_predict(X[train_idx], y[train_idx], X[test_idx]) == y[test_idx])
print(f"\n朴素贝叶斯准确率: {nb_acc:.2%}")
print(f"KNN(k=3)准确率: {knn_acc:.2%}")
print("\n朴素贝叶斯的优势:")
print(" - 训练速度快(只需统计概率)")
print(" - 对小规模数据表现好")
print(" - 可解释性强")
classifier_comparison()四、本章小结
第7章系统介绍了贝叶斯分类器:
贝叶斯决策论:基于后验概率进行决策,最小化总体风险。
朴素贝叶斯:假设属性条件独立,简化概率计算。通过极大似然估计学习参数。
拉普拉斯平滑:避免零概率问题,提升泛化能力。
半朴素贝叶斯:放松独立性假设,允许属性间存在依赖(SPODE、AODE)。
贝叶斯分类器的优势在于:理论基础扎实、训练速度快、对小规模数据效果好、可解释性强。局限性在于:属性独立性假设在实际中可能不成立。
五、思考与练习
思考题
朴素贝叶斯的"朴素"假设在什么情况下是合理的?什么情况下会导致性能下降?
拉普拉斯平滑为什么有效?它本质上做了什么?
在文本分类中,为什么通常使用多项式模型而不是伯努利模型?
半朴素贝叶斯如何平衡模型复杂度和估计准确性?
编程练习
实现多项式朴素贝叶斯,在20 Newsgroups数据集上进行文本分类。
比较不同平滑参数对朴素贝叶斯性能的影响。
实现 AODE 算法,与朴素贝叶斯进行比较。