☰
MATLAB CNN目标分类实战:从数据准备到训练评估全流程
2026/9/27 1:11:38 网站建设 项目流程

简介:这份MATLAB源码资源面向深度学习入门者与图像分类方向的在校学生、算法工程师,提供一套可直接运行的CNN目标分类训练与测试仿真方案。包内共3103个文件,以3101张jpg图像构成分类数据集,另含1个mat数据文件与1个m脚本文件,压缩包约2.44MB,体量轻便,便于快速上手。源码覆盖数据预处理、网络架构搭建、损失函数与优化器选择、模型训练、验证评估、超参数调整、训练过程可视化以及模型保存与加载等完整流程,并支持在测试集上评估泛化能力。已有135人学习关注。读者可借此理解卷积层、池化层与全连接层的协作机制,掌握MATLAB深度学习工具箱的建模方法,并参考脚本结构完成自己的分类任务实验,适合作为课程设计或入门练手的实践素材。

1. 从一份 MATLAB 源码说起:CNN 目标分类到底怎么跑通

手头只有一份标题写着「matlab-基于CNN卷积神经网络的目标分类训练和测试matlab仿真-源码」的工程,很多人第一反应是打开主脚本直接 F5,然后被一堆路径报错、维度不匹配、GPU 未识别劝退。我见过太多这样的情况:代码本身没大问题,卡住人的是环境、数据组织方式和训练参数这三件事。这篇笔记就围绕这份源码该有的结构,把 MATLAB 里用 CNN 做目标分类的完整链路拆开——从数据怎么摆、网络怎么搭、训练参数怎么设,到测试阶段怎么验证模型没训崩。适合刚拿到类似工程、想在自己机器上复现一遍的读者,也适合已经跑通但准确率上不去、想搞清楚每个参数在干什么的人。读完你应该能独立把一份 CNN 分类工程从零跑起来,并且知道哪几个地方最容易翻车。

2. 数据准备与网络搭建:MATLAB 里 CNN 分类的两块地基

2.1 目标分类的数据集该怎么组织成 MATLAB 认的格式

MATLAB 的 Deep Learning Toolbox 对图像分类任务有一套约定俗成的目录结构,不按这个来,后面imageDatastore读进去的标签就是乱的。常见做法是每个类别一个文件夹,文件夹名就是类别名,图片直接放在对应文件夹下:

dataset/ ├── cat/ │ ├── 001.jpg │ ├── 002.jpg │ └── ... ├── dog/ │ ├── 001.jpg │ └── ... └── bird/ └── ...

用imageDatastore读取时,它会自动根据文件夹名生成标签:

% 读取数据集,指定标签来源为文件夹名 imds = imageDatastore('dataset', ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); % 查看类别分布,确认没有某一类样本过少 countEachLabel(imds) % 按 7:3 划分训练集和测试集,保证每类比例一致 [imdsTrain, imdsTest] = splitEachLabel(imds, 0.7, 'randomized');

IncludeSubfolders必须为 true,否则只读顶层目录;LabelSource设为foldernames才会用文件夹名当标签。splitEachLabel的randomized参数很关键,不加的话是按文件名顺序切,如果数据本身有序,训练集和测试集分布会严重偏斜。划分完之后建议再跑一次countEachLabel(imdsTrain)和countEachLabel(imdsTest),确认两边每类数量大致成比例。

图片尺寸不一致是另一个高频问题。CNN 输入层要求固定尺寸,所以要么在读取时统一缩放,要么用增强数据存储做在线变换:

% 统一缩放到 224x224,适配常见骨干网络输入 inputSize = [224 224 3]; augimdsTrain = augmentedImageDatastore(inputSize, imdsTrain); augimdsTest = augmentedImageDatastore(inputSize, imdsTest);

augmentedImageDatastore除了缩放,还能顺带做数据增强。训练时加一点随机翻转和平移,对小数据集提升明显:

% 训练集加轻度增强,测试集只做缩放不做增强 augimdsTrain = augmentedImageDatastore(inputSize, imdsTrain, ... 'DataAugmentation', imageDataAugmenter( ... 'RandXReflection', true, ... 'RandRotation', [-10 10], ... 'RandXTranslation', [-10 10], ... 'RandYTranslation', [-10 10]));

注意测试集绝对不能加增强,否则评估结果没有意义。RandRotation范围别开太大,目标分类里旋转超过 15 度可能让类别语义都变了。

2.2 从零搭一个 CNN 还是拿预训练网络改

这是选型时第一个要回答的问题。数据量小于几千张、类别数不多的情况下,从零训一个 CNN 很容易过拟合,准确率卡在 60% 上下上不去。更稳的做法是拿预训练网络做迁移学习,MATLAB 里几行就能改:

