朴素贝叶斯:错的假设为何也能赢


文档摘要

朴素贝叶斯:错的假设为何也能赢 本节摘要:「朴素」(Naive)这个假设是错的,但它照样奏效——这正是朴素贝叶斯(Naive Bayes)的美妙之处。你要做文本分类:邮件分垃圾/非垃圾、客户评论分正/负、工单分类。你有几千个特征(每词一个)和有限训练数据,多数分类器在这里会噎住:逻辑回归需要足够样本才能可靠估计几千个权重;决策树一次只按一个词分裂、疯狂过拟合;KNN 在一万维里毫无意义,因为每个点到其他点都一样远。朴素贝叶斯能扛住,它做了一个数学上错的假设(给定类别后所有特征相互独立),却在文本分类上、尤其是小训练集上,胜过更「聪明」的模型。它单遍扫描训练数据即可,可扩展到百万特征。

朴素贝叶斯:错的假设为何也能赢

本节摘要:「朴素」(Naive)这个假设是错的,但它照样奏效——这正是朴素贝叶斯(Naive Bayes)的美妙之处。你要做文本分类:邮件分垃圾/非垃圾、客户评论分正/负、工单分类。你有几千个特征(每词一个)和有限训练数据,多数分类器在这里会噎住:逻辑回归需要足够样本才能可靠估计几千个权重;决策树一次只按一个词分裂、疯狂过拟合;KNN 在一万维里毫无意义,因为每个点到其他点都一样远。朴素贝叶斯能扛住,它做了一个数学上错的假设(给定类别后所有特征相互独立),却在文本分类上、尤其是小训练集上,胜过更「聪明」的模型。它单遍扫描训练数据即可,可扩展到百万特征。理解「为何一个错假设能导出好预测」会教会你机器学习的根本一课:最好的模型不是最正确的那个,而是对你的数据偏差方差权衡最佳的那个。本节将推导贝叶斯定理、独立性假设、为何排序正确就够了、拉普拉斯平滑、对数空间计算,从零实现多项式和 高斯朴素贝叶斯,并对比逻辑回归。

学习目标

阅读完本节,你应当能够:

  1. 从零实现带拉普拉斯平滑的多项式朴素贝叶斯,用于文本分类。
  2. 解释为何朴素独立性假设在数学上是错的,但在实践中能产出正确的类别排序。
  3. 对比多项式、伯努利、高斯三种朴素贝叶斯变体,为给定特征类型选对的那个。
  4. 在高维稀疏数据上评估朴素贝叶斯对比逻辑回归,解释其中的偏差方差权衡。

一、问题与直觉

你要做文本分类。邮件分垃圾/非垃圾。客户评论分正/负。工单分类。你有几千个特征(每词一个)和有限训练数据。

多数分类器在这里噎住。逻辑回归需要足够样本才能可靠估计几千个权重。决策树一次只按一个词分裂、疯狂过拟合。KNN 在一万维里毫无意义,因为每个点到其他点都一样远。

朴素贝叶斯能处理这个。它做一个数学上错的假设(给定类别后每个特征与其他特征独立),却仍在文本分类上胜过「更聪明」的模型,尤其是小训练集。它单遍扫描数据即可训练,可扩展到百万特征,产出概率估计(尽管由于独立性假设,常常校准很差)。

理解为何一个错假设能导出好预测,会教会你机器学习的根本一课:最好的模型不是最正确的那个,而是对你的数据偏差方差权衡最佳的那个。

贝叶斯定理(快速回顾)

贝叶斯定理翻转条件概率:

P(类别 | 特征) = P(特征 | 类别) * P(类别) / P(特征)

我们要的是 P(类别 | 特征)——给定文档里的词,它属于某类的概率。这可以从以下量算出:

  • P(特征 | 类别)——在该类文档里看到这些词的似然。
  • P(类别)——该类的先验概率(垃圾邮件通常有多常见?)。
  • P(特征)——证据,对所有类相同,比较时可以忽略。

P(类别 | 特征) 最高的类获胜。

朴素独立性假设

精确计算 P(特征 | 类别) 需要估计所有特征一起的联合概率。一万词的词表,你要估计一个覆盖 2^10,000 种可能组合的分布。不可能。

朴素假设:给定类别后,每个特征条件独立。

P(w1, w2, ..., wn | 类别) = P(w1 | 类别) * P(w2 | 类别) * ... * P(wn | 类别)

不是一个不可能的联合分布,而是 n 个简单的每特征分布,每个只需要一个计数。

这个假设显然是错的。「机器」和「学习」在任何文档里都不独立。但分类器不需要正确的概率估计。它需要正确的排序——哪个类概率最高。独立性假设引入系统性误差,但这些误差对所有类影响相似,于是排序保持正确。

