7机器学习

贝叶斯分类器

概率分类

贝叶斯决策论朴素贝叶斯半朴素贝叶斯

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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)}$$

python
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)$$

最优决策:选择使条件风险最小的类别。

python
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)$$

python
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|}$$

python
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$ 的可能取值数。

python
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):将每个属性轮流作为超父,然后平均。

python
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 案例一:文本分类

python
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 案例二:不同平滑方法对比

python
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 其他分类器

python
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 算法,与朴素贝叶斯进行比较。

7/16贝叶斯分
0%