简介面向毕业设计与课程作业场景这份基于CNN与ResNet的垃圾分类实战源码包适合正在学习图像分类、需要快速搭建可运行项目的Python开发者。压缩包共6个文件包含两个Python脚本模型定义与预测推理、两个PyTorch权重文件、一个验证打包压缩包及一份说明文档整体约4MB轻量易部署。内容覆盖从模型训练到预测的典型流程PyTorch权重可直接加载使用配合预测脚本即可对图片进行垃圾类别推断说明文档则帮助理解项目结构与运行方式辅助完成课程报告或毕设答辩。同时代码结构清晰便于替换数据集进行迁移学习可结合自己的训练数据微调模型。目前已有77人学习下载对于希望参考CNN与ResNet在实际分类任务中差异、并获取完整可复现方案的读者是一份简洁实用的参考资料。1. 毕设里的垃圾分类实战先分清 CNN 基线和 ResNet 迁移是两件事打开压缩包之前先想清楚一件事标题里的“基于 CNN 和 ResNET”不是二选一而是一条从浅到深的路线。毕设和课程作业里最常见的死法是一上来就加载 ResNet50 预训练模型直接训练然后对着一条不降的验证损失发愣。垃圾分类这个图像识别任务难点不在网络结构有多新而在数据不均衡、类别相似度高比如塑料袋和保鲜膜以及评价标准没有立住。这篇笔记按“先搭 CNN 基线、再换 ResNet 迁移学习”的顺序把数据集怎么分、超参怎么定、五个常见翻车点的现象与解决办法讲清楚。适合正在做毕业设计或课程大作业、想在答辩前把指标和可解释性都拿得出手的同学。2. 先把数据集和评判标准立住垃圾分类不是“能跑就行”拿到压缩包第一步别急着打开模型文件先花半小时把数据摊开看。垃圾分类实验里绝大多数“模型不行”的结论其实都不是模型的问题而是数据划分和评价指标在项目开始时就埋了雷。2.1 数据集选择与标签设计先确认你手里是几分类问题毕设和课程作业常见的数据集有三类走向。第一种是直接使用公开数据集比如 Kaggle 上那个六类垃圾分类纸板、玻璃、金属、纸、塑料、垃圾桶背景干净、类别边界清楚适合先把整条代码流程验证通。第二种是国内竞赛里常见的垃圾分类数据按可回收、厨余、有害、其他四分类组织也有细分到四十类左右的版本更贴近真实场景但类别不均衡、细类相似的问题也更明显。第三种是自拍的小样本几十到几百张这种数据过拟合风险最大后面所有防止过拟合的手段都要用上。不管数据是哪来的先写一段统计脚本看每类数量import os from collections import Counter data_dir data counter Counter() for cls in os.listdir(data_dir): cls_dir os.path.join(data_dir, cls) if not os.path.isdir(cls_dir): continue files [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .jpeg, .png))] counter[cls] len(files) for cls, n in counter.most_common(): print(f{cls}: {n})这段代码只做一件事遍历 data 目录下的类别文件夹按扩展名过滤出图片文件并统计数量。值得注意的参数是扩展名过滤很多课程数据集里同时混着.jpg和.png漏掉任何一种统计结果就和之后ImageFolder实际加载的数量对不上。打印结果按数量降序排列哪一类样本偏少一眼就能看出来。接着是划分训练集和验证集。我一般不用简单的random_split(0.8)而是按类别分层切分保证每个类别在验证集里都有样本。PyTorch 里先把所有样本路径按类别组织成数组用sklearn.model_selection.train_test_split加stratifyy参数再分别构造 Dataset。这里最容易踩的坑是不设random_state导致每次跑代码划分结果都变训练曲线的对比全部失去意义。分层划分的代码写起来就多几行但小类别能在验证集里占一席之地评测才算数。数据划分大致有三种方案划分方案适用场景需要警惕的问题全部数据随机划分各类数量接近验证集可能缺某个小类按类别分层划分各类数量差异大必须固定随机种子按拍摄批次/来源划分有多个采集来源同一来源同时进训练和验证会泄漏2.2 评价指标别只报 accuracy答辩老师一眼看穿单看准确率在垃圾分类上很骗人。假设验证集里有 60% 可回收、20% 厨余、15% 其他、5% 有害一个什么都不学、永远输出“可回收”的模型准确率也有 60%。课程作业里经常看到“准确率 85%”这样的结论听起来不错但按类别一拆有害类的召回率可能是 0因为样本太少模型把它们全判成了别的类。所以我的习惯是第一轮就跑一个多数类 baseline 和一个完整的分类报告import numpy as np from sklearn.metrics import classification_report, confusion_matrix # y_val: 验证集标签数组; preds: 模型在验证集上的预测 majority np.argmax(np.bincount(train_labels)) # 训练集中最多的类别 baseline np.full_like(y_val, majority) print(majority baseline acc:, (baseline y_val).mean()) print(classification_report(y_val, preds, target_namestrain_ds.classes)) print(confusion_matrix(y_val, preds))这段代码分两层。先用np.bincount(train_labels)统计训练集标签频次argmax取样本最多的类别把它当成“躺平模型”的输出算出的准确率就是这个任务的地板值如果连这个地板都不如说明训练过程本身有问题。再用classification_report输出每个类别的 precision、recall、F1。唯一要注意的参数是target_names顺序必须和标签编码一致否则打印出来的类别名字对不上。把混淆矩阵存成图放答辩 PPT 里效果比报数字好得多。老师问“你这模型哪类分不好”直接指矩阵里最亮的非对角元素比背两个数字有说服力。当 macro-F1 比 accuracy 低五个百分点以上时基本可以断定是不均衡问题下一步该往类权重、重采样方向走而不是继续堆模型层数。3. 从零搭一个 CNN 基线结构、训练超参与第一次调参有了数据和指标先别急着碰 ResNet。花一两个小时把 CNN 基线跑通后面换 ResNet 时才有对比对象也才知道迁移学习到底帮你涨了多少分。CNN卷积神经网络是这个方向的入门主干PyTorch 也是现在毕设里最常见的深度学习框架下面的代码在普通电脑上都能跑没有独显也能完成实验。3.1 最小可运行的 CNN 结构Conv-BN-ReLU 四层怎么排模型故意不做花哨设计四个卷积块加全局平均池化再挂一个分类头。每一块都是 Conv3×3 → BN → ReLU → MaxPoolimport torch.nn as nn class GarbageCNN(nn.Module): def __init__(self, num_classes6): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1), ) self.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x).flatten(1))结构选择的逻辑是Conv 后面紧跟 BN 再接 ReLU是现代 CNN 的默认顺序。BN 让每层的输入分布更规整ReLU 提供非线性MaxPool 负责降分辨率把 224×224 逐级压到 28×28最后用 AdaptiveAvgPool2d(1) 拿到 256 维特征向量。这里最值得说的是 AdaptiveAvgPool 的好处图片输入是 224 还是 112到最后一层之前的尺寸都是 1×1中途想换小分辨率省时间时模型代码一行不用改。分类头只放 Dropout 和一层全连接数据量不大时再加隐藏层只会更快过拟合Dropout 0.3 是起步值。3.2 训练超参的保守起点batch size、学习率、epoch 怎么定训练循环里每个参数都是保守起点先保证能收敛再谈优化import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(data/train, transformtransform) val_ds datasets.ImageFolder(data/val, transformtransform) val_ds.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse) model GarbageCNN(num_classeslen(train_ds.classes)) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr3e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size8, gamma0.5) for epoch in range(20): model.train() train_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: optimizer.zero_grad() out model(images) loss criterion(out, labels) loss.backward() optimizer.step() train_loss loss.item() correct (out.argmax(1) labels).sum().item() total labels.size(0) scheduler.step() print(fepoch {epoch:02d} | loss {train_loss / len(train_loader):.4f} f| acc {correct / total:.4f})几个参数先说清楚。batch size 32 在 CPU 上跑 224 输入不会把内存吃满显存小就降到 16学习率 3e-4 配 Adam 是课程作业里最不容易发散的组合别一上来用 1e-2StepLR 每 8 个 epoch 把学习率减半避免后期在损失面上来回震荡。代码里有个容易漏掉的细节验证集的 transform 要单独写因为 ImageFolder 只接受一个 transform如果训练集里写了 RandomHorizontalFlip 而验证集也继承指标会虚高这叫数据泄漏。用Resize((224, 224))时注意它是先统一短边再裁剪还是直接拉伸容器参数写法不同结果差很多课程作业里一般直接拉伸最省事。提示每个 epoch 结束在验证集上算一次准确率只保留指标最优的那一次权重这是防止过拟合翻车的第一道保险。这个训练循环在 CPU 上跑得动吗可以。四层卷积的参数量在九十万左右一个 epoch 在普通笔记本 CPU 上大约几分钟20 个 epoch 一个晚上能跑完。跑完后记下准确率和 macro-F1这就是后面所有模型要对比的地板。如果对深度学习框架还不熟可以把《动手学深度学习》里卷积神经网络那一章过一遍再回来改超参。4. 换 ResNet 预训练模型迁移学习的参数与三个关键选择CNN 基线跑通后接下来才是标题里真正的重头戏把 backbone 换成 ResNet 预训练模型。迁移学习要调的面比 CNN 多涨分的确定性也高但前提是选型和参数没有踩坑。4.1 选 ResNet-18 还是 ResNet-50样本量和算力决定层数ResNet 的核心是残差连接每两层之间有一条短路让梯度能直接回传所以网络能做得深而不明显退化。但对垃圾分类这种几万张以内的数据ResNet-50 不一定比 ResNet-18 好多少。ResNet-18 的参数量大约一千一百万ResNet-50 是它的两倍多而两者在 ImageNet 上的精度差距主要体现在细粒度分类上。垃圾图片类别少、类间差距大ResNet-18 的容量完全够用训练时间省一半显存占用也省一半。只有样本量到万级以上、并且打算全量微调 backbone 时ResNet-50 才有必要。课程作业答辩时ResNet-18 足够把“预训练 微调”这个考点讲清楚数字上也更容易复现。4.2 冻结 backbone 还是全量微调两段式方案与分组学习率用 torchvision 加载预训练权重并替换分类头import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes), ) # 方案 A冻结 backbone只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True方案 A 适合样本特别少每类不到一百张的情况。backbone 不动只学一个新分类头训练快也不容易过拟合代价是 ImageNet 上学到的特征和垃圾图片有分布偏差准确率上限低一些。方案 B 是两段式先按方案 A 冻结 backbone 训五轮左右再把全部参数放开用更小的学习率继续训练。放开后的优化器要按参数组分配学习率这是迁移学习里最值得记的参数设计optimizer torch.optim.Adam([ {params: model.fc.parameters(), lr: 1e-3}, {params: (p for n, p in model.named_parameters() if not n.startswith(fc)), lr: 1e-4}, ])这段代码的核心是分类头和 backbone 用不同学习率分类头是随机初始化需要大步长快速学backbone 已有预训练权重学习率太大一步就把学好的特征冲坏了网上大量“换了 ResNet 反而打不过自己搭的 CNN”的帖子根子都在这。backbone 学习率取分类头的十分之一到五分之一1e-4 是常见安全值。第二阶段把整个优化器重建一遍所有参数统一用 1e-4 到 5e-5继续训十轮左右收敛。注意weightsmodels.ResNet18_Weights.DEFAULT的写法需要 torchvision 0.13 及以上版本旧教程里的pretrainedTrue在新版本里已经弃用。微调方案冻结范围学习率参考适用样本量风险点只训分类头backbone 全冻fc: 1e-3每类 100 张上限低两段式微调先冻后全放开backbone: 1e-4每类 200 张BN 状态易出错4.3 预处理三连坑归一化、输入尺寸与 BN 的 eval 状态第一个坑是归一化。预训练模型的权重是在 ImageNet 的 mean 和 std 下学出来的所以输入必须用0.485, 0.456, 0.406/0.229, 0.224, 0.225做标准化。很多人从 CNN 基线带过来一套自己算的统计量换到 ResNet 后训练曲线直接乱掉原因是输入分布和预训练特征分布错位。第二个坑是输入尺寸。ResNet 第一个卷积是 7×7 步长 2图片缩到 112 以下信息损失很快统一用 224×224 最省事。第三个坑在方案 A 里冻结 backbone 后如果模型整体还处于train()模式BatchNorm 的 running 统计量依然会被当前 batch 更新预训练特征的分布会被一点一点带偏。解决方法是把 backbone 里的 BN 层手动切到eval()或者干脆采用方案 B 全量微调。还有一个实际操作问题加载预训练权重时 torchvision 会自动检查本地缓存首次运行需要联网下载权重文件先单独跑一次确认下载完整再进训练流程避免训练到一半卡在下载上。5. 避坑清单垃圾分类实战里最常翻车的 5 个地方下面五个问题是课程作业和毕设里反复出现、我也踩过的按出现频率排序。每条都按现象、原因、解决三步拆开对照着排查比重新读一遍文档快。5.1 训练 loss 一直在降验证准确率却原地踏步现象训练集 loss 从 2.3 降到 0.4验证集准确率停在 65% 左右不动肉眼可见地过拟合。原因垃圾分类数据集通常不大模型容量相对过剩训练只做了水平翻转和颜色扰动数据增强太弱也没有保存最优模型的机制最后拿去做验证的是最后一个 epoch 的权重。解决在增强里加上随机旋转、缩放和随机擦除每项小步加观察验证指标而定每个 epoch 结束都在验证集上算指标只保存最好的 checkpoint。增强参数不要一次拉满一下加太多反而让训练集和真实分布偏离。5.2 换 ResNet 预训练模型后验证准确率反而不如 CNN 基线现象CNN 基线已经到 75%换 ResNet-18 预训练权重微调二十轮只有 70%。原因三个常见原因按概率排序。一是归一化统计量还是自己算的那套没有切回 ImageNet 的 mean/std二是冻结 backbone 后 BN 层在 train 模式下 running 统计量被污染三是两段式微调时 backbone 学习率设得和分类头一样大预训练特征被冲坏。解决先检查预处理是否和 torchvision 的 ResNet 训练配置完全一致再分别用model.eval()和model.train()验证一次确认 BN 状态的影响最后把 backbone 学习率降到 1e-4 重新跑。5.3 整体准确率挺高但某几个类别的 F1 是 0现象验证集准确率 82%打开 classification_report有害垃圾这一类的 precision 和 recall 全是 0。原因训练集有害类样本只有几十张其他类别几百张CrossEntropyLoss 默认按均匀权重优化模型学到的是“全猜多数类也能把 loss 压得很低”的路径。解决给损失函数加类别权重。PyTorch 里用torch.tensor(weights)传入CrossEntropyLoss(weight...)权重取各类样本数的倒数再归一化或者用sklearn.utils.class_weight.compute_class_weight一键生成。加了权重后准确率可能略降但 macro-F1 会明显上升答辩时讲清楚这是主动取舍比回避问题体面得多。5.4 保存再加载模型时报 size mismatch答辩前一晚崩溃现象训练时跑得好好的第二天torch.load后model.load_state_dict(state_dict)报size mismatch for fc.weight。原因保存时用的是整模型torch.save(model, path)或者加载时模型结构对不上更常见的是保存前改了分类头的类别数旧权重里 fc 层的 shape 和新模型不一样。解决约定只用torch.save(model.state_dict(), path)加载前先按同样的结构重新构造模型再load_state_dict。如果确实改了类别数旧 checkpoint 是没法直接用的别硬加载重训五轮也比排查一个坏掉的权重文件快。size mismatch的报错信息里会写明是哪一层的 shape 不一致照着那层去对代码五分钟内能定位。5.5 显存 OOM或者训练越跑越慢现象batch size 32 跑 ResNet-50第三个 epoch 直接 CUDA out of memory没 OOM 的也越跑越慢。原因OOM 是 ResNet-50 加 224 输入太吃显存越跑越慢常见是因为验证集和训练集共用了 transform 对象、DataLoader 的num_workers开太大把系统资源占满或者训练循环里每次 forward 都新建了计算图没释放。解决先换 ResNet-18 或把 batch size 降到 16确认验证 transform 是独立对象Windows 下把num_workers设成 0多进程 DataLoader 在课程环境里经常卡死这个坑遇到一次就老实了。混合精度训练torch.cuda.amp是终极手段但课程作业用不上别为了它多引入一个排错变量。6. 给答辩加分的最后一招把错误样本捞出来而不是继续调参模型跑通、指标也看了先别急着换更深的结构。我每次实验收尾都会做一件事把验证集里预测错的图片连同文件名、真实类别、预测类别导出来按类别分组看规律。model.eval() wrong [] with torch.no_grad(): for batch_idx, (images, labels) in enumerate(val_loader): preds model(images).argmax(dim1) for i, (p, l) in enumerate(zip(preds, labels)): if p ! l: sample_idx batch_idx * val_loader.batch_size i path, _ val_ds.samples[sample_idx] wrong.append((path, l.item(), p.item()))注意这里的val_loader必须shuffleFalse否则用 batch_idx 推算的样本下标对不上真实路径。把 wrong 列表存成 CSV按真实类别统计错误分布很快就能发现模型老是分不清哪几对类别。看错误样本得到的结论通常两类。一类误判来自光照、遮挡和背景干扰这类靠增强就能补另一类是标签边界本身模糊比如“纸”和“纸盒”在数据集里原样就标得不一致这种错误换什么网络都消不掉正确处理是合并类别或者修正标注。我毕设里从 88% 磨到 92%靠的更多是修数据而不是换模型——模型是黑匣子能直接干预的只有喂进去的数据这条经验比任何一条超参都值钱。答辩时把“发现并修正了一批错标样本”写进工作量比反复强调准确率数字更能顶住追问。希望帮到你。本文还有配套的精品资源点击获取