为何它仍然有效

三个原因:

  1. 排序重于校准。 分类只需要排第一的类正确。即使 P(垃圾)=0.99999 而真实概率是 0.7,分类器仍正确选垃圾。我们不需要正确的概率,我们需要正确的赢家。

  2. 高偏差、低方差。 独立性假设是一个强先验。它重重约束模型,防过拟合。在有限训练数据下,一个略错但稳定的模型,胜过一个理论上正确但不稳定的模型。这就是偏差方差权衡在行动。

  3. 特征冗余相互抵消。 相关特征提供冗余证据。分类器把这份证据重复计数,但它对正确的类也重复计数。如果「机器」和「学习」总一起出现,两者都为「科技」类提供证据。NB 把它们计两次,但对正确的类计两次。

第四个、实践性的原因:朴素贝叶斯极快。训练是单遍扫数据数频率,预测是一次矩阵乘法。百万文档几秒训完。这种速度让你更快迭代、试更多特征集、跑更多实验。

一步步算数学

我们走一个具体例子。假设两类:垃圾和非垃圾。词表三个词:「free」「money」「meeting」。

训练数据:

  • 垃圾邮件提到「free」80 次、「money」60 次、「meeting」10 次(共 150 词次)。
  • 非垃圾邮件提到「free」5 次、「money」10 次、「meeting」100 次(共 115 词次)。
  • 40% 邮件是垃圾,60% 非垃圾。

带拉普拉斯平滑(alpha=1):

P(free | 垃圾) = (80 + 1) / (150 + 3) = 81/153 = 0.529 P(money | 垃圾) = (60 + 1) / (150 + 3) = 61/153 = 0.399 P(meeting | 垃圾) = (10 + 1) / (150 + 3) = 11/153 = 0.072 P(free | 非垃圾) = (5 + 1) / (115 + 3) = 6/118 = 0.051 P(money | 非垃圾) = (10 + 1) / (115 + 3) = 11/118 = 0.093 P(meeting | 非垃圾) = (100 + 1) / (115 + 3) = 101/118 = 0.856

新邮件含:「free」2 次、「money」1 次、「meeting」0 次。

log P(垃圾 | 邮件) = log(0.4) + 2*log(0.529) + 1*log(0.399) + 0*log(0.072) = -0.916 + 2*(-0.637) + (-0.919) + 0 = -3.109 log P(非垃圾 | 邮件) = log(0.6) + 2*log(0.051) + 1*log(0.093) + 0*log(0.856) = -0.511 + 2*(-2.976) + (-2.375) + 0 = -8.838

垃圾以巨大优势获胜。「free」出现两次是垃圾的强证据。注意「meeting」未出现对两个对数和都贡献 0(0 * log(P))——在多项式 NB 里,缺席的词无影响。是伯努利 NB 显式建模词的缺席。

三种变体

朴素贝叶斯有三种变体,各以不同方式建模 P(特征 | 类别)

多项式朴素贝叶斯(Multinomial NB)

把每个特征建模为计数。最适合特征是词频或 TF-IDF 的文本。

P(词_i | 类别) = (类别中 词_i 的计数 + alpha) / (类别中总词次 + alpha * 词表大小)

alpha 是拉普拉斯平滑。这个变体是文本分类的主力。

高斯朴素贝叶斯(Gaussian NB)

把每个特征建模为正态分布。最适合连续特征。

P(x_i | 类别) = (1 / sqrt(2 * pi * var)) * exp(-(x_i - mean)^2 / (2 * var))

每类对每特征有自己的均值和方差。当特征在每个类内真的呈钟形时效果好。

伯努利朴素贝叶斯(Bernoulli NB)

把每个特征建模为二元(出现/缺席)。最适合短文本或二元特征向量。

P(词_i | 类别) = (类别中含 词_i 的文档数 + alpha) / (类别中文档总数 + 2 * alpha)

与多项式不同,伯努利显式惩罚词的缺席。如果「free」通常在垃圾里出现而这封邮件没有它,伯努利把这算作反对垃圾的证据。

各变体何时用

变体 特征类型 最适合 例子
多项式 计数或频率 文本分类,词袋 邮件垃圾过滤,主题分类
高斯 连续值 特征近似正态的表格数据 鸢尾花分类,传感器数据
伯努利 二元(0/1) 短文本,二元特征向量 短信垃圾,出现/缺席特征

拉普拉斯平滑

当一个词在测试数据出现,但在训练数据里从未在某类中出现,会怎样?

