做毕业设计那会儿,我选了“深度学习基于CNN卷积网络的蔬菜识别”这个方向。一句话讲,就是拿一张蔬菜照片,交给一个卷积神经网络模型,让它判断这是黄瓜、西红柿还是茄子。任务听起来简单,但实际做下来,从数据收集、模型设计到训练调参,每一步都有坑。这篇文章就把我整个项目的完整过程写出来,包括技术选型的原因、数据怎么处理、网络怎么搭、参数怎么调,以及我在实操中踩过的坑和验证过有效的技巧,希望能给正在做类似图像分类项目的同学一些参考。
1. 毕业设计定位与技术选型:为什么用CNN做蔬菜识别
在动手写代码之前,我两周时间都用在了“想清楚”上。很多同学一上来就打开框架文档开始搭模型,结果做到一半发现数据不对、评估指标不对、硬件跑不动,回头再来改,很浪费产设阶段的时间。我的做法是先把毕业设计题目拆成几个功能模块:输入是什么、输出是什么、中间需要哪些环节、在什么环境上跑、用什么指标验收。
1.1 核心需求拆解
我的毕设题目对应的是典型的图像分类任务:给定一张蔬菜图片,模型输出每个类别的概率,取最大概率对应的标签作为预测结果。需求表面上一句话,背后却牵扯出至少五个环节。
- 数据环节:需要采集或者获取足够的蔬菜图片,并且覆盖目标类别、不同背景、不同光照条件,然后做清洗和划分。
- 预处理环节:图片尺寸不一致、像素值范围不统一、类别样本数不均衡,这些都要在送入模型前解决。
- 模型环节:选择合适的卷积网络结构,是从零搭建一个CNN,还是用预训练模型做迁移学习,需要先定下来。
- 训练环节:选择优化器、损失函数、学习率、正则化手段,并设计实验对比方案。
- 评估环节:用准确率、F1分数、混淆矩阵等指标衡量模型效果,待全部调优完成后再在测试集上做最终验证。
我在立项时把自己的验收目标写得很具体:识别10个常见蔬菜类别,测试集准确率不低于90%,训练和推理流程能完整跑通,并且能输出可视化的预测效果。这个目标不算激进,但对于本科毕设来说已经足够体现工作量,也给自己留了调参余量。
1.2 为什么传统图像处理方案被CNN取代
我在确定方案前,专门去查了蔬菜识别相关的资料,也尝试过用传统方法做一个小实验。传统思路通常是三步:先提取图片的颜色直方图、纹理特征、形状描述子,然后用这些手工特征训练分类器,比如支持向量机或者随机森林。这个方法在背景简单、类别差异明显的图片上能跑出还不错的结果,但一旦环境变复杂,问题就来了。
蔬菜本身的颜色和形态变异非常大。西红柿有红色、黄色、绿色,甚至同一颗西红柿上会同时存在不同颜色区域;辣椒有细长型、灯笼型,颜色从绿色到红色跨越很大;土豆表面可能带泥土,不同品种的形状差异也很大。光照条件变化时,颜色直方图会发生剧烈漂移,手工特征的鲁棒性远远不够。
CNN的核心优势是“端到端学习”。它不需要人为设计特征,而是通过多层卷积操作自动从像素级数据里逐层抽象出特征:浅层学到边缘、纹理,中层学到部件结构,高层学到类别相关的语义特征。这种特征提取方式天然对平移、光照、局部形变有更强的鲁棒性。我当时做了个简单对比,用颜色直方图加SVM在我的十类蔬菜数据集上只能跑到80%左右的准确率,而一个结构简单的CNN轻轻松松就能超过90%。这也是我最终选择CNN的根本原因。
2. 数据集构建与预处理:决定模型上限的“地基”
我刚拿到题目时,第一反应是去找公开的蔬菜图像数据集。但真正开始用的时候发现,网上流传的很多数据集质量参差不齐,有的类别样本数量严重不均衡,有的图片分辨率乱七八糟。如果直接拿来训练,模型效果一定不会好。所以我在数据上花的时间比模型还多。
2.1 数据来源与类别设计
我最后采用的是“公开数据+自采补充”的组合方式。先整理了一批开源蔬菜分类图片,再自己用手机补拍了一部分类别,最终确定为10个常见类别:青椒、黄瓜、茄子、胡萝卜、西红柿、土豆、洋葱、南瓜、白萝卜、大蒜。选类别的原则是“日常可见、形态上有区分度但有相似性”。太简单的类别,比如土豆和洋葱,一眼就能区分,模型随便练都能过;太难的类别,比如不同品种的生菜,很容易让模型无法收敛,不适合作为毕设范围。这10个类别里,胡萝卜和白萝卜、土豆和洋葱之间存在一定的视觉相似度,既保证了任务有意义,又不至于难到无法完成。
每类图片我最终保留了不少于800张,总共约9000张。为什么定800张这个数量?因为对于小网络来说,加上数据增强,800张每类基本能让模型学到稳定的类内特征;少于500张,模型很容易过拟合。如果每类只有两三百张,就强烈建议用预训练迁移学习,而不是从零训练。
数据清洗这一步不能省。我先把所有图片统一转成JPG格式,去掉那些模糊到人眼都看不出来的照片,删掉严重遮挡的图片,还有明显重复的样本。有人觉得数据清洗浪费时间,实际上不清理的话,这些噪声样本对模型的负面影响非常大,一张过曝的图片可能会让验证集准确率掉一个点。
2.2 图像增强与样本均衡策略
原始图片不能直接喂给模型,因为尺寸不一致、像素值范围不统一。我统一将图片处理后输入尺寸设为224×224,这是目前图像分类领域最常用的输入尺寸,理由是大多数预训练模型默认支持这个尺寸,方便后续做迁移学习对比。
训练时我使用了在线图像增强,而不是把增强后的图片提前存盘。在线增强的意思是,每轮训练时对同一个样本随机做不同的变换,等效于每轮都看到“新”的样本,能显著扩大数据多样性。我常用的增强组合是:
- 随机水平翻转(概率0.5);
- 随机旋转,角度在±15度之间;
- 随机调整亮度、对比度和饱和度,幅度范围控制在0.8~1.2;
- 随机裁剪缩放,先随机裁剪一块区域再缩放到224×224;
- 以0.05的概率加入小额高斯噪声。
需要注意,增强强度不能过大。为了提高泛化能力,把颜色饱和度调得太过,会让模型学到错误的颜色分布;把旋转角度设得太大,蔬菜的方向特征可能被破坏。我实测下来±15度旋转和0.8~1.2的亮度变化是相对安全的范围。
样本均衡我也专门处理过。统计后发现原始的公开数据中,黄瓜和西红柿数量偏多,白萝卜和大蒜偏少,最多类和最少类接近3倍差距。如果不处理,模型会偏向预测数量多的类别,导致少数类召回率很低。我采用的方案是WeightedRandomSampler,按类别数量的倒数给每个样本赋予采样权重,让每个epoch里各个类别的出现频率基本一致。这个方案比简单复制少数类图片更高效,也避免过拟合。
2.3 数据集划分方式
划分上我坚持“训练集、验证集、测试集”三分离,比例是60%、20%、20%。训练集用于更新模型参数,验证集用于调超参数和模型选择,测试集只在最终评估时用一次,绝不用它来做调参。很多新手最容易犯的错误,就是把验证集和测试集混着用,反复跑测试集挑最好的结果,最后得到的准确率虚高,没有说服力。
划分时必须做分层抽样,保证每个类别在三个集合中的比例与总体一致。如果用不均衡的随机划分,很可能出现某个类的样本全部落在训练集,测试集里这个类的样本数极少,评估结果就不可靠。在PyTorch里可以通过Dataset的索引列表自己实现分层划分,也可以用sklearn的train_test_split并传入stratify标签数组。划分完成后,目录结构差不多是这样的:
data/ ├── train/ │ ├── carrot/ │ ├── cucumber/ │ └── ... ├── val/ └── test/3. 模型结构设计与关键参数:从零搭建CNN到迁移学习
模型结构是整个项目的核心,但也是最容易“抄过来就用”的部分。我的建议是:先自己搭一个轻量级CNN把流程跑通,再用预训练模型做对比实验。这样做的好处是,你能真正理解每一层在干什么,而不是只会调用现成接口。
3.1 从零搭建一个轻量级CNN基线模型
我设计的基线网络结构是这样的:
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x输入3×224×224的图片,经过三次卷积下采样,空间尺寸每次减半:224 → 112 → 56 → 28。通道数从3增长到32、64、128。最后把二维特征图展开成一维向量,送入全连接层输出10个类别的logits。
这里有几个设计细节值得解释。第一,卷积核都用3×3,没有用5×5或7×7。因为多层3×3卷积堆叠可以等效获得更大的感受野,但参数量更少,非线性更强。第二,每个卷积层后面都接BatchNorm。这个层的作用是对当前batch的特征做归一化,让网络对初始化不敏感,训练更稳定,也能放宽对学习率的要求。第三,池化采用MaxPool2d,它是取区域内最大值,能保留较强的边缘特征,同时提供平移不变性。
全连接层的Dropout设成0.5,这是比较经典的经验值。Dropout的作用是训练时随机“丢弃”一部分神经元,迫使网络不过度依赖某个单一特征,从而降低过拟合。注意Dropout只应在训练阶段用,推理时要关掉,PyTorch在model.eval()模式下会自动处理。
3.2 迁移学习:毕业设计的加分项
光靠上面这个简单结构,我在十类数据集上能到93%的准确率,但再往上就难了。为了拿到更好的结果,我又加了迁移学习实验:使用在ImageNet上预训练过的ResNet18作为特征提取器,把最后一层全连接换成适合自己任务的输出层。
import torchvision.models as models model = models.resnet18(pretrained=True) num_features = model.fc.in_features model.fc = nn.Linear(num_features, 10)这里要重点理解“迁移”的含义:ImageNet上有1000类日常物体,模型已经学到了一套通用的特征提取能力,比如边缘、纹理、形状、颜色组合等。这些低层和中层特征对于蔬菜识别同样是有效的。我只需要把高层分类器换掉,针对蔬菜类别重新训练即可。
实际操作中,我做了两种对比方案:一种是冻结特征层,只训练最后的全连接层;另一种是解冻所有层,用较小学习率对整个网络微调。前者的优点是训练快,不容易过拟合,但准确率上限偏低;后者的效果更好,但需要小心选择学习率,通常用更小的学习率如1e-4,避免破坏预训练学到的特征。
我的最终方案是两阶段训练:先冻结特征层训练10个epoch,让全连接层收敛;然后解冻全部层,用很小的学习率微调10个epoch。这个方案在验证集上的准确率比纯随机初始化高出了约4个百分点,最后达到了97%左右。
3.3 正则化与防止过拟合
模型结构定了,还要考虑如何防止过拟合。过一个毕设,最怕的就是训练集准确率99%,测试集只有80%。除了数据增强和Dropout,我常用的手段还有三个。
第一是权重衰减,也就是在优化器里加weight decay参数。可以理解为给模型的权重加一个L2范数约束,鼓励权重值尽量小,避免模型记住噪声。我一般设成1e-4,如果过拟合明显,会调到5e-4。
第二是早停机制。在训练过程中监控验证集准确率,如果连续5个epoch验证集准确率没有提升,就停止训练,并恢复到验证集最佳状态的模型权重。这个机制能尽量避免多余训练带来的过拟合。
第三是标签平滑。普通交叉熵损失把真实类别的目标设为1,其他设为0,模型会拼命放大真实类别的logit,容易造成过置信。标签平滑的做法是把目标的1换成比如0.9,剩余的0.1分摊到其他9个类别上。这样模型不需要对所有训练样本都输出极端概率,泛化能力通常会更好。在PyTorch里可以用CrossEntropyLoss的label_smoothing参数,设成0.1即可。
4. 训练过程与超参数调优实录
网络结构只是骨架,训练过程才决定模型最终能不能收敛。这一部分我把自己实验记录里最核心的参数、曲线判断方法和调优经验整理出来。
4.1 优化器与学习率策略
训练我使用的是AdamW优化器,初始学习率1e-3,权重衰减1e-4,batch size为32。为什么不用传统的Adam?AdamW是Adam的改进版,把权重衰减和动量解耦,理论上能带来更好的泛化效果,实践上在图像分类任务中也更稳定。
学习率的设置需要特别小心。学习率太大,loss会在一个较高的位置反复震荡,模型根本学不到东西;学习率太小,loss下降非常缓慢,训练时间成倍增加,而且在有限epoch内达不到理想效果。我一般用1e-3作为小模型的默认初始值,迁移学习微调时则降到1e-4。
训练中我还加了学习率调度器,选的是余弦退火。也可以简单用StepLR每10个epoch把学习率乘以0.7。学习率调度器的意义是整个训练过程中前期用大步长快速搜索,后期用小步长精调,有利于收敛到更优的局部最小值。
完整的训练循环里,我还固定了随机种子,包括Python、NumPy和PyTorch的随机种子。这个操作看似无关紧要,但能让实验可复现。没有固定种子时,同样的代码每次跑出来的结果可能差1-2个百分点,你没调任何东西却以为自己的改动有效,非常影响调参判断。
4.2 损失函数与评价指标
分类任务我选用交叉熵损失,这是因为输出层的softmax能把logits转换成概率分布,交叉熵衡量的是预测分布和真实分布之间的距离。相比均方误差,交叉熵在分类问题上梯度更友好,训练收敛更快。
只看准确率是不够的。在类别不均衡或者某些类特别容易混淆时,我需要同时看精确率、召回率和F1分数。我最终评估时计算了宏平均F1,也就是每个类别算一次F1再取平均,这样能避免大类压制小类。混淆矩阵也是必看的工具,它能直观看出哪些类别经常被互相认错。
用PyTorch做验证循环,我习惯直接在验证集上逐批计算预测结果,收集所有预测标签和真实标签,然后一次性用一个专门的评估函数处理。
4.3 训练曲线怎么读
我每跑一个实验,都会记录训练loss、验证loss和验证准确率。这里我总结几个常见的曲线形态对应的含义。
如果训练loss和验证loss都持续下降,说明模型还在学习,处于正常状态。如果训练loss下降,验证loss先下降再拐头上升,而训练loss还在继续下降,这就是非常典型的过拟合信号。模型在死记训练样本,对验证集失去泛化能力。遇到这个情况,我会先看当前epoch是否已经足够多,如果已经超过30,优先考虑降低模型容量或增强Dropout。
还有另一个容易被忽略的问题:验证准确率一开始就有小幅度上升,然后突然掉下来再升回去,这很可能是学习率偏大导致的震荡。我遇到这种情况的处理办法是把学习率降为原来的十分之一,重新开始训练,通常曲线就会稳定很多。
在迁移学习实验中,我第一阶段的验证准确率增长很快,因为预训练特征已经在ImageNet上训练好了,分类头只需要几轮就能收敛。第二阶段解冻微调时,验证准确率会有一个先下降再上升的过程,这是正常的,不要看到下降就急着重启。
5. 常见问题排查与经验技巧
一个毕设项目,最耽误时间的往往不是写代码,而是排查问题。下面这几个问题,我在自己项目里真实遇到过,也跟周围同学交流过,算是这个方向比较有共性的坑。
5.1 过拟合的典型表现与排查思路
现象一:训练集准确率不断上涨到99%,验证集准确率却停滞在85%左右。出现这个情况时我会按顺序排查:
- 检查数据划分是否真实随机。如果验证集和训练集来自不同拍摄环境、不同光照条件,分布差距太大,模型学的是训练环境特征,验证效果自然差。这种问题要通过数据侧解决,比如采集更接近真实分布的验证集。
- 检查模型容量。简单网络在数据量不够时也会过拟合,可以尝试减小通道数、增加Dropout、增大weight decay。
- 检查数据增强是否有效。如果没做增强,赶紧加上;如果增强强度太小,也可以略微加大。
我自己的经验是,先从数据侧检查,再调模型,因为数据修复带来的收益通常比调参更大。
5.2 某一类别正确率特别低怎么办
通过混淆矩阵,我发现模型经常把“白萝卜”和“胡萝卜”搞混,把“大蒜”和“洋葱”搞混。说实话,这类错误有一定合理性,因为白萝卜和胡萝卜切片后颜色相近,大蒜和洋葱都是鳞茎类,形状相似。针对这种情况,我做了三件事:
- 检查该类别图片质量。发现白萝卜部分图片拍摄时有大量背景干扰,我重新裁剪清洗了这一类的数据。
- 增加该类别专属的增强策略。比如对胡萝卜和白萝卜这类颜色相近的类,我把饱和度和亮度的扰动范围缩小,避免增强后“红萝卜”变成“白萝卜”。
- 为少数类使用过采样。简单说就是让困难类别在训练中出现的频率更高,模型有更多机会学习它们之间的细微差异。
经过这几步,白萝卜的召回率从78%提升到了91%,效果非常明显。
5.3 推理和部署阶段容易踩的细节
模型训练完了,最后要做演示环境。这时候很容易忽略训练和推理时的预处理不一致。训练时用的图片是224×224,但摄像头拍出来的照片是1920×1080,直接缩放成224×224会把长宽比压扁,影响效果。
我的处理方式是先对原始图片做中心裁剪,裁出与原始图片长宽比一致的方形区域,再缩放到224×224。另外,归一化用的均值和方差也要和训练时保持一致。如果使用了ImageNet预训练模型,可以直接用通用的均值0.485、0.456、0.406和方差0.229、0.224、0.225;如果是从零训练的网络,需要自己统计训练集的均值和方差,不能随便拿一组通用的数来用。
推理时的另一个小技巧是测试时增强,对同一张输入图片做水平翻转等多次变换,分别得到预测概率再取平均。这个操作通常能把准确率提升0.5-1个百分点,但会增加推理耗时,在演示时要注意不能太卡。
6. 可视化展示与后续可扩展的方向
毕业设计除了模型本身,还需要有拿得出手的成果展示。与其做一堆复杂界面,不如把核心结果可视化做好,让导师一眼看到模型的判断依据和整体效果。
6.1 用混淆矩阵和热力图辅助答辩展示
我画了一张测试集混淆矩阵热力图,横轴和纵轴都是10个类别标签,对角线越亮代表预测越准确。展示时,再挑几张典型图片,把模型预测的置信度分布画成柱状图,旁边标注真实标签。曾经有一个特别有用的展示是错误样本分析,比如模型把某张白萝卜图片错判成胡萝卜,我把模型预测为每个类别的概率都画出来,能直观看出模型在两个类别之间犹豫,置信度差异很小。
如果想让展示更“有技术含量”,可以考虑加一个Grad-CAM类激活热力图,它能把模型分类时主要关注的图像区域画出来。比如模型判断一张辣椒图片时,热力图集中在辣椒梗和表面纹理上,说明模型确实在学蔬菜特征,而不是背景。这个可视化是用深度学习做图像分类时最经典可解释性展示之一,答辩时非常加分。
6.2 从分类走向检测:下一步还能做什么
图像分类只是视觉任务的第一步。如果一个场景里有多个蔬菜,分类模型就不好用了,这时候需要做目标检测。可以把分类网络最后一层去掉,换成检测头,用YOLO或者Faster R-CNN来同时定位和分类多个目标。如果想部署到移动端,可以把网络换成MobileNet或ShuffleNet,模型体积更小,识别速度更快。更进一步,还可以结合电子秤硬件,做一个“识别+称重+计价”的智能蔬菜结算系统,这也是目前智慧零售场景里比较有实际价值的应用方向。
最后再分享一个我自己的体会:做这类毕设,不需要一开始就用特别复杂的模型,先把简单的CNN流程完整跑通,再逐步加迁移学习、调参、可视化。每一步都要记录清楚实验设置和结果,这样不仅论文有素材,答辩时回答“你做过哪些对比实验”也更有底气。遇到模型效果不好,先冷静分析数据、损失曲线和混淆矩阵,别急着换网络结构——很多时候问题根本不在模型本身。