贝叶斯分类器原理与Python实现:从公式到避坑指南

发布时间:2026/9/26 1:20:15
贝叶斯分类器原理与Python实现:从公式到避坑指南 简介南大《机器学习导论》第7章贝叶斯分类器课件是面向机器学习、人工智能初学者的经典教学材料系统讲解贝叶斯决策论、后验概率与条件风险、贝叶斯判定准则、极大似然估计、朴素贝叶斯分类器、半朴素贝叶斯分类器以及贝叶斯网等核心内容。课件以概率框架下的分类决策为主线对比判别式与生成式模型重点剖析属性独立性假设、拉普拉斯修正、SPODE、TAN、AODE等扩展策略并借助贝叶斯网的有向无环图、条件概率表、道德图与有向分离来说明复杂依赖关系。整个资源包只有1个PDF文件大小约1.05MB轻量便携适合正在系统学习机器学习导论课程或复习分类器原理的读者。目前已有118人学习下载。课件保留了课堂原汁原味的公式推导和图示能帮助读者深入理解贝叶斯分类器的数学推导、适用场景与实现思路并为后续集成学习、聚类等内容的学习做好铺垫也便于考前速览和知识查漏。1. 期末看南大《机器学习导论》第07章贝叶斯分类器23页为什么够用期末周把南大《机器学习导论》第7章这23页PDF翻了三遍题目还是不会写这种感觉我太熟悉了。贝叶斯分类器是典型的“公式一看就懂一写代码就翻车”的模型它解决的是带不确定性的分类问题垃圾邮件过滤、文本情感、医疗筛查里都能见到核心就一句话——用先验和似然算出后验选后验最大的类当答案。适合谁读正在入门机器学习、期末复习、或者只想快速做出第一个文本分类 baseline 的人。这篇会把课件里没写透的推导、Python 复现路径、平滑参数怎么设以及五个高频踩坑点一次讲清楚。2. 贝叶斯分类器先把原理立在手上从贝叶斯定理到可写代码的判别函数2.1 最小错误率决策后验概率是“证据更新后的信念”贝叶斯分类器不是一个具体算法而是一族用贝叶斯定理做决策的方法。设类别为 c样本特征为 x我们想要的是后验概率 P(c|x)也就是看到样本之后它属于 c 的概率。贝叶斯定理把它拆成三块P(c|x) P(x|c) * P(c) / P(x)P(c) 是先验来自训练集里各类别的占比P(x|c) 是似然描述“类别 c 长什么样”P(x) 是证据对所有类别都一样。做预测时只比较分子大小所以分母可以整个丢掉决策规则变成c_hat argmax_c P(c) * P(x|c)这个 argmax 就是最小错误率决策。只要 P(c|x) 估计得准选择后验最大的类就是在 0-1 损失下的最优解。举一个马上能算的例子。1000 封邮件里垃圾邮件 300 封先验 P(垃圾)0.3。垃圾邮件里出现“发票”的概率是 0.5正常邮件里是 0.05。现在来了一封带“发票”的邮件P(垃圾|发票) 0.3×0.5 / (0.3×0.5 0.7×0.05) ≈ 0.81这个 0.81 就是“证据更新后的信念”。朴素贝叶斯的全部工作就是把 P(x|c) 估计出来然后套这个公式。2.2 朴素贝叶斯为什么“敢”朴素独立性假设把复杂度从指数降到线性直接估计 P(x|c) 在特征多的时候是灾难。假设 d 个二值特征给定类别后需要估计 2^d 个参数d20 时就是 100 万个组合训练集根本喂不饱。朴素贝叶斯做了一个很强的假设给定类别后每个特征相互独立。于是P(x|c) P(x1|c) × P(x2|c) × … × P(xd|c)参数数量从指数级降到线性级每个特征只需要估计它自己的条件概率。这就是“朴素”的含义——明知假设不一定成立但为了让计算可行先这么干。从模型分类看朴素贝叶斯属于生成式模型它先分别描述每一类数据的分布再用贝叶斯公式找边界。逻辑回归是判别式模型直接学 P(c|x) 的决策边界。两者各有各的适用场景但做文本分类的第一个 baseline我几乎总是先上朴素贝叶斯因为它快、稳、对高维稀疏特征友好。2.3 特征类型决定似然函数连续、离散、混合特征的参数表P(x|c) 具体怎么算取决于特征是什么类型。拿到一份讲义最先要看的就是它把特征当成哪种分布。特征形态似然模型要估计的参数sklearn 对应类连续实数高斯分布每类每特征的均值、方差GaussianNB0/1 二值伯努利分布每类每特征出现的概率BernoulliNB非负计数多项式分布每类每个词/特征占比MultinomialNB连续但多峰核密度估计带宽 h需自行封装连续特征最常用高斯似然对每个类别 c 和每个特征 i 估计均值 μci 和方差 σ²ci然后把 x_i 带入高斯密度函数取对数log P(x_i|c) -0.5×log(2πσ²ci) - (x_i - μci)² / (2σ²ci)二值和计数特征用频率统计再平滑。如果一份数据里同时有连续和离散特征常见做法是分别建模最后在 log 域相加sklearn 里可以用 FeatureUnion 拼但工程上我更多是先做分箱或直接用梯度提升树省去分布假设的麻烦。2.4 log 域改造把连乘变成连加再谈拉普拉斯平滑刚才的决策规则是连乘。d 很大时每个概率都小于 1连乘结果会小到浮点数直接归零这叫概率下溢。所以任何工程实现都要先取对数score(c) log P(c) Σ_i log P(x_i|c)log 是单调函数取 log 不改变 argmax 的结果却能把乘法变成加法数值稳定得多。写成伪代码就是def nb_score(x, c, model): score np.log(model.prior[c]) for i in range(len(x)): score np.log(gaussian_pdf(x[i], model.mean[c][i], model.var[c][i])) return score这段代码是所有朴素贝叶斯实现的主干先加先验的对数再加每个特征对数似然最后比大小。后面第 3 章的完整实现就是它的填空版。离散特征还需要加平滑。如果不平滑训练集里某个词在某类中一次都没出现P(x_i|c)0整个连乘直接归零后验全被打没。拉普拉斯平滑公式是P(x_i|c) (N_ci α) / (N_c α × n_i)N_ci 是特征 i 在类别 c 中出现的次数N_c 是类别 c 的样本数n_i 是特征 i 的取值个数α 是平滑强度。α1 就叫拉普拉斯平滑α 小于 1 时叫 Lidstone 平滑。连续特征不套这个而是给方差加一个极小膨胀系数 var_smoothing防止某类只有一个样本时方差为 0log 里出现负无穷。3. 从 PDF 到能跑用 Python 复现高斯朴素贝叶斯并与 sklearn 对齐3.1 环境准备先把最小闭环跑通动手之前建一个干净的虚拟环境。下面的命令在任何 macOS 或 Linux 终端都通用Windows 把 source 换成 .venv\Scripts\activate 即可。python -m venv .venv source .venv/bin/activate pip install numpy scikit-learn这里只需要 numpy 和 scikit-learn。numpy 用来手写高斯似然scikit-learn 用来做对照验证。装完后在 Python 里执行 import sklearn 不报错环境就算就绪。3.2 从零实现一个 GaussianNBfit 只存三类量我手写实现时故意不用 sklearn 内部接口用 numpy 把 fit 和 predict 拆开这样每一步都能看到数据流。fit 阶段对每个类别只做三件事算先验、算均值、算方差。import numpy as np class MyGaussianNB: def __init__(self, var_smoothing1e-9): self.var_smoothing var_smoothing def fit(self, X, y): self.classes_ np.unique(y) self.priors_ {} self.means_ {} self.vars_ {} n len(X) for c in self.classes_: Xc X[y c] self.priors_[c] len(Xc) / n self.means_[c] Xc.mean(axis0) self.vars_[c] Xc.var(axis0) self.var_smoothing return self def _log_likelihood(self, x, c): var self.vars_[c] eps 1e-12 log_prob -0.5 * np.sum(np.log(2 * np.pi * var eps)) log_prob - 0.5 * np.sum((x - self.means_[c]) ** 2 / var) return log_prob def predict(self, X): preds [] for x in X: scores [] for c in self.classes_: score np.log(self.priors_[c]) self._log_likelihood(x, c) scores.append((score, c)) preds.append(max(scores, keylambda t: t[0])[1]) return np.array(preds)这段代码的逻辑很直白fit 把先验、每类均值、每类方差存成字典predict 对每个样本逐个类计算对数得分取最大者。var_smoothing 是加到每个特征方差上的膨胀系数默认 1e-9防止某个类别只出现一次时方差为 0。对数似然里 eps 的作用是防止 log(0)。var 已经被平滑过理论上有 1e-9 的兜底但数值计算中保留一个极小正数更保险。这个实现没有做矩阵化循环在千万级样本上会慢但作为理解算法的范本牺牲性能换可读性完全值得。3.3 与 scikit-learn 对齐验证同一个数据集上对比准确率手写实现的正确性不能靠“我感觉对”来判断要和 sklearn 在同一个测试集上对拍。用鸢尾花数据集跑一遍from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score from sklearn.naive_bayes import GaussianNB X, y load_iris(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) my_clf MyGaussianNB(var_smoothing1e-9).fit(X_train, y_train) sk_clf GaussianNB(var_smoothing1e-9).fit(X_train, y_train) print(手写准确率:, accuracy_score(y_test, my_clf.predict(X_test))) print(sklearn准确率:, accuracy_score(y_test, sk_clf.predict(X_test)))正常情况下两个准确率只差一个样本以内甚至完全相等。如果差得多优先检查三件事方差是总体方差还是样本方差sklearn 和我的手写版用的都是总体方差var_smoothing 是否设置一致先验是否都是用样本频率估计。sklearn 内部对数值做了额外保护比如 _joint_log_likelihood 里会处理极端的方差值。手工实现只要逻辑正确、量级一致不需要追求每一步都对照到小数点后几位。这个对拍本身就是最好的测试用例。3.4 文本分类标准姿势MultinomialNB 与 BernoulliNB 的选择连续特征用 GaussianNB文本这类离散计数特征就要换模型。MultinomialNB 适合“词出现了多少次”的计数特征BernoulliNB 适合“词是否出现”的 0/1 特征。用一个小到不能再小的中文演示from sklearn.feature_extraction.text import CountVectorizer from sklearn.naive_bayes import MultinomialNB from sklearn.pipeline import make_pipeline docs [ 特效不错剧情也很紧凑, 画面很美推荐, 剧情拖沓浪费时间, 演技尴尬看不下去, ] y [1, 1, 0, 0] pipe make_pipeline( CountVectorizer(analyzerchar, ngram_range(1, 2)), MultinomialNB(alpha1.0) ) pipe.fit(docs, y) print(pipe.predict([剧情不错特效也还可以]))这里用 char n-gram 是因为演示样本太少按词切分几乎每个词只出现一次。现实项目里文本至少几百条起步再用 CountVectorizer 默认的按词切分。alpha1.0 就是拉普拉斯平滑防止测试文本出现训练集里没见过的字或词后概率直接归零。alpha 是 MultinomialNB 最重要的参数。α 越大概率越向均匀分布靠拢对噪声越钝感α 越小越依赖训练集计数在小样本上容易过拟合。我从 1.0 起步调参时在 0.1 到 5.0 之间按对数网格搜索很少超过 10。4. 拿到这份 23 页 PPT 后的学习路线先扫页码再手推最后复现4.1 全篇只有 23 页先按这张表把公式位置标出来拿到 PDF 先别从头精读。南大这份《机器学习导论》第 7 章只有 23 页正常顺序应该包含定理、推导、例题、小结。用一支记号笔把下面七类内容的位置标出来你就知道这份课件到底在讲什么。要定位的内容为什么重要看懂的判断标准贝叶斯定理与先验、后验定义全部公式的符号基础能写出三块分别代表什么最小错误率决策规则理解 argmax 和分母约掉能解释为什么 P(x) 可忽略朴素贝叶斯独立性假设决定计算复杂度能说清假设违背时后果连续特征的似然估计对应 GaussianNB能写出高斯对数似然公式离散特征的似然估计对应 MultinomialNB能写出频率估计和平滑公式拉普拉斯平滑对应 alpha 参数能手动算一次平滑后的概率log 域计算工程实现必做能解释为什么连乘会下溢我一般拿到讲义会先花 15 分钟做这个定位而不是从第一页开始啃。23 页的材料只需要半小时就能标完之后每一遍复习都在这些标记点之间跳转。4.2 手推一个完整的离散朴素贝叶斯例子公式只看不推期末题照样不会做。找一个人力可算的小例子三列特征两个类别。下面这个表是经典“天气是否打球”的变体样本天气风是否打球1晴小打球2晴大不打3阴小打球4阴大不打5雨小打球6雨大不打先验很好算P(打球)3/60.5P(不打)3/60.5。再算条件概率P(天气晴|打球)1/3P(风大|打球)1/3P(天气晴|不打)1/3P(风大|不打)2/3现在预测一个样本天气晴风大P(打球|晴,大) ∝ 0.5 × (1/3) × (1/3) 0.0556P(不打|晴,大) ∝ 0.5 × (1/3) × (2/3) 0.1111归一化后是 1/3 对 2/3分类结果是不打球。这个手推过程就是完整算法算先验、算条件概率、乘起来、选最大。如果某个特征组合在训练集里没出现就把拉普拉斯平滑的 α 设为 1 重算一遍理解平滑对零概率的兜底作用。4.3 用 CategoricalNB 把上面推导验证一遍手工算完要立刻用代码验证。sklearn 1.1 以上版本提供了 CategoricalNB专门处理类别型特征正好可以对这个例子import numpy as np from sklearn.naive_bayes import CategoricalNB # 0晴, 1阴, 2雨风列 0小, 1大 X np.array([[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]]) y np.array([1, 0, 1, 0, 1, 0]) clf CategoricalNB(alpha0.0).fit(X, y) print(clf.predict_proba([[0, 1]]))输出应该接近 [[0.333, 0.667]]对应不打球的概率更高。alpha0.0 是为了对上手推真实项目千万不要设 0否则未见组合的概率直接是 0。从 alpha1.0 起步再按验证集调。如果你用的 sklearn 版本没有 CategoricalNB就把类别特征做成独热编码后改用 BernoulliNB结果也基本一致。5. 贝叶斯分类器避坑五个翻车现场与排查路径5.1 概率连乘下溢为 0在 log 域做判别现象自己手写的朴素贝叶斯训练完预测结果全是 0或者返回 nan用 sklearn 没这个问题但手写版本一上高维特征就崩。原因特征维度多时每个条件概率都小于 1几十个连乘在 float64 下变成 0.0。0 的对数是负无穷argmax 自然失效。解决所有计算都走 log 域。手写实现里不要先乘后取 log要直接累加 log 概率score np.log(self.priors_[c]) for i in range(len(x)): score gaussian_log_pdf(x[i], self.means_[c][i], self.vars_[c][i])如果需要输出归一化的概率也不要直接对原始 score 做 exp而是先用 scipy.special.logsumexp 把最大分数提出来再 exp避免 exp 下溢。这是记住一个技巧就能省一个晚上的问题。5.2 零概率事件拉普拉斯平滑的 alpha 不是越大越好现象MultinomialNB 在文本测试集上遇到训练集没出现过的词整个句子被分到错误类别加上 alpha1.0 后缓解但调大 alpha 到 100 又发现准确率掉得厉害。原因文本特征空间巨大测试集一定会有未见词。alpha 太小兜不住零概率alpha 太大又把真实词频差异抹平了。解决先保证 alpha 在 0.1~5.0 之间做小范围搜索不要一上来就用几十。常写from sklearn.model_selection import GridSearchCV from sklearn.naive_bayes import MultinomialNB param_grid {alpha: [0.1, 0.3, 0.5, 1.0, 2.0, 5.0]} grid GridSearchCV(MultinomialNB(), param_grid, cv5) grid.fit(X_train, y_train)alpha 的选择本质是在“记住训练集”和“容忍未见特征”之间找平衡。小样本用 1.0 起步样本量上万后可以把下限降到 0.1。5.3 特征相关性让独立性假设失效先查相关矩阵现象连续特征里有两个强相关特征比如“身高”和“体重”朴素贝叶斯把它们的证据重复计算预测概率极其自信但准确率却不如逻辑回归。原因独立性假设要求特征在给定类别后不相关。强相关特征会被乘两次等于给同一个信号投了两票后验被推向极端。解决先用相关矩阵检查连续特征import pandas as pd corr pd.DataFrame(X_train).corr().abs() print(corr.to_numpy().max())最大相关系数超过 0.7 就要处理。常见做法是去掉高相关特征、做 PCA 降维或者干脆换逻辑回归。朴素贝叶斯这时候不是不能用而是别把它输出的概率当成真实概率只看排序结果。5.4 类别不均衡先验淹没证据手动调 prior 与阈值现象正样本只占 5%模型在测试集上把所有样本都预测成负类准确率反而有 95%。一看混淆矩阵正样本全丢了。原因先验 P(c) 来自样本频率多数类在 score 里天然多出一大截 log 先验。少数类要靠似然证据压过多数的先验优势样本少时做不到。解决把先验改成均衡值或者只在决策阈值上做文章clf GaussianNB(priors[0.5, 0.5]).fit(X_train, y_train) proba clf.predict_proba(X_test)[:, 1]手动设 priors 后不要再从训练集里算先验否则设置等于没设。更稳妥的方式是保留频率估计但用验证集搜索一个能平衡查全率和查准率的概率阈值比如 0.3 而不是 0.5。5.5 预测结果全是同一类先查方差再查泄漏现象训练时准确率不错测试时模型把所有样本都预测成第一类连特征分布差别很大的样本也被拉回默认类。原因两个高频原因。一是某个特征方差为 0高斯似然里 log(0) 变成负无穷主导了整个 score二是预处理时把测试集统计信息泄漏进了训练导致测试特征分布失真。解决先打印每个特征的方差print(X_train.var(axis0))看到 0 就调整 var_smoothingclf GaussianNB(var_smoothing1e-6).fit(X_train, y_train)泄漏问题的排查更麻烦检查 StandardScaler、归一化、缺失值填充是否都是先 fit 训练集再 transform 测试集。我一般把整个流程包进 sklearn Pipeline从源头避免手写顺序出错。6. 让贝叶斯分类器真正用出价值校准、阈值与 baseline 习惯6.1 概率输出先过校准曲线朴素贝叶斯给出的概率往往过于极端相关特征会叠出接近 0 或 1 的置信度但真实比例没那么自信。如果你要把概率当分数用先画一条校准曲线import matplotlib.pyplot as plt from sklearn.calibration import calibration_curve proba clf.predict_proba(X_test)[:, 1] fraction, mean_pred calibration_curve(y_test, proba, n_bins5) plt.plot(mean_pred, fraction, markero) plt.plot([0, 1], [0, 1], linestyle--)点在对角线上说明校准良好如果点都在下方说明模型过度自信。做排序场景可以让它继续这样但如果要按概率阈值做业务决策建议叠加 Platt scaling 或等渗回归校准。6.2 阈值平移最便宜的后悔药很多项目里正例漏报和误报的代价不一样。医疗筛查漏掉病人比多召回一个疑似病人严重得多这时候不要急着换模型先在验证集上平移阈值for thr in np.linspace(0.05, 0.95, 19): score business_cost(y_val, proba_val thr)阈值是比模型参数更好调的旋钮。贝叶斯分类器的概率即使不校准排序关系通常还是可靠的顺着排序找阈值往往能把 F1 拉回两三个点。6.3 我现在的固定习惯先跑朴素贝叶斯再看值不值得换模型我现在接任何分类需求第一件事永远是先用朴素贝叶斯跑一个最简陋的 baseline把数据泄漏、特征质量、评估口径全部理顺再决定要不要上逻辑回归、随机森林或梯度提升树。贝叶斯分类器不是拿来拿冠军的它是拿来做尺子的如果复杂模型连朴素贝叶斯都不能稳定超过三五个点说明问题多半在数据而不在模型复杂度。这个习惯帮我避免了很多次“模型越换越复杂、线上越跑越慢”的尴尬也希望帮到你。本文还有配套的精品资源点击获取