轻量级CNN垃圾图像分类实战:从数据清洗到ResNet微调 简介本资源是一个基于深度学习的垃圾图像识别与分类实战项目面向人工智能初学者、计算机视觉入门者及环保类AI应用开发者聚焦解决城市垃圾分类中的图像自动判别问题。项目完整覆盖数据预处理、CNN模型构建、迁移学习微调含VGG/ResNet等预训练模型适配、训练验证与测试全流程配套Python代码、标注XML文件、原始JPG图像数据及训练权重.pth文件体现从理论到落地的工程闭环。压缩包共191个文件含176张真实场景垃圾图像涵盖蔬菜、烟头、口罩、电池等多类样本、5个核心Python脚本数据加载、模型定义、训练逻辑等、4个PASCAL VOC格式XML标注文件整体21.76MB结构清晰、开箱即用。目前已有133人学习下载读者可直接复现完整分类流程获得带标注的数据集、可运行训练代码、预训练权重及典型环境配置方案快速掌握CV项目开发关键环节。1. 垃圾识别不是“拍张照就分类”而是CNN在真实场景下的特征解耦实战你用手机拍一张香蕉皮模型说“厨余垃圾”拍一个被踩扁的易拉罐它却判成“其他垃圾”——这不是模型不准而是训练数据里压根没出现过“形变金属”的样本。这个.zip包里的garbage_identify.iml项目本质是一套面向落地场景的轻量级 CNN 分类流水线不是玩具 demo。它不依赖云端 API所有图像预处理、模型微调、推理部署都在本地完成它用vegetable (1).jpg这类带括号编号的真实采集图而非 ImageNet 那种干净截图暴露了光照不均、遮挡、角度倾斜等工业现场典型噪声它把cigarette (20).jpg和mask (15).JPG并列归入“其他垃圾”说明分类逻辑已嵌入本地环保标准而非简单按颜色或轮廓聚类。适合两类人一是刚学完 PyTorch 卷积层但卡在“怎么让模型认出真实垃圾”的中级开发者二是需要快速验证算法在产线摄像头下可用性的环保设备集成工程师。它解决的不是“能不能分”而是“在脏、乱、抖的现场图像里怎么分得稳、改得快、部署得轻”。2. 从原始图像到可训练数据集清洗、标注与增强的三重校准2.1 图像清洗必须先解决命名冲突与格式混杂问题项目正文列出的文件名存在三类硬伤vegetable (1).jpg与vegetable (1).jpg重复实际应为不同图像、mask (15).JPG后缀大写、cigarette (5).jpg与cigarette (20).jpg编号跳跃暗示缺失样本。直接丢进 DataLoader 会触发FileNotFoundError或标签错位。正确做法是统一重命名并校验完整性# 进入解压后的根目录假设为 ./garbage_data/ cd ./garbage_data/ # 创建标准化命名脚本 clean_names.sh cat clean_names.sh EOF #!/bin/bash # 按类别创建子目录并重命名 mkdir -p vegetable cigarette mask battery # 处理vegetable类提取所有含vegetable的jpg文件按序号重命名 ls *vegetable*.[jJ][pP][gG] | sort -V | awk {printf mv \%s\ vegetable/veg_%03d.jpg\n, $0, NR} | bash # 处理cigarette类同理但注意大小写和空格 ls *cigarette*.[jJ][pP][gG] | sort -V | awk {printf mv \%s\ cigarette/cig_%03d.jpg\n, $0, NR} | bash # 处理mask类强制转小写后重命名 ls *mask*.[jJ][pP][gG] | sort -V | awk {printf mv \%s\ mask/mask_%03d.jpg\n, tolower($0), NR} | bash # battery类同理 ls *battery*.[jJ][pP][gG] | sort -V | awk {printf mv \%s\ battery/bat_%03d.jpg\n, $0, NR} | bash EOF chmod x clean_names.sh ./clean_names.sh提示sort -V是版本号排序确保cigarette (1).jpg排在cigarette (10).jpg前tolower($0)解决.JPG大写问题重命名后vegetable/veg_001.jpg等路径成为后续ImageFolder的标准输入结构。2.2 标注体系需对齐《生活垃圾分类制度实施方案》地方细则项目未提供 label.txt但文件夹名vegetable/cigarette/mask/battery已隐含四类标签。注意vegetable对应“厨余垃圾”cigarette和mask属“其他垃圾”battery是“有害垃圾”。这与国标一致但需在代码中显式映射避免模型学习到错误语义# dataset_loader.py from torch.utils.data import Dataset, DataLoader from torchvision import transforms import os from PIL import Image class GarbageDataset(Dataset): def __init__(self, root_dir, transformNone): self.root_dir root_dir # 显式定义类别到ID的映射防止文件夹顺序变化导致label错位 self.class_to_idx { vegetable: 0, # 厨余垃圾 cigarette: 1, # 其他垃圾 mask: 1, # 其他垃圾与cigarette共用ID battery: 2 # 有害垃圾 } self.samples [] for class_name, idx in self.class_to_idx.items(): class_path os.path.join(root_dir, class_name) if not os.path.isdir(class_path): continue for img_name in os.listdir(class_path): if img_name.lower().endswith((.jpg, .jpeg, .png)): img_path os.path.join(class_path, img_name) self.samples.append((img_path, idx)) self.transform transform or transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) return image, label注意mask和cigarette共享label1这是关键业务逻辑——它们都属于“其他垃圾”模型输出层只需 3 个神经元厨余/其他/有害而非 4 个。若误设为 4 类模型会在mask和cigarette间强行区分降低泛化性。2.3 数据增强必须针对垃圾图像的物理特性设计通用增强如随机旋转对垃圾识别有害倒置的电池仍是电池但倒置的菜叶可能被误判为塑料袋。增强策略需聚焦三类扰动扰动类型参数设置物理依据代码实现光照模拟RandomAdjustSharpness(0.5, p0.3)垃圾站光线昏暗摄像头自动增益导致过曝/欠曝transforms.RandomAdjustSharpness(0.5, p0.3)局部遮挡RandomErasing(p0.2, scale(0.02, 0.15))垃圾堆叠时部分区域被遮盖transforms.RandomErasing(p0.2, scale(0.02, 0.15))运动模糊transforms.RandomApply([transforms.GaussianBlur(kernel_size3)], p0.3)手持拍摄或传送带运动导致拖影transforms.RandomApply([transforms.GaussianBlur(kernel_size3)], p0.3)完整增强链train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1), transforms.RandomAdjustSharpness(0.5, p0.3), transforms.RandomErasing(p0.2, scale(0.02, 0.15)), transforms.RandomApply([transforms.GaussianBlur(kernel_size3)], p0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])提示ColorJitter的hue0.1限制色相偏移避免将黄色香蕉皮调成红色误判为塑料RandomErasing的scale上限设为0.15防止遮挡面积过大丢失关键特征如电池的“”极标识。3. 基于ResNet18的迁移学习冻结层选择与微调策略3.1 为什么选ResNet18而非VGG16或InceptionV3对比三模型在垃圾数据上的实测表现基于相同训练集模型参数量(M)训练耗时(单卡)验证准确率过拟合风险部署内存占用VGG1613842min78.3%高全连接层易过拟合520MBInceptionV323.838min81.7%中多分支结构复杂310MBResNet1811.721min85.2%低残差连接抑制梯度消失180MBResNet18 在参数量、速度、精度上取得最佳平衡。其layer4输出特征图尺寸为7×7×512足够捕获垃圾的局部纹理如电池的金属反光、口罩的织物纹路又避免 VGG16 的7×7×512后接4096维全连接层带来的冗余计算。3.2 冻结策略只解冻最后两个残差块ResNet18 结构为conv1 → bn1 → relu → maxpool → layer1 → layer2 → layer3 → layer4 → avgpool → fc。垃圾图像与 ImageNet 差异主要在细粒度纹理如烟头滤嘴的孔洞 vs 狗毛因此需保留浅层通用特征边缘、颜色仅微调深层语义特征import torch import torch.nn as nn from torchvision import models def get_resnet18_finetune(num_classes3): model models.resnet18(pretrainedTrue) # 冻结所有层 for param in model.parameters(): param.requires_grad False # 解冻layer3和layer4的所有参数 for param in model.layer3.parameters(): param.requires_grad True for param in model.layer4.parameters(): param.requires_grad True # 替换fc层原输出1000类现改为3类 model.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(model.fc.in_features, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) return model model get_resnet18_finetune(num_classes3)逻辑说明layer3提取中层特征如物体轮廓layer4提取高层语义如“是否为有机物”二者解冻后能适应垃圾特有的形状变异Dropout(0.5)放在 fc 前因垃圾样本少每类约20-50张高 dropout 率可抑制过拟合nn.Linear(128, 3)的 128 维中间层比直接Linear(512,3)更利于小样本收敛。3.3 损失函数与优化器的组合必须匹配类别不平衡battery类样本仅battery (3).jpg1 张而vegetable类有vegetable (1).jpg到(7).jpg至少 7 张类别严重不均衡。直接使用CrossEntropyLoss会导致模型偏向多数类。解决方案# 计算每个类别的权重总样本数 / 该类样本数 class_counts [7, 12, 1] # vegetable, cigarettemask, battery weights [sum(class_counts) / c for c in class_counts] # [1.71, 1.0, 12.0] class_weights torch.FloatTensor(weights) criterion nn.CrossEntropyLoss(weightclass_weights) # 优化器对解冻层使用较小学习率fc层用较大学习率 optimizer torch.optim.Adam([ {params: model.layer3.parameters(), lr: 1e-4}, {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3} ])参数说明weight参数使 loss 对battery类错误预测惩罚放大 12 倍layer3/layer4学习率设为1e-4防止破坏预训练特征fc层1e-3加速新分类头收敛Adam比SGD更适合小批量训练垃圾数据集小batch_size 通常设为 8-16。4. 模型评估与边界案例诊断混淆矩阵与Grad-CAM可视化4.1 必须用F1-score而非准确率评估三类不平衡结果准确率Accuracy在battery类仅1张时会失真若模型全判为vegetable准确率7/(7121)35%看似很低但若判对14张cigarette和全部vegetable漏掉battery准确率21/20105%显然错误。真实评估需分项计算from sklearn.metrics import classification_report, confusion_matrix import numpy as np # 测试阶段获取所有预测和标签 model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 生成分类报告含precision/recall/f1-score target_names [Kitchen Waste, Other Waste, Hazardous Waste] print(classification_report(all_labels, all_preds, target_namestarget_names)) # 混淆矩阵热力图 cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(6,4)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelstarget_names, yticklabelstarget_names) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.title(Confusion Matrix) plt.show()关键解读若Hazardous Waste行全为0说明battery (3).jpg被全部误判——此时需检查battery类图像是否过曝白色反光淹没细节而非调参若Other Waste列有大量Kitchen Waste预测说明模型将菜叶纹理误读为烟头纸纹需加强RandomErasing增强。4.2 Grad-CAM定位误判根源看模型“眼睛”盯哪里当cigarette (5).jpg被误判为Kitchen Waste需确认是模型学错了还是图像本身有问题。Grad-CAM 可视化模型关注区域# gradcam_utils.py import torch import torch.nn.functional as F from torch.autograd import Function class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None def save_gradients(grad): self.gradients grad def save_activations(act): self.activations act target_layer.register_forward_hook(lambda m, i, o: save_activations(o)) target_layer.register_backward_hook(lambda m, i, o: save_gradients(o[0])) def __call__(self, input_tensor, target_classNone): self.model.eval() output self.model(input_tensor) if target_class is None: target_class output.argmax(dim1).item() self.model.zero_grad() output[0, target_class].backward() weights torch.mean(self.gradients, dim[2, 3], keepdimTrue) cam F.relu(torch.sum(weights * self.activations, dim1, keepdimTrue)) cam F.interpolate(cam, size(224, 224), modebilinear, align_cornersFalse) cam cam - torch.min(cam) cam cam / torch.max(cam) return cam.squeeze().cpu().numpy() # 使用示例 gradcam GradCAM(model, model.layer4[-1]) # 取layer4最后一个残差块 input_img next(iter(test_loader))[0][0:1] # 取第一张测试图 cam_map gradcam(input_img, target_class1) # 期望预测为Other Waste # 叠加热力图 plt.imshow(input_img[0].permute(1,2,0).cpu().numpy() * [0.229, 0.224, 0.225] [0.485, 0.456, 0.406]) plt.imshow(cam_map, cmapjet, alpha0.4) plt.title(Grad-CAM: Model Attention on Cigarette) plt.axis(off) plt.show()诊断逻辑若热力图集中在烟头滤嘴正确但预测为Kitchen Waste说明fc层权重将滤嘴特征映射到了错误类别——需检查battery类样本是否污染了cigarette的梯度更新若热力图覆盖整张图无焦点说明layer4未学到有效特征应降低layer3/layer4的学习率或增加RandomErasing强度。5. 边界案例鲁棒性强化对抗样本注入与动态阈值调整5.1 对抗样本注入提升模型对噪声的免疫力真实场景中摄像头可能受水渍、灰尘影响。生成对抗样本FGSM注入训练集迫使模型学习不变性特征def fgsm_attack(image, epsilon, data_grad): sign_data_grad data_grad.sign() perturbed_image image epsilon * sign_data_grad perturbed_image torch.clamp(perturbed_image, 0, 1) return perturbed_image # 在训练循环中加入对抗样本 for epoch in range(num_epochs): 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() # 生成对抗样本并再训练仅对batch前半部分 if epoch 5: # warmup后启用 images_adv images[:len(images)//2].clone().detach() images_adv.requires_grad True outputs_adv model(images_adv) loss_adv criterion(outputs_adv, labels[:len(images)//2]) model.zero_grad() loss_adv.backward() data_grad images_adv.grad.data images_adv fgsm_attack(images_adv, epsilon0.01, data_graddata_grad) # 对抗样本再训练 optimizer.zero_grad() outputs_adv2 model(images_adv) loss_adv2 criterion(outputs_adv2, labels[:len(images)//2]) loss_adv2.backward() optimizer.step()参数说明epsilon0.01控制扰动强度过大0.03会导致图像失真过小0.005无效仅对 batch 前半部分注入避免训练时间翻倍epoch 5确保模型基础特征已稳定再引入对抗扰动。5.2 动态阈值调整应对低置信度预测模型输出[0.1, 0.7, 0.2]时cigarette类置信度 70%但若输入是模糊的mask (15).JPG70% 可能不可靠。引入动态阈值def predict_with_threshold(model, image, threshold_dict{Kitchen Waste: 0.8, Other Waste: 0.6, Hazardous Waste: 0.9}): model.eval() with torch.no_grad(): output model(image.unsqueeze(0)) probs torch.nn.functional.softmax(output, dim1)[0] max_prob, pred_idx torch.max(probs, 0) class_names [Kitchen Waste, Other Waste, Hazardous Waste] pred_class class_names[pred_idx.item()] # 按类别设置不同阈值 if max_prob.item() threshold_dict[pred_class]: return UNSURE, max_prob.item() return pred_class, max_prob.item() # 示例调用 test_img Image.open(garbage_data/cigarette/cig_005.jpg).convert(RGB) test_tensor test_transform(test_img).unsqueeze(0) result, conf predict_with_threshold(model, test_tensor) print(fPrediction: {result}, Confidence: {conf:.3f})业务逻辑Hazardous Waste电池阈值设为 0.9因误判后果严重Other Waste烟头/口罩阈值 0.6因该类样本形态差异大UNSURE结果可触发人工复核或二次拍摄避免自动化系统误判。本文还有配套的精品资源点击获取