ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

水果识别毕设实战:CNN模型训练到Flask系统部署全流程

水果识别毕设实战:CNN模型训练到Flask系统部署全流程 简介面向毕业设计与期末大作业场景这套基于Python与深度学习CNN的水果识别系统资源包适合计算机相关专业学生作为完整项目参考。项目曾获评审98分源码均经过本地编译与严格调试可直接运行涵盖数据准备、模型训练、识别测试以及答辩PPT等关键环节能够较完整地支撑课程答辩与毕业设计。压缩包合计2000个文件主要包含大量C/C底层源码与头文件、HTML前端页面、Python脚本、PDF文档、Markdown说明文档及一份答辩PPT压缩包大小约114.65MB目录分级清晰便于按模块查阅。目前已有149人学习下载。读者通过学习可掌握CNN图像分类的完整实现流程包括数据预处理、网络结构搭建、模型训练与评估调优同时也可参考项目中的答辩材料与工程组织方式快速迁移到其他视觉识别任务中节省从零搭建的时间。1. 水果识别毕设为什么选 CNN一个能演示、能讲深、能扩展的方向水果识别系统这几年几乎成了毕设和期末大作业的标配题目原因很简单它直观、数据好找而且 CNN 分类器在 PyTorch 里几十行就能跑起来。但这个题目同时也是翻车重灾区——不少同学在上交前一周发现 loss 不降、界面跑不通、答辩时模型对一张苹果图片给出“香蕉”的预测。这篇笔记就是按我实际带项目的顺序从数据集准备、模型搭建、训练调参到 Flask 界面和答辩 PPT完整拆一条 1 到 2 周能走通的路线。适合正在选毕设题、或者需要快速交付期末大作业的读者也适合已经跑通基础代码、想补足工程细节的人。2. 水果识别数据集准备从 Fruits-360 到本地训练集的三个步骤2.1 数据集选型公开数据集和自采照片为什么都要用常见做法是先拿公开数据集把模型跑通再准备一小部分自己拍或网上找的图片做额外测试。公开数据集里最常用的是 Fruits-360它按类别分目录苹果、香蕉、橙子这些都有独立文件夹背景干净、光照也相对统一对初学者很友好。但它的缺点也在这里太干净了和真实场景差距明显模型在公开测试集上表现好不代表在教室灯光下拍的手机照片上也能好。所以我一般不会只用公开数据集而是会在项目里留出“额外测试集”这个环节。做法是模型训练完以后用手机拍 20 到 30 张水果照片放进一个单独目录作为演示和答辩时的“盲测图”。这些图片不用参与训练只用来检验模型是不是真的学懂了水果特征而不是背下了数据集里的固定背景。这个环节花不了多少时间但答辩时拿真实照片现场预测比翻测试集曲线更有说服力。2.2 目录清洗与拆分训练验证测试怎么分Fruits-360 下载下来以后训练集和测试集往往是官方已经分好的但每个类别里还可能包含不同批次的采集图片。这里有个隐藏问题同一颗水果在不同帧里几乎一模一样如果不小心把同一批次的图片同时分进训练集和验证集验证准确性会虚高。因此拿到数据后不要直接开训先写脚本扫描整目录统计每个类别的图片数量再按固定随机种子重拆一遍。import os import random import shutil src_root fruits-360/Training # 原始训练目录 dst_root fruits_split # 拆分后的输出目录 train_ratio, val_ratio, test_ratio 0.7, 0.2, 0.1 random.seed(42) # 固定种子保证可复现 for class_name in os.listdir(src_root): class_dir os.path.join(src_root, class_name) if not os.path.isdir(class_dir): continue images [f for f in os.listdir(class_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) n_train int(len(images) * train_ratio) n_val int(len(images) * val_ratio) parts { train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:], } for part_name, file_list in parts.items(): out_dir os.path.join(dst_root, part_name, class_name) os.makedirs(out_dir, exist_okTrue) for img_name in file_list: shutil.copy( os.path.join(class_dir, img_name), os.path.join(out_dir, img_name) )这段脚本先把每个类别的图片随机打乱再按 7:2:1 拆成训练、验证、测试三个目录。random.seed(42)这行很关键保证每次运行得到同样的拆分结果答辩时如果老师问“你的训练集和验证集怎么分的”你可以直接复现给他看。实际使用时我会把src_root里的路径改成自己解压后的目录同时注意原数据集里有的类别图片数是 0 或者特别少这类类别数量太少会导致训练时那个类的样本不够建议直接过滤掉。2.3 预处理脚本统一尺寸与归一化的关键参数Fruits-360 的图片尺寸不大常见版本是 100x100 左右所以不需要像 ImageNet 那样硬塞 224x224。我一般把输入尺寸统一到 100x100既能保留水果的纹理特征又能明显加快训练。预处理要写成一个独立的脚本模块因为训练、验证、推理三个阶段必须共用同一套逻辑。from PIL import Image import torchvision.transforms as T IMG_SIZE 100 train_transforms T.Compose([ T.Resize((IMG_SIZE, IMG_SIZE)), T.RandomHorizontalFlip(p0.5), T.RandomRotation(15), T.ColorJitter(brightness0.2, contrast0.2, saturation0.2), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transforms T.Compose([ T.Resize((IMG_SIZE, IMG_SIZE)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])训练和验证用两套不同变换是常规做法。训练侧加了随机翻转、随机旋转和颜色抖动相当于每轮都在看“略有变化的水果”能缓解过拟合验证侧只做裁剪缩放和归一化保证指标稳定。Normalize用的三个 mean 和三个 std 是 ImageNet 上的公共经验值直接搬过来用没问题但要注意推理时也必须用完全相同的数值否则预测结果会变得很奇怪。这一处是最容易被忽略的很多同学训练时一个 mean推理时又写另一组最后模型输出完全对不上。3. 搭建 CNN 识别模型自建卷积网络与 MobileNet 微调的取舍3.1 CNN 为什么适合水果分类局部特征与平移不变性水果分类并不需要看全局上下文。比如判断一个水果是不是苹果主要看它的颜色、表皮纹理和大致轮廓这些信息都是局部特征。CNN 用卷积核在整张图上滑动同一个卷积核可以在不同位置检测同一类特征这就是“参数共享”和“平移不变性”的直观意义。相比全连接网络把每个像素都当成独立输入CNN 的参数少得多也更贴合图像数据本身的结构。还有一点容易在答辩时被问到为什么不直接用 ResNet答案不是 ResNet 不好而是水果识别这个任务用小网络已经能到 95% 上下模型越深、训练时间越长、过拟合风险越大未必划算。我一般会先做一个自建小网络作为 baseline再视情况换预训练模型。这样无论准确率如何你都能说出“我做过对比实验而不是只抄了一个模型”。3.2 自建小网络的结构与 PyTorch 实现自建 CNN 不需要堆很深。我用的是两层卷积加一个全局平均池化的结构每层卷积都配 BatchNorm 和 ReLU。卷积核固定用 3x3通道数从 32 涨到 64最后接一个输出等于类别数的线性层。这个结构在 100x100 输入上参数量很小CPU 也能训得动。import torch import torch.nn as nn class FruitCNN(nn.Module): def __init__(self, num_classes15): 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), ) self.global_pool nn.AdaptiveAvgPool2d((1, 1)) self.classifier nn.Sequential( nn.Dropout(p0.3), nn.Linear(64, num_classes), ) def forward(self, x): x self.features(x) x self.global_pool(x) x torch.flatten(x, 1) return self.classifier(x)这里AdaptiveAvgPool2d((1, 1))把任意尺寸的特征图压成一个通道数为 64 的向量替代了全连接层的一长串展平操作参数量更小也不容易过拟合。Dropout(p0.3)是训练时才生效的推理时会自动关闭。num_classes要根据上一章数据划分后的实际类别数来设置比如选了 15 种水果就填 15。很多同学在这里翻车模型全连接输出写死成 10结果数据目录里实际有 15 个类别训练一启动就报 shape 不匹配。3.3 MobileNetV3 微调什么时候该用预训练模型如果自建网络在验证集上不够稳或者某些水果长得太像比如青苹果和绿梨我会换 MobileNetV3 做预训练微调。MobileNetV3 是轻量级网络比 ResNet 参数量小很多在 CPU 上也能跑推理。预训练模型在 ImageNet 上学过通用特征对我们这种几百上千张水果图片的小数据集有很强的迁移效果。import torchvision.models as models def build_mobilenet(num_classes15): model models.mobilenet_v3_large(pretrainedTrue) in_features model.classifier[-1].in_features model.classifier[-1] nn.Linear(in_features, num_classes) return model替换分类头时要注意in_features是从原模型读出来的不要硬编码。不同版本的 torchvision 里 MobileNetV3 分类头的层数可能有差异直接取classifier[-1]是最稳妥的写法。微调时要重点做两件事一是先用预训练权重初始化全部层再让整体模型以较小学习率训练二是不要从头开始训练否则预训练优势会丢光。实际经验是100x100 的水果图上自建网络和 MobileNet 准确率差距一般在 2 到 5 个百分点但当类别相似度很高、或者你想在答辩时给自己留一个“我试过对比实验”的空间MobileNet 是更安全的选择。4. 训练调参与避坑排查学习率、过拟合与剪枝出错的解决顺序4.1 学习率、batch size、epoch 三个参数怎么配训练参数里最容易翻车的是学习率不是 epoch。自建网络从零开始训练时我一般用 Adam初始学习率1e-3如果换成 MobileNet 微调学习率降到1e-4或5e-5。学习率太大loss 会来回震荡不下降太小训练半天只降一点点。batch size 在水果识别这个任务上 16 或 32 都够用显存不够就减半不要硬撑。epoch 我习惯先给 30 轮同时记录验证集上最好的模型而不是傻傻等 30 轮训完。import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_dataset ImageFolder(fruits_split/train, transformtrain_transforms) val_dataset ImageFolder(fruits_split/val, transformval_transforms) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model FruitCNN(num_classeslen(train_dataset.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) best_acc 0.0 for epoch in range(30): model.train() total_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() total_loss loss.item() * images.size(0) model.eval() correct 0 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) total labels.size(0) correct (predicted labels).sum().item() acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pt) scheduler.step() print(fepoch {epoch1}, loss {total_loss/len(train_dataset):.4f}, val acc {acc:.4f})这段训练循环里有几个设计点需要注意。ImageFolder会自动按子目录名生成类别索引因此len(train_dataset.classes)就是实际类别数模型定义不用写死。scheduler.step()每 10 个 epoch 把学习率乘 0.1作用是训练后期让权重更新更精细。best_model.pt只在验证准确率创新高时保存这样即使后几轮过拟合手里仍有最好的那份权重。如果你看到 loss 一直在降但验证准确率上不去优先检查model.train()和model.eval()有没有放对位置dropout 和 BatchNorm 在两种模式下的行为差异很大。4.2 过拟合时先调哪个dropout、增强与早停过拟合的典型表现是训练 loss 一路走低验证 loss 先降后涨。遇到这种情况先别急着换网络按下面的顺序去调。第一步加数据增强随机翻转和旋转是最低成本的方案第二步把Dropout从 0.3 提到 0.5同时给 Adam 增加weight_decay第三步做早停保存验证集上表现最好的模型而不是最后一轮。这个顺序我一般不变因为改模型结构是成本最高的操作放在最后。随机种子也要固定。PyTorch 里如果不定torch.manual_seed(0)同一份代码每次跑出来的结果会有波动这是正常的随机性。答辩前我会把所有实验重新跑一遍确保拿到最好的一组结果而不是靠运气挑一次记录。4.3 三类高频踩坑记录现象、原因与解决下面三条是水果识别这类图像分类项目里出现频率最高的坑我按“现象到解决”的方式记录了真实排查思路。第一条loss 卡在 2.3 左右一直不降。原因是分类头输出维度和类别数不匹配或数据加载时标签已经错位网络对每个类别都输出近似相等的概率时交叉熵值就在类别数的对数附近。解决方法是先打印一个 batch 的outputs.shape再核对model.classifier[-1].out_features是否等于train_dataset.classes数量。第二条训练时报CUDA out of memory。原因是 batch size 太大或者网络里全连接层参数量过高。解决办法很简单batch size 从 32 减到 16 或 8同时把输入尺寸从 224 减到 100。自己写模型时尤其注意不要在大卷积层后面接一个特别宽的Linear那是最耗显存的地方。第三条测试集准确率和自己预期差很多。这个坑有 80% 概率出在推理侧要么预处理不一致要么忘了model.eval()。推理时必须要torch.no_grad()否则 BatchNorm 和 Dropout 的行为和训练时混合在一起输出会变得不稳定。还有一个小问题容易被忽略模型在 GPU 上训练完保存的是 GPU 状态字典加载到 CPU 推理时要用torch.load(path, map_locationcpu)否则会报设备不匹配的错误。提示训练完成后画一张验证准确率曲线再画一张混淆矩阵这两张图在答辩 PPT 里比任何文字都管用。5. 本地跑通水果识别系统Flask 界面与推理脚本的最小实现5.1 从训练好的模型到推理函数预处理顺序必须一致训练只是第一步答辩时老师要看到的是“系统”也就是能让用户上传图片、返回识别结果的界面。这个环节里最常见的翻车点是推理代码的预处理和训练代码不一致。比如训练时用Resize((100, 100))推理时却传了原始大小训练时用ToTensor()把像素缩到 0 到 1推理时忘了做。这些细节都会导致识别结果明显变差。import io from PIL import Image import torch from torchvision import transforms model FruitCNN(num_classes15) model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() infer_transforms transforms.Compose([ transforms.Resize((100, 100)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def predict_one_image(image_bytes): img Image.open(io.BytesIO(image_bytes)).convert(RGB) tensor infer_transforms(img).unsqueeze(0) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) prob, idx torch.max(probs, dim1) return class_names[idx.item()], round(prob.item() * 100, 2)Image.open之后强制.convert(RGB)是一个重要习惯因为用户上传的图片可能是 PNG 带透明通道也可能是灰度图如果不转成 RGB通道数不一致会直接把模型搞挂。torch.softmax把输出转成概率分布这样除了返回类别还能给出置信度答辩演示时这个百分数非常加分。还有一点model.eval()必须在torch.no_grad()之前顺序反了也会出问题。5.2 Flask 最小界面上传图片返回类别与置信度Flask 是这类本地系统里最稳妥的框架代码量少答辩演示时只需要python app.py一条命令就能启动。前端只要一个文件上传表单后端把图片交给上面的预测函数再把结果渲染到页面。import os import time from flask import Flask, request, render_template, jsonify from werkzeug.utils import secure_filename app Flask(__name__) UPLOAD_FOLDER uploads os.makedirs(UPLOAD_FOLDER, exist_okTrue) app.config[UPLOAD_FOLDER] UPLOAD_FOLDER app.route(/, methods[GET, POST]) def index(): if request.method POST: file request.files.get(file) if file is None or file.filename : return render_template(index.html, error请先选择图片) ext file.filename.rsplit(., 1)[-1].lower() if ext not in {jpg, jpeg, png}: return render_template(index.html, error仅支持 jpg/jpeg/png) save_name f{int(time.time())}_{os.path.basename(file.filename)} save_path os.path.join(app.config[UPLOAD_FOLDER], save_name) file.save(save_path) with open(save_path, rb) as f: label, confidence predict_one_image(f.read()) return render_template( index.html, image_urlf/static/{save_name}, labellabel, confidenceconfidence, ) return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)代码里刻意避开了secure_filename直接保存因为它在处理中文文件名时会返回空字符串导致保存失败。这里用时间戳加原名拼接的方式既避免了重名覆盖也绕开了中文文件名的问题。文件类型校验是必要的前端防御否则用户传一个.txtPIL打开就会抛异常。答辩时老师如果问“如果上传的文件格式不对怎么办”这段代码已经给了答案。5.3 项目源码怎么组织从 train.py 到 app.py 的文件清单拿到这个题目后很多同学会直接把所有代码堆在一个 jupyter notebook 里这在一周内没问题但交项目或者答辩时就不太好看。我一般建议按下面这个清单组织源码每个文件职责单一老师扫一眼就能理解你的工作。文件名职责关键内容data_prepare.py数据清洗与拆分类别统计、按比例划分、过滤坏图model.py模型定义FruitCNN 或 MobileNet 构建函数train.py训练流程数据加载、超参数、保存最佳权重predict.py推理函数图片读取、预处理、softmax 输出app.pyFlask 界面文件上传、调用 predict、结果展示requirements.txt依赖清单torch、torchvision、flask、pillowREADME.md运行说明数据目录、训练命令、启动命令在train.py里用argparse加上--lr、--epochs、--batch_size这类命令行参数也是我强烈建议做的一件事。它能让你在答辩现场快速演示不同参数的效果不用改代码就能切换配置。配套的requirements.txt建议列出主要依赖不要固定具体版本号用torch2.0这类写法更通用避免和机器上已有的环境冲突。6. 答辩 PPT 怎么把 CNN 讲清楚一页一页的组织顺序和三类追问应对6.1 PPT 每页该放什么从背景到改进的结构答辩 PPT 的核心不是炫技而是让老师快速看到你的完整思考过程。我习惯按“背景—数据—模型—训练—系统—总结”讲大约 12 页。背景 1 页讲清楚水果识别能用在智能零售或农业生产分拣上数据 2 页放一两张样本图展示训练集和额外测试集模型 3 页画网络结构图标注每层输出尺寸训练 2 页放验证准确率曲线和混淆矩阵系统展示 2 页放 Flask 界面的截图和一次完整预测过程总结 1 页列出准确率、不足和后续改进方向。不用准备逐字稿每页想好三个关键词就够了。6.2 答辩追问怎么应对三个必问题目与回答逻辑第一个必问为什么用 CNN 而不用其他方法回答紧扣局部特征和参数共享。水果识别的关键在于颜色、纹理、边缘这些局部信息CNN 通过卷积核在图像上滑动提取特征参数共享又让模型不至于过度膨胀在小数据集上比全连接网络更稳。第二个必问自建网络和 MobileNet 怎么选坦白说结论是自建网络在干净背景上已经够用加 MobileNet 是为了对比迁移学习的效果。这个对比表放在 PPT 里很容易加分。第三个必问遇到训练集没有的水果会怎样回答要诚实模型会把未知水果强行归到最相近的一个已知类别所以系统只承诺在训练类别范围内有效扩展类别需要重新收集数据并微调。这种回答比“系统是万能的”可靠得多。最后建议你在答辩前一天把模型重新跑一遍确认best_model.pt在app.py能启动额外测试集的图片路径没丢。我见过太多人在现场因为图片路径不对、依赖缺失或模型文件没上传而翻车这些不是技术问题是准备动作没做完整。希望这份从数据到答辩的路线能帮你少走几步弯路也希望帮到你顺利把项目交出去。本文还有配套的精品资源点击获取
返回列表