贝叶斯分类Matlab源码与操作界面:训练预测、调参及混淆矩阵

简介:面向机器学习入门者与需要快速验证分类流程的Matlab用户,这份贝叶斯分类代码包提供从数据读取、模型训练到结果评估的可运行示例,并配有图形操作界面,适合课程实验、算法理解与二次开发。压缩包共28个文件,整体约37KB,以17个m脚本与函数为主,配合8个txt样本及属性数据、2个prj工程文件和1个fig界面文件,内含带变量版本、图形界面版本和多组测试数据,便于分别调试与对照。已有3686人学习下载。读者可据此理清朴素贝叶斯在Matlab中的实现脉络,观察先验概率、条件概率估计与分类预测的衔接方式,并通过界面完成数据导入、参数设置和结果查看;源码结构便于对照修改,既能作为小项目原型,也可用于课堂演示和逐步复现实验过程。

1. 从一张“会算概率的界面”说起:贝叶斯分类源码落地要解决什么

如果你拿到的是一份贝叶斯分类 Matlab 源码,还带操作界面,最先要确认的不是算法高不高级,而是它能不能把“读表—训练—预测—看混淆矩阵”这条链路跑通。很多课程设计或模拟项目X的代码,算法函数写得漂亮,界面一点就报错,问题往往出在数据约定和回调状态上。这套源码适合想快速搭一个可演示、可改参数的分类工具的人:新手能照着界面走完流程,熟手能拆开函数看先验、似然和平滑系数怎么落地。它解决的是“有算法但不成工具”的尴尬,不是替你做特征工程。

2. 贝叶斯分类的骨架:先验、似然与决策规则怎么在 Matlab 里落位

朴素贝叶斯的核心不是“贝叶斯”三个字,而是它把后验概率拆成先验和类条件概率的乘积,再用一个很硬的假设把高维联合分布压成逐特征相乘。这个假设叫条件独立:给定类别后,各特征互不影响。它在真实数据里几乎不成立,但在小样本、多分类、需要快速给出概率输出的场景里,常常能当一个很稳的基线。带操作界面的源码如果只贴一个fitcnb调用,读者看不到参数边界;更常见的做法是自己写训练和预测函数,把均值、方差、先验、平滑系数都暴露出来,界面才有东西可调。

2.1 为什么选朴素贝叶斯而不是判别式模型

判别式模型像逻辑回归、支持向量机,目标是直接找决策边界,通常在特征相关性强、样本量足够时表现更好。朴素贝叶斯的优势在另一个方向:训练快、可增量更新、天然输出后验概率、对小样本不敏感,而且多分类不需要一对多拆。模拟项目X里经常出现“特征只有十几个、样本几百条、还要在界面上解释为什么判成这一类”的需求,这时候把先验和似然摆出来,比只给一个类别标签更有说服力。

选型时要看三个边界。第一,特征是否近似连续且分布不太偏,连续特征用高斯假设最省事;第二,特征是否是计数或二值,计数型更适合多项式模型,二值更适合伯努利模型;第三,类别是否严重不平衡,朴素贝叶斯默认按类别频率估计先验,不平衡时会把多数类先验抬得很高,界面上的后验概率会明显偏向多数类。源码默认走高斯朴素贝叶斯,是因为它最容易和数值型 UITable 对接,也最容易把均值、方差、先验三个参数讲清楚。代价是它对特征分布形态敏感,遇到长尾特征时,方差估计会被极端值拖偏,预测结果就会显得“玄学”。

2.2 高斯朴素贝叶斯的公式与矩阵化实现

高斯朴素贝叶斯对每个类别、每个特征估计一个均值和方差,预测时把样本代入高斯密度,取对数后相加,再加上类别先验的对数,最后取最大。直接连乘容易下溢,尤其特征维度一多,概率乘积会小到浮点数表示不了,所以实现里统一走对数域。下面这段训练函数把类别切分、均值方差估计、先验归一化放在一起,varSmoothing是方差平滑项,用来兜住常数特征。

