☰
基于CNN的水果识别系统:PyTorch课程设计完整方案
2026/9/28 16:09:48 网站建设 项目流程

简介:一份基于深度学习CNN的水果识别系统毕业设计项目,适合计算机相关专业学生用于期末大作业、毕业设计或项目实战练习。项目曾获导师认可,评审分98分,代码经过本地编译与严格调试,可直接运行,内容难度适中,并已通过助教老师审定。压缩包共2000个文件,约114.65MB,类型最多的是C语言源文件与头文件,同时包含Python脚本、HTML页面、Markdown与TXT说明文档、PDF资料和答辩PPT,目录结构清晰,便于查阅。目前已有149人浏览学习。整个资源覆盖CNN模型构建、水果图像数据处理、模型训练评估与结果展示等关键环节;Python脚本中体现了识别系统主流程,HTML文件可用于可视化界面参考,答辩PPT则能帮助快速梳理项目背景、技术路线和答辩要点。获取后既可直接部署运行,也可作为二次开发与论文写作的参照。

1. 用CNN做水果识别系统:课程设计里最“稳”的深度学习选题

临近答辩,你会发现一个规律:同组的同学有的在做目标检测、有的在跑Transformer,真正能在现场流畅演示、被提问时不翻车的,反而是那些把“深度学习CNN”这条线走扎实的项目。这个Python水果识别系统就属于这一类——它不追求模型结构多新,而是把“数据准备—模型训练—界面交互—答辩材料”整条链路补齐,正好对应毕设和期末大作业最看重的完整性。它能解决的问题很具体:拿到一批水果图片,训练一个能区分苹果、香蕉、橘子这类类别的CNN模型,再做一个能上传图片或调用摄像头实时识别的图形界面,最后把整个技术栈讲成一场逻辑自洽的答辩汇报。适合谁?刚入门深度学习、手里算力有限、需要在两到三周内交付一个能跑能讲项目的在校生。这套方案最大的优点是每层都有标准答案,不会让你在选题阶段就卡住。

2. 系统架构与模型选型:先想清楚这两层再动手写代码

2.1 三层架构:数据层、模型层、界面层各管什么

很多同学拿到这类源码包,第一反应是直接跑训练脚本,跑通就以为完事了。但答辩老师问的第一个问题往往是“你这个系统的框架是怎么设计的”。所以动手之前,先把系统的三个层次在脑子里立起来。数据层负责把原始图片整理成模型能吃的样子,包括目录拆分、尺寸统一、归一化和数据增强;模型层是核心,用卷积神经网络做特征提取和分类,输出每个类别的置信度;界面层解决“别人怎么用”的问题,常见做法是用Tkinter或PyQt写一个窗口,加上“选择图片”“开始识别”“摄像头识别”几个按钮。这三层在代码里要尽量解耦,模型文件单独放,界面文件单独放,训练脚本单独放,答辩时老师问“你的模型和界面怎么对接的”,你就能直接回答通过一个加载模型的文件路径完成推理调用。

我一般会在项目根目录下建四个文件夹:

  • data/存放原始数据集和划分好的训练集、验证集
  • models/存放训练好的权重文件
  • src/存放Python源码,包括训练脚本、推理脚本、界面脚本
  • ppt/存放答辩PPT和相关图表

这样的目录结构本身就是答辩材料的一部分,老师看到的是一个工程化的项目,不是一个单文件脚本。另外把requirements.txt放在根目录,写清torch、torchvision、opencv-python、numpy、Pillow这几个核心依赖,评阅老师要复现你的项目时,不用去猜环境怎么配。

2.2 为什么选CNN而不是手工特征或迁移学习

直接说结论:水果识别这个任务,CNN是性价比最高的选择。传统的手工特征方案,比如提取颜色直方图、纹理特征再喂给SVM,确实在小数据集上能跑,但你得花大量时间调特征组合,而且泛化能力很差——换一批拍摄环境不同的图片,准确率立刻掉下来。CNN通过卷积核自动学习颜色、边缘、形状的分层特征,前几层学到的往往是通用的边缘纹理,后几层才能组合出“苹果的轮廓”“香蕉的弯度”这类高层语义,这正是图像分类任务需要的表达能力。

