☰
PyTorch垃圾分类识别系统:数据工程、迁移学习与部署实战
2026/10/26 13:58:10 网站建设 项目流程

项目标题里"课程设计"和"毕设"这两个词一摆出来,做过的人心里大概都有数:这类题目年年有人选,年年有人卡在同一个地方。我前后帮人看过几个基于 PyTorch 的垃圾识别与分类系统,从最初的"跑通一个图像分类 Demo",到最后能站在答辩台上把一套完整的训练、评估、演示链路讲清楚,中间踩的坑远比想象中多。这个项目的表层是"用 PyTorch 训练一个图像分类模型",里层其实是一整套数据工程、实验设计和工程化落地的小型闭环:数据怎么来、怎么洗、怎么划分,模型为什么选这个不选那个,训练曲线怎么看,评估指标怎么读,最后一键演示的时候为什么结果和训练时对不上。这篇内容适合三类人:正在做课程设计、需要一个能讲出深度又不至于失控的题目的人;准备做毕设、想把工作量做扎实的人;以及已经跑通了教程代码、但发现自己说不清楚"为什么这么做"的人。下面我按实际动手的顺序,把每个环节的判断依据和容易翻车的地方摊开讲。

1. 垃圾分类识别为什么成了高频选题,以及它真正难在哪里

1.1 选题的三个天然优势,也是它容易被低估的原因

垃圾分类识别这个题目受欢迎,理由很实在。第一,任务定义清晰,输入是一张图片,输出是一个类别标签,属于最标准的单标签图像分类,不需要标注框、不需要分割掩码,标注成本可控。第二,场景有现实说服力,答辩时不需要费力解释"这东西有什么用",评委一听就懂。第三,技术栈成熟,PyTorch 生态里现成的骨干网络、预训练权重、数据增强库一应俱全,一个基础版本两三天就能跑出结果。

但正因为"两三天能跑出结果",很多人会误判它的难度,把一个数据工程问题当成调参问题来做。我见过不少项目,模型换了四五个,学习率试了十几组,准确率卡在 80% 上下不动,最后发现问题出在数据集的类别定义上——"塑料袋"和"保鲜膜"被不同的人标成了不同类别,模型当然学不明白。

这个项目的真实技术含量分布大致是这样的:数据处理和评估设计占六成,模型选型和训练技巧占三成,界面与部署占一成。新手最容易把精力全押在那三成上,然后在答辩时被一句"你的各类别召回率是多少"问住。

1.2 类别体系先定死,再谈其他

动手写第一行代码之前,必须先确定类别体系。常见的做法有两套:一套是四分法(可回收物、厨余垃圾、有害垃圾、其他垃圾),一套是更细的按照具体物品分类(纸类、塑料、玻璃、金属、织物等)。

四分法的优点是贴近实际投放场景,缺点是类别内部差异极大——"可回收物"里既有矿泉水瓶也有纸箱,视觉特征天差地别,模型要学的是一个极其宽泛的概念。细分类的优点是类内一致性高、模型容易学到特征,缺点是类别数量多、每类样本少,而且答辩时容易被问"你这个分法和实际投放标准对得上吗"。

我的建议是:课程设计用四分法或六分法,侧重讲清楚"类别粒度对模型难度的影响";毕设如果想把工作量做足,可以做细分类,但要额外补一段类别体系映射的说明,把细类映射回四大类,展示你对业务的理解。这个映射表放在报告里非常加分,成本几乎为零。

提示:类别名称一旦定下来就不要中途改。改一次类名意味着所有标注、所有划分文件、所有训练日志全部作废,我在一个项目里因为把"其他垃圾"改成"其他"重跑了三次训练,纯属自找。

2. 数据集这一步基本决定了项目的天花板

2.1 公开数据打底加自建数据补充的组合策略

数据来源通常两条路:用现成的公开图像数据集,或者自己拍自己标。纯用公开数据的问题是数据分布单一,背景干净、主体居中、光照均匀,模型在测试集上能到 90% 以上,一放到手机拍的杂乱背景照片上就崩。纯自建数据的问题是工作量大,一个类别没有三百张以上,训练出来的模型基本不可用。

