
简介这份资源面向计算机相关专业的本科毕业生及需要完成深度学习课程设计的学生提供一套基于Python与深度学习的水果识别系统完整方案可解决毕业设计选题难、代码跑不通、文档不会写等常见问题。压缩包共277个文件约17.6MB其中8个py文件承载模型训练与识别核心逻辑114个js、26个css与7个html构成前端交互界面另有gif、jpg、png等图片素材及txt、md说明文档整体结构清晰、便于按模块查阅。资源包含源代码、文档说明、数据集与训练好的模型源码均经本地编译验证可运行评审分达95分以上内容经助教老师审定难度适中。目前已有397人学习下载适合希望快速搭建可演示系统、理解深度学习图像分类流程并顺利完成答辩的读者参考使用。1. 从零复现一个水果识别系统这套毕业设计到底能跑出什么结果如果你正在搜「Python毕业设计-基于深度学习的水果识别系统的源代码文档说明数据集模型」大概率不是想听概念而是想知道三件事这东西能不能在我电脑上跑起来、数据集和模型到底长什么样、我改一改能不能当成自己的毕设交上去。我当年第一次做图像分类课设就是被「水果识别」这四个字骗了——以为随便找个 CNN 套上去就行结果卡在数据集划分、类别不均衡、模型过拟合上整整两周。后来把整套流程拆开重做才发现真正决定成败的不是网络多深而是数据怎么组织、训练怎么监控、推理怎么落地。这套方案的核心链路很清晰用 Python 做开发语言用深度学习里的卷积神经网络做特征提取和分类输入一张水果图片输出它属于哪一类苹果、香蕉、橙子、葡萄等。它适合两类人一类是刚入门深度学习、需要一个完整闭环项目练手的同学另一类是要交毕业设计、需要源代码文档数据集模型四件套齐全的人。下面我按「先立住原理、再动手复现、最后讲坑」的顺序把这条链路完整走一遍你照着做就能得到一个能演示、能答辩、能继续改的系统。2. 水果识别系统的技术选型为什么是 CNN 而不是传统方法2.1 传统图像分类在水果场景下为什么容易翻车在深度学习普及之前水果识别常见做法是「手工特征 分类器」先提取颜色直方图、HOG 梯度方向直方图、SIFT 尺度不变特征再喂给 SVM 或随机森林。这套方法在背景干净、光照固定的实验室图片上能跑到 80% 左右但一换场景就崩。原因很直接水果的类间差异有时很小青苹果 vs 青梨类内差异又很大红苹果 vs 青苹果 vs 切开的苹果手工特征根本描述不了这种复杂分布。卷积神经网络解决的就是这个问题。它通过卷积核在图像上滑动自动学习从边缘、纹理到语义部件的层级特征。浅层卷积学到的是颜色斑块和边缘深层卷积学到的是「圆形轮廓 果柄」这种组合模式。你不需要告诉它「苹果是圆的」它自己从数据里学。这也是为什么现在做水果识别默认起点就是 CNN而不是再去调 HOG 参数。常见做法是选一个轻量骨干网络比如 MobileNetV2、ResNet18 或 EfficientNet-B0。它们参数量小、训练快在几千张水果图上就能收敛得不错。如果你追求更高精度且机器够用可以上 ResNet50但要注意过拟合风险会同步上升。2.2 数据集怎么组织目录结构决定你后面少踩多少坑拿到数据集后第一件事不是写模型而是把目录整理成框架能直接读的格式。主流做法是按类别分文件夹每个文件夹放对应水果的图片dataset/ ├── train/ │ ├── apple/ │ ├── banana/ │ ├── orange/ │ ├── grape/ │ └── mango/ ├── val/ │ ├── apple/ │ ├── banana/ │ ├── orange/ │ ├── grape/ │ └── mango/ └── test/ ├── apple/ ├── banana/ ├── orange/ ├── grape/ └── mango/这种结构的好处是PyTorch 的ImageFolder或 TensorFlow 的image_dataset_from_directory可以直接读取不需要你手写标签映射。训练集、验证集、测试集的比例我一般按 7:2:1 切如果数据量少于 2000 张就按 8:1:1把更多数据留给训练。提示切分前一定要先打乱否则如果原始数据是按类别顺序存的直接切会导致某个类别全在验证集里训练时模型根本没见过它。2.3 数据增强小数据集能不能跑出高精度的关键水果数据集通常不大几千张算多的。不做增强模型几轮就过拟合。我常用的增强组合是随机水平翻转、随机旋转 ±15 度、随机裁剪再缩放、颜色抖动亮度、对比度、饱和度微调。这些操作在 PyTorch 里用torchvision.transforms几行就能配好import torchvision.transforms as T train_transform T.Compose([ T.RandomResizedCrop(224, scale(0.8, 1.0)), # 随机裁剪并缩放到224 T.RandomHorizontalFlip(p0.5), # 一半概率水平翻转 T.RandomRotation(15), # 随机旋转±15度 T.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), # 颜色抖动 T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], # ImageNet标准均值 std[0.229, 0.224, 0.225]) # ImageNet标准方差 ]) val_transform T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有几个参数值得说清楚。RandomResizedCrop的scale(0.8, 1.0)表示每次裁出原图 80% 到 100% 的区域再缩放到 224×224模拟不同拍摄距离。RandomRotation(15)是角度上限水果一般不会倒着拍所以不要设太大。ColorJitter的四个值分别控制亮度、对比度、饱和度和色调的扰动幅度hue 我一般不超过 0.05否则苹果可能被调成蓝色反而引入噪声。归一化用的均值和方差是 ImageNet 的统计值因为后面要用预训练权重必须保持一致。验证集和测试集只做 Resize CenterCrop 归一化不能加随机增强否则每次评估结果都在变你没法判断模型到底有没有变好。3. 用迁移学习把模型训起来从加载预训练权重到保存最佳模型3.1 为什么直接训练不如迁移学习稳如果你从随机初始化开始训一个 ResNet18在几千张水果图上大概率收敛到 70% 多就上不去了而且训练时间很长。迁移学习的思路是先用 ImageNet 上训练好的权重初始化骨干网络这些权重已经学会了通用的边缘、纹理、形状特征你只需要让模型微调最后几层去适应水果分类任务。具体做法是替换掉原网络的分类头把输出类别数改成你的水果类别数然后分两组设置学习率骨干网络用小学习率比如 1e-4新加的分类头用大学习率比如 1e-3。这样既保留预训练知识又让新层快速适应。import torch import torch.nn as nn from torchvision import models def build_model(num_classes5, pretrainedTrue): # 加载ResNet18预训练权重 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT if pretrained else None) # 替换最后的全连接层输出类别数改为水果类别数 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), # 防止过拟合 nn.Linear(in_features, num_classes) ) return model model build_model(num_classes5) criterion nn.CrossEntropyLoss() # 分组学习率骨干网络小分类头大 optimizer torch.optim.Adam([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer4.parameters(), lr: 1e-4}, {params: model.layer3.parameters(), lr: 1e-4}, ], weight_decay1e-4)Dropout(0.3)放在全连接层前面训练时随机丢弃 30% 神经元降低过拟合。weight_decay1e-4是 L2 正则进一步约束权重。如果你发现验证集准确率震荡很大可以把分类头学习率降到 5e-4或者加一个学习率调度器比如CosineAnnealingLR。3.2 训练循环里必须监控的三个量训练不是跑完就完事你得盯着三个量训练损失、验证损失、验证准确率。训练损失持续下降但验证损失开始上升就是过拟合信号两个都下降但验证准确率不动可能是学习率太小训练损失震荡剧烈多半是 batch size 太小或学习率太大。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return running_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) running_loss loss.item() * images.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return running_loss / total, correct / totalmodel.train()和model.eval()必须成对出现前者启用 Dropout 和 BatchNorm 的训练行为后者固定它们。torch.no_grad()在验证时关闭梯度计算省显存也提速。每轮训练后比较验证准确率只保存最好的那个模型权重best_acc 0.0 for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(fEpoch {epoch1}: train_loss{train_loss:.4f}, train_acc{train_acc:.4f}, fval_loss{val_loss:.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_fruit_model.pth) print(f - 保存最佳模型验证准确率 {best_acc:.4f})30 轮是我在几千张图上常用的起点如果你数据更少可以加到 50 轮但一定要配合早停连续 5 轮验证准确率不提升就停。3.3 推理脚本怎么写才能直接演示训练完拿到best_fruit_model.pth后你需要一个独立推理脚本输入单张图片输出类别和置信度。这个脚本也是答辩时最常被要求现场跑的东西。from PIL import Image import torch import torch.nn.functional as F from torchvision import transforms CLASS_NAMES [apple, banana, orange, grape, mango] def predict(image_path, model_pathbest_fruit_model.pth): device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classeslen(CLASS_NAMES), pretrainedFalse) model.load_state_dict(torch.load(model_path, map_locationdevice)) model.to(device) model.eval() 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]) ]) image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) # 增加batch维度 with torch.no_grad(): outputs model(tensor) probs F.softmax(outputs, dim1)[0] conf, idx probs.max(0) return CLASS_NAMES[idx.item()], conf.item() if __name__ __main__: label, confidence predict(test.jpg) print(f预测结果{label}置信度{confidence:.4f})unsqueeze(0)是把单张图的[3,224,224]变成[1,3,224,224]因为模型要求输入有 batch 维度。F.softmax把 logits 转成概率方便你看置信度。如果置信度低于 0.6实际演示时最好提示「不确定」而不是硬报一个类别。4. 避坑与排查水果识别项目里最容易翻车的五个地方4.1 训练准确率 99% 但演示时全错现象训练日志里 train_acc 到 0.99val_acc 也有 0.95但拿手机拍一张真实水果照片去预测结果乱七八糟。原因数据集里的图片和真实拍摄图片分布差异太大。公开水果数据集多是白底、正面、光照均匀的图而你演示时拍的是桌面、有阴影、有背景杂物。模型学到的是「白底 圆形」而不是「苹果」。解决训练时加入更多真实场景增强比如随机背景替换、随机遮挡、更强的颜色抖动。如果条件允许自己用手机拍几十张不同角度、不同背景的图混进训练集。推理时如果背景太乱可以先做一次简单的前景分割再送模型。4.2 验证集准确率比训练集还高现象某一轮 train_acc0.82val_acc0.88看起来「反常」。原因大概率是验证集太小或者验证集和训练集有重叠图片。另一个可能是 Dropout 在训练时拉低了训练准确率而验证时关闭了 Dropout所以验证表现更好。解决先检查训练集和验证集有没有同名文件或重复图片用 md5 去重。如果确认没重叠且验证集样本数大于 500那这个现象可以接受继续观察验证损失是否稳定。4.3 训练到一半 loss 变成 nan现象前几轮正常突然 loss 打印出 nan准确率崩到随机水平。原因学习率太大导致梯度爆炸或者数据里有损坏图片全黑、全白、尺寸为 0。也有可能是某张图的归一化后数值异常。解决先把学习率降一个数量级再跑。然后在 Dataset 的__getitem__里加异常捕获遇到打不开的图片就跳过并记录文件名。训练前用脚本扫一遍所有图片检查尺寸和通道数。from PIL import Image import os def check_images(root_dir): bad_files [] for dirpath, _, filenames in os.walk(root_dir): for fname in filenames: if not fname.lower().endswith((.jpg, .jpeg, .png)): continue fpath os.path.join(dirpath, fname) try: img Image.open(fpath) img.verify() # 验证文件完整性 img Image.open(fpath) # verify后需重新打开 if img.size[0] 10 or img.size[1] 10: bad_files.append((fpath, 尺寸过小)) except Exception as e: bad_files.append((fpath, str(e))) return bad_files4.4 类别不均衡导致小类全被吞现象苹果有 800 张芒果只有 120 张训练完模型几乎不预测芒果。原因交叉熵损失默认每类权重相同模型偏向样本多的类就能降低总损失。解决给CrossEntropyLoss传weight参数按类别频率的倒数设置权重。或者用重采样让每个 batch 里各类别数量接近。import numpy as np class_counts np.array([800, 750, 600, 300, 120]) # 各类样本数 class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() * len(class_counts) class_weights torch.FloatTensor(class_weights).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)4.5 换一台机器就加载不了模型现象在自己电脑上保存的.pth文件拷到同学电脑上load_state_dict报 key 不匹配。原因保存时用了torch.save(model, path)保存整个模型对象换环境后类定义路径变了就加载失败。或者保存时模型结构和你加载时定义的结构不一致。解决永远只保存model.state_dict()加载时先实例化相同结构的模型再load_state_dict。如果换了 GPU/CPU加map_location参数。# 保存推荐 torch.save(model.state_dict(), best_fruit_model.pth) # 加载兼容CPU和GPU device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes5, pretrainedFalse) model.load_state_dict(torch.load(best_fruit_model.pth, map_locationdevice)) model.to(device)5. 把准确率再往上推一档三个我反复验证过的技巧第一个技巧是测试时增强TTA。推理时对同一张图做多次变换原图、水平翻转、轻微旋转分别预测后取平均概率。这个操作不需要重新训练代码量很小但在小数据集上通常能涨 1 到 3 个百分点。我一般做 3 到 5 次变换再多收益就递减了。def predict_with_tta(image_path, model, device, n_aug5): image Image.open(image_path).convert(RGB) base 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]) ]) aug transforms.Compose([ transforms.Resize(256), transforms.RandomHorizontalFlip(p1.0), transforms.RandomRotation(10), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) tensors [base(image).unsqueeze(0)] for _ in range(n_aug - 1): tensors.append(aug(image).unsqueeze(0)) batch torch.cat(tensors, dim0).to(device) model.eval() with torch.no_grad(): probs F.softmax(model(batch), dim1).mean(dim0) conf, idx probs.max(0) return CLASS_NAMES[idx.item()], conf.item()第二个技巧是冻结骨干网络先训分类头再解冻微调。很多人一上来就全网络微调结果预训练权重被大梯度冲垮。我一般先冻结layer1到layer4只训fc跑 5 轮让分类头稳定再解冻layer3和layer4用更小的学习率跑 15 轮。这样收敛更稳最终准确率也更高。第三个技巧是保存「最佳模型」而不是「最后一轮模型」。我见过太多人直接拿最后一轮的权重去演示结果比最佳轮次低了 5 个点。训练时每轮验证后比较val_acc只保存最高的那次。如果连续 8 轮不提升就停掉别浪费电。技巧预期收益额外成本适用场景测试时增强 TTA1% ~ 3%推理时间翻倍演示前冲精度先冻结再解冻2% ~ 5%训练轮次增加小数据集保存最佳模型避免 -3% ~ -5%几乎为零所有场景类别权重小类召回 10% 以上需统计类别频率类别不均衡最后说个我自己的习惯每次改完数据增强或学习率我都会把训练日志存成文本文件文件名带上日期和关键参数比如log_20250115_lr1e-3_augv2.txt。这样过一周回头看能清楚知道哪次改动真正起了作用而不是凭感觉调参。水果识别这个方向不难难的是把每个环节的细节都抠到位。希望帮到你。本文还有配套的精品资源点击获取