那为什么不一定非得上ResNet、VGG这种深层网络?因为毕业设计的数据集通常只有几千张图,算力也多是CPU或者入门级显卡,深层网络训练时间长、调参难度大,而且在这个数据量下精度优势并不明显。这里说一个排错时的参考:如果你的训练集每类只有一两百张图,更合理的路线是使用在ImageNet上预训练过的ResNet18,只把最后一层全连接改成自己的类别数,用较小的学习率进行微调;如果你的数据量能到每类五百张以上,可以尝试从头训练一个三到四层卷积的小型CNN。这两条路线在这个项目里都有人走通,选哪条取决于你的数据集规模和训练时间预算。实际写代码时的常见选择是:训练脚本里保留两个模型定义,一个SimpleCNN用于自定义小型网络,一个ResNet18用于迁移学习,通过一个命令行参数切换,这样答辩时可以现场对比两类方法的效果。

3. 水果数据集准备:从采集到DataLoader的完整流程

3.1 数据集结构设计与标签编码

数据集是整个项目的命根子,也是第一个容易翻车的地方。很多网上流传的水果数据集存在图片尺寸不统一、部分图片带水印或背景杂乱的问题。拿到数据后第一件事不是写模型,而是把数据集按train/val/test三个目录重新组织,每个类别一个子文件夹。PyTorch的torchvision.datasets.ImageFolder要求的就是这种目录结构,它会自动把子文件夹的名字解析成类别标签,省掉手写标签映射的麻烦。目录结构如下:

data/ train/ apple/ banana/ orange/ ... val/ apple/ banana/ orange/ ... test/ apple/ banana/ orange/ ...

训练集和验证集的拆分比例我一般用8:2,测试集单独保留,不要混进训练过程。有一个容易忽略的细节:类别文件夹名不要用中文,虽然代码层面能处理,但答辩时在另一台机器上复现,Windows系统上中文路径偶尔会引发编码异常,这种问题排查起来非常浪费时间。用apple、banana这样的英文名,最后在界面层做一个英文标签到中文显示名称的映射字典就行,比如{"apple": "苹果", "banana": "香蕉"},这个映射表放在一个独立的labels.json里,答辩演示时界面显示中文水果名,代码内部用英文标签,两边不冲突。

3.2 数据增强的参数设置与代码实现

数据集整理好后,加载部分的代码决定了模型能不能学到有用的特征。下面这段是项目里最常见的数据加载写法,包含训练集的数据增强和验证集的简单预处理。训练集做了随机水平翻转、随机旋转和归一化,验证集只做尺寸统一和归一化,不参与任何随机增强,这样才能客观评估模型在稳定输入下的表现。

import torch from torchvision import datasets, transforms # 训练集增强:适度,不要过度 train_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转,提升泛化 transforms.RandomRotation(degrees=15), # 旋转范围控制在15度内,避免破坏形状特征 transforms.ToTensor(), # 把PIL图片转成Tensor,像素值从0-255缩放到0-1 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集:只做尺寸统一和归一化 val_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder(root="data/train", transform=train_transforms) val_dataset = datasets.ImageFolder(root="data/val", transform=val_transforms) train_loader = torch.utils.data.DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=2 ) val_loader = torch.utils.data.DataLoader( val_dataset, batch_size=32, shuffle=False, num_workers=2 )

参数设置上有三个点值得细说。第一,Resize((224, 224))是标准输入尺寸,和ImageNet预训练模型的默认输入一致。如果你的界面里要支持上传任意大小的图片,需要在推理脚本里同样做一次224的resize,否则模型输入维度不匹配。第二,RandomRotation(degrees=15)这个参数很多人喜欢调大,认为旋转越多增强越强,但对水果识别来说属于过度操作,转45度以上会让“苹果还是苹果”这个人类都能看错,模型更学不到稳定的形状特征,精度反而下降。第三,Normalize的均值和标准差用的是ImageNet的标准值,因为大多数预训练模型是基于ImageNet训练的,沿用这套参数能让输入分布和预训练时的分布保持一致,微调效果更好。如果是从头训练的小型CNN,也可以直接用mean=0.5, std=0.5做简单归一化,差别不大。