我实际用下来比较稳的组合是:公开数据打底,每类几百张,保证模型能收敛;自建数据补充,每类再拍一百到两百张,故意拍得"脏"一点——不同角度、不同光照、不同背景、部分遮挡、手持拍摄的轻微模糊。这样最终的模型在演示环节的表现会明显更自然。

自建数据的采集有个小技巧:不要一个人拍完所有照片。同一个人在同一间屋子里拍出来的照片,背景、光线、色温高度一致,模型很容易把"背景"当成判别依据。找两三个不同的人、在不同的房间各拍一部分,模型被迫去学物体本身的特征。这是我在一个项目里吃了亏才想明白的——那个模型在实验室的测试集上 94%,换到走廊里拍的照片只剩 61%,原因就是它学的是桌面的木纹。

2.2 清洗、去重与类别不平衡,三件不能省的事

拿到数据后的第一件事不是训练,是清洗。三个动作必须做:

视觉去重。公开数据集里常常有同一张图的不同尺寸版本,或者同一段视频抽出来的连续帧。这些图如果被随机划分到训练集和验证集,验证集准确率会虚高好几个点。用感知哈希(pHash)算一下图片指纹,汉明距离小于 5 的判为重复,只保留一张。

剔除损坏与异常样本。有些图是纯色块、有些是截图带水印、有些尺寸只有 30×30。写个脚本扫一遍,过滤掉长边小于 100 像素的,过滤掉通道数不是 3 的。

统计类别分布。这一步经常被跳过。我自己经手的一个数据集,六类里最多的"塑料瓶"有 800 张,最少的"陶瓷碎片"只有 90 张,差了将近九倍。不处理的话,模型会把所有不确定的样本都预测成"塑料瓶",整体准确率看起来还不错,但对小类几乎完全失效。

处理不平衡的手段有三档,从轻到重:

手段具体做法适用场景代价
重采样WeightedRandomSampler 按类别频率加权采样不平衡比例 3~10 倍实现简单,小类会重复采样导致过拟合
损失加权CrossEntropyLoss 的 weight 参数按类别频率倒数设置不平衡比例 5~20 倍一行代码,但权重过大时训练不稳定
数据补充对小类做定向采集或强增强任意比例最有效,但要花时间

我一般先上采样器,观察 macro-F1 有没有改善,如果小类召回率还是低于 0.6,再叠加损失加权。两者同时上的时候要注意,权重不要设得太激进,否则模型会过度偏向小类,大类召回率掉得厉害。

2.3 划分方式选错,前面所有努力都会被一个虚高的数字掩盖

随机按 8:1:1 划分训练、验证、测试是最常见也最容易出问题的做法。如果同一物体的多张照片(比如一个易拉罐的正反面)被分到了训练集和验证集,模型只要记住"这个易拉罐的表面划痕"就能答对,验证准确率会虚高十个百分点以上。

更严谨的做法是分组划分:给每张图打一个 group 标签,同一个物体拍的所有照片共享同一个 group id,划分时以 group 为单位整体分到某一侧。公开数据集如果没有 group 信息,可以退而求其次,用感知哈希的聚类结果作为 group。

另一个常被忽略的点是测试集的使用时机。测试集只能在所有决策都定下来之后跑一次,跑完就锁住。我见过有人拿测试集当验证集反复调参,最后报告里写的 92% 其实已经过拟合到测试集上了,答辩时被要求现场跑一遍新的图片,结果差了一大截。

3. 骨干网络的选择:不是越深越好,而是越匹配越好

3.1 迁移学习几乎是必选项,但冻结策略有讲究

从零训练一个 ResNet 在几千张图上基本不可能收敛到可用水平,所以大家都在用 ImageNet 预训练权重做迁移。这里有个细节决定成败:一次性把整个网络的学习率设成同一个值,往往效果不如分阶段。

