ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习毕设选题:基于CNN卷积网络的蔬菜识别完整实战指南

深度学习毕设选题:基于CNN卷积网络的蔬菜识别完整实战指南

每年到了毕业设计开题的季节,总有不少同学来找我问“深度学习方向选什么题比较好”。我一般会反问一句:你希望这个题目是“做完就扔”,还是“能从头到尾走通一遍完整流程”?如果是后者,我会认真推荐一个方向——基于CNN卷积网络的蔬菜识别。

这个题目看起来简单,但它的“性价比”非常高。它不是一个为了堆噱头而设计的伪命题,而是一个典型的图像分类任务,涉及的环节非常完整:数据采集与清洗、模型选型与训练、评估与调优、可视化与部署,几乎覆盖了深度学习入门者的全部必备技能。而且,蔬菜种类的识别在农业智能化、商超结算、食堂备餐等场景中都有真实需求,答辩时“这个项目有什么用”这个问题,天然好回答。

这篇文章我想把整个项目的完整思路理一遍。我会从题目拆分开始,一路讲到模型选型、训练细节、常见坑点,最后落到“可演示的系统”怎么做。无论你是刚接触PyTorch的本科同学,还是想在简历里多一个“拿得出手”项目的求职者,这篇文章都值得你从头看到尾。

1. 为什么“蔬菜识别”是毕设好题目:先拆清楚题再动手

做任何项目之前,先把题目拆明白。很多同学拿到“基于卷积网络的蔬菜识别”这样的题目,第一反应是“这不就是一个分类网络吗”,然后就开始找数据集、跑代码。这个思路没有错,但如果只做到这一步,你的毕设撑死也只能拿个“合格”,因为缺少了学术上的完整性和工程上的闭环感。

站在“毕设评审”的角度来看,一个好题目要满足三点:有明确的问题定义、有可复现的实验过程、有可展示的成果形态。“蔬菜识别”恰好三点全占。

1.1 题目背后的真实问题边界

“蔬菜识别”到底要识别到什么粒度?这是一个需要从一开始就明确的问题。是按大类识别(叶菜类、根茎类、瓜果类),还是按具体品种识别(生菜、菠菜、油麦菜、西红柿、黄瓜、茄子、青椒、胡萝卜……),这两种做法的难度完全不同。从公开数据集和业界实践来看,按具体品种识别是主流做法,也是更有说服力的毕设方案。

以常见的数据集为例,一般会覆盖10到30个蔬菜类别,每个类别几百到上千张图片。类别数选多少合适?我的建议是控制在10到20类之间。太少体现不出分类难度,太多又会让性能和训练时间吃紧,而且容易因为相似类别太多导致准确率不好看。

还有一个容易忽略的问题:蔬菜识别的场景是单目标特写还是多目标混杂?如果是后者,问题就变成了目标检测或实例分割,难度陡增。毕设阶段,严格定义为“单目标图像分类”是更稳妥的选择。在论文摘要中,就要把这个边界说清楚:“本研究聚焦于餐盘中单一蔬菜种类的图像分类任务”,这句话能帮你挡住一半的答辩质疑。

1.2 一个完整毕设项目应有的“交付物”

这里有必要明确一下毕设不是“跑通一个模型”那么简单。我能给出的建议是,从第一天就建立“交付物清单”,后面每走一步都往清单上填充。一份标准清单包括:

交付物内容说明完成标志
数据集整理好的图片数据,按类别分文件夹每个类别图片数量均衡,无重复无低质量图
预处理脚本重命名、清洗、划分训练集/验证集/测试集的代码一键运行即可完成数据准备
模型代码网络结构定义、训练、评估的完整工程代码能从零开始复现训练过程
训练日志与曲线损失曲线、准确率曲线有清晰的收敛过程记录
评估报告混淆矩阵、各类别的精确率/召回率/F1值对结果有量化分析
演示系统Web或桌面端图片上传识别demo答辩现场能进行交互演示

知道要交付什么,你就知道时间该花在哪里。很多同学把时间全耗在调参上,最后演示系统随便凑一个,答辩效果非常吃亏。我见过不少学生模型准确率能做到95%以上,但演示界面粗糙得不行,老师点几下就没了兴趣——这真的很可惜。

1.3 需要的工具栈与前置知识

