4统计学习方法(第2版)

朴素贝叶斯法

概率分类器

贝叶斯定理朴素贝叶斯参数估计

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

第4章:朴素贝叶斯法(Naive Bayes)

一、导读

1.1 本章学习目标

第4章介绍朴素贝叶斯法——基于贝叶斯定理和特征条件独立假设的分类方法。朴素贝叶斯是最简单的生成模型之一,它在文本分类、垃圾邮件过滤、情感分析等领域有着广泛的应用。尽管"朴素"(特征条件独立)的假设在现实中很少成立,朴素贝叶斯却常常表现出人意料地好。

本章的核心学习目标包括:

  • 理解贝叶斯定理:先验概率、似然函数、后验概率之间的关系
  • 掌握朴素贝叶斯的核心假设:特征条件独立假设的含义和影响
  • 掌握参数估计方法:极大似然估计和贝叶斯估计(拉普拉斯平滑)
  • 理解三种朴素贝叶斯模型:多项式模型、伯努利模型、高斯模型
  • 实现朴素贝叶斯分类器:从零开始编码,在文本分类任务上测试
  • 理解朴素贝叶斯的优缺点:简单高效但对特征相关性敏感

1.2 为什么本章如此重要

朴素贝叶斯是生成模型的代表。与判别模型(如感知机、逻辑斯谛回归)直接学习 P(Y|X) 不同,生成模型学习联合概率 P(X,Y),然后通过贝叶斯定理推导 P(Y|X)。这种"先理解数据如何生成,再做预测"的思路在很多场景中更加自然。

朴素贝叶斯还揭示了机器学习中一个有趣的现象:错误的假设(特征条件独立)有时也能产生好的结果。这是因为分类决策只依赖于概率的相对大小,而不是绝对值。即使概率估计不精确,只要排序正确,分类就可能是准确的。

1.3 核心问题引导

在阅读本章时,请带着以下问题思考:

贝叶斯定理的直觉含义是什么?为什么后验概率 = 似然 × 先验 / 证据?

"朴素"假设(特征条件独立)为什么叫"朴素"?这个假设在现实中成立吗?

为什么特征条件独立假设在实际中很少成立,朴素贝叶斯却仍然有效?

零概率问题是什么?拉普拉斯平滑如何解决这个问题?

多项式模型和伯努利模型有什么区别?分别适用于什么场景?

朴素贝叶斯在文本分类中为什么特别有效?


二、核心概念详解

4.1 贝叶斯定理

贝叶斯定理是概率论中最重要的定理之一。它描述了如何根据新的证据更新信念:

P(Y|X) = P(X|Y) · P(Y) / P(X)

其中:

  • P(Y):先验概率(Prior),在看到数据之前对 Y 的信念
  • P(X|Y):似然函数(Likelihood),给定 Y 时观察到 X 的概率
  • P(Y|X):后验概率(Posterior),在看到 X 之后对 Y 的更新信念
  • P(X):证据(Evidence),X 的边际概率,起归一化作用
python
import numpy as np

# 贝叶斯定理的直观示例:疾病诊断
# 某种疾病的发病率是 1%(先验概率)
# 检测的准确率是 99%(真阳性率)
# 检测的误报率是 5%(假阳性率)
# 如果一个人检测为阳性,他真正患病的概率是多少?

P_disease = 0.01          # P(患病)
P_positive_given_disease = 0.99  # P(阳性|患病)
P_positive_given_healthy = 0.05  # P(阳性|健康)

# 全概率公式
P_positive = P_positive_given_disease * P_disease + P_positive_given_healthy * (1 - P_disease)

# 贝叶斯定理
P_disease_given_positive = P_positive_given_disease * P_disease / P_positive

print(f"先验概率 P(患病) = {P_disease}")
print(f"检测阳性后 P(患病|阳性) = {P_disease_given_positive:.4f}")
print(f"\n直觉:虽然检测准确率99%,但阳性结果中只有{P_disease_given_positive:.1%}真正患病")
print(f"原因:疾病本身很罕见(1%),即使假阳性率只有5%,绝对数量也远超真阳性")

