
简介基于深度学习CNN网络的水果蔬菜识别系统是一套适合毕业设计、课程设计及实践项目使用的完整源码包面向计算机相关专业学生、高校教师与开发者。项目包含数据预处理、模型训练、测试评估、界面登录等Python脚本并附带配套论文报告能够帮助使用者快速理解卷积神经网络在图像分类任务中的应用流程。压缩包共78个文件涵盖16个py源码、40张png图片、6张jpeg、2张jpg及2个pdf论文等其中图片既包含实验样本也包含系统界面与评估曲线截图txt为说明与训练日志整体仅12.72MB轻量易部署。目前已有111人学习使用代码完整且附有运行说明尤其适合希望在CNN图像识别方向上二次开发或深入钻研的学霸型同学即使基础薄弱依据文档也能顺利跑通并可私信获得远程指导。1. 一套可以直接跑的水果蔬菜识别系统是怎么拆的如果手里只有几百张果蔬图片没有预训练权重也不打算上云端API还想在答辩现场完整体现“数据增强、CNN训练、评估曲线、桌面界面”整套链路那这个项目是一个值得对照的参考实现。它不依赖外部服务代码里自带数据切分脚本、增强脚本、训练脚本、测试脚本和Tkinter界面配套PDF论文和评估曲线图属于比较典型的“课程设计型CNN工程”。我按“数据准备 → 模型训练 → 评估验证 → 界面部署”的顺序完整跑了一遍发现它的代码路径清晰没有花哨设计。比较适合两类人一类是把毕业设计、课程设计定位在“完整深度学习应用展示”的同学另一类是刚学完PyTorch、想找一个非MNIST的完整项目练手的从业者。下面把每个脚本的实现思路、参数含义和踩过的坑展开讲。2. 数据增强与样本切分Data_enhancement.py 和 data_split.py 的配合逻辑2.1 为什么果蔬识别必须先做样本切分再谈增强项目代码里先执行的是data_split.py这一点很关键。很多初学者上来就对全部图片做增强然后整体丢进模型训练最后发现训练集和验证集之间有重叠图片评估曲线虚高答辩时被问一句“你的验证集里有没有训练过的图”就很难解释。data_split.py做的事情是把同一类果蔬的图片按照指定比例拆到train和val两个目录。以西红柿为例如果你有100张西红柿图片它会按8:2拆成80张训练、20张验证。这个步骤保证了同一张原图只出现在一个集合里。代码如下import os import random import shutil from sklearn.model_selection import train_test_split def split_class_data(src_dir, train_dir, val_dir, test_size0.2, seed42): os.makedirs(train_dir, exist_okTrue) os.makedirs(val_dir, exist_okTrue) all_images [f for f in os.listdir(src_dir) if f.lower().endswith((.jpg, .jpeg, .png))] train_files, val_files train_test_split( all_images, test_sizetest_size, random_stateseed ) for f in train_files: shutil.copy(os.path.join(src_dir, f), os.path.join(train_dir, f)) for f in val_files: shutil.copy(os.path.join(src_dir, f), os.path.join(val_dir, f)) print(f{src_dir}: total{len(all_images)}, ftrain{len(train_files)}, val{len(val_files)})这个脚本里有几个点值得说。第一是test_size0.2对于样本量本身不大的果蔬数据集验证集比例不建议再高否则训练数据太少第二是random_state42固定随机种子不然每次切分结果都不同后面调整模型时没法对比第三是它直接复制文件而不是移动原始数据被保留了重复实验成本低。train_test_split内部用的是分层抽样和随机置换虽然这里没传stratify参数但建议在样本量比较大的情况下加上让每类果蔬的训练验证比例一致。如果某类只有二三十张图片分层与否差别不大但如果有一类特别多、一类特别少不带分层可能会让少数类全部掉进验证集。2.2 增强策略位移、翻转、颜色扰动怎么配数据切分完成后Data_enhancement.py作用在训练集上。果蔬图片有个特点拍摄角度、光照、背景差异大但物体本身没有方向性——西红柿倒过来还是西红柿。因此水平翻转是安全操作旋转角度不宜过大因为竖长的胡萝卜旋转90度后特征并不常见。增强脚本的核心逻辑是用PyTorch的torchvision.transforms组合出一套在线数据增强管线。之所以强调“在线增强”是因为这种做法的好处是每个epoch模型看到的图片都是经过随机扰动的新版本等效于扩大了训练集的多样性。对于样本量只有几百张的项目这一步骤带来的提升比换模型结构还要明显。from torchvision import transforms train_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.RandomAffine(degrees0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])参数按我跑通这个项目的经验解释一下。RandomHorizontalFlip(p0.5)设置成0.5是比较稳妥的默认值太高会让模型看到太多翻转样本太低则增强效果不明显。RandomRotation(degrees15)对果蔬这种中心物体分类来说比较安全超过30度会把非方向性样本扭曲到难以辨认尤其对苹果、梨这类有明显果柄的类别旋转过大反而引入噪声。ColorJitter里面亮度对比度饱和度都设置在0.2这个数值对手机拍摄的果蔬照片比较友好不会出现颜色溢出hue0.05必须设得很小因为色相偏移过大时青苹果会被增强成红苹果直接导致标签错乱。要注意的是验证集不能做随机增强只做Resize、ToTensor、Normalize。如果验证集也用随机翻转和颜色扰动评估结果就失去了稳定性同一张图两次评估结果会不一样。这也是测试线上模型和本地评估指标对不上的常见原因之一。3. 自建CNN的训练管线网络结构、超参与收敛判断3.1 为什么不用预训练模型课设场景下的合理选型这个项目没有用ResNet或者VGG预训练权重而是自己搭了一个小型CNN。初始看起来这是个“缺点”但仔细想其实是对的场景。毕设和课设的答辩重点通常在于学生是否掌握了卷积、池化、全连接、损失函数这些基础概念自建CNN结构简单透明每一层的参数和特征图变化都能在答辩现场讲清楚。差分的对比实验也能做加深一层卷积、加一个Dropout、换一下池化方式模型行为会产生肉眼可见的变化。另外要考虑压缩包自带的评估曲线显示训练轮次较多且收敛正常如果在CPU机器上跑VGG16的一个epoch可能要好几分钟而自建网络几秒钟就能跑完一个epoch这对调试超参非常有利。预训练迁移学习更适合数据量大或追求最高精度的场景但作为课程项目可控性和可解释性优先。3.2 网络结构设计与训练脚本解析train_cnn.py定义了模型的整体训练流程网络结构可复现为三个卷积块加全连接层。每个卷积块包括卷积层、批归一化、ReLU和最大池化。先看结构定义部分的代码import torch import torch.nn as nn class FruitVegCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))这里的结构选择有明确依据。第一层用32个卷积核第二层和第三层翻倍到64和128这是小型CNN比较常用的通道数递增方式。BatchNorm2d在每层卷积之后加它有稳定训练的作用可以让学习率设置得更激进一些也在果蔬这种颜色和纹理差异不固定的数据上避免梯度震荡。AdaptiveAvgPool2d(1)比较关键它把任意尺寸的特征图压缩成1x1这样全连接层的输入维度就固定了后面换输入分辨率比如从224改成128时不用改网络定义。下面这个表是训练阶段的核心超参配置参数取值说明输入尺寸224x224与预训练模型兼容特征信息充足batch_size32小数据集上收敛稳定显存友好epoch50配合早停机制避免过拟合优化器Adamlr0.001自适应学习率无需精细调整损失函数CrossEntropyLoss多分类标准选择内部含softmax学习率调度StepLRstep10每10轮学习率乘以0.5训练主循环中还有一个容易被忽略的细节model.train()与model.eval()的切换。这个项目里在训练前调用train()验证前调用eval()。由于网络里有BatchNorm和Dropout如果不切换状态验证时BatchNorm仍会使用当前batch的统计量Dropout也会随机失活神经元导致验证准确率出现波动。for epoch in range(epochs): model.train() train_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * images.size(0) model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_correct (predicted labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total print(fEpoch {epoch1}/{epochs} | loss{train_loss/len(train_loader):.4f} f| val_acc{val_acc:.4f})这个循环里面有两点值得注意。optimizer.zero_grad()如果在循环里忘写梯度会在每个batch之间累积叠加模型几乎不会收敛这是最常见的“loss不下降”原因之一。torch.no_grad()在验证阶段必须加否则PyTorch会为每个中间张量构建计算图验证变慢且显存暴增。验证集指标应该在每组 epoch 结束后记录最终训练脚本生成的cnn_train.txt就包含每轮的损失和准确率记录可以拿来做 Overfitting 分析——具体来说就是看训练准确率是不是持续上升而验证准确率在某个 epoch 后开始下滑如果是那就说明模型开始过拟合了。4. 混淆矩阵与Heatmap验证别让准确率曲线骗了你4.1 从验证脚本到每类的真实表现训练完成后test_model.py负责在测试集上评估最终模型。很多人只看一张 Accuracy 曲线就觉得训练完成了但这个项目额外生成了heatmap_test.png这个混淆矩阵热力图。果蔬识别这类任务中总体准确率即使到了88%细看混淆矩阵也可能发现“圣女果”和“西红柿”互相看错这是整体准确率暴露不出来的问题。这里给出用测试脚本统计混淆矩阵的常见做法不依赖外置库import numpy as np import torch import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay def evaluate_with_confusion(model, test_loader, class_names, device): model.eval() y_true, y_pred [], [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, predicted torch.max(outputs, 1) y_true.extend(labels.cpu().numpy()) y_pred.extend(predicted.cpu().numpy()) cm confusion_matrix(y_true, y_pred) disp ConfusionMatrixDisplay(cm, display_labelsclass_names) disp.plot(cmapBlues, xticks_rotation45) plt.savefig(heatmap_test.png, dpi150, bbox_inchestight) np.savetxt(model_test.txt, cm, fmt%d, delimiter,)这个脚本里的model_test.txt是混淆矩阵的数值版本答辩时用表格呈现比热力图更精确也方便算每类的精确率和召回率。ConfusionMatrixDisplay直接用scikit-learn的封装即可不用自己写双重循环输出会自带颜色映射。4.2 两张评估曲线怎么读才不算白跑项目中同时出现了Loss曲线和Accuracy曲线分别是Loss_cnn.png和Accuracy_cnn.png这是训练日志文件cnn_train.txt的可视化结果。训练日志里记录的是每个epoch的训练损失和验证准确率可以用下面的脚本画在任何PyTorch项目里import matplotlib.pyplot as plt logs [] with open(cnn_train.txt, r) as f: for line in f: if val_acc in line: parts line.strip().split(|) loss float(parts[1].split()[1].strip()) acc float(parts[2].split()[1].strip()) logs.append((loss, acc)) losses [x[0] for x in logs] accs [x[1] for x in logs] fig, ax1 plt.subplots() ax1.plot(losses, b-, labelTrain Loss) ax1.set_xlabel(Epoch) ax1.set_ylabel(Loss, colorb) ax2 ax1.twinx() ax2.plot(accs, r-, labelVal Acc) ax2.set_ylabel(Accuracy, colorr) plt.savefig(combined_curve.png, dpi150)双y轴图把损失下降和精度上升放在一张图里可以快速判断训练是否正常推进。但也别忽略项目里loss_test.png和accuracy_test.png分开存放的意义如果损失在后期降到很低、验证准确率却不再上升说明模型已经拟合到训练数据的细节上继续训练意义不大应该做的是加大增强力度或降低模型容量。4.3 Heatmap类激活可视化是怎么来的这个项目里出现了heatmap_cnn.png和heatmap_test.png两个文件它们在深层含义上有所区别前者来自训练集特征后者是测试集侧的验证结果。测试集热力图美化之后还可以继续往下分析。CNN可视化里常见的做法是用Grad- CAM看模型焦点这需要拿到最后一个卷积层的输出梯度。果蔬识别里做类激活图的典型做法是Hooks挂载在model.features[-1]这个层上def grad_cam(model, image_tensor, target_class, device): activations {} gradients {} def forward_hook(module, input, output): activations[value] output.detach() def backward_hook(module, grad_input, grad_output): gradients[value] grad_output[0].detach() hook_layer model.features[-1] fh hook_layer.register_forward_hook(forward_hook) bh hook_layer.register_full_backward_hook(backward_hook) output model(image_tensor.unsqueeze(0).to(device)) model.zero_grad() one_hot torch.zeros_like(output) one_hot[0, target_class] 1 output.backward(gradientone_hot) fh.remove() bh.remove() weights gradients[value].mean(dim(2, 3), keepdimTrue) cam (weights * activations[value]).sum(dim1, keepdimTrue) cam torch.relu(cam) cam torch.nn.functional.interpolate( cam, sizeimage_tensor.shape[1:], modebilinear, align_cornersFalse ) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam所谓Grad-CAM其核心思想是把类别得分对最后一个卷积层输出的梯度做全局平均池化得到每个通道的权重再对激活值做加权求和得到的响应区域直接表示“模型在看图片的哪个部位”。这个实现里register_full_backward_hook比旧版register_backward_hook在新版本PyTorch中更稳妥。如果模型预测某个胡萝卜样本时热力图集中在叶子而不是根部说明模型学到的是背景或叶子特征需要给训练集补充不同形态的胡萝卜图片或者调整光照方向。5. 把CNN装进Tkinter界面推理链路与容易忽略的四个坑5.1 桌面端加载模型并完成实时识别项目的window.py和login_main.py组合起来是一套带登录功能的桌面应用。第一次运行时登录窗口检查用户密码然后进入主界面主窗口支持上传图片、显示图片和触发推理。模型推理部分的代码被分离得比较清楚加载权重用的是torch.load加model.eval()这部分逻辑值得说一下因为它和训练脚本的torch.save必须严格配对。如果训练时保存的是整个模型那么加载也要用整个模型方式如果保存的是state_dict加载时就要先实例化网络再载入权重import torch from PIL import Image from torchvision import transforms def predict_image(model_path, image_path, class_names, device): model FruitVegCNN(num_classeslen(class_names)) state_dict torch.load(model_path, map_locationdevice) model.load_state_dict(state_dict) model.to(device) model.eval() preprocess transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) image Image.open(image_path).convert(RGB) input_tensor preprocess(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) probabilities torch.softmax(outputs, dim1) confidence, predicted torch.max(probabilities, 1) return class_names[predicted.item()], confidence.item()这里特别要注意Image.open(image_path).convert(RGB)这个操作。项目里测试图片包含PNG格式而PNG可能带有Alpha通道如果不转成RGB图片会被读成4通道和训练时的3通道输入不一致模型会直接报维度错误。另外识别完成之后如果想在界面上展示识别结果常规做法是在图像上绘制矩形框和类别文本并把PIL图像转为Tkinter可以显示的ImageTk.PhotoImage。5.2 实用技巧做一次完整的推理一致性检查整个项目跑通之后建议把test_images目录作为固定的验收基准。这个目录下统一放验证集呢还是单独预留一些绝不参与训练的照片呢在这里建议单独留出几张“完全没见过”的照片包括网上下载的不同分辨率、不同光照的果蔬图专门用于最后的人工验收。具体操作是训练完成后记录模型在这几张图上的输出类别和置信度再开发一个小脚本自动输出识别结果和运行耗时然后人工核对。如果某张图置信度低于0.6说明模型对这类图片的特征表达不足返回检查训练集中是否有相似背景的样本。5.3 界面部署时最容易忽略的四个问题第一个问题是图像预处理不一致。训练时做的是先缩放到224x224再做归一化界面端加载图片也必须走同一套预处理管线否则训练时的高准确率在界面上显示不出来。常见做法是把训练脚本里的transforms.Compose抽成公共函数训练和推理共用一份代码而不是在界面文件里重新写一遍。第二个问题是线程阻塞。Tkinter主循环是单线程的如果在主线程里直接执行模型推理加载图片和处理期间窗口会无响应表现为“假死”。常见做法是点击按钮后先禁用该按钮用threading.Thread启动推理线程推理完成后通过队列或after方法把结果传回主线程更新界面。第三个问题是设备兼容。训练时如果用了GPU保存的state_dict键名里会带有cuda字样在无GPU机器上直接加载会报RuntimeError: Attempting to deserialize object on a CUDA device。map_locationdevice配合device cuda if torch.cuda.is_available() else cpu可以解决这个跨设备问题。第四个问题是中文路径。压缩包解压后如果路径含有中文PyTorch在某些Windows环境下读取数据集会报错这与Python默认编码有关。项目说明里着重强调了解压路径不能包含中文实际上训练时最好把整个项目放在纯英文路径下包括用户名为中文的情况就是C:\Users\张三\这种路径也会出问题建议放到D:\projects\fruit_veg这种纯英文位置的目录。如果做完上面这四步模型在界面上的识别表现基本能和评估曲线反映的水平对齐这时候整个项目从数据到训练到推理的闭环就算真正打通了。本文还有配套的精品资源点击获取