无平滑:P(词 | 类别) = 0/N = 0。一个零乘进整个乘积,让 P(类别 | 特征) = 0,无视所有其他证据。一个未见词摧毁整个预测,无论多少其他证据支持它。

拉普拉斯平滑(Laplace Smoothing)给每个特征计数加一个小计数 alpha(通常 1):

P(词_i | 类别) = (count(词_i, 类别) + alpha) / (类别中总词次 + alpha * 词表大小)

alpha=1 时,每个词至少拿到一个小概率。测试邮件里出现的「discombobulate」不再杀死垃圾概率。平滑有贝叶斯解释:等价于在词分布上放一个均匀的狄利克雷先验。

alpha 越大平滑越强(分布更均匀)。alpha 越小模型越信数据。alpha 是你要调的超参数。

alpha 的影响:

Alpha 效果 何时用
0.001 几乎不平滑,信数据 训练集很大,不期待未见特征
0.1 轻平滑 训练集大
1.0 标准拉普拉斯平滑 默认起点
10.0 重平滑,压平分布 训练集很小,期待很多未见特征

对数空间计算

几百个概率(每个小于 1)相乘导致浮点下溢。乘积在浮点里变成 0,即使真实值是一个很小的正数。

解法:在对数空间工作。不乘概率,加它们的对数:

log P(类别 | x1, x2, ..., xn) = log P(类别) + sum_i log P(xi | 类别)

这把预测变成点积:

log_scores = X @ log_feature_probs.T + log_class_priors prediction = argmax(log_scores)

矩阵乘法。这就是朴素贝叶斯预测如此快的原因——它和单层线性模型是同一操作。

朴素贝叶斯 vs 逻辑回归

两者都是文本的线性分类器。区别在于它们建模什么。

方面 朴素贝叶斯 逻辑回归
类型 生成式(建模 P(X|Y)) 判别式(建模 P(Y|X))
训练 数频率 优化损失函数
小数据 更好(强先验有帮助) 更差(样本不足以估权重)
大数据 更差(错假设有害) 更好(灵活边界)
特征 假设独立 处理相关
速度 单遍,极快 迭代优化
校准 概率差 概率更好

经验法则:从朴素贝叶斯开始。若数据够多且 NB 到瓶颈,换逻辑回归。

分类流水线

实践中我们在对数空间工作以避免浮点下溢。与其乘许多小概率,不如加它们的对数:

log P(类别 | 特征) = log P(类别) + sum_i log P(特征_i | 类别)

二、从零实现

code/naive_bayes.py 从零实现多项式和高斯朴素贝叶斯。

MultinomialNB

从零实现:

  1. fit(X, y):对每类,数每特征频率,加拉普拉斯平滑,算对数概率,存类先验(类频率的对数)。
  2. predict_log_proba(X):对每样本,算 log P(类) + 所有类的 sum log P(特征_i | 类)。这是一次矩阵乘法:X @ log_probs.T + log_priors
  3. predict(X):返回对数概率最高的类。
class MultinomialNB: def __init__(self, alpha=1.0): self.alpha = alpha def fit(self, X, y): classes = np.unique(y) n_classes = len(classes) n_features = X.shape[1] self.classes_ = classes self.class_log_prior_ = np.zeros(n_classes) self.feature_log_prob_ = np.zeros((n_classes, n_features)) for i, c in enumerate(classes): X_c = X[y == c] self.class_log_prior_[i] = np.log(X_c.shape[0] / X.shape[0]) counts = X_c.sum(axis=0) + self.alpha self.feature_log_prob_[i] = np.log(counts / counts.sum()) return self

关键洞见:拟合后,预测就是矩阵乘法加一个偏置。这就是朴素贝叶斯如此快的原因。

GaussianNB

对连续特征,我们估计每类每特征的均值和方差:

class GaussianNB: def __init__(self): pass def fit(self, X, y): classes = np.unique(y) self.classes_ = classes self.means_ = np.zeros((len(classes), X.shape[1])) self.vars_ = np.zeros((len(classes), X.shape[1])) self.priors_ = np.zeros(len(classes)) for i, c in enumerate(classes): X_c = X[y == c] self.means_[i] = X_c.mean(axis=0) self.vars_[i] = X_c.var(axis=0) + 1e-9 self.priors_[i] = X_c.shape[0] / X.shape[0] return self

预测用每特征的高斯 PDF,跨特征相乘(对数空间相加)。

演示:文本分类

代码生成合成词袋数据,模拟两类(科技文章 vs 体育文章)。每类有不同的词频分布。MultinomialNB 用词计数分类它们。

