简介:这是一套基于ShuffleNet轻量级CNN的菠萝成熟度分类实战项目,面向图像分类初学者与轻量网络应用开发者,解决8种不同阶段(如没熟、半熟、成熟等)果实的自动识别问题。压缩包为7z格式,共2000个文件,内含1992张jpg图片、4个Python脚本、2个txt说明、1份readme和1个json配置,整体约201MB,训练集4808张、测试集806张,目录按类别分好,解压即可直接运行,数据集为真实菠萝图像,覆盖不同成熟阶段的视觉差异。目前已有125人学习浏览。项目使用约一百万参数的ShuffleNet,配合cos学习率衰减训练50个epoch,测试集最高精度达87%,run_results中存有最优权重、训练日志及loss/精度曲线;预测时可直接使用predict脚本,自动推理inference目录下所有图片并标注前三概率类别,方便快速查看分类结果。readme还说明了训练自定义数据的方法,代码会自动生成类别个数等配置,便于迁移到其他分类任务,无需手动修改网络结构。
1. 用 ShuffleNet 给菠萝成熟度分类:8个阶段分得准,难点不在模型
在水果催熟库、分拣线或是电商质检现场,要上一套“8种不同阶段菠萝成熟度分类”的视觉系统,最务实的起点不是堆算力上大模型,而是用 ShuffleNet 这类轻量级 CNN 网络,在相机端或本地小主机上实时跑出结果。ShuffleNet 参数少、推理快,CPU 上也能达到实时帧率,预算和部署环境都很友好,这也是图像分类实战项目里最常见的落地口径。但这个题目真正卡人的不是网络结构,而是“8个成熟阶段”怎么定义、怎么标注——适熟和完熟可能只差半天光照,人眼都会判错。这篇笔记面向准备做端侧图像分类落地、以及刚入深度学习想完整跑一个 CNN 项目的人,我会把选型、数据、训练和踩坑完整铺开。
2. 轻量级网络的工程选型:这道题为什么选 ShuffleNetV2 而不是 MobileNet
2.1 三个选型维度:先卡算力,再看吞吐,最后看精度底线
做图像分类实战,第一件事不是看榜单,是看现场设备。常见部署端有三种:摄像头内置芯片(海思、瑞芯微这类)、工控机 CPU、低端 GPU 盒。每种设备对应的模型体量完全不同,先确认“模型单帧推理要跑到多少毫秒以内”,再谈选哪个网络。比如在四核 ARM 板子上跑实时视频流,单帧推理最好控制在 80ms 以内;在 x86 工控机上可以放宽到 200ms。这个门槛没定,选 MobileNet 还是 ShuffleNet 都是空谈。
第二步看吞吐,也就是同一设备上能同时跑多少路视频。摄像头多路接入时,网络结构的内存访问量(MAC)比 FLOPs 更关键,因为内存带宽往往是瓶颈。现在工业相机一路就能吃掉不少带宽,多路并行时模型对缓存的友好程度直接决定能不能稳定跑满帧率。ShuffleNetV2 设计时把“内存访问成本”当作核心优化目标,这一点在真实设备上比纯算力数字更可信。我习惯先拿一个最小模型在目标设备上实测一遍,而不是对着理论 FLOPs 做推断。
最后才是精度底线。菠萝成熟度分类的错误代价不是对等的:把“未熟”判成“完熟”会导致售后投诉,把“完熟”判成“变质”会造成损耗。所以选型时不能只看测试集平均准确率,要看“关键相邻阶段”的类别召回率。轻量级模型在单一特征(颜色、纹理)差异大的任务上,和大模型的差距非常小;成熟度分类恰恰是这种任务,没必要为了零点几个点的准确率换上几千万参数的模型,边际收益很低,部署成本却直线上升。
2.2 ShuffleNetV2 的四条设计准则:怎么做到又快又准
ShuffleNetV2 有四个在工程上很好记的准则:一是输入输出通道相等时,内存访问成本最低;二是过多的组卷积会增加内存访问,需要控制组数;三是网络碎片化会降低并行度,结构要尽量“直”;四是逐元素操作(ReLU、Add)虽不算 FLOPs,但占用时间,要精简。这四条不是论文里的空理论,直接对应到我们在板子上看到的真实延迟。
通道混洗(Channel Shuffle)是这个网络最特别的操作。分组卷积把通道切成几组,每组各学各的,组与组之间信息不互通;混洗就是把通道顺序打乱,让下一层的每组都能拿到上一层各组的信息。用打牌来记最直观:一副牌分成几摞,洗一下再重新发,每个人手里的牌就来自不同摞了。代码实现上只需 reshape 加 transpose,非常轻量。这也是 ShuffleNet 命名和精度的核心,比 MobileNet 的深度可分离卷积多了一步“信息打通”。
理解到这一步,就明白为什么在“类别多但特征集中在颜色、纹理”的任务上,ShuffleNet 的精度不容易掉:它把通道间信息交换做得很充分,而成熟度判别的关键特征(果皮颜色、果眼密度、光泽)恰好分布在多个通道组合里。很多人在实际项目里踩过另一个坑:组卷积里组数设得太大,通道碎片化反而拖慢了速度。ShuffleNetV2 在结构上刻意控制了组数上限,这也是它在大核数 CPU 上仍然能保持稳定性能的原因。
2.3 与 MobileNetV3、EfficientNet-Lite 的对比:边界在哪
| 对比项 | ShuffleNetV2 | MobileNetV3 | EfficientNet-Lite |
|---|---|---|---|
| 内存访问控制 | 重点优化 MAC | 一般 | 一般 |
| 结构复杂度 | 简单,适合手写复现 | 含 SE 模块,稍复杂 | 复合缩放规则偏复杂 |
| 并行度 | 较高,碎片少 | 中等 | 较低,分支多 |
| 常用宽度因子 | x0.5 / x1.0 两档 | small / large 两档 | b0 / b1 两档 |
| CPU 部署效果 | 好,老式 ARM 也稳 | 好,但 SE 模块在 CPU 上有代价 | 取决于框架对缩放规则的支持 |
实际选型时我一般按这个顺序判断:设备只有 CPU 且在意延迟,优先 ShuffleNetV2;设备是手机端且有厂商 NPU,MobileNetV3 更顺手;如果精度要求高、且允许 GPU,才考虑 EfficientNet-Lite。对菠萝成熟度这种项目,ShuffleNetV2 的性价比最高,后面所有内容都按它来写。别急着追最新的图像分类模型榜单,先把手头设备跑通,比多刷一个百分点的验证集更有价值。
3. 菠萝成熟度 8 分类的数据整理:标注规范、目录组织与训练集划分
3.1 先定8个阶段的标注标准:拍什么、怎么标、哪些别拍
分类必须先定义边界。8 个成熟阶段不是随便贴标签,每个阶段都要有可观察、可复现的外观描述,我把标准整理成下面这张表,拍照前先跟质检同事过一遍:
| 类别编号 | 阶段名 | 外观可判特征 | 不建议拍入该类的边界情况 |
|---|---|---|---|
| 0 | 深绿未熟 | 果皮整体深绿无光泽,果眼棱角硬 | 果皮颜色不均匀但整体偏绿 |
| 1 | 浅绿半熟 | 绿色变浅,微亮,果眼间隙开始饱满 | 有明显黄色斑块的绿果 |
| 2 | 转色初期 | 绿色为主,黄色占比低于三成 | 黄色占比刚好一半的“骑墙”样本 |
| 3 | 转色中期 | 黄绿参半,黄略大于绿 | 花萼处带黄但果身全绿 |
| 4 | 适熟 | 黄色为主,果眼平,按压微弹 | 过软或出现褐斑的样本 |
| 5 | 完熟 | 金黄、有香气,果眼平且边缘略深 | 稍有褐斑但果肉完好的临界果 |
| 6 | 过熟 | 出现褐斑或褐纹,局部软塌,光泽下降 | 褐斑显著、有流汁倾向的归为 7 |
| 7 | 变质/损伤 | 霉斑、发黑、流汁、明显机械伤 | 只有碰伤但无霉变的归为过熟 |
一个重要的经验:不要在拍照现场“随手标”,先让负责质检的同事把临界样本挑出来,集体定完标准再整批标注。否则后面会有一堆骑墙样本,模型训练时同一张照片换个人标就换类,数据噪声直接体现在测试集上。每个阶段拍照时不只拍正面,侧面、顶部花萼区、底部果眼区各来一张,同一个果实至少拍 2 到 4 张,为后面按“果”划分数据集做准备。
3.2 采集与增强策略:光照、角度和遮挡怎么补齐
采集时每个成熟阶段至少要有 300 到 500 张图,注意覆盖不同时间段、不同天气、不同产季的光线条件。拍摄距离和分辨率尽量统一,建议不低于 640x640,后期缩到 224 时才不容易丢纹理信息。同一个菠萝的多个视角不是为了凑数量,是为了让模型学会“从多个角度判断成熟度”,而不是只记住某一个拍摄姿势。
在线增强是补数据不足的主要手段,但参数要谨慎,下面这个是我常用的训练增强配置:
import torchvision.transforms as T train_transform = T.Compose([ T.RandomResizedCrop(224, scale=(0.75, 1.0), ratio=(0.9, 1.1)), T.RandomRotation(degrees=15), T.ColorJitter(brightness=0.25, contrast=0.25, saturation=0.05, hue=0.02), T.RandomHorizontalFlip(p=0.3), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])参数说明:RandomResizedCrop 的 scale 控制截取区域占原图比例,给它 0.75 到 1.0 是为了保留果皮整体颜色分布,截得太小会只拍到局部,成熟度信息反而丢失;饱和度抖动只给 0.05,因为成熟度分类高度依赖颜色,抖动太大等于把正确答案改错;翻转只做水平翻转,不做上下翻转,菠萝果柄和果眼的方向是有语义的,翻开会制造假样本。
验证集和测试集只做 Resize 到 224 和 Normalize,不要加任何随机增强,否则评估结果每天都不一样。这一点在多人协作时特别容易翻车,有人图省事直接复制了训练增强的配置去跑测试,最后报告出来的准确率根本没法复现。
3.3 生成 train/val/test 划分脚本:按“果”分组,防数据泄露
划分训练集最典型的错误是用随机划分,导致同一颗菠萝的照片同时出现在训练集和验证集。成熟度的颜色变化在相邻阶段本身就接近,这种数据泄露会让验证集虚高,部署后掉点明显。正确做法是先给每个菠萝一个 unique_id,再按 id 分组划分。我用 GroupShuffleSplit 来实现,脚本如下:
from sklearn.model_selection import GroupShuffleSplit from glob import glob import os # 文件名格式:菠萝ID_类别_序号.jpg,例如 A001_3_02.jpg image_paths = glob("raw/*.jpg") group_ids = [os.path.basename(p).split("_")[0] for p in image_paths] labels = [int(os.path.basename(p).split("_")[1]) for p in image_paths] gss = GroupShuffleSplit(n_splits=1, test_size=0.3, random_state=42) train_idx, temp_idx = next(gss.split(image_paths, labels, groups=group_ids)) # 从临时集合里再切一半作为验证集,一半作为测试集 val_ratio = 0.5 val_count = int(len(temp_idx) * val_ratio) val_idx = temp_idx[:val_count] test_idx = temp_idx[val_count:] print(f"train={len(train_idx)}, val={len(val_idx)}, test={len(test_idx)}")这段代码先把 30% 的整组样本拆出来留作验证加测试,再把这 30% 一分为二,得到 15% 验证、15% 测试。关键在 GroupShuffleSplit 的 groups 参数:它保证同一个菠萝 ID 的所有图片只会出现在一个集合里,不会一半在训练一半在验证。random_state 固定是为了实验可复现,调参时不会因为数据切分不同而无法比对。
val_idx 的数量可以用 len 打出来确认。常见坑是 GroupShuffleSplit 在 group 数量太少时给出的验证集合特别小,此时宁可就地重拍几组新果,也别靠复制贴图硬撑。
3.4 脏样本清洗:类别不平衡检查与颜色直方图排错
划分完先统计每类数量。菠萝成熟度采集时“转色初期/中期”往往很好拍,“变质/损伤”很难拍到足够数量,这类样本少是正常的,要在训练阶段做平衡采样而不是靠重复复制硬凑。如果某一类少于总样本的 5%,我就认为它不足以单独训练,要么补拍,要么把相邻阶段合并后重新设计类别。
颜色直方图是排查脏样本最快的手段。同一个成熟阶段如果有照片颜色分布明显离群,多半是白平衡偏差或滤镜导致,直接删掉。我还会按文件大小、清晰度(用 Laplacian 方差判断模糊)再过滤一轮,模糊样本不标注,硬标进去只会让模型更困惑。模糊样本单独放一个 unlabeled 目录,等后续补拍再人工决定归属。脏样本清洗没有玄学,就是拿工具把每一类的分布都过一遍眼睛,模型后面的收敛速度会好很多。
4. ShuffleNetV2 训练与调参:跑通最小实验,再谈收敛
4.1 用 PyTorch 搭一个最小训练脚本
先跑通、再调优。最小训练脚本不需要花哨,核心是把数据读进来、模型改好、跑几个 epoch 看到 loss 下降即可。ShuffleNet 在 PyTorch 里有官方实现,直接用 torchvision 加载预训练权重,再替换最后的全连接层。
import torch import torch.nn as nn import torchvision.models as models from torch.utils.data import DataLoader def build_model(num_classes=8): # pretrained 使用 ImageNet 预训练权重,成熟度分类也适用 # 新版 torchvision 推荐用 weights=..., 这里保留常见旧式写法 model = models.shufflenet_v2_x0_5(pretrained=True) in_features = model.fc.in_features # 最后一层原来输出 1000 类,换成自己的 8 分类头 model.fc = nn.Linear(in_features, num_classes) return model model = build_model(num_classes=8) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() # 微调经验值:SGD 动量0.9,权重衰减 4e-5,初始学习率 0.01 optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=4e-5) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=80, eta_min=1e-5) for epoch in range(1, 81): model.train() train_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() train_loss += loss.item() * images.size(0) scheduler.step() val_loss, val_f1 = evaluate(model, val_loader, device) print(f"epoch {epoch:2d} | train_loss {train_loss/len(train_loader.dataset):.4f} " f"| val_loss {val_loss:.4f} | val_macro_f1 {val_f1:.4f}")逻辑说明:预训练模型前三层学到的底层特征(边缘、棱角、颜色过渡)对菠萝同样有效,直接在 ImageNet 权重上微调比随机初始化收敛更快,也更能稳住早期训练。替换最后一层是因为原模型输出 1000 类,而这里要 8 类。交叉熵损失直接用在全连接层输出上,不需要额外做 softmax,PyTorch 的 CrossEntropyLoss 内部已经包含 log_softmax 与 NLLLoss 的计算。
参数说明:SGD 配动量和余弦退火是图像分类微调里最稳的组合;Adam 收敛虽快,但后期在类别不平衡数据上容易出现验证集指标波动。学习率 0.01 对预训练模型来说偏大,但它配合余弦退火会在 80 epoch 内平滑降到 1e-5,前段能快速适应菠萝数据,后段能精细收敛。batch size 这个脚本按 64 设计,显存不够就降到 32,但要注意降低 batch 时学习率也要等比调整,否则收敛不稳。
4.2 四个必调参数:lr、batch size、weight_decay、学习率策略
| 参数 | 推荐起点 | 调节方向与理由 |
|---|---|---|
| 初始学习率 | 0.01(SGD) | 预训练微调用 0.005~0.02;从随机初始化开始则用 0.1 基准值 |
| batch size | 64 | 显存紧张降到 32;此时 lr 大约减半,避免单批梯度噪声变大 |
| weight_decay | 4e-5 | 值太大会让模型欠拟合,值太小则后期 val_loss 反复弹跳 |
| 学习率策略 | CosineAnnealingLR | 不用 StepLR 的原因:余弦退火在类别不平衡时后期更稳,不容易卡在平台期 |
权重衰减对 ShuffleNet 的影响比想象中大。这个网络本身结构紧凑,如果 weight_decay 取 1e-3 这种偏大的值,后期几乎每个卷积核都被拖到接近零范数,特征表达被削平。常见做法是先按 4e-5 跑一个基准,观察 train_loss 和 val_loss 的间距,间距大说明过拟合,再加到 1e-4;间距很小甚至 val 比 train 好,就维持或降低。
另外,多卡训练时 batch size 翻倍,学习率最好也按比例翻倍。单卡 64 与双卡 128 不能直接用同一学习率,否则收敛路径会变。这些参数没调好,往往不是网络结构的问题,而是优化器与批量大小没对齐。
4.3 早停与模型保存:按验证集宏F1选模型,别只看val_loss
训练过程中每轮都在 val_loader 上计算指标。成熟度分类类别不平衡,val_acc 会被多数类主导,而 val_loss 对错分“变质/损伤”这种少数类不敏感。所以我一般把验证集的宏 F1(macro F1)当作选模型指标,它对每个类平等看待,少数类拉胯时它会明显掉。
def evaluate(model, loader, device): from sklearn.metrics import f1_score all_preds, all_labels = [], [] model.eval() with torch.no_grad(): for images, labels in loader: images = images.to(device) outputs = model(images) preds = outputs.argmax(dim=1).cpu().tolist() all_preds.extend(preds) all_labels.extend(labels.tolist()) return f1_score(all_labels, all_preds, average="macro")保存模型时不要只存 state_dict,把预处理参数也一起存:训练时用的 mean、std、输入尺寸、类别顺序。工程部署时常常会忘记这些细节,到时候在推理脚本里重新翻训练代码找参数,非常浪费时间。一次存好,推理端直接读配置文件,这是实战项目里很值得坚持的小习惯。
早停的 patience 设置 15 到 20 epoch。在成熟度分类这种任务上,模型在 30 到 60 epoch 之间还会持续提升,patience 设 5 容易在模型还没收敛完就停了。patience 设 20,配合余弦退火,基本能让你放心地跑满 80 epoch 再选点。
5. ShuffleNet 训练避坑:5个典型现象与排查记录
5.1 现象:训练 loss 下降缓慢,val acc 卡在 30% 附近
loss 一直在 2.0 以上徘徊,准确率始终在 1/8 上下,说明模型基本没学到东西。最常见原因不是网络坏了,是标签写错:类别编号从 1 开始而不是 0,CrossEntropyLoss 默认 ignore_index 是 -1,对第 0 类不惩罚,模型就自动放弃这一类。如果 train_loader 里 labels 范围是 1 到 8,而模型输出 8 类,输出索引 0 永远不会被训练,自然永远预测不了 0 类。
解决方法很简单,打印确认:
print(ys.min(), ys.max(), len(set(ys.tolist())))如果输出是 1 8 8,说明标签从 1 开始,需要把全部标签转换为 0 到 7。另一个次常见原因是预处理 Normalize 的 mean/std 写成了 [0,0,0] 和 [1,1,1],图像没有归一化,梯度信号发散,同样会 loss 跌不下去。用 ImageNet 标准值是安全的。
5.2 现象:验证集宏F1挺高,一到测试集掉点严重,翻车率明显
这是典型的数据划分泄露。统计时看起来 val 有 0.94,测试集只剩 0.86,且通常不是个别类掉,而是所有类都掉 2 到 5 个点。原因是同一个菠萝在不同角度、不同光照下的照片被随机划分到了训练集和验证集,验证时相当于模型见过同源的近亲样本。成熟度的相邻阶段本身差异极小,这种同源数据泄露特别隐蔽。
解决思路在 3.3 已经铺过:用 GroupShuffleSplit 按菠萝的唯一 ID 分组划分。如果项目是后期才发现的,修补方式是直接把训练集里同组图片全部挪到验证集,重新训练一次。你会发现 val 掉了一些,但测试集和 val 差距明显缩小,这才是真实水平。
5.3 现象:增强一加强就过拟合,不增强又欠拟合
为了数据更稳,我曾把 ColorJitter 的 saturation 调到 0.3,结果训练集 loss 能降到 0.05,验证集反而更差。成熟度分类几乎完全靠颜色,饱和度抖动 0.3 意味着把“适熟”的黄色调到接近“完熟”的金黄,等于在训练集里制造了大量标签错误的样本,模型只能靠死记硬背来拟合,自然过拟合。
解决:把 saturation 和 hue 压到很小,brightness 和 contrast 可以适度保留。我常用的值是 saturation=0.05、hue=0.02、brightness=0.25。还想加鲁棒性,就加 RandomResizedCrop scale=(0.75, 1.0),让模型适应不同观察距离,而不是去改变颜色分布。
5.4 现象:8分类模型只会输出两三类,几乎不预测“过熟/变质”
训练完成后用测试集一数,发现预测结果集中在 0、2、5 三个类,6 和 7 几乎没有预测。原因通常有两类:一是 6/7 类样本数量太少,模型在损失中不太在乎它们;二是 6/7 类样本本身集中在某个拍摄条件里,模型没有看到多样化环境下的样本。
解决分三步:先统计类频数,少的那几类做过采样或 WeightedRandomSampler;再检查这几类的图片是否只有单一背景或单一光照,如果是,就去补拍不同环境下的样本;最后评估时改看宏 F1,别让极少数类的表现被淹没在整体准确率里。这个阶段不要急着加网络复杂度,数据补上之后 ShuffleNet 通常会立刻见效。
5.5 现象:CPU 推理延迟比预期高一截,帧率上不去
ShuffleNet 的 FLOPs 数字很漂亮,但在老式 CPU 上跑 PyTorch 模型时,延迟往往比理论值高不少。原因在于通道混洗的实现涉及张量 reshape、transpose、chunk,这些操作在 CPU 上会产生额外的内存搬运,而且 PyTorch 的逐个算子调度会放大碎片化开销。多线程环境下一路 CPU 推理反而比单线程更慢,也是常见现象。
解决时我一般分三步走:先把模型转成 ONNX,再用框架的 CPU 加速后端跑,x86 上用 OpenVINO 或 ONNX Runtime 的 CPU 加速算子,ARM 板上尝试 NCNN。如果项目允许量化,把权重转成 int8 精度损失通常不到 0.5%,但延迟能再降一半。注意转换后要重新跑一遍测试集,排除量化掉点过大的情况,尤其是 6/7 这种纹理细节多的类别。
6. 进阶一步:用混淆矩阵找成熟度阶段边界,让准确率再提 3 个点
6.1 混淆矩阵要看“相邻阶段交叉”,不是只看对角线
模型训练结束后,先别急着部署,跑一遍测试集混淆矩阵。常见结果是相邻阶段的错分特别扎眼,0/1 之间、1/2 之间、3/4 之间,这类错分并不是模型不行,而是标注时“阶段边界”在不同人手里不一致。我之前遇到过“适熟”和“完熟”错分最多的案例,把错分样本调出来看,发现果皮颜色确实接近,差异只在果眼平展程度和光泽——这些在现场图片里很容易被光照掩盖。
遇到这种相邻错分,有效的处理不是继续堆训练,而是把错分样本整理成一份“边界复核表”,发给一线质检人员逐张确认。确认之后有两类做法:如果错分样本里确实有一半标错,就修正标签后重新训练;如果标签没标错,说明这两个阶段在图像上确实过于接近,考虑把这两类合并成一个大类(如“适熟-完熟”),模型输出的业务价值反而更高。这一步通常能一次性把对应类别的准确率拉高 3 到 5 个点,比调任何训练参数都明显。
6.2 用置信度阈值做二次校验:不确定就让现场人工看
最后一步是给推理端加置信度阈值。部署场景里最有价值的不是让模型“硬判”,而是让它知道自己“不确定”。我一般会为每个阶段单独设阈值,因为不同阶段的难易程度不同。
| 阶段 | 阈值 | 低于阈值时的现场动作 |
|---|---|---|
| 深绿未熟 | 0.85 | 默认输出,复核果柄断面 |
| 转色初期 | 0.75 | 弹窗提示人工复核 |
| 适熟 | 0.80 | 交由质检员目检 |
| 过熟/变质 | 0.90 | 必须人工复核,防止漏判 |
低于阈值的样本不直接进入分拣动作,而是归入“待人工复核”队列。这条规则看着简单,实际让整个系统的错误率下降了一个量级,因为机器承担了九成的简单分拣,剩下的一成临界样本由人来接手。阈值可以在部署后用小批量样本滚动调,不用追求一次到位。
我第一次做这个项目时特别迷信训练指标,把 val acc 刷到 0.95 就急着上线。后来才发现真正让现场验收满意的是“误判可控”——模型拿不准时主动说不知道,比全盘硬判更让人放心。现在我的习惯是先跑混淆矩阵,再定阈值,最后才回头看训练参数。希望这个思路对你的菠萝成熟度项目也有帮助。
本文还有配套的精品资源,点击获取