4.2 朴素贝叶斯分类器

4.2.1 从贝叶斯定理到分类

在分类问题中,我们想求后验概率 P(Y=c_k|X=x),然后选择概率最大的类别:

y = argmax_{c_k} P(c_k | x) = argmax_{c_k} P(x | c_k) · P(c_k) / P(x)

由于 P(x) 对所有类别相同,可以忽略:

y = argmax_{c_k} P(x | c_k) · P(c_k)

4.2.2 朴素假设:特征条件独立

直接计算 P(x|c_k) 是困难的——如果 x 有 d 个特征,每个特征有 v 个取值,需要估计 v^d 个参数。

朴素贝叶斯的核心假设是:在给定类别的条件下,各特征相互独立。

P(x | c_k) = P(x_1, x_2, ..., x_d | c_k) = Π_i P(x_i | c_k)

这个假设将参数数量从 v^d 降低到 v·d,极大地简化了计算。

python
import numpy as np

class NaiveBayes:
    def __init__(self):
        self.classes = None
        self.class_prior = {}
        self.feature_prob = {}
    
    def fit(self, X, y):
        self.classes = np.unique(y)
        n_samples, n_features = X.shape
        
        for c in self.classes:
            X_c = X[y == c]
            
            # 先验概率 P(Y=c)
            self.class_prior[c] = len(X_c) / n_samples
            
            # 条件概率 P(X_j=x_j | Y=c),对每个特征
            self.feature_prob[c] = {}
            for j in range(n_features):
                values, counts = np.unique(X_c[:, j], return_counts=True)
                # 拉普拉斯平滑
                probs = (counts + 1) / (len(X_c) + len(values))
                self.feature_prob[c][j] = dict(zip(values, probs))
        
        return self
    
    def predict_one(self, x):
        scores = {}
        for c in self.classes:
            # log P(Y=c) + Σ log P(X_j=x_j | Y=c)
            score = np.log(self.class_prior[c])
            for j, xj in enumerate(x):
                prob = self.feature_prob[c][j].get(xj, 1e-10)
                score += np.log(prob)
            scores[c] = score
        return max(scores, key=scores.get)
    
    def predict(self, X):
        return np.array([self.predict_one(x) for x in X])

# 示例:简单分类
X_train = np.array([
    [1, 1, 1], [1, 1, 0], [1, 0, 1], [1, 0, 0],
    [0, 1, 1], [0, 1, 0], [0, 0, 1], [0, 0, 0],
    [1, 1, 1], [1, 1, 0], [0, 0, 0], [0, 0, 1],
])
y_train = np.array([1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0])

nb = NaiveBayes()
nb.fit(X_train, y_train)

X_test = np.array([[1, 1, 0], [0, 0, 1], [1, 0, 1]])
predictions = nb.predict(X_test)
print(f"测试数据: {X_test.tolist()}")
print(f"预测结果: {predictions}")

4.3 参数估计

4.3.1 极大似然估计

极大似然估计(MLE)是最直接的参数估计方法。对于离散特征:

P(X_j = a_{jl} | Y = c_k) = Σ_i I(x_i^{(j)} = a_{jl}, y_i = c_k) / Σ_i I(y_i = c_k)

即:在类别 c_k 的样本中,特征 j 取值为 a_{jl} 的频率。

4.3.2 贝叶斯估计(拉普拉斯平滑)

极大似然估计有一个严重问题:如果某个特征值在训练集中没有出现,概率就是0。由于朴素贝叶斯将所有特征概率相乘,任何一个0都会导致整个后验概率为0。

拉普拉斯平滑(Laplace Smoothing)通过给每个计数加1来解决这个问题:

P(X_j = a_{jl} | Y = c_k) = (Σ_i I(...) + 1) / (Σ_i I(y_i = c_k) + S_j)

其中 S_j 是特征 j 的可能取值数。