这个项目最推荐的技术栈是Python + PyTorch加上TorchVision,原因后面细说。前置知识不需要很多,你只要有Python基础语法、一点儿NumPy操作经验、能把张量维度搞明白,就可以开工。深度学习理论的部分,比如卷积、池化、反向传播的数学推导,暂时不需要精通,先会用再补原理,这跟我平时写工程代码的思路一致——不是先学完所有理论才动手。

整体项目周期(按每天花2小时算)大概是三到四周:一周搞数据,一周跑通基线模型,一周调优加分析,最后一周做演示系统。如果你只有两周deadline,也可以压缩到:三天搞数据、四天训练调优、三天做系统,但会很紧张。

2. 数据是第一道坎:公开数据集与自制数据集的取舍

图像分类项目里有一句老话:“数据和特征决定上限,模型只是去逼近这个上限。”对毕设项目来说,数据准备往往决定最终成绩的下限。

2.1 三种主流数据获取方案

方案一:使用现成的公开数据集。常见的一个选择是某开源平台上流传较广的蔬菜图像数据集,类别通常覆盖十余种常见蔬菜。优点是用起来省事,有现成的目录结构和标注,方便快速跑通流程。缺点是图片来源单一、清晰度普遍太高,模型在真实场景下泛化能力可能偏弱,答辩时容易被问“这个模型在你自己的照片上表现如何”。

方案二:利用图像搜索引擎批量获取图片。用爬虫脚本从图片网站按关键词批量下载。这种方式能拿到数量可观的真实场景图片,但是噪声很大,需要花不少时间做清洗。这里我特别提醒一句:爬取图片要注意图片版权和使用规范,毕设属于学习研究用途问题不大,但将来如果要产品化或发布,需要替换成合规的数据来源。

方案三:手机实拍。想做一个真正有说服力的项目,建议至少对一部分类别补充实拍图片。不需要拍几千张,每类补30到50张即可。实拍图能有效缩小“训练集场景”和“真实场景”之间的差距,答辩演示时直接拿自己拍的蔬菜照片测试,效果比数据集里的照片好得多。

我一般推荐方案一打底 + 方案三补充的组合。公开数据集保证训练量足够,实拍图片作为验证集的一部分,用来检验模型的泛化能力。

2.2 数据清洗与整理的关键动作

从网上批量下载的数据,质量参差不齐,这一步不能跳过。你需要做以下清洗操作:

  • 去掉纯背景图或卡通图:蔬菜识别要的是真实照片,漫画、图标、手绘一律删掉。
  • 去掉严重模糊和过曝/欠曝的图:可以用脚本统计图片清晰度(方差算子),把低于阈值的挑出来人工确认。
  • 去掉重复图片:搜索引擎下载的图片经常有完全重复的,用哈希相似度对比去重。
  • 修正错误标注:这个坑很隐蔽。我在帮某同学检查数据时,发现“青椒”目录里混了一堆“辣椒”,“生菜”目录里有“油麦菜”的图。这种错误标注对模型的伤害比样本少还严重。

清洗之后,把所有图片统一格式为JPG,统一短边不低于256像素,并按照类别放到文件夹里。目录结构建议这样组织:

vegetable_data/ train/ broccoli/ 0001.jpg 0002.jpg ... cucumber/ 0001.jpg 0002.jpg ... ... val/ broccoli/ ... cucumber/ ... test/ broccoli/ ... cucumber/ ...

结构清晰了,后面代码也简单。PyTorch的torchvision.datasets.ImageFolder可以直接认这种目录结构,不用手写数据加载器。

2.3 数据划分:别让你的测试集“泄题”

模型训练中一个很基础但很重要的问题:测试集绝对不能用来调参。很多小白犯的错误是训练到一半,觉得效果不好,就去测试集上跑一下看看结果,然后回头改参数——这相当于把测试集变成了训练的一部分,最后的结果肯定会虚高。

正确的做法是先把数据分成三份:60%训练、20%验证、20%测试,划分时要确保各类别在三个集合中的比例基本一致,避免某类在训练集里占比过高、测试集里却几乎没有。可以用sklearn的train_test_split带stratify参数实现分层抽样。后面的所有决策(选模型、调超参、早停)都只基于验证集,测试集只在你最终确定模型后跑一次。

还有一个细节:所有类别最好数量近似。如果胡萝卜有1000张、土豆只有200张,模型很容易把土豆学成胡萝卜。最简单的处理是把每类图片数量截断到同样的数量,或者对少样本类别做额外的数据增强。

3. 模型选型:从零训练、经典CNN还是迁移学习

