简介:这是一份面向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.0~2.0 之间,翅膀长度在 1.0~2.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 理论上就能逼近任意连续函数,但具体用多少个神经元没有解析解,通常靠经验公式起手,再根据验证集表现调整。常见的粗略估计是「特征维度 + 类别数」的二倍左右,蠓虫两特征两分类可以试试 6~12 个,鸢尾花四特征三分类从 8~15 个起步。神经元太少欠拟合,分类边界过于粗糙;太多则参数量膨胀,几十个样本的小数据集很容易把噪声也学进去。
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 的取舍
工具箱默认的训练算法是trainlm(Levenberg-Marquardt),它利用二阶导数近似信息,收敛快且精度高,但需要存储和计算雅可比矩阵,内存开销随参数数量平方级增长。蠓虫网络参数量(权重 + 偏置)大概在几十的量级,trainlm没有任何压力,一般几十轮迭代就能到很低的误差。
trainscg(Scaled Conjugate Gradient)属于共轭梯度类算法,每次迭代只需要计算一阶梯度,内存占用小,在处理参数规模较大的网络或内存受限的环境下更有优势。它的收敛速度通常比trainlm慢,但不容易在极小值附近震荡。对小数据集,两类算法最终准确率差别不大,差异更多体现在训练曲线和迭代次数上:
| 训练函数 | 收敛速度 | 内存占用 | 适用场景 |
|---|---|---|---|
trainlm | 快,通常 10~50 轮 | 高,需存储雅可比矩阵 | 中小规模网络、函数逼近、分类 |
trainscg | 较慢,通常 30~200 轮 | 低,只存梯度 | 大规模参数、内存受限 |
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 混淆矩阵:比准确率多一层信息
蠓虫测试样本可能只有 5~8 个,准确率高低容易受单个样本影响。这时候混淆矩阵能给出更细的信息——模型把哪个类错分成了哪个类。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 时两层都差,欠拟合;8~12 附近测试集准确率最高;超过 20 之后训练集准确率接近满分,但测试集明显回落——这就是过拟合的特征。另一个被忽略的事实是,神经元数量增加让训练时间非线性增长,32 个神经元的训练耗时可能是 8 个神经元的 5 倍以上,而准确率并没有提升。
5.2 一种更小的网络也能达到同样精度的可能性
隐藏层神经元数量对收敛速度的影响也很明显。神经元越多,需要拟合的权重参数就越多,每次迭代的计算量随之增加。更关键的是,优化面的局部极小点数量也随参数量增加。这导致神经元多的网络虽然表达能力更强,但在小数据集上更容易收敛到一个「聪明但狭隘」的解——对训练集拟合得很好,对未见数据则没有泛化能力。
选择隐藏层规模的常见经验规则:
| 规则名称 | 计算方式 | 备注 |
|---|---|---|
| 经验法则一 | (输入维度 + 输出维度) / 2 | 最保守的起点 |
| 经验法则二 | sqrt(输入维度 × 输出维度) | 介于两者之间 |
| 经验法则三 | 2 × 输入维度 + 1 | 偏向充分拟合 |
| 手动搜参 | [4, 6, 8, 10, 12]逐一试 | 数据量小的时候推荐 |
鸢尾花四输入三输出,保守起点在 3~4 个神经元,充分拟合需要 9 个左右。建议直接用[4, 6, 8, 10, 12]五个候选交叉验证,取验证集准确率最高、且与训练集准确率差距最小的那个。追求更稳妥的话,可以将输出层激活函数改为softmax,配合crossentropy——它可以在不改动网络规模的前提下让收敛路径更平滑,小样本场景下往往比默认的tansig+purelin+ 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'选项输出纯矩阵计算代码,方便移植。
本文还有配套的精品资源,点击获取