☰
基于卷积神经网络的马铃薯叶片病害识别:VGG-16图像分类实践
2026/10/18 10:00:24 网站建设 项目流程

马铃薯这东西,看着皮实,其实一场晚疫病下来,整片地减产三成是常事。我最近在跑一个图像分类实验,就是把马铃薯叶片的照片丢给卷积神经网络,让模型自己判断是健康、早疫病还是晚疫病。模型选的是深度学习里一个非常经典的骨架——VGG-16。

这篇文章把整个实验过程拆开讲一遍:从数据集怎么准备、网络结构为什么长这样、训练时踩了哪些坑,到最后模型能跑到多少准确率。如果你是刚接触深度学习的同学,或者正准备做课程实验、毕业设计,可以参考这份完整流程。我会尽量把每一步的“为什么”也说清楚,而不是只扔给你一段能跑的代码。

1. 项目思路与整体设计

1.1 为什么选 VGG-16 而不是更“新”的网络

先说结论:在这个马铃薯病害识别的三分类任务上,VGG-16 是一个非常稳妥的选择,理由有三。

第一,VGG-16 结构足够规整。它就是“卷积层堆叠 + 池化 + 全连接”的经典套路,每一层干了什么事都能讲清楚。对做实验和写报告的人来说,这种可解释性非常重要。你要是换一个带残差连接、注意力机制的网络,跑出来的准确率可能更高,但分析问题时就得多花不少口舌。

第二,VGG-16 的迁移学习表现很好。ImageNet 预训练权重里已经包含了大量通用的底层特征,比如边缘、纹理、颜色渐变等。马铃薯叶片的病斑识别,本质上就是靠这些基础特征组合出来的高级语义,预训练模型有天然优势。

第三,数据量的问题。我手头整理出来的马铃薯叶片图像,每一类也就几百张,从零训练一个深层网络很容易过拟合。但用 VGG-16 加载预训练权重,只微调分类层和后面几层卷积,在中小规模数据集上就能拿到不错的效果。

实验最终是做一个三分类任务:健康叶片、早疫病叶片、晚疫病叶片。早疫病的典型症状是叶片上有带同心纹的褐色圆斑,晚疫病则表现为水渍状、暗褐色的大块斑,健康叶片颜色均匀、没有异常斑点——这三类在视觉上是有明显差异的,所以这个任务对模型来说并不算太难,关键是让模型学到“什么样的斑点组合算早疫病、什么样的算晚疫病”。

1.2 项目整体流程

整个实验的流程分六步:

  1. 数据集整理与划分
  2. 图像预处理与数据增强
  3. 搭建 VGG-16 模型(加载预训练权重)
  4. 训练与验证循环
  5. 测试集评估与混淆矩阵分析
  6. 模型导出与应用思路

这六步是图像分类任务的标准流水线。每一步单独看都不复杂,但串起来之后,任何一个环节出问题都会直接影响最终准确率。尤其是数据预处理和训练策略这两块,占了我整个实验百分之六十的时间。

2. 环境准备与数据集构建

2.1 实验环境与硬件配置

这个实验我用的是 PyTorch 框架,版本 2.x。为什么选 PyTorch?因为它的动态图和丰富的预训练模型库,做这种课程实验和学习性质的工程非常顺手,调试起来也比静态图框架直观。

硬件方面,我用的是一块 8GB 显存的显卡。VGG-16 的参数量大约 1.38 亿,训练时占用的显存并不小,8GB 显存跑 batch size 32、224x224 的输入刚好比较从容。如果显存不够,可以把 batch size 降到 16 或者使用混合精度训练。没有 GPU 的话,CPU 也能跑,但一个 epoch 可能要等很久,我建议至少租一个云端 GPU 实例,省下来的时间远比你付的那点费用值。

环境依赖大致如下:

  • Python 3.9+
  • PyTorch 2.x
  • torchvision
  • numpy、matplotlib、scikit-learn、tqdm

安装方式很简单,用 pip 或者 conda 都可以。重点提醒一下:torchvision 的版本要和 PyTorch 版本匹配,否则加载预训练权重的时候容易报错。

2.2 数据集结构划分

我从一个开源农业图像数据集中整理出了马铃薯叶片的图像,按类别放在三个文件夹里:

potato_dataset/ ├── train/ │ ├── healthy/ 540 张 │ ├── early_blight/ 460 张 │ └── late_blight/ 420 张 ├── val/ │ ├── healthy/ 120 张 │ ├── early_blight/ 100 张 │ └── late_blight/ 90 张 └── test/ ├── healthy/ 80 张 ├── early_blight/ 70 张 └── late_blight/ 60 张

这里有个细节很重要:划分数据集的时候一定要保证三个阶段的数据互不重叠,而且要按类别分层划分,不能直接用random_split乱切。否则容易出现训练集和验证集里出现同一张图的情况,最终的评估结果会虚高,而且你根本发现不了问题在哪。

每类图像的数量并不完全均衡,晚疫病样本偏少一些。类别不均衡会引导模型偏向样本多的类别,后面我通过加权损失函数解决了这个问题。

2.3 图像预处理:为什么要改尺寸和归一化

VGG-16 在全连接层之前需要固定尺寸的输入,标准做法是把图像缩放为 224x224。不管原始图片是 800x600 还是 1200x900,都要统一到这个尺寸。

归一化处理也是必不可少的一步。标准做法是使用 ImageNet 数据集的均值和标准差对每个通道做标准化:

mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225]

原因很简单:加载的预训练模型是在 ImageNet 上训练出来的,模型内部已经“适应”了这种分布规律的输入数据。如果你不归一化,图像像素值分布在 0-255 之间,和预训练分布差异太大,模型的初始表现会非常差,收敛速度也会慢很多。

数据增强方面,我用了这几种操作:

  • RandomResizedCrop:随机裁剪到 224x224,模拟不同尺度的拍摄
  • RandomHorizontalFlip:随机水平翻转,增加样本多样性
  • RandomRotation:随机旋转 15 度以内,模拟拍摄角度变化
  • ColorJitter:轻微调整亮度、对比度、饱和度,模拟不同光照环境

这些增强操作相当于在原始数据集基础上“无中生有”地造出了更多训练样本,对抑制过拟合非常有效。我用了一个很直观的对比:不做数据增强时验证集准确率大概 85%,做了之后能稳定在 95% 附近,差距就是这么明显。

3. VGG-16 网络结构拆解

3.1 参数配置总览

VGG-16 的名字来源于它有 16 个权重层,其中 13 个是卷积层,3 个是全连接层。整个网络的配置参数如下表所示:

模块层操作输出尺寸参数量
Block 1Conv3-64 x2 + MaxPool112x112x64约 38K
Block 2Conv3-128 x2 + MaxPool56x56x128约 221K
Block 3Conv3-256 x3 + MaxPool28x28x256约 1.47M
Block 4Conv3-512 x3 + MaxPool14x14x512约 5.90M
Block 5Conv3-512 x3 + MaxPool7x7x512约 7.07M
全连接FC-4096 + FC-4096 + FC-10001x1x1000约 102M

最后一个全连接层本来是输出 1000 类,对应 ImageNet 的分类任务,在迁移学习时我会把它替换成 3 类输出。

3.2 为什么用 3x3 卷积堆叠而不是更大的卷积核

VGG-16 最核心的设计思想,是用多个小卷积核堆叠来替代一个大卷积核。

两个 3x3 卷积叠加,感受野等于一个 5x5 卷积;三个 3x3 卷积叠加,感受野等于一个 7x7 卷积。但参数数量却少很多。假设输入通道都是 C,一个 5x5 卷积的参数是 25C²,而两个 3x3 卷积的参数是 2 x 9C² = 18C²,省了接近三成。三个 3x3 卷积对比一个 7x7 卷积,参数从 49C² 降到 27C²,省得更多。

更重要的是非线性表达能力。每多一层卷积,中间就多一次 ReLU 激活,网络的非线性特征就更强。简单说就是:同样的参数量,堆小卷积核能学到更复杂的特征。

3.3 网络各层在病害识别中扮演什么角色

从特征学习的角度看,VGG-16 的前几层学到的是基础视觉特征:叶片边缘、叶脉纹理、颜色渐变;中间层学到的是局部形状:圆形斑点、不规则病斑、边缘的锯齿状;后几层学到的是语义信息:这些形状组合在一起到底像早疫病的同心纹,还是晚疫病的大块水渍斑。