我实际用的流程是三步走。第一步只训分类头,骨干全部冻结,学习率设大一点(比如 1e-3),训练五到十个 epoch,让随机初始化的分类头先收敛到一个合理位置,避免一开始就用巨大的梯度把预训练权重冲垮。第二步解冻骨干的后面几层(比如 layer3、layer4),学习率降到原来的十分之一,做微调。第三步如果数据量足够,全网络解冻,学习率再降一个数量级,用余弦退火跑完剩下的 epoch。

这套流程的核心逻辑是:预训练权重携带的底层特征(边缘、纹理、颜色斑块)对垃圾分类任务同样有效,不该被破坏;而高层语义特征需要针对新任务重新调整,所以从后往前逐步解冻。

参数层面给一组我自己反复验证过的配置,可以作为起点:

# 头部训练阶段 optimizer = torch.optim.AdamW(head_params, lr=1e-3, weight_decay=1e-4) # 微调阶段 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)

3.2 几个候选骨干的实测取舍

我在同一份约 4000 张、六类的数据集上对比过几个常用骨干,输入统一 224×224,batch size 32,训练 30 个 epoch。结果大致是这样的:

骨干参数量验证集 macro-F1单张推理耗时(CPU)备注
ResNet1811M0.87约 25ms训练快、稳,基线首选
ResNet5025M0.89约 65ms提升有限,显存占用翻倍
MobileNetV3-Small2.5M0.85约 12ms轻量,部署友好
EfficientNet-B05M0.89约 30ms性价比好,但对超参敏感
ViT-B/16(小数据从头微调)86M0.78约 180ms数据量不够时明显吃亏

结论不复杂:在几千张图这个量级上,ResNet18 和 EfficientNet-B0 是性价比最高的两个选择,MobileNetV3 适合最终要部署到资源受限设备的场景。ViT 那类结构对数据量要求高,小数据集上从头微调很容易翻车,除非用更强的增强和更长的训练。

如果项目最终要做一个桌面演示程序,跑在普通笔记本上,我建议直接用 ResNet18 训练,最后导出时再做量化,这样训练阶段不用为了省显存而牺牲效果。

3.3 输入分辨率和批大小的显存换算

这一块经常有人问我"为什么 batch 设到 64 就 OOM"。粗略的估算方法是:显存占用约等于 参数量 × 4 字节(fp32 权重) + 激活值显存,而激活值跟 batch size、分辨率、网络层数成正比。224×224 输入、ResNet18、batch 32,在 8GB 显存的卡上大约是 3.5GB,翻到 batch 64 就接近 7GB,加上系统占用很容易爆。

几个立竿见影的手段:开混合精度训练(torch.cuda.amp),显存能省三到四成,速度还能快一点;把 batch 调到显存的上限,学习率按 batch 比例线性缩放;如果还是不够,用梯度累积模拟大 batch。

scaler = torch.cuda.amp.GradScaler() for i, (imgs, labels) in enumerate(loader): with torch.cuda.amp.autocast(): out = model(imgs) loss = criterion(out, labels) / accum_steps scaler.scale(loss).backward() if (i + 1) % accum_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()

混合精度有一个坑要注意:某些自定义损失函数(比如自己实现的 Focal Loss)在 fp16 下可能出现数值不稳定,表现为 loss 突然变成 nan。解决办法是把 loss 计算强制放在 fp32 下进行,或者干脆关掉 amp 先确认模型能正常收敛,再加回来。

4. 训练脚本里那些决定成败的细节

4.1 数据增强要有分寸,垃圾图像有几个禁忌

增强策略的选择跟数据特性强相关。通用配方是 RandomResizedCrop(scale 0.7~1.0)、RandomHorizontalFlip、ColorJitter(亮度对比度各 0.3,饱和度色相各 0.1),再加上轻量的 RandomRotation(±10 度)。

垃圾图像有两个特殊之处。一是很多物体有明确的方向性,比如饮料瓶竖着和躺着的视觉差异很大,如果把旋转角度开得太大,模型可能学不到稳定的特征;二是颜色本身就是重要的判别依据(比如不同颜色的垃圾桶、不同材质的包装),ColorJitter 开得太狠会破坏这个线索。我的经验是色相抖动控制在 0.05 以内,饱和度 0.2 以内,宁轻勿重。

