简介:一套基于 Matlab 编写的贝叶斯分类完整实现,配备图形用户界面,适合机器学习初学者、算法研究人员以及需要快速完成特征分类任务的工程人员使用;内容涵盖朴素贝叶斯分类器核心代码,覆盖数据预处理、训练集与测试集划分、模型训练、参数估计、分类预测和性能评估等完整流程。压缩包共 28 个文件,以 17 个 .m 源码文件为核心,配合 8 个 txt 数据与说明文件、2 个 prj 工程文件、1 个 fig 界面布局文件,整体仅 37KB,轻量且便于部署;其中 fig 与 prj 让不熟悉编程的用户也能通过界面导入数据、调整参数并直接查看分类结果。目前已有 3681 人浏览学习,附带多组不同格式的测试数据,包括六列属性以空格或逗号分隔的样本、概率相等数据等,能够验证算法在不同输入场景下的鲁棒性。深入阅读源码,可以学习 fitcnb、predict 等工具箱函数在贝叶斯建模中的实际用法,也能掌握如何将算法封装成 GUI 程序,对后续开展分类实验或二次开发都有参考价值。
1. 贝叶斯分类在 Matlab 里是常被低估的基线:凭什么先做它
拿到一份带标签的表格数据,很多人第一反应是直接上随机森林或 XGBoost,但在 Matlab 这个环境里,贝叶斯分类才是那个最容易被低估的起点。它代码量小、训练秒级完成、直接输出每个类别的后验概率,在特征几百个、样本几万行的中小规模数据上,表现常常不比复杂模型差,而这个结论很多工程师是在复杂模型调不动之后才回头验证的。这篇笔记把贝叶斯分类在 Matlab 里的两条路线都讲透:fitcnb 的朴素贝叶斯和 fitcdiscr 的判别式贝叶斯分类,并给出能直接复现的最小代码、关键参数和踩坑记录。适合正在做分类任务的工程师,也适合赶图像处理大作业、需要一个可靠基线模型的学生。
2. 贝叶斯分类的理论先立住:朴素假设、MAP 与判别式分类器
2.1 从后验最大化出发:为什么朴素贝叶斯“朴素”还能打
对一个样本 x,贝叶斯决策要找的是让后验概率 P(y|x) 最大的类别 y。用贝叶斯公式展开:
P(y|x) = P(x|y) * P(y) / P(x)
分母 P(x) 与类别无关,实际比较时直接约掉。为了防止概率连乘产生下溢,实现里通常取对数,把乘法变成加法:
log P(y|x) 正比于 log P(x|y) + log P(y)
朴素贝叶斯的“朴素”,在第二步:它假设在给定类别 y 的条件下,各个特征条件独立,于是:
P(x|y) = P(x1|y) * P(x2|y) * ... * P(xd|y)
每个特征的分布单独估计,互不干扰。这里的“条件独立假设”绝大多数真实数据都不满足,但注意我们做的是 argmax,不是估计绝对概率。只要每个类别的似然估计偏差方向一致、后验排序不乱,分类结果依然正确。很多资料只讲“朴素所以天真”,没讲它错得稳定也是一种能力——这正是贝叶斯分类能当基线、甚至在一些文本分类任务上压制复杂模型的主要原因。
先验 P(y) 的设定也要说清楚。如果先验完全从数据里统计,贝叶斯分类等价于最大似然估计;如果业务上已知某个类别更容易出现,就把这个知识通过 P(y) 注入。这就是 MAP 和 MLE 的区分:MLE 不考虑先验,MAP 考虑。在 Matlab 的 fitcnb 里,这个参数叫 Prior,下面第 4 章会专门讲怎么调。
2.2 判别式贝叶斯分类:LDA/QDA 与朴素贝叶斯的分水岭
朴素贝叶斯不建模特征之间的相关性,而判别式贝叶斯分类直接在多元高斯假设下建模整个特征向量。LDA(线性判别)假设所有类别的协方差矩阵相同,所以决策边界是线性的;QDA(二次判别)允许每个类别有自己的协方差矩阵,决策边界是二次曲面。Matlab 里对应函数是 fitcdiscr,通过 DiscrimType 参数切换。
如果把高斯朴素贝叶斯写成矩阵形式,它等于假设特征协方差矩阵是对角阵,即特征之间在类别内部完全无关。这个认识很有用:当你的特征明显相关时,朴素贝叶斯会吃亏,而 fitcdiscr 能利用这部分信息。代价是参数数量上升:LDA 要估计一个特征维度平方量级的共享协方差矩阵,QDA 则对每个类别都估计一个。每类样本数小于特征数时协方差矩阵奇异,表现就是 Matlab 直接报错,后面避坑章节会专门讲。
选型的经验是:特征数量少、样本量中等、各类协方差结构相似,优先 LDA;样本量大、各类数据形态差异明显,试 QDA;特征之间相关性弱、想快速拿基线,朴素贝叶斯就够了。实际项目中我不只一次看到有人把三种模型都跑一遍,最后发现 fitcnb 和 LDA 结果几乎一样——那说明你的特征相关性没有想象中严重,用简单的那个就行。
3. 用 Matlab 跑通贝叶斯分类:fitcnb 最小可复现流程
只要装好的 Matlab 自带 Statistics and Machine Learning Toolbox,本文全部代码都能跑,不用额外装任何包。下面从最小流程开始,先跑通再讲参数。
3.1 最小训练与预测:fitcnb + cvpartition 的最小流程
clear; clc; close all; rng(2024); % 固定随机种子,保证结果可复现 % 自带鸢尾花数据集,meas 是 150x4 的特征矩阵 load fisheriris Y = categorical(species); % 将类别转为 categorical X = meas; % 按 7:3 划分训练/测试集;cvpartition 对分类标签默认分层 cv = cvpartition(Y, 'HoldOut', 0.3); idxTrain = training(cv); idxTest = test(cv); XTrain = X(idxTrain, :); YTrain = Y(idxTrain); XTest = X(idxTest, :); YTest = Y(idxTest); % 训练朴素贝叶斯分类器 mdl = fitcnb(XTrain, YTrain, ... 'ClassNames', {'setosa', 'versicolor', 'virginica'}, ... 'DistributionNames', 'normal'); % 预测:第一个输出是硬标签,第二个是后验概率 [predLabel, posterior] = predict(mdl, XTest); % 混淆矩阵看错在哪里 figure confusionchart(YTest, predLabel); acc = mean(predLabel == YTest); fprintf('测试集准确率: %.2f%%\n', acc * 100);rng(2024) 的作用是把随机划分固定住,否则每次跑结果都有细微差异,排错时很难判断是代码问题还是数据划分运气问题。cvpartition 的 HoldOut 参数指定测试集比例,0.3 表示测试集占三成;它对分类标签默认做分层划分,类别不平衡时不会把某一类全抽到测试集里。ClassNames 必须显式写全,这是新手最容易漏的:如果训练集切分后恰好缺失某个类别,predict 阶段类别映射会错位甚至报错。
DistributionNames 先用 'normal',也就是每个类、每个特征单独估计均值和方差,这是最稳的默认选择。跑完看混淆矩阵,如果对角线上的数字明显占优,说明模型没有翻车,可以从这里继续往下调。
3.2 判别式贝叶斯分类:fitcdiscr 的 LDA 与 QDA 模板
% LDA:要求各类协方差相同,边界是线性的 ldaMdl = fitcdiscr(XTrain, YTrain, ... 'DiscrimType', 'linear'); % QDA:每类一个协方差矩阵,边界是二次的 qdaMdl = fitcdiscr(XTrain, YTrain, ... 'DiscrimType', 'quadratic'); % 样本量不足或协方差奇异时,先用 pseudo 版本,不报错但会损失部分精度 pseudoMdl = fitcdiscr(XTrain, YTrain, ... 'DiscrimType', 'pseudoLinear'); predLDA = predict(ldaMdl, XTest); predQDA = predict(qdaMdl, XTest);fitcdiscr 和 fitcnb 的差别在 DiscrimType。'linear' 用所有类别共享的协方差矩阵,参数少、鲁棒性高,但假设各类数据形状一样;'quadratic' 每类各自估计协方差矩阵,能刻画形状差异,但每类需要的样本量也更大,经验值是每类样本数至少超过特征数。pseudoLinear 和 pseudoQuadratic 是协方差奇异时的“后悔药”,用伪逆或其他修正手段让训练不崩,精度会有损失,但至少能出结果。
选型判断就一句话:先跑 linear,看混淆矩阵里哪些类容易混;如果混的类在特征图里形态差异明显,再换成 quadratic 比较。注意 QDA 不是免费午餐,每类样本太少时换上 quadratic 反而比 linear 更容易过拟合。
3.3 拿回后验概率:predict 的第二个返回值怎么用
% predict 有三个输出:硬标签、后验概率、期望代价 [predLabel, posterior, cost] = predict(mdl, XTest); % 后验概率的列顺序就是 ClassNames 里的顺序 [maxScore, idx] = max(posterior, [], 2); predByProb = mdl.ClassNames(idx); % 与 predLabel 应该一致 % 低置信样本单独挑出来看 lowConf = find(maxScore < 0.7); fprintf('低置信样本数: %d\n', length(lowConf));predict 的第一个输出是模型替你做的取舍,第二个输出才是核心资产:每一行是样本对每个类别的后验概率。第三个输出 cost 是期望误分类代价,模型选标签时实际比较的是 cost,不是直接取后验最大的类——这一点很容易误解。
后验概率的用处很多。做图像分割大作业时,可以把每个像素的颜色值当特征喂给 fitcnb,输出一张和原图同尺寸的置信度图,阈值一调就能做前景背景分离。这个用法比单纯看准确率有价值得多,后面第 6 章展开讲。
4. 把模型参数调明白:DistributionNames、Prior 与误分类代价
fitcnb 的参数不多,但每个都对结果有实质影响。我常用的调参顺序是:先定分布假设,再看先验,最后用代价矩阵做业务修正。
4.1 连续特征用 normal 还是 kernel:分布设定的选型逻辑
| 分布设置 | 适用特征 | 使用场景 | 注意点 |
|---|---|---|---|
| normal | 连续、近似正态 | 多数表格数据的默认选择 | 特征严重偏态时概率估计会失真 |
| kernel | 连续、偏态/多峰/重尾 | 数据分布形态明显非高斯时 | 样本少时带宽估计不稳,高维下内存压力大 |
| mvmn | 离散/名义变量 | 类别型特征 | 训练集没见过的取值直接报错 |
DistributionNames 默认是 'normal',它对每个类、每个特征单独估计均值和方差。这个假设在特征近似单峰、对称时没问题,但真实数据经常是偏态或多峰的,比如传感器振动数据、收入分布这类长尾特征。此时 normal 会把一个双峰分布硬拟合成一个高斯,概率密度估计失真,后验排序就可能乱。
kernel 选项会换成核密度估计,Matlab 对每个特征自动估计带宽,不需要你手工调。它的拟合能力强,但样本量少时带宽估计会不稳,出现过拟合;特征维度高时每个类都要拟合 d 个核密度,训练时间会明显上涨。我的习惯是:先用 normal 跑通,如果交叉验证分数不理想,再挑几个偏态明显的特征换成 kernel,不要一上来就全局 kernel。
mvmn 是给离散特征用的多项式分布,模型直接统计每个取值在各类里出现的频次。它简单直观,但有一个硬伤:预测时遇到训练集没出现过的取值,后验概率直接变成 0,Matlab 会报错。解决办法见第 5 章。
4.2 类别不平衡时修正先验:Prior 参数背后的逻辑
% 默认按样本比例估计先验 mdlDefault = fitcnb(XTrain, YTrain); % 认为三个类别应当等权重 mdlUniform = fitcnb(XTrain, YTrain, 'Prior', 'uniform'); % 自定义先验:按业务经验给出 mdlCustom = fitcnb(XTrain, YTrain, 'Prior', [0.7 0.2 0.1]);Prior 参数的默认值是 'empirical',就是从训练集里统计类别频率。当类别分布均衡时没问题,但数据不平衡时会出大问题:比如故障样本只占 1%,默认先验会让模型倾向于把一切都预测为正常类,因为先验概率已经把正常类推到了压倒性位置。
设成 'uniform' 会让模型回到“只看似然”的状态,少数类召回率会提上来,但代价是多数类准确率下降。更专业的做法是自定义先验:如果业务上知道故障发生率就是 5%,就把 [0.95, 0.05] 直接传给 Prior。注意 Prior 对 trained model 的 predict 阶段同样生效,改先验等于同时改了训练和推理时的决策偏向。
这里要纠正一个常见误区:类别不平衡本身不是问题,代价不对称才是。如果漏报故障和误报正常的损失一样,那按频率做先验其实合理。先想清楚业务上哪种错误更贵,再决定 Prior 怎么设。
4.3 误分类代价与阈值:贝叶斯决策业务化
% 两类:1=正常,2=故障;漏报故障的代价设成误报的 5 倍 Cost = [0 1; 5 0]; mdlCost = fitcnb(XTrain, YTrain, 'Cost', Cost);Cost 矩阵是让贝叶斯分类器“业务化”的接口。Cost(i,j) 表示真实类别 i 被预测成 j 的代价,默认全是 1。上面这个例子中,把故障漏报的代价设为 5,模型在两类后验接近时会更偏向预测为故障,因为误报的期望代价比漏报小。
要注意 Cost 影响的是 predict 阶段选标签的策略,不影响后验概率本身。换句话说,训练出来的概率是一样的,只是决策线被移动了。这点和直接对 posterior 设阈值本质等价:二分类时 Cost 非对称就等价于把决策阈值从 0.5 挪到一个更保守的位置。选一种方式实现即可,不要两个同时调,否则你自己都说不清模型在做什么。
5. 贝叶斯分类常见问题与避坑:5 个翻车现场还原
5.1 mvmn 遇到未见取值:离散特征带来的零概率
现象:预测时报错,提示训练数据里没有出现过的 level,后验计算直接失败。
原因:mvmn 分布下,模型记录的是每个离散取值在各类里的出现频次。新取值没有任何统计记录,条件概率 P(x|y) 算出来是 0,连乘后整行后验都为 0,分类无从谈起。这是朴素贝叶斯在离散特征上最经典的翻车点,fitcnb 没有开箱即用的拉普拉斯平滑参数,不能指望模型自己兜底。
解决:训练前做取值归并,把频次低于阈值的离散取值统一合并成 'Other' 类;预测前对新数据做同样的映射,保证训练和预测走同一个预处理函数。如果离散特征取值天生就是开放集合,比如文本分类里的词,建议改用 kernel 或者干脆换成别的模型。
5.2 后验概率全压成 0 或 1:别把概率当校准值用
现象:预测结果看着不错,准确率很高,但 posterior 输出大量 0.99 甚至 1.0,看起来很“自信”。
原因:朴素贝叶斯把弱相关的特征当成独立证据累乘,重复信息被反复计票,概率估计的绝对值会被推向极端。分类决策通常不受影响,因为相对排序仍然正确,但后验的绝对值已经不能当作真实置信度来读。
解决:不要把后验当校准概率用。需要给业务方解释“置信度”时,优先展示排序关系;如果非要校准,单独留一个验证集做 Platt 缩放或保序回归。更简单的方式是只取后验做阈值筛选,不纠结它是不是真实概率。
5.3 特征强相关时朴素假设失效:重复特征等于翻倍投票
现象:把两个强相关特征同时放进模型,训练精度不但没提升,反而下降;或者把同一个特征复制一份放进特征矩阵,模型立刻变“自信”了。
原因:朴素贝叶斯的条件独立假设下,特征被当成独立证据。两个强相关特征提供的是同一份信息,却被计了两票,相当于给决策边界加了错误权重。
解决:先做相关性筛查,相关系数超过 0.8 的特征只留一个,或者用 PCA 降维后再喂给 fitcnb。如果项目里特征相关性强,直接用 fitcdiscr 的 linear 模式,它能建模协方差,天然规避这个问题。
5.4 kernel 密度在过拟合与速度之间摇摆:带宽的玄学
现象:训练集上准确率接近满分,交叉验证分数波动很大;特征一多,训练时间从秒级变成分钟级。
原因:kernel 的带宽是 Matlab 基于数据自动估计的。样本量少时,带宽估计会偏小,核密度曲线跟着每个样本走,过拟合随之而来。高维场景下每个类、每个特征都要拟合一个密度估计,计算量和内存都上去了。
解决:kernel 慎用在高维数据上。可以先跑 normal 看基线,只在明显偏态的单特征上换成 kernel。如果必须用 kernel,先做特征选择把维度压到十几个以内。带宽自动化对多数场景够用,不建议手工去碰。
5.5 fitcdiscr 的奇异协方差:pseudo 是最后的后悔药
现象:fitcdiscr 训练直接报错,提示协方差矩阵奇异或非正定。
原因:LDA 和 QDA 都要估计协方差矩阵。特征数接近或超过每类样本数时,矩阵不满秩,求逆失败。QDA 更敏感,因为每个类别都要单独估一个矩阵,只要有某一类样本偏少,就会整场崩溃。
解决:把 DiscrimType 换成 pseudoLinear 或 pseudoQuadratic,让 Matlab 用修正手段强行训练。更稳的路线是先做 PCA 把特征维度压到样本量以下再上 fitcdiscr。如果是 QDA 崩了,先检查是不是某个类别的样本数太少,优先补样本而不是换模型。
6. 进阶:把贝叶斯分类器用作不确定性估计与集成基模型
6.1 阈值拒识:让低置信预测留在流程外
thr = 0.6; [maxPost, idxPred] = max(posterior, [], 2); accept = maxPost >= thr; % accept 为 0 的样本不进自动流程,转人工复核或标记待定 rejectIdx = find(~accept);这个技巧在业务落地里比调任何参数都实用。分类器不需要对所有样本都硬给一个标签,低置信样本直接踢出自动流程,准确率立刻提升。thr 的取值通过验证集上的 ROC 曲线来定:横轴是误报率,纵轴是召回率,找到业务能接受的点,再反查对应的后验阈值。
6.2 随机投影 + 朴素贝叶斯集成:打破特征相关的一种低成本做法
% 用两个随机子空间训练两个朴素贝叶斯,测试时投票 rng(7) proj1 = randn(size(X, 2), 8); proj2 = randn(size(X, 2), 8); mdl1 = fitcnb(X * proj1, Y, 'DistributionNames', 'kernel'); mdl2 = fitcnb(X * proj2, Y, 'DistributionNames', 'kernel'); p1 = predict(mdl1, X * proj1); p2 = predict(mdl2, X * proj2); % 示例:两票一致才通过,不一致标记待定 finalPred = p1; finalPred(p1 ~= p2) = '待定';随机投影把原始特征映射到低维子空间,相当于每次只保留一部分混合信息,天然打断特征间的强相关。多个投影子空间训练多个朴素贝叶斯,投票时模型之间差异越大,集成的提升越明显。这个做法不追求单个模型精度,追求的是“不同视角下的分歧”,和随机森林的随机特征选择思路一致。特征多、样本不太少时值得一试,样本量小时直接降维更稳。
6.3 验证习惯:交叉验证与 ROC 一起看
% 5 折交叉验证看整体损失 cvm = crossval(mdl, 'KFold', 5); loss = kfoldLoss(cvm); fprintf('交叉验证损失: %.4f\n', loss); % 新版本用 rocmetrics 画 ROC,旧版本用 perfcurve,目的一样 scores = posterior(:, 2); rocObj = rocmetrics(YTest, scores, mdl.ClassNames(2)); plot(rocObj);只看测试集准确率是不够的,尤其在类别不平衡数据上,准确率天然虚高。交叉验证损失给出的是模型稳定性的判断,ROC 曲线给出的是决策阈值可调空间的判断。两个一起看,才能决定贝叶斯分类器是作为最终方案留下,还是只当基线。
我个人的习惯是,任何分类项目都先花五分钟跑一个 fitcnb 基线,把训练耗时、混淆矩阵、ROC 三个东西放在桌面上,再决定要不要上复杂模型。以前做图像分割大作业时,用颜色直方图加 fitcnb,输出后验置信度图,效果比预想的好,后期把阈值调高做拒识就交了差。贝叶斯分类不是万能的,但它是验证“数据里到底有没有信号”的最快手段,省下来的时间值得花在特征和业务规则上。希望帮到你。
本文还有配套的精品资源,点击获取