资讯动态

VGG-16迁移学习实战:马铃薯叶片病害图像分类全流程解析

发布时间:2026/10/18 11:48:43 来源:尧图企业网站定制
前段时间我拿经典分类网络VGG-16做了一次完整的马铃薯叶片病害识别实验目标是给一张叶片照片判断它属于健康、早疫病还是晚疫病。这个实验的初衷很简单我想找一个“结构足够经典、调参不会太难、任务本身有真实应用价值”的图像分类项目而不是继续在 toy dataset 上跑循环。VGG-16 虽然已经是 2014 年的网络但它的规整结构和预训练权重让它成为理解卷积网络行为、复现迁移学习流程的最佳样本之一。整套实验做下来验证集准确率在二分类场景下能稳定到 90% 以上多分类也基本能到 85% 左右。这篇文章我会按完整链路来写数据怎么收集和清洗、目录怎么组织、VGG-16 的每一段卷积在叶片图像上到底在做什么、为什么要用 ImageNet 预训练权重、训练过程中我踩过的那些坑又是怎么逐个排查掉的。无论你是刚做完深度学习课设、想找一个能复现的项目练手还是正在做农业图像分类方向的工作这篇记录应该都能帮你少走几步弯路。1. 数据底子先打好叶片图片怎么收、怎么清洗、怎么划分1.1 图像来源与人工清洗做植物病害识别数据是最容易被低估的一环。很多人拿到一个现成数据集就开始训练结果模型在测试集上表现很好一拿到自己拍的照片就崩大概率是数据本身有问题图片缩略图重复、部分照片压根不是叶片、病斑标注错误、甚至背景占了整张图大半面积。我这次实验的数据来自两个渠道一部分是公开的植物病理图像集另一部分是模拟实验环境里自行拍摄的补充样本。公开数据的好处是类别齐全、数量够用坏处是质量参差不齐。我花了大约两天时间做清洗主要做三件事去掉分辨率过低的图片比如小于 200×200 的这类图放大到 224×224 后信息损失严重。剔除标注明显错误的图片。比如某张“健康”叶片上有一大块病斑或者“早疫病”图片里其实拍的是虫害痕迹。过滤背景占比异常的照片。如果一张图片里叶片只占中心很小一块区域模型学的很可能不是病斑而是环境背景。清洗完成后每类图片保留 800 张左右总数在 2400 张上下。这个规模对 VGG-16 来说并不大所以后面迁移学习和数据增强是必须的而不是可选优化项。1.2 按子目录组织数据让加载器自动生成标签目录结构我强烈建议直接用分类器默认约定的方式即train/早疫病/xxx.jpg这种按类别分文件夹的形式。这样做的好处在于借助 PyTorch 的torchvision.datasets.ImageFolder它会自动读取每个子目录的名称作为类别标签并按字母顺序生成索引。你不用手写 CSV、不用做标签映射数据组织本身就成了标签的一部分。我的目录划分如下data/ train/ healthy/ early_blight/ late_blight/ val/ healthy/ early_blight/ late_blight/ test/ healthy/ early_blight/ late_blight/训练集、验证集、测试集按 6:2:2 的比例划分。有一点容易被忽略划分前要先对图片做全局随机打散再按比例分配不然某个目录下如果恰好按拍摄时间排序前 60% 可能是同一个生长阶段的叶片会造成验证集和训练集分布不一致。1.3 类别比例与样本数量底线马铃薯叶片常见病害里晚疫病和早疫病的病斑形态差异相对明显晚疫病病斑多为水渍状、边缘模糊早疫病病斑有明显的同心轮纹健康叶片表面干净。可是在图像分类任务中光照、拍摄角度、叶龄都会让类内差异远大于类间差异。这也是为什么样本数量不能太少。我自己试过把每类压到 300 张同样的训练轮次下准确率掉了七八个点尤其是早疫病因为它的轮纹特征在小样本下很容易被当成叶脉纹理。如果数据量确实不够可以考虑按病害类别做垂直翻转、小角度旋转来扩充但有一个原则测试集和验证集的清洗严格程度要高于训练集宁可少也不能混入错误样本否则评估分数就是“脏数据上的自嗨”。2. VGG-16 结构拆解13 层卷积究竟在叶片图像上学到了什么2.1 从边缘到病斑纹理VGG 卷积层的层级特征VGG-16 的结构用一句话概括就是反复堆叠 3×3 卷积和 2×2 最大池化。它没有残差连接、没有注意力机制结构朴素到近乎“无脑”。但正是这种无脑堆叠让它成了理解卷积神经网络特征提取过程的教学范本。输入图片是 224×224×3 的 RGB 叶片图。第一个卷积块的 64 个卷积核本质上是在学习边缘和颜色斑块叶片的轮廓、叶脉的走向、背景和叶面的颜色差异。到了中间层卷积核的感受野变大开始组合出纹理信息早疫病的轮纹边界、晚疫病病斑的浸润感都是在第三、第四个卷积块里逐渐成形的。最后的卷积特征图则更抽象基本是“哪个区域更像病斑”的激活响应。有两个细节值得展开说。第一个是 3×3 卷积核的意义。两个 3×3 卷积串联等价于一个 5×5 卷积的感受野但参数量只有后者的 18/25且中间多了一层非线性激活表达能力更强。第二是最大池化在叶片识别中的实际作用它把空间尺寸减半相当于对每个局部区域保留最强的激活值这对病斑这种“局部高响应”的特征特别友好。2.2 5 个卷积块和 3 个全连接层的参数账本VGG-16 的参数量约 1.38 亿听起来很吓人但其中绝大部分集中在前两个全连接层。下表是每部分的参数规模估算模块结构说明输出尺寸大致参数量卷积块 12×3×3 卷积64 通道224×224×64约 3.8 万卷积块 22×3×3 卷积128 通道112×112×128约 22 万卷积块 33×3×3 卷积256 通道56×56×256约 147 万卷积块 43×3×3 卷积512 通道28×28×512约 590 万卷积块 53×3×3 卷积512 通道14×14×512约 590 万全连接层4096-4096-10001×1000约 1.02 亿看到全连接层那一亿多参数就能理解一个关键问题为什么在数据集只有几千张叶片图时从零训练 VGG-16 几乎必然过拟合。因为最后一层要学习从 25088 维特征向量到 1000 个类别的映射这个映射的容量太大了它会把训练集里的噪声细节也记住。对策就是接下来要讲的替换分类头 迁移学习。2.3 结构规整背后的实验优势选 VGG-16 而不是更先进的 ResNet 或 EfficientNet并不是因为它在病害识别上精度更高。相反在同等数据量下ResNet 通常更容易训出好结果。我坚持用 VGG-16 的原因有三个结构完全规整每个卷积块内部都是相同的 3×3 卷积改动分类头、冻结特征层时逻辑非常清晰适合做代码演示。训练行为更“透明”过拟合现象出现得比 ResNet 更明显反而方便观察迁移学习逐步生效的过程。预训练权重好找torchvision 一行就能加载而且和其他经典网络共用同一套数据增强和标准化参数减少实验变量。如果你的目标是“拿高分”而不是“看清原理”那可以直接换 ResNet如果你想真正理解卷积网络的层次行为和微调机制VGG-16 依然是最好的实验载体。3. 训练集和验证集必须区别对待数据增强与加载器配置3.1 训练集增强与验证集标准化的差别这是新手最容易踩的一个坑把数据增强同时作用在训练集和验证集上。逻辑上听起来像“增加了多样性”实际上会让验证集分数失去意义。验证集的作用是模拟真实场景下的分布它应该只做最小必要变换比如缩放、中心裁剪、标准化。我的 transform 配置是这样的from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])训练集里的 RandomResizedCrop 会随机裁剪叶片局部模拟不同拍摄距离RandomRotation 模拟叶片摆放角度不固定ColorJitter 模拟早晚光照差异。这些变换幅度都不算激进但对病害识别非常有效——因为病斑特征不会因为翻转或旋转而改变而颜色抖动可以削弱模型对拍摄环境的依赖。3.2 Normalize 的均值方差为什么必须是 ImageNet 那套这里要解释一个关键点[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]是 ImageNet 数据集的 RGB 均值方差。如果直接用随机初始化的 VGG-16你完全可以用自己数据集的均值方差但一旦加载 ImageNet 预训练权重就必须沿用这套数值。原因很简单预训练模型的特征提取器是在“标准化到这套均方差”的图像上完成学习的如果喂进去的数据分布不一致前几层卷积的激活值就会偏差很大微调效果会大打折扣。很多人在这一步翻过车加载了预训练权重却用自己的均值方差做了标准化结果 loss 初始值奇高训练半天不收敛。排查半天发现是标准化不一致。3.3 batch size 与 worker 数量的取舍batch size 我最终选了 32这在 VGG-16 8GB 显存的环境下比较稳。batch size 过小比如 8 或 16训练集噪声会放大loss 曲线抖动明显过大比如 128在 VGG-16 这种参数量下普通显卡容易显存溢出而且大 batch 对泛化不一定有帮助。如果是课程实验32 是体验和稳定性的平衡点。num_workers方面Windows 下我通常设 2~4Linux 下可以设 4~8。想确认数据加载没有成为瓶颈可以看一眼训练时 GPU 利用率如果经常掉到 50% 以下多半是图片解码拖了后腿这时加大 worker 数比换网络更有效。4. 预训练权重到底帮了多少忙冻结、微调与分类头替换4.1 为什么必须用 ImageNet 预训练权重前面算过 VGG-16 的参数量再想想我们只有 2400 张叶片图结论非常明确从零训练大概率只能得到一个在训练集上 100% 准确、验证集上徘徊在 60% 的过拟合模型。预训练权重的价值在于它已经学会了一年半载才能学到的通用视觉特征——边缘、纹理、形状。这些特征对马铃薯叶片完全适用因为叶片的病斑纹理本质上也是边缘和纹理的组合。迁移学习在计算机视觉里可以类比成“先学素描基本功再学画特定类型的树叶”。ImageNet 预训练权重就是那套素描基本功而我们的病害识别只需要在它的基础上再学一点叶片领域的特殊纹理。4.2 替换分类头并冻结特征层torchvision 里加载 VGG-16 的方式很简单import torchvision.models as models import torch.nn as nn model models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) num_classes 3 model.classifier[6] nn.Linear(4096, num_classes)model.classifier是一个Sequential容器索引 6 恰好是最后的 1000 分类层。替换成 3 分类后特征层的权重保留分类层重新初始化。这一步之后第一次训练时我建议把特征层全部冻结for param in model.features.parameters(): param.requires_grad False for param in model.classifier.parameters(): param.requires_grad True冻结特征层之后参与训练的参数量从 1.38 亿降到只有约 1 亿全连接层里的最后两层约 4.2 万——不过这里要注意classifier里的前三层如果不是最后一层仍然保持原有权重。由于全连接层的输入来自冻结的卷积特征前几轮训练实际是在“让新分类头学会读旧特征”这个过程通常跑 10 个 epoch 左右就能稳定到 80% 以上的验证准确率。4.3 解冻部分层做微调的经验参数等新分类头收敛后下一步是解冻特征层的部分模块做微调。我个人的习惯是只解冻最后两个卷积块for name, param in model.features.named_parameters(): if name.startswith(28) or name.startswith(31): param.requires_grad True else: param.requires_grad False这里28和31对应第五个卷积块内部的卷积层索引。训练时要用更小的学习率一般分类头用 1e-3特征层用 1e-4 或直接给特征层乘 0.1。优化器我用的是带动量的 SGDmomentum0.9weight_decay5e-4。对 VGG-16 这种大网络权重衰减能有效抑制微调阶段的过拟合。为什么不能一开始就解冻所有层因为新分类头还没学会与旧特征配合此时让特征层也跟着动优化的方向会被带偏。先固定不动等分类头稳定再逐步放开特征层两者配合会顺滑很多。5. 训练曲线的信号loss 不降、过拟合、类别失衡的排查记录5.1 训练主循环与模型保存策略训练主循环没什么神秘的但有几个细节值得注意。第一个是验证集评估时一定要切到model.eval()模式同时包在torch.no_grad()里否则 BatchNorm 的行为会随 batch 变化而且会多算一遍梯度。第二个是模型保存策略不要每轮都存我通常只在验证集准确率突破历史最高值时保存一次命名带上 epoch 和分数比如vgg16_epoch12_acc0.91.pth。这样即使后面训练意外中断手上也有一个最优版本。如果是多分类 CrossEntropyLoss初始 loss 在 1.1 左右是正常的因为从 logits 到 3 类的平均交叉熵大约是 log(3)。如果你的初始 loss 在 7 或 8大概率不是模型问题而是数据标签或者标准化环节出了问题。5.2 loss 不降和 NAN 的排除顺序我在这套实验里遇到过一次 loss 震荡不下降的情况。当时的排查顺序如下先确认数据增强没有引入异常样本。比如 ColorJitter 的 saturation 参数设到 0.5 以上叶片颜色可能变成无法辨识的形状。再看学习率是不是过大。带 SGD 的 VGG 在微调阶段学习率超过 1e-3loss 很容易在某个区间来回抖动。然后检查标签是否错位。ImageFolder 的标签是按目录字母序排列的如果我把early_blight、healthy、late_blight三个目录名改过索引顺序会变而可视化脚本里如果硬编码了旧顺序就会张冠李戴。NAN 的情况我只遇到过一种。我在某些 resized crop 后出现了全黑图片也就是所有像素都是 0 的图片。标准化后这些像素变成 -0.485/std不会导致 NAN但当颜色抖动把饱和度和亮度调得过低时极端情况可能让某些通道出现 inf。解决办法是给 ColorJitter 的参数设置合理范围并且加载图片时做一次像素范围检查。5.3 过拟合信号出现后的处理手段VGG-16 在 2400 张训练图片上过拟合几乎是一定的区别只是快慢。我观察到的典型信号是训练集准确率在第三个 epoch 冲到 95%验证集却停在 82% 上下。这说明模型的容量在“背”训练集而不是“学”病害规律。处理顺序我建议这样来先加强数据增强把 RandomRotation 从 10 度加到 20 度ColorJitter 的亮度范围加大到 0.3。这一步通常能带来 2~3 个点的验证集提升。再给 classifier 里加 Dropout。VGG 原始结构里全连接层之间已经有 Dropout(0.5)但最后一层替换后没重新加我是手动在model.classifier中补一个 Dropout(0.3)。最后考虑降低学习率让微调过程更谨慎。从 1e-3 降到 3e-4配合更长的训练轮数。如果这三个手段都试过验证集还是停滞那瓶颈多半不在模型而在数据。比如某个病害类别的图像形态本身太接近人工清洗时应该把边界样本重新归类。5.4 某类样本召回率偏低的处理在这个实验里早期出现过“健康”和“早疫病”的召回率较高但“晚疫病”召回率只有 70% 左右的情况。原因是晚疫病初期病斑不明显很多图片上的病斑面积很小模型把它学成了“健康”。我采取的措施有两步。第一步是统计每个类别的训练样本数量晚疫病稍微少一点就先做轻微过采样——不是直接重复复制图片而是对晚疫病样本额外做一次更强的增强比如多几次随机裁剪和旋转。第二步是给损失函数加类别权重class_weights torch.tensor([1.0, 1.0, 1.3]).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)加权损失的逻辑很简单模型预测错晚疫病时惩罚更大梯度更新时就会更重视这个类别。但要注意权重加得太大比如 2.0会让模型过度偏向晚疫病结果晚疫病召回率上去了健康类的误报率又爆炸。1.2 到 1.4 之间是比较稳的区间。6. 准确率之外还看什么混淆矩阵、注意力热力图与部署思路6.1 各指标的实际含义很多实验报告写到准确率就停了。但在类别不平衡或者病斑形态接近时准确率是有欺骗性的。如果三类比例是 80% 健康、10% 早疫病、10% 晚疫病那模型即使把后两类全猜错准确率也有 80%。所以我在评估时固定输出四组指标每个类别的精确率、召回率、F1以及总体混淆矩阵。计算方式用 sklearn 的classification_report和confusion_matrix非常方便只需要把模型在测试集上的预测结果和真实标签收集起来即可。对每个类别的 F1 值我会格外关注因为 F1 是精确率和召回率的调和平均能在两个方向都出问题时给一个明确的信号。6.2 混淆矩阵里最值得关注的两个格子三分类混淆矩阵是 3×3其中对角线是正确预测非对角线是混淆。我最关心的四个非对角元素里健康被误判成早疫病、早疫病被误判成晚疫病这两个尤其值得写进实验分析里。健康被误判成早疫病大概率是因为健康叶片上有虫咬痕迹或生理性斑点形态与早疫病的早期病斑相似。这属于真实业务里很难消除的噪声只能通过增加这两种类别的困难样本训练量来缓解。早疫病被误判成晚疫病则更多是因为拍摄距离过远病斑细节在 224×224 的输入下已经丢失模型看到的都是模糊的暗色斑块。这类误判可以通过提高输入分辨率来改善但显存开销也会同步增大。6.3 用特征热力图看模型到底在看哪里训练结束后我还用 Grad-CAM 生成了一批注意力热力图目的是验证模型是不是真的在看病斑而不是碰巧用了其他线索。做法很直接取最后一个卷积块输出的特征图按类别得分反向传播梯度然后把梯度加权后的特征图叠加到原图上生成热力图。这一步很有价值。一开始我发现热力图的亮点集中在叶片边缘和背景区域说明模型在偷懒它可能把“叶片形状是否完整”当成了分类依据而不是看病斑。后来我在训练集里刻意加入了一些带遮挡的叶片图片并加强了随机裁剪热力图才逐渐转移到病斑区域。这个现象提醒我们图像分类模型默认会走捷径数据增强不只是提高泛化也是在“逼”模型找到真正有区分度的特征。6.4 部署与后续扩展实验做完之后如果想要落地最简单的路径是把模型导出成 TorchScript 或者 ONNX。TorchScript 在 PyTorch 环境内集成最快model.eval() scripted_model torch.jit.script(model.cpu()) scripted_model.save(vgg16_leaf_disease.pt)导出后可以放到移动端或者服务端做单张预测。不过说实话VGG-16 推理速度偏慢参数也大实际部署时我更建议把它当成一个 baseline用它验证“这个任务有没有可能用深度学习方法解决”。如果验证阶段就能到 90% 以上后续再替换成 Mobilenet 或 EfficientNet-Lite 做效率优化方向上是可靠的。再往后扩展的思路我不太建议直接增加病害类别到几十类那样数据采集成本会失控。更合理的路线是先保留这个 VGG-16 模型作为粗筛把健康叶片和疑似病害叶片分开然后对疑似样本做区域级检测定位病斑位置。这也是从“图像分类”到“目标检测”的一个自然过渡两者在特征提取上很多组件是可以共用的。最后分享一点个人体会。这套实验给我最深的教训不是网络选型而是数据的决定性作用。我在清洗数据和设计增强上花的时间大约是调参时间的三倍但换来的是模型的稳定性和可解释性。如果你也想复现这个项目我建议先把时间多分配在观察图片本身——每一类的典型病斑形态、容易混淆的边界样本、光照和背景的多样性。这些投入看起来不像训练那么“硬核”但对最终效果的影响往往比换一个更大的网络更直接。VGG-16 只是一个顺手的工具真正教会我东西的是那些叶片照片里隐藏的噪声和规律。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