这一点在用 Grad-CAM 画热力图的时候看得特别清楚。早疫病样本的热力集中区域就是叶片上的斑点位置,而晚疫病样本的热力集中区域是叶片边缘的病斑扩散区。网络确实在关注我们人类认为“合理”的区域,而不是在靠背景或图像水印硬猜,这也说明模型学到的特征是可靠的。

4. 模型搭建与训练实现

4.1 加载预训练模型并替换分类头

这个环节直接决定了训练的上限,代码并不复杂:

import torch import torch.nn as nn import torch.nn.functional as F from torchvision import models # 加载 ImageNet 预训练权重 model = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1) # 查看最后的全连接层结构 # model.classifier[6] 是输出 1000 类的线性层 # 替换分类头为 3 类输出 num_features = model.classifier[6].in_features model.classifier[6] = nn.Linear(num_features, 3) # 冻结前面所有层,只训练分类头(可选) for param in model.features.parameters(): param.requires_grad = False

这里我尝试过两种训练策略:

第一种是只微调分类头。预训练权重的卷积特征已经很强了,如果数据量不大,只训练最后的线性层就能达到 88% 左右的准确率,而且训练非常快。

第二种是解冻部分卷积层一起微调。把最后一两个卷积块也放开训练,模型能针对马铃薯叶片的特征进一步调整底层表达,准确率能提高到 96% 以上。

我的做法是:先只训练分类头 10 个 epoch,让模型快速收敛,然后再解冻 Block 5 的卷积层,用更低的学习率再训练 20 个 epoch。这种渐进式微调比一开始就全量训练更稳定,不容易破坏预训练权重。

4.2 数据加载与训练循环

数据加载我用 PyTorch 的ImageFolder加DataLoader:

from torchvision import transforms, datasets from torch.utils.data import DataLoader train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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]) ]) train_dataset = datasets.ImageFolder('potato_dataset/train', transform=train_transform) val_dataset = datasets.ImageFolder('potato_dataset/val', transform=val_transform) test_dataset = datasets.ImageFolder('potato_dataset/test', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=4)

注意验证集和测试集不要做数据增强,只用 Resize 和 CenterCrop,保证评估结果的稳定性。

训练循环的灵魂是损失函数和优化器。损失函数用的是CrossEntropyLoss,处理多分类问题的标配。优化器我选了 SGD 加动量:

optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9, weight_decay=5e-4) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3)

为什么不用 Adam 而用 SGD?在迁移学习场景里,SGD 加合适的学习率往往能取得更好的泛化能力,尤其是数据集比较小的时候。Adam 收敛快,但在验证集上的表现有时反而不如细心调过学习率的 SGD。我自己实测的结果是 SGD 最终准确率比 Adam 高 1-2 个百分点,但需要多一点耐心调整学习率。

4.3 关键超参数的取值逻辑

超参数取值选择依据
输入尺寸224x224VGG-16 标准输入
Batch Size328GB 显存下的折中选择
初始学习率1e-4预训练模型微调常用值,过大容易破坏权重
权重衰减5e-4正则化,抑制过拟合
训练轮数30不算多,配合 ReduceLROnPlateau 足够

学习率是一个需要特别谨慎调的值。对预训练模型来说,1e-3 以上就偏大了,可能直接把预训练的损失“冲飞”;1e-5 又太保守,收敛速度慢得让人怀疑程序卡住了。1e-4 是一个比较稳妥的起点,后面用调度器自动调整。

5. 训练过程与调参实录

5.1 第一轮训练:过拟合现场

我的第一轮实验做得很粗糙:没有数据增强,所有层解冻训练,学习率设为 0.001。结果训练集准确率一路冲到 98%,但验证集准确率卡在 85% 左右上不去,而且训练 loss 和验证 loss 之间的差距越来越大。

这就是典型的过拟合。模型在训练集上表现“非常好”,但学到的是训练图像的特有噪声而不是普适特征。尤其是晚疫病样本量最少,模型对这类样本的泛化能力明显不足。

我分析了一下原因:数据量太少 + 模型容量过大。VGG-16 有 1 亿多参数,对几千张训练图像来说,完全有能力“背下”所有训练样本,却学不会真正的病斑规律。

解决办法有两个方向:一是增加数据量,二是降低模型容量或者增强正则化。在这个实验里,数据增强是性价比最高的方案。

5.2 加入数据增强和正则化后的变化

