这个项目叫“基于迁移学习 半监督学习的智能垃圾分类系统”翻译成人话就是让一台普通摄像头能认出瓶子、纸箱、果皮、电池分别该扔进哪个桶而且不用把几万张图片全部人工标注一遍。我做这个项目的起因很朴素社区里的智能回收箱经常被吐槽认错可回收的塑料瓶被当成其他垃圾拒收厨余垃圾桶里混个易拉罐也识别不出来。与其等厂商更新设备不如自己折腾一套能持续学习的小系统。如果你也在做图像分类或者想给垃圾分类、安防、工业质检这类场景加一个“会自己长进”的视觉入口这篇实战记录应该能给你一些直接能抄作业的思路。1. 项目背景与方案选型1.1 为什么智能垃圾分类要选“老办法新办法”最早我考虑过两条路子一条是干脆买现成的智能垃圾桶另一条是从零训练一个全新的卷积网络。前者省事但没法按我的需求定制经常出现“只认识固定几种瓶子”的情况后者则过于理想化垃圾分类图片的标注成本非常高团队里只有我和另一位同事轮流标数据标了一周也才两千多张样本分布还极不均匀。转了一圈我决定把组合拳打在“迁移学习 半监督学习”上。迁移学习解决的是“从零训练太难、样本太少”的痛点。ImageNet预训练模型已经在几百万张自然图像上学过纹理、边缘、形状这些通用特征我们不需要从头再学一遍“怎么看一条轮廓线”只需要让它学会“塑料瓶和纸盒的轮廓差异”。而半监督学习解决的是“标注太贵、无标注数据白扔”的痛点。垃圾分类场景里摄像头每天都在拍几万张不带标签的垃圾图片躺在硬盘里人工标注既不现实也没必要半监督方法恰恰能把这批“沉默的数据”用起来。1.2 迁移学习解决“数据少”半监督解决“标注贵”先看数据我手上有约1.2万张有标注图片类别覆盖可回收物、厨余垃圾、有害垃圾、其他垃圾四类。这个规模在深度学习里属于“小样本中的小样本”直接从头训练MobileNet都容易过拟合。倾向选择迁移学习后模型自带的先验知识能让它在几千张图片上就达到不错的表现。但光靠迁移学习还不够。我的问题是有标注数据分布很偏比如“有害垃圾”只有三百多张而“其他垃圾”有六千多张同时社区摄像头积累了六万多张完全没标签的图片。这六万张里包含大量真实光线、遮挡、多物体混杂的样本正是训练集最缺的“硬样本”。半监督学习恰好能处理这种结构少量有标数据提供类别基准大量无标数据提供分布规律。两者结合等于把标注成本省下八成还把泛化能力提上去了。1.3 最终选型四分类 两级推理的折中设计最终系统没有做成“一刀切”的四分类而是采用两级推理第一级判断图片中是否包含“可能属于某类”的垃圾主体第二级输出具体类别和置信度。置信度低于阈值的样本不会硬猜而是进入“待人工复核”队列。这个折中设计是因为实测中很多误判来自用户随手拍塑料袋反光、瓶身带标签、残羹和餐盒混在一起硬分类只会让模型“自信地犯错”。模型方面选了EfficientNet-B0。ResNet50准确率确实稍高一点但参数量大、推理速度慢部署在边缘盒子上的性价比不高。EfficientNet-B0在ImageNet上表现稳定结构上也方便后续换头做半监督训练。整套方案的思路就是用迁移学习降低数据门槛用半监督降低标注门槛用两级推理降低误判风险。2. 系统架构与数据准备2.1 整体链路从拍照到类别输出系统的核心链路是摄像头或手机拍摄图片先做质量筛选过滤掉模糊、过曝和完全空背景的图再送到预处理模块统一尺寸和归一化。预处理后的图片进入模型推理输出四类概率和置信度。如果置信度大于设定阈值则直接显示“可回收物/厨余垃圾/有害垃圾/其他垃圾”并配合语音提示如果小于阈值则提示“请重新拍摄”或进入待复核列表。这里有一个容易忽略的细节模型推理不是终点后面还要接一个反馈回路。每次人工复核的结果和被拒识的图片都会进入增量数据集定期触发一次半监督再训练。这样系统不是“部署完就定型”而是越用越准。从架构上我把推理服务拆成了独立进程用消息队列接收图片结果避免摄像头帧率波动导致推理阻塞。2.2 数据准备公开集打底、自拍补充、无标签池有标签数据我分了三部分公开数据集打底、自己补拍、以及从回收箱摄像头抽取并人工清洗。公开集好处是类别均衡、拍摄环境相对规范坏处是真实场景不够“脏”很多图片是单一物体、背景干净。我在某公开垃圾分类数据集基础上叠加了大约三千张自拍图片包含夜间、室内灯光、阳光直射、遮挡等实际场景类别分布尽可能贴近真实社区垃圾。无标签池构建是半监督的关键环节。我把摄像头连续一周采集的图片全部抽出来按帧间隔去重再用一个初期版本模型做初步筛选丢弃置信度过低和明显空白的图片。这一步不要求无标签数据绝对干净只需要确保整体分布与真实场景一致。太干净反而不好半监督学习需要的是“有信息量的难例”而不是重复的演示图。清洗过程中最耗时的是“标注一致性”。同一种纸箱在不同光线下的颜色差异很大铝罐和铁罐容易混淆我在标注规范里明确写了“只看材质和最终处置方式不看包装品牌”要不然模型很容易被logo带偏。这个规范虽然简单但直接决定了后续伪标签的质量。2.3 预处理与数据增强一个容易被忽略的精度放大器预处理阶段我做了三件事统一缩放、去均值、归一化。所有图片都缩放到224x224用ImageNet的均值和方差做标准化这一步在迁移学习里几乎是必须的因为预训练权重就是在这个统计口径下训练出来的。如果换用自己的均值和方差相当于强制模型重新适应一个它没见过的“色温”精度会掉一到两个点。数据增强则要分“有标数据增强”和“无标数据增强”两套。有标数据我用了常规组合随机水平翻转、随机裁剪、颜色抖动、轻度旋转。无标数据分成了弱增强和强增强两支弱增强只做随机翻转和平移强增强则在弱增强基础上叠加随机亮度对比度变化、随机擦除和模拟污渍。为什么要区分强弱这关系到3.2节要讲的半监督一致性约束。我的经验是强增强不是越猛越好。垃圾图片里很多是细小物体强度过高的RandomAugment会把瓶身上的字全抹掉导致模型学到错误的“无字就是塑料”的捷径。训练前我先画了几组增强效果图人工过目确认强增强后人类仍能辨认类别才最终使用。3. 核心算法实现与训练细节3.1 迁移学习落地加载预训练权重、冻结、换头、微调迁移学习的常规操作是加载预训练权重、替换分类头、冻结前几层、微调后面层。我用timm库加载EfficientNet-B0的预训练权重然后把最后一层全连接换成四分类import timm import torch.nn as nn model timm.create_model(efficientnet_b0, pretrainedTrue) num_features model.classifier.in_features model.classifier nn.Linear(num_features, 4)冻结策略上我没有一刀切冻结所有层而是冻结了前两个stage的骨干参数只训练分类头和后半段特征层。原因是垃圾图片和ImageNet的自然图像在低层特征上差异不大边缘、纹理但高层语义差异很大完全冻结会导致模型学不到垃圾分类特有的“材质感”和“包装形态”。前期只训练后半段可以有效控制参数量、防止过拟合。训练10个epoch后我再把全部层解冻用很低的学习率做全局微调。这一步能带来1%到2%的精度提升但必须开着早停和模型EMA指数滑动平均否则很容易在最后的几个epoch里把验证集准确率打下来。迁移学习不是“加载权重就不管了”后期微调和强正则同样重要。3.2 半监督学习核心伪标签 强弱增强一致性半监督部分我采用的思路是“伪标签 一致性正则”这也是当前半监督视觉任务里最常用、最稳的一种组合。具体做法是每个batch同时包含有标注图片和无标注图片无标图片分别经过弱增强和强增强。弱增强结果送入模型得到预测概率如果最大类概率超过阈值就把这个类别当作伪标签然后要求强增强版本的预测结果尽量和伪标签一致计算无监督损失。伪代码大概是下面这样for x_l, y_l, x_u_w, x_u_s in batch: logits_l model(x_l) loss_l cross_entropy(logits_l, y_l) with torch.no_grad(): logits_u_w model(x_u_w) # 弱增强 prob_u_w softmax(logits_u_w, dim1) mask prob_u_w.max(dim1).values tau # 高置信度才用 pseudo_label prob_u_w.argmax(dim1) # 伪标签 logits_u_s model(x_u_s) # 强增强 loss_u cross_entropy(logits_u_s, pseudo_label) * mask loss loss_l w_u * loss_u.mean()一致性约束的本质是“模型在不同视角下应该给出稳定预测”。弱增强提供可靠的伪标签强增强模拟真实环境里的复杂变化迫使模型学到对光线、遮挡、形变都稳定的特征。我在项目里把阈值tau设为0.95无监督损失权重w_u设为1.0。实际跑下来加入无标数据后验证集准确率从82%提升到90%左右说明这六万张无标图确实贡献了真金白银。3.3 损失函数、动态阈值与类别平衡有标注数据的类别非常不均衡直接使用交叉熵会让模型偏向“其他垃圾”。我在有监督分支上用了带权重的交叉熵每个类别的权重按样本量倒数归一化。后来又试了Focal Loss降难样本权重调起来比较麻烦最终还是加权交叉熵最省心。半监督分支也有自己的“类别失衡”陷阱。伪标签很容易被多数学品类主导导致训练过程进一步强化偏置。我借鉴了动态阈值的思想不是所有类别都用一个固定阈值0.95而是根据每个类的伪标签置信度分布动态调整。对“有害垃圾”这类样本量少的类别阈值适当降低让模型有机会从难样本中学到信息对“可回收物”这类已经学得很好的类别阈值提高减少噪声标签进入梯度。损失组合最终是总损失 有监督加权交叉熵 无监督伪标签交叉熵 最小化类别预测分布的熵正则项熵正则项的目的很简单防止模型对无标签数据输出过于平滑的预测。“过于平滑”意味着模型什么都学不会只知道输出均匀概率。加了熵正则后模型被迫做出更锐利的判断这在半监督训练后期尤其管用。4. 训练过程、评估与问题排查4.1 训练流程与超参数设置整个训练分两个阶段。第一阶段只用有标签数据用迁移学习训练出一个基线模型目标是把测试集准确率稳定到80%以上。第二阶段再加入无标签数据开启半监督联合训练。这个顺序很重要如果一开始就用伪标签模型本身还没学会分类伪标签噪声会非常大把训练直接带偏。超参数方面我的配置如下参数设置说明基础模型EfficientNet-B0推理快适合后期部署输入尺寸224x224与预训练权重一致有标batch size32训练初期较小避免过拟合无标batch size32与有标batch对齐稳定伪标签更新初始学习率3e-4配合AdamW后期衰减学习率策略CosineAnnealing60个epoch内平滑下降EMA衰减率0.999提升模型稳定性混合精度开启训练时间缩短约40%有标数据量不大我刻意调低了学习率防止模型在训练后期反复震荡。EMA的作用也很明显虽然它不直接参与梯度计算但测试时用EMA参数往往比用实时参数高0.5到1个点。4.2 评估不只准确率还要看混淆矩阵和真实场景我留了500张社区实拍图片作为测试集这部分数据没有参与过任何训练或半监督伪标签生成。基线的测试准确率是82.3%加入半监督后提升到90.6%光看数字挺好看但混淆矩阵暴露了真正的问题。预测类别可回收物厨余垃圾有害垃圾其他垃圾实际可回收物92%2%0%6%实际厨余垃圾1%95%0%4%实际有害垃圾12%0%78%10%实际其他垃圾8%4%1%87%“有害垃圾”的召回率只有78%样本量太少模型即使做了加权还是容易把一些纽扣电池误判成其他垃圾。真实场景评估更残酷夜间光线不足时瓶子的纹理特征几乎丢失模型经常把矿泉水瓶识别成“其他垃圾”。于是我又补充了夜间图像的专门增强并在预处理里加入自适应直方图均衡效果才好转。4.3 我踩过的四个坑与排查实录第一个坑是伪标签噪声。半监督训练前期模型把一部分其他垃圾当成了可回收物这些错误伪标签带着高置信度进入无监督损失导致模型在后续几步里错误越来越顽固。排查后我决定引入“伪标签温度缩放”在softmax之前除以温度系数把概率分布拉得更尖只有极有把握的样本才能超过阈值。第二个坑是有害垃圾样本太少。我试过复制粘贴同一批有害垃圾图片到训练集结果模型严重过拟合到特定电池品牌。正确的解法是类别加权损失和对有害垃圾单独做弱增强同时在无标签池里专门检索疑似有害垃圾的图片把它们的小样本问题放到半监督框架里缓解。第三个坑是过拟合到公开数据集风格。纯用公开数据集训练时模型看到真实场景里的塑料瓶会懵因为它只见过“白背景、正中央、无手指”的示范图。解决办法是把自拍图片比例提高到40%并且训练时随机混用公开图和自拍图让模型在批量归一化阶段就适应两种分布。第四个坑是推理速度。初版模型在树莓派上跑一张图要120ms加上画面抓拍和预处理整体时延逼近半秒。后来导出ONNX并用FP16推理把时延压到35ms左右才做到摄像头画面不卡顿。这个坑让我意识到模型精度再高部署后跑不动等于零。5. 部署优化与后续迭代5.1 从PyTorch到ONNX到边缘部署模型最终跑在边缘盒子上没有用云端GPU。导出的核心是转ONNXimport torch dummy_input torch.randn(1, 3, 224, 224, devicecuda) torch.onnx.export( model.eval(), dummy_input, garbage_cls.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version12 )导出后用onnxruntime直接加载再打开CPU或GPU的FP16推理模式。EfficientNet-B0有很多结构化的SE注意力模块ONNX导出时会自动展开不用手动改造算子。如果遇到个别算子不支持可以在onnx-simplifier里跑一遍图优化。真正让我头疼的是边缘端的批量归一化层在动态batch时表现不稳。后来我把导出的onnx固定为batch1并把输入尺寸固定到224x224牺牲了灵活性换来了稳定性。联网升级时直接替换onnx文件即可不需要重新编译程序这个部署方式很适合小团队迭代。5.2 推断速度与精度的取舍实践部署后我做了三轮优化第一轮是FP16把推理时间从120ms降到60ms第二轮是INT8量化降到30ms但准确率掉了一个多点。因为垃圾分类场景对误抛很敏感最终我选择了FP16而不是INT8精度损失接近为零速度也够用。另一项优化是“动态分辨率”。摄像头能拍到画面中央的高分辨率区域但四周往往是背景。我只对检测框内的垃圾主体做224x224推理不整帧做全图识别。这样相当于把算力花在最关键的区域对提升整体准确率比单纯换大模型更有效。测试时也发现对细小垃圾来说裁出主体再做分类比直接压缩全图更靠谱。速度优化和精度优化不是二选一。我用EfficientNet-B0做主体分类额外加了一个很小的MobileNetV3做“空背景检测器”相当于先用轻量模型筛掉大量没垃圾的帧再用大模型精细分类。看起来增加了模型个数但整体系统耗时反而下降因为大部分摄像头画面都是空的垃圾箱。5.3 主动学习闭环让系统越用越准部署之后我发现纯半监督训练只是第一步。再强的无标注数据利用也不如人工介入关键样本来得高效。于是我在系统里加了主动学习回路推理置信度低于阈值的图片进入“待人工复核”队列人工在后台打标后进入增量训练集同时定期从无标签池里抽一批置信度中等偏高的样本让模型“自我评估”是否值得标注。这个闭环让系统跑了一个月后测试集准确率从90.6%又涨到92.3%。尤其是有害垃圾的召回率从78%提升到85%因为它在真实场景里的假阴性样本被主动挑选出来优先复核。半监督学习和主动学习其实是一对很好的搭档前者利用无标签数据的大规模分布后者定向解决“模型最困惑的那一批数据”。实施时要注意节奏不能每天重训。我设定为一个星期左右触发一次再训练训练完成后先跑一遍500张验证集回归确认没有掉点才发版。真实环境里的垃圾种类不是固定的夏天饮料瓶多冬天纸箱多每两周补一批自拍数据比一次堆几千张效果更好。我个人做下来的体会是迁移学习和半监督学习不是玄学而是把“已有知识”和“海量未标注数据”这两块资源真正用起来。如果你也打算做垃圾分类或者类似的图像识别项目先别急着堆模型把数据分层和伪标签阈值调好比换更大的网络更能涨点。还有一个实操小技巧上线初期把置信度阈值调高一点宁可拒识也别乱认用户对“认错”的容忍度比“不认识”低得多。