验证集和测试集的预处理必须是确定性的:Resize 到 256、CenterCrop 到 224、ToTensor、Normalize。归一化参数用 ImageNet 的均值和方差(0.485/0.456/0.406 和 0.229/0.224/0.225),因为预训练权重就是在这个分布下训出来的。这一点后面部署时还会再提一次,它是导致"训练好、部署差"的头号元凶。

如果想要更强的增强,可以上 RandAugment 或者 Mixup/CutMix。Mixup 在小数据集上通常能涨一到两个点,但它会改变标签语义,跟 CrossEntropy 配合没问题,跟某些需要硬标签的评估流程配合时要注意。

4.2 学习率调度和早停,比调网络结构更划算

学习率是整个训练里最值得花时间调的单一超参。我的做法是先跑一个小的学习率扫描:固定其他条件,把 lr 在 1e-5 到 1e-2 之间取五六个值,各跑 5 个 epoch,看 loss 下降速度,选下降最快且不发散的那个数量级,再在这个数量级内细化。

调度器方面,余弦退火(CosineAnnealingLR)在分类任务上表现稳定,配合 warmup 效果更好——前 3 个 epoch 学习率从 1e-5 线性升到目标值,避免训练初期梯度爆炸。OneCycleLR 也可以,收敛更快,但对总 epoch 数敏感,如果中途改 epoch 数就得重来。

早停的判断依据不要只看验证集准确率,要看验证集 loss。准确率是离散的,可能在某个平台期反复横跳,loss 的下降趋势更能反映模型是否还在学习。连续 8 个 epoch 验证 loss 没有改善就停,同时保存这段时间内 loss 最低的权重,而不是最后一个 epoch 的权重。

torch.save保存的时候建议保存 state_dict 而不是整个模型对象,前者更小、加载更灵活,而且不会因为代码结构变化导致加载失败。文件名里带上关键超参和时间戳,比如resnet18_lr1e-4_bs32_ep30_f1-0.892.pth,后期做消融对比时会感谢自己。

4.3 训练曲线怎么读,以及三种典型异常形态

训练日志里最该盯的三条线是:训练 loss、验证 loss、验证 macro-F1。它们的组合形态能直接告诉你问题出在哪。

第一种,训练 loss 持续下降,验证 loss 先降后升,验证 F1 在高点之后回落。这是标准的过拟合,处理顺序是:先加增强,再加正则(weight_decay 从 1e-4 提到 1e-3),还不行再考虑减小模型或增加数据。不要一上来就加 Dropout,分类任务里它的效果常常不如增强明显。

第二种,训练 loss 和验证 loss 都下降得很慢,而且两者差距很小。这是欠拟合,通常是学习率太小或者模型容量不足,也可能是数据增强过头了。我遇到过一次,增强开得太猛,模型在训练集上的准确率只有 55%,关了 Mixup 之后立刻恢复正常。

第三种,训练 loss 剧烈震荡甚至变成 nan。排查顺序是:学习率是不是太大、有没有开 amp 导致数值溢出、数据里有没有异常值(比如归一化之后出现 inf 的像素)、损失函数里有没有除零操作。

这三种形态的诊断逻辑其实是一条:训练 loss 反映拟合能力,验证 loss 反映泛化能力,两者的差距反映过拟合程度,两者的绝对水平反映欠拟合程度。把这四个量对齐看,比单看准确率有用得多。

5. 评估环节:一个准确率数字骗不了答辩老师

5.1 混淆矩阵才是这个项目的核心产出

整体准确率在类别不平衡的数据上有严重的欺骗性。六类数据,如果最大的类占 40%,一个"永远预测成最大类"的傻模型也能拿到 40% 的准确率。所以我建议报告里必须放三样东西:混淆矩阵、每类的精确率/召回率/F1 表格、macro-F1 和 weighted-F1 两个汇总数字。