合成数据这样构造:我们造 200 个「词」(特征列)。词 0-39 在科技文章高频、体育低频。词 80-119 在体育高频、科技低频。词 40-79 在两者都中频。这创造了一个现实场景:有些词是强类指示符,其他是噪声。

演示:连续特征

代码生成类鸢尾花数据(3 类、4 特征、高斯簇)。GaussianNB 用每类均值和方差分类。每类有不同的中心(均值向量)和不同的展宽(方差),模拟真实世界里不同类别测量值系统性不同。

代码还演示:

  • 平滑对比:用不同 alpha 训 MultinomialNB,展示平滑强度对准确率的影响。
  • 训练规模实验:NB 准确率如何随训练数据从 20 涨到 1600 而提升。NB 即使样本很少也能达到不错准确率——这是它的主要优势。
  • 混淆矩阵:每类的精确率、召回率、F1,展示 NB 在哪犯错。

预测速度

朴素贝叶斯预测是一次矩阵乘法。对 n 个样本、d 个特征、k 个类:

  • MultinomialNB:一次矩阵乘 (n x d) @ (d x k) = O(n * d * k)
  • GaussianNB:n * k 次高斯 PDF 求值,每次覆盖 d 特征 = O(n * d * k)

两者在每个维度都是线性的。对比 KNN(要对所有训练点算距离)或 RBF 核 SVM(要对所有支持向量算核)。NB 在预测时快几个数量级。

三、框架对比

用 sklearn,两种变体都是一行:

from sklearn.naive_bayes import GaussianNB, MultinomialNB gnb = GaussianNB() gnb.fit(X_train, y_train) print(f"GaussianNB 准确率: {gnb.score(X_test, y_test):.3f}") mnb = MultinomialNB(alpha=1.0) mnb.fit(X_train_counts, y_train) print(f"MultinomialNB 准确率: {mnb.score(X_test_counts, y_test):.3f}")

文本分类配 Pipeline

from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline text_clf = Pipeline([ ("vectorizer", CountVectorizer()), ("classifier", MultinomialNB(alpha=1.0)), ]) text_clf.fit(train_texts, train_labels) accuracy = text_clf.score(test_texts, test_labels)

code/naive_bayes.py 在同一数据上把从零实现与 sklearn 对比以验证正确性。

TF-IDF 配朴素贝叶斯

原始词计数给每个词每次出现同等权重。但「the」「is」在每个类都频繁出现——它们不携带信息。TF-IDF(词频-逆文档频率)降权常见词、升权稀有且具区分性的词。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import Pipeline text_clf = Pipeline([ ("tfidf", TfidfVectorizer()), ("classifier", MultinomialNB(alpha=0.1)), ])

TF-IDF 值非负,可与 MultinomialNB 配合。TF-IDF + MultinomialNB 是文本分类最强基线之一,在训练样本少于 1 万的数据集上经常击败更复杂模型。

BernoulliNB 处理短文本

对短文本(推文、短信、聊天消息),BernoulliNB 可优于 MultinomialNB。短文本词计数低,MultinomialNB 依赖的频率信息噪声大。BernoulliNB 只关心出现/缺席,在短文本上更可靠。

from sklearn.naive_bayes import BernoulliNB from sklearn.feature_extraction.text import CountVectorizer text_clf = Pipeline([ ("vectorizer", CountVectorizer(binary=True)), ("classifier", BernoulliNB(alpha=1.0)), ])

CountVectorizer 的 binary=True 把所有计数转成 0/1。没有它,BernoulliNB 仍能跑但看到的是它未为之设计的计数。

校准 NB 概率

NB 概率校准很差。NB 说 P(垃圾)=0.95 时,真实概率可能是 0.7。若你需要可靠概率估计(例如设阈值或与其他模型组合),用 sklearn 的 CalibratedClassifierCV:

from sklearn.calibration import CalibratedClassifierCV calibrated_nb = CalibratedClassifierCV(MultinomialNB(), cv=5, method="sigmoid") calibrated_nb.fit(X_train, y_train) proba = calibrated_nb.predict_proba(X_test)

这用交叉验证在 NB 的原始分数上拟合一个逻辑回归,得到的概率更接近真实类频率。

常见坑

  1. 负特征值。 MultinomialNB 要求非负特征。若有负值(如某些设置的 TF-IDF 或标准化特征),改用 GaussianNB,或把特征平移为正。

  2. 零方差特征。 GaussianNB 除以方差。若某特征在某类内方差为零(所有值相同),概率计算崩溃。代码给所有方差加一个小平滑项(1e-9)防止这个。

  3. 类别不平衡。 若 99% 邮件非垃圾,先验 P(非垃圾)=0.99 强到压倒似然证据。你可手动设类先验或用 sklearn 的 class_prior 参数。

  4. 特征缩放。 MultinomialNB 不需缩放(它处理计数)。GaussianNB 也不需缩放(它估每特征统计量)。这是相对逻辑回归和 SVM 的优势,后者对特征尺度敏感。