function model = trainGaussianNB(X, y, varSmoothing, classPriors) % TRAINGAUSSIANNB 训练高斯朴素贝叶斯模型 % X: n×d 数值特征矩阵 % y: n×1 类别标签,categorical/string/cellstr 均可 % varSmoothing: 方差平滑系数,防止某类某特征方差为 0 % classPriors: 可选先验向量,空则按类别频率估计 if nargin < 3 || isempty(varSmoothing) varSmoothing = 1e-6; end y = categorical(y); % 统一标签类型,固定类别顺序 classes = categories(y); [n, d] = size(X); k = numel(classes); if nargin < 4 || isempty(classPriors) classPriors = countcats(y) / n; % 按样本频率估计先验 else classPriors = classPriors(:) / sum(classPriors); end mu = zeros(k, d); sigma2 = zeros(k, d); for i = 1:k idx = (y == classes{i}); Xi = X(idx, :); mu(i, :) = mean(Xi, 1); sigma2(i, :) = var(Xi, 0, 1) + varSmoothing; end model.classes = classes; model.priors = classPriors; model.mu = mu; model.sigma2 = sigma2; model.varSmoothing = varSmoothing; model.featureNames = {}; end

这段代码的逻辑很直:先固定类别顺序,再按类别取子矩阵,逐列算均值和方差。var(Xi,0,1)的第二个参数 0 表示按样本数减一归一化,第三个参数 1 表示沿列方向算方差。varSmoothing默认给1e-6,不是越小越好;如果某个特征在某一类里全是同一个值,方差为 0,取对数会变成负无穷,预测分数直接失效。平滑项加到方差上,相当于给每个特征一个最小可信波动。classPriors允许手工传,是为了后面处理类别不平衡,传进来后统一归一化,避免界面输入的权重不满足概率和为一。

预测函数同样走对数域,并且把后验概率归一化后返回,方便界面显示“这一类有多大的把握”。注意归一化只是展示用,分类决策仍然看对数后验的最大值。

function [yPred, score] = predictGaussianNB(model, X) % 对数域计算高斯朴素贝叶斯后验,避免概率连乘下溢 n = size(X, 1); k = numel(model.classes); logPost = zeros(n, k); for i = 1:k mu = model.mu(i, :); s2 = model.sigma2(i, :); logLik = -0.5 * sum(log(2 * pi * s2), 2) ... -0.5 * sum(((X - mu).^2) ./ s2, 2); logPost(:, i) = log(model.priors(i)) + logLik; end [~, maxIdx] = max(logPost, [], 2); yPred = model.classes(maxIdx); score = exp(logPost - max(logPost, [], 2)); % 归一化后验,仅用于展示 end

参数s2是加了平滑后的方差向量,log(2*pi*s2)对应高斯密度里的归一化常数,((X-mu).^2)./s2对应指数项。max(logPost,[],2)先取每个样本的最大对数后验,再用exp做减法和归一化,避免直接exp(logPost)溢出。代码里没有处理缺失值,也没有处理非数值列,这两件事必须在训练入口前做掉,否则mean和var会直接报错或者返回NaN。

2.3 数据表约定与训练入口脚本

带操作界面的源码最容易翻车的地方不是算法,而是表结构。常见约定是:UITable 的前 d 列是数值特征,最后一列是类别标签;列名保留,训练时按列名取特征,预测时按同一组列名校验。下面这个入口脚本负责读表、删缺失行、训练、保存模型,适合先在命令行跑通再接到界面上。

T = readtable('sim_features.csv'); labelName = T.Properties.VariableNames{end}; if any(ismissing(T), 'all') T = rmmissing(T); % 先做最简单的缺失值处理 end y = T.(labelName); X = T{:, 1:end-1}; % 前 d 列作为特征 model = trainGaussianNB(X, y, 1e-6); model.featureNames = T.Properties.VariableNames(1:end-1); save('nb_model.mat', 'model');

readtable读进来的列可能是 double、string、cellstr 混合,T{:,1:end-1}要求这些列都能拼成数值矩阵,所以读表后最好先检查varfun(@class,T,'OutputFormat','cell')。rmmissing是保底手段,直接删行会丢样本,样本少的时候要改成按列中位数填补,或者在界面上给出缺失值提示。model.featureNames必须和模型一起保存,后面单样本预测时用它校验输入顺序,不能只靠列号。

3. 带操作界面的实现:从回调函数到预测结果回显

Matlab 里做操作界面,常见两条路:老一点的 GUIDE 和现在更常用的 App Designer。源码包如果带.mlapp,大概率是 App Designer;如果是.fig加.m,就是 GUIDE。两者思路一样:控件收集参数,回调函数读数据、调训练或预测函数、把结果写回控件。区别在 App Designer 把界面和代码放在一个类里,属性可以直接挂模型和数据表,状态管理更清楚。下面按 App Designer 的写法拆,GUIDE 也能照着搬,只是app.换成handles.。