混淆矩阵建议用归一化版本(按行归一化,每行加起来是 1),这样能直接读出"某一类有多少比例被错分到了哪一类"。我在一个项目里看到,塑料瓶这一类的召回率只有 0.71,看混淆矩阵发现其中 20% 被分到了"其他垃圾"——原因是数据集里的塑料瓶大多是透明或半透明的,跟"其他垃圾"里的玻璃杯、保鲜膜视觉上非常接近。这个发现直接指导了后续的数据补充方向:专门补了一批透明塑料制品的样本,让标注员明确区分"硬质透明塑料"和"软质透明薄膜"。

提示:混淆矩阵不要只放一张总的,建议再放一张"错误样本可视化图",从验证集里挑出置信度最高但预测错误的 16 张图,拼成 4×4 网格。这张图在答辩时的杀伤力极大,它证明你真的看过模型错在哪,而不是只跑了个脚本。

5.2 经常被误判的几组类别对,和背后的原因

垃圾图像的误判不是随机的,几乎都集中在几组固定搭配上。我总结了几组高频的:

透明材质 vs 其他垃圾。前面说过的,透明塑料瓶、玻璃杯、保鲜膜三类之间互相混淆。本质原因是这类物体的形状和纹理特征被背景透射干扰,模型抓不到稳定的轮廓。解决办法是在预处理阶段加重背景分离,或者在数据采集时尽量用纯色背景。

纸质 vs 可回收混合。纸箱、纸杯、报纸都属于纸类,但纸杯内壁有淋膜、纸箱有印刷,跟"其他垃圾"里的餐巾纸、卫生纸容易混。这其实是个类别定义问题,不是模型问题,需要在类别说明里写清楚判定规则。

同类别内部差异过大。四分法下的"可回收物"包含金属、玻璃、纸、塑料、织物,视觉差异极大,模型很难用一个统一的特征空间把它们聚在一起。这也是为什么我在前面建议做细分类再映射回大类。

小目标与遮挡。一张图里有多个垃圾物品时,模型只能输出一个标签,这时候标注的"主物体"是谁就成了关键。建议在标注规范里明确规定:以画面中心、面积最大的物体为准。

搞清楚这些误判模式,报告里"不足与改进"那一章就有实打实的内容可写,而不是空泛地说"数据集还不够大"。

6. 从训练脚本到一个能演示的系统

6.1 桌面端界面是性价比最高的演示方案

毕设演示环节最怕的是现场打开一个 Jupyter Notebook 逐格运行。评委的观感会立刻从"一个系统"降级成"一段代码"。花一天时间做一个简单的桌面界面,投入产出比极高。

技术选型上,PyQt5 或 PySide6 是最省事的选择,一个窗口、一个按钮、一个图片显示区域、一个结果文字区域,不到两百行代码就能搞定。核心逻辑就三步:用 QFileDialog 选图,用 PIL 读图并走一遍和验证集完全一致的预处理,把 tensor 送进模型拿 argmax,把类别名和置信度显示出来。

关键点是预处理必须复用同一份代码。我吃过这个亏——界面里手动写了一遍transforms.Resize((224, 224)),而训练时用的是Resize(256) + CenterCrop(224),结果演示时模型输出的置信度普遍偏低,分类也经常出错。后来我把预处理封装成一个build_transform(is_train)函数,训练和推理都调用它,问题彻底消失。

如果不想写桌面程序,Streamlit 或者 Gradio 也是可选项,几行代码就能起一个网页版的上传界面,缺点是样式定制空间小,而且现场演示依赖浏览器。我的偏好还是 PyQt5,离线可用,双击就能跑,不会因为网络问题翻车。

6.2 模型导出与推理加速,顺手做能加分

训练完的模型直接torch.load也能用,但如果想在报告里体现一点工程意识,可以多做两步。

第一步是 TorchScript 导出。用torch.jit.trace把一个固定的输入 shape 追踪一遍,导出成.pt文件,加载时不需要原始模型类定义,部署更干净。注意 trace 不支持动态控制流,如果你的 forward 里有 if 分支,要用torch.jit.script或者把分支改写掉。

model.eval() example = torch.randn(1, 3, 224, 224) traced = torch.jit.trace(model, example) traced.save("garbage_cls_resnet18.pt")