% 加载预训练网络,这里以 squeezenet 为例,体积小适合快速验证 net = squeezenet; % 查看网络最后几层,确认要替换哪一层 lgraph = layerGraph(net); analyzeNetwork(net) % 替换最后的分类层,numClasses 换成自己的类别数 numClasses = numel(categories(imdsTrain.Labels)); newConvLayer = convolution2dLayer(1, numClasses, ... 'WeightLearnRateFactor', 10, ... 'BiasLearnRateFactor', 10, ... 'Name', 'new_conv'); newClassLayer = classificationLayer('Name', 'new_class'); lgraph = replaceLayer(lgraph, 'conv10', newConvLayer); lgraph = replaceLayer(lgraph, 'ClassificationLayer_predictions', newClassLayer);

WeightLearnRateFactor和BiasLearnRateFactor设成 10,意思是新加的层学习率是原来层的 10 倍,让它更快适应新任务。替换的层名必须和analyzeNetwork里看到的一致,不同预训练网络层名不一样,写错了会直接报找不到层。

如果坚持从零搭,一个够用的浅层 CNN 结构大致是这样:

layers = [ imageInputLayer([224 224 3], 'Name', 'input') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv3') batchNormalizationLayer('Name', 'bn3') reluLayer('Name', 'relu3') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu4') dropoutLayer(0.5, 'Name', 'dropout') fullyConnectedLayer(numClasses, 'Name', 'fc2') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ];

Padding设为same保证卷积后尺寸不变,池化层负责降维。batchNormalizationLayer放在卷积和激活之间是标准做法,能明显加快收敛。dropoutLayer的 0.5 是经验值,数据量大的时候可以降到 0.3。这个结构参数量不大,适合在 CPU 上先跑通流程,确认没问题再换预训练网络冲精度。

3. 训练参数怎么设:让 CNN 在 MATLAB 里真正收敛

3.1 trainingOptions 里那几个必须调的参数

网络搭好之后,训练能不能收敛、多久收敛,几乎全看trainingOptions怎么配。下面是一份我常用的配置:

options = trainingOptions('sgdm', ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.1, ... 'LearnRateDropPeriod', 10, ... 'MaxEpochs', 30, ... 'MiniBatchSize', 32, ... 'ValidationData', augimdsTest, ... 'ValidationFrequency', 30, ... 'Shuffle', 'every-epoch', ... 'Verbose', true, ... 'Plots', 'training-progress', ... 'ExecutionEnvironment', 'auto');

InitialLearnRate设 1e-3 是迁移学习的常用起点,从零训练可以降到 1e-4。LearnRateSchedule选piecewise,配合DropPeriod为 10,意思是每过 10 个 epoch 学习率乘 0.1,这是防止后期震荡的标准手段。MiniBatchSize受显存限制,32 是通用值,显存不够就降到 16 或 8,但太小会让 batch normalization 效果变差。

ValidationData直接传测试集的增强数据存储,ValidationFrequency设为 30 表示每 30 次迭代验证一次。Shuffle设为every-epoch很重要,每个 epoch 重新打乱能避免模型记住样本顺序。ExecutionEnvironment设auto会自动检测 GPU,没有 GPU 就退回 CPU,不会报错。

训练启动就一行:

trainedNet = trainNetwork(augimdsTrain, lgraph, options);

跑起来之后盯着training-progress窗口看两条曲线:训练准确率和验证准确率。如果训练准确率一直涨、验证准确率早早平了甚至往下掉,就是过拟合,该加 dropout 或者加数据增强。如果两条都上不去,先检查学习率是不是太大或太小。

3.2 训练过程中的三个关键观察点

第一个观察点是 loss 曲线有没有正常下降。正常情况是前几个 epoch 快速下降,然后逐渐平缓。如果 loss 从头到尾在震荡不降,八成是学习率太大,把InitialLearnRate除以 10 再试。如果 loss 降得极慢,可以适当调大,但别超过 1e-2。

第二个观察点是验证准确率什么时候到顶。到顶之后继续训就是浪费时间,MaxEpochs设 30 是保守值,实际可能 15 个 epoch 就到顶了。可以开'OutputFcn'做早停,验证准确率连续 5 次不提升就停:

options = trainingOptions('sgdm', ... 'InitialLearnRate', 1e-3, ... 'MaxEpochs', 50, ... 'ValidationData', augimdsTest, ... 'ValidationPatience', 5, ... 'OutputFcn', @(info) stopIfAccuracyNotImproving(info, 5));

