跳到主要内容

朴素贝叶斯

朴素贝叶斯(Naive Bayes, NB)是基于贝叶斯定理特征条件独立假设的概率分类模型,是数学建模竞赛中处理文本分类、快速基线、小样本分类问题的高性价比工具。它"训练 = 数数"(统计各类别的均值/方差或词频),没有迭代优化过程,训练快、参数少、可解释性强,是垃圾邮件识别、情感分析、文本主题分类等题目的标配方法之一。本文从贝叶斯公式、三类常见变体(高斯 NB、多项式 NB、伯努利 NB)、评价指标、可视化诊断到可运行代码(手写实现与 sklearn 对照),完整梳理朴素贝叶斯的竞赛实战用法。

一、算法含义

1.1 一句话理解

朴素贝叶斯回答的问题是:已知样本的特征 xx,它属于哪一类的概率最大? 它的做法分两步:

  1. 贝叶斯公式:把"给定特征求类别"的后验概率 P(yx)P(y \mid x) 翻转为"给定类别求特征"的似然 P(xy)P(x \mid y) 乘上先验 P(y)P(y)——后者往往更容易估计;
  2. 朴素假设:假设各特征在类别给定的条件下相互独立,于是联合似然可以拆成各特征似然的连乘,参数从指数级骤降到线性级,训练只需要"数数"。

"朴素"二字指的就是这个(几乎总是不成立的)独立假设——但实践证明它在很多任务上依然好用,是机器学习史上最著名的"错误假设 + 正确结果"案例。

1.2 贝叶斯公式与后验概率

对二分类(或多分类)问题,设类别为 yky_kk=1,2,,Kk=1,2,\dots,K),特征向量为 x=(x1,x2,,xd)x=(x_1,x_2,\dots,x_d)。贝叶斯定理给出后验概率:

P(ykx)=P(xyk)P(yk)P(x)=P(xyk)P(yk)kP(xyk)P(yk)P(y_k \mid x) = \frac{P(x \mid y_k)\, P(y_k)}{P(x)} = \frac{P(x \mid y_k)\, P(y_k)}{\sum_{k'} P(x \mid y_{k'})\, P(y_{k'})}

其中:

  • P(yk)P(y_k)先验概率(prior),不看特征、仅凭类别频率做出的判断,由训练集各类别占比估计 P^(yk)=Nk/N\hat P(y_k) = N_k / N
  • P(xyk)P(x \mid y_k)似然(likelihood),类别 yky_k 下观察到特征 xx 的概率;
  • P(x)P(x)证据(evidence / 归一化常数),与类别无关,比较各类别时可直接忽略(它不改变 argmax 的结果);
  • P(ykx)P(y_k \mid x)后验概率(posterior),观察到特征 xx 后,样本属于类别 yky_k 的概率——这就是我们要的输出。

1.3 朴素条件独立假设

直接估计联合似然 P(xyk)=P(x1,x2,,xdyk)P(x \mid y_k) = P(x_1, x_2, \dots, x_d \mid y_k) 需要 O(KVd)O(K \cdot V^d) 量级的参数(每个特征取值组合都要一个概率),样本量远远不够。朴素贝叶斯做了一步激进的简化:假设给定类别时各特征相互独立:

P(xyk)=j=1dP(xjyk)P(x \mid y_k) = \prod_{j=1}^{d} P(x_j \mid y_k)

于是参数从指数级降到线性级 O(Kd)O(K \cdot d),每个条件概率 P(xjyk)P(x_j \mid y_k) 只需在单特征维度上统计。代价是忽略了特征间的关联(如"身高"与"体重"给定性别后显然仍相关),后验概率的绝对值会被扭曲;但 argmax 决策对"概率误差方向一致"的情况不敏感,所以分类结果常常仍然正确

1.4 决策规则:最大后验概率(MAP)

朴素贝叶斯的预测规则是最大后验概率(Maximum A Posteriori, MAP):把证据 P(x)P(x) 当常数扔掉,并取对数把连乘变连加(防止数值下溢):

y^=argmaxk  P(yk)j=1dP(xjyk)y^=argmaxk[logP(yk)+j=1dlogP(xjyk)]\hat{y} = \arg\max_k \; P(y_k)\prod_{j=1}^{d} P(x_j \mid y_k) \quad \Longleftrightarrow \quad \hat{y} = \arg\max_k \left[\log P(y_k) + \sum_{j=1}^{d} \log P(x_j \mid y_k)\right]

"训练"就变成了三件事:估计先验 P^(yk)\hat P(y_k)、估计各类别的条件分布 P^(xjyk)\hat P(x_j \mid y_k),预测时做加权求和取最大。没有损失函数、没有梯度下降、没有迭代——这是它与 Logistic 回归、神经网络最本质的区别。

1.5 三种常见模型变体

"条件分布 P(xjyk)P(x_j \mid y_k) 长什么样"决定了朴素贝叶斯的具体形态,竞赛中最常用三种:

(1)高斯朴素贝叶斯(GaussianNB)——连续特征

假设给定类别 yky_k 时,特征 xjx_j 服从正态分布 N(μkj,σkj2)N(\mu_{kj}, \sigma_{kj}^2),似然为:

P(xjyk)=12πσkj2exp((xjμkj)22σkj2)P(x_j \mid y_k) = \frac{1}{\sqrt{2\pi\sigma_{kj}^2}} \exp\left(-\frac{(x_j - \mu_{kj})^2}{2\sigma_{kj}^2}\right)

参数由训练集直接估计(按类别算均值与方差):

μ^kj=1Nki:yi=kxij,σ^kj2=1Nki:yi=k(xijμ^kj)2\hat\mu_{kj} = \frac{1}{N_k}\sum_{i:\,y_i=k} x_{ij}, \qquad \hat\sigma_{kj}^2 = \frac{1}{N_k}\sum_{i:\,y_i=k} \left(x_{ij} - \hat\mu_{kj}\right)^2

代入 1.4 的对数形式,丢弃与类别无关的常数项 d2ln2π\frac{d}{2}\ln 2\pi 后:

y^=argmaxk[logP(yk)12j=1d(lnσkj2+(xjμkj)2σkj2)]\hat y = \arg\max_k \left[\log P(y_k) - \frac{1}{2}\sum_{j=1}^{d}\left(\ln\sigma_{kj}^2 + \frac{(x_j-\mu_{kj})^2}{\sigma_{kj}^2}\right)\right]

两个直观结论:① 与类均值 μk\mu_k马氏距离越小、类内方差越小,该类的分数越高——高斯 NB 本质是"加权最近类中心";② 各类方差不同时,决策边界是二次曲线(椭圆/双曲线),所以高斯 NB 能处理一定的非线性,边界不一定是直线。

(2)多项式朴素贝叶斯(MultinomialNB)——词频/计数特征

适用于计数特征(词频、字符频次、TF 向量),是文本分类的事实标准。样本 xx 是词频向量(xjx_j = 第 jj 个词在该文档出现次数),类条件分布为多项式分布:

P(xyk)j=1VP(wjyk)xjy^=argmaxk[logP(yk)+j=1VxjlogP(wjyk)]P(x \mid y_k) \propto \prod_{j=1}^{V} P(w_j \mid y_k)^{x_j} \qquad\Longrightarrow\qquad \hat y = \arg\max_k \left[\log P(y_k) + \sum_{j=1}^{V} x_j \log P(w_j \mid y_k)\right]

其中 P(wjyk)P(w_j \mid y_k) 是"类别 kk 的文档中随机抽一个词是 wjw_j"的概率,由各类别词频归一化估计(含拉普拉斯平滑,见 1.6):

P^(wjyk)=cntkj+αj=1Vcntkj+αV,cntkj=i:yi=kxij\hat P(w_j \mid y_k) = \frac{\mathrm{cnt}_{kj} + \alpha}{\sum_{j'=1}^{V} \mathrm{cnt}_{kj'} + \alpha V}, \qquad \mathrm{cnt}_{kj} = \sum_{i:\,y_i=k} x_{ij}

注意多项式 NB 用的是全文档的总词频,因此文档越长、词出现次数越多,证据越强——长文本天然获得更高置信度(这也是它对短评论容易摇摆的原因之一)。

(3)伯努利朴素贝叶斯(BernoulliNB)——0/1 特征

特征只取"出现/不出现"二值(xj{0,1}x_j \in \{0,1\}),例如"邮件是否包含某词"。类条件分布为伯努利分布:

P(xjyk)=θkjxj(1θkj)1xj,θ^kj=Nkj+αNk+2αP(x_j \mid y_k) = \theta_{kj}^{x_j} (1-\theta_{kj})^{1-x_j}, \qquad \hat\theta_{kj} = \frac{N_{kj} + \alpha}{N_k + 2\alpha}

其中 NkjN_{kj} 是类别 kk 中包含该特征的样本数。与多项式 NB 的区别:多项式 NB 计数(一个词出现 10 次当 10 条证据),伯努利 NB 只记有无(出现 10 次当 1 条证据)。短文本(微博、标题)常选用伯努利 NB。

选择速查:连续数值特征 → GaussianNB;词频/计数特征 → MultinomialNB;短文本、二值出现与否 → BernoulliNB。

1.6 拉普拉斯平滑(零概率问题)

用频率直接估计 P^(wjyk)=cntkj/jcntkj\hat P(w_j \mid y_k) = \mathrm{cnt}_{kj} / \sum_j \mathrm{cnt}_{kj} 有个致命缺陷:训练集中没见过的词(或没见过的取值组合),估计概率为 0。预测时只要有一个特征项为 0,连乘结果立刻归零,一条"从未见过的评论"在任何类别下概率都是 0,模型拒绝回答。

解决办法是拉普拉斯平滑(Laplace / add-one smoothing):给每个计数加上 α\alpha(常取 1),分母相应加上 α×\alpha \times(取值个数):

P^(wjyk)=cntkj+αjcntkj+αV,V=词表大小\hat P(w_j \mid y_k) = \frac{\mathrm{cnt}_{kj} + \alpha}{\sum_{j'} \mathrm{cnt}_{kj'} + \alpha V}, \qquad V = \text{词表大小}

平滑的直观含义:假设每个词在每个类别里"虚拟地"预先出现了 α\alpha。这样训练中未出现的词获得一个非零但很小的概率 1/(总词频+αV)1/(\text{总词频} + \alpha V),模型对未知词不再"一票否决"。sklearn 的 MultinomialNB(alpha=1.0) 默认即做拉普拉斯平滑。注意高斯 NB 没有零概率问题(正态分布处处非零),但会遇到"某类某特征方差为 0"(特征在该类中恒为常数)导致似然爆炸,需加 var_smoothing 给方差兜底。

1.7 参数估计小结

朴素贝叶斯没有损失函数与优化器,训练就是闭式统计

模型先验 P^(yk)\hat P(y_k)类条件参数平滑
高斯 NBNk/NN_k/N各类别各特征均值 μkj\mu_{kj}、方差 σkj2\sigma_{kj}^2var_smoothing(防 0 方差)
多项式 NBNk/NN_k/N各类别词频归一化 P(wjyk)P(w_j\mid y_k)alpha=1(拉普拉斯)
伯努利 NBNk/NN_k/N各类别出现频率 θkj\theta_{kj}alpha=1

全部都是"数数 + 归一化",单遍扫描训练集即可完成,时间复杂度 O(Nd)O(Nd)

1.8 优缺点

优点

  1. 训练与预测极快:没有迭代、没有梯度,一次扫描即可,天然适合竞赛中反复调数据的场景;
  2. 小样本表现好:每个类条件概率只在一维上估计,参数少、方差小,几十上百条样本就能用(Ng & Jordan 2002 证明:样本很少时 NB 常常优于 Logistic 回归);
  3. 可处理高维稀疏特征:文本动辄上万维词表,NB 每维独立估计,维度灾难影响远小于 KNN/神经网络;
  4. 直接输出各类别概率,可以做风险排序、按阈值调整决策;
  5. 可解释性中等偏上:每类的"最有区分度特征"就是各类别条件概率最高的词/特征,天然对应"结论";
  6. 天然支持增量学习与流式更新:新样本只需更新计数;
  7. 对缺失值、无关特征相对鲁棒:缺失的特征项在连乘/连加中自然"缺席",不像回归会被迫插补。

缺点

  1. 条件独立假设几乎总是不成立(特征强相关时后验概率被严重扭曲,概率值不可信);
  2. 概率校准差:NB 输出常"过度自信"(概率接近 0 或 1),做精确概率校准(如风控定价)需 Platt/isotonic 校准或换 Logistic;
  3. 高斯假设不满足时(特征重尾、多峰)判别力下降,需变换或分箱;
  4. 计数特征对词频绝对量敏感,文档长度差异大时需归一化;
  5. 没有特征选择机制:垃圾特征与噪声特征会平等地贡献分数,需要预处理筛特征。

一句话总结:"快 + 简单 + 概率输出"是 NB 的立足之本,"独立假设"是它付出的代价——代价常被高估,好处常被低估。


二、何时使用(适用场景与条件)

2.1 适合朴素贝叶斯的三个核心条件

  1. 文本/计数类分类任务:垃圾邮件识别、情感分析、新闻分类、弹幕/评论分类——词频特征天然适配多项式 NB,这是 NB 的主场;
  2. 需要快速基线或实时预测:竞赛中先跑一个 NB 当"地板"(准确率再低也是参照),或部署环境算力紧张;
  3. 样本量少、特征维度高:几千维词表 + 几百条样本,NB 依然能稳定工作,回归类模型则容易过拟合。

2.2 竞赛典型场景

  • 文本分类题:垃圾短信/邮件识别、情感正负面判定(电商评论、微博、影评)、新闻主题分类、论坛灌水帖识别、客服工单自动分流;
  • 作弊/异常检测:评论文本是否为机器刷单、水军评论识别(配合 TF-IDF、词向量特征);
  • 快速多分类基线:任何分类题的第一版模型,为后续 SVM/GBDT 提供参照基准;
  • 小样本诊断题:医疗、化工等样本极少的分类问题,NB 的"低方差"特性比复杂模型更不易过拟合。

竞赛中典型问法:"建立模型对评论进行情感分类,给出分类准确率""设计垃圾邮件过滤系统,要求高召回率"——文本分类题几乎必考 NB(或至少作为对照模型出现)。

2.3 使用前提(经验法则)

  1. 特征尽量接近条件独立:连续特征先看各类别的组内相关系数矩阵(第六节代码会打印),组内相关越弱 NB 越准;文本词袋特征天然近似满足;
  2. 概率分布假设要匹配:连续特征做直方图/QQ 图检查是否近似正态,偏差大可取对数、Box-Cox 变换或分箱后用多项式 NB;
  3. 训练集要覆盖足够的词:词表外词靠拉普拉斯平滑兜底,但训练文本太少(如每类不足几十条)时字符级特征(analyzer="char")常比词级更稳;
  4. 类别平衡或先验可控:NB 的先验直接来自类别频率,类别极不平衡时要手动设置 priors 或重采样,否则模型天然偏向多数类。

2.4 不适用情形

  • 特征强相关且需要精确概率:如金融风控中强相关的财务指标,独立假设会让后验概率失真(分类也许还行,概率绝不可信)→ 用 Logistic 回归/随机森林;
  • 需要精确概率校准的场景(信用评分、保险定价):NB 概率常过度自信 → 换 Logistic 或加 Platt 校准;
  • 回归任务:预测连续数值(销量、价格)不是分类,NB 无能为力(有高斯朴素贝叶斯回归的变体,但竞赛中直接用线性回归更稳妥);
  • 特征间存在强交互、决策边界高度复杂:NB 靠独立假设工作,交互效应完全被忽略 → 用 SVM(RBF 核)、随机森林/GBDT、神经网络;
  • 样本极多、算力充足:此时 Logistic/GBDT 通常反超 NB,NB 的小样本优势不复存在。

2.5 与其他分类方法的对比选择

方法概率输出可解释性处理非线性高维小样本何时选它
朴素贝叶斯有(常过度自信)中(各类别高频特征)弱(独立假设)文本分类、快速基线、样本很少
Logistic 回归有(校准好)强(系数/OR)需特征工程一般需要概率 + 可解释系数
SVM无(需 Platt 缩放)强(RBF 核)较强非线性边界、中小样本
KNN可(投票比例)弱(维度灾难)极低维、无参数假设

竞赛建议:文本类题目以多项式 NB 为主力、随机森林/GBDT 或神经网络做提升对照;一般分类题先跑 NB 与 Logistic 两个"快基线"(一个有概率、一个有系数),再决定是否上复杂模型。


三、算法指标

以下指标的符号建立在混淆矩阵上。约定"正类"是竞赛中关心的那一类(如"垃圾邮件""正面评论")。

3.1 混淆矩阵(基础)

预测为 0预测为 1
真实为 0TN(真负例)FP(假正例,误报)
真实为 1FN(假负例,漏报)TP(真正例)

由它可派生两个率:真正率 TPR=TPTP+FNTPR=\dfrac{TP}{TP+FN}(正类中被找出的比例)、假正率 FPR=FPFP+TNFPR=\dfrac{FP}{FP+TN}(负类中被误报的比例)。

3.2 准确率 Accuracy

公式:Acc=TP+TNTP+TN+FP+FN\mathrm{Acc}=\dfrac{TP+TN}{TP+TN+FP+FN};取值范围 [0,1][0,1];越大越好。

解读:全部样本中预测正确的比例,只在两类平衡时才有意义。若负类占 95%,"全预测为负"的傻瓜模型准确率就有 0.95,却一条正类都找不出——不平衡数据请看 F1、AUC。

3.3 精确率 Precision

公式:Precision=TPTP+FP\mathrm{Precision}=\dfrac{TP}{TP+FP};取值范围 [0,1][0,1];越大越好。

解读:预测为正的样本中真正为正的比例,衡量"宁缺毋滥"——误报代价高的场景(如给正常用户误发警告)优先看它。垃圾邮件过滤通常要求精确率高(不能把正常邮件当垃圾)。

3.4 召回率 Recall

公式:Recall=TPTP+FN\mathrm{Recall}=\dfrac{TP}{TP+FN};取值范围 [0,1][0,1];越大越好。

解读:真实正类中被找出的比例,衡量"宁错杀不放过"——漏报代价高的场景(垃圾邮件漏进收件箱、故障漏报)优先看它。精确率与召回率此消彼长,调整分类阈值即可在二者间权衡。

3.5 F1 分数

公式:F1=2PRP+R=2TP2TP+FP+FNF_1=\dfrac{2PR}{P+R}=\dfrac{2TP}{2TP+FP+FN};取值范围 [0,1][0,1];越大越好。

解读:精确率与召回率的调和平均,一个变低 F1 就低,能惩罚"偏科"的模型。竞赛中类别不平衡时优先报 F1。

3.6 ROC-AUC

ROC 曲线:把决策阈值从 1 扫到 0,每取一个阈值算一对 (FPR,TPR)(FPR, TPR) 描点连成曲线,横轴 FPR、纵轴 TPR。AUC 即曲线下面积(NB 的后验概率可直接当作阈值排序的分值)。

  • 取值范围 [0,1][0,1];AUC = 0.5 等价于随机猜测(对角线),越大越好;
  • 统计含义:随机取一对正负样本,正样本后验概率高于负样本的概率
  • 与阈值无关,衡量"排序能力":0.50.7 较差,0.70.8 可接受,0.8~0.9 较好,0.9 以上优秀;
  • 类别极不平衡时 ROC 偏乐观,可补充 PR 曲线。

3.7 对数损失(交叉熵)LogLoss

公式:LogLoss=1ni=1n[yilnpi+(1yi)ln(1pi)]\mathrm{LogLoss}=-\dfrac{1}{n}\sum_{i=1}^{n}\Big[y_i\ln p_i+(1-y_i)\ln(1-p_i)\Big],其中 pi=P(yi=1xi)p_i = P(y_i=1 \mid x_i);取值范围 [0,+)[0,+\infty);越小越好。

解读:衡量概率本身的质量而非只看类别对错——预测错了还非常自信(真实为 0 却输出 p=0.99)会遭到指数级惩罚。对 NB 尤其重要:NB 概率常过度自信,LogLoss 是暴露这一点的最直接指标。若只做排序决策(AUC 高)但 LogLoss 大,说明概率值不可直接当"风险概率"使用。

3.8 先验概率估计

公式:P^(yk)=NkN\hat P(y_k)=\dfrac{N_k}{N}NkN_k 为训练集中类别 kk 的样本数,NN 为总样本数;取值范围 [0,1][0,1],各类别之和为 1。

解读:这是 NB 自身的学习结果之一,也是"模型从哪里出发"的展示。解读要点:

  • 先验即类别频率:若训练集正类占 30%,模型对"没见过的证据"默认输出 0.3 的正类概率——先验是模型的"出生偏见";
  • 先验必须反映真实分布:训练集类别比例与真实场景不一致时(如抽样不平衡),要先重采样或手动设置 priors,否则模型系统性偏向多数类;
  • 先验与似然共同决定决策:即使某类先验很小,若似然证据极强(特征极具判别性),后验仍可能翻盘——这正对应"小概率类别靠特征翻案"的场景。

3.9 指标汇总表

指标公式取值范围解读要点竞赛参考
准确率 AccuracyTP+TNTP+TN+FP+FN\frac{TP+TN}{TP+TN+FP+FN}[0,1][0,1]分对的比例;不平衡时误导两类平衡时必报
精确率 PrecisionTPTP+FP\frac{TP}{TP+FP}[0,1][0,1]预测为正里真正为正;怕误报看它误报代价高场景
召回率 RecallTPTP+FN\frac{TP}{TP+FN}[0,1][0,1]真正类被找出;怕漏报看它漏报代价高场景
F1 分数2PRP+R\frac{2PR}{P+R}[0,1][0,1]P、R 的调和平均不平衡时主报指标
ROC-AUC01TPRd(FPR)\int_0^1 TPR\,d(FPR)[0,1][0,1]阈值无关的排序能力分类题必报
对数损失 LogLoss1n[ylnp+(1y)ln(1p)]-\frac{1}{n}\sum[y\ln p+(1-y)\ln(1-p)][0,+)[0,+\infty)概率质量;NB 常偏大需要概率时必报
先验概率 P^(yk)\hat P(y_k)NkN\frac{N_k}{N}[0,1][0,1]类别频率;模型出发点检查是否反映真实分布

四、可视化图表

下表汇总本节要求的 4 张图(图名与第六节代码保存的文件名一一对应,均存于 figures/ 目录,前缀 nb_)。

图名(文件)用途关键解读点
① 决策边界图 + 类别概率热力图(nb_boundary_prob.png展示高斯 NB 的二次决策边界与 predict_proba 概率面,讲清"概率如何随位置平滑变化"左图黑色实线(P=0.5)为分类边界,虚线(P=0.25/0.75)围成"犹豫带";边界是二次曲线(椭圆/双曲线),说明高斯 NB 能处理一定非线性;右图概率面从深蓝到亮黄平滑过渡,两类中心附近概率接近 0/1,交界处概率≈0.5,颜色越"渐变缓慢"说明判别越犹豫
② 各类别特征似然分布曲线(nb_likelihood.png展示高斯 NB 学习到的类条件分布 P(xjyk)P(x_j\mid y_k),直观看出"两类重叠多少"两曲线重叠面积越大,该特征判别力越弱;虚线标注各类均值(曲线中心),曲线越宽(方差大)说明类内越分散;若某特征两曲线几乎完全重合,说明该特征基本无用
③ 混淆矩阵热力图(nb_confusion.png直观看出四类样本的数量分布对角线颜色深、数字大 = 分得对;右上 FP、左下 FN 分别对应误报与漏报;结合场景判断哪类错误代价更高
④ ROC 曲线(nb_roc.png阈值无关的判别能力总览,竞赛最常用图曲线越贴近左上角越好;标注的 AUC 值越大越好(>0.8 较好);与对角线(随机猜测)的距离代表模型增益;NB 的 ROC 常呈"先快后缓"的折线(后验概率分布集中在 0/1 两端所致),这是概率过度自信的表现

好图的特征:边界两侧颜色分明、犹豫带窄;似然曲线重叠小;混淆矩阵对角占主导;ROC 明显凸向左上角且 AUC 高。

异常图的特征(出现即警惕):边界把整个平面判为一类(先验失衡或特征无效);某特征两似然曲线完全重合(该特征无判别力,可删);混淆矩阵某一行全为 0(模型偷懒全预测一类);ROC 贴着对角线(AUC≈0.5,检查特征或标签)。


五、符号说明

符号含义示例/单位
x=(x1,,xd)x=(x_1,\dots,x_d)特征向量(样本)词频向量或数值特征
yy类别标签y=1y=1 表示垃圾邮件,y=0y=0 表示正常邮件
yky_kkk 个类别(k=1,,Kk=1,\dots,K二分类时 y0,y1y_0, y_1
KK类别个数K=2K=2
N, NkN,\ N_k训练集样本总量、类别 kk 样本数N=400, N1=200N=400,\ N_1=200
dd特征个数(连续数据)d=2d=2
VV词表大小(文本特征数)V=45V=45
P(yk)P(y_k)先验概率,类别 kk 的占比0.5
P(xyk)P(x \mid y_k)类条件似然密度值或概率
P(x)P(x)证据(归一化常数)与类别无关
P(ykx)P(y_k \mid x)后验概率0.98
y^\hat y预测类别(MAP 决策)y^=argmaxkP(ykx)\hat y = \arg\max_k P(y_k\mid x)
μkj, σkj2\mu_{kj},\ \sigma_{kj}^2高斯 NB:类别 kkjj 特征均值、方差μ11=1.40, σ112=1.50\mu_{11}=1.40,\ \sigma_{11}^2=1.50
cntkj\mathrm{cnt}_{kj}多项式 NB:类别 kk 中词 wjw_j 的总词频次数
wjw_j词表中第 jj 个词(或字符)"好""特"
θkj\theta_{kj}伯努利 NB:类别 kk 中特征 jj 出现概率0.3
α\alpha拉普拉斯平滑参数1.0
logP\log P对数概率(防下溢)负实数
TP/FP/FN/TNTP/FP/FN/TN混淆矩阵四个元素个数
TPR/FPRTPR/FPR真正率/假正率[0,1][0,1]
P, RP,\ R精确率、召回率[0,1][0,1]
F1F_1F1 分数[0,1][0,1]
AUCAUCROC 曲线下面积0.96
LogLoss\mathrm{LogLoss}对数损失0.26,无量纲
pip_iii 个样本的正类预测概率0.92

六、可运行程序(完整代码)

说明:以下代码块按顺序拼接保存为一个 .py 文件即可运行(如 naive_bayes_demo.py)。环境要求 Python 3.12,仅依赖 numpy、scipy、scikit-learn、matplotlib、pandas 五个库。程序用 np.random.seed(42) 生成合成二分类数据(2 个特征、n=400、两类各 200 条且分布有重叠、组内特征相关以演示独立假设被违反),无任何外部文件依赖。程序依次完成:手写高斯朴素贝叶斯(按类别算均值/方差 → 联合对数概率 → 后验概率 → 预测)→ sklearn GaussianNB 对照(打印参数与预测一致率)→ 计算第三节全部指标 → 绘制第四节全部 4 张图(保存到 figures/ 并以 nb_ 为前缀)并 plt.show() → 文本情感分类小例子(15 条短评论:CountVectorizer(analyzer="char") 字符词频与手写词频矩阵对照、手写多项式 NB + 拉普拉斯平滑与 sklearn MultinomialNB 对照)。运行输出的典型数值解读见第七节。

# -*- coding: utf-8 -*-
# ========== 0. 导入库与全局设置 ==========
# 运行环境: Python 3.12; 依赖库: numpy / scipy / scikit-learn / matplotlib / pandas
import os
import warnings
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg") # 后台渲染: 无图形界面环境也能运行, 图片统一存到 figures/ (有图形界面时注释本行即可弹窗)
import matplotlib.pyplot as plt
from scipy import stats

from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB, MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer
from sklearn import metrics

# ---- matplotlib 中文显示设置 (必须放在所有绘图代码之前) ----
plt.rcParams["font.sans-serif"] = ["PingFang SC", "Arial Unicode MS", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False

# ---- 无图形界面环境下 plt.show() 会提示 "non-interactive", 过滤掉以保持输出干净 ----
warnings.filterwarnings("ignore", message=".*non-interactive.*")

# ---- 创建图片输出目录 ----
os.makedirs("figures", exist_ok=True)

# ========== 1. 生成合成二分类数据 (n=400, 2 个特征, 两类高斯分布且有重叠) ==========
np.random.seed(42) # 固定随机种子, 保证结果完全可复现
n0, n1 = 200, 200 # 两类各 200 条, 平衡数据
mu0 = np.array([-1.3, -0.8]) # 类别 0 的均值
mu1 = np.array([1.3, 0.8]) # 类别 1 的均值
cov0 = np.array([[1.6, 0.5], [0.5, 1.2]]) # 类别 0 的协方差 (特征正相关)
cov1 = np.array([[1.5, -0.4], [-0.4, 1.3]]) # 类别 1 的协方差 (特征负相关)
X0 = np.random.multivariate_normal(mu0, cov0, n0)
X1 = np.random.multivariate_normal(mu1, cov1, n1)
X = np.vstack([X0, X1])
y = np.hstack([np.zeros(n0, dtype=int), np.ones(n1, dtype=int)])

print("样本量 n = %d, 正类比例 = %.3f (两类完全平衡)" % (len(X), y.mean()))
print("类别0均值 = %s, 类别1均值 = %s" % (mu0, mu1))
print("两特征总体相关系数 = %.3f" % np.corrcoef(X.T)[0, 1])
print("类别0组内相关系数 = %.3f, 类别1组内相关系数 = %.3f "
"(组内相关≠0, 说明条件独立假设并不严格成立)" % (
np.corrcoef(X0.T)[0, 1], np.corrcoef(X1.T)[0, 1]))

# 按 7:3 分层划分训练集与测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
print("训练集 %d 条, 测试集 %d 条" % (len(X_train), len(X_test)))

以上代码生成数据并划分数据集。下面手写实现高斯朴素贝叶斯的核心:按类别算均值/方差、联合对数概率、后验概率与预测。

# ========== 2. 手写高斯朴素贝叶斯 (按类别算均值/方差 -> 后验概率 -> 预测) ==========
class GaussianNBManual:
"""手写高斯朴素贝叶斯:
训练: 对每个类别 k、每个特征 j 估计均值 mu_kj 与方差 sigma2_kj;
预测: log P(y_k|x) = log P(y_k) - 0.5*Σ_j[ln(2πσ²_kj) + (x_j-μ_kj)²/σ²_kj], 取最大。
"""
def __init__(self):
self.classes_ = None # 类别标签
self.priors_ = None # 先验概率 P(y_k)
self.theta_ = None # 各类别各特征的均值 μ_kj, 形状 (K, d)
self.var_ = None # 各类别各特征的方差 σ²_kj, 形状 (K, d)

def fit(self, X, y):
self.classes_ = np.unique(y)
n = len(y)
self.priors_ = np.array([np.sum(y == c) / n for c in self.classes_])
self.theta_ = np.stack([X[y == c].mean(axis=0) for c in self.classes_])
self.var_ = np.stack([X[y == c].var(axis=0) for c in self.classes_])
return self

def joint_log_prob(self, X):
"""计算联合对数概率 log P(x, y_k) = log P(x|y_k) + log P(y_k), 形状 (n, K)"""
log_lik = -0.5 * np.sum(
np.log(2.0 * np.pi * self.var_)
+ (X[:, None, :] - self.theta_[None, :, :]) ** 2 / self.var_[None, :, :],
axis=2)
return log_lik + np.log(self.priors_)

def predict_proba(self, X):
"""后验概率 P(y_k|x) = softmax(联合对数概率)"""
jll = self.joint_log_prob(X)
jll -= jll.max(axis=1, keepdims=True) # 减去每行最大值, 防止指数溢出
p = np.exp(jll)
return p / p.sum(axis=1, keepdims=True)

def predict(self, X):
return self.classes_[np.argmax(self.joint_log_prob(X), axis=1)]

# ========== 3. 训练: 手写高斯 NB vs sklearn GaussianNB ==========
nb_manual = GaussianNBManual().fit(X_train, y_train)
nb_sk = GaussianNB(var_smoothing=1e-9).fit(X_train, y_train)

print("\n---- 参数对比 (类别1 = 正类) ----")
print("先验概率: 手写 %s | sklearn %s"
% (np.round(nb_manual.priors_, 4), np.round(nb_sk.class_prior_, 4)))
print("均值 theta_: 手写\n%s" % np.round(nb_manual.theta_, 4))
print(" sklearn\n%s" % np.round(nb_sk.theta_, 4))
print("方差 var_: 手写\n%s" % np.round(nb_manual.var_, 4))
print(" sklearn\n%s" % np.round(nb_sk.var_, 4))

y_pred_m = nb_manual.predict(X_test)
y_pred_s = nb_sk.predict(X_test)
proba_m = nb_manual.predict_proba(X_test)
proba_s = nb_sk.predict_proba(X_test)
agree_gauss = np.mean(y_pred_m == y_pred_s)
print("\n手写高斯NB 与 sklearn GaussianNB 测试集预测一致率 = %.3f" % agree_gauss)
print("两者后验概率的最大绝对差 = %.3e (差异来自 var_smoothing=1e-9)" % np.abs(proba_m - proba_s).max())

# ---- 单个样本的后验概率明细 (演示公式计算过程) ----
i = 0
print("\n---- 测试集第 1 个样本的后验概率明细 ----")
print("样本特征 x = %s, 真实标签 y = %d" % (np.round(X_test[i], 3), y_test[i]))
jll = nb_manual.joint_log_prob(X_test[i:i + 1])[0]
print("手写: log P(x|y=0)+logP(y=0) = %.3f, log P(x|y=1)+logP(y=1) = %.3f" % (jll[0], jll[1]))
print(" 归一化后验 P(y=0|x) = %.4f, P(y=1|x) = %.4f -> 预测 %d"
% (proba_m[i, 0], proba_m[i, 1], y_pred_m[i]))
print("sklearn: P(y=0|x) = %.4f, P(y=1|x) = %.4f -> 预测 %d"
% (proba_s[i, 0], proba_s[i, 1], y_pred_s[i]))

# ========== 4. 打印全部评价指标 (测试集, 手写模型) ==========
cm = metrics.confusion_matrix(y_test, y_pred_m)
acc = metrics.accuracy_score(y_test, y_pred_m)
pre = metrics.precision_score(y_test, y_pred_m, zero_division=0)
rec = metrics.recall_score(y_test, y_pred_m, zero_division=0)
f1 = metrics.f1_score(y_test, y_pred_m, zero_division=0)
auc_val = metrics.roc_auc_score(y_test, proba_m[:, 1])
ll_val = metrics.log_loss(y_test, proba_m)

print("\n---- 测试集评价指标 (n=%d, 混淆矩阵 TN=%d FP=%d FN=%d TP=%d) ----"
% (len(y_test), cm[0, 0], cm[0, 1], cm[1, 0], cm[1, 1]))
metrics_df = pd.DataFrame({
"指标": ["准确率 Accuracy", "精确率 Precision", "召回率 Recall", "F1 分数",
"ROC-AUC", "对数损失 LogLoss", "先验 P(y=1)"],
"取值": ["%.4f" % acc, "%.4f" % pre, "%.4f" % rec, "%.4f" % f1,
"%.4f" % auc_val, "%.4f" % ll_val, "%.4f" % nb_manual.priors_[1]],
})
print(metrics_df.to_string(index=False))

手写实现与 sklearn 对比完成后,用学到的参数绘制第四节要求的 4 张图(概率面由手写模型的 predict_proba 在网格上计算)。

# ========== 5. 绘制 4 张图 (保存到 figures/, 前缀 nb_) ==========
# ---- 图 1: 决策边界图 + 类别概率热力图 (predict_proba 概率面) ----
xx, yy = np.meshgrid(np.linspace(X[:, 0].min() - 0.6, X[:, 0].max() + 0.6, 300),
np.linspace(X[:, 1].min() - 0.6, X[:, 1].max() + 0.6, 300))
grid = np.c_[xx.ravel(), yy.ravel()]
proba_grid = nb_manual.predict_proba(grid)[:, 1].reshape(xx.shape)

fig, axes = plt.subplots(1, 2, figsize=(13, 5.2))
ax = axes[0]
cs = ax.contourf(xx, yy, proba_grid, levels=np.linspace(0, 1, 21),
cmap="RdYlBu", alpha=0.85)
ax.contour(xx, yy, proba_grid, levels=[0.5], colors="k", linewidths=2.5)
ax.contour(xx, yy, proba_grid, levels=[0.25, 0.75], colors="k",
linewidths=1.2, linestyles="--")
ax.scatter(X[y == 0, 0], X[y == 0, 1], c="#1f77b4", s=18, alpha=0.7, label="类别 0")
ax.scatter(X[y == 1, 0], X[y == 1, 1], c="#d62728", s=18, alpha=0.7, label="类别 1")
ax.set_xlabel("特征 x1"); ax.set_ylabel("特征 x2")
ax.set_title("(a) 决策边界 (P=0.5 实线, P=0.25/0.75 虚线)")
ax.legend(loc="upper right", fontsize=9)
fig.colorbar(cs, ax=ax, label="P(y=1|x)")

ax = axes[1]
im = ax.pcolormesh(xx, yy, proba_grid, cmap="viridis", shading="auto")
ax.scatter(X[y == 0, 0], X[y == 0, 1], c="w", s=12, alpha=0.6,
edgecolors="k", linewidths=0.4, label="类别 0")
ax.scatter(X[y == 1, 0], X[y == 1, 1], c="yellow", s=12, alpha=0.75,
edgecolors="k", linewidths=0.4, label="类别 1")
ax.set_xlabel("特征 x1"); ax.set_ylabel("特征 x2")
ax.set_title("(b) 类别概率热力图 P(y=1|x)")
ax.legend(loc="upper right", fontsize=9)
fig.colorbar(im, ax=ax, label="P(y=1|x)")
plt.tight_layout()
plt.savefig("figures/nb_boundary_prob.png", dpi=150)
plt.show()
plt.close()

# ---- 图 2: 各类别特征似然分布曲线 (高斯 NB 的条件分布 p(x_j|y_k)) ----
fig, axes = plt.subplots(1, 2, figsize=(12, 4.8))
colors = ["#1f77b4", "#d62728"]
for j, ax in enumerate(axes):
xg = np.linspace(X[:, j].min() - 0.8, X[:, j].max() + 0.8, 300)
for k in range(2):
mu, var = nb_manual.theta_[k, j], nb_manual.var_[k, j]
ax.plot(xg, stats.norm.pdf(xg, mu, np.sqrt(var)), color=colors[k], lw=2,
label="类别 %d: N(%.2f, %.2f)" % (k, mu, var))
ax.axvline(mu, color=colors[k], ls=":", lw=1.5, alpha=0.8)
ax.fill_between(xg, stats.norm.pdf(xg, mu, np.sqrt(var)),
alpha=0.12, color=colors[k])
ax.set_xlabel("特征 x%d" % (j + 1))
ax.set_ylabel("条件似然密度 p(x|y)")
ax.set_title("特征 x%d 的各类别条件似然分布" % (j + 1))
ax.legend(fontsize=9)
plt.tight_layout()
plt.savefig("figures/nb_likelihood.png", dpi=150)
plt.show()
plt.close()

# ---- 图 3: 混淆矩阵热力图 ----
fig, ax = plt.subplots(figsize=(6, 5.2))
im = ax.imshow(cm, cmap="Blues")
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
ax.text(j, i, str(cm[i, j]), ha="center", va="center", fontsize=20,
color="white" if cm[i, j] > cm.max() / 2 else "black")
ax.set_xticks([0, 1]); ax.set_yticks([0, 1])
ax.set_xticklabels(["预测 0", "预测 1"]); ax.set_yticklabels(["真实 0", "真实 1"])
ax.set_xlabel("预测类别"); ax.set_ylabel("真实类别")
ax.set_title("混淆矩阵 (测试集)")
fig.colorbar(im, ax=ax)
plt.tight_layout()
plt.savefig("figures/nb_confusion.png", dpi=150)
plt.show()
plt.close()

# ---- 图 4: ROC 曲线 ----
fpr, tpr, _ = metrics.roc_curve(y_test, proba_m[:, 1])
plt.figure(figsize=(6, 6))
plt.plot(fpr, tpr, "b-", lw=2, label="高斯朴素贝叶斯 (AUC = %.3f)" % auc_val)
plt.plot([0, 1], [0, 1], "k--", lw=1, label="随机猜测 (AUC = 0.5)")
plt.xlabel("假正率 FPR"); plt.ylabel("真正率 TPR")
plt.title("ROC 曲线")
plt.legend(loc="lower right")
plt.tight_layout()
plt.savefig("figures/nb_roc.png", dpi=150)
plt.show()
plt.close()

最后是文本情感分类小例子:15 条短评论,用 CountVectorizer(analyzer="char") 做字符词频,并与手写词频矩阵、手写多项式 NB(拉普拉斯平滑)逐项对照。特别注意CountVectorizer(analyzer="char") 会把空格也当作字符特征收录进词表(本例第 4 条评论故意含一个空格),手写词频统计必须保留空格才能与 sklearn 完全一致。

# ========== 6. 文本情感分类小例子 (15 条短评论, 字符词频 + 拉普拉斯平滑) ==========
comments = [
("这部电影太好看", 1),
("剧情无聊透顶", 0),
("演员演技非常棒", 1),
("特效 很糟糕", 0), # 注意: 含一个空格, 用于演示空格也是字符特征
("我很喜欢这部电影", 1),
("太失望了", 0),
("节奏紧凑很好看", 1),
("完全浪费时间", 0),
("配乐好听", 1),
("剧情太烂了", 0),
("值得一看的好片", 1),
("完全看不下去", 0),
("演员演得很棒", 1),
("剧情老套", 0),
("烂片一部", 0),
]
docs = [c[0] for c in comments]
labels = np.array([c[1] for c in comments])
docs_train, docs_test = docs[:10], docs[10:] # 前 10 条训练, 后 5 条测试
y_tr, y_te = labels[:10], labels[10:]

# sklearn: CountVectorizer(analyzer="char") 字符词频 (小写化, 空格与标点都算特征)
vec = CountVectorizer(analyzer="char")
Xc_tr = vec.fit_transform(docs_train).toarray()
Xc_te = vec.transform(docs_test).toarray()
vocab = vec.get_feature_names_out().tolist()
print("\n---- 文本例子: 词表信息 ----")
print("词表大小 V = %d, 前 15 个特征: %s" % (len(vocab), vocab[:15]))
print("词表中是否含空格字符 ' ': %s (CountVectorizer(analyzer='char') 会把空格当特征)"
% (" " in vocab))


def char_count_matrix(docs, vocab):
"""手写字符词频统计: 与 CountVectorizer(analyzer='char') 完全一致
(先小写化, 再逐字符计数; 空格、标点都是特征, 必须保留)"""
idx = {ch: i for i, ch in enumerate(vocab)}
X = np.zeros((len(docs), len(vocab)), dtype=int)
for i, doc in enumerate(docs):
for ch in doc.lower():
if ch in idx:
X[i, idx[ch]] += 1
return X

Xc_tr_m = char_count_matrix(docs_train, vocab)
Xc_te_m = char_count_matrix(docs_test, vocab)
print("手写字符词频矩阵与 sklearn CountVectorizer 完全一致: %s"
% np.array_equal(Xc_tr_m, Xc_tr))


class MultinomialNBManual:
"""手写多项式朴素贝叶斯 (拉普拉斯平滑 alpha=1):
训练: log P(y_k) = log(N_k/N); log P(w_j|y_k) = log[(cnt_kj+α) / (Σ_j cnt_kj + αV)]
预测: argmax_k [ log P(y_k) + Σ_j x_j * log P(w_j|y_k) ]
"""
def __init__(self, alpha=1.0):
self.alpha = alpha

def fit(self, X, y):
self.classes_ = np.unique(y)
n = len(y)
self.class_log_prior_ = np.array(
[np.log(np.sum(y == c) / n) for c in self.classes_])
fc = np.stack([X[y == c].sum(axis=0) for c in self.classes_]) # 各类别词频
V = X.shape[1]
fc_smooth = fc + self.alpha # 拉普拉斯平滑
self.feature_log_prob_ = np.log(fc_smooth) - np.log(
fc_smooth.sum(axis=1, keepdims=True))
return self

def predict(self, X):
jll = X @ self.feature_log_prob_.T + self.class_log_prior_
return self.classes_[np.argmax(jll, axis=1)]

mnb_m = MultinomialNBManual(alpha=1.0).fit(Xc_tr_m, y_tr)
mnb_s = MultinomialNB(alpha=1.0).fit(Xc_tr, y_tr)
pred_m, pred_s = mnb_m.predict(Xc_te_m), mnb_s.predict(Xc_te)

print("\n---- 文本例子: 手写多项式NB vs sklearn MultinomialNB ----")
print("先验: 手写 %s | sklearn %s"
% (np.round(np.exp(mnb_m.class_log_prior_), 4),
np.round(np.exp(mnb_s.class_log_prior_), 4)))
print("测试集预测一致率 = %.3f (公式完全相同, 应为 1.000)" % np.mean(pred_m == pred_s))
txt_df = pd.DataFrame({
"评论": docs_test,
"真实": y_te,
"手写预测": pred_m,
"sklearn预测": pred_s,
})
print(txt_df.to_string(index=False))
print("文本例子测试集准确率 = %.3f" % metrics.accuracy_score(y_te, pred_m))

# 各类别最有区分度的字符 (正类 log P(w|y) - 负类 log P(w|y))
diff = mnb_m.feature_log_prob_[1] - mnb_m.feature_log_prob_[0]
order = np.argsort(-diff)
print("正类特征概率最高的 5 个字符: %s"
% [vocab[i] for i in order[:5]])
print("负类特征概率最高的 5 个字符: %s"
% [vocab[i] for i in order[-5:][::-1]])

print("\n全部图片已保存到 figures/ 目录:")
for f in sorted(os.listdir("figures")):
if f.startswith("nb_"):
print(" - figures/%s" % f)
print("\n程序运行结束, 无报错。")

七、结果解读与注意事项

7.1 本次示例的运行结果与解读

上述程序(随机种子固定为 42)的输出完全可复现,任何机器上运行结果相同。关键输出如下。

数据与假设检查:样本量 n=400,正类比例 0.500(两类完全平衡);总体相关系数 0.431,类别 0 组内相关系数 0.363、类别 1 组内相关系数 -0.288——组内相关不等于 0,说明"给定类别后特征相互独立"的假设在本数据上并不成立,正好检验"假设被违反时 NB 依然好用"这一论断。

模型参数(手写与 sklearn 完全一致,先验均为 [0.5, 0.5]):

参数类别 0类别 1
均值 μ(特征 x1, x2)(-1.3111, -0.8793)(1.3953, 0.6710)
方差 σ²(特征 x1, x2)(1.4317, 1.1821)(1.4992, 1.2854)

解读:

  • 均值估计接近真值(真值 (-1.3, -0.8) 与 (1.3, 0.8),抽样误差约 0.01~0.13):NB 的"训练即统计"在这里直接可见——没有迭代、没有优化,扫一遍训练集就得到全部参数;
  • 两类的方差、均值都不同,因此决策边界是二次曲线(见图 1),这正是高斯 NB 能比线性模型多做的一点"非线性";
  • 手写高斯 NB 与 sklearn GaussianNB 测试集预测一致率 = 1.000,后验概率最大绝对差仅 9.585e-10(sklearn 的 var_smoothing=1e-9 给方差加了一点点兜底项)——手写实现与官方实现逐样本一致。

单个样本的后验概率明细(测试集第 1 个样本 x = [1.651, 0.454],真实标签 1):联合对数概率 log P(x,y=0) = -6.610 < log P(x,y=1) = -2.899,归一化后 P(y=0|x) = 0.0239、P(y=1|x) = 0.9761 → 预测类别 1。该样本两个特征都落在类别 1 均值附近(1.3953, 0.6710),所以后验概率接近 1——这正是 1.4 节公式的逐步落地:先算各类别"距离类中心"的对数似然,加上先验,softmax 归一化即得后验。

评价指标(测试集,n=120,混淆矩阵 TN=52、FP=8、FN=1、TP=59):

指标取值解读
准确率 Accuracy0.925092.5% 的样本分对;本例两类平衡,该指标可信
精确率 Precision0.8806预测为正的 67 个样本中 59 个正确(误报 8 个)
召回率 Recall0.983360 个真实正类中找出 59 个(漏报仅 1 个)
F1 分数0.9291精确率与召回率较均衡
ROC-AUC0.9594随机抽一对正负样本,正样本概率更高的概率约 96%,优秀档
对数损失 LogLoss0.2608明显低于零模型基线 ln2≈0.693,概率质量良好
先验概率 P(y=1)0.5000与生成数据的类别比例一致,模型出发点无偏

文本情感分类小例子(15 条短评论,前 10 条训练、后 5 条测试,字符词频 + 拉普拉斯平滑):

  • 词表大小 V = 45,且词表中含空格字符 ' '——CountVectorizer(analyzer="char") 把空格也当特征收录(第 4 条训练评论"特效 很糟糕"含一个空格);手写 char_count_matrix 保留空格后,词频矩阵与 sklearn 完全一致(True)
  • 手写多项式 NB 与 sklearn MultinomialNB 先验均为 [0.5, 0.5],测试集预测一致率 = 1.000(两者公式逐项相同);
  • 预测结果:值得一看的好片 1→1 ✓、完全看不下去 0→0 ✓、演员演得很棒 1→1 ✓、剧情老套 0→0 ✓、烂片一部 0→1 ✗,测试集准确率 0.800
  • 各类别最有区分度的字符:正类 top5 = 好、影、演、看、这;负类 top5 = 了、剧、情、顶、特——与直觉完全吻合。

"烂片一部"被误判为正面是小样本文本分类的典型现象:字符"片""部"在训练集正面评论("这部电影"出现两次)中出现更多,负面证据只有"烂"一个字,加上每条评论总词频少、各类别先验相同,模型便倒向了正面。对策:增加训练语料(让"烂"多出现几次)、改用词级特征或 TF-IDF 权重、引入伯努利 NB 只看"有无"。

7.2 四张图如何解读

  1. 决策边界图 + 概率热力图:左图黑色实线(P=0.5)把平面分成两半,虚线(P=0.25/0.75)围出"犹豫带";边界呈曲线(二次曲线),说明高斯 NB 有非线性能力;红色与蓝色散点在边界两侧基本分开,边界附近混叠的点正是被分错的样本。右图概率面从蓝色(P≈0)平滑过渡到黄色(P≈1),两类中心附近概率饱和(过度自信的直观表现),交界处概率≈0.5——概率面的"过渡带宽窄"就是判别难易的视觉化
  2. 似然分布曲线:两个特征上两类的条件分布都明显错开(x1 的两条曲线中心相距约 2.7,重叠面积小),因此两个特征都有较强判别力;但曲线较宽(方差约 1.21.5,即标准差约 1.11.2,均值差约为两个标准差),重叠区仍有不少样本——这与测试集 92.5% 的准确率(而非 100%)一致。若某特征两条曲线几乎重合,说明该特征无用,应从模型中删除
  3. 混淆矩阵热力图:对角块(TN=52、TP=59)颜色最深,绝大部分样本分对;右上 FP=8(把负类误报为正类)多于左下 FN=1(把正类漏报为负类),说明本模型更"激进"地判正类——若误报代价高,可提高决策阈值(默认 0.5)加以抑制。
  4. ROC 曲线:曲线明显凸向左上角、远离对角线,AUC≈0.96(优秀档);曲线在低 FPR 区快速上升(模型对最有把握的正类排序极准),这与 NB 后验概率集中在 0/1 两端的"过度自信"特征相符。

7.3 常见坑

  1. 无视条件独立假设的后果:特征强相关时(如本例组内相关系数 0.36/-0.29),NB 会把相关特征"重复计票",后验概率被推向极端(过度自信)。分类结果往往仍可用(决策正确),但概率值绝不能直接当真实概率使用——需要概率的场景(定价、风险排序)换 Logistic 回归或加校准。
  2. 零概率问题与平滑:未做平滑的多项式/伯努利 NB 遇到训练集中没见过的词会输出概率 0,一条评论若含新词则所有类别分数都归零。务必使用 alpha=1.0(拉普拉斯平滑);高斯 NB 则注意某类某特征方差为 0(特征恒为常数)导致的除零,用 var_smoothing 兜底。
  3. 高斯假设不满足:特征重尾(如收入、点击量)或多峰分布时,用"一个均值 + 一个方差"去套会严重失真。先做对数/Box-Cox 变换、分箱,或改用多项式 NB;必要时画直方图/QQ 图检查。
  4. 类别不平衡时只看准确率:95% 负类时"全预测负"就有 0.95 的准确率。不平衡数据请报告 F1、AUC、PR 曲线,并通过重采样或手动设置 priors 修正先验——NB 的先验直接来自类别频率,先验错了,模型从出发点上就歪了
  5. 文本特征不归一化的偏差:多项式 NB 对词频绝对量敏感,长文档天然获得更高置信度。文档长度差异大时改用 TF-IDF 权重或伯努利 NB。
  6. 字符级与词级特征的选择:中文文本用 analyzer="char"(字符级)能绕开分词误差、词表更小,适合小语料;语料充足时词级 + n-gram 通常更准。注意 analyzer="char" 会把空格和标点也当特征(第六节已演示),预处理时不要误删空格导致与 sklearn 输出不一致。
  7. 测试集信息泄漏:CountVectorizer、均值方差、先验都只能在训练集上拟合再套用到测试集(fit 只对训练集调用,transform 才作用于测试集),否则测试指标虚高。
  8. 连续特征未标准化就用高斯 NB:NB 对量纲本身不敏感(每维独立估计),但方差差异巨大的特征权重悬殊,量纲悬殊时可先标准化(均值为 0、方差为 1),让各特征贡献更均衡。

7.4 竞赛论文写作建议(话术模板)

  • 模型构建:"考虑到评论文本的高维稀疏特性与样本量限制,构建基于字符词频特征的多项式朴素贝叶斯情感分类模型,测试集准确率达 92.5%,ROC-AUC 达 0.96,分类性能良好。"
  • 公式交代:"模型基于贝叶斯定理 P(ykx)P(yk)jP(xjyk)P(y_k|x)\propto P(y_k)\prod_j P(x_j|y_k),采用拉普拉斯平滑(α=1)处理未登录词,先验概率由训练集类别频率估计。"
  • 对比结论:"手写实现与 sklearn 的 GaussianNB/MultinomialNB 预测一致率达 100%,验证了模型实现与理论公式的一致性;与 Logistic 回归(AUC 0.94)相比,朴素贝叶斯 AUC 更高且训练仅需单遍扫描,最终选用朴素贝叶斯。"
  • 稳健性:"采用 5 折交叉验证,平均 AUC 为 0.95±0.02,模型性能稳定;在 300 条人工标注的验证评论上 F1 达 0.91,泛化能力良好。"
  • 假设讨论:"虽然特征条件独立假设在数据上并不严格成立(组内相关系数最高达 0.36),但实验表明其对分类决策的影响有限,模型仍取得较高判别精度,符合朴素贝叶斯'假设失真、结论稳健'的已知特性。"

八、延伸阅读

8.1 贝叶斯网络

朴素贝叶斯是贝叶斯网络(Bayesian Network / 信念网络)最简特例:一个有向无环图(DAG),类别节点指向全部特征节点,且特征之间没有边(即条件独立)。更一般的贝叶斯网络允许特征之间存在依赖边,每个节点携带条件概率表(CPT):

P(x1,,xd,y)=P(y)j=1dP(xjPa(xj))P(x_1, \dots, x_d, y) = P(y) \prod_{j=1}^{d} P\big(x_j \mid \mathrm{Pa}(x_j)\big)

其中 Pa(xj)\mathrm{Pa}(x_j)xjx_j 的父节点集合。贝叶斯网络用"局部依赖"替代 NB 的"全局独立",能刻画任意因果/相关结构,但结构学习(learn structure)与参数推断(EM、消息传递)成本远高于 NB。竞赛中当特征间存在明确的因果链(如"天气 → 交通 → 延误")时可考虑;仅做分类时 NB 仍是首选。

8.2 半朴素贝叶斯

在"全独立"(NB)与"全依赖"(贝叶斯网络)之间有一族半朴素贝叶斯(Semi-Naive Bayes),核心思想是允许每个特征依赖一个(而非全部)其他特征(One-Dependent Estimator, ODE):

P(xyk)=j=1dP(xjyk, xpa(j))P(x \mid y_k) = \prod_{j=1}^{d} P\big(x_j \mid y_k,\ x_{pa(j)}\big)

常见变体:

  • SPODE(Super-Parent ODE):所有特征共享同一个"超父"特征;
  • TAN(Tree-Augmented NB):以类别为根、特征间构成最大权生成树,用条件互信息做边权,计算量可控且常常优于 NB;
  • AODE(Averaged ODE):对所有可能的超父取平均,稳健性更好。

sklearn 未内置 TAN/AODE(可用 pgmpy 等库),竞赛中若 NB 效果欠佳且特征不多,可在论文中提及 TAN 作为改进方向。

8.3 与 Logistic 回归的概率联系

朴素贝叶斯与 Logistic 回归分别是生成式模型(对 P(x,y)P(x,y) 建模)与判别式模型(直接对 P(yx)P(y|x) 建模)的代表,二者有深刻的概率联系:

  • 对偶关系:对二分类写出后验的对数几率:

lnP(y=1x)P(y=0x)=lnπ1π0+j=1dlnP(xjy=1)P(xjy=0)\ln\frac{P(y=1 \mid x)}{P(y=0 \mid x)} = \ln\frac{\pi_1}{\pi_0} + \sum_{j=1}^{d} \ln\frac{P(x_j \mid y=1)}{P(x_j \mid y=0)}

当类条件分布取特定形式时,右端恰为 xx线性函数:例如高斯 NB 在各类方差相等时退化为线性判别(LDA 型边界),伯努利 NB、多项式 NB 的对数几率都是特征的线性组合——此时 NB 与 Logistic 回归共享同一个假设空间,NB 相当于用"统计频率"而不是"最大似然迭代"去求参数。

  • 收敛速度不同:Ng & Jordan (2002) 证明,样本量小时 NB 收敛更快(参数少、只在一维上估计),Logistic 回归需要更多样本才能追平;而当独立假设明显不成立且样本充足时,Logistic 回归的渐近误差更小。这解释了"小样本用 NB、大样本用 Logistic"的实践经验。
  • 概率质量不同:两者输出同属 (0,1),但 NB 的概率常过度自信(独立假设下证据被重复计数),Logistic 回归的概率校准更好——需要可靠概率输出时选 Logistic,或对 NB 做 Platt/isotonic 校准后使用。

推荐资料:周志华《机器学习》第 7 章;李航《统计学习方法》第 4 章;Ng & Jordan, "On Discriminative vs. Generative Classifiers" (NIPS 2002);吴恩达 CS229 Lecture Notes 4(Generative Learning Algorithms);sklearn 官方文档 naive_bayes 页面。