第二步是动态量化。对 CPU 推理场景,torch.quantization.quantize_dynamic把 Linear 和 Conv 层量化成 int8,模型体积能缩小到四分之一左右,CPU 推理速度提升明显,精度损失通常在一个点以内。这个操作非常适合演示场景——笔记本上单张图推理从 25ms 降到 10ms 以内,用户点一下按钮几乎立刻出结果。

第三步可选,导出 ONNX。如果报告里想强调"模型具备跨框架部署能力",导出一个 ONNX 文件并用 onnxruntime 验证一遍输出一致性,是很好的加分项。导出时记得设opset_version=12以上,并且设dynamic_axes让 batch 维度可变。

6.3 工程目录怎么组织,答辩时会被翻代码

代码组织得好不好,直接影响评委对你工程能力的判断。我推荐的目录结构是这样的:

garbage_cls/ ├── configs/ # 超参配置文件 ├── data/ │ ├── raw/ # 原始数据,只读 │ ├── processed/ # 清洗去重后的数据 │ └── splits/ # train/val/test 的 csv 划分文件 ├── src/ │ ├── dataset.py # Dataset 与 transforms │ ├── model.py # 骨干网络与分类头 │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估与混淆矩阵 │ └── inference.py # 单图推理封装 ├── app/ # 界面代码 ├── weights/ # 模型权重 ├── outputs/ # 日志、图表、混淆矩阵 └── requirements.txt

data/raw保持只读是个好习惯,所有清洗结果输出到processed,这样任何时候都能复现数据处理流程。splits目录里放 CSV 文件(三列:路径、标签、group_id),训练脚本读这个文件加载数据,而不是用ImageFolder扫描目录。这样做的好处是划分可复现,换个机器跑结果一致,也能明确证明测试集没有参与调参。

7. 踩坑实录:那些让准确率虚高或训练崩掉的瞬间

7.1 验证集准确率虚高的三种典型成因

这是我帮人排查最多的一类问题。三种成因按出现频率排序:

数据泄漏。同一物体的多张照片跨了训练集和验证集,或者同一段视频的连续帧被分到了两侧。症状是验证准确率异常高(95% 以上),但实际上线效果差。诊断方法很简单:从验证集里挑几张预测正确且置信度高于 0.99 的图,跟训练集做相似度检索,如果能找到几乎相同的图,就是泄漏了。

预处理不一致。训练时用了随机裁剪和颜色抖动,验证时也误用了带随机的 transform,导致同一个 epoch 内验证集的结果每次都不一样。症状是验证 loss 曲线抖动得厉害,而且同一个 checkpoint 反复评估得到的数字不同。诊断方法:固定随机种子,连续评估两次,看结果是否一致。

类别标签错误。标注时把某些类别的序号搞反了,或者 CSV 文件里的标签和 ImageFolder 的目录名对不上。这种情况下模型照样能学到东西,只是输出的类别名是错的。症状是混淆矩阵主对角线很亮,但你在界面上看到的结果总是对不上。诊断方法:从每个类别里各抽 5 张图,打印标签和实际内容,肉眼核对一遍。这个检查五分钟能做完,能省下几小时的困惑。

我遇到的最隐蔽的一次是第三种,但是半错——六类里有两类被交换了,混淆矩阵看起来很完美,因为这两类的样本本身就容易混,直到演示时发现"香蕉皮"被识别成"易拉罐"才暴露出来。

7.2 训练不稳、显存爆掉和 DataLoader 卡死的处理顺序

训练稳定性问题,我的一般排查顺序是:先看数据和标签是否正常,再看损失函数和输出维度是否匹配,最后才怀疑超参。

训练 loss 一直是 2.3 左右不动,六分类的交叉熵初始值就是 ln6≈1.79,如果卡在 2.3 附近,说明模型根本没在学。最常见的原因忘了optimizer.zero_grad(),梯度一直在累加,参数被推向奇怪的方向。其次是学习率太小,1e-6 这种量级在三十个 epoch 内基本看不出变化。