模型选型是这个项目的核心决策。很多同学一上来就想“我能不能自己设计一个卷积网络”,有这个想法很好,但对毕设来说不是最优解。

3.1 CNN是怎么工作的:一个通俗版理解

在选模型之前,先花几十秒理解CNN的核心机制。你可以把图像想象成一个巨大的棋盘,每个像素是一个格子。全连接网络要“看”每一个格子和所有其他格子的关系,计算量巨大且容易过拟合。而卷积神经网络只用一个“放大镜”(卷积核)在棋盘上滑动,每次只观察一个小区域,提取局部特征;然后在多层之间逐级组合这些特征——低层看到边缘和纹理,中层看到形状,高层看到物体部件。

这个机制有两个天然优势:一是参数共享,同一个卷积核在整张图上滑动,参数量远小于全连接;二是局部感知,更贴合图像的数据结构。所以CNN天然适合图像,不需要引入过多的额外机制。

3.2 三个路线的对比

做蔬菜识别,你有三条路线:

路线优点缺点适用场景
自建小网络结构简单,代码量少,训练快精度通常不高,需要反复调试快速理解CNN原理的实验
经典CNN从头训练可以深入理解结构,可控性强数据少容易过拟合,训练时间长数据量大、时间充裕
迁移学习精度高,训练快,代码复用成熟需要理解微调逻辑,不能“白嫖”到底绝大多数情况下的最佳选择

这三条路线的实际效果我对比过:自建网络在十几个类别上反复调也只能做到85%左右,而且对光照、角度变化非常敏感。经典CNN从头训练在几万张样本下才有优势,几千张图基本白给。而迁移学习很容易做到95%以上,节省的时间可以用来做演示系统和论文分析。

3.3 为什么迁移学习是毕设的“最优解”

迁移学习的原理不复杂:拿一个已经在百万级图片数据集(比如ImageNet)上训练好的模型,把它学到的通用特征能力迁移到我们的蔬菜识别任务中。你可以把预训练模型想象成一个在“视觉世界里见多识广”的人,他已经知道怎么分辨边缘、纹理、形状这些基本构成要素。你只需要让他学一下“原来这些特征组合起来叫黄瓜、叫茄子”。

在torchvision.models里,官方提供了大量预训练权重可以直接用。我的默认推荐是ResNet18或ResNet50。

  • ResNet18:网络较浅,参数量小,CPU也能跑推理,训练速度快,对十几类蔬菜数据完全够用。
  • ResNet50:更深更宽,精度上限更高,但训练时间翻倍,显存占用也更大。如果你的GPU是入门级,还是优先考虑ResNet18。

细心的同学可能会问:为什么不选更轻量的MobileNet或EfficientNet?因为它们更适合移动端部署,对毕设来说反而多了一重不必要的复杂度。ResNet是学术界认可度最高的经典结构,论文里写“以ResNet为骨干网络”在答辩时最有说服力。

3.4 微调的具体做法

用TorchVision加载预训练模型后,把最后一层全连接替换成我们自己的分类头。在PyTorch里代码很简洁:

import torchvision.models as models # 加载在ImageNet上预训练过的ResNet18 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 获取最后一层全连接的输入维度 num_features = model.fc.in_features # 替换为新的分类层:10个类别(按你的实际类别数调整) model.fc = nn.Linear(num_features, 10)

微调策略上,建议分两阶段。第一阶段把前面所有层冻结(requires_grad = False),只训练新加的全连接层,跑几个epoch让分类头先“稳住”;第二阶段解冻全部层,用一个更小的学习率整体微调。这个两阶段策略能避免一开始梯度太大破坏掉预训练特征。

# 第一阶段:只训练 fc 层 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True # 第二阶段(解冻全部) for param in model.parameters(): param.requires_grad = True

实际跑下来,第二阶段的收敛速度非常快,经常只需要10到20个epoch就能达到很好的效果。

4. 训练实操:超参数、数据增强与过拟合的正面交锋

模型结构定下来之后,真正决定最终准确率的,是训练过程中的一系列细节。这一章节我把踩过最多的坑和最有用的经验都写出来。

4.1 数据增强:你的免费数据扩容包

在蔬菜识别这类数据量不大的任务中,数据增强绝对是最有效的“作弊器”。它的原理很简单:对训练图片在每次训练迭代时做随机变换,相当于用同一批原始图片不断制造“新样本”,让模型见过更多角度的数据,从而降低过拟合。