维度 手写实现 sklearn
校准 CalibratedClassifierCV 一键校准
稀疏数据 密集 ndarray 原生支持稀疏矩阵,大词表省内存
变体 多项式、高斯 多项式、高斯、伯努利、补集 NB

四、可复用产物

本节产出:

  • outputs/skill-naive-bayes-chooser.md——一个选对 NB 变体的决策技能。
  • code/naive_bayes.py——从零的 MultinomialNB 和 GaussianNB,配 sklearn 对比。

朴素贝叶斯何时失败

NB 在独立性假设导致错误排序(不只是错误概率)时失败。这发生在:

  1. 强特征交互。 若类别依赖两个特征的组合但不依赖任一单独特征(类 XOR 模式),NB 会完全错过。每个特征单独无证据,NB 无法非线性组合它们。

  2. 高度相关但证据相反的特征。 若特征 A 说「垃圾」、特征 B 说「非垃圾」,但 A 和 B 完美相关(现实中总一致),NB 会看到本不存在的冲突证据。

  3. 非常大的训练集。 数据足够多时,判别式模型如逻辑回归学到真实决策边界并超越 NB。在小数据上有帮助的独立性假设,现在反而拖后腿。

实践中,这些失败模式在文本分类里罕见。文本特征多、个体弱、独立性假设的误差倾向于相互抵消。对有少数强相关特征的表格数据,先考虑逻辑回归或树模型。

五、练习

  1. 平滑实验:在文本数据上用 alpha 值 0.01、0.1、1.0、10.0、100.0 训 MultinomialNB,画准确率对 alpha 的曲线。性能峰值在哪?为何极高 alpha 有害?

  2. 特征独立性测试:取一个真实文本数据集,挑两个明显相关的词(「机器」「学习」)。计算 P(词1 | 类) * P(词2 | 类) 并对比 P(词1 且 词2 | 类)。独立性假设错得多离谱?它影响分类准确率吗?

  3. 伯努利实现:给代码加 BernoulliNB 类,把词袋转成二元(出现/缺席),在文本数据上对比 MultinomialNB。伯努利何时赢?

  4. NB vs 逻辑回归:在文本数据上同时训两者,从 100 训练样本增到 1 万,画准确率对训练集大小。逻辑回归在何处超越朴素贝叶斯?

  5. 垃圾过滤器:搭一个完整垃圾分类器:对原始邮件文本分词、构建词表、造词袋特征、训 MultinomialNB、用精确率和召回率评估(不只准确率——为什么?)。

本节要点回顾

  1. 「朴素」=条件独立假设:给定类别后特征相互独立,数学上明显错误,但只在排序上引入系统性偏差,对所有类影响相似,排序仍正确。
  2. 排序重于校准:分类只需排第一的类正确,即使概率估计差(NB 说 0.99999 而真值 0.7),只要垃圾比非垃圾分数高就分对。
  3. 高偏差低方差:强先验重重约束模型防过拟合,小数据上略错但稳定的模型胜过理论正确但不稳定的模型。
  4. 三种变体各有所长:多项式(计数/TF-IDF,文本主力)、高斯(连续,表格)、伯努利(二元,短文本)。
  5. 拉普拉斯平滑防零概率:给每特征计数加 alpha(通常 1),未见词不再杀死整个预测;有狄利克雷先验的贝叶斯解释。
  6. 对数空间防下溢:把概率相乘转成对数相加,预测变成点积 X @ log_probs.T + log_priors,这就是矩阵乘法,所以 NB 预测极快。
  7. 生成式 vs 判别式:NB 建模 P(X|Y) 数频率,逻辑回归建模 P(Y|X) 优化损失;小数据 NB 赢、大数据 LR 赢。
  8. TF-IDF + MultinomialNB 是强基线:训练样本少于 1 万时经常击败更复杂模型;BernoulliNB 在短文本上更可靠。
  9. NB 概率校准差:需可靠概率时用 CalibratedClassifierCV 在原始分数上拟合逻辑回归。
  10. NB 在强交互或强相关特征上失败:类 XOR 模式、证据相反的完美相关特征、极大训练集(此时判别式模型学真实边界超越 NB)。

下一节,我们讲时间序列基础——为什么标准 ML 的随机切分在时间数据上是错的,以及如何用滚动验证和滞后特征诚实地建模与评估。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U