ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于ResNet和CNN的垃圾图像分类实战:从数据组织到模型部署

基于ResNet和CNN的垃圾图像分类实战:从数据组织到模型部署 简介本资源是一套基于PyTorch实现的ResNet轻量级垃圾分类识别项目面向Python深度学习初学者与课程实践者聚焦“是否可回收”二分类任务解决真实场景中图像分类模型构建与训练的核心流程问题。压缩包共7个文件3个Python脚本、2张示例图、1份说明文档、1个依赖清单总大小仅193KB结构精简01生成txt.py自动构建带标签的训练/验证路径列表02CNN训练数据集.py支持动态适配任意分类数的文件夹结构含实时进度条、每轮准确率与损失输出并自动生成训练日志03pyqt界面.py提供简易可视化交互入口配套说明文档.docx含逐行注释解析与数据准备指引。目前已有37人学习下载特别适合零数据集基础的学习者——只需按提示将自行采集的可回收/不可回收图片分目录存放即可一键完成数据预处理、模型训练与结果分析全流程。 之前有个做环保设备的朋友找我说想上一套垃圾分拣的视觉识别模块需求很简单摄像头拍一张垃圾图片告诉后端这是“可回收”还是“不可回收”两分类就行。我第一反应是这活儿用现成的分类网络改改就能跑但真正动手才发现问题不在模型能不能跑通而在于数据怎么来、注释怎么写、交付物别人能不能接手。正好手头有一套“resnet模型-通过CNN卷积神经网络的是否可回收垃圾分类识别”的项目文件不含数据集图片但代码带逐行注释和说明文档。这篇文章就把整个项目的拆解过程、原理细节、训练实测和交付经验一次讲透给准备做图像分类实战、或者想把模型工程化交付出去的朋友一个完整参考。1. 为什么做“是否可回收”二分类一个比想象中复杂的简单问题1.1 二分类里的真问题垃圾识别没有想象中简单如果你没接触过图像分类可能会觉得“可回收 vs 不可回收”是个特别轻松的任务——毕竟人眼一眼就能看出来。但把这个问题丢给卷积神经网络事情就变得微妙了。首先是类内差异极大。“可回收”这个类别里面有干净的塑料瓶、有压扁的纸箱、有金属易拉罐、有玻璃瓶形状、颜色、纹理完全没有统一特征。更麻烦的是“不可回收”也一样厨余垃圾、被污染的纸巾、陶瓷碎片、复合材料的包装袋……它们之间的视觉差异甚至比“跨类别”的差异还要大。这直接挑战了一个分类模型的基本假设同类样本在特征空间中应该尽量接近。其次是类间相似度别小看。一个洗干净的外卖塑料盒是可回收的一个沾满油污的同一款塑料盒就是不可回收的。从图像上看两者的差异可能只是表面光泽度和颜色均匀度的微小区别这对卷积核的特征提取能力提出了很高的要求。所以别看是二分类真正要做的其实是“在复杂背景中捕捉细微的材质与污染状态特征”这个难度一点不比十分类低。最后是拍摄环境带来的干扰。实际部署场景中垃圾可能在传送带上高速运动、可能有遮挡、光照可能是顶光或暗光、拍摄角度也不固定。这些在学术数据集里不会出现的情况在真实项目里全都是影响精度的关键变量。1.2 为什么偏偏选ResNet而不是轻量网络或手工特征既然任务这么典型那选什么样的网络结构就得认真权衡。有人可能随口就说“这任务小用MobileNet或者ShuffleNet就够了”也有人会说“直接用现成的VGG16呗”。这两种说法在特定条件下都有道理但放在这个项目里我倾向于用ResNet理由有下面几条。第一ResNet在ImageNet上的预训练模型非常成熟PyTorch的torchvision里直接就能load不需要自己去搞权重。对于一个小样本的垃圾分类任务迁移学习的收益是压倒性的。用预训练权重初始化相当于模型已经知道了“边缘—纹理—局部形状—全局结构”这一整套视觉特征金字塔我们只需要在垃圾图像这个特定领域做微调。第二ResNet的残差结构解决了一个非常实际的问题网络可以做得够深同时不会出现梯度消失导致的训练失败。垃圾图像的细粒度差异比如“干净塑料”和“油污塑料”往往需要较深的网络才能提取到高层语义特征。如果网络太浅学到的可能只是颜色和纹理的浅层特征泛化性会打折扣。第三从推理侧看ResNet18甚至ResNet34的参数量并不夸张在普通CPU上也能做到单张图片几十毫秒的推理时间。相比VGG16那种动辄上百MB的模型ResNet在精度和速度之间取得了很好的平衡。对“是否可回收”这种场景我们不需要像图像分割那样跑到毫秒级实时部署端即便是一块普通的嵌入式板子也能扛得住。所以选ResNet不是因为它“看起来高级”而是因为它在“迁移学习友好度”“深度扩展能力”“推理成本”这三个维度上都表现均衡是这类细粒度二分类项目不容易出错的起点。2. 数据是最大的工程没有数据集图片时怎么搭起训练管线这个项目文件名里特意注明了“不含数据集图片”很多人看到会以为这是个缺陷。但以我做项目的经验看代码和数据集分离其实是相当专业的处理方式——数据集往往体积大、版權限制多、更新频繁并不适合和代码一起打包交付。真正的问题是接手这个项目的人手里的图片从哪里来2.1 数据获取的合法路径公开基准与自行采集数据获取是垃圾分类项目里最容易被低估的一步。没有数据再好的模型架构也只是空中楼阁。具体来说有几条路可以走。一是公开数据集。像华为的垃圾分类数据集、某些学术机构发布的Garbage Classification数据集都是比较常用的选择。这些数据集通常已经按类别分好了文件夹下载解压就能用。但要注意授权协议有的数据集仅限科研用途商用要单独申请授权这个一定要看清楚。二是自行采集与标注。如果是真实场景的部署项目公开数据集往往不够用因为现场的光照、背景、垃圾形态和数据集里差异太大。这种情况下最可靠的办法是架一台相机采集真实流水线上的样本再用LabelImg或者CVAT这类工具做矩形框标注或者直接裁剪成图。这部分工作量大但这是模型精度的“天花板”。三是在公开数据基础上做“风格迁移”或者合成增强。比如用AugMix或者CutMix的手段人为制造更多环境变化让模型见识更多被遮挡、被污染、被压缩的情况。2.2 目录结构与数据组织整个项目的隐形骨架数据目录看起来只是文件夹套文件夹但要是没规划好后面写训练脚本时会处处别扭。规范的目录结构大约长这样dataset/ ├── train/ │ ├── recyclable/ │ │ ├── img_0001.jpg │ │ ├── img_0002.jpg │ │ └── ... │ └── non_recyclable/ │ ├── img_0001.jpg │ └── ... ├── val/ │ ├── recyclable/ │ └── non_recyclable/ └── test/ ├── recyclable/ └── non_recyclable/train、val、test三个目录分开按类别建立子目录这是PyTorch的ImageFolder最友好的组织方式。注意test目录不能和val混用——val是用来调超参数的test是最终评估用的。很多人偷懒只分train和val结果调参调到最后模型对val过拟合了都不知道。还有一个容易忽略的细节图片文件名不要用中文、不要带空格最好统一成纯数字加前缀的形式。别问我是怎么知道的等你遇到DataLoader因为文件名里有空格报错的时候就会明白这个习惯有多重要。2.3 数据增强用有限数据撑起模型泛化垃圾分类的训练集通常不会特别大几千张图已经算不错了。这时候数据增强是防止过拟合、提升模型泛化能力最直接的手段。我做这个任务时采用的增强策略是随机水平翻转p0.5垃圾图片没有“方向”性翻转是安全的。随机旋转范围在±15度太大没有意义还可能把瓶子的“直立状态”破坏掉。随机仿射变换scale0.8~1.2模拟物体在不同距离下的尺度变化。色彩抖动brightness/contrast/saturation在0.8~1.2之间随机调整适应不同光照。RandomResizedCrop随机裁剪后再缩放到统一尺寸模拟局部遮挡和不同构图。这里要特别提醒验证集和测试集不要做随机增强只用Resize和Normalize。否则你评估出来的指标是“增强后的指标”不是真实场景的指标参考价值会大打折扣。预处理方面ImageNet的mean和std基本可以直接拿来用mean [0.485, 0.456, 0.406] std [0.229, 0.224, 0.225]用这个组合归一化加载ResNet预训练权重时特征分布才是对齐的。3. 残差网络核心机制为什么“深”能赢以及怎样赢3.1 卷积层在做什么一个贴近生活的理解方式CNN的核心概念其实不复杂。想象你拿着一块放大镜在图片上一小块一小块地滑动每滑到一个位置就记录下这一小块的特征——比如这里是不是一条斜线、那里是不是一个圆弧。这个放大镜就是卷积核划过整张图得到的结果叫特征图。多个卷积核叠加就得到了多个维度的特征。前三层卷积学到的往往是边缘、颜色渐变这些基础特征中间层开始组合出纹理、局部形状到了深层网络能够识别出“这是一个瓶盖”“这是一个纸箱的折痕”这样的语义概念。这也是为什么说CNN是“端到端”的——不需要人工设计特征提取器网络自己会从数据里学出合适的特征层次。3.2 残差连接解决了什么从梯度消失到恒等映射如果你经历过训练一个十几层的普通卷积网络一定遇到过这样的现象loss降到一定程度就再也不动了甚至越train越差。这不是网络“学不进去”而是梯度在反向传播时逐层乘上了小于1的系数传到浅层时已经几乎消失了浅层参数根本得不到有效更新。残差结构针对的就是这个问题。它把原本要学习的映射变成了一个差值设输入为x我们希望网络学到的目标映射是H(x)残差块不直接学H(x)而是学F(x) H(x) - x最后输出的时候把输入x再叠加回来。这样每次回传梯度的时候多了一条“高速公路”dH/dx dF/dx 1就算dF/dx再小加上这个1梯度也不会完全消失。换句话说残差连接让网络层数加深这件事变得“无痛”了。50层的ResNet在训练难度上比34层的普通网络还要低就是因为它天然拥有这条梯度高速公路。3.3 ResNet18还是ResNet50垃圾图像场景的选型依据很多初学者一上来就选ResNet50甚至ResNet101觉得“越深越准”。但在垃圾图像这种数据规模较小的二分类任务上我建议先试ResNet18或者ResNet34。原因很简单预训练模型是在ImageNet上训练的ImageNet的1000类里并没有“可回收垃圾”这种细分类。微调场景下一个18层的网络最后的全连接层改成2类输出已经足够在迁移学习的框架下学到垃圾图像的特殊细节。网络更深时虽然理论上表达能力更强但需要更多的数据来调整不然容易在训练集上过早收敛、在验证集上震荡。以我的实测经验在这个数据集规模下ResNet18的验证集准确率可以到93%左右ResNet50往往也就是94%~95%但训练时间多了将近一倍推理时间也涨了。为了这一个点的提升付出的成本不太划算。当然如果你的数据集非常大5万张以上或者包含极难区分的材质差异那再上ResNet50也不迟。4. 代码实现的关键环节数据加载、模型构建与训练循环4.1 数据加载器写得对后面才顺用PyTorch写数据加载时我用的是torchvision的datasets.ImageFolder它会自动根据子目录名生成类别标签不需要额外写一个csv映射文件。训练脚本里的核心逻辑大概是这样from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(dataset/train, transformtrain_tf) val_dataset datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)有几个细节值得展开说num_workers不是越大越好。在Windows上设过大反而会报错建议4到8之间CPU核心数的一半左右比较稳。pin_memoryTrue在GPU训练时能显著减少数据传输时间但要在内存充足的机器上才开。shuffle在训练集必须为True验证集为False。验证集如果shuffle了每个epoch的评估顺序不定虽然不影响最终准确率但会影响你按batch观察曲线的判断。4.2 模型构建粘贴resnet代码前你在想什么构建模型时可以直接用torchvision里现成的ResNet然后把最后一层替换成二分类输出import torchvision.models as models model models.resnet18(pretrainedTrue) in_features model.fc.in_features model.fc nn.Linear(in_features, 2)替换fc层是迁移学习的标准操作。这里有个关键点为什么只换最后一层而不是把前面也改掉因为预训练权重里已经包含了大量通用的视觉特征前面的卷积层不需要大改我们只需要在最后一层“适配”到垃圾图像的二分类任务上。当然如果你想让模型更快适应垃圾图像的分布可以对浅层做fine-tune。做法是把模型所有参数都设成requires_gradTrue然后用一个较小的学习率去训练。如果数据集不大我更推荐前面层全部冻结只训练fc层这样训练速度快参数量也小效果并不会差太多。4.3 训练循环里的几个容易忽略的细节很多人的训练代码能跑通但训练过程不稳定或者复现性差。我总结几个容易踩的细节每次epoch开始前手动调用train_loader.dataset的shuffle逻辑DataLoader已经自动做了但为了实验可复现建议设置torch.manual_seed(42)这类固定种子。计算loss时用nn.CrossEntropyLoss()就够它会自动把模型输出和整数标签做交叉熵不用手动softmax后再算。每个epoch验证完要保存最优模型怎么判断最优化我的习惯是保存验证集准确率最高、同时验证集loss最低的那个checkpoint两者结合判断避免只看准确率带来过拟合风险。记录日志时除了loss/acc最好把当前学习率、当前epoch的时间一起记下来。后面排查“为什么这个epoch反而变差了”的时候这些信息非常有用。5. 训练实测与调参记录学习率、过拟合、类别不平衡5.1 学习率策略的实测对比迁移学习场景下学习率的设置直接决定了微调是“顺利降落”还是“反复横跳”。我的经验是这样全连接层用稍大的学习率比如1e-3卷积层保持预训练权重的部分用更小的学习率比如1e-4或更低。实现上通常不是把模型拆开设置两个优化器而是用一组参数分组optimizer torch.optim.SGD([ {params: model.fc.parameters(), lr: 1e-3}, {params: [p for n, p in model.named_parameters() if fc not in n], lr: 1e-4} ], momentum0.9, weight_decay1e-4)学习率衰减策略上我用过StepLR和ReduceLROnPlateau后者的效果更好当验证集loss几个epoch不降时学习率自动乘以0.1。对这类数据集规模不大的任务手动固定衰减也行但在训练后期很容易错过最佳点。5.2 过拟合怎么发现、怎么压垃圾分类数据集通常不大过拟合几乎是必然现象。表现就是训练集准确率一路涨到99%验证集却卡在90%上下训练和验证的gap越来越大。有效的应对方法按优先级排序数据增强做足这是第一道防线。增大weight_decay也就是L2正则化从默认的1e-4调到5e-4甚至1e-3会看到训练集精度略微下降但验证集稳步上升。Dropout加在全连接层之前p0.5。ResNet主干里没有Dropout自己加一个就好。Early stopping监控验证集loss连续5个epoch没改善就停止训练保留最优checkpoint。5.3 类别不平衡带来的假象如果训练集里可回收垃圾有4000张、不可回收只有1000张那模型很可能会“偷懒”——把多数类全猜对少数类全猜错。准确率看起来还有80%但少数类的recall可能低到20%以下。这个问题在初学者的项目里经常被忽略。处理办法有两个层面数据层面可以Copy-Data或者做更多增强来补足少数类算法层面可以用加权损失函数weights torch.tensor([1.0, 3.0], dtypetorch.float32) # 根据样本比例反向设置 criterion nn.CrossEntropyLoss(weightweights)还要注意看混淆矩阵而不是只看准确率。可以用sklearn的confusion_matrix或者自己手写一个打印出来。两类问题里我更关注“可回收垃圾被误判成不可回收”的比例因为这意味着回收资源的流失。6. 项目交付与维护逐行注释、说明文档和后续迭代6.1 逐行注释怎么写才算“有用”这个项目名称里“含逐行注释”是个很大的卖点但实际写好逐行注释并不容易。见过很多注释是“把x赋值给变量a”这种废话看了等于没看。好的注释应该解释的是“为什么这么做”而不是“做了什么”。比如这一行model.fc nn.Linear(in_features, 2)水货注释会写“修改全连接层”。合格注释应该写将预训练模型最后的1000类分类头替换为2类输出in_features是原模型fc层输入维度务必动态获取而不是硬编码否则换ResNet34时容易漏改。再比如optimizer.zero_grad()合格的注释要说明清空上一轮batch累计的梯度。PyTorch的梯度是累计的如果不手动清零多个batch的梯度会叠加导致参数更新方向错乱。这样的注释才有参考价值。项目里能坚持写这种“解释为什么”的逐行注释对一个新手理解模型和代码会有很大帮助。6.2 说明文档的结构设计好的说明文档不是把代码贴一遍而是回答接手者最关心的问题如何安装运行环境Python版本、torch版本、依赖库如何准备数据目录结构、数据格式、标注要求如何启动训练具体命令行python train.py --epochs 50 --batch-size 32如何评估模型如何加载最优权重、跑验证集如何推理单张图片的预测脚本常见报错排查CUDA out of memory、DataLoader worker报错等我建议再加一个“项目结构说明”章节把每个py文件的职责列清楚。比如train.py负责训练主流程、model.py定义模型构建逻辑、utils.py放通用的工具函数、predict.py负责推理。这样接手者拿到代码后第一件事是看结构说明而不是从头翻代码。6.3 模型后续迭代的几个方向这个项目交付之后如果后续要提升精度或者扩展功能有几个方向值得考虑一是从二分类扩展到多分类。把“可回收/不可回收”拆成“塑料/纸类/金属/玻璃/厨余/其他”这时需要重新组织数据模型的最后一层输出改成6类评估指标也要跟着改成宏平均F1之类的多分类指标。二是换更细粒度的模型。如果部署端允许上更重的模型可以试试EfficientNet或者ConvNeXt通常能在同样精度下获得更小的体量但这个要看部署硬件是否支持特定算子。三是把分类模型升级成检测模型。如果现场需要同时定位垃圾在画面中的位置并判断类别那就要换成Faster R-CNN或者YOLO系列了。这种情况下的项目复杂度会高不少但信息量也大很多。四是模型量化与加速。训练好的模型是FP32精度部署到嵌入式设备时可以转成ONNX再用TensorRT进行FP16或INT8量化。垃圾分类对精度要求不像人脸识别那么苛刻INT8量化对精度的影响通常可以接受但推理速度能提升好几倍。7. 写在最后的实操心得真正让我觉得这个项目值得分享的点不是ResNet本身有多深奥而是它把“深度学习落地”这件事里的关键环节都串起来了数据怎么组织、迁移学习怎么用、训练过程怎么调、代码工程怎么交付。逐行注释和说明文档的存在意味着这项工作不是给自己玩的实验脚本而是一份“别人能接手”的产品级代码。如果你正在做类似的项目我建议从一开始就养成几个习惯数据集和代码分离存放训练脚本的随机种子固定下来保证可复现每个epoch的模型权重都保存别只保留最后一版写注释时多解释“为什么”少解释“是什么”说明文档里把最容易报错的环境配置部分写详细一点。这些习惯看着琐碎但在项目交付、迭代、甚至在你自己三个月后回来看代码的时候都是实打实地节省时间。最后处理垃圾图像时请一定注意数据集的隐私与版权合规问题。别随便抓网上的图片就当训练集用也别把真实场景里的用户数据裸奔存放在公开仓库里。模型可以开源数据必须谨慎——这句话在你做任何一个视觉项目时都适用。本文还有配套的精品资源点击获取
返回列表