ValidationPatience设为 5 就是连续 5 次验证不提升就停,比手动设 epoch 数靠谱。

第三个观察点是 GPU 利用率。如果ExecutionEnvironment设了gpu但训练速度跟 CPU 差不多,多半是MiniBatchSize太小,GPU 根本没吃饱。在显存允许范围内尽量把 batch 调大,32 起步,能上 64 就上 64。

提示:训练前先用少量数据跑 2 个 epoch 验证整个流程通不通,别一上来就全量跑,省得跑到一半发现标签错了。

4. 测试与评估:确认模型不是靠猜

4.1 用 classify 做批量预测并算混淆矩阵

训练完拿到trainedNet,测试阶段核心就两步:预测和评估。预测用classify:

% 对测试集做预测 [predLabels, scores] = classify(trainedNet, augimdsTest); % 取出真实标签 trueLabels = imdsTest.Labels; % 算准确率 accuracy = mean(predLabels == trueLabels); fprintf('测试集准确率: %.2f%%\n', accuracy * 100); % 画混淆矩阵,看具体哪类容易混 figure; confusionchart(trueLabels, predLabels); title('CNN 目标分类混淆矩阵');

classify返回两个东西:predLabels是预测类别,scores是每个类别的置信度分数。准确率只是一个大盘指标,真正有用的是混淆矩阵。如果发现猫和狗互相混得厉害,说明这两类特征太接近,要么加数据,要么在网络上做文章。

scores还能用来筛低置信度样本:

% 找出置信度低于 0.6 的样本,这些是模型拿不准的 maxScores = max(scores, [], 2); lowConfIdx = find(maxScores < 0.6); fprintf('低置信度样本数: %d\n', numel(lowConfIdx)); % 把这些样本单独拿出来看,往往是标注有问题或图像质量差 for i = 1:min(5, numel(lowConfIdx)) idx = lowConfIdx(i); img = readimage(imdsTest, idx); figure; imshow(img); title(sprintf('真实: %s, 预测: %s, 置信度: %.2f', ... string(trueLabels(idx)), string(predLabels(idx)), maxScores(idx))); end

这一步很多人跳过,但实际排查时特别有用。低置信度样本里经常混着标错的图,把它们挑出来重新标一遍,比调网络参数提升还大。

4.2 单张图片推理和模型保存

实际用的时候不可能每次都跑整个测试集,单张推理是必备能力:

% 读一张图,做和训练时一样的预处理 img = imread('test.jpg'); imgResized = imresize(img, [224 224]); % 单张预测 [label, score] = classify(trainedNet, imgResized); fprintf('预测类别: %s, 置信度: %.4f\n', string(label), max(score));

注意imresize的尺寸必须和训练时imageInputLayer的输入尺寸完全一致,差一个像素都会报维度错误。如果训练时用了augmentedImageDatastore做缩放,这里也要用同样的缩放方式,别一个用imresize一个用别的。

模型保存和加载:

% 保存训练好的网络 save('trained_cnn.mat', 'trainedNet'); % 下次用的时候直接加载 loadedNet = load('trained_cnn.mat'); trainedNet = loadedNet.trainedNet;

保存成.mat文件是最省事的方式,网络结构和权重都在里面。如果要在别的 MATLAB 版本用,注意版本兼容性,低版本可能读不了高版本存的网络。

5. 避坑与排查:那些让 CNN 训练翻车的细节

5.1 路径和标签相关的坑

现象:imageDatastore读进来之后countEachLabel显示只有一个类别,或者类别数是 0。

原因:文件夹层级不对,或者IncludeSubfolders没开。MATLAB 只认「一级子文件夹名 = 类别名」这个规则,如果图片放在dataset/cat/images/这种多一层目录下,它会把images当类别名。

解决:把图片直接放在类别文件夹下,不要多套一层。用imageDatastore('dataset', 'IncludeSubfolders', true, 'LabelSource', 'foldernames')确认读出来的标签正确。

5.2 维度不匹配的坑

现象:训练时报错Expected input image size to be [224 224 3], but received [256 256 3]。

原因:imageInputLayer里写的尺寸和实际喂进去的图片尺寸不一致。如果用了augmentedImageDatastore,它的outputSize必须和网络输入层一致。

解决:统一改成一个尺寸。要么改网络输入层,要么改augmentedImageDatastore的outputSize,两边对齐。改完记得重新analyzeNetwork确认。

5.3 显存不足的坑

现象:训练到一半报Out of memory on device,或者 MATLAB 直接卡死。

原因:MiniBatchSize太大,或者图片尺寸太大,显存吃不下。

