
把 ResNet50 在自建数据集上训到 92% 的验证准确率导出模型丢进投放点上方那颗摄像头里跑第二天翻日志真实图片上的准确率只剩六成出头——落差几乎全部来自“训练时看到的图和现场拍到的图不是一回事”。基于卷积神经网络的垃圾图像分类系统听起来是一个标准的图像分类作业实际上它把数据工程、类别体系设计、迁移学习策略和端侧部署的坑全凑齐了。这篇内容面向已经能跑通分类 Demo、准备把模型推到真实场景的人也面向刚接手这类课题、想知道除了调参之外还要处理什么的人。我会把从类别体系怎么定、数据怎么攒、骨干网络怎么挑到训练故障怎么排查、指标怎么看、模型怎么落地这一整条链路拆开讲中间穿插一些常规文档里不会写、但真做起来一定会撞上的问题。1. 这个任务的难点从来不在网络结构1.1 输出到底给谁用决定了后面所有设计动手写第一行代码之前有一件事必须和需求方对齐模型的分类结果是给人看还是给机器用。这两个方向的差别大到会影响数据采集量和模型容量。如果结果是给投放的人看屏幕上弹一句“请投放到其他垃圾”那么四分类可回收物、厨余垃圾、有害垃圾、其他垃圾基本够用模型只需要在粗粒度上判断对偶尔把纸巾认成塑料袋用户体验的损失有限。如果结果要驱动分选线、机械臂或者称重计费粒度就得往细里走纸、塑料、玻璃、金属、织物往往要各自成类塑料还要再分 PET、HDPE、PP因为不同材质后续的处理工艺完全不同。我在实际项目里见过一种很常见的前期失误直接拿开源的四分类数据集训一个模型然后发现下游需要区分“干净的可回收塑料瓶”和“被油污污染的一次性餐盒”这两个东西在四分类标签体系里根本没有对应的格子模型再怎么调也表达不出来。所以类别体系的定义要写进项目文档明确每个类别的判定条件、边界案例、以及和当地投放标准的一致性这份文档的详细程度直接决定了后面标注质量的上限。1.2 塑料袋、纸巾、纸杯真实世界的边界比标签模糊得多图像分类的教科书例子是猫和狗类别之间界限清晰视觉特征差异明显。垃圾图像完全不是这样。一次性纸杯多数地方算其他垃圾因为内壁有淋膜但一个干净的、没有被污染的纸杯在不同地区的标准里可能被归到可回收。保鲜膜和塑料袋的视觉差异极小都可能是透明的、带褶皱的薄膜。更要命的是“污染程度”这个维度一个干净的塑料瓶和一个沾满油污的塑料瓶在很多标准里归类不同但两张图的像素差异可能只有瓶身一小片油渍。这带来一个非常具体的技术后果——标签噪声不是随机的而是系统性的。同一个人在不同时间看到同一张图可能给出不同标签不同标注员对“污染到什么程度算污染”的理解也不一样。随机噪声还能靠模型容量和正则化扛一扛系统性噪声会直接把模型往错误的方向拉因为模型会去学“油污的纹理”而不是“瓶子这个物体”。解决这件事只能靠标注规范前置而不是靠后面换个损失函数。1.3 训练分布和部署分布之间的那条鸿沟开源垃圾数据集里大量图片是白底、正面、居中、光照均匀的单物体图构图接近商品拍摄。投放点摄像头拍到的是什么俯拍、逆光、阴影、物体互相遮挡、套着袋子、堆叠在一起、广角镜头边缘畸变。同一个塑料瓶在数据集里是白底正面照在摄像头里是一个被压扁的、带反光的、只露出三分之一的深色形状。这种分布漂移是实验室指标和现场指标差距的主因。还有一个容易被忽略的因素是摄像头本身。消费级摄像头的自动白平衡和自动曝光会在不同时段对同一个物体给出差异很大的颜色早上八点和下午六点的同一袋垃圾色温可能完全不同。如果训练时用的增强手段只有随机裁剪和翻转模型对色温变化的鲁棒性基本为零。这一点在后面的数据增强小节会再展开。2. 数据这一关过不去后面全是无用功2.1 采集渠道的拼盘与必须做的清洗动作数据来源通常是三块拼起来开源数据集、网络图片、自己拍。开源数据集的好处是量大、标注现成问题有三个——类别体系和你的对不齐、国外垃圾包装和国内差异明显、白底单物体图占比过高导致分布严重偏斜。网络图片的问题是真假混杂很多是宣传图、插画、带水印的电商图。自己拍的质量最好但成本最高需要覆盖不同时段、不同天气、不同角度。拼起来之后必须做一轮机械清洗这一步不要省。我常用的动作是用感知哈希pHash做近重复检测把同一张图的不同尺寸版本剔掉否则同一物体在训练集和验证集各出现一次就是数据泄漏用拉普拉斯方差筛掉模糊图方差低于阈值的直接丢弃再按文件大小和分辨率过滤掉缩略图级别的图片。这三步做完通常会去掉 5% 到 15% 的数据但换来的是训练过程的可信度。import cv2 import numpy as np from PIL import Image import imagehash def is_blurry(path, threshold100): img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: return True return cv2.Laplacian(img, cv2.CV_64F).var() threshold def dedup_by_phash(paths, max_dist5): seen [] keep [] for p in paths: h imagehash.phash(Image.open(p)) if all(h - s max_dist for s in seen): seen.append(h) keep.append(p) return keep2.2 标注一致性比标注数量重要得多一个常见的误区是拿标注量当 KPI标了五千张就很满意但一测一致性发现两个标注员之间只有 0.6 的 Kappa 系数意味着这批标签本身就在互相打架。我的做法是让两个人独立标同一批 300 张的样本算 Cohens Kappa低于 0.75 就说明标准文档写得不够细要回去补边界案例而不是催标注员加快速度。标准文档要写到什么程度举个例子“塑料瓶”这个类别需要明确瓶盖算不算、瓶身标签要不要看、压扁的算不算、带残留液体的算不算、瓶身被撕掉一半的算不算。每一条都要配一张示例图。这份文档写两三天能省掉后面两三周的返工性价比极高。2.3 长尾分布是常态处理手法要分场景真实采集的数据几乎不可能均衡。纸张、饮料瓶这类高频物品很容易攒到几千张电池、灯管、过期药品这类有害垃圾可能只有一两百张。类别采集到样本量占比典型问题塑料瓶320038%背景单一白底图过多纸类210025%与纸杯、纸巾混标玻璃90011%透明物体边缘难辨金属罐7809%与塑料瓶外形相似织物6207%与塑料袋纹理接近有害垃圾3805%样本极少形态差异大其他4205%类内差异极大处理长尾有三条路各自的代价不同。重采样最直接对少数类做随机过采样或者对多数类做欠采样问题是过采样容易过拟合欠采样会丢信息。类别加权损失函数更温和把交叉熵里的权重按类别频次反比设置效果稳定但收益有限。针对性补采是最根本的办法专门去收集有害垃圾的样本哪怕用摆拍的方式补充一部分也比在算法上折腾半天强。合成数据要谨慎把电池贴到不同背景上生成样本很容易让模型学到“边缘不自然”这个伪特征反而损害泛化。3. 骨干网络选型和训练策略的真实取舍3.1 从 ResNet 到轻量骨干选的是约束不是精度很多人一上手就选最大的模型觉得精度高总没错。垃圾图像分类的实际约束通常来自部署端投放点上的设备可能是一块低功耗边缘板子算力和内存都有限模型再准跑不动就是零。所以骨干网络的选择应该从部署条件反推。骨干网络参数量约ImageNet Top-1单张推理耗时相对值适用场景MobileNetV3-Large5.4M75.2%1.0边缘设备、实时视频流EfficientNet-B05.3M77.1%1.2边缘设备、精度稍优先ResNet5025.6M76.1%3.5服务器端、批量离线分析ConvNeXt-T28.6M82.1%4.0服务器端、追求精度ViT-B/1686M81.8%6.0数据量大时的备选在垃圾这种类内差异大、纹理重要的任务上卷积结构的归纳偏置局部性、平移等变性其实是优势纯 Transformer 通常需要更多数据才能追平。我的默认选择是边缘端 MobileNetV3-Large 或 EfficientNet-B0服务器端 EfficientNet-B3 或 ResNet50先跑通再看要不要往上加。别一上来就上大模型先确认数据质量配不配得上模型的容量。3.2 冻结与解冻的时机比学习率数值更关键迁移学习里最常见的操作失误是直接全网络解冻、用统一的学习率训练。随机初始化的分类头在最初几个 batch 里梯度非常大这个梯度会顺着反向传播灌进预训练的骨干把原本有用的特征图搅乱等于是把好不容易学来的东西砸了。稳妥的做法是分两段。第一段冻结骨干只训分类头跑 3 到 5 个 epoch让分类头先收敛到一个合理的初始状态。第二段全网络解冻但给骨干一个很小的学习率给分类头一个大一些的学习率用参数组的方式实现差分学习率。import torch import torch.nn as nn from torchvision import models model models.mobilenet_v3_large(weightsIMAGENET1K_V1) in_features model.classifier[3].in_features model.classifier[3] nn.Linear(in_features, num_classes) # 第一阶段冻结骨干只训分类头 for p in model.features.parameters(): p.requires_grad False optimizer torch.optim.AdamW(model.classifier.parameters(), lr1e-3) # 第二阶段解冻差分学习率 for p in model.features.parameters(): p.requires_grad True optimizer torch.optim.AdamW([ {params: model.features.parameters(), lr: 1e-5}, {params: model.classifier.parameters(), lr: 1e-4}, ], weight_decay1e-4)差分学习率这个细节很多教程会一笔带过但在小数据集上它的影响非常明显。骨干的学习率设置成分类头的十分之一是个比较稳的起点数据量越小这个比例越应该拉大。3.3 数据增强不是越多越好垃圾任务尤其要注意边界增强手段的选择要和任务特性绑定不能照抄 ImageNet 的配方。逐条说几个关键点。随机裁剪缩放RandomResizedCrop是标配但裁剪比例要收着点。垃圾里存在电池、药片板、灯管这类小目标裁剪比例下限设到 0.5 以下的时候小目标经常被裁得只剩一个角标签还在但信息没了等于给模型喂噪声。我的经验是 scale 范围设成 (0.6, 1.0) 比较安全。水平翻转安全几乎可以无脑开。垂直翻转要看拍摄角度如果数据全是俯拍垂直翻转后图像在物理上仍然合理可以开如果数据里有大量正视角度的图翻转后会出现“瓶子倒立”这种不自然的形态建议关掉或者用低概率。颜色抖动ColorJitter在垃圾任务里要格外小心。颜色本身携带信息——有害垃圾的容器颜色、水果蔬菜的颜色、塑料瓶的材质色差都可能被过度的色调偏移破坏。亮度、对比度可以适度调整对付现场光照变化饱和度控制在 0.3 以内色相hue尽量不要动或者限制在 0.05 左右。MixUp 和 CutMix 这类混合增强对分类任务有帮助能提升校准度和鲁棒性但用在垃圾数据上要评估一个副作用混合之后生成的图可能呈现出两件垃圾叠在一起的样子这在真实场景里确实存在所以通常是加分的。但如果类别体系里有“污染程度”这类细粒度标签混合会破坏这个语义就要谨慎。3.4 损失函数和类别不平衡的配合方式最基础的交叉熵配合 0.1 的标签平滑是我在绝大多数情况下的起点。标签平滑的作用是防止模型对训练标签过度自信缓解标注噪声带来的过拟合垃圾数据标签本身有系统性噪声这个改动收益很直接。当类别不平衡超过 10:1 时加权交叉熵或者 Focal Loss 值得试。Focal Loss 的 gamma 参数控制对易分样本的抑制程度gamma 越大越关注难样本。但要注意一个反直觉的现象Focal Loss 在训练早期会让损失下降变得很慢因为大量易分样本的贡献被压掉了如果你盯着 loss 曲线看很容易误判成训练出了问题然后去调别的超参。我的做法是先关掉 Focal 用加权 CE 跑通确认基线指标之后再开 Focal 做对比实验。4. 训练曲线不对劲时的排查顺序4.1 训练损失下降、验证损失回升先怀疑数据再怀疑正则化看到这条曲线大部分人的第一反应是加 Dropout、加权重衰减。但在这个任务上更常见的原因是训练集和验证集的分布不一致。比如训练集里白底图占八成验证集是从现场图里抽的那验证损失上升根本不是过拟合是模型没学到现场那部分特征。判断方法很简单从训练集里单独切出一小份“现场风格”的子集看它的损失曲线是跟着训练集走还是跟着验证集走。如果它跟着验证集走问题在分布不在正则化。真正处理分布不一致要靠补数据、靠针对性增强比如模拟俯拍、模拟阴影、模拟逆光而不是靠调 dropout 比率。4.2 验证准确率突然高得离谱八成是泄漏如果验证准确率在训练几个 epoch 后直接冲到 98% 以上先别高兴先查泄漏。最常见的三种泄漏同一张图的不同增强版本分别落在训练集和验证集同一物体的连拍照片被随机划分到了两边用整图统计量比如全局均值方差做归一化时把验证集算进去了。第二种泄漏在自采数据里特别容易出现因为一个人拿着手机对着一个垃圾桶连拍三十张这三十张图高度相似随机按 8:2 划分一定有相似的图跨在两边。正确的划分方式是按“采集批次”或者“物体实例”分组划分也就是 GroupSplit保证同一个物体只在一边出现。这个方法实现起来就是给每张图打一个 group id然后用 sklearn 的 GroupShuffleSplit。4.3 指标卡在某个值不动可能是学习率或增强的锅训练跑了二十个 epoch准确率在 78% 附近来回震荡怎么也不涨。按顺序排查先看学习率是不是太大导致在最优点附近跳用余弦退火或者 ReduceLROnPlateau 试一下再看增强是不是过强尤其是裁剪和颜色抖动临时关掉增强跑一轮对比最后看模型容量是不是真的不够换一个大一档的骨干试试。还有一个隐蔽的原因数据加载的 shuffle 没开或者用了固定的 sampler导致每个 epoch 的训练顺序一样梯度方向的周期性震荡会让指标卡住。这个错误在自定义 Dataset 的时候很容易犯检查一下 DataLoader 的 shuffle 参数。4.4 小 batch 下的 BatchNorm 陷阱边缘部署场景往往要求模型小训练时为了省显存也会把 batch size 压到 8 甚至 4。这时候 BatchNorm 层统计的均值和方差会非常不稳定因为每个 batch 的样本太少统计量噪声大训练和推理时的行为差异明显。处理方式有两种。一是换 GroupNorm 或者 LayerNorm代价是需要重新预训练或者至少微调较长时间因为预训练权重是按 BN 训的。二是保持 BN 但在训练末尾用较大的 batch 跑几百个 iteration 重新校准 BN 的 running statistics具体做法是把模型切到 train 模式但冻结所有参数只让 BN 层更新统计量。第二种方式成本低得多我在小 batch 场景下基本都用这个。5. 评估这件事准确率是最没用的那个数5.1 混淆矩阵要按类别看别只看总体总体准确率在类别均衡时还算合理在长尾分布下会严重失真。占比 38% 的塑料瓶全分对占比 5% 的有害垃圾全分错总体准确率依然能到 90% 以上但这个模型在有害垃圾上是完全不可用的。必须看的是每类召回率和宏平均 F1。有害垃圾这一类召回率低于 0.75 的时候我会直接判定这个模型不能上线因为漏检有害垃圾的后果比把塑料瓶错认成其他垃圾严重得多。混淆矩阵要打印出来逐格看重点看“高危混淆对”玻璃和塑料都是透明容器、纸杯和纸巾、电池和其他。这些格子对应的样本要单独拿出来分析看看是标注问题还是特征确实可分。评估指标适用场景我的常用阈值总体准确率类别均衡时的粗略参考不作为验收依据宏平均 F1类别不均衡的主指标≥ 0.85有害垃圾召回率安全相关的硬指标≥ 0.85混淆矩阵最大误判对定位具体问题单独分析Top-3 准确率人机协同场景≥ 0.955.2 加一个拒识通道比强行分类更实用真实场景里存在大量模型没见过的物体强行让模型在已知类别里选一个结果就是高置信度的错误。实际部署时留一个拒识机制很有价值当最大 softmax 概率低于某个阈值时输出“无法判断请人工确认”。这个阈值怎么定不能拍脑袋。做法是在验证集上画置信度分布看正确分类和错误分类的置信度分布重叠区域把阈值设在能接受误拒率的位置。我一般会控制在拒识率 8% 到 12% 之间这个区间里通常能挡掉三成以上的高置信度错误。5.3 线上验收要靠抽样而不是靠离线指标模型上线之后每周从真实日志里随机抽 200 到 300 张图人工标注一遍算线上指标。这个数字通常比离线低 5 到 15 个百分点差距就是分布漂移的量化结果。抽样的时候要注意分层按时段、按投放点、按摄像头型号各抽一部分不然抽到的全是光照最好的中午时段指标会虚高。6. 从训练脚本到能跑的服务中间还有一段路6.1 导出和加速的实际选择训练完的 PyTorch 模型直接拿去推理速度通常够用但不划算。边缘部署基本都要走 ONNX 导出再配合推理引擎做图优化。导出时要注意两点一是固定输入尺寸动态轴虽然支持但会损失一部分优化空间二是确认算子兼容自定义的激活函数或者特殊的池化方式可能导致导出失败。量化是另一个提速手段。训练后静态量化PTQ用一批校准数据跑一遍通常能把模型体积压到原来的四分之一推理速度提升 2 到 3 倍精度损失在 1 个百分点以内。校准数据的选取很关键必须用真实场景的图不能拿训练集的子集凑数否则量化参数会偏。import torch model.eval() dummy torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, clf.onnx, input_names[input], output_names[logits], opset_version13, dynamic_axesNone, # 固定尺寸换取更好的图优化 )6.2 服务封装的几个细节封装成 HTTP 服务的时候几个细节值得注意。输入图片要统一做尺寸和归一化但归一化的均值方差必须和训练时完全一致这个参数建议写进配置文件跟着模型一起版本管理我曾经因为均值方差写错导致线上指标莫名下降排查了大半天。批量推理能显著提升吞吐但要注意把小请求攒批的超时设短一点投放点场景对延迟敏感。日志要记录原始图片、预测类别、置信度、模型版本这是后面回流训练的数据来源。存储成本允许的话建议按天分目录存原图方便按时间段抽样。6.3 数据回流才是模型持续可用的关键任何一个上线时的模型都会随时间衰减因为垃圾的品类在变——新的包装材料、新的产品形态、季节性的物品。所以上线只是开始真正决定模型长期表现的是回流机制。我的做法是每周从日志里按“低置信度”“拒识”“人工纠正”三个条件各抽一批图标注之后混入训练集做增量训练。增量训练不需要从头训加载上一版权重用较小的学习率训 3 到 5 个 epoch 就够然后在固定的回归测试集上验证没有退化再发布。回归测试集要单独维护不能混进训练数据样本量 500 张左右就够覆盖所有类别和主要场景。最后分享一个小技巧如果投放点有多个不同点位的光照和背景差异很大的话与其训一个通用模型硬扛不如加一个极轻量的场景判别分支先判断图片来自哪类点位再走对应的分类头。这个改动的工程量不大效果往往比单纯加大模型容量明显。