显存爆掉除了前面说的降 batch、开 amp,还有一个常被忽视的原因是 DataLoader 的num_workers设得太大。多进程会复制一部分数据到共享内存,worker 数设成 8 在 16GB 内存的机器上很容易把内存吃满,表现为程序被系统杀掉但报的是显存错误。我一般在笔记本上设 2~4,服务器上设 CPU 核数的一半。

DataLoader 卡死在第一个 batch 是很经典的 Windows 平台问题,原因是多进程启动方式不同。解决办法是把主训练逻辑包在if __name__ == "__main__":里,或者临时把num_workers设成 0 确认是不是这个原因。

还有一个很容易忽略的:pin_memory=True只在 GPU 训练时有用,如果开着但它跟num_workers的组合不匹配,反而会拖慢数据加载。这个参数的收益不大,我一般直接关掉。

7.3 部署阶段输入预处理不一致,是最冤枉的一类失误

训练时一切正常,导出模型之后在界面上跑,结果全乱。这种问题的根源十有八九在预处理。

具体的差异点包括:训练用了 ImageNet 归一化而推理忘了做;训练 resize 到 256 再中心裁剪 224 而推理直接 resize 到 224;PIL 读图是 RGB 而 OpenCV 读图是 BGR;ToTensor把像素值缩放到 [0,1] 而推理时用了原始 0~255 的数组。这四种差异里任意一种出现,模型的表现都会断崖式下跌。

我的解决办法是在inference.py里定义一个preprocess(image_path)函数,内部调用和验证集完全相同的 transform 对象,并且写一个自检测试:从验证集里抽 20 张图,分别用evaluate.py的流程和inference.py的流程跑一遍,比对预测结果是否完全一致。这个测试如果能通过,部署阶段的输入问题就基本排除了。

8. 答辩与文档:把做过的事讲出层次感

8.1 老师最常问的几个问题,以及怎么答得体面

根据我旁听和被问的经验,这类项目的提问集中在几个方向。第一个是"为什么选这个网络",如果你的回答是"因为它是经典网络",基本会被追问到底。更好的答法是给出对比实验的数据:我试了三个骨干,在同样的数据和训练配置下,ResNet18 的 macro-F1 是 0.87、推理耗时 25ms,EfficientNet-B0 是 0.89、耗时 30ms,考虑到部署环境和训练成本,最终选了前者。

第二个是"数据量够不够"。诚实回答就行:公开数据加自建数据一共四千多张,对于从零训练确实不够,所以用了迁移学习;也正是因为数据量有限,我没有上更大的模型。这种回答体现的是判断力,比硬撑说"够了"要好得多。

第三个是"模型为什么会分错"。这时候把混淆矩阵和错误样本可视化图拿出来,讲清楚"透明材质之间的混淆是主要错误来源,原因是背景透射导致轮廓特征不稳定"。能讲到这个层次,说明你真的做过分析。

第四个是"如果给你更多时间会怎么改进"。准备两三条具体的方向:补充透明材质和遮挡场景的样本、试试知识蒸馏把大模型的能力迁移到轻量模型上、把单标签分类扩展成多标签检测。有具体方向比空谈"继续优化"强很多。

8.2 报告里值得放的那几张图

报告篇幅有限,图的取舍很关键。我认为必须有的四张:数据集类别分布柱状图(证明你看过数据)、训练过程的 loss 与 F1 双曲线(证明训练是可控的)、归一化混淆矩阵(证明评估是严谨的)、错误样本可视化网格(证明你分析过错因)。

可选但很加分的两张:不同骨干网络的对比表格配柱状图、演示界面的截图配预处理一致性验证的说明。

不建议放的:模型结构图(网上到处都是,放上去显得凑数)、大段粘贴的代码(放附录即可)、未经处理的原始 loss 曲线(抖动太厉害反而显得不专业,建议做滑动平均后再画)。

最后分享一个我自己一直在用的小习惯:训练脚本每次运行都自动把当前的超参、数据划分文件的哈希值、git 提交号(如果有的话)写进日志文件的第一行。后期做消融实验或者回答"这个结果是怎么跑出来的"时,翻日志比翻记忆靠谱得多。这个习惯成本很低,但在需要复现结果的时候,能省掉大量猜测的时间。

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

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

立即咨询