答辩前一晚,模型在测试集上跑出了 98.6% 的准确率,屏幕上那条 loss 曲线漂亮得让人心安。第二天现场演示,随手拿起一颗刚买的苹果放在摄像头前,系统斩钉截铁地打出“腐烂,置信度 94%”。全场安静了三秒。后来排查发现,训练集里“新鲜”这一类大多是白底商品图,而“腐烂”是自己在桌上用手机拍的,背景偏暖黄,模型学到的是背景色调,不是果皮状态。这类事故在做果蔬新鲜腐烂识别检测系统的同学里,出现频率远高于想象。基于深度学习的果蔬新鲜腐烂识别检测系统,说白了就是让模型看一张果蔬照片,判断它还能不能吃、坏到了什么程度。听起来是个标准图像分类任务,实际上从标签定义、数据采集到模型部署,每一环都埋着与通用分类任务完全不同的坑。下面这些内容适合正在做这个题目、或者准备做类似农产品视觉检测的读者,无论你只会跑通 MNIST 还是已经能自己改网络结构,都能从里面挑到能直接用的东西。
1. “腐烂”这个词本身没定义清楚,模型就永远学不明白
拿到题目的第一反应通常是把问题简化成二分类:新鲜 vs 腐烂。这个简化在开题报告里写起来很顺,落到数据标注上立刻卡住。一颗苹果侧面有一块指甲盖大小的褐色软斑,其余部分完好,它算新鲜还是腐烂?同一个人上午标成新鲜、下午标成腐烂的情况非常常见。这不是标注员不认真,而是“腐烂”本身是一个连续过程,而你要求模型输出的是离散类别。
1.1 二分类的崩塌往往从标注一致性开始
我见过一个比较典型的翻车过程:三个人分别标注同一批 2000 张图,先统一了口径“有明显霉斑或大面积变色算腐烂”,结果交叉验证的标注一致率只有 71%。也就是说,光是人跟人之间就有近三成的判断分歧,模型训练到 90% 准确率时,你根本无法判断那 10% 的错误是模型不行还是标签本身有噪声。更麻烦的是,这种噪声不是随机分布的,它会集中出现在“轻微变质”这个区间,而这恰恰是最有实用价值的区间——消费者和分拣线真正需要判断的就是“还能不能卖”。
处理办法不是硬扛二分类,而是先把连续状态离散化成有序等级。我建议至少分三档,条件允许就分四档:
| 等级 | 判定标准(以常见仁果类为例) | 数据特征 |
|---|---|---|
| 0 新鲜 | 表皮完整、色泽均匀,无可见斑点或仅有机械擦伤 | 纹理清晰,颜色饱和度稳定 |
| 1 轻微 | 局部出现小面积变色或轻微软化,面积占比低于 10% | 斑点边缘模糊,与正常组织对比度低 |
| 2 中度 | 明显褐变、霉点或软化区域,面积占比 10% 到 40% | 出现纹理断裂、颜色跃变 |
| 3 严重 | 大面积霉变、渗液、塌陷,面积占比超过 40% | 结构失真,反光异常 |
分档之后有个额外好处:如果你后面想把分类结果映射回二分类,只需要把 0、1 归为“可接受”,2、3 归为“不可接受”,随时能调。反过来,一开始只做二分类,后面想细分就几乎没有回头路,标注得全部重做。
1.2 不同果蔬的腐烂表现根本不是一回事
第二个容易被低估的问题是类内差异。叶菜类腐烂的典型表现是水渍状、发黄、边缘焦枯;柑橘类是青霉绿霉,长毛;苹果梨这类是褐色软腐;番茄则常常从蒂部开始凹陷。这几种视觉模式在纹理、颜色、形状上的差异,比“猫和狗”的差异还要大。如果你把所有果蔬混在一个 4 类模型里训,模型很容易学成“按种类分类”,而不是“按新鲜度分类”——因为种类这个特征太强了,抢占了全部判别容量。
实际可行的做法有两种。第一种是按品类拆分成多个子模型,比如叶菜模型、柑橘模型、仁果模型,每个模型内部只做新鲜度分级。这样单模型的任务变简单,准确率好看,工程上也容易扩展,缺点是模型数量多、维护成本高。第二种是保持单模型但把品类作为辅助标签做多任务学习,主任务是新鲜度分级,辅任务是品类识别,共享主干网络,两个损失加权求和。这种做法在论文里更有说头,实现上也不复杂,一个共享 backbone 加两个分类头即可:
import torch import torch.nn as nn from torchvision import models class MultiTaskNet(nn.Module): def __init__(self, num_fresh=4, num_kind=6, dropout=0.3): super().__init__() backbone = models.mobilenet_v3_large(weights='IMAGENET1K_V2') in_features = backbone.classifier[0].in_features self.features = backbone.features self.pool = nn.AdaptiveAvgPool2d(1) self.dropout = nn.Dropout(dropout) self.head_fresh = nn.Linear(in_features, num_fresh) self.head_kind = nn.Linear(in_features, num_kind) def forward(self, x): x = self.features(x) x = self.pool(x).flatten(1) x = self.dropout(x) return self.head_fresh(x), self.head_kind(x)训练时两个损失按 1.0 和 0.5 加权,主任务权重给高,辅任务起到正则化作用。实测下来,加入辅任务后主任务的验证集准确率通常能涨 1 到 2 个百分点,尤其是小样本品类受益明显。
1.3 标签体系的边界要在论文里写死
还有一件很多人到盲审才想起来的事:你的标签定义必须写进论文,而且要写得能让评审看懂你为什么这么分。不要只写“将数据集分为新鲜和腐烂两类”,要写清楚“以可见变色面积占比 10% 为阈值,由两名标注人员独立标注,分歧样本由第三人裁定”。这句话看起来是细节,实际上它直接决定了你的实验结果可不可信。评审如果追问“你怎么保证标签是对的”,你答不上来,整个实验部分的说服力就塌了。
2. 数据这块,公开数据集基本填不满你的需求
几乎所有做这个题的人第一步都是去搜现成数据集,然后发现能用的很少。网上确实存在一些植物叶片病害的图像集合,但那些大多是田间拍摄的叶片病害,跟采后储藏期的腐烂完全是两码事:前者背景是土壤和枝叶,后者背景是货架和包装箱;前者的病斑是病原侵染特征,后者更多是组织崩解特征。直接迁移过去,模型在你自己拍的照片上表现会掉得很厉害。
2.1 公开数据能借用的是方法,不是图像
我的判断是,公开数据最大的价值在于帮你快速搭起训练流程、验证网络结构能不能收敛,而不是充当最终实验数据。你可以用它跑通一遍从 dataloader 到推理的完整链路,确认环境没问题、显存够用、增强函数没写错。等这套流程顺了,再换成自己采的数据。这样做的另一个好处是,你手上会有一份“在公开数据上的基线结果”,论文里可以作为对比参照,说明你的模型在更贴近实际的场景下依然有效。
真正要用的是自采数据。这里必须强调一点:自采数据的工作量是这个题目最大的护城河,也是最容易糊弄、最容易在答辩时被问穿的地方。答辩老师问一句“你的数据从哪来的、采了多少天、有没有控制变量”,答得含糊,印象分直接掉。
2.2 同一个样本要连续拍,这是最容易被忽略的一条
绝大多数翻车案例的共同点是:每个类别拍一批不同的果蔬,新鲜的一批、腐烂的一批,然后混在一起随机划分。这种采法有个致命问题——模型学到的是“这两批照片之间的系统性差异”,比如拍摄日期不同导致的光照差异、果蔬品种差异、甚至包装箱差异。你以为模型在判断新鲜度,其实它在做批次识别。
正确的做法是纵向采集:选一批样本(比如 60 个苹果、40 个柑橘、30 把叶菜),从购入当天开始,每天或每两天在固定条件下拍一轮,连续拍 10 到 14 天,让同一个物理样本自然走完从新鲜到严重腐烂的全过程。这样每一档等级都有来自同一样本不同时间点的图像,模型没法靠批次特征偷懒。
纵向采集还带来一个额外要求:你必须给每张图记录样本 ID 和采集天数。没有这两个字段,后面的数据划分就无从谈起。
| 字段 | 示例 | 作用 |
|---|---|---|
| sample_id | A023 | 标识同一个物理样本 |
| day_index | 0 / 2 / 4 … | 采集时间点 |
| grade | 0–3 | 新鲜度等级 |
| kind | apple | 品类 |
| light | led / daylight | 光照条件 |
| device | phoneA | 采集设备 |
这张表看着朴素,但它决定了你的实验能不能站住脚。尤其是sample_id,它是后面做正确数据划分的唯一依据。
2.3 按样本划分还是按图片划分,这一个决定能差出 20 个百分点
这是我认为整个项目里最需要反复强调的一点。假设你有一个样本 A023,在 14 天里拍了 14 张图。如果按图片随机划分,很可能第 3 天和第 5 天的图分别进了训练集和测试集。这两张图拍的是同一个苹果、同一个背景、同一束光,差异只有一点点。模型在训练时见过这个苹果,测试时当然能认出来,准确率虚高得离谱。这不是模型能力强,这是数据泄漏。
正确的做法是按sample_id分组划分,同一个样本的所有图像只能出现在一个集合里。常见的比例是 7:1.5:1.5,但更重要的是保证每个集合里各等级都有足够样本。用sklearn的GroupShuffleSplit一行就能搞定:
from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=42) train_idx, temp_idx = next(gss.split(df, groups=df['sample_id'])) temp_df = df.iloc[temp_idx] gss2 = GroupShuffleSplit(n_splits=1, test_size=0.5, random_state=42) val_idx, test_idx = next(gss2.split(temp_df, groups=temp_df['sample_id']))我做过一次对照实验,同一份数据、同一个网络,按图片划分能到 96.8%,按样本划分只有 78.4%。这个差距就是数据泄漏的量级。论文里如果用了错误划分,方法部分写得再漂亮,实验部分也是虚的,而且一旦被问到就很尴尬。反过来说,如果你老老实实按样本划分,即使准确率只有 80%,这个数字是站得住的,你甚至可以在论文里专门写一段讨论“为什么本研究的指标低于部分文献”,把数据泄漏这个点讲出来——这反而是加分项,说明你真的理解自己在做什么。
2.4 数据增强要有节制,颜色类增强是重灾区
通用图像分类的教程里,颜色抖动、随机亮度对比度、色调偏移是标配增强。放到这个任务上,这些操作会直接破坏标签语义。腐烂程度的一个重要视觉线索就是褐变程度和饱和度变化,你把色调扰动 0.4,等于人为把新鲜样本的颜色改成了接近腐烂样本的颜色,标签就错了。同理,大角度的随机旋转和裁剪也可能把关键病灶区域裁掉,尤其是在轻微变质这一档,病灶可能只占画面很小一块。
我倾向于把增强分成两档,按任务严格程度选用:
- 安全增强:水平翻转、小角度旋转(±10 度以内)、小范围随机裁剪(scale 0.85–1.0)、轻微高斯噪声
- 慎用增强:色调偏移、饱和度抖动、大幅亮度变化、Cutout、Mixup
Mixup 和 CutMix 在这个任务上要格外小心,因为混合两张图生成的新样本可能在物理上不存在——一张新鲜图和一张腐烂图各取一半,混合出来的东西既不是新鲜也不是腐烂,标签变成噪声。如果非要用,建议只在中度、严重这两档之间混合,或者用带标签平滑的软标签方式处理。
光照条件反而应该作为采集时的控制变量,而不是靠增强模拟。固定用一盏色温 5000K 左右的补光灯,或者固定在靠窗同一位置的自然光下拍,背景用纯灰或纯白卡纸,相机高度和角度固定。这些看起来是笨功夫,但它能把模型的学习难度降一大截,比你在增强上折腾半天有效得多。
2.5 类别不均衡是必然的,但别急着上重采样
自然采集的数据一定不均衡:新鲜状态可能只占前两三天,而中度、严重状态能占后面七八天,如果按天均匀采样,后两档样本会多得多;反过来如果你刻意控制每档数量相同,又会破坏自然分布。我一般建议在采集阶段就做点规划,比如每天拍的时候保证每个样本都拍到,最后按等级统计一下数量,如果某一档不足其他档的 1/3,再考虑处理。
处理方式优先级我这样排:先试类别加权损失,再试 Focal Loss,最后才考虑重采样。加权损失改动最小,只在 loss 里给少数类更高权重,不影响数据分布:
weights = torch.tensor([1.0, 1.5, 2.0, 2.5]) # 对应 grade 0-3 criterion = nn.CrossEntropyLoss(weight=weights, label_smoothing=0.1)注意label_smoothing在这里很有用。因为你的标签本身就存在人为判断的模糊性,硬标签会逼着模型对边界样本过度自信,平滑一下反而让模型更稳。0.05 到 0.1 之间比较合适,再大就会明显拖慢收敛。
3. 模型选型:先想清楚你要分类还是要定位
很多人在开题时把“识别检测”四个字照抄进标题,但实际做的时候没想清楚是分类任务还是检测任务。这两者的差别不是换个损失函数那么简单,它决定了你的标注成本、模型复杂度和最终系统的形态。
3.1 分类和检测的取舍取决于你要回答什么问题
如果你的系统只需要回答“这张图里的果蔬整体处于什么状态”,那就是分类,标注成本低,一个人一天能标上千张,模型训练也快。如果你的系统需要回答“图里有三个苹果,左边那个坏了,右边两个是好的”,那就是目标检测,需要画框标注,成本高出一个量级,而且在小目标、密集堆叠的场景下框会重叠得很厉害,标注质量很难保证。
对于毕业设计这个体量,我的建议是先做分类,做得扎实,再考虑加检测作为扩展。分类做扎实的意思是:分级合理、数据划分正确、消融实验完整、可视化到位。这套东西的分量足够支撑一篇本科或硕士论文。如果一开始就奔着检测去,很可能框标了一半就没时间了,最后两头都不深。
如果确实要做检测,建议不要从零标注,而是先用分类模型在图上做滑窗或者用简单的阈值分割找到候选区域,再人工修正框。这样能把标注时间压缩到原来的三分之一左右。
3.2 主干网络的实测对比:别迷信参数量
我在一个约 8000 张图、4 个等级、6 个品类的自采数据集上做过一轮主干对比,硬件是一张 8GB 显存的消费级显卡,输入统一缩放到 224×224,训练 40 个 epoch,余弦退火,batch size 32。结果大致如下(准确率取验证集最优值,耗时是单张推理的平均值):
| 主干 | 参数量 | GPU 单张耗时 | CPU 单张耗时 | 验证准确率 | 备注 |
|---|---|---|---|---|---|
| ResNet18 | 11.7M | 6ms | 28ms | 81.2% | 收敛快,容易过拟合小类 |
| ResNet50 | 25.6M | 14ms | 96ms | 84.6% | 稳定,显存占用可接受 |
| MobileNetV3-Large | 5.5M | 8ms | 22ms | 82.9% | 性价比高,端侧首选 |
| EfficientNet-B0 | 5.3M | 11ms | 41ms | 84.1% | 对输入分辨率敏感 |
| ViT-B/16 | 86M | 31ms | 320ms | 83.5% | 小数据下不如 CNN 稳 |
| Swin-T | 28M | 26ms | 180ms | 85.3% | 效果最好但训练慢 |
几个值得说的观察。第一,ViT 系列在这类中小规模数据集上并不占优,它需要更多数据才能发挥注意力机制的优势,8000 张图明显喂不饱,训练后期 loss 波动很大。第二,MobileNetV3 的准确率和 ResNet50 只差 1.7 个百分点,但参数量只有五分之一,如果你的系统要跑在边缘设备上,这个差距完全可以用别的办法补回来。第三,EfficientNet-B0 对输入分辨率很敏感,我一开始用 224 得到 82%,改到 300 直接跳到 84.1%,因为它内部的复合缩放策略对分辨率有隐含假设。
选型结论我一般这么给:如果最终要部署到消费级硬件或边缘设备,选 MobileNetV3 或 EfficientNet-B0;如果只追求论文指标、部署在服务器上,选 ResNet50 或 Swin-T。不要一上来就上最大的模型,先跑通再说。
3.3 迁移学习的正确配置:分层学习率省掉一半调参时间
从头训练在这个数据量下基本不可能,必须用预训练权重。关键是分层设置学习率——主干网络已经学好了通用特征,只需要微调,学习率要小;新加的分类头是随机初始化的,需要大学习率快速收敛。
backbone_params = [] head_params = [] for name, param in model.named_parameters(): if 'head' in name or 'classifier' in name: head_params.append(param) else: backbone_params.append(param) optimizer = torch.optim.AdamW([ {'params': backbone_params, 'lr': 1e-4}, {'params': head_params, 'lr': 1e-3} ], weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=40, eta_min=1e-6 )再配合前 3 个 epoch 的 warmup,学习率从 1e-5 线性爬到设定值,能有效避免训练初期把预训练权重冲坏。这套配置我用了很多次,基本不需要怎么调就能收敛到一个不错的位置。
还有一个细节:冻结策略。常见的做法是先冻结主干只训头,训几个 epoch 再解冻全部。但在小数据集上,我实测发现直接全量微调配合分层学习率,效果往往比先冻结后解冻更好,因为冻结期间头部的梯度会累积出偏差,解冻后需要额外时间纠正。如果你想省事,直接全量微调就行。
3.4 训练过程中最容易误判的三个信号
第一个信号是训练准确率很快到 99% 而验证准确率停在 80% 左右。这时候大多数人的第一反应是加正则、加 dropout。但在这个任务上,更可能的原因是你前面的数据划分出了问题,验证集里有跟训练集同源的样本。先回去检查sample_id分组,再考虑正则化。
第二个信号是验证 loss 先降后升,但准确率还在涨。这通常说明模型在变得过度自信,对已经分对的样本给出越来越高的置信度,对分错的样本给出越来越低的置信度。这时候label_smoothing和早停比调学习率有用。我一般会在验证 loss 连续 5 个 epoch 不降的时候停下来,保存验证准确率最高的那个 checkpoint,而不是最后一个。
第三个信号是某些类别的召回率极低。这时候不要急着改网络,先把混淆矩阵画出来看清楚是哪两类在互相混。果蔬分级里最常见的混淆是 0 和 1、2 和 3,也就是相邻等级之间混,跨等级混淆很少见。这符合直觉,也说明模型其实学到了有序性,只是边界不清。针对性地在边界样本上做难例挖掘,比换模型有效得多。
4. 从“脚本能跑”到“系统能演示”,中间隔着一个工程量的鸿沟
训练脚本跑通只是走完了一半,剩下的工作量在很多人计划里被严重低估。答辩现场要的是一个能点、能上传图片、能出结果的界面,而不是一个 Jupyter Notebook。这一块做得好不好,直接影响评审对项目完成度的判断。
4.1 模型导出与推理加速的实际收益
PyTorch 训练出来的模型直接拿去推理,速度不是最优的。标准流程是导出 ONNX,再做量化或者转 TensorRT。这里给个实测参考:MobileNetV3-Large 在 CPU 上,原始 PyTorch 模型单张 22ms,导出 ONNX 后开启动态量化降到 9ms,模型体积从 21MB 压到 5.6MB,准确率掉不到 0.5 个百分点。
import torch.onnx model.eval() dummy = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, "freshness.onnx", input_names=["input"], output_names=["logits"], dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}}, opset_version=13 )量化用 ONNX Runtime 的quantize_dynamic就行,几行代码,不需要校准数据集:
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( "freshness.onnx", "freshness_int8.onnx", weight_type=QuantType.QInt8 )要注意的是,动态量化只量化权重,激活值还是浮点,所以加速比有限但精度损失极小。如果你要部署到边缘设备,可以进一步做静态量化,但需要准备一批校准图像,精度可能掉 1 到 2 个百分点,要权衡。
4.2 界面不需要好看,但必须让评审一秒钟看懂
界面这块我给的建议比较实用主义:用 Gradio 或者 Streamlit,二三十行代码就能出一个能上传图片、显示 Top-3 结果和置信度的页面。不要在这上面花两周时间做花哨的 UI,评审看的是功能闭环,不是设计感。
import gradio as gr import numpy as np GRADE_NAMES = ["新鲜", "轻微变质", "中度变质", "严重腐烂"] def predict(image): x = preprocess(image).unsqueeze(0) with torch.no_grad(): logits = model(x) probs = torch.softmax(logits, dim=1)[0].numpy() return {GRADE_NAMES[i]: float(probs[i]) for i in range(len(GRADE_NAMES))} gr.Interface( fn=predict, inputs=gr.Image(type="pil"), outputs=gr.Label(num_top_classes=4), title="果蔬新鲜度检测" ).launch()界面里有两个细节必须做。一是显示 Top-3 而不是只显示一个结果,因为模型在边界样本上本来就不该给出 99% 的自信,展示概率分布反而显得专业。二是加一个置信度阈值提示,比如最高概率低于 60% 时提示“结果不确定,建议人工复核”。这个小设计在答辩时是个加分点,说明你考虑了实际使用场景而不只是刷指标。
4.3 现场演示的保命方案
现场演示出问题的概率比想象中高。网络可能不通、摄像头可能对不上焦、光线可能和训练时差异很大。我一般会准备三层预案:
- 本地预置 20 张测试图,覆盖四个等级,直接从磁盘加载,不走摄像头
- 模型权重和 ONNX 文件都放在本地,不依赖任何在线服务
- 准备一段录屏,万一现场环境彻底跑不起来,直接放录屏讲解
还有个小技巧:演示前用现场的光线和设备拍几张图,快速看一眼输出。如果发现模型在新环境下偏移明显,临时在界面上加个白平衡校正,或者干脆只用预置图演示。这不是作弊,这是工程实践里正常的鲁棒性处理,只要你在论文里如实说明测试条件就行。
5. 论文和答辩:把工作量讲成工作量
做完系统只是把材料备齐了,怎么把这些材料组织成一篇能过审的论文、怎么在答辩时把问题答圆,是另一套技能。
5.1 实验矩阵要能回答“你为什么这么选”
很多论文的实验部分只有一张表,罗列最终准确率。这种写法的问题是,它只证明了“我的方法有效”,没证明“为什么这个方法比别的方法好”。评审最容易问的就是后者。
我的建议是搭一个三层实验矩阵。第一层是主干对比,回答“为什么选这个网络”,就是前面那张表。第二层是消融实验,回答“每个模块贡献多少”,把多任务学习、损失函数、增强策略逐个拆开看。第三层是错误分析,回答“模型在哪里失败”,用混淆矩阵和 Grad-CAM 可视化呈现。
Grad-CAM 这一项尤其值得做。它能直观显示模型在判断“腐烂”时关注的是果皮上的病灶区域,还是背景、包装、阴影。如果热力图集中在病灶上,说明模型学到了正确的东西;如果集中在背景边缘,那你的准确率再高也是可疑的。这张图放在论文里,说服力很强。
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layers = [model.features[-1]] cam = GradCAM(model=model, target_layers=target_layers) grayscale_cam = cam(input_tensor=x, targets=None) visualization = show_cam_on_image(rgb_img, grayscale_cam[0], use_rgb=True)5.2 对比实验要选对参照物
跟谁比,比什么,这个要想清楚。跟随机猜测比没有意义,跟其他深度网络比是常规操作,但我建议再加一组跟传统方法的对比,比如用颜色直方图加 SVM、或者用 LBP 纹理特征加随机森林。这类方法实现起来快,在你自己的数据上跑一遍也就半天,但它的价值在于说明“深度学习在这个任务上到底带来了多少提升”。如果传统方法能到 70%,你的深度模型到 84%,那 14 个百分点的提升就是有说服力的;如果传统方法也能到 82%,你就得反思是不是数据太简单了。
5.3 答辩时最可能被问的几个问题
按经验,这个题目被追问的频率最高的方向集中在三处。
第一处是数据来源和规模。“你的数据是自己采的吗,采了多少,标注怎么保证一致?”这个问题必须答得具体,把采集周期、样本数量、标注流程、一致性检验的数据都准备好。如果你做过标注一致率统计,这时候抛出来,效果非常好。
第二处是指标虚高。“准确率 95% 是不是过拟合了,有没有做交叉验证?”这时候你要能说清楚数据是按样本分组的、测试集是完全没见过的样本、并且给出测试集上的混淆矩阵。如果做过按采集天数的留出验证(比如用第 1 到 10 天的数据训练,第 11 到 14 天的数据测试),那更好,说明你考虑了时间维度上的泛化。
第三处是实用性。“这个系统能不能用在分拣线上,速度够不够?”这时候你要能给出推理耗时的实测数据,以及在不同硬件上的表现。如果只做过 GPU 推理,就如实说 GPU 上的数字,同时说明量化后在 CPU 上的预期表现,不要硬吹。
5.4 一个容易被忽略的加分项:系统边界
论文里如果专门写一小节讨论系统的适用范围和失败案例,评审的印象会明显不同。比如说明“本系统在单一品类、受控光照条件下表现稳定,在多品类混合、复杂背景、强反光条件下准确率下降约 15 个百分点”,再配几张失败案例图,分析是因为反光遮挡了病灶、还是因为品类差异超出了训练分布。这种坦诚的表述不会减分,反而证明你对自己做的东西有清醒认识。我在实际操作中的体会是,凡是主动写出局限性的部分,答辩时反而没人追问;凡是把话说满的地方,一定被追着问。
最后分享一个我觉得挺有用的小做法:把所有实验的随机种子、配置文件、数据划分索引都存进一个版本目录,每次跑实验前 git commit 一次,实验名带上 commit 短哈希。这个习惯在做消融实验那几天能救命——你会发现三天前那个 85.3% 的结果,配置文件里到底改了什么,翻一眼就知道,不用凭记忆去猜。果蔬这个东西放两天就变样,数据是没法重采的,所以能复现比什么都重要。