python
import numpy as np

def mle_vs_laplace():
    """对比极大似然估计和拉普拉斯平滑"""
    # 假设训练数据中,类别1有10个样本
    # 特征A在类别1中的取值分布:A=1出现8次,A=2出现2次,A=3出现0次
    
    n_class = 10
    counts = {1: 8, 2: 2, 3: 0}
    n_values = 3
    
    print("极大似然估计:")
    for v in [1, 2, 3]:
        p = counts[v] / n_class
        print(f"  P(A={v}|Y=1) = {counts[v]}/{n_class} = {p:.4f}")
    
    print("\n拉普拉斯平滑:")
    for v in [1, 2, 3]:
        p = (counts[v] + 1) / (n_class + n_values)
        print(f"  P(A={v}|Y=1) = ({counts[v]}+1)/({n_class}+{n_values}) = {p:.4f}")
    
    print("\n问题:如果用MLE,当测试数据A=3时,P(A=3|Y=1)=0")
    print("整个后验概率变成0,无论其他特征多么支持类别1")

mle_vs_laplace()

4.4 三种朴素贝叶斯模型

4.4.1 多项式模型(Multinomial NB)

适用于离散特征计数数据,如文本分类中的词频。

python
import numpy as np

class MultinomialNB:
    def __init__(self, alpha=1.0):
        self.alpha = alpha  # 平滑参数
    
    def fit(self, X, y):
        self.classes = np.unique(y)
        n_samples, n_features = X.shape
        
        self.class_log_prior = {}
        self.feature_log_prob = {}
        
        for c in self.classes:
            X_c = X[y == c]
            self.class_log_prior[c] = np.log(len(X_c) / n_samples)
            
            # 拉普拉斯平滑
            feature_counts = X_c.sum(axis=0) + self.alpha
            self.feature_log_prob[c] = np.log(
                feature_counts / (feature_counts.sum() + self.alpha * n_features)
            )
        
        return self
    
    def predict_one(self, x):
        scores = {}
        for c in self.classes:
            scores[c] = self.class_log_prior[c] + x @ self.feature_log_prob[c]
        return max(scores, key=scores.get)
    
    def predict(self, X):
        return np.array([self.predict_one(x) for x in X])
    
    def predict_proba(self, X):
        scores = {}
        for c in self.classes:
            scores[c] = self.class_log_prior[c] + X @ self.feature_log_prob[c]
        
        # softmax归一化
        max_score = max(scores.values())
        exp_scores = {c: np.exp(s - max_score) for c, s in scores.items()}
        total = sum(exp_scores.values())
        return {c: v / total for c, v in exp_scores.items()}

# 文本分类示例
# 词频矩阵:每行是一个文档,每列是一个词的词频
X_train = np.array([
    [2, 1, 0, 0, 1],  # 文档1
    [1, 0, 1, 0, 0],  # 文档2
    [0, 0, 0, 2, 1],  # 文档3
    [3, 1, 0, 0, 0],  # 文档4
    [0, 0, 2, 1, 0],  # 文档5
    [1, 0, 0, 1, 1],  # 文档6
])
y_train = np.array([1, 1, 0, 1, 0, 0])  # 1=体育, 0=科技

mnb = MultinomialNB(alpha=1.0)
mnb.fit(X_train, y_train)

X_test = np.array([[2, 0, 0, 0, 1], [0, 1, 1, 0, 0]])
print(f"预测: {mnb.predict(X_test)}")
print(f"1=体育, 0=科技")

4.4.2 伯努利模型(Bernoulli NB)

适用于二值特征,如文本分类中的词是否出现(不考虑词频)。

python
import numpy as np

