第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 的边际概率,起归一化作用
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,极大地简化了计算。
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 的可能取值数。
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)
适用于离散特征计数数据,如文本分类中的词频。
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)
适用于二值特征,如文本分类中的词是否出现(不考虑词频)。
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)
适用于连续特征,假设每个特征在给定类别下服从高斯分布。
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 垃圾邮件分类
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 特征相关性的影响
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的结果对比。