第二轮训练,我做了三件事:

  1. 启用数据增强(随机裁剪、翻转、旋转、颜色抖动)
  2. Dropout 概率从默认的 0.5 提高到 0.6
  3. 把学习率降到 1e-4,并且使用 ReduceLROnPlateau 动态调整

效果非常明显:验证集准确率从 85% 提升到了 94%,训练集和验证集的准确率差距也缩小到 4% 以内。虽然训练 loss 比之前高了一点,但这反而说明模型没有在“背题”了。

第三轮我又尝试解冻 Block 5,但把学习率降到 5e-5,只针对最后的卷积块做精调。这一轮下来,验证集准确率最后稳定在 96.7%。

5.3 训练日志与最终评估指标

训练过程中,我坚持每个 epoch 都打印完整的指标,而不是只盯着 loss。一段典型的训练日志长这样:

Epoch [8/30] Train Loss: 0.1523 Train Acc: 94.82% Val Loss: 0.1089 Val Acc: 95.33% LR: 0.000100

从第 10 个 epoch 开始,验证集准确率就在 95% 附近震荡,提升非常平缓。第 15 个 epoch 时学习率下调到 5e-5,准确率又往上跳了一点。到第 28 个 epoch,模型基本收敛,继续训练只会浪费时间,更适合做的是提前停止。

测试集上的最终结果:

类别精准率 Precision召回率 RecallF1-Score
健康0.9750.9500.962
早疫病0.9380.9430.940
晚疫病0.9120.9330.922

健康叶片最容易识别,晚疫病相对最难。从混淆矩阵来看,主要的错误集中在早疫病和晚疫病互相误判,这也符合病理学常识——两种病害在视觉特征上本来就有相似之处,早期症状更是难以区分。

6. 常见问题与排查技巧

6.1 典型问题速查表

问题表现可能原因解决方案
训练 loss 不下降学习率过大或过小尝试 1e-3 到 1e-5 的学习率区间
开训后 loss 为 NaN学习率太大,权重爆炸降低学习率,检查数据是否有损坏像素
训练集 100% 但验证集差过拟合加数据增强、Dropout,减模型容量
验证集准确率一直很低数据预处理不正确检查归一化参数、图像 resize 尺寸
显存溢出batch 太大或图像太大降低 batch size,使用混合精度训练
验证集准确率波动大验证集太小或未固定随机种子增大验证集,设置 torch.manual_seed
迁移学习准确率反而低训练时破坏了预训练权重冻结底层,只微调高层,降低学习率

6.2 几条独家经验

第一个坑:一定要先让模型过拟合一个 batch。用正常的学习率和超参数,把 batch size 设为 1 或者很小,训练几十步,如果模型 loss 没有下降、准确率没有上升,那大概率是代码逻辑有问题。别急着跑完整数据集,先跑一个 batch 验证整个流程通不通,能省下大量排查时间。

第二个坑:保存模型的时候,不要只在训练结束后存一次,而是每个 epoch 都在验证集上评估一次,保存验证集准确率最高的那个权重。我一开始保存的是最后一轮 epoch 的模型,训到后期验证集准确率反而掉了一点,导致测试结果不是最优的。后来改成记录 best model,准确率又提高了 1-2 个百分点。

第三个坑:固定随机种子。神经网络训练有随机性,数据加载顺序、权重初始化、数据增强都会影响结果。训练之前设置torch.manual_seed(42),必要时还设置random.seed(42)和numpy.random.seed(42),保证每次跑出来的结果尽量一致,这样实验之间的对比才有意义。

第四个坑:测试时用model.eval()并且配合torch.no_grad(),否则模型里的 Dropout 层和 BatchNorm 层会进入训练模式,推理结果会不稳定。这个问题看起来简单,但真踩到的人不少。

我个人做完这个实验的最大感受是:VGG-16 虽然已经不算什么新东西,但它像一本教科书一样,把卷积神经网络的基本套路展示得清清楚楚。从特征提取到分类决策,每一步都可以可视化、可解释。对想做实物的同学来说,这个项目还有很大的扩展空间:比如把模型导出成 ONNX 格式部署到手机端,或者把图像分类升级成目标检测,定位病斑的具体位置。后续我准备尝试一下更轻量的 MobileNet 系列,在保证精度的情况下把模型体积压下来,毕竟农业场景里,能在低端设备上跑起来的模型才是真正有用的模型。

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

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

立即咨询