3.1 App Designer 的控件布局与数据流

一个够用的布局分三块。左边是数据区:一个“加载数据”按钮、一个 UITable 显示训练表、一个标签显示行列数。中间是参数区:方差平滑系数的数值编辑框、先验选择下拉框、一个“训练模型”按钮、一个状态标签。右边是预测区:单样本输入框(或者一行数值编辑框)、一个“预测”按钮、一个显示预测类别的标签、一个显示后验概率的小表;下面再放一个批量预测的 UITable 和混淆矩阵按钮。控件不用多,关键是数据流单向:加载数据只改TrainTable,训练只改Model和状态标签,预测只读Model和输入控件,不互相偷偷改。

App Designer 里建议加这几个属性:TrainTable存原始表,Model存训练好的结构体,FeatureNames存特征列名,ClassNames存类别顺序。属性一多,回调里就不会出现“这个变量从哪来的”这种黑匣子问题。状态标签要写清楚当前是“未训练”“训练中”“训练完成,3 类,12 个特征”,不要只写“成功”,否则后面排查时根本不知道成功的是哪一步。

3.2 训练按钮回调:参数读取、模型保存与状态复位

训练按钮回调的第一件事不是训练,而是校验。表为空、列数少于 2、标签列全是缺失值、平滑系数不是正数,这些都要在训练前拦掉。下面这段回调把 UITable 读出来,删缺失行,取特征和标签,调用训练函数,最后把特征名和类别顺序写进模型。

function TrainButtonPushed(app, event) % 训练按钮回调:从 UITable 读数据,训练并缓存模型 T = app.UITable.Data; if isempty(T) || width(T) < 2 app.StatusLabel.Text = '数据为空或列数不足'; return; end labelName = T.Properties.VariableNames{end}; if any(ismissing(T), 'all') T = rmmissing(T); app.UITable.Data = T; % 回写,保证界面和训练数据一致 end y = T.(labelName); X = T{:, 1:end-1}; smooth = app.SmoothEditField.Value; if ~isscalar(smooth) || smooth <= 0 app.StatusLabel.Text = '平滑系数必须为正标量'; return; end app.Model = trainGaussianNB(X, y, smooth); app.FeatureNames = T.Properties.VariableNames(1:end-1); app.Model.featureNames = app.FeatureNames; app.StatusLabel.Text = sprintf('训练完成:%d 类,%d 个特征', ... numel(app.Model.classes), size(X, 2)); end

这段回调的关键在app.UITable.Data = T这一句。删缺失行后如果只改局部变量,界面上还是旧表,用户看到的行数和模型实际用的行数不一致,后面预测就会对不上。app.Model.featureNames和app.FeatureNames同时存,是为了模型保存后单独加载也能校验输入。平滑系数从SmoothEditField读,读出来先判断正标量,避免用户输入空值或向量导致var行为异常。

训练完成后不要立刻做预测,也不要在训练回调里顺手画混淆矩阵。训练和评估分开,按钮职责单一,界面才不会越点越乱。如果数据量大,训练前把按钮置灰,训练后恢复,避免重复点击叠加训练。

3.3 预测按钮回调:单样本与批量表格两条路径

单样本预测和批量预测要分开写。单样本走一行数值输入,适合演示“改一个特征值,类别怎么变”;批量预测走 UITable,适合跑测试集。两条路径都要先校验特征数,再调同一个predictGaussianNB。下面先看单样本回调。

function PredictButtonPushed(app, event) % 单样本预测:输入一行特征,输出类别和后验概率 if isempty(app.Model) app.StatusLabel.Text = '请先训练模型'; return; end x = app.SingleSampleEditField.Value; if numel(x) ~= numel(app.FeatureNames) app.StatusLabel.Text = '单样本特征数与训练不一致'; return; end [yPred, score] = predictGaussianNB(app.Model, x); app.PredLabel.Text = string(yPred); app.ScoreTable.Data = array2table(score, ... 'VariableNames', cellstr(app.Model.classes)); end

SingleSampleEditField.Value在 App Designer 里可以是一个数值向量控件,也可以是一组数值编辑框拼出来的向量,具体看界面怎么搭。numel(x)必须等于训练特征数,这个校验不能省,因为用户很可能只改了某一列的顺序,数量没变但含义变了。score是归一化后的后验,转成 table 是为了让列名直接显示类别名,界面上看起来比裸矩阵清楚。

