ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于MATLAB与VGG19的果树病虫害识别迁移学习实践

基于MATLAB与VGG19的果树病虫害识别迁移学习实践 简介MATLAB深度学习入门实例《果树病虫害识别VGG19版》是一份面向初学者的完整工程化学习包围绕“数据准备—网络搭建—模型训练—测试评估”全流程展开并以多种果树病虫害识别为实战落脚点方便读者把理论落到代码与图像上。资源压缩包共包含935个文件以928张JPG病虫害图像为数据主体另有.m训练脚本、.mlx实时脚本、.mat网络与特征文件、.xlsx标签清单及说明txt整体503.61MB目录结构清晰可对照学习。该资源已有24399人浏览学习是同类MATLAB深度学习实例中关注度较高的一款。使用这套资料读者不仅能复现VGG19迁移学习识别果树病虫害的完整步骤还能学会利用MATLAB深度学习工具箱处理图像数据、调整网络参数、评估模型效果并可将方法迁移至其他农作物病害检测场景。1. 果树病虫害识别里的VGG19先解决两个入门弯路很多刚接触深度学习的 MATLAB 用户第一反应是从零搭一个卷积网络然后在自己那几百张图片上训练最后得到一条不下降的 Loss 曲线于是把问题归咎于 MATLAB 深度学习「不友好」。实际上用 MATLAB 做果树病虫害识别这类图像分类任务最稳的第一步是迁移学习拿预训练的 VGG19 当特征提取器只替换最后的全连接层和分类层进行微调。这套流程在一个中等规模的数据集上从准备数据到跑完训练通常只要十几分钟验证准确率能到 90% 以上而从头训练一个 VGG19 量级的网络需要几十个小时和成倍的样本这不是入门阶段该做的事。这篇实例围绕「病害图像分类」这个具体场景把 VGG19 在 MATLAB 里的加载、改造、训练和验证过程拆开讲适合有 MATLAB 基础但第一次走完整深度学习流程的人。VGG19 不是越深越好它是你借来的眼睛。2. 数据从哪来病虫害图像集的组织方式与 MATLAB 预处理2.1 用 imageDatastore 把文件夹变成可训练的数据集MATLAB 不认识一张照片是不是「苹果黑腐病」它只认识路径。所以数据组织的核心是把病害类型直接写进文件夹名。常见做法是在一个根目录下建 4 到 5 个子文件夹每个文件夹名对应一种病害比如 Apple_Scab、Apple_Black_Rot、Apple_Cedar_Rust目录里放对应图片。文件夹名就是标签这个名字定了后面所有流程都跟着走。自己拍摄的照片要注意统一去除背景、避免叶片之间互相遮挡公开数据集则尽量选背景干净或已经切好的版本因为 VGG19 的前几层卷积对背景纹理非常敏感。% 读取根目录下所有子文件夹的图片 imds imageDatastore(fruit_pests, ... IncludeSubfolders, true, ... LabelSource, foldernames); % 查看每种类别的图片数量 tbl countEachLabel(imds); disp(tbl);这段代码里IncludeSubfolders必须设为 true否则子文件夹里的图片一张都不会被读进来这是新手最容易漏的参数。LabelSource设为foldernamesMATLAB 会把每个子文件夹名自动转成 categorical 标签你不用自己写一个标签文本文件。countEachLabel返回一个表tbl.Label是类别列表tbl.Count是每类图片数量。注意观察样本分布如果某一类只有十几张而其他类有几百张后续要么对少样本类别做数据增强要么在trainingOptions里考虑类别权重否则训练出的模型几乎不会预测这个少数类。2.2 用 splitEachLabel 切分训练集、验证集与测试集切分数据集是监督学习里不能跳的一步。很多入门代码把全部数据丢进trainNetwork然后用训练集本身的准确率来评判模型这不叫评估叫背答案。验证集用于训练过程中挑选模型测试集只在最终训练完成后用一次用来模拟模型面对没见过的照片时的真实表现。% 先按 70% / 30% 切分训练集和剩余数据 [imdsTrain, imdsRemain] splitEachLabel(imds, 0.7, randomized); % 再把剩余数据对半拆成验证集和测试集各占 15% [imdsVal, imdsTest] splitEachLabel(imdsRemain, 0.5, randomized);这里有一个值得注意的行为splitEachLabel是逐类别按比例切分的不是从全部数据里随机抽 70%。类别不均衡时每一类都会按相同比例各切一刀样本少的类别不会被切没这比你自己用randperm写随机索引要稳妥。第二次调用splitEachLabel是对imdsRemain再切一半最终比例是 70% / 15% / 15%。如果要严格的 70% / 20% / 10%第二刀应该按三分之一和三分之二切或者直接把第一次的 30% 改成 0.3、第二次按 0.667 处理。验证集和测试集的划分比例不是越壕越好。总图片数在 1000 张以内时测试集切 15% 就已经是 150 张足够看混淆矩阵的分布继续切大反而挤压训练数据。图片总量少于 300 张时建议改用 K 折交叉验证的思路评估而不是一次切掉太多数据。2.3 把图片归一化到 224×224 与 AugmentedImageDatastore 的选择VGG19 的输入层写死了 224×224×3你拍的果树病害照片几乎不可能正好是这个尺寸。直接丢给trainNetwork会报输入尺寸不匹配。常见的处理方法是让augmentedImageDatastore在读取时自动缩放图片它更像一个「带预处理的数据管道」而不是一个把图片一次性载入内存的数据加载器。% 训练集带数据增强旋转和轻微缩放 augTrain augmentedImageDatastore([224 224], imdsTrain, ... DataAugmentation, imageDataAugmenter( ... RandRotation, [-20 20], ... RandXScale, [0.8 1.2], ... RandYScale, [0.8 1.2])); % 验证集只做尺寸归一化不做随机增强 augVal augmentedImageDatastore([224 224], imdsVal); augTest augmentedImageDatastore([224 224], imdsTest);[224 224]是目标尺寸不写第三维 3因为augmentedImageDatastore默认保留通道数。训练和验证的数据增强配置必须分开验证集如果也做随机旋转那每次验证得到的准确率都会漂同一个模型不同时刻评出两个分数没法判断训练是否真的在进步。增强的强度也要注意旋转角度超过 20 度对果实和叶片图像意义不大过度的仿射变换反而把病斑形态扭曲成噪声。数据量低于每类 50 张时增强是刚需每类超过 500 张时增强对准确率的提升明显减弱可以考虑只保留随机翻转。注意如果有人告诉你「把增强开满」别全信。增强的本质是告诉模型「这些变换不影响病害类别」但旋转 45 度后叶片正反面都会变病斑颜色特征也跟着乱过度增强只会拖慢收敛。保守起步看验证集反馈再调。3. VGG19 在 MATLAB 里的加载、改造与训练3.1 用 analyzeNetwork 先拆 VGG19 的参数结构加载 VGG19 只需要一行命令但它背后不是单纯的代码调用而是依赖 Deep Learning Toolbox 的 VGG19 支持包首次执行时会联网下载预训练权重。网络不通时net vgg19;会一直卡住这不是死机是下载被阻塞。net vgg19; analyzeNetwork(net);执行analyzeNetwork后弹出的窗口会显示完整的网络结构图。VGG19 的核心结构可以概括为下表理解这张表比背层名更重要结构段卷积层数量输出特征图尺寸参数量级作用第 1 段卷积2 层224×224×64约 3.8 万提取边缘与颜色块第 2 段卷积2 层112×112×128约 22 万组合局部纹理第 3 段卷积4 层56×56×256约 120 万提取病斑、叶脉等中粒度特征第 4 段卷积4 层28×28×512约 470 万提取形态与空间结构第 5 段卷积4 层14×14×512约 470 万病害语义特征全连接段3 层4096、4096、1000约 1.08 亿把特征映射到分类空间注意参数量分布卷积部分加起来约 2000 万三层全连接却占了超过 1 亿参数。全连接层是 VGG19 冗肿的主要原因也是迁移学习时最优先替换的部分因为 ImageNet 的 1000 类与你需要识别的 4 类果树病害在语义空间上根本不是一回事。前五段卷积在 ImageNet 上学到的边缘、纹理、形状特征对病虫害识别同样有效所以保留它们只重训最后的三层全连接是迁移学习的核心逻辑。analyzeNetwork窗口里每一层都标注了层名和可学习参数数量这个信息后面replaceLayer要用。VGG19 的最后一层全连接层在多数版本中名为fc1000最后的分类层名为ClassificationLayer_predictions但版本之间存在差异判断标准以analyzeNetwork实际显示为准。3.2 用 layerGraph 替换全连接层与分类层拿到预训练模型后不能直接拿去训练自己的数据因为它的输出是 1000 个类别而你的数据只有 4 类。常见做法是用layerGraph把网络最尾部的全连接层和分类层替换成自己尺寸的新层。lgraph layerGraph(net); % 动态获取类别数不要写死 numClasses numel(categories(imdsTrain.Labels)); % 新建全连接层和分类层 newFC fullyConnectedLayer(numClasses, Name, fc_fruit); newClass classificationLayer(Name, classification_fruit); % 按层名替换 lgraph replaceLayer(lgraph, fc1000, newFC); lgraph replaceLayer(lgraph, ClassificationLayer_predictions, newClass);这里必须解释清楚replaceLayer的行为第二个参数是被替换层的名字第三个参数是新层。如果你写错了名字MATLAB 会报找不到指定层这时候回到analyzeNetwork的窗口确认层名即可。替换完成后整个网络里只有fc_fruit这一层的权重是随机初始化的其余所有卷积层参数仍然是 ImageNet 预训练值。新手最容易犯的错误是replaceLayer选错层把倒数第二层全连接给替换掉。VGG19 有三层全连接前两层尺寸 4096第三层 1000。替换第三层是合理的前面两层保留可以继续承担特征降维如果数据量少也可以把两层都换成尺寸更小的 512 或 256 维全连接层减少参数量、降低过拟合风险。3.3 trainingOptions 里的关键参数与判断依据改完网络结构下一步是配置训练过程。trainingOptions是trainNetwork的训练配置入口参数一多就有人乱调其实最关键的就五个。options trainingOptions(sgdm, ... MiniBatchSize, 16, ... MaxEpochs, 10, ... InitialLearnRate, 1e-4, ... Shuffle, every-epoch, ... ValidationData, augVal, ... ValidationFrequency, 20, ... Plots, training-progress, ... Verbose, false, ... OutputNetwork, best-validation);优化器为什么选sgdm而不是adam迁移学习微调场景下sgdm的动量方向更稳定不容易把预训练特征冲得太远adam自带自适应学习率在从头训练时不挑参数但在微调阶段初始步长稍大就会把预训练权重改崩。InitialLearnRate设置在1e-4到1e-3这个区间内比较安全高于1e-3会让 Loss 曲线直接起飞低于1e-5则基本学不动。如果你的模型验证准确率始终在 50% 以下徘徊第一件事就是把学习率降一个数量级而不是换网络。MiniBatchSize受显存限制。VGG19 在 224×224 输入下单张图片的前向和反向传播显存开销远高于轻量网络16 是一个入门稳定的数值如果你使用 8GB 显存的 GPU可以尝试 32但要注意监控显存占用。ValidationFrequency是每隔多少迭代计算一次验证准确率它与MaxEpochs有关一个 epoch 的迭代次数约等于训练图片总数除以MiniBatchSize把验证频率设在「每个 epoch 末尾附近」比较合理设得太密训练过程会频繁停顿设得太疏则可能错过最佳模型。OutputNetwork是关键的一个选项它设为best-validation时MATLAB 会保存验证准确率最高那一次迭代的网络权重而不是最后一次迭代的权重。很多入门代码没设这个参数结果训练后期过拟合了保存下来的反而是更差的模型。3.4 训练过程中的三个观察点训练窗口弹出后别光盯着准确率那个数看Plots窗口里有几条曲线它们透露出训练的真正状态。第一Loss 曲线的形状。一轮 epoch 训练 Loss 应该是一个阶梯式下降的过程每个 epoch 开始时因为Shuffle打乱了数据顺序Loss 会有一次小幅度回升然后再降。如果 Loss 整体不降只在某个值附近抖动先检查学习率是不是设大了。第二训练准确率和验证准确率的差值。如果训练集准确率已经到 95%验证集只有 82%两者差距超过 10 个百分点说明模型开始过拟合常见解决办法是减小InitialLearnRate、增加数据增强强度或者把新加的全连接层尺寸改小。第三验证 Loss 曲线。如果验证 Loss 在下降一段时间后开始回升训练 Loss 还在降这就是经典的过拟合分界点OutputNetwork选best-validation的意义就在于此。训练结束后MATLAB 会返回一个SeriesNetwork对象它可以直接作为下一步预测推理的输入不需要额外导出权重文件netFruit trainNetwork(augTrain, lgraph, options);提示训练中途想停可以随时点训练窗口中的停止按钮或者按 CtrlC已完成的迭代不会丢失最终返回的模型是最后一次迭代的状态。不过别频繁这样做OutputNetwork会在验证间隔点自动保存最佳模型正常情况下让它自己跑完即可。4. 跑通一次完整训练最小脚本与三个高频报错4.1 把数据读取、模型改造和训练串成一份最小脚本把前两章的代码连起来就得到一份可以直接稍加修改变成项目骨架的脚本。实际项目里这个脚本的职责划分是可以优化的通常生成训练脚本、预测脚本和评估脚本三个文件方便分阶段排查问题而不是把所有代码堆在一个文件里。%% 数据准备 imds imageDatastore(fruit_pests, ... IncludeSubfolders, true, ... LabelSource, foldernames); [imdsTrain, imdsRemain] splitEachLabel(imds, 0.7, randomized); [imdsVal, imdsTest] splitEachLabel(imdsRemain, 0.5, randomized); augTrain augmentedImageDatastore([224 224], imdsTrain, ... DataAugmentation, imageDataAugmenter( ... RandRotation, [-20 20], RandXScale, [0.8 1.2])); augVal augmentedImageDatastore([224 224], imdsVal); %% 模型改造 net vgg19; lgraph layerGraph(net); numClasses numel(categories(imdsTrain.Labels)); lgraph replaceLayer(lgraph, fc1000, ... fullyConnectedLayer(numClasses, Name, fc_fruit)); lgraph replaceLayer(lgraph, ClassificationLayer_predictions, ... classificationLayer(Name, classification_fruit)); %% 训练 options trainingOptions(sgdm, ... MiniBatchSize, 16, MaxEpochs, 10, ... InitialLearnRate, 1e-4, Shuffle, every-epoch, ... ValidationData, augVal, ValidationFrequency, 20, ... OutputNetwork, best-validation); netFruit trainNetwork(augTrain, lgraph, options); %% 保存 save(fruit_vgg19.mat, netFruit);这份脚本在设计上有一个值得学习的细节numClasses是从imdsTrain.Labels动态取出来的不是手写的数字。这样以后数据集里加了一个新的病害类别只需要往文件夹里放新图片脚本不需要改任何代码。如果你想把验证频率和每个 epoch 的迭代次数对齐可以先算一步iterPerEpoch ceil(numel(imdsTrain.Files) / options.MiniBatchSize)然后把ValidationFrequency设置成iterPerEpoch的整数倍。4.2 报错一输入图像尺寸不是 224×224×3报错信息里出现类似Expected input to be of size 224x224x3时第一反应不应该是怀疑augmentedImageDatastore没生效而要检查数据源本身的通道数。augmentedImageDatastore会缩放空间尺寸但如果你混入了灰度图它的第三维是 1不是 3就会触发这个报错。常见的来源是数据集里混有单通道历史扫描图或某些传感器拍摄的红外图。% 检查前 10 张图的尺寸和通道数 for i 1:10 im readimage(imds, i); fprintf(%d: %s\n, i, mat2str(size(im))); end如果确认是灰度图混入稳妥的做法是统一转换成三通道。repmat(im, [1 1 3])可以把灰度图复制成三通道因为灰度图的三通道值完全相同不会引入视觉上不存在的信息。这个操作最好在构建imageDatastore之前完成也就是提前处理原始文件而不是在训练脚本里临时转换。4.3 报错二显存或内存不足VGG19 是出了名的「重」。即使只训练最后的全连接层前向传播过程中所有卷积层的特征图都要保留在内存里显存不足是训练中最常见的崩溃点。报错一般发生在训练开始后几个迭代处因为那时计算图已完整建立。第一策略是调小MiniBatchSize。从 16 降到 8显存占用几乎减半收敛速度会慢一些但至少能跑。第二策略是在数据准备阶段就把图片统一存成 224×224不要在每次迭代时对原图做一次缩放这样既省内存又省时间。% 预缩放全部图片到 224×224并覆盖保存到新的目录 outputDir fruit_pests_resized; for i 1:numel(imds.Files) im readimage(imds, i); im imresize(im, [224 224]); [~, name, ext] fileparts(imds.Files{i}); imwrite(im, fullfile(outputDir, [name ext])); end这段代码刻意不用并行池因为入门阶段开并行的开销可能比省下来的时间还大。数据量超过几万张时再考虑用parfor。另外检查一下系统里是不是还有别的程序占着显存尤其是浏览器开了一堆视频标签页的情况经常比模型自己吃掉的显存还多。4.4 报错三类别数与标签不匹配报错可能是Number of classes in the last fully connected layer must match the number of classes in the response variable或者是验证集标签出现训练集没有的类别。问题几乎都出在numClasses被写死成固定数字或者数据集切分时类别分布被破坏。如果numClasses是手动填的 5而数据里只有 4 类MATLAB 会在训练开始前就报错。统一用numel(categories(imdsTrain.Labels))生成类别数是第一道保险。如果类别数没问题但验证集报标签不匹配检查一下imdsVal的标签来源是否和imdsTrain一致——切片之后两个imageDatastore的Labels向量应当来自同一个分类体系如果出现过类别文件夹改名导致 categorical 顺序不一致用以下命令统一imdsVal.Labels categorical(imdsVal.Labels, categories(imdsTrain.Labels));这条命令的语义是用训练集的类别顺序重新编码验证集标签。categorical 的类别顺序是字母序两个数据集的类别集合如果完全一致顺序也会一致不需要手动处理只有当两个数据集类别集合有差异时才需要手动对齐。5. 验证与进阶混淆矩阵、gradCAM 热图与 fc7 特征加 SVM5.1 用 confusionchart 看哪两类病害最容易互相混淆验证准确率只是一个数字它无法告诉你模型把「苹果黑腐病」错认成了「苹果霉斑病」还是把「健康叶片」错认成了「早疫病」。用混淆矩阵替代准确率来评估你会发现不同病害的错误模式完全不同。[pred, score] classify(netFruit, augTest); figure; cm confusionchart(imdsTest.Labels, pred); cm.RowSummary row-normalized; cm.ColumnSummary column-normalized;classify返回两个输出第一个是每张测试图的预测标签第二个是每个类别的置信度分数尺寸是「测试图片数 × 类别数」。confusionchart的RowSummary设为row-normalized后每行会按该行真实样本总数归一化显示百分比适合看「真实类别被误判到哪去了」。如果某两类之间的混淆率超过 15%说明你的数据增强策略还不够或者这两类病害在视觉特征上本来就接近需要补充更多边界样本。5.2 用 gradCAM 看模型关注的是不是病斑区域准确率和混淆矩阵都只能告诉你「结果错没错」不能告诉你「模型在看哪里」。病虫害识别场景里模型极容易走捷径——它可能靠识别图像角落的水印、EXIF 信息残留或背景土壤颜色来判断病害而不是真正识别病斑。gradCAM函数可以为单张图片生成注意力热图这是验证模型有没有「作弊」的最直接手段。gradCAM 是 Deep Learning Toolbox R2021a 引入的函数旧版本没有需要更新工具箱或手动实现热图计算。img imread(imdsTest.Files{1}); img imresize(img, [224 224]); [~, cam] gradCAM(netFruit, img, imdsTest.Labels(1)); figure; subplot(1, 2, 1); imshow(img); subplot(1, 2, 2); imagesc(cam);对每一类病害随机抽 20 张图做热图叠加观察热力集中区域如果热图高亮区域稳定落在叶片病斑处说明模型学到了有意义的形态纹理特征如果热图总在叶片边缘或背景区域说明训练数据存在混淆因素需要清理数据背景。这一步不用做太多张每类 20 张足够暴露明显的偷懒行为也可以顺便筛出需要人工复核的难例用于后续补充标注。5.3 小样本场景的另一条迁移路径fc7 特征加 SVM当每类样本只有几十张时端到端微调 VGG19 还是容易过拟合。这个量级下更稳的常见做法是把 VGG19 完全冻结只把它当特征提取器对每个样本取出fc7层的 4096 维激活向量然后用fitcecoc训练一个多分类 SVM。这套方案的逻辑是绕开反向传播让 SVM 在固定特征空间里做分类可控性和可解释性都更好。% 提取训练集的 fc7 层特征 featTrain activations(net, augTrain, fc7, OutputAs, rows); featTest activations(net, augTest, fc7, OutputAs, rows); % 在特征向量上训练多分类 SVM svmModel fitcecoc(featTrain, imdsTrain.Labels); % 预测 predSVM predict(svmModel, featTest); accSVM mean(predSVM imdsTest.Labels);OutputAs, rows让返回的特征矩阵变成「样本数 × 通道数」的二维矩阵这是 SVM 要求的输入格式。activation的计算比训练快得多几百张图几分钟就能跑完。SVM 需要调的参数比 CNN 少得多主要是一个正则项系数默认值在大部分病害识别数据集上表现稳定。把特征提取和分类器分开等于把深度学习问题降回传统机器学习问题后续无论换用 PCA 降维、集成树还是在线更新分类器都不用再动 VGG19 那几十层权重这也是我在小样本病虫害项目里最后经常会切回的一条兜底路线。本文还有配套的精品资源点击获取
返回列表