我常用的一组增强配置(基于TorchVision的transforms)如下:

train_transforms = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

这几个变换的解释如下:

  • RandomResizedCrop:随机裁剪并缩放回224x224。它模拟了“从不同距离/角度看到蔬菜”的情形,是几个变换中对提升泛化效果最显著的一个。
  • RandomHorizontalFlip:水平翻转。蔬菜本身没有方向性,翻转不会改变类别,但能增强模型的旋转不变性。
  • RandomRotation:小幅旋转。注意角度不要太大,15度以内足够,旋转过头会导致黄瓜、胡萝卜这类长条形蔬菜的形态失真。
  • ColorJitter:调整亮度、对比度、饱和度。不同光源下拍出的蔬菜颜色差异很大,这个变换能提升模型对光照变化的鲁棒性。

验证集和测试集不应用任何随机增强,只做统一缩放和归一化。很多人在这里偷懒忘了给验证集写Normalize,结果训练loss下降但验证准确率一直上不去,排查半天才发现是pipeline不一致。

4.2 关键超参数:一组可以直接“抄作业”的默认值

超参数我直接给一组经过多次验证的默认配置:

超参数推荐值理由
输入尺寸224x224ResNet默认输入尺寸,所有预训练参数都按这个尺寸对齐
Batch Size32(显存不够就16)保证梯度稳定,ResNet18下显存占用不大
损失函数CrossEntropyLoss分类任务的标准选择,内部已经包含Softmax
优化器AdamW(或SGD)AdamW收敛快,SGD加动量最终精度略高但需要调学习率
第一阶段学习率1e-3新分类头可以从较高学习率快速学习
第二阶段学习率1e-4到3e-5微调阶段不能太激进,否则破坏预训练特征
Epochs30到50配合早停策略,防止过度训练
权重衰减1e-4缓解过拟合

这里重点说两个参数。

学习率怎么定。别直接用默认的0.001一条路走到黑。我建议在开始训练前用一个简单的“学习率扫描”找到量级:从小到大逐渐调大学习率,画loss曲线,选下降最陡的位置对应的学习率作为初始值。对微调任务而言,第二阶段学习率在1e-4附近通常是安全的。更实用的建议是配一个ReduceLROnPlateau调度器,当验证loss多轮不下降时自动把学习率乘以0.1。这个调度器是我训练所有图像模型必开的“保险栓”。

Batch Size怎么选。一个常见的经验类说法是“Batch Size越大越好”,但实测下来在蔬菜识别这个数据量级别,Batch Size从32增加到128,最终准确率并没有稳定提升,反而可能因为batch太大导致训练不稳定。在入门级单卡上,32是一个性价比很高的选择。

4.3 过拟合的识别与应对

几千张图片训练一个几十层的网络,过拟合几乎是必然要面对的。一段典型的训练日志长这样:

Epoch [18/30] Train Loss: 0.280 Acc: 91.2% Val Loss: 0.142 Acc: 94.5% Epoch [19/30] Train Loss: 0.215 Acc: 93.1% Val Loss: 0.137 Acc: 94.7% Epoch [20/30] Train Loss: 0.160 Acc: 95.0% Val Loss: 0.151 Acc: 94.3%

注意看第20个epoch的变化:训练loss还在下降,但验证loss开始回升。这就是“模型开始死记训练集”的明确信号。此时应该停止训练(早停),并回去载入验证loss最低点的模型权重,而不是用最后一个epoch的权重。我在代码里总是设置torch.save(model.state_dict(), "best_model.pt")在每个验证指标提升的epoch后覆盖保存,这样哪怕错过了早停点,也不怕丢模型。

如果发现验证集准确率和训练集差距始终非常大(比如训练98%、验证82%),优先检查三件事:

  1. 数据增强是否真的应用在训练集上(往往只在验证集上走了pipeline)。
  2. 是否做了类别数量均衡,某类图片过少会导致该类严重过拟合。
  3. 是否解冻了太多层且学习率过高——试试解冻全部层的同时把学习率降到1e-5。

4.4 一个实用的完整训练代码框架