解决:先把MiniBatchSize减半,还不行就减图片尺寸。224 是常见值,但 128 在很多任务上精度损失不大,显存占用能降不少。另外训练前clear掉不用的变量,MATLAB 的工作区变量也占内存。

5.4 准确率虚高的坑

现象:测试集准确率 99%,但拿新图片一测全错。

原因:训练集和测试集划分时没打乱,或者测试集图片和训练集有重复。splitEachLabel不加randomized就是按顺序切,如果数据本身按类别排好序,测试集可能全是某一类。

解决:划分时一定加'randomized'。划分完检查两边类别分布,用countEachLabel对比。另外确认测试集图片没有混进训练集,文件名重复的尤其要查。

5.5 训练不收敛的坑

现象:loss 一直不降,准确率在随机水平附近晃。

原因:学习率太大导致震荡,或者标签编码有问题,或者数据预处理没做归一化。

解决:先把学习率降到 1e-4 试。然后检查标签是不是从 1 开始编号,MATLAB 分类层要求标签是 categorical 类型,不能是纯数字。最后确认图片像素值有没有归一化到 [0,1],imread读进来是 0-255 的 uint8,直接喂进去梯度会炸。

6. 把训练脚本改造成可复用的实验框架

跑通一次不难,难的是每次换数据集、换网络结构都能快速重跑。我一般会把整个流程拆成三个函数:数据准备、网络构建、训练评估,主脚本只负责调参数。

function [imdsTrain, imdsTest, augimdsTrain, augimdsTest] = prepareData(dataDir, inputSize, trainRatio) % 统一的数据准备入口,换数据集只改 dataDir imds = imageDatastore(dataDir, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); [imdsTrain, imdsTest] = splitEachLabel(imds, trainRatio, 'randomized'); augimdsTrain = augmentedImageDatastore(inputSize, imdsTrain, ... 'DataAugmentation', imageDataAugmenter( ... 'RandXReflection', true, ... 'RandRotation', [-10 10])); augimdsTest = augmentedImageDatastore(inputSize, imdsTest); end

这个函数把数据相关的操作全包了,换数据集只改dataDir,换输入尺寸只改inputSize。训练比例也参数化,做交叉验证的时候直接循环改trainRatio就行。

网络构建也单独抽出来:

function lgraph = buildNetwork(numClasses, inputSize, usePretrained) if usePretrained net = squeezenet; lgraph = layerGraph(net); newConv = convolution2dLayer(1, numClasses, ... 'WeightLearnRateFactor', 10, ... 'BiasLearnRateFactor', 10, ... 'Name', 'new_conv'); newClass = classificationLayer('Name', 'new_class'); lgraph = replaceLayer(lgraph, 'conv10', newConv); lgraph = replaceLayer(lgraph, 'ClassificationLayer_predictions', newClass); else layers = [ imageInputLayer(inputSize, 'Name', 'input') convolution2dLayer(3, 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution2dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool2') fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; lgraph = layerGraph(layers); end end

usePretrained这个开关很实用,快速验证流程时用从零搭的小网络,正式跑精度时切预训练网络,一行参数的事。

主脚本就变得很干净:

% 主实验脚本,改参数就能跑不同配置 dataDir = 'dataset'; inputSize = [224 224 3]; trainRatio = 0.7; usePretrained = true; [~, imdsTest, augimdsTrain, augimdsTest] = prepareData(dataDir, inputSize, trainRatio); numClasses = numel(categories(imdsTest.Labels)); lgraph = buildNetwork(numClasses, inputSize, usePretrained); options = trainingOptions('sgdm', ... 'InitialLearnRate', 1e-3, ... 'MaxEpochs', 30, ... 'MiniBatchSize', 32, ... 'ValidationData', augimdsTest, ... 'ValidationPatience', 5, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'ExecutionEnvironment', 'auto'); trainedNet = trainNetwork(augimdsTrain, lgraph, options); [predLabels, ~] = classify(trainedNet, augimdsTest); accuracy = mean(predLabels == imdsTest.Labels); fprintf('最终测试准确率: %.2f%%\n', accuracy * 100);

这套框架我用了很久,换数据集、换网络、调参数都不用动核心逻辑。有个习惯值得养成:每次实验把trainRatio、InitialLearnRate、MiniBatchSize和最终准确率记到一个表格里,跑多了就能看出哪些参数组合有效。我自己的记录表里,迁移学习加InitialLearnRate为 1e-3、MiniBatchSize为 32 的组合在多数小数据集上都能到 85% 以上,而从零训练同样的数据往往只有 70% 出头。这个差距不是网络结构的问题,是预训练权重带来的先验知识在起作用。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询