Naive Bayes —— 朴素贝叶斯
前言
在前几篇文章中,我们讨论了线性回归、逻辑回归、支持向量机和KNN。这些算法有一个共同点——它们都是判别式模型(Discriminative Model) :直接学习 P(y∣x),即给定输入 x 时输出 y 的条件概率。
朴素贝叶斯(Naive Bayes) 则完全不同。它是一种生成式模型(Generative Model) :先学习联合概率分布 P(x,y),再通过贝叶斯定理计算出后验概率 P(y∣x)。
朴素贝叶斯的核心思想极其简单:假设所有特征在给定类别的情况下是相互独立的。这个假设如此“朴素”(Naive),以至于它几乎从未在真实数据中成立。但令人惊讶的是,正是这个“错误”的假设,让朴素贝叶斯在文本分类任务中大放异彩,成为NLP领域最经典的Baseline模型之一。
这篇文章,我们将从贝叶斯定理出发,一步步推导朴素贝叶斯的完整公式,深入理解三种分布假设(高斯、多项式、伯努利)的适用场景,解释拉普拉斯平滑的数学动机,最后对比生成式模型与判别式模型的本质差异。
一、贝叶斯定理与条件独立假设
1.1 贝叶斯定理回顾
贝叶斯定理描述了在已知先验概率和条件概率的情况下,如何计算后验概率:
P(y∣x)=P(x)P(x∣y)⋅P(y)其中:
- P(y):先验概率(Prior) ——在观察任何特征之前,对类别 y 的初始判断
- P(x∣y):似然(Likelihood) ——在类别 y 下观察到特征 x 的概率
- P(x):证据(Evidence) ——特征 x 出现的边缘概率,对所有类别是常数
- P(y∣x):后验概率(Posterior) ——观察到特征 x 后,属于类别 y 的概率
在分类任务中,我们只需要比较不同类别的后验概率大小,而 P(x) 对所有类别都是相同的,因此可以忽略:
P(y∣x)∝P(x∣y)⋅P(y)1.2 条件独立假设
现在的问题是:如何计算 P(x∣y)?如果 x 有 d 个特征 x=(x1,x2,...,xd),直接计算联合概率 P(x1,x2,...,xd∣y) 在现实数据中几乎是不可能的——参数数量会随着特征数量的增加而指数增长。
朴素贝叶斯的核心假设就是在这里发挥作用:
在给定类别 y 的条件下,所有特征之间是相互独立的。
用数学语言表达为:
P(x1,x2,...,xd∣y)=j=1∏dP(xj∣y)这个假设将复杂的联合概率分解为 d 个独立的条件概率的乘积,极大地简化了计算。
1.3 朴素贝叶斯的完整公式
结合贝叶斯定理和条件独立假设,朴素贝叶斯的分类决策规则为:
y^=argymaxP(y)j=1∏dP(xj∣y)这个公式的含义是:对于给定的样本,计算它在每个类别下的“得分”(先验概率 × 所有特征的条件概率的乘积),选择得分最高的类别作为预测结果。
1import numpy as np2from collections import defaultdict3
4class NaiveBayesBase:5 """朴素贝叶斯基类——包含核心的贝叶斯公式框架"""6
7 def __init__(self):8 self.class_priors = {} # P(y)9 self.class_feature_probs = {} # P(x_j | y)10 self.classes = None11
12 def _calculate_priors(self, y):13 """计算先验概率 P(y)"""14 n_samples = len(y)15 for cls in self.classes:16 self.class_priors[cls] = np.sum(y == cls) / n_samples17
18 def predict(self, X):19 """预测:选择后验概率最大的类别"""20 predictions = []21 for x in X:22 scores = {}23 for cls in self.classes:24 # log P(y) + sum(log P(x_j | y))25 # 使用 log 避免数值下溢26 score = np.log(self.class_priors[cls])27 for j, x_j in enumerate(x):28 score += np.log(self._get_feature_prob(cls, j, x_j))29 scores[cls] = score30 predictions.append(max(scores, key=scores.get))31 return np.array(predictions)32
33 def _get_feature_prob(self, cls, feature_idx, value):34 """获取 P(x_j = value | y) —— 子类实现"""35 raise NotImplementedError二、三种朴素贝叶斯分布假设
scikit-learn 提供了三种朴素贝叶斯变体,它们的核心区别在于对特征的条件概率分布 P(xj∣y) 的假设不同。
2.1 高斯朴素贝叶斯(GaussianNB)
适用场景:连续型特征,且特征服从(或近似服从)正态分布(高斯分布) 。例如身高、体重、温度、房价等。
数学形式:
对于类别 y 下的第 j 个特征,假设其服从正态分布:
P(xj∣y)=2πσyj21exp(−2σyj2(xj−μyj)2)其中 μyj 和 σyj2 分别是类别 y 下第 j 个特征的均值和方差。
1class GaussianNB(NaiveBayesBase):2 """高斯朴素贝叶斯:假设特征服从正态分布"""3
4 def fit(self, X, y):5 self.classes = np.unique(y)6 self.class_priors = {}7 self.class_stats = {} # {cls: {mean: [...], var: [...]}}8
9 for cls in self.classes:10 X_cls = X[y == cls]11 self.class_priors[cls] = len(X_cls) / len(X)12 self.class_stats[cls] = {13 'mean': np.mean(X_cls, axis=0),14 'var': np.var(X_cls, axis=0) + 1e-9 # 加小值防止除零15 }16 return self17
18 def _get_feature_prob(self, cls, feature_idx, value):19 stats = self.class_stats[cls]20 mu = stats['mean'][feature_idx]21 sigma2 = stats['var'][feature_idx]22 # 高斯分布的概率密度函数23 coef = 1 / np.sqrt(2 * np.pi * sigma2)24 exp = np.exp(-(value - mu) ** 2 / (2 * sigma2))25 return coef * exp注意:高斯朴素贝叶斯通常不用于文本分类,因为文本特征(词频)不满足正态分布。
2.2 多项式朴素贝叶斯(MultinomialNB)
适用场景:离散型计数特征,最典型的就是文本分类中的词频(Term Frequency) 。
数学形式:
假设特征 xj 是非负整数(如单词在文档中出现的次数),且服从多项分布:
P(x∣y)=∏jxj!(∑jxj)!j∏θyjxj在实践中,我们通常忽略前端的系数(因为对所有类别都相同),直接使用:
P(x∣y)∝j∏θyjxj其中 θyj=P(特征 j 出现在类别 y 的文档中),通过频率估计得到。
核心区别:多项式朴素贝叶斯关心的是“单词出现了多少次”。
1class MultinomialNB(NaiveBayesBase):2 """多项式朴素贝叶斯:适用于计数特征(如词频)"""3
4 def fit(self, X, y, alpha=1.0):5 """6 X: 词频矩阵,shape (n_samples, n_features)7 alpha: 拉普拉斯平滑参数8 """9 self.classes = np.unique(y)10 self.alpha = alpha11 self.class_priors = {}12 self.feature_probs = {} # {cls: array of P(feature_j | cls)}13
14 for cls in self.classes:15 X_cls = X[y == cls]16 # 先验概率17 self.class_priors[cls] = len(X_cls) / len(X)18 # 计算每个特征在类别 cls 下的总计数19 feature_counts = np.sum(X_cls, axis=0)20 total_counts = np.sum(feature_counts)21 # 拉普拉斯平滑22 self.feature_probs[cls] = (feature_counts + alpha) / (total_counts + alpha * X.shape[1])23 return self24
25 def _get_feature_prob(self, cls, feature_idx, value):26 return self.feature_probs[cls][feature_idx] ** value2.3 伯努利朴素贝叶斯(BernoulliNB)
适用场景:二值特征(0/1,True/False)。在文本分类中,它表示**“某个单词是否在文档中出现”**,而不是出现了多少次。
数学形式:
对于每个特征 xj∈{0,1}:
P(xj∣y)=θyjxj⋅(1−θyj)1−xj其中 θyj=P(xj=1∣y)。
核心区别:伯努利朴素贝叶斯关心的是“单词有没有出现”,而不是出现了几次。
1class BernoulliNB(NaiveBayesBase):2 """伯努利朴素贝叶斯:适用于二值特征(词是否出现)"""3
4 def fit(self, X, y, alpha=1.0):5 """6 X: 二值矩阵,shape (n_samples, n_features),值只能是 0 或 17 """8 self.classes = np.unique(y)9 self.alpha = alpha10 self.class_priors = {}11 self.feature_probs = {} # {cls: array of P(feature_j=1 | cls)}12
13 for cls in self.classes:14 X_cls = X[y == cls]15 self.class_priors[cls] = len(X_cls) / len(X)16 # 计算每个特征在类别 cls 下取值为 1 的样本数17 ones_count = np.sum(X_cls, axis=0)18 total = len(X_cls)19 # 拉普拉斯平滑(二值情况分母加 2)20 self.feature_probs[cls] = (ones_count + alpha) / (total + 2 * alpha)21 return self22
23 def _get_feature_prob(self, cls, feature_idx, value):24 p = self.feature_probs[cls][feature_idx]25 if value == 1:26 return p27 else:28 return 1 - p2.4 三种模型的对比总结
| 模型 | 特征类型 | 特征取值 | 典型场景 | 文本分类适用性 |
|---|---|---|---|---|
| GaussianNB | 连续型 | 实数 | 身高、体重、温度 | ❌ 不适用 |
| MultinomialNB | 离散计数 | 非负整数 | 词频、TF-IDF | ✅ 最常用 |
| BernoulliNB | 二值 | {0, 1} | 词是否出现 | ✅ 适用(短文本) |
如何选择:
- 对于长文档或需要词频信息的任务 → MultinomialNB
- 对于短文本(如短信、标题),词频信息有限 → BernoulliNB 可能更合适
- 两种都试一下,选择效果更好的
三、拉普拉斯平滑:从数学动机到公式推导
3.1 零概率问题
朴素贝叶斯的核心计算涉及概率的连乘:
P(y)j=1∏dP(xj∣y)这里有一个致命的问题:如果任何一个 P(xj∣y)=0,整个乘积就变成 0。
这在文本分类中尤其常见。假设训练集中没有出现“机器学习”这个词,但测试集中出现了。那么:
P(“机器学习”∣“科技类”)=0于是,任何包含“机器学习”的文档,被分类为“科技类”的概率都变成了 0——即使其他所有特征都强烈支持“科技类”。
3.2 拉普拉斯平滑的数学推导
拉普拉斯平滑(Laplace Smoothing) 的核心思想是:给每个可能的取值都“预分配”一个小的计数,避免出现零概率。
对于多项式朴素贝叶斯,特征 j 在类别 y 下的概率估计为:
θ^yj=Ny+α⋅VNyj+α其中:
- Nyj:类别 y 中特征 j 出现的总次数
- Ny:类别 y 中所有特征出现的总次数
- V:特征的总数(词汇表大小)
- α≥0:平滑参数,通常取 α=1
当 α=1 时,称为 “加一平滑”(Add-One Smoothing) 。
对于伯努利朴素贝叶斯,概率估计为:
θ^yj=Ny+2αNyj+α其中 Nyj 是类别 y 中特征 j 出现的样本数,Ny 是类别 y 的样本总数。
3.3 拉普拉斯平滑的贝叶斯解释
拉普拉斯平滑可以理解为在参数上施加了一个均匀先验(Uniform Prior) 。也就是说,我们在看到任何数据之前,先假设所有可能的取值都有相同的初始概率。
随着训练数据量的增加,平滑引入的“先验偏见”会逐渐被数据“淹没”,估计值趋近于真实概率。
3.4 代码实现:带平滑的朴素贝叶斯
1from sklearn.feature_extraction.text import CountVectorizer2from sklearn.naive_bayes import MultinomialNB, BernoulliNB3from sklearn.model_selection import train_test_split4from sklearn.metrics import accuracy_score, classification_report5
6# 示例:垃圾邮件分类7corpus = [8 "免费领取奖品 点击链接 立即注册",9 "恭喜您获得一等奖 请联系客服",10 "明天下午三点开会 请准时参加",11 "项目进度报告 请查阅附件",12 "免费 优惠 折扣 限时抢购",13 "本周五团队建设活动 自愿报名",14]15labels = [1, 1, 0, 0, 1, 0] # 1: 垃圾邮件, 0: 正常邮件16
17# 文本向量化(词频)18vectorizer = CountVectorizer()19X = vectorizer.fit_transform(corpus).toarray()20
21X_train, X_test, y_train, y_test = train_test_split(22 X, labels, test_size=0.3, random_state=4223)24
25# 多项式朴素贝叶斯(适合词频)26mnb = MultinomialNB(alpha=1.0) # alpha=1 即拉普拉斯平滑27mnb.fit(X_train, y_train)28print(f"MultinomialNB 准确率: {accuracy_score(y_test, mnb.predict(X_test)):.4f}")29
30# 伯努利朴素贝叶斯(适合词是否出现)31bnb = BernoulliNB(alpha=1.0)32bnb.fit(X_train, y_train)33print(f"BernoulliNB 准确率: {accuracy_score(y_test, bnb.predict(X_test)):.4f}")关于平滑参数的选择:
alpha=0:无平滑,可能遇到零概率问题alpha=1:标准拉普拉斯平滑alpha > 1:更强的平滑,适合词汇表很大的场景alpha < 1:较弱的平滑
四、生成式模型 vs 判别式模型
4.1 两种建模范式的根本区别
机器学习模型可以分为两大类:
| 生成式模型(Generative) | 判别式模型(Discriminative) | |
|---|---|---|
| 建模目标 | 联合分布 P(x,y) | 条件分布 P(y∣x) |
| 学习方式 | 先学数据如何生成,再推分类 | 直接学分类边界 |
| 典型代表 | 朴素贝叶斯、HMM、GDA | 逻辑回归、SVM、决策树 |
| 能否生成新样本 | ✅ 可以 | ❌ 不可以 |
| 对缺失数据的处理 | 更灵活 | 较困难 |
生成式模型的核心是:先建模数据的“生成过程” ——即特征 x 和标签 y 是如何被联合产生的——然后再用贝叶斯定理反推分类。
判别式模型的核心是:直接学习输入到输出的映射,不关心数据是如何生成的。
4.2 朴素贝叶斯 vs 逻辑回归:一对“生成-判别”搭档
朴素贝叶斯和逻辑回归构成了一对经典的生成-判别搭档(Generative-Discriminative Pair) 。
| 对比维度 | 朴素贝叶斯(生成式) | 逻辑回归(判别式) |
|---|---|---|
| 建模对象 | P(x,y) | P(y∣x) |
| 假设强度 | 强(条件独立假设) | 弱(直接建模后验) |
| 收敛速度 | 快——小样本即可 | 慢——需要更多数据 |
| 渐近误差 | 较高 | 较低 |
| 计算复杂度 | 极低 | 中等 |
| 对特征相关性的处理 | 忽略(独立性假设) | 自动学习特征间的关系 |
Andrew Ng 和 Michael Jordan 在 NIPS 2001 的经典论文中指出:
判别式学习有更低的渐近误差,但生成式分类器可能以更快的速度趋近其(较高的)渐近误差。
这意味着:
- 数据量小的时候:朴素贝叶斯往往表现更好(因为假设强、收敛快)
- 数据量很大的时候:逻辑回归往往表现更好(因为假设弱、渐近误差低)
4.3 为什么朴素贝叶斯在文本分类中如此成功?
尽管条件独立假设在现实中几乎从不成立,朴素贝叶斯在文本分类中依然表现出色。原因有三:
- 文本数据的“稀疏性” :大多数词在大多数文档中都不出现,独立性假设的破坏影响有限
- 分类决策对概率估计不敏感:我们只需要知道哪个类别的后验概率最大,而不需要精确的概率值
- 极快的训练和预测速度:朴素贝叶斯训练只需扫描一次数据,预测时只需查表和连乘
五、完整代码示例:垃圾邮件分类
1import numpy as np2from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer3from sklearn.naive_bayes import MultinomialNB, BernoulliNB, GaussianNB4from sklearn.model_selection import train_test_split, cross_val_score5from sklearn.metrics import accuracy_score, f1_score, classification_report6from sklearn.datasets import fetch_20newsgroups7
8# 加载数据:使用 20 Newsgroups 的二分类子集9categories = ['rec.sport.baseball', 'sci.space']10newsgroups = fetch_20newsgroups(subset='all', categories=categories, shuffle=True, random_state=42)11
12X = newsgroups.data13y = newsgroups.target14
15# 划分训练集和测试集16X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)17
18# ============ 使用 CountVectorizer(词频) ============19vectorizer = CountVectorizer(max_features=5000, stop_words='english')20X_train_count = vectorizer.fit_transform(X_train)21X_test_count = vectorizer.transform(X_test)22
23# 多项式朴素贝叶斯24mnb = MultinomialNB(alpha=1.0)25mnb.fit(X_train_count, y_train)26y_pred_mnb = mnb.predict(X_test_count)27print(f"MultinomialNB (词频) 准确率: {accuracy_score(y_test, y_pred_mnb):.4f}")28
29# 伯努利朴素贝叶斯(需要将计数转为二值)30X_train_binary = (X_train_count.toarray() > 0).astype(int)31X_test_binary = (X_test_count.toarray() > 0).astype(int)32bnb = BernoulliNB(alpha=1.0)33bnb.fit(X_train_binary, y_train)34y_pred_bnb = bnb.predict(X_test_binary)35print(f"BernoulliNB (二值) 准确率: {accuracy_score(y_test, y_pred_bnb):.4f}")36
37# ============ 使用 TfidfVectorizer(TF-IDF) ============38tfidf = TfidfVectorizer(max_features=5000, stop_words='english')39X_train_tfidf = tfidf.fit_transform(X_train)40X_test_tfidf = tfidf.transform(X_test)41
42mnb_tfidf = MultinomialNB(alpha=1.0)43mnb_tfidf.fit(X_train_tfidf, y_train)44y_pred_tfidf = mnb_tfidf.predict(X_test_tfidf)45print(f"MultinomialNB (TF-IDF) 准确率: {accuracy_score(y_test, y_pred_tfidf):.4f}")46
47# ============ 高斯朴素贝叶斯(不适用,仅供对比) ============48# 将稀疏矩阵转为密集数组(仅用于演示)49X_train_dense = X_train_count.toarray()50gnb = GaussianNB()51gnb.fit(X_train_dense, y_train)52y_pred_gnb = gnb.predict(X_test_count.toarray())53print(f"GaussianNB 准确率: {accuracy_score(y_test, y_pred_gnb):.4f}")54# 通常 GaussianNB 在文本分类中表现很差六、总结
| 概念 | 核心内容 |
|---|---|
| 贝叶斯定理 | P(y∥x)∝P(x∥y)P(y),后验 ∝ 似然 × 先验 |
| 条件独立假设 | P(x1,...,xd∥y)=∏P(xj∥y),朴素贝叶斯的“朴素”来源 |
| GaussianNB | 适用于连续、正态分布的特征 |
| MultinomialNB | 适用于计数特征(词频),文本分类最常用 |
| BernoulliNB | 适用于二值特征(词是否出现),适合短文本 |
| 拉普拉斯平滑 | θ^yj=(Nyj+α)/(Ny+αV),解决零概率问题 |
| 生成式 vs 判别式 | 朴素贝叶斯是生成式(建模 P(x,y)),逻辑回归是判别式(建模 P(y∥x)) |
核心要点回顾
-
朴素贝叶斯的“朴素” 来自条件独立假设——在给定类别的情况下,所有特征相互独立。这个假设几乎从不成立,但极大地简化了计算。
-
三种分布假设对应三种不同的特征类型:
- 高斯:连续值(身高、体重)
- 多项式:计数(词频)
- 伯努利:二值(词是否出现)
-
拉普拉斯平滑解决零概率问题——给每个可能的取值“预分配”一个计数,避免因训练集不完整而导致概率为 0。
-
生成式 vs 判别式:朴素贝叶斯是生成式模型,建模联合分布 P(x,y);逻辑回归是判别式模型,直接建模条件分布 P(y∣x)。数据量小时朴素贝叶斯收敛更快,数据量大时逻辑回归渐近误差更低。
-
文本分类中的选择:长文档用 MultinomialNB(词频信息重要),短文档用 BernoulliNB(是否出现更重要)。两种都试试,选效果更好的。
Some information may be outdated