这里还有一个小技巧:把batch_size设成32,num_workers设成2,在大部分学生电脑上内存占用和加载速度都适中。如果训练时提示内存不足,优先把num_workers减到0(在Windows上0是必须的,否则会报BrokenPipeError),再考虑调小batch_size。

4. PyTorch训练CNN水果分类模型:核心代码与参数调整

4.1 模型定义:ResNet18微调还是自定义小型CNN

数据准备就绪后,进入整个项目最核心的部分——模型训练。这里给出两种模型定义方式,你在写代码前先想清楚自己的算力条件,再选择其中一种作为主模型,另一种作为答辩时的对比模型。

第一种是用ResNet18做迁移学习。这种做法的思路是把在ImageNet上已经学会识别通用特征的网络拿过来,只替换最后的分类头。实现代码如下:

import torch import torch.nn as nn import torch.optim as optim from torchvision import models # 加载预训练的ResNet18 model = models.resnet18(pretrained=True) # 冻结前面的特征提取层,只训练最后的全连接层 for param in model.parameters(): param.requires_grad = False # 替换最后一层全连接,输出类别数为num_classes num_classes = 5 model.fc = nn.Linear(model.fc.in_features, num_classes) # 让最后一层可训练 for param in model.fc.parameters(): param.requires_grad = True

这段代码的逻辑是先冻结全部参数,再单独放开最后一层,这样训练时只更新最后一层全连接的权重,参数量小、收敛快、不容易过拟合。代价是特征提取部分完全沿用ImageNet学到的知识,如果水果类别在ImageNet里没有太多相似样本,精度会受限制。一种折中方案是model.fc换成两层的小分类头,中间加一个ReLU激活和一个Dropout,把requires_grad放开给最后两层,让分类头有更强的表达能力。如果你想更进一步,可以把model.layer4也解冻,用较低的学习率微调,但这需要更多的训练轮次和更大的显存。

第二种是自定义小型CNN,适合数据量大、想展示“从零训练”过程的场景。一个三层卷积的网络结构如下:

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes): super(SimpleCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x

输入图片是224x224,经过三次卷积加池化,特征图从224降到28,通道数从3升到128,最后全连接层输出每个类别的logits。这里的Dropout(p=0.5)是防过拟合的关键,训练时随机丢弃一半神经元,让网络不能过度依赖某几个节点的输出,推理时Dropout自动关闭,所有神经元一起参与计算。答辩时如果你的训练集不大,这个设计的价值很容易讲清楚。

4.2 训练脚本的编写与超参数选择

模型定义完之后,训练脚本是整套代码最容易“改坏”的地方。下面给出一个完整的最小训练流程,包含了损失函数、优化器、轮次循环和模型保存:

import torch import torch.nn as nn import torch.optim as optim from tqdm import tqdm device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) num_epochs = 25 best_val_acc = 0 for epoch in range(num_epochs): # 训练阶段 model.train() train_loss = 0.0 train_correct = 0 train_total = 0 for inputs, labels in tqdm(train_loader): inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) train_total += labels.size(0) train_correct += (predicted == labels).sum().item() # 验证阶段 model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_acc = val_correct / val_total train_acc = train_correct / train_total scheduler.step() # 保存验证集上效果最好的模型 if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "models/best_model.pt") print(f"Epoch {epoch+1}/{num_epochs} | " f"Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f} | " f"Loss: {train_loss/train_total:.4f}")

有几个参数值得展开讲。CrossEntropyLoss在PyTorch里已经内置了Softmax,所以模型最后一层输出的是裸的logits,不需要手动再套一层Softmax。Adam优化器初始学习率1e-3是通用选择,比SGD的收敛速度快、调参压力小,但缺点是后期容易在最优值附近震荡,所以配合了StepLR每10个epoch把学习率降到原来的1/10,让后期收敛更稳。如果你用的是从头训练的小型CNN,学习率也可以从1e-3起步;如果用ResNet微调,建议把学习率调低到1e-4,因为预训练权重已经在一个较好的位置,学习率太大会把学好的参数冲乱。