批量预测回调和单样本不同,它要把预测结果写回表里,还要处理真实标签列。真实标签列不一定叫TrueLabel,所以代码里先判断列名,找不到就不画混淆矩阵,只回写预测列。

function BatchPredictButtonPushed(app, event) % 批量预测:读批量表,回写预测列,有真实标签时画混淆矩阵 T = app.BatchTable.Data; if isempty(app.Model) || isempty(T) app.StatusLabel.Text = '模型或批量数据为空'; return; end d = numel(app.FeatureNames); X = T{:, 1:d}; [yPred, ~] = predictGaussianNB(app.Model, X); T.Predicted = yPred; app.BatchTable.Data = T; if any(strcmp(T.Properties.VariableNames, 'TrueLabel')) figure; confusionchart(T.TrueLabel, T.Predicted); end end

T{:,1:d}按列号取特征,前提是批量表的列顺序和训练表一致。更稳的写法是按app.FeatureNames逐列取,遇到缺列直接报错。confusionchart要求真实标签和预测标签的类别集合一致,如果测试集里少了一个类别,图会显示不全,但不会报错,这种“看起来正常”的结果最容易骗人。批量预测完在状态标签里补一句“已预测 N 行”,比只画图更利于排查。

3.4 混淆矩阵与交叉验证结果的界面展示

混淆矩阵不要每次预测都弹新图,弹多了窗口叠在一起,关起来很烦。常见做法是单独一个“评估”按钮,读批量表里的真实标签和预测列,先检查两者类别集合,再画图。交叉验证结果可以放在同一个评估回调里,用cvpartition跑 K 折,把每折准确率显示在一个小表里。界面只负责展示,验证逻辑放在独立函数里,这样命令行也能复现。

评估回调里要注意类别顺序。confusionchart默认按字母或数值排序,如果训练时categories(y)的顺序和测试集不一致,矩阵的行列会对不上。稳妥做法是构造categorical时显式传入类别顺序:categorical(T.TrueLabel, app.Model.classes)。后验概率表也一样,列名用app.Model.classes,不要用unique重新排。

4. 参数怎么设:平滑、特征分布与验证策略的三个开关

带界面的源码把参数暴露出来是好事,但参数一多,用户很容易把平滑系数拉到1以为更稳,结果后验全被压平,分类退化成按先验猜。参数设置要看数据形态,不是看哪个数值顺眼。下面三个开关最值得先调:方差平滑、连续特征的处理方式、验证策略。每个开关都有对应的观察指标,不能只看训练集准确率。

4.1 平滑系数与零概率问题

平滑系数的作用是给方差一个下限。高斯朴素贝叶斯里,如果某类某特征方差为 0,对数似然会出现-Inf,这个类别直接出局。平滑系数太小兜不住,太大又会把真实方差抹掉。常用起点是1e-6到1e-2,特征量纲大、常数特征多时往大调,特征已经标准化且样本充足时往小调。判断方法不是看训练准确率,而是看交叉验证准确率的均值和标准差,均值高但标准差大的参数,在界面上演示时更容易翻车。

下面这段脚本扫描一组平滑系数,每个系数跑 5 折分层交叉验证,输出均值和标准差。cvpartition的Stratify设为true,保证每折类别比例接近。

smoothList = [1e-8, 1e-6, 1e-4, 1e-2, 1e-1]; for s = smoothList acc = zeros(5, 1); cvp = cvpartition(y, 'KFold', 5, 'Stratify', true); for i = 1:5 tr = training(cvp, i); te = test(cvp, i); m = trainGaussianNB(X(tr, :), y(tr), s); yp = predictGaussianNB(m, X(te, :)); acc(i) = mean(yp == y(te)); end fprintf('smooth=%g, acc=%.3f±%.3f\n', s, mean(acc), std(acc)); end

training(cvp,i)和test(cvp,i)返回逻辑索引,直接拿来切矩阵。acc存每折准确率,最后打印均值和标准差。平滑系数不是越大多好,1e-1在某些特征上会把方差抬得过高,导致类条件分布重叠,准确率反而掉。脚本跑完不要只挑均值最高的,标准差明显大的参数要慎用,界面演示时换一批测试样本就可能变脸。

4.2 连续特征离散化还是高斯假设