class BernoulliNB:
    def __init__(self, alpha=1.0):
        self.alpha = alpha
    
    def fit(self, X, y):
        self.classes = np.unique(y)
        n_samples, n_features = X.shape
        
        self.class_log_prior = {}
        self.feature_log_prob = {}
        self.feature_log_neg_prob = {}
        
        for c in self.classes:
            X_c = X[y == c]
            self.class_log_prior[c] = np.log(len(X_c) / n_samples)
            
            # P(X_j=1 | Y=c)
            prob = (X_c.sum(axis=0) + self.alpha) / (len(X_c) + 2 * self.alpha)
            self.feature_log_prob[c] = np.log(prob)
            self.feature_log_neg_prob[c] = np.log(1 - prob)
        
        return self
    
    def predict_one(self, x):
        scores = {}
        for c in self.classes:
            score = self.class_log_prior[c]
            for j in range(len(x)):
                if x[j] == 1:
                    score += self.feature_log_prob[c][j]
                else:
                    score += self.feature_log_neg_prob[c][j]
            scores[c] = score
        return max(scores, key=scores.get)
    
    def predict(self, X):
        return np.array([self.predict_one(x) for x in X])

# 二值特征示例(词是否出现)
X_train = np.array([
    [1, 1, 0, 0, 1],
    [1, 0, 1, 0, 0],
    [0, 0, 0, 1, 1],
    [1, 1, 0, 0, 0],
    [0, 0, 1, 1, 0],
    [1, 0, 0, 1, 1],
])
y_train = np.array([1, 1, 0, 1, 0, 0])

bnb = BernoulliNB()
bnb.fit(X_train, y_train)
print(f"伯努利NB预测: {bnb.predict(np.array([[1, 0, 0, 0, 1]]))}")

4.4.3 高斯模型(Gaussian NB)

适用于连续特征,假设每个特征在给定类别下服从高斯分布。

python
import numpy as np

class GaussianNB:
    def fit(self, X, y):
        self.classes = np.unique(y)
        self.class_prior = {}
        self.mean = {}
        self.var = {}
        
        for c in self.classes:
            X_c = X[y == c]
            self.class_prior[c] = len(X_c) / len(y)
            self.mean[c] = X_c.mean(axis=0)
            self.var[c] = X_c.var(axis=0) + 1e-9
        
        return self
    
    def _gaussian_pdf(self, x, mean, var):
        return (1 / np.sqrt(2 * np.pi * var)) * np.exp(-0.5 * (x - mean) ** 2 / var)
    
    def predict_one(self, x):
        scores = {}
        for c in self.classes:
            score = np.log(self.class_prior[c])
            score += np.sum(np.log(self._gaussian_pdf(x, self.mean[c], self.var[c])))
            scores[c] = score
        return max(scores, key=scores.get)
    
    def predict(self, X):
        return np.array([self.predict_one(x) for x in X])

# 连续特征示例
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target

gnb = GaussianNB()
gnb.fit(X[:120], y[:120])
accuracy = np.mean(gnb.predict(X[120:]) == y[120:])
print(f"高斯NB在鸢尾花数据集上的准确率: {accuracy:.4f}")

4.5 朴素贝叶斯的优缺点

优点:

  • 简单高效,训练和预测都很快
  • 对小规模数据表现好
  • 可以处理多分类问题
  • 对缺失数据不敏感
  • 可解释性强

缺点:

  • 特征条件独立假设通常不成立
  • 对特征相关性敏感
  • 需要估计概率分布时可能不准确

三、实战案例

3.1 垃圾邮件分类

python
import numpy as np