轮次设计上,num_epochs=25配合StepLR在15轮左右做第一次学习率衰减,是一个比较稳妥的组合。整个训练完成需要多久不好打包票,但你可以这样判断进度:如果训练集准确率持续上升、验证集准确率在10轮后不再变化,说明模型已经收敛,可以提前停。反之如果训练集和验证集准确率差距越来越大,说明过拟合在加剧,此时不要盲目加轮次,应该减少轮次或增强数据增强。这里还有一个小习惯:在验证阶段一定要写model.eval(),否则模型中的Dropout仍然生效,验证准确率会随机波动,这个坑几乎每个做该方向的人都会踩一次,表现为两次连续验证的准确率忽高忽低。另外把torch.max(outputs, 1)取预测结果,返回的是概率最大的索引和对应的值,索引直接对应dataset.classes里的类别顺序,输出打印时用dataset.classes[predicted]就能显示类别名。

5. 水果识别系统避坑指南:5个高频问题的现象、原因与解决

5.1 训练集准确率很高,但识别新图片时一塌糊涂

这是所有做图像分类的初学者都会撞上的第一堵墙。现象是训练过程中准确率一路涨到95%以上,验证集结果也不差,但是拿手机随手拍的水果照片放进系统,识别结果完全不对,甚至报出离谱的类别。

原因大概率是数据集和真实场景的分布不一致——采集的训练图片背景干净、光照均匀、水果位置居中,而手机拍的图片背景杂乱、光线偏暗、水果有遮挡或阴影,模型学到的是“背景干净、光线均匀的图片”这个模式,而不是水果本身。解决分两步:第一步,做推理测试时不要只用测试集图片,至少找十张与训练集风格完全不同的图片(比如网上的美食摄影图、自己拍的实拍图)作为额外的对抗样本;第二步,在训练时加强数据增强,把RandomHorizontalFlip之外再加上ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),让模型适应不同的光照和色彩偏移。要注意的是数据增强不是万能药,如果实拍图与训练图差距太大,最实际的方案是补充真实场景的数据,再重新训练。

5.2 训练Loss不下降或早期就变成NaN

这个坑在换环境跑源码时特别常见。现象是loss值在几十个epoch后依然在初始值附近波动,或者某一步突然变成nan。

原因有两种可能。第一种是学习率设得太高,Adam虽然对学习率相对鲁棒,但如果初始lr=0.1甚至更高,梯度更新步长过大,参数直接发散,loss就会变nan,这情况在自定义的小型CNN上比在ResNet微调上更容易出现。解决方式是把学习率降到1e-3或1e-4,同时给优化器加一个weight_decay=1e-4的正则项,约束参数不要跑太远。第二种原因是输入数据里出现了异常值,比如某些图片解码失败后产生了全黑图或全白图,归一化后仍然方差极大,梯度被这几张图带偏。排查方法是遍历一遍数据集,检查每张图的像素值范围,把损坏文件移出去,一个简单的检查脚本就能定位到具体的坏文件。

5.3 界面打开摄像头时黑屏或闪退

界面部分的代码不少同学是直接复制粘贴的,摄像头功能是最容易出问题的一环。现象是在OpenCV的VideoCapture(0)这一步有时能打开但有延迟,有时直接返回False导致程序闪退。

原因是摄像头索引号在不同机器上不一定都是0,有些笔记本的摄像头被占用、有些外接摄像头索引为1,另外OpenCV在部分Windows环境和Python 3.10以上版本存在兼容问题。我的处理方式是先把摄像头初始化代码单独拉出来调试,不要直接嵌进界面逻辑里。先运行下面这段代码确认摄像头能出图:

import cv2 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("摄像头打开失败,尝试索引1") cap = cv2.VideoCapture(1) ret, frame = cap.read() print("读取成功" if ret else "读取失败", frame.shape if ret else "") cap.release()

如果索引0失败,改成1;如果都失败,检查摄像头驱动是否正常、是否被其他软件占用。还有一个细节:在读帧的循环里加一个1到2毫秒的延时(cv2.waitKey(1)),否则画面刷新频率过高导致界面卡死,这在很多界面上表现得像是“系统没响应”。

5.4 Windows下中文标签显示为乱码

Tkinter界面里把模型输出的英文标签映射成中文显示时,经常出现一串类似“???”的乱码。原因不是代码问题,是编码问题——Windows控制台的默认编码是GBK,Python脚本的源文件编码是UTF-8,两者不一致。