高斯假设适合连续、近似对称、没有极端离群值的特征。遇到长尾分布、计数型特征、明显偏态的特征,强行套高斯会把均值拉偏。常见替代是离散化后走多项式模型,或者二值化后走伯努利模型。离散化不是随便分箱,分箱边界要在训练集上确定,再应用到测试集,不能拿全体数据一起分箱,否则验证结果会偏乐观。

Matlab 里可以用discretize做等宽或等频分箱,边界存进模型。下面这段只演示等宽分箱的思路,edges必须来自训练集。

% 在训练集上确定分箱边界,再应用到测试集 edges = linspace(min(X(tr, 1)), max(X(tr, 1)), 6); XtrBin = discretize(X(tr, 1), edges); XteBin = discretize(X(te, 1), edges); XteBin(isnan(XteBin)) = 1; % 测试集超出边界时归到第一箱

linspace生成 6 个边界,得到 5 个箱。discretize对超出边界的值返回NaN,测试集出现训练集没见过的范围时要单独处理,归到边界箱或者标记为缺失。离散化后特征从连续变类别,训练函数要换成多项式或伯努利版本,不能继续用高斯均值方差。源码如果只带高斯版本,至少要在界面上给一个“特征类型”下拉框,让用户知道当前假设是什么。

4.3 分层交叉验证与类别不平衡

类别不平衡时,普通随机划分可能让某一折里少数类样本极少,准确率看起来很高,实际少数类全错。分层交叉验证按类别比例分折,能缓解这个问题。更彻底的做法是调先验:把少数类先验手工抬高,或者在训练前做重采样。朴素贝叶斯的先验是显式参数,调起来比改模型结构直接。

参数表可以按下面这个粒度记,界面上不用全暴露,但源码里要能改:

参数作用常用起点观察点
varSmoothing方差不等于零的下限1e-6~1e-2常数特征多时加大
classPriors类别先验权重类别频率少数类召回是否上升
KFold交叉验证折数5小样本用 3 或留一
验证集比例留出法切分0.2~0.3类别少时必须分层
分箱数连续特征离散化5~10箱内样本是否过少

先验调整不要和重采样同时上,否则少数类被抬两次,后验会过度偏向少数类。比较稳的顺序是:先分层划分,再看混淆矩阵,确认少数类召回低,再调先验,最后才考虑重采样。每次只动一个参数,界面上的后验概率表才能反映真实变化。

5. 避坑与排查:界面能跑但结果不对的五个现场

带操作界面的贝叶斯分类源码,最怕的不是报错,而是不报错但结果不对。报错至少知道去哪找,结果不对往往要翻数据、翻回调、翻模型结构。下面五条是这类源码里反复出现的现场,每条按“现象—原因—解决”拆开写,照着排查能省不少时间。

5.1 训练集和预测集特征顺序不一致

现象:训练集准确率能到 0.9,界面上单样本预测却乱跳,改一个明明很关键的特征,预测类别不变;批量预测的准确率也明显低于训练集。原因通常有两个:UITable 的列被用户拖动过,列顺序变了但代码仍按列号取特征;或者批量表里多了一列 ID,T{:,1:d}把 ID 当成了第一个特征。列号取特征在演示时很方便,一旦列顺序变化,模型看到的特征含义就全变了。

解决分三步。第一,训练时把featureNames存进模型,预测时按名称取列,不按列号。第二,单样本输入控件按特征名排列,界面上显示列名,不只显示“特征 1、特征 2”。第三,批量预测前做列名校验,缺列直接报错,不要静默补零。按名称取列的写法比按列号啰嗦,但能挡住大部分“看起来能跑”的错位。

5.2 类别标签类型混用导致索引错位

现象:混淆矩阵的对角线看起来正常,但某一类的后验概率列名和真实类别对不上;或者保存模型后重新加载,预测类别变成了数字。原因通常是标签类型不统一:训练时用 cellstr,预测时用 string,categorical自动排序后类别顺序变了;或者代码把数值标签直接当类别索引,classes(maxIdx)和maxIdx混用。countcats和categories的顺序不一定等于你想象的标签顺序,尤其是字符串标签按字典序排。

解决方式是训练入口统一转categorical,把categories(y)的顺序存进model.classes,预测时只用model.classes(maxIdx)取类别名,不拿索引去反查原始标签。保存模型时把classes一起存,加载后先检查iscategorical和numel(model.classes),再允许界面预测。如果标签是数值,也不要直接当索引用,先转成 categorical 或者做一层映射表。

