ARTICLE DETAIL

资讯详情

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

垃圾分类毕设实战:CNN与ResNet模型选择、训练调参与避坑指南

垃圾分类毕设实战:CNN与ResNet模型选择、训练调参与避坑指南 简介这份资源面向计算机类毕业设计与课程作业场景聚焦深度学习中的垃圾分类实战任务适合具备一定Python基础、正在寻找完整可运行项目参考的本科生或自学者。资源以CNN与ResNet两种经典网络结构为主线帮助读者理解图像分类从数据到模型落地的完整链路。压缩包共6个文件约4MB包含2个Python脚本、2个PyTorch模型权重文件、1个配套压缩包及1份Markdown说明文档脚本负责训练与预测流程权重文件可直接加载推理说明文档则梳理项目结构与使用方式。目前已有77人学习下载。通过这份资源读者可获得一套结构清晰的垃圾分类实战方案既能对照代码理解卷积网络与残差网络的实现差异也能借助预训练权重快速验证预测效果适合作为毕设选题参考或课程作业的实践起点。1. 从一份垃圾分类毕设说起CNN 和 ResNet 到底怎么选每年到了毕设季和课程作业周后台被问得最多的一类问题就是手头有一个垃圾分类图像数据集想用深度学习做识别标题里写着 CNN 和 ResNet但打开代码一看卷积层堆了一堆残差块又套了一层完全不知道从哪下手改。这个场景太典型了——垃圾分类本身是一个标准的图像分类任务输入是塑料瓶、纸箱、果皮、电池这些日常垃圾的照片输出是类别标签而 CNN 和 ResNet 恰好是这条技术路线上最常被拿来用的两个模型。问题在于很多人把这两个词当成并列的两个选项实际上 ResNet 本身就是 CNN 的一种改进结构它解决的是深层网络训练时梯度消失和退化的问题。换句话说你选 CNN 还是 ResNet本质上是在选网络深度和训练策略而不是在选两个完全不同的东西。这篇文章就围绕垃圾分类这个具体任务把数据准备、模型搭建、训练调参、踩坑排查整条链路讲清楚适合正在做毕设或课程作业、需要一份能跑通的代码和一套能解释清楚的方案的人。2. 垃圾分类数据集怎么处理从原始图片到可训练张量2.1 先搞清楚你的数据长什么样垃圾分类数据集通常有两种来源一种是学校或竞赛提供的标注好的图片文件夹按类别分目录存放另一种是自己从网上爬的需要手动清洗。不管哪种第一步都是统计每个类别的样本数量。常见做法是用 Python 的os和PIL快速扫一遍看看有没有损坏图片、有没有类别严重不均衡。我一般会先跑一个统计脚本把每个类别的文件数和图片尺寸分布打印出来心里有数再动手。import os from PIL import Image from collections import defaultdict data_dir garbage_dataset # 根目录下面按类别分子文件夹 stats defaultdict(int) size_stats defaultdict(list) for cls in os.listdir(data_dir): cls_path os.path.join(data_dir, cls) if not os.path.isdir(cls_path): continue for fname in os.listdir(cls_path): fpath os.path.join(cls_path, fname) try: img Image.open(fpath) img.verify() # 验证文件完整性 stats[cls] 1 size_stats[cls].append(img.size) except Exception as e: print(f损坏文件: {fpath}, 错误: {e}) for cls, count in stats.items(): sizes size_stats[cls] avg_w sum(s[0] for s in sizes) / len(sizes) avg_h sum(s[1] for s in sizes) / len(sizes) print(f{cls}: {count} 张, 平均尺寸 {avg_w:.0f}x{avg_h:.0f})这段代码的逻辑很直接遍历每个类别文件夹用Image.verify()过滤掉打不开的图片同时记录数量和尺寸。参数上唯一需要注意的是data_dir要指向你的数据集根目录类别文件夹名就是标签名。跑完之后你会得到一张表如果某个类别只有几十张而其他类别有上千张那后面训练时就要考虑重采样或者加类别权重否则模型会偏向多数类。2.2 数据增强和归一化别让模型记住背景垃圾分类图片有一个很隐蔽的坑很多照片的背景是桌面、地板、垃圾桶内壁模型很容易学会“看到木纹桌面就预测可回收物”这种错误关联。解决办法就是数据增强让模型关注物体本身而不是背景。常见做法是随机裁剪、水平翻转、颜色抖动再加一点随机旋转。归一化参数用 ImageNet 的均值和标准差就行因为后面大概率要用预训练模型。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), # 随机裁剪并缩放到224 transforms.RandomHorizontalFlip(p0.5), # 水平翻转 transforms.RandomRotation(15), # 随机旋转±15度 transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), # 颜色抖动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], # ImageNet均值 std[0.229, 0.224, 0.225]) # ImageNet标准差 ]) val_transform 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]) ])这里RandomResizedCrop的scale参数控制裁剪区域占原图的比例设成 0.6 到 1.0 意味着至少保留 60% 的画面避免裁得太狠把物体裁没了。ColorJitter的三个参数分别控制亮度、对比度、饱和度的扰动幅度0.3 是一个比较温和的值再大可能让颜色失真影响分类。验证集只用 Resize 和 CenterCrop不做随机增强保证评估结果稳定。2.3 用 DataLoader 把数据喂给模型数据增强定义好之后用ImageFolder和DataLoader组装成批次。这里有几个参数直接影响训练效率和显存占用batch_size、num_workers、pin_memory。我一般先在本地用batch_size32试跑如果显存够就往上加不够就降到 16 或 8。num_workers设成 CPU 核心数的一半左右太多反而会因为进程切换拖慢速度。from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader, random_split full_dataset ImageFolder(rootdata_dir, transformtrain_transform) val_size int(0.2 * len(full_dataset)) train_size len(full_dataset) - val_size train_set, val_set random_split(full_dataset, [train_size, val_size]) val_set.dataset.transform val_transform # 验证集换增强策略 train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(f训练集: {train_size} 张, 验证集: {val_size} 张) print(f类别: {full_dataset.classes})注意random_split之后验证集的 transform 需要单独替换否则验证集也会用训练增强导致评估指标波动。pin_memoryTrue在 GPU 训练时能加快数据传输但如果你的内存紧张可以关掉。shuffleTrue只对训练集开验证集必须保持顺序一致方便复现。3. CNN 和 ResNet 模型搭建从零写还是拿预训练权重3.1 一个能跑通的简易 CNN 基线如果你只是想先跑通流程或者课程作业要求自己搭网络那从三层卷积加两层全连接开始就够了。这个基线模型结构简单训练快适合用来验证数据管道有没有问题。输入 224x224 的图片经过三次卷积池化最后展平接全连接分类。import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes6): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), # 输入3通道输出32通道 nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 224 - 112 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 112 - 56 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 56 - 28 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), # 全局平均池化输出128x1x1 nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(64, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return xnum_classes要改成你数据集的类别数垃圾分类常见是 4 到 6 类。BatchNorm2d放在卷积后面、激活前面能加速收敛。AdaptiveAvgPool2d(1)把任意尺寸的特征图压成 1x1这样就不用关心输入图片尺寸变化。Dropout(0.5)在全连接层之间做正则防止过拟合。这个模型参数量大概几十万在 CPU 上也能跑适合快速验证。3.2 ResNet 预训练模型怎么接自己的分类头ResNet 的核心是残差连接让梯度能绕过某些层直接回传从而训练更深的网络。做垃圾分类时我一般直接用torchvision里的 ResNet18 或 ResNet50加载 ImageNet 预训练权重然后把最后的全连接层换成自己类别数的输出。这样即使你的数据集只有几千张也能借助预训练特征拿到不错的效果。import torchvision.models as models def build_resnet(num_classes6, pretrainedTrue): model models.resnet18(pretrainedpretrained) # 加载ImageNet预训练权重 in_features model.fc.in_features # ResNet18是512 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model model build_resnet(num_classes6) print(f可训练参数: {sum(p.numel() for p in model.parameters() if p.requires_grad)})pretrainedTrue会下载 ImageNet 权重第一次运行需要联网。如果网络不通可以提前下载好权重文件放到~/.cache/torch/hub/checkpoints/目录。替换model.fc时加一个Dropout是常见做法因为预训练模型在全连接层容易过拟合小数据集。ResNet18 的in_features是 512ResNet50 是 2048这个值不用手动记用model.fc.in_features自动获取就行。3.3 冻结与微调什么时候该动预训练层加载预训练权重后有两种训练策略一种是冻结前面的卷积层只训练新加的全连接层另一种是全部解冻用较小的学习率微调整个网络。我的经验是如果数据集少于 2000 张先冻结卷积层训练 5 到 10 个 epoch再解冻全部微调 10 到 20 个 epoch。这样能避免一开始就大改预训练特征导致震荡。# 阶段一冻结卷积层 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) # 阶段二解冻全部用小学习率微调 for param in model.parameters(): param.requires_grad True optimizer torch.optim.Adam(model.parameters(), lr1e-4)冻结时用filter只把需要梯度的参数传给优化器避免更新被冻结的层。解冻后学习率要降一个数量级因为预训练权重已经比较好了大步长反而会破坏。如果训练时发现验证集准确率一直不涨可以检查一下是不是忘记切换model.train()和model.eval()这个坑后面还会细说。4. 训练循环与参数调优让垃圾分类模型真正收敛4.1 损失函数、优化器和学习率调度垃圾分类是单标签多分类任务损失函数用交叉熵CrossEntropyLoss就行。优化器我一般选 Adam 或 SGDAdam 收敛快但最终精度可能略低SGD 加动量调好了泛化更好。学习率调度用余弦退火或者 StepLR前者更平滑后者更可控。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30, eta_min1e-6)AdamW是 Adam 的权重衰减修正版比 Adam 更稳定。weight_decay1e-4是常用的正则强度太大欠拟合太小过拟合。CosineAnnealingLR的T_max设成总 epoch 数eta_min是最小学习率一般设到 1e-6 就够。如果训练轮数少比如只有 20 个 epochT_max就设 20。4.2 完整训练循环与验证指标记录训练循环里最容易翻车的地方是忘记切换train和eval模式导致 BatchNorm 和 Dropout 在验证时行为不对。下面是一个标准的训练加验证流程每个 epoch 记录训练损失、验证损失和验证准确率。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) _, preds outputs.max(1) correct (preds labels).sum().item() total imgs.size(0) return total_loss / total, correct / total def validate(model, loader, criterion, device): model.eval() total_loss, correct, total 0, 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) _, preds outputs.max(1) correct (preds labels).sum().item() total imgs.size(0) return total_loss / total, correct / total best_acc 0 for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc validate(model, val_loader, criterion, device) scheduler.step() if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_garbage_model.pth) print(fEpoch {epoch1:02d} | Train Loss {train_loss:.4f} Acc {train_acc:.4f} | fVal Loss {val_loss:.4f} Acc {val_acc:.4f})model.train()和model.eval()必须成对出现前者启用 Dropout 和 BatchNorm 的训练行为后者固定它们。torch.no_grad()在验证时关闭梯度计算省显存也提速。保存模型时只存state_dict()加载时先建同结构模型再load_state_dict()。best_acc用来追踪最佳验证精度避免保存过拟合的最后一轮。4.3 学习率和批大小的联动调整学习率和批大小是联动的批大小翻倍学习率一般也要适当放大。如果你从batch_size32改成 64学习率可以从 1e-3 提到 2e-3 试试。反过来如果显存不够只能跑batch_size8学习率要降到 3e-4 左右否则梯度噪声太大损失会震荡。另外如果训练损失下降但验证损失上升说明过拟合了可以加数据增强、加 Dropout、或者减小模型容量。如果两个都不降那是欠拟合要加大学习率或者检查数据标签有没有错。5. 避坑与排查垃圾分类训练中最容易翻车的 5 个地方5.1 现象训练准确率很高验证准确率只有随机水平原因最常见的是数据泄漏训练集和验证集里有同一张图片的不同副本或者验证集的 transform 用了训练增强。另一个可能是标签映射错了ImageFolder按文件夹名排序生成类别索引如果你自己手动映射标签顺序对不上就会全错。解决先用random_split固定随机种子确保切分可复现。然后打印full_dataset.class_to_idx确认类别索引。验证集 transform 单独设置不要复用训练增强。如果还是不对拿几张验证集图片手动过一遍模型看预测结果和真实标签的对应关系。5.2 现象损失变成 NaN训练直接崩掉原因学习率太大、数据里有脏样本全黑或全白图片、或者用了BCEWithLogitsLoss却没做标签平滑。垃圾分类用交叉熵一般不会 NaN但如果图片归一化参数写错比如把标准差写成 0就会除零。解决先把学习率降到 1e-4 试跑几个 batch。检查归一化参数std不能为 0。用torch.isnan(loss)在训练循环里加断言一旦出现 NaN 就打印当前 batch 的图片统计信息。另外AdamW的weight_decay不要设太大超过 1e-2 也可能导致数值不稳定。5.3 现象显存溢出报 CUDA out of memory原因batch_size太大、模型参数量太大、或者验证时没有用torch.no_grad()导致梯度累积。还有一种情况是num_workers设太高每个 worker 都复制了一份数据到显存。解决先把batch_size减半如果还不行就换小模型比如从 ResNet50 换成 ResNet18。验证循环必须包在torch.no_grad()里。num_workers一般设 2 到 4 就够太多反而占资源。如果用的是 GPU可以在训练前加torch.cuda.empty_cache()清理缓存。5.4 现象验证集准确率波动很大一会儿高一会儿低原因验证集太小或者 BatchNorm 在验证时统计量不稳定。如果验证集只有几十张准确率波动是正常的。另外如果训练时shuffleTrue但验证时也开了 shuffle评估结果就不可比。解决验证集至少占总数据的 15% 到 20%如果数据量实在少用 K 折交叉验证。验证集shuffle必须设False。BatchNorm 在eval模式下用训练时累积的 running mean 和 var如果训练不充分这些统计量不准可以尝试用 GroupNorm 替代 BatchNorm。5.5 现象模型对某些类别完全预测不出来原因类别不均衡少数类样本太少模型倾向于预测多数类。或者某些类别的图片特征太相似比如“其他垃圾”和“厨余垃圾”里都有塑料袋。解决先统计每个类别的样本数对少数类做过采样或者用WeightedRandomSampler。损失函数里加类别权重CrossEntropyLoss(weightclass_weights)权重和样本数成反比。如果特征确实难分考虑用更强的 backbone比如 ResNet50 或 EfficientNet或者加注意力模块。但要注意如果人工都分不清模型也很难学会这时候要回头检查标注质量。6. 从能跑到好用垃圾分类模型的进阶技巧与验证习惯模型能跑通之后下一步是让它真正好用。我一般会做三件事第一用混淆矩阵看哪些类别容易混针对性补数据第二用 Grad-CAM 可视化模型关注区域确认它看的是物体而不是背景第三导出 ONNX 或 TorchScript 做推理速度测试确保部署时不会太慢。混淆矩阵用sklearn.metrics.confusion_matrix几行就能画Grad-CAM 可以用pytorch-grad-cam库ONNX 导出用torch.onnx.export。这些工具不需要改模型结构属于后悔药级别的排查手段。from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds outputs.max(1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesfull_dataset.classes)) cm confusion_matrix(all_labels, all_preds) print(cm)classification_report会输出每个类别的精确率、召回率和 F1比只看总体准确率有用得多。混淆矩阵的非对角线元素就是误分类情况如果“可回收物”大量被预测成“其他垃圾”说明这两个类的特征边界模糊需要加更多区分性样本。我习惯每个项目至少跑一次这个报告不然心里没底。还有一个血泪经验永远保留一个独立的测试集不要用验证集调参又用验证集报告最终结果。测试集只在最后跑一次用来估计真实泛化能力。如果测试集准确率比验证集低很多说明调参过拟合了验证集。另外随机种子要固定torch.manual_seed(42)、np.random.seed(42)、random.seed(42)三件套加上torch.backends.cudnn.deterministic True保证每次训练结果可复现。这些习惯看起来琐碎但到了写论文或答辩的时候能帮你省掉很多解释不清的麻烦。希望帮到你。本文还有配套的精品资源点击获取
返回列表