尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MATLAB BP神经网络分类实战:从蠓虫到鸢尾花全流程

MATLAB BP神经网络分类实战:从蠓虫到鸢尾花全流程 简介这是一份面向MATLAB初学者的ANN神经网络入门资料聚焦BP神经网络在分类问题中的完整实现适合刚接触神经网络或希望在MATLAB中动手实践分类任务的读者。压缩包内共7个文件含5个txt数据文件与2个m源程序结构清晰数据与代码分离便于对照运行。资源大小仅6KB轻量易用。已有290人学习下载。内容覆盖蠓虫分类与Fisher鸢尾花分类两个经典案例分别提供训练数据、测试数据及对应分类源程序可帮助读者理解BP网络从数据预处理、网络构建到训练验证的基本流程并可直接修改数据或参数迁移到其他分类场景是入门神经网络分类问题的实用参考。1. 从蠓虫到鸢尾花用 MATLAB 亲手复现 BP 神经网络的分类全流程做模式识别的人大概率都见过那两个经典数据集一个是根据触角长度和翅膀长度区分两种蠓虫Af 与 Apf另一个是费希尔鸢尾花的四维特征三分类问题。它们几乎被写进了每一本模式识别教材但真正在 MATLAB 里从零把 BP 神经网络跑通、跑到能对新样本分类的人并不多。手头这份资源正好把两条完整的实验链路打包了——每个案例都包含trainData.txt、testData.txt、分类源程序蠓虫对应Classify_mosquito.m鸢尾花对应Classify_Iris.m。它适合两类人刚接触 ANN 想找一份能复现的入门代码的初学者以及需要在课程设计或实验报告里快速搭建基线模型的从业者。下文按数据预处理、网络构建、训练验证、排错调参的顺序展开所有代码都基于 MATLAB 神经网络工具箱可以直接对照运行。2. 数据预处理与标签编码mapminmax 与 ind2vec 的配合2.1 两类数据集的结构差异决定了读入方式不同蠓虫数据是典型的二分类小样本问题。trainData.txt里每行两个特征触角长度、翅膀长度最后一列是类别标签testData.txt结构和它一致。鸢尾花则有四个特征花萼长宽、花瓣长宽类别是三分类。读取上两者没有本质区别统一用load或importdata都能解决关键是后续的归一化和标签处理。% 蠓虫数据读取 data load(trainData.txt); X data(:, 1:2); % 特征 labels data(:, 3); % 原始标签1 或 2 % 鸢尾花数据读取特征维度为 4 iris_data load(trainData.txt); iris_X iris_data(:, 1:4); iris_labels iris_data(:, 5);这里X是输入矩阵行对应样本、列对应特征维度。按 MATLAB 神经网络工具箱的习惯后续网络接口要求输入是「特征 × 样本」的布局也就是行是维度、列是观测所以读进来之后通常要转置再喂给网络。很多初学者的第一个train报错就出在矩阵方向上后面会专门说。2.2 归一化为什么不能跳过BP 神经网络对输入特征的尺度非常敏感。蠓虫的触角长度可能集中在 1.02.0 之间翅膀长度在 1.02.5 之间量纲看起来差别不大但鸢尾花的花瓣长度和花萼宽度之间就有明显的数值分布差异。如果原始数值直接进网络梯度下降在权重更新时会被大数值特征主导小数值特征对误差的贡献被稀释收敛速度变慢且可能陷入局部极小。MATLAB 工具箱对归一化有内置机制mapminmax默认把输入映射到[-1, 1]区间train函数在训练前会自动执行。但显式做一遍有两个额外好处一是测试集可以用训练集的归一化参数ps做同样的变换保证数据分布一致二是你自己能清楚地看到网络实际接收的数值范围排查问题时少一层黑盒。[X_norm, ps] mapminmax(X); % 注意这里对转置后的矩阵做归一化 test_data load(testData.txt); X_test_raw test_data(:, 1:2); X_test_norm mapminmax(apply, X_test_raw, ps);ps里面保存了训练集每个特征的最小值和缩放比例测试集必须复用这份参数而不是自己再单独归一化一遍。如果测试集独立调mapminmax等于把两个数据集各自映射到了不同的数值空间模型的泛化评估就失真了。2.3 标签的两种编码方式蠓虫原始标签是 1 和 2鸢尾花可能是 1、2、3。这种顺序编码直接拿去训练有两个隐患一是类别之间的数值差距会被网络理解为某种「距离关系」比如 3 和 1 的差异被放大成 2 和 1 的两倍但类别本身是无序的二是输出层如果只有一个神经元阈值切割的位置很难选得稳定。更稳妥的做法是转为 one-hot 向量工具箱里对应函数是ind2vec。它把类别索引转换成稀疏矩阵每一列只有一个 1其他位置都是 0。比如三个样本依次属于第 1、第 2、第 3 类转换后得到的是一个 3×3 的矩阵第i列的第c(i)行为 1。输出层神经元数量就和类别数一致每个神经元对应一个类别的响应强度分类决策取最大值所在的行号即可。% 原始标签是列向量每个元素是类别索引 target ind2vec(labels); % 输出是稀疏矩阵size 为类别数 × 样本数 % 训练完成后的预测过程vec2ind 取每列最大值所在行索引 predict_labels vec2ind(net(X_test_norm));用ind2vec编码后网络输出层的神经元个数由vec2ind反向决定——类别数是 2 就输出 2 个神经元类别数是 3 就输出 3 个。这个设计让网络在输出层天然形成「每个类别一个响应通道」的结构配合softmax或竞争传递函数分类边界比单输出加阈值要平滑得多。提示如果某个类别的样本数量特别少比如三分类中某一类只有 5 个样本ind2vec之后仍能正常训练但验证集的混淆矩阵里那一类基本全错。小样本分类问题更应该在评估时看总准确率以外的指标比如每类的召回率。3. 网络构建与训练feedforwardnet、patternnet 与 train 的参数细节3.1 feedforwardnet 还是 patternnet先看任务再选网络MATLAB 工具箱里创建前馈网络最常用的是feedforwardnet和patternnet。两者的底层结构几乎一样都是多层感知机MLP但默认配置面对的任务类型不同。feedforwardnet是通用的函数逼近器既可以做回归也可以做分类patternnet则在创建时就默认把输出层传递函数设置为softmax损失函数选交叉熵这是标准分类网络的配置。如果你只是完成蠓虫或鸢尾花分类直接patternnet(hiddenSizes)更省事。但如果想手动控制输出层的激活函数比如改成purelin做回归风格的输出再自己设阈值那就得用feedforwardnet然后手动改outputLayer的传递函数。手头这两份源程序里使用的是朴素的前馈网络结构这正好适合展示底层配置的过程——先把网络拆开看每一层长什么样再决定改哪个参数。% 隐藏层 10 个神经元输出层默认 purelin net feedforwardnet(10); % 查看各层设置 net.layers{1}.size % 隐藏层神经元数 net.layers{1}.transferFcn % 隐藏层激活函数默认 tansig net.layers{2}.transferFcn % 输出层激活函数默认 purelin % 改成分类任务标准配置 net.layers{2}.transferFcn softmax; net.trainFcn trainscg; % 或 trainlm后面会对比一层隐藏层的 MLP 理论上就能逼近任意连续函数但具体用多少个神经元没有解析解通常靠经验公式起手再根据验证集表现调整。常见的粗略估计是「特征维度 类别数」的二倍左右蠓虫两特征两分类可以试试 612 个鸢尾花四特征三分类从 815 个起步。神经元太少欠拟合分类边界过于粗糙太多则参数量膨胀几十个样本的小数据集很容易把噪声也学进去。3.2 train 的调用方式与训练集/验证集/测试集拆分train函数在 MATLAB 里身兼数职它会根据网络属性里的divideFcn自动拆分数据默认dividerand把数据随机分成 70% 训练、15% 验证、15% 测试。对小样本的蠓虫数据来说这种做法有风险——训练集总共可能只有二三十个样本再被拆走 15% 做验证实际训练数据更少模型的方差会很大。更可控的处理方式是手动划分训练集占 70%验证集 15%测试集 15%。但样本量实在太小时建议直接用留一法交叉验证。蠓虫数据集规模通常在 15 个样本左右留一法完全可行——每轮拿 1 个样本做验证轮流 15 次最后统计平均准确率。% 手动指定划分比例 net.divideFcn divideind; % 按索引划分 net.divideParam.trainInd train_idx; net.divideParam.valInd val_idx; net.divideParam.testInd test_idx; % 或者用随机划分但固定随机种子保证结果可复现 rng(42); net.divideFcn dividerand; net.divideParam.trainRatio 0.7; net.divideParam.valRatio 0.15; net.divideParam.testRatio 0.15;如果只想在完整训练集上拟合然后去测测试集可以用trainlm配合关闭验证集的早停机制。首先要明确一点——train函数默认开启 early stopping即验证集误差连续上升若干次就提前终止训练。这在数据充足时是防止过拟合的好机制但数据量太小的时候验证集本身波动就大误触发的概率很高。遇到训练一轮就停的情况应该优先怀疑验证集划分太激进而不是模型本身。3.3 训练函数选型trainlm 与 trainscg 的取舍工具箱默认的训练算法是trainlmLevenberg-Marquardt它利用二阶导数近似信息收敛快且精度高但需要存储和计算雅可比矩阵内存开销随参数数量平方级增长。蠓虫网络参数量权重 偏置大概在几十的量级trainlm没有任何压力一般几十轮迭代就能到很低的误差。trainscgScaled Conjugate Gradient属于共轭梯度类算法每次迭代只需要计算一阶梯度内存占用小在处理参数规模较大的网络或内存受限的环境下更有优势。它的收敛速度通常比trainlm慢但不容易在极小值附近震荡。对小数据集两类算法最终准确率差别不大差异更多体现在训练曲线和迭代次数上训练函数收敛速度内存占用适用场景trainlm快通常 1050 轮高需存储雅可比矩阵中小规模网络、函数逼近、分类trainscg较慢通常 30200 轮低只存梯度大规模参数、内存受限traingd慢且依赖学习率设置最低教学演示实际项目不推荐net.trainFcn trainlm; net.trainParam.epochs 500; % 最大迭代轮数 net.trainParam.goal 1e-5; % 目标误差 net.trainParam.min_grad 1e-7; % 梯度下限低于此值停止 net.trainParam.max_fail 6; % 验证集连续失败次数上限 [net, tr] train(net, X_norm, target);tr结构体里保存了整个训练过程的记录tr.epoch是每轮的迭代次数tr.perf是每轮训练集误差tr.vperf是验证集误差tr.tperf是测试误差。训练完先看tr.bestEpoch——如果模型在很靠前的轮次就因为验证集误差上升而停止说明要么网络容量过大要么数据划分不合理。4. 分类效果验证与可视化混淆矩阵、决策边界绘制4.1 sim 前向传播与预测结果的后处理训练完成后对新样本分类的官方接口是sim也可以用net(x)直接调用网络对象。两者本质相同都是执行前向传播。预测输出是一个类别数 × 测试样本数的矩阵每个元素代表网络认为该样本属于对应类别的响应强度。通过vec2ind取每列最大值的位置就得到了具体的类别编号。% 蠓虫测试集分类 load(testData.txt); % 假设文件已在当前目录 X_test testData(:, 1:2); X_test_norm mapminmax(apply, X_test, ps); Y_pred net(X_test_norm); % 输出是 2×N 的概率分布softmax 后 pred_labels vec2ind(Y_pred); % 得到 1 或 2 % 与真实标签对比 true_labels testData(:, 3); accuracy sum(pred_labels true_labels) / length(true_labels); fprintf(测试集准确率: %.2f%%\n, accuracy * 100);注意true_labels的方向要和pred_labels一致。testData用load读进来是「样本 × 特征」布局第三列是标签取出来是行向量vec2ind返回的也是行向量两者直接比较没问题。但如果之前对标签做过ind2vec转换真实标签也要记得还原成原始编号再比较否则形如稀疏矩阵的格式会直接比较出错。4.2 混淆矩阵比准确率多一层信息蠓虫测试样本可能只有 58 个准确率高低容易受单个样本影响。这时候混淆矩阵能给出更细的信息——模型把哪个类错分成了哪个类。MATLAB 工具箱自带confusionmat函数直接传入真实标签和预测标签即可C confusionmat(true_labels, pred_labels); disp(C); % 输出示例二分类 % 3 0 % 1 4这个矩阵的行是真实类别列是预测类别。对角线上的值是被正确分类的样本数非对角线就是错分样本。上面例子里C(2,1)1表示真实类别 2 的样本中有 1 个被误判成了类别 1。对鸢尾花三分类来说混淆矩阵能直观看出哪两类最容易互相混淆——通常是山鸢尾和杂色鸢尾因为它们的花瓣长宽分布区间有重叠区域。4.3 用 plot 画出蠓虫数据的分类边界蠓虫数据只有两个特征是少数能直接在二维平面上画出决策边界的分类问题。具体做法是在特征范围内生成网格点对每个网格点用训练好的网络做预测再用contourf填充不同分类区域的颜色。这张图放到实验报告里比单纯贴准确率有说服力得多。% 生成网格 [x1_grid, x2_grid] meshgrid(... linspace(min(X(1,:))-0.1, max(X(1,:))0.1, 100), ... linspace(min(X(2,:))-0.1, max(X(2,:))0.1, 100)); % 网格点展平并归一化 grid_points [x1_grid(:); x2_grid(:)]; grid_norm mapminmax(apply, grid_points, ps); % 预测网格点类别 grid_pred vec2ind(net(grid_norm)); grid_pred reshape(grid_pred, size(x1_grid)); % 绘制分类区域与原始样本点 figure; contourf(x1_grid, x2_grid, grid_pred, LineWidth, 0.5); hold on; gscatter(X(1,:), X(2,:), labels, rb, o*, 8); xlabel(触角长度); ylabel(翅膀长度); title(BP 神经网络对蠓虫数据的分类边界);contourf的填充颜色取决于grid_pred矩阵的数值默认配色下两个类的区域对比明显。样本点用gscatter叠加在图上能直观看到哪些点落在了错误区域附近——右下角越界的样本往往就是测试集里被误判的那一个。注意mapminmax的processFcns默认作用于 MATLAB 的行特征维度所以网格点的组织方式必须是「2 × N 」每个特征占一行。如果网格展平后grid_points方向搞反了后续预测几乎全错这类问题靠打印size(grid_points)就能排查。5. 隐藏层神经元数量对泛化性能的影响与调参实验5.1 从欠拟合到过拟合一组对比实验隐藏层神经元数量是 BP 网络最敏感的超参数之一。直接用鸢尾花训练集跑一组对照实验固定训练算法和迭代轮数只改隐藏层神经元数量观察训练集与测试集准确率的变化。样本量少时这种现象更极端——神经元从 2 个增加到 20 个训练集准确率可能从 60% 升到 100%但测试集准确率先升后降。hidden_sizes [2, 4, 8, 16, 32]; train_acc zeros(size(hidden_sizes)); test_acc zeros(size(hidden_sizes)); for i 1:length(hidden_sizes) net patternnet(hidden_sizes(i)); net.trainFcn trainscg; net.divideFcn dividerand; net.divideParam.trainRatio 0.7; net.divideParam.valRatio 0.15; net.divideParam.testRatio 0.15; rng(1); % 控制数据划分一致 [net, ~] train(net, X_norm, target); % 训练集表现 pred_train vec2ind(net(X_norm)); train_acc(i) sum(pred_train labels) / length(labels); % 测试集表现用同样的归一化参数 ps pred_test vec2ind(net(mapminmax(apply, X_test_norm_raw, ps))); test_acc(i) sum(pred_test test_labels) / length(test_labels); end % 绘制曲线 figure; plot(hidden_sizes, train_acc, -o, DisplayName, 训练集准确率); hold on; plot(hidden_sizes, test_acc, -s, DisplayName, 测试集准确率); xlabel(隐藏层神经元数量); ylabel(准确率); legend; grid on;典型的结果是神经元为 2 时两层都差欠拟合812 附近测试集准确率最高超过 20 之后训练集准确率接近满分但测试集明显回落——这就是过拟合的特征。另一个被忽略的事实是神经元数量增加让训练时间非线性增长32 个神经元的训练耗时可能是 8 个神经元的 5 倍以上而准确率并没有提升。5.2 一种更小的网络也能达到同样精度的可能性隐藏层神经元数量对收敛速度的影响也很明显。神经元越多需要拟合的权重参数就越多每次迭代的计算量随之增加。更关键的是优化面的局部极小点数量也随参数量增加。这导致神经元多的网络虽然表达能力更强但在小数据集上更容易收敛到一个「聪明但狭隘」的解——对训练集拟合得很好对未见数据则没有泛化能力。选择隐藏层规模的常见经验规则规则名称计算方式备注经验法则一(输入维度 输出维度) / 2最保守的起点经验法则二sqrt(输入维度 × 输出维度)介于两者之间经验法则三2 × 输入维度 1偏向充分拟合手动搜参[4, 6, 8, 10, 12]逐一试数据量小的时候推荐鸢尾花四输入三输出保守起点在 34 个神经元充分拟合需要 9 个左右。建议直接用[4, 6, 8, 10, 12]五个候选交叉验证取验证集准确率最高、且与训练集准确率差距最小的那个。追求更稳妥的话可以将输出层激活函数改为softmax配合crossentropy——它可以在不改动网络规模的前提下让收敛路径更平滑小样本场景下往往比默认的tansigpurelin MSE 组合更容易找到一个稳健的分类面。5.3 训练结果随机性的控制同样数据、同样网络结构、同样超参数两次训练出来的准确率可能有明显波动。原因至少有两处第一dividerand随机划分数据每次训练集和验证集的构成不同第二网络权重初始化是随机的rng默认状态不同导致不同的初始解梯度下降的起点不一样最终收敛位置也不一样。MATLAB 控制随机性的方式很直接rng(42); % 固定全局随机种子 net patternnet(8); net.initFcn initlay; % 按层初始化配合下面这句 net.layers{1}.initFcn initnw; % Nguyen-Widrow 初始化 net.initParam.seed 42; % 各层初始化种子 net init(net); % 手动触发初始化initnw是工具箱默认的层初始化方法它根据输入输出维度调整初始权重范围比纯随机初始化更有利于快速收敛。rng(42)固定全局种子后如果数据划分方式也一样两次训练通常能得到几乎一致的准确率。如果做了 K 折交叉验证每一折都要重新设种子否则各折的初始条件不同平均准确率的方差会偏大。6. 终极技巧把 train 函数玩成交叉验证与早停控制的组合拳6.1 手动实现留一法交叉验证蠓虫数据太小常规的 70/15/15 划分很不稳定。留一法每次只留一个样本做验证其余全部训练循环N次N 为样本数。这样每次训练集占绝大部分数据模型学到的信息最充分最终准确率是这 N 次结果的平均方差比单次划分小得多。N size(X, 2); correct 0; for i 1:N % 第 i 个样本做验证其余做训练 train_idx setdiff(1:N, i); val_idx i; net patternnet(6); net.trainFcn trainscg; net.divideFcn divideind; net.divideParam.trainInd train_idx; net.divideParam.valInd val_idx; net.divideParam.testInd []; % 留作空集 net.trainParam.epochs 200; net.trainParam.max_fail 20; % 放宽早停阈值 rng(10 i); % 每折使用不同随机种子 [net, ~] train(net, X_norm, target); % 对留出样本预测 y_pred vec2ind(net(X_norm(:, i))); if y_pred labels(i) correct correct 1; end end loocv_acc correct / N; fprintf(留一法交叉验证准确率: %.2f%%\n, loocv_acc * 100);这段代码的关键在第 7 行——train_idx和val_idx都是索引向量valInd里放的是留出的单个样本的索引。testInd设为空数组表示没有单独的测试集。每折训练结束后直接用net(X_norm(:, i))对这个唯一的验证样本做预测。max_fail放宽到 20 是因为每折只有 1 个验证样本误差曲线波动较大默认值 6 很容易提前终止。6.2 用训练记录tr判断早停是否误触发train返回的第二个输出tr里藏着完整的训练历史。出现「训练没到epochs就停了」的情况时第一步不是调大epochs而是看验证集误差在哪一轮开始上升[net, tr] train(net, X_norm, target); figure; plot(tr.epoch, tr.perf, -o, DisplayName, 训练集误差); hold on; plot(tr.epoch, tr.vperf, -s, DisplayName, 验证集误差); xlabel(迭代轮数); ylabel(均方误差); legend; grid on; fprintf(最佳迭代轮数: %d\n, tr.bestEpoch);tr.bestEpoch的值很能说明问题。如果最佳轮数在 5 以内且验证集误差从第 2 轮就开始上升说明网络容量过大或数据划分不均匀如果最佳轮数在 50 附近但验证集误差曲线震荡很厉害说明学习率偏大或trainscg的搜索步长不够稳定。工具箱里对应的调节参数是net.trainParam.sigma和net.trainParam.lambda分别控制共轭梯度方向的二阶近似步长和正则化强度一般不需要主动改但如果验证曲线高频震荡可以尝试把lambda调大一个数量级。6.3 用训练好的网络导出分类决策函数最后一步往往被忽略。在实验报告或答辩里只贴一张train的 GUI 截图是不够的——如果能把网络的前向传播写成独立的函数在 MATLAB 里直接调用而不再依赖train后的内存变量说服力会强得多。工具箱提供的genFunction可以生成一个独立的 MATLAB 函数文件里面固化了权重和偏置任何新数据进来都无需再加载训练环境genFunction(net, mosquito_classify.m); y mosquito_classify(X_test_norm); % 直接调用生成的函数 pred_labels vec2ind(y);genFunction生成的代码完全是自包含的内部硬编码了归一化参数和网络权重可以脱离原始训练脚本独立运行。演示时只需要带上这个.m文件和测试集整个「训练」痕迹都可以隐去直接展示从特征输入到类别输出的完整过程。如果需要部署到别的环境比如 C 语言嵌入genFunction还支持指定MatrixOnly选项输出纯矩阵计算代码方便移植。本文还有配套的精品资源点击获取
返回列表