放一份精简版核心代码,基本是我项目里用的结构的浓缩,方便你搭骨架:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", factor=0.1, patience=3) best_val_acc = 0.0 for epoch in range(30): model.train() train_loss, train_correct, train_total = 0.0, 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) _, preds = torch.max(outputs, 1) train_correct += (preds == labels).sum().item() train_total += labels.size(0) val_loss, val_correct, val_total = 0.0, 0, 0 model.eval() with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() * images.size(0) _, preds = torch.max(outputs, 1) val_correct += (preds == labels).sum().item() val_total += labels.size(0) train_acc = train_correct / train_total val_acc = val_correct / val_total print(f"Epoch {epoch+1:02d} | Train Loss: {train_loss/train_total:.4f} | " f"Train Acc: {train_acc:.4f} | Val Loss: {val_loss/val_total:.4f} | Val Acc: {val_acc:.4f}") if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_model.pt") scheduler.step(val_loss)

这段代码没有做两阶段冻结展示,但你可以在此基础上轻松扩展。跑通之后你自然会遇到一个问题:每个epoch太慢怎么办?在数据量不大的情况下,建议优先确认数据加载是瓶颈还是GPU计算是瓶颈。如果训练时GPU利用率很低,多半是数据读取跟不上,把torch.utils.data.DataLoader的num_workers调大(Windows上注意不要超过4)即可。

5. 评估要“求甚解”:准确率之外的结论怎么挖

很多同学的实验报告只有一行字:“最终准确率95.3%”。这个结论太单薄了。一个完整的、能让答辩老师眼前一亮的评估,至少包含三个层面:量化指标、错误分析和可视化。

5.1 不要只看Overall Accuracy

对于多分类问题,准确率会被“多数类”主导。如果胡萝卜图片占全部数据的30%,即便模型把所有胡萝卜都认对、其他类别一塌糊涂,整体准确率也可能有70%以上。所以一定要分类别计算精确率(Precision)、召回率(Recall)和F1值。

举个例子,假设番茄这个类别的数据是这样:模型把40张番茄图片中的35张认对了,但把另外5张认成了青椒,同时又把6张青椒误判为番茄。那么番茄的:

  • 精确率 = 35 / (35 + 6) ≈ 0.85,表示“模型预测为番茄的结果中,有多少真的番茄”;
  • 召回率 = 35 / 40 = 0.875,表示“真正的番茄有多少被找了出来”;
  • F1是这两者的调和平均。

你可以用sklearn.metrics.classification_report一行代码输出所有类别的这些指标。论文里可以加一张表格,把表现最差的两个类别单独拎出来分析原因。

5.2 混淆矩阵:找到模型“最容易搞混的对子”

混淆矩阵是分类任务最重要的可视化工具。用sklearn.metrics.confusion_matrix计算之后,用matplotlib的imshow画出来,并对角线越亮越好、非对角线亮点越少越好。

观察混淆矩阵你会发现两类典型问题:

视觉相似型。比如“番茄”和“圣女果”之间、 “生菜”和“油麦菜”之间互相误判。这很合理,因为它们在形状、颜色、纹理上太接近,很多真人也不一定能分清楚。这时候的处理方式有两种:要么合并相似类别(如果任务允许),要么为这些类别专门补充更具区分性的训练样本。

拍摄角度与状态型。模型可能把“完整的胡萝卜”和“切段的胡萝卜”分开处理,导致同一类在不同状态下互相误认。解决思路是数据增强时增加更多随机的方向变化,或者干脆在数据集中把多状态样本都补充进去。

5.3 Grad-CAM热力图:让模型“开口解释”

这是答辩现场最能打动人的一个分析。Grad-CAM能生成一张热力图,表示模型在分类时重点关注图像的哪些区域。比如识别“黄瓜”时,热力应该集中在黄瓜的纹理区域而不是盘子背景上。

实现上不需要自己从头写,很多开源工具库已经封装得很完备。核心思路是:取模型中最后一个卷积层的输出,结合对目标类别的梯度,计算出一个“重要性权重”,再和特征图加权求和映射回原图尺寸。

展示方式很直观:左边原图、中间热力图覆盖在原图上、右边模型预测结果。放进论文的“模型可解释性分析”章节里,直接拉升整个项目的学术质量。我也见过有同学在答辩演示系统里加了“查看关注区域”按钮,老师点击后看到模型确实在“看”蔬菜本体,现场效果非常好。

6. 从模型到“作品”:一个能现场演示的识别系统

最后一个重要板块:把模型变成能给别人用的东西。毕设答辩的真理是——能跑起来演示的代码,胜过PPT上十页原理图。

6.1 选什么做演示框架:Flask与Streamlit的对比

