简介这套Python基于深度学习CNN的水果识别系统是面向计算机相关专业毕业设计、期末大作业及项目实战学习者的完整源码包。项目经导师指导并评定为98分源码均通过本地编译和严格调试可直接运行。资源包共2000个文件大小约114.65MB主要包含C语言底层源码875个.h和813个.c、Python脚本30个.py、HTML页面188个.html以及Markdown文档、PDF和答辩PPT覆盖数据准备、模型训练、结果展示等环节目录结构清晰便于定位和二次开发。配套答辩PPT可帮助快速梳理研究思路和答辩要点Shell脚本与工程配置则降低了环境搭建门槛。目前已有149人学习下载适合需要高质量参考项目以支撑毕设、课程设计或论文实践的学生是一套即拿即用的可运行方案。1. Python CNN 水果识别这套毕业设计源码到底能帮你解决什么问题用 Python 写深度学习 CNN 水果识别最怕的不是模型调不出来而是调出来之后答辩现场翻车。这套毕业设计级别的项目源码正好卡在这个需求点上基于卷积神经网络的水果识别系统代码路径清晰、数据集组织规范训练、验证、预测一条线都能跑通还带一份答辩 PPT适合正在做课程设计、期末大作业的学生直接复现也适合想学 CNN 实战流程的初学者拿来当模板。它不是只给核心代码的残缺资源而是按“能跑、能调、能讲”来组织的完整项目。整条链路从数据预处理、卷积特征提取到模型训练与权重保存再到单张图片预测展示每一段都能在本地跑起来。下面从环境搭建开始拆开讲哪里容易卡住我会直接指出来。2. 环境与项目结构先把依赖定死再看源码包里哪些文件碰不得拿到源码别急着打开 train.py 直接跑。我拆别人的项目有个习惯先锁环境再理文件结构。Python 依赖这东西版本差一个数跑起来就是两种结果。之前见过有人用 Python 3.12 跑 torchvision 老代码一顿操作下来全是弃用警告最后在 Dataloader 上报错。花十分钟把环境固定住后面会省掉大量排查时间。2.1 Python 环境与依赖安装版本选型与安装步骤先说我这几年在 Windows 上跑这类项目的固定组合Python 3.9 PyTorch 2.x CPU 版 torchvision 对应版本。水果识别是小数据集任务CPU 跑 30 个 epoch 也就是十几到二十几分钟答辩演示完全够用。如果电脑有 N 卡后面可以换 CUDA 版但第一步别折腾驱动和 CUDA 工具包先把 CPU 流程跑通确认代码本身没问题再考虑要不要上 GPU。conda create -n fruit_cnn python3.9 -y conda activate fruit_cnn pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install matplotlib pillow numpy opencv-python第一行创建虚拟环境名字叫 fruit_cnnPython 定在 3.9。不用 conda 的话用python -m venv fruit_cnn效果类似但 conda 在 Windows 上处理包依赖更省心尤其是后面要装不同版本的 torch。安装 torch 时指定--index-url是为了拿 CPU 轮子避免默认拉一个几个 GB 的 CUDA 版本确定要 GPU 的话去掉这行最后的 cpu换成官方对应 CUDA 版本的源。后面的 matplotlib、Pillow、numpy、opencv-python 分别是画图、读图、数值计算和图像处理的常用库项目里基本都用到一次性装齐。装完验证一下版本是否匹配这一步能提前发现 torch 和 torchvision 版本不对应的问题python -c import torch, torchvision; print(torch.__version__, torchvision.__version__); print(CUDA:, torch.cuda.is_available())能正常输出两个版本号就说明环境通了。CUDA: False是正常的因为我们装的是 CPU 版。用 VSCode 打开项目根目录时按 CtrlShiftP 选择 fruit_cnn 解释器这样终端里直接敲 python 走的就是虚拟环境不会出现“conda 里装好的包在 VSCode 里 import 不到”这种经典问题。注意项目路径不要带中文和空格比如D:\毕业设计\水果识别系统这种路径在部分 torchvision 版本里读写缓存会出乱码建议放到D:\fruit_cnn_project这种纯英文路径下。2.2 源码包里那批 C 文件是什么为什么它们不影响 Python 主流程解压源码包之后你会看到一批 .py 文件同时在根目录或某个子文件夹里躺着几个很突兀的 C 文件fruit_data.c、ff.c、cc936.c、cc949.c、cc950.c、cc932.c、sockets.c、mib2.c、httpd.c。第一次接触这类资源的同学看到这些 C 文件容易慌以为代码不完整或者要交叉编译。先给个定心丸这批 C 文件不是 Python 识别主流程的一部分不需要编译也不需要管它。文件来源与作用处理方式fruit_data.c从命名看是数据采集或打包相关的 C 实现忽略ff.cFatFs 文件系统核心实现常用于嵌入式存储忽略cc936.c / cc932.c / cc949.c / cc950.cFatFs 的编码页转换文件分别对应简中、日文、韩文、繁中忽略sockets.clwIP 协议栈的套接字层实现忽略mib2.cSNMP MIB-2 库网络管理相关忽略httpd.c嵌入式 HTTP 服务器实现忽略这批文件放在一起基本可以判断资源里的某些模块是从嵌入式平台上搬过来的——大概率是带文件系统FatFs和网络功能lwIP HTTP Server的采集端工程打包时把底层源码一起带了进来。对 PyTorch 的 CNN 水果识别流程而言它们就是旁边堆着的建材不影响你盖房子。你要关注的是 .py 文件、权重 .pth 文件和数据目录。如果压缩包里带了 data 或 dataset 文件夹下一步先确认它的目录组织方式因为 torchvision 的 ImageFolder 对目录结构有非常明确的约定。3. 数据准备从一堆水果图片到 CNN 能直接吃的张量CNN 吃的是张量不是一张张 JPEG。水果图片在进入网络之前必须经过读入、缩放、转张量、归一化这几道工序而数据集的目录结构直接决定你用 ImageFolder 还是手动写 Dataset。这套源码的数据组织方式遵循 torchvision 标准套路照着做能少踩很多坑。很多初学者在这一步翻车不是因为模型写错而是图片根本没有被正确加载。3.1 目录结构规范ImageFolder 的隐性要求ImageFolder 的规则很死板root 下每个子文件夹是一个类别文件夹名就是类别名。做毕业设计一般 5~10 个类别就够比如 apple、banana、orange、grape、pear。文件夹命名不要用中文不要有空格用英文小写加下划线最稳因为后面保存 .pth 时会把类别名写进 checkpoint中文路径在 Windows 上容易出编码问题。dataset/ ├── train/ │ ├── apple/ │ │ ├── apple_001.jpg │ │ ├── apple_002.jpg │ │ └── ... │ ├── banana/ │ │ └── ... │ └── orange/ │ └── ... └── val/ ├── apple/ │ └── ... └── banana/ └── ...train 和 val 分开是最理想的组织方式。如果资源只给了 train 目录也可以手动按比例抽一部分当验证集。每个类别的图片数量尽量均衡至少每类 50 张以上不然模型容易偏向样本多的类别。Fruits-360 数据集有上百类毕业设计用不到那么多从里面挑几个常见类别做成子集就行自己拍照采集的话注意光线和背景差异别只在同一个桌面拍的图否则训练集和真实场景差距太大。3.2 数据增强与归一化参数怎么设、为什么这么设数据增强是水果识别这种小数据集任务最有效的“免费增广”手段。我的常用配置是 Resize 到 224 乘 224加随机翻转、随机旋转和颜色抖动最后归一化用 ImageNet 的均值方差。这里每个操作都有明确目的不是随手抄的。from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset datasets.ImageFolder(rootdataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootdataset/val, transformval_transform)操作参数作用Resize(224, 224)统一输入尺寸ResNet 系网络默认是 224RandomHorizontalFlip概率默认 0.5随机水平翻转等于把样本量翻倍RandomRotation15 度模拟水果摆放角度差异ColorJitter亮度/对比度/饱和度各 0.2让模型不依赖特定颜色分布ToTensor无PIL 图片转成 0~1 的 float32 张量Normalizemean / std 用 ImageNet 常数把输入分布拉到接近零均值收敛更稳归一化参数看起来是玄学其实道理很直接RGB 值范围是 0~255ToTensor 压到 0~1 之后如果不减均值不除方差网络第一层的输入整体偏正梯度更新容易震荡。ImageNet 的 mean 和 std 是一整套经过大规模数据集验证过的统计值直接用没问题。验证集 transform 不需要增强只做 Resize、ToTensor、Normalize保证评估结果反映真实水平。3.3 DataLoader 与数据集划分batch、num_workers 和随机抽样如果资源没有提供独立验证集用 random_split 从训练集里扣 20% 出来当验证集。这里有个细节random_split 的划分不是按类别均匀抽的如果每个类别样本数量差距大最好自己写按类别比例的切分代码。数据量均衡的话直接用 random_split 就行。from torch.utils.data import DataLoader, random_split val_size int(len(train_dataset) * 0.2) train_size len(train_dataset) - val_size train_subset, val_subset random_split(train_dataset, [train_size, val_size]) train_loader DataLoader(train_subset, batch_size32, shuffleTrue, num_workers2, pin_memoryTrue) val_loader DataLoader(val_subset, batch_size32, shuffleFalse, num_workers2, pin_memoryTrue)batch_size 设 32 在 224 乘 224 输入上比较稳妥显存不够就降到 16。shuffleTrue 只在训练集开验证集保持顺序。num_workers2 在 Linux 上能加速数据读取Windows 上却经常触发 BrokenPipeError遇到这个报错就把它改成 0。pin_memoryTrue 在 GPU 训练时有加速效果CPU 训练不影响结果。训练前花十秒检查一下增强后的 batch比闷头跑一个 epoch 再发现问题要快得多。import matplotlib.pyplot as plt import torchvision def show_batch(images, labels, class_names): grid torchvision.utils.make_grid(images[:9], nrow3) plt.figure(figsize(8, 8)) # make_grid 输出是 (C, H, W)matplotlib 需要 (H, W, C)要转置 plt.imshow(grid.permute(1, 2, 0).numpy()) plt.title( | .join([class_names[i] for i in labels[:9].tolist()])) plt.axis(off) plt.show()这段代码把增强后的前 9 张图拼成 3 乘 3 网格显示标题列出真实标签。看到图片方向、颜色、内容都正常再进入模型训练。归一化后的图片显示出来会偏暗偏灰这是 Normalize 导致的属正常现象。4. 模型搭建与训练把 CNN 跑起来并保存权重模型是整个系统的骨架。CNN 的核心逻辑三句话能讲完卷积核在图上滑动提取局部纹理和边缘特征池化把特征图缩小保留主要响应最后把二维特征压平交给全连接层做分类。答辩时老师大概率会问“为什么用 CNN 不用全连接”——答案是参数共享和局部连接这两个概念在卷积层代码注释里就能对上。4.1 轻量 CNN 网络结构自定义卷积网络怎么搭这套项目用自定义 CNN 是合理的因为水果识别任务不算复杂不需要很深的大模型课堂讲起来也更好解释。下面这个结构是三段卷积加两层全连接规模适中CPU 就能训练。import torch.nn as nn class FruitCNN(nn.Module): def __init__(self, num_classes5): 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))层输出尺寸作用Conv2d(3, 32)(32, 224, 224)提取低层边缘和颜色纹理MaxPool2d(2)(32, 112, 112)降采样减小特征图尺寸Conv2d(32, 64)(64, 112, 112)提取组合纹理特征Conv2d(64, 128)(128, 56, 56)提取更抽象的特征AdaptiveAvgPool2d(1)(128, 1, 1)把任意尺寸特征压到 1 乘 1Linear(256, num_classes)(num_classes,)输出每个类别的 logits通道数从 32 翻倍到 128这是因为越靠后的特征图空间分辨率越小需要更多通道来承载语义信息。每个卷积层后面都接 BatchNorm2d它能让每层输入分布稳定训练收敛更快对样本量不大的水果数据集尤其重要。最后用 AdaptiveAvgPool2d(1) 而不是 Flatten 接全连接好处是网络不依赖输入尺寸答辩时老师问起来也有得讲。Dropout(0.5) 在全连接之前随机丢弃一半神经元防止模型把训练集的背景纹理背下来。4.2 损失函数、优化器与学习率超参设置的依据分类问题最常用 CrossEntropyLossPyTorch 里它已经内置了 softmax不需要在网络最后再接一层激活。优化器选 Adam它对学习率不那么敏感毕业设计场景不需要去调 SGD 的 momentum 和 nesterovweight_decay 设 1e-4 相当于给权重加 L2 约束对抗过拟合。学习率 0.001 起步是所有 Adam 用户的默认经验值loss 震荡就降到 0.0003。import torch model FruitCNN(num_classeslen(train_dataset.classes)) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001, weight_decay1e-4)num_classes必须从数据集里动态取不要手写死。这里len(train_dataset.classes)就是目录里类别文件夹的数量写死的话换数据集就会维度对不上。CrossEntropyLoss 要求 logits 和 labels 都是 LongTensorDataLoader 输出的 label 默认就是这个类型不用额外转换。4.3 训练主循环与模型保存每轮输出什么、存什么训练循环的写法有很多种核心是五个步骤前向传播、算损失、梯度清零、反向传播、更新参数。下面这个版本是最直白的 for 循环每轮结束打印 loss 和准确率方便在控制台实时盯训练状态。EPOCHS 30 best_acc 0.0 for epoch in range(EPOCHS): model.train() running_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_loss running_loss / total train_acc correct / total print(fepoch {epoch 1:02d} | loss {train_loss:.4f} | acc {train_acc:.4f}) torch.save({ model_state_dict: model.state_dict(), class_names: train_dataset.classes, num_classes: len(train_dataset.classes), }, fruit_cnn.pth)model.train()必须写在训练循环开头它把 Dropout 和 BatchNorm 切到训练模式对应地验证时要用model.eval()否则 Dropout 在推理时也会随机丢神经元结果不稳定。保存时用字典格式把权重、类别名、类别数一起打包这是比单独存权重更稳的做法——加载时不需要猜测原来用了多少类。每轮打印的 loss 和 acc 都要看loss 下降、acc 上升是正常如果 acc 到了 95% 以上但 loss 还在缓慢下降说明模型还在拟合训练集的细节可以提前停。4.4 迁移学习兜底数据量不够时换 ResNet18如果自己采集的数据每类只有几十张从头训自定义 CNN 很容易过拟合验证集准确率上不去。果识别任务不一定要自己把卷积网络从零训出来常见做法是直接用预训练好的 ResNet18替换最后一层全连接。ImageNet 上训过的骨干网络已经学会了通用纹理和形状特征你只需要微调后面的分类头。import torchvision.models as models def build_resnet(num_classes): model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.fc nn.Linear(model.fc.in_features, num_classes) return modelweightsmodels.ResNet18_Weights.DEFAULT是 torchvision 新版本的推荐写法老版本写pretrainedTrue两个都行选哪个取决于 torchvision 版本。model.fc.in_features是 ResNet18 最后一个全连接层的输入维度固定是 512换成这个写法就不用记数字。迁移学习训练时学习率可以降到 0.0003因为骨干网络已经收敛过了学习率太大会把预训练权重冲坏。5. 训练与复现中的常见问题排查五次翻车换来的五条经验这部分是拆项目时最想直接写给你的内容。源码能跑不代表你本地能一次跑通数据、环境、路径、设备任何一环出问题都会让你以为是模型写错了。以下五条都是实际踩过的坑每条按“现象、原因、解决”讲清楚遇到同类问题能直接对照处理。5.1 数据与训练阶段Loss 不降、进程崩溃和维度报错第一条Loss 卡住不动acc 一直在 20% 左右。现象是训练几轮之后 loss 稳定在 2.3 附近准确率低得跟随机猜一样。原因九成是归一化没写对——忘了 Normalize或者数据增强里只有 Resize 和 ToTensor输入分布完全偏在正区间梯度更新被带偏。另一个常见原因是学习率设成 0.01对 Adam 来说这个值太大参数在最优值附近来回震荡。解决办法先把 transform 改成 3.2 节的标准配置确认transforms.ToTensor()和transforms.Normalize都在学习率降到 0.001。改完这两个地方loss 通常一两个 epoch 就开始明显下降。第二条Windows 上报 BrokenPipeError。现象是第一个 epoch 跑了一半或者刚启动训练控制台刷出一堆红字里面提到BrokenPipeError和dataloader worker。原因是 Windows 的 DataLoader 多进程机制和 Linux 不一样子进程初始化阶段在主进程推进时容易崩。解决办法最直接把num_workers改成 0让数据加载在住进程内完成。训练脚本入口外面再包一层if __name__ __main__:防止 Jupyter 或 IDE 里重复递归启动进程。这个问题和模型没关系改完环境设置就安静了。第三条报mat1 and mat2 shapes cannot be multiplied。现象是训练第一步前向传播就能报错提示两个矩阵维度对不上。原因很明确模型最后一层Linear(256, num_classes)的 num_classes 和数据集实际类别文件夹数量不一致或者数据集里有一个空文件夹导致了多余类别。解决办法打印train_dataset.classes看一眼实际类别列表确认文件夹里没有损坏或空的目录构造模型时用len(train_dataset.classes)动态传入。这里还有一个隐藏情况random_split切分之后报维度错误通常是类别文件夹里有非图片文件ImageFolder 把隐藏文件也算进去了。5.2 保存、加载与演示阶段权重文件与预测前处理第四条加载 .pth 报Unexpected key(s)或Missing key(s)。现象是load_state_dict抛出异常提示找不到某些层的权重。原因基本是保存和加载的方式不对称有人保存了完整的model对象有人保存了包含 epoch、optimizer 的完整 checkpoint加载时直接往新的 model 里塞key 自然对不上。解决办法保存和加载统一用state_dict。加载时要看 checkpoint 里有没有model_state_dict字段如果有就取这个字段再加载重建模型时传入的 num_classes 必须和保存时一致这是最容易忽略的点。checkpoint torch.load(fruit_cnn.pth, map_locationcpu) model FruitCNN(num_classescheckpoint[num_classes]) model.load_state_dict(checkpoint[model_state_dict])map_locationcpu可以让 GPU 上保存的权重在纯 CPU 机器上加载答辩用笔记本演示时很实用。加载完顺手打印一下checkpoint[class_names]确认类别顺序和你的预期一致——类别顺序由训练时的目录名决定和文件夹排序有关不能凭记忆猜。第五条训练集准确率 95%单独预测一张图却完全识别错。现象是验证集指标很漂亮但拿一张手机拍的苹果图片走预测脚本结果输出一个完全不相干的类别。原因几乎每次都是预测时没有复用训练时的预处理流水线有人用 OpenCV 读图得到的是 BGR 三通道直接喂给在 RGB 图片上训练的模型有人忘记 Resize图片原始尺寸直接进网络还有人没做 Normalize模型看到的输入分布跟训练时完全两回事。解决办法是把 transform 定义成一个公共对象训练脚本和预测脚本都 import 它而不是在预测文件里再写一份。from PIL import Image def preprocess(image_path): img Image.open(image_path).convert(RGB) return val_transform(img).unsqueeze(0)convert(RGB)是为了把可能带透明通道的 PNG、或者是灰度图统一转成三通道 RGB避免模型输入维度报错。预测前先打印一下预处理后的张量尺寸应该是torch.Size([1, 3, 224, 224])这个形状不对后面的结果都不可信。这五条坑有一个共性绝大多数翻车都发生在数据流和状态切换上而不是卷积网络本身。6. 把模型变成答辩能用的识别工具预测脚本与可视化模型训完最后一步是把它变成答辩现场能点开的识别工具。见过太多同学拿训练 acc 说事结果现场加载模型识别一张苹果图弹出的标签却是梨。问题不在模型而在预测脚本没有复用训练时的预处理流水线。训练指标再漂亮都不如演示时一次正确识别有说服力。6.1 单张图片预测封装一个不走样的识别函数预测脚本的核心是把预处理、前向传播和结果解析封装成一个函数训练用哪个 transform预测就用哪个不要写成两份。下面这个函数直接用之前定义好的val_transform保证前后一致。import torch def predict_one(model, image_path, devicecpu): model.eval() img Image.open(image_path).convert(RGB) x val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1) top1 torch.argmax(prob, dim1).item() return checkpoint[class_names][top1], prob[0][top1].item()model.eval()把 Dropout 关掉BatchNorm 切换到用全局统计量这一步漏掉的话同一张图每次预测结果都可能不一样。unsqueeze(0)是在 batch 维度上补一个 1因为模型期望的输入是四维张量(batch, channel, height, width)。torch.no_grad()告诉 PyTorch 不需要保存梯度计算图推理时能省不少内存。softmax把 logits 转成加和为 1 的概率top1取最大概率对应的下标再用保存的class_names映射回水果名。这个函数的返回值是(类别名, 置信度)置信度低于 0.5 的时候不要硬解释为“识别成功”建议在界面上提示用户重新换一张角度更清晰的照片。模型对没见过的新品种水果给出低置信度是正常行为设计演示流程时把这个边界考虑进去。6.2 用可视化柱状图增强答辩演示的置信度展示只打印一个字符串标签答辩现场的效果远远不够。把模型对每个类别的概率画成横向柱状图一眼就能看出模型不只是猜对而是真的在多个类别之间做了比较。import matplotlib.pyplot as plt def show_topk(class_names, prob, k3): topk torch.topk(prob, kmin(k, len(class_names))) names [class_names[i] for i in topk.indices.tolist()] scores [float(v) for v in topk.values.tolist()] plt.figure(figsize(6, 4)) plt.barh(names[::-1], scores[::-1]) plt.xlabel(probability) plt.tight_layout() plt.show()torch.topk返回概率最高的前 k 个类别[::-1]反转顺序是为了让柱状图从上到下按概率从高到低排列。实际演示时我会先调用predict_one拿到 top1再调用show_topk展示前三名的概率分布这一套流程在答辩现场非常有说服力——老师看到的不只是一个结果而是模型内部的“思考过程”。从那以后我每次做完识别模型都会强制自己走一遍从原始图片到预测结果的完整脚本不拿训练集里任何一张图而是现场拍一张或者从网上下载一张不同角度、不同光照的水果图。这个习惯帮我拦下了好几次现场翻车。希望帮到你。本文还有配套的精品资源点击获取