def simple_text_classification():
    """简化的文本分类示例"""
    # 训练数据:简化的词频矩阵
    # 特征:[免费, 中奖, 会议, 报告, 项目,  urgent, click, money]
    X_train = np.array([
        [3, 2, 0, 0, 0, 2, 3, 1],  # 垃圾邮件
        [0, 0, 2, 3, 2, 0, 0, 0],  # 正常邮件
        [2, 1, 0, 0, 0, 1, 2, 2],  # 垃圾邮件
        [0, 0, 3, 2, 1, 0, 0, 0],  # 正常邮件
        [1, 0, 0, 0, 0, 3, 1, 1],  # 垃圾邮件
        [0, 0, 1, 4, 3, 0, 0, 0],  # 正常邮件
        [4, 1, 0, 0, 0, 2, 4, 3],  # 垃圾邮件
        [0, 0, 2, 1, 2, 0, 0, 0],  # 正常邮件
    ])
    y_train = np.array([1, 0, 1, 0, 1, 0, 1, 0])  # 1=垃圾, 0=正常
    
    mnb = MultinomialNB(alpha=1.0)
    mnb.fit(X_train, y_train)
    
    # 测试
    X_test = np.array([
        [2, 1, 0, 0, 0, 1, 2, 1],  # 疑似垃圾
        [0, 0, 2, 2, 1, 0, 0, 0],  # 疑似正常
    ])
    
    predictions = mnb.predict(X_test)
    labels = {0: '正常邮件', 1: '垃圾邮件'}
    for i, pred in enumerate(predictions):
        print(f"文档{i+1}: {labels[pred]}")

simple_text_classification()

3.2 特征相关性的影响

python
import numpy as np

def feature_correlation_effect():
    """演示特征相关性对朴素贝叶斯的影响"""
    np.random.seed(42)
    n = 500
    
    # 场景1:特征独立
    X1_independent = np.column_stack([
        np.random.randn(n),
        np.random.randn(n),
    ])
    y1 = (X1_independent[:, 0] + X1_independent[:, 1] > 0).astype(int)
    
    # 场景2:特征高度相关
    x_base = np.random.randn(n)
    X2_correlated = np.column_stack([
        x_base + np.random.randn(n) * 0.1,
        x_base + np.random.randn(n) * 0.1,
    ])
    y2 = (x_base > 0).astype(int)
    
    # 在两个场景上训练高斯NB
    gnb1 = GaussianNB()
    gnb1.fit(X1_independent[:400], y1[:400])
    acc1 = np.mean(gnb1.predict(X1_independent[400:]) == y1[400:])
    
    gnb2 = GaussianNB()
    gnb2.fit(X2_correlated[:400], y2[:400])
    acc2 = np.mean(gnb2.predict(X2_correlated[400:]) == y2[400:])
    
    print(f"特征独立时准确率: {acc1:.4f}")
    print(f"特征相关时准确率: {acc2:.4f}")
    print("特征相关性导致NB的概率估计不准确,但分类可能仍然正确")

feature_correlation_effect()

四、本章小结

第4章介绍了朴素贝叶斯法:

贝叶斯定理:后验概率 = 似然 × 先验 / 证据。它提供了一种在观察到数据后更新信念的数学框架。

朴素假设:特征条件独立假设将参数数量从指数级降低到线性级,使计算变得可行。

参数估计:极大似然估计可能产生零概率问题,拉普拉斯平滑通过加1避免这个问题。

三种模型:多项式模型适用于词频数据,伯努利模型适用于二值数据,高斯模型适用于连续数据。

实际应用:朴素贝叶斯在文本分类中特别有效,因为它简单、高效、可扩展。

朴素贝叶斯的核心启示是:一个简单的模型,即使假设不完全正确,也可以在实践中表现良好。


五、思考与练习

思考题

朴素贝叶斯的"朴素"假设在什么情况下会导致严重的性能下降?能否构造一个反例?

拉普拉斯平滑的参数 α 对模型有什么影响?α 越大越好还是越小越好?

多项式模型和伯努利模型在文本分类中有什么区别?什么时候用哪个?

朴素贝叶斯是生成模型。与判别模型(如逻辑斯谛回归)相比,它在什么情况下更有优势?

如何处理连续特征的朴素贝叶斯?除了高斯模型,还有什么方法?

编程练习

从零实现多项式朴素贝叶斯,在一个简单的文本分类任务上测试效果。

实现拉普拉斯平滑,对比平滑前后在稀疏数据上的效果差异。

在鸢尾花数据集上实现高斯朴素贝叶斯,与sklearn的结果对比。


4/12朴素贝叶
0%