演示系统的实现方式很多,我的推荐范围里只有两个选项:Flask和Streamlit。

  • Flask:更传统,需要手写前端HTML和路由逻辑,代码量大一点,但可控性最强。
  • Streamlit:基于Python的极简Web框架,几行代码就能渲染出上传组件和结果显示,对没有前端基础的同学极其友好。

如果只是毕设演示用途,我认为Streamlit是更合理的选择。它的开发成本低,代码量只有Flask的三分之一,而且长得比Flask默认样式更好看。下面这份代码可以直接保存在app.py里:

import streamlit as st import torch from PIL import Image from torchvision import transforms import torchvision.models as models class_names = ["broccoli", "cucumber", "tomato", ...] # 按训练时类别顺序填写 @st.cache_resource def load_model(): model = models.resnet18(weights=None) model.fc = torch.nn.Linear(model.fc.in_features, len(class_names)) model.load_state_dict(torch.load("best_model.pt", map_location="cpu")) model.eval() return model 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]) ]) st.title("蔬菜识别 Demo") uploaded = st.file_uploader("上传一张蔬菜图片", type=["jpg", "png", "jpeg"]) if uploaded is not None: image = Image.open(uploaded).convert("RGB") st.image(image, caption="你上传的图片", use_container_width=True) model = load_model() tensor = transform(image).unsqueeze(0) with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1) top5 = torch.topk(probs.squeeze(), 5) st.write("识别结果:") for i, (prob, idx) in enumerate(zip(top5.values, top5.indices)): st.write(f"Top {i+1}: {class_names[idx]} - {prob.item()*100:.2f}%")

这里的class_names顺序必须和训练时ImageFolder自动生成的类别索引顺序完全一致。很多同学在这里栽了跟头——训练时的类别排序是按文件夹名字母排序来的,如果自定义列表里顺序不对,识别的标签就会张冠李戴。稳妥的做法是在训练时把dataset.classes打印出来保存成JSON文件,再在演示代码里读取。

6.2 推理时的预处理容易踩的坑

模型训练用的图片尺寸是224x224,但用户上传的图片宽高比五花八门,直接缩放到224x224会产生拉伸变形,导致识别率下降。正确做法是先Resize(256)再CenterCrop(224),把图片从中心裁切出正方形区域。这是TorchVision评估流程的标准做法,和训练时的随机裁剪不同,推理时固定用中心裁剪,保证结果的稳定性。

另一个坑是图片通道和格式。用户可能上传PNG格式的图片,可能带有Alpha通道,也可能上传灰度图。代码里convert("RGB")这个动作必不可少,它能统一所有输入到三通道RGB,避免张量维度不匹配的错误。

6.3 答辩演示的准备要点

系统写完之后,一定要提前演练整个演示流程。我见过最惨烈的一次答辩,是某同学演示到一半,发现模型权重文件还在训练机上没拷贝过来,现场只能展示PPT。这里有几个准备工作:

  • 把best_model.pt放进项目文件夹,确保演示机器上可以直接加载。
  • 准备5到8张不同来源的测试图片(公开数据集里的、网上找的、自己拍的),分别验证模型表现,做到心中有数。如果有哪张图识别错了,提前准备好分析话术:“这张图的光照条件比较特殊,模型把它误判成了XXX,这个问题可以通过补充类似场景的训练数据来改善。”
  • 如果现场没有网络,提前处理好依赖库安装问题。Streamlit应用的启动命令streamlit run app.py要在答辩前试跑一遍,确认端口和浏览器都能正常打开。

6.4 在演示系统中加一点“信息密度”

矩阵和训练曲线也能放进演示系统。Streamlit可以展示训练过程中保存的history.json中的loss曲线和准确率曲线,甚至可以旁边放一张混淆矩阵图片,这样老师在操作界面时也能直接看到训练分析结果。答辩时你只需要开场讲一句“右边这个混淆矩阵是训练后对全量验证集进行评估得到的结果”,剩下的让图像自己说话,比你自己背数据有力得多。

最后分享一个我在整个项目过程中体会最深的一件事:虽然大部分时间花在了训练和调参上,但真正拉高成绩下限的,反而是演示系统和错误分析这两块。一个模型准确率高、解释清晰、演示流畅,再加上理论基础扎实,就是一个标准的“优秀毕设项目”画像。建议大家在动手前先把上面说的交付物清单打印出来贴在桌前,每完成一项就划掉一项——这个过程本身,就是把“一个题目”拆成“一条技术路线”的实战训练。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进