解决方式有两种。第一种是界面窗口设置root.title("水果识别系统")之前先执行import tkinter并在文件顶部声明# -*- coding: utf-8 -*-,同时把标签显示部分的字体设置成支持中文的字体,比如font=("Microsoft YaHei", 12)。第二种更省事的方法是标签映射文件labels.json里存中文,代码读取时用json.load指定encoding="utf-8",避免依赖系统默认编码。这里提醒一句,与其把中文硬编码在界面源码里,不如统一放在配置文件里,答辩演示时如果现场机器编码环境不同,只需要改配置文件不用改代码。这个细节能挡住一次现场演示的“翻车”。

5.5 加载模型时报state_dict不匹配

现象是训练完成后运行推理脚本,报错信息类似size mismatch for fc.weight: copying a param with shape torch.Size([5, 512]) from checkpoint, the shape in current model is torch.Size([6, 512])。

原因非常明确:训练时的类别数和推理时模型定义的类别数不一致。比如你训练了5类水果,推理脚本里num_classes=6,或者反过来。还有另一种情况是训练时用了ResNet18,推理时却用的是同一个检查点但加载到了SimpleCNN上。解决方式是在推理脚本里设定num_classes时必须与训练时保持一致,我建议把这个值也写进一个config.json或者写在labels.json里,让训练脚本和推理脚本都从同一个配置文件读取类别数,这样两边永远不会不同步。另外如果保存模型时用的是model.state_dict(),加载时就要先实例化模型再load_state_dict,不要直接给torch.load出来的完整对象,形态对不上会报错。

6. 答辩材料的组织逻辑与最后的验证清单

6.1 答辩PPT的四个模块怎么搭

答辩PPT含金量取决于你能不能把“会做”讲成“懂做”,而不是贴满代码截图。我建议把整个PPT控制在12到15页,分四个模块。第一模块是选题背景和意义,一页就够,重点写清楚“水果识别在农业分拣、零售结算场景中有实际需求,CNN适合解决图像分类问题”。第二模块是技术方案,三到四页,画一张系统架构图,列出数据集规模、模型结构、训练参数,这里的关键是放一张训练过程准确率曲线图,这张图比任何文字都有说服力,答辩老师看到曲线收敛平滑就知道你的实验是真实的。第三模块是系统演示,放界面的截图、识别结果对比表格,现场用摄像头或上传图片演示。第四模块是成果与后续展望,写模型仍有改进空间,比如引入更深的网络结构或在数据增强上继续优化。

每一页PPT上放逻辑主线,不要大段贴不变的内容——代码只在关键技术参数页放关键片段,比如卷积层设置和数据增强部分,其余代码统一放附录。记住答辩的时长一般5到10分钟,把时间留给“为什么选CNN”“数据怎么处理”“训练遇到什么问题以及怎么解决”这三个问题的回答上。最后这个项目有源码和数据集的话,README里要写清楚运行顺序:先安装依赖,再训练模型,最后启动界面。

6.2 交付前自检清单与我的个人习惯

交项目之前,依照标题里“项目源码+答辩PPT”这个交付要求,我每次都会过一遍这个清单:

检查项操作通过标准
环境复现换一台无环境的机器按README操作从pip install到界面启动不超过15分钟
数据路径把所有绝对路径改成相对路径任意目录下python src/main.py能运行
界面验收分别用单张图片和摄像头各测十次识别无崩溃、置信度正常显示
答辩演练按5分钟讲解PPT并演示一次不超时、中间不停顿

还有一个容易被忽视的细节:把训练的随机种子固定,例如torch.manual_seed(42),保证每次训练的关键结果可复现。答辩老师可能会问“你的准确率是多少”,如果你在不同机器上跑出的数字差很多,有理也说不清,固定种子后重新训练的结果基本一致,这个数字才站得住。我个人的习惯是最后再跑一次推理脚本,用验证集整体算一个准确率,再单独挑一张测试集图片打印置信度分布——既能证明模型真实可用,也比只报一个准确率更有说服力。至于PPT里提到的后续改进方向,写成“尝试使用注意力机制提升遮挡场景下的准确率”比“会继续优化”具体得多,也更容易接住老师追问。希望这些经验能帮你在毕业设计或期末大作业里少走几步冤枉路,把时间省下来打磨真正该打磨的东西。

本文还有配套的精品资源,点击获取

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

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

立即咨询