5.3 缺失值与常数特征把方差算成零

现象:训练时弹出NaN或Inf,预测时所有样本都判成同一类;后验概率表里某一列全是 1,其他列全是 0。原因有两个:表里有缺失值,mean和var返回NaN,对数似然变成NaN,max的比较结果不可靠;或者某个特征在某一类里全是常数,方差为 0,log(0)变成-Inf,这个类别的分数直接失效。

解决方式是在训练前检查缺失值,按列做中位数填补或删行,不要等到var报错才处理。常数特征要么删掉,要么给方差加平滑项,平滑系数从1e-6起步,不要用eps这种过小的值。训练函数里可以加一句断言:assert(all(sigma2(:) > 0), '存在零方差特征'),把问题拦在训练阶段,而不是等到界面上预测出怪结果再回头找。

5.4 回调里重复训练导致界面卡死

现象:点一次训练按钮,界面转圈很久;连点几次后更卡,状态标签还停在“训练中”。原因通常是回调里做了重活:每次训练都重新读大文件、重复save模型、在循环里更新控件;按钮没有置灰,用户以为没点上就继续点,回调排队执行。App Designer 的回调默认在主线程跑,训练量大时界面会失去响应。

解决方式是训练前把按钮Enable设为'off',状态标签改成“训练中”,调一次drawnow让界面刷新,训练完再恢复。模型保存只在训练成功后做一次,不要放在循环里。数据量大到界面明显卡顿时,把训练放到parfeval后台执行,回调里只取Future结果,不要在主线程里硬扛。界面卡死不是算法慢,很多时候是重复训练和重复保存叠出来的。

5.5 模型保存后加载报结构体字段不匹配

现象:在 A 机器上训练保存的nb_model.mat,换到 B 机器加载后预测报“字段不存在”;或者改了训练函数后,旧模型还能加载,但预测结果全错。原因是模型结构体字段变了,旧模型没有新字段;或者保存时只存了mu、sigma2,没存featureNames和classes,加载后按新代码的字段去取,自然对不上。

解决方式是在模型里加一个版本字段,比如model.version = 2,加载时先检查字段是否齐全,缺字段就拒绝预测并提示重新训练。保存时把classes、featureNames、varSmoothing、priors一起打包,不要只存均值和方差。界面加载模型后先做一次字段校验,再允许点预测按钮。旧模型不是不能兼容,但兼容代码要显式写,不能靠“应该能跑”的侥幸。

6. 进阶用法:把“一次训练”变成可复用的验证流水线

界面能点通只是第一步。要把这套贝叶斯分类源码用在模拟项目X的对比实验里,我一般会把训练、预测、验证拆成三个独立函数,GUI 只负责收集参数和展示结果。这样换数据、换平滑系数、换先验时不用点界面,直接脚本跑批。验证策略可以按样本量选:

策略适用场景注意点
留出法样本多、快速看趋势必须分层抽样
K 折交叉验证样本中等K=5 起步
重复分层 K 折样本少、类别不平衡重复 5~10 次看均值方差

下面这段脚本做重复分层交叉验证,外层重复 5 次,内层 5 折,观察不同平滑系数下准确率的均值和标准差。cvpartition每次重新分层,减少单次划分的偶然性。

smoothList = [1e-6, 1e-4, 1e-2]; for s = smoothList acc = zeros(5, 1); for r = 1:5 cvp = cvpartition(y, 'KFold', 5, 'Stratify', true); for i = 1:5 tr = training(cvp, i); te = test(cvp, i); m = trainGaussianNB(X(tr, :), y(tr), s); yp = predictGaussianNB(m, X(te, :)); acc(i) = mean(yp == y(te)); end end fprintf('smooth=%g, acc=%.3f±%.3f\n', s, mean(acc), std(acc)); end

acc的长度是 5,对应内层每折准确率,外层重复只用来观察波动,不会把 25 个结果混在一起平均。看结果不要只挑最高均值,标准差大的参数在界面上演示时更容易翻车。把FeatureNames、classes、varSmoothing一起存进模型,加载后先做字段校验,再允许界面预测。从那以后我每次改完回调,都强制先用模拟数据跑一遍训练—预测—混淆矩阵三段式,再交给别人点按钮。希望帮到你。

本文还有配套的精品资源,点击获取