资讯动态

基于CNN的青椒变质识别:从数据采集到部署的完整实战

发布时间:2026/10/22 14:13:24 来源:尧图企业网站定制
去年构思毕业设计的时候我其实纠结了很久。图像分类这个方向猫狗识别、垃圾分类、手势识别这些题目网上随便一搜都是现成的基础代码做出来容易但想讲出点自己的东西很难。直到有次逛菜市场看到摊主把表皮微微发皱、失去光泽的青椒挑出来单独便宜卖我忽然意识到一个问题判断青椒是否变质对消费者来说主要靠经验和眼力对商家来说靠人工挑拣整个过程既费时又主观。如果让深度学习模型直接学会这件事是不是有点搞头于是就有了这个项目——基于Python和CNN的深度学习训练实现青椒是否变质的自动识别。这篇文章就把我从数据采集、模型选型、调参训练到最终部署的全过程拆开讲清楚适合正在找毕业设计题目的同学也适合想做农业视觉识别方向入门项目的朋友。1. 青椒变质识别为什么值得做一个CNN毕业设计1.1 这个题目不是又一张图像分类很多同学一听图像分类就觉得老套但如果仔细想想青椒变质识别和常见的果蔬品种分类有本质区别。品种分类是区分苹果和梨不同类别之间外形差异巨大模型很容易抓住颜色、形状上的显著特征。而青椒变质识别是同一类目标内部的状态判别新鲜青椒和刚进入变质阶段的青椒外形差异非常细微——可能只是表面光泽暗了一点果蒂处多了几道皱缩纹理或者果肉手感变软了。这种类间差异小、类内差异大的任务恰恰比单纯的品种分类更有挑战性也更能体现深度学习特征提取的能力。实际落地场景也说得通。生鲜零售行业每年因为变质损耗的果蔬数量相当可观智能摄像头如果能在货架端实时提醒店员挑出变质食材就能减少浪费餐饮后厨也可以用它做食材入库质检。当然毕业设计不需要做到产品级但只要有清晰的应用背景答辩时就能把为什么做这个问题讲得很扎实。1.2 难点恰恰是这个项目的价值点做之前我列了一下这个任务的技术难点做完回头看这些难点全都变成了论文里的分析素材。第一青椒表皮光滑且有反光不同光照条件下拍出来同一种状态的颜色分布可能差得很远。菜市场暖黄灯光下的青椒和室内白光下的青椒成像差异肉眼可见。第二变质是渐进过程新鲜和变质之间没有明确边界同一个青椒可能表面还有七八成新鲜果蒂已经开始发褐。第三人工标注具有主观性不同标注者对初变质的判定标准可能不一致这会让数据集本身的噪声变大。这些难点对于一个毕业设计来说反而是好事。难点意味着有分析空间有调优过程有对比实验可以做。一个毫无挑战的题目写报告的时候撑不过三页而这个题目光是数据定义和数据清洗就能写出一大章。1.3 技术选型的初步判断确定题目后技术路线其实没有太多悬念。图像分类任务最成熟稳妥的做法就是CNN。PyTorch框架生态完善torchvision里面预训练模型直接调用做迁移学习也非常方便。Python在深度学习领域的普及度决定了遇到任何问题都能顺利搜到解决办法对毕设党来说这点太重要了。后续我围绕CNN做了三件事搭建基线模型、引入预训练模型微调、对模型输出进行可视化分析。这三件事构成了整个项目的四条主线——数据、模型、训练、评估。2. 数据集搭建采集、标注与清洗的实操细节2.1 先定义清楚变质否则标签会反噬你这个项目最容易翻车的环节不是模型训练而是数据标注前没有把变质定义清楚。我一开始的想法很简单两类新鲜、变质。结果标注了300张图之后发现问题大了——有些青椒表面只有一小块褐斑整体还硬挺这算新鲜还是变质有些青椒已经明显发软但颜色还是绿的这又怎么算后来我查了一些农产品品质分级的资料又把题目改成了三分类新鲜、初变质、深度变质。判定标准也重新定了不能只看某一个特征而是综合看四个维度——表皮皱缩程度、光泽度变化、果实质地软硬、是否有褐斑霉点。类别主要特征场景定位新鲜表皮饱满有光泽果肉硬挺果蒂翠绿正常销售状态初变质局部表皮轻微皱缩光泽减弱果蒂开始变褐临界促销或降级销售深度变质大范围皱缩软化明显褐斑霉点甚至渗水必须丢弃或处理把二分类改成三分类表面上是增加了工作量实际上是在降低标注的一致性和模型的学习难度。类别之间的过渡地带被单独抽出来作为一个类别模型不用强行把连续变化的过程压进两个框里收敛起来反而更稳定论文里可以分析的东西也多了。2.2 采集方案手机拍摄就够了但要有规范数据采集我用的就是手机三台不同品牌的手机换着拍这样能自然引入不同摄像头的色彩差异。每类最终各收集了约600张原始图片总计1800多张这个规模对毕业设计完全够用。采集规范上有几条血的教训。一是拍摄背景要多样白纸、木质桌面、深色托盘、透明保鲜袋都用来当过背景但不能让某一种背景占绝对多数。二是光照条件要覆盖常见场景白天窗边自然光、室内顶灯、较暗的阴天光线都要有。三是同一颗青椒可以换角度、换位置多拍几张但不能原地反复拍同一张否则本质上就是同一张图的复制品。这里还要特别注意一个问题青椒的状态是随时间变化的。我今天买了十颗青椒分三天拍摄第一天拍的新鲜图到了第三天那批青椒可能已经变成了深度变质。如果采集和标注不是同一天完成就必须给每张图片记录拍摄日期。我就是因为周末拍完一批图周一才集中标注结果一部分标注为新鲜的图实际上已经是变质状态白白污染了数据集。2.3 标注工具用文件夹组织就是最省力的方案很多教程一上来就推荐LabelImg画框标注但对于这个项目来说全图分类用文件夹组织就够了。数据集目录结构是data/ ├── train/ │ ├── fresh/ # 新鲜 │ ├── initial/ # 初变质 │ └── deteriorated/# 深度变质 ├── val/ └── test/把图片按类别放进对应文件夹再用Python脚本读取目录名作为标签省时省力。如果后续想升级为细粒度定位再转用Labelme标注变质区域也不迟。我在标注过程中找了一位同学做交叉校验每人独立标注一遍对结果不一致的样本重新对照实物讨论确认。这样做虽然慢一点但明显提高了标签的可信度后面训练出来的模型也没有因为标签噪声而反复震荡。2.4 数据增强做好提升也要克制数据增强是这个小数据集项目里最关键的保命手段之一。我用torchvision自带的transforms实现了标准增强流程from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])注意我这里的ColorJitter参数亮度、对比度、饱和度都只给了0.2色相hue只给了0.05。为什么这么克制因为青椒的绿色本身就是一个重要特征色相如果拉得太猛青椒会被增强成番茄色、紫薯色反而让模型在正常绿色样本上表现得一塌糊涂。这个问题我后面踩坑实录里还会详细说这里先提醒一句颜色敏感任务的颜色增强必须轻手轻脚。归一化的均值和标准差我最初直接用ImageNet的标准值跑通流程没有问题。如果想要更严谨可以自己统计数据集得到专属的mean和std能让一开始的loss波动小一点但对于最终准确率影响不大看个人选择。2.5 数据划分最容易做错的一步这一步的教训比较深刻。我最开始把全部图片随机打乱按7:2:1划分训练集、验证集、测试集。训练出来的模型在验证集上准确率高达97.8%我当时还挺高兴。结果在真实场景一测掉到了85%以下差距非常大。排查之后发现问题出在随机划分上。同一批拍摄的青椒光照、背景、手机型号完全相同随机划分后这批图片会被同时分进训练集和验证集模型相当于提前见过了验证集的光线分布这属于典型的数据泄露。验证集准确率虚高完全不能反映真实泛化能力。正确做法是按拍摄批次划分。我在代码里给每张图片记录批次号保证同一个批次的所有图片只出现在一个集合内。改成这种划分方式之后验证集准确率从虚高的97.8%回落到92%左右但真实场景测试的结果反而更接近验证集表现。做数据划分时批次优先、随机其次这条原则适用于所有带环境背景差异的图像任务。3. CNN网络选型从经典模型到轻量化方案的取舍3.1 卷积神经网络到底在做什么很多没接触过CNN的同学会觉得它是个黑盒。我用自己的话拆解一下卷积神经网络其实是在逐层提取图像的特征等级。最浅的卷积层只能看到边缘和颜色块就像人眼先辨认轮廓中等层会组合出纹理和局部形状比如青椒表皮的光滑区域、皱缩区域的纹路到了深层网络能识别出果蒂褐斑霉点这样的高等级部件概念。最后接一个全连接分类层把学到的特征映射到三个类别上。这个理解在后来的实验里得到了验证。当我去可视化模型最后一个卷积层的激活区域时模型确实把注意力集中在果蒂和皱纹明显的区域而不是背景。这一定程度上说明CNN没有作弊是真的在学青椒的关键部位。3.2 三条路线的对比我在选型时同时考虑了三条路线从零训练一个浅层自定义CNN、使用ResNet预训练模型迁移学习、使用轻量化网络MobileNet做部署准备。三个模型都做了对比实验结果整理成了一张表方案参数量验证集准确率单张推理耗时(CPU)备注自定义5层CNN约220万84.6%35ms基线训练较慢收敛ResNet18迁移学习约1120万93.1%55ms精度最高参数大MobileNetV2迁移学习约350万92.4%22ms精度接近部署友好最终我论文里的主模型用的是ResNet18迁移学习另用MobileNetV2作为部署对比方案。表里能看出来预训练权重带来的提升非常明显自定义浅层CNN虽然也能学但在只有1800张图片的小数据集上从零训练很难学到足够泛化的纹理特征。3.3 为什么迁移学习在小数据集上是默认答案三个方案里我最推荐迁移学习特别是对数据量在每类几百张的毕业设计项目。打个比方预训练模型就像一个已经学会辨认各种颜色、纹理、形状的实习生他要学的新任务只是青椒的哪种纹理组合代表变质上手非常快而从零训练的模型就像一个完全没见过图像的新人连什么是边缘都要重新学给他1800张图根本不够用。ResNet18在ImageNet上已经学会了通用的视觉特征这些特征对青椒也是适用的——毕竟青椒也是自然物体它的表皮纹理边缘与自然图像里的其他物体共享相似的视觉规律。我在实践里用的是torchvision里带预训练权重的写法import torch.nn as nn import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) # 替换最后一层全连接输出3个类别 model.fc nn.Linear(model.fc.in_features, 3)迁移学习的使用里有一条经验微调时的学习率一定要小。预训练权重已经是一个很好的解空间起始点学习率设太大就好比把实习生辛苦积累的经验全部推翻重来效果反而不如从头训练。我用的初始学习率是1e-4比常规从头训练的1e-3低一个数量级。后面会在调参部分细说。4. 训练调参全过程学习率、Batch与过拟合的拉锯战4.1 训练环境与基本配置整个项目的训练环境我没有用本地显卡租卡又太贵实际是用免费云GPU环境完成的配了一块大约16GB显存的显卡训练ResNet18这种体量的模型绰绰有余。框架用的是PyTorch 2.x配合torchvision数据加载使用DataLoader设置了num_workers4显存占用不大一次训练大约十几分钟就能跑几十轮。训练前的关键初始化配置batch_size 32 epochs 80 init_lr 1e-4 weight_decay 1e-4 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lrinit_lr, weight_decayweight_decay) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs)4.2 学习率最重要的一个旋钮学习率是这个项目里最需要调也最容易调炸的参数。我做了几组对比实验后的结论是迁移学习场景下1e-4是最稳妥的起点。如果学习率偏大比如到了1e-3训练前期loss虽然下降很快但到中后期验证集准确率开始上下抖动甚至出现loss爆炸那是因为预训练权重被大幅破坏模型退化成从零训练。如果学习率偏小比如1e-5loss下降速度太慢80轮都未必能收敛到位。调度策略我用了CosineAnnealingLR它会先保持较高学习率快速下降loss再逐步减速逼近最优解。相比StepLR的阶梯式下降余弦退火在小数据集上表现更平滑最终精度也更高一点。训练过程中手动打印每个epoch的loss和验证集准确率我判断收敛的参考标准是验证集准确率连续10轮不再提升且loss保持平稳就停止训练。4.3 类别不平衡一个容易被忽略的细节三分类中如果拍摄时对变质样本刻意多采集三个类别的数量大体均衡。但实际采摘或者购买青椒时深度变质样本本来就少很多同学的原始数据里这一类可能只有新鲜样本的一半。如果不处理模型会对样本少的类别严重误判。我用PyTorch自带的WeightedRandomSampler解决过这类问题更简单的方案是给损失函数计算类别权重。这里用sklearn的计算方式生成权重然后传入CrossEntropyLossimport numpy as np from sklearn.utils.class_weight import compute_class_weight labels [] # 训练集所有标签 class_weights compute_class_weight(balanced, classesnp.unique(labels), ylabels) weights torch.tensor(class_weights, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightweights)加了这个后模型的recall在少数类上明显回升整体F1得到了改善。不过也要提醒一句加了权重后训练过程会变得更敏感需要配合更小的学习率。4.4 防止过拟合的整套组合拳我在这类小数据集上常用的防过拟合措施按效果排序大概是数据增强、预训练权重、早停、Dropout、Weight Decay。数据增强和预训练权重前面说过了是两个最大的贡献者。早停Early Stopping是毕业设计里非常推荐的自保手段——训练过程中如果验证集准确率连续很多轮没有提升就主动结束训练并把历史上最好的模型参数保存下来避免后续过拟合。实现起来只需要监控验证集指标在循环里加判断。Dropout对ResNet这种带BatchNorm的网络效果不那么明显我更多是在自定义浅层CNN里加了nn.Dropout(p0.5)。Weight Decay在所有优化器里都有效我的经验是1e-4这个量级对这个小项目足够太大会让模型欠拟合。另外Batch Size的选择也值得记录。我试过16、32、64三档最终选择了32。小Batch训练时噪声更大反而起到类似正则化的作用泛化更好大Batch如果配合偏大的学习率容易收敛到尖锐的极小值验证集表现会震荡。显存有限也没关系16GB跑Batch32毫无压力。最后提一下混合精度训练。PyTorch自带AMP开启后训练时间能缩短大概30%模型精度几乎无损失。代码如下from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for images, targets in train_loader: images, targets images.cuda(), targets.cuda() optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, targets) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()训练曲线我这里就不贴了正常过程是训练loss快速下降验证集准确率从82%逐步爬升到92%左右中途会有几次平台期余弦退火会在平台期帮模型慢慢蹭上去。整体训练80轮最终保存的模型在验证集上达到了93.1%准确率。5. 评估与部署从准确率到实际场景的差距5.1 混淆矩阵里藏着的真实短板准确率只是一个汇总数字真正能帮我定位问题的是混淆矩阵和分类报告。训练完成后我在测试集完全未参与训练和验证上跑了分类报告from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt report classification_report(y_true, y_pred, target_names[fresh, initial, deteriorated]) print(report) cm confusion_matrix(y_true, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[fresh, initial, deteriorated], yticklabels[fresh, initial, deteriorated]) plt.xlabel(Predicted) plt.ylabel(True) plt.show()三个类别的结果差异非常明显。深度变质的precision和recall都很高接近98%因为深变质样本有显著的褐斑霉点特征明确。新鲜类的recall也不错大约95%。绊脚石集中在初变质这一类precision只有75%左右——换句话说被模型判定为初变质的样本里有四分之一其实是新鲜的。这其实符合预期因为初变质和新鲜本来就处在一条连续的渐变带上人类标注都容易分歧模型在这个衔接带上犯错再正常不过。这个结果给了我一个论文分析的思路与其追求所有类别都完美不如承认类别之间的边界是本任务固有难度并且提出后续改进方向比如集成多帧图像投票、或者把分类改成回归连续变质度。答辩时这段分析很能体现思考深度。5.2 Grad-CAM可视化给自己的模型做一次体检只给一个93%的准确率数字答辩时很容易被追问模型到底看哪里做判断。所以我给模型加了一个可视化工具——Grad-CAM它能生成热力图标示模型决策时重点关注的图像区域。我用的是pytorch_grad_cam这个库使用很简单from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layer model.layer4[-1] cam GradCAM(modelmodel, target_layers[target_layer]) grayscale_cam cam(input_tensor)[0] heatmap show_cam_on_image(image_transformed, grayscale_cam, use_rgbTrue)可视化结果显示模型对新鲜青椒的关注点主要集中在完整光滑的果面对初变质青椒热力图高亮区往往出现在果蒂周围和局部皱缩区域对深度变质青椒注意力则集中在褐斑霉点上。这说明模型确实学习到了与人类认知一致的关键特征而不是抓住背景颜色之类的捷径。这套可视化对毕设答辩的意义不只是锦上添花它直接证明了你理解自己的模型而不只是会调包。5.3 部署测试从Notebook到Web服务训练完的模型不能一直留在Notebook里我做了一个轻量级的Web演示。首选方案是Flask加一个简单的前端页面用户上传青椒照片后端调用PyTorch模型输出预测结果。这里必须提一个经验把训练好的PyTorch模型直接塞进Web服务的坑在于文件太大、推理慢。ResNet18的权重文件约45MB还能接受但如果你用了更大的模型就需要考虑中间表示格式。我把模型导出成ONNX格式然后用ONNX Runtime做推理。导出的代码非常短import torch import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc torch.nn.Linear(model.fc.in_features, 3) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, pepper.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}}, opset_version17 )导出后用ONNX Runtime推理CPU上的单张耗时大约比PyTorch快20%-30%还省去了依赖管理问题。如果部署到树莓派或者手机端还可以进一步用动态量化压缩体积。对于毕业设计来说能把模型导出ONNX并跑通一个Web页面工程的完整度已经非常加分了。真实场景测试我做了大概40张新拍的照片测试结果比测试集大约低了5个百分点主要是在暗光环境下模型容易把光泽减弱误判成初变质。这个其实不是模型单独的问题——暗光下人类自己也很难判断青椒的光泽度就像在夜里很难看清水果新不新鲜一样。性能下降可以被合理归因到成像信息本身的流失。6. 踩坑实录三个月里最值得分享的五个坑如果只让说一句话给做类似项目的人我会说这个项目的坑十个里有八个出在数据上而不是模型上。下面这五个坑每一个都是我实际踩过并花时间填平的。6.1 坑一标签时间漂移前面在采集章节提到过我周末买了一批青椒拍了一组照片当时全是新鲜状态但标注拖到了周一晚上。第二天打开冰箱看到的情景让我傻眼了一部分青椒在冰箱里放了一天一夜表皮虽然没有大变但果蒂处开始轻微发褐严格按标准已经算初变质。我标注时按照实物状态标而不是按照照片里当时的状态标导致一批本该是新鲜的训练图被我标成了初变质。这批脏数据混进训练集之后模型的初变质类别怎么都学不好loss也反复波动。后来重新按拍摄时间逐张核对剔除了当时状态和标注状态不一致的图片重新训练后一切恢复正常。教训就是生鲜类数据拍摄与标注之间的时间间隔越短越好必须记录拍摄日期。6.2 坑二同批次随机划分造成数据泄露这个坑在数据划分章节已经细说了。同一个拍摄批次的图片共享相同的光照和背景随机划分会让验证集准确率虚高。我修好这个问题之后验证集准确率下降了约5个百分点但真实场景的表现和验证集变得一致了。做农业图像类项目划分数据时请一定记住批次优先随机其次。6.3 坑三白色背景成了模型的最强捷径我的采集阶段过于省事有将近一半的原始照片是放在白色托盘上拍的。模型是有捷径可走的——它发现画面底部有大面积白色和新鲜之间的相关性非常强于是大量使用背景特征做判断。真实场景一换成木质桌面、深色桌布准确率立刻掉了将近10个百分点。发现这个问题靠的还是Grad-CAM热力图部分样本的热力图高亮区居然跑到了青椒轮廓以外的白色背景上。回看数据分布之后我补拍了一批复杂背景的图片并重做数据划分这个问题才被压下去。如果你在采集初期就有意识地让背景多样化后面能省一大段时间。6.4 坑四颜色增强过度青椒被增强成紫薯我在调ColorJitter的时候走过极端以为增强越猛越防过拟合把hue调到了0.4。结果训练集上模型loss一路下降验证集却从头到尾都在82%附近晃荡怎么都上不去。后来打印了几张增强后的样本我才发现青椒在色相偏移下被变成了紫色、蓝色已经完全不像青椒了。模型在训练中被迫适应这些假的彩色青椒反过来干扰了它对正常绿色的判断。颜色敏感的图像任务里ColorJitter一定要克制hue控制在0.05以内saturation和brightness在0.2左右就够了。数据增强的目标是模拟真实环境的光线变化不是制作梵高滤镜。6.5 坑五把大模型直接塞进Flask服务的尴尬做部署演示时我最初直接把PyTorch模型文件放进Flask里调用模型加载慢不说CPU推理速度也只能说勉强能用。模型文件大、依赖库繁琐、每次启动都要等几秒整个演示体验很差。换成ONNX Runtime之后加载变快了推理也快了还顺手把整个推理逻辑封装成了一个独立的类前端调用后端的结构清爽了很多。对于毕业设计而言部署环节不需要追求极致性能但模型管理的思路值得建立——训练产物和部署产物分开保存best_model.pth做进一步实验导出pepper.onnx用于演示服务。这样用户上传一张青椒照片页面几秒钟内返回三类概率和热力图整个过程非常顺畅。7. 写在最后青椒项目还能往哪里走整个项目做下来我最深的体会是毕业设计最值钱的部分不是模型准确率而是完整走了一遍需求分析—数据采集—数据清洗—模型选型—训练调参—评估分析—部署演示的闭环。这中间踩过的五个坑、调过的十几个参数、重新划分的三次数据才真正让课本里的CNN概念变成了我自己的判断力。如果让我再往前推进一步这个项目有很多自然的扩展方向。一是把三分类改成连续的变质程度回归输出0到1的变质分数这对实际应用更友好。二是引入注意力的细粒度模型专门学习果蒂、皱缩区域这些局部特征。三是部署到手机端做一个拍照即检测的小程序那就需要把模型量化到足够小。四是与硬件结合配合光谱或气体传感器做多模态判别。这些都是打开思路后的可选方向具体做哪个取决于你手里的时间和对哪个环节最感兴趣。最后分享一个小建议如果你也准备做类似课题请从第一天开始养成记录每个实验的习惯。我当时建了一个简单的表格记录每次实验的数据划分方式、增强参数、学习率、验证准确率、备注三个月后写论文时这份实验记录帮了大忙。做实验没有记录就像打游戏没有存档所有经验都会随着时间模糊掉。希望你踩过的坑比我少收获比我多。

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

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

免费获取报价 →
↑