☰
基于ResNet50迁移学习的华为垃圾数据集分类实战:从数据组织到模型微调
2026/10/11 21:24:04 网站建设 项目流程

简介:这份资源面向具备一定Python与深度学习基础的学习者,提供一套基于ResNet50迁移学习的华为垃圾数据集图像分类系统源码,可用于理解预训练模型微调、数据清洗与分类评估的完整流程。压缩包共18个文件,约2.21MB,包含6个py脚本、3个zbak备份、3个txt记录、2个png训练曲线图、1个json分类规则、1个md说明及pyc缓存等,覆盖数据标注、npy生成、模型训练、预测与UI界面等模块。已有73人学习下载。读者可参考其中的迁移学习实现思路、损失与准确率曲线、分类规则配置及界面代码,快速搭建自己的图像分类实验,并在此基础上调整网络结构或替换数据集,适合课程设计、毕业项目与技能进阶练习。

1. 从一张华为垃圾数据集到 ResNet50 迁移学习分类系统:这套源码到底解决什么问题

很多人第一次拿到「华为垃圾数据集」时,会以为它跟手机品牌有关,其实它指的是华为云 ModelArts 平台上公开的一套垃圾分类图像数据集,常见版本按可回收物、厨余垃圾、有害垃圾、其他垃圾等大类组织,图像数量在数千到上万张量级,类别之间样本量并不均衡。真正上手做分类时,痛点立刻暴露:从零训练一个卷积网络,要么显存不够,要么几十个 epoch 下去验证集准确率还在 60% 附近打转。基于 ResNet50 的 Python 迁移学习方案,就是冲着这个痛点来的——用 ImageNet 预训练权重做底座,冻结主干、换掉分类头、小学习率微调,几百张到几千张图就能把准确率拉到可用区间。这套源码适合三类人:刚学完 Python 和 PyTorch、想找一个完整图像分类项目练手的新手;手里有自己行业图像数据、想套用迁移学习模板的工程师;以及需要快速验证「垃圾分类 + 深度学习」可行性的产品同学。下面我按自己实际跑通的顺序,把数据组织、模型改造、训练调参、踩坑排查一路讲清楚。

2. 迁移学习为什么在垃圾图像分类上比从零训练更靠谱

2.1 从零训练在小数据集上的三个硬伤

垃圾图像分类的典型数据规模是几千张、十几类,这个量级对卷积网络来说属于「小样本」。从零训练会遇到三个绕不开的问题。第一是过拟合,网络参数量动辄两千万以上,而样本只有几千张,训练集准确率冲到 99% 时验证集往往还在 70% 徘徊,这就是典型的记住了训练集噪声。第二是收敛慢,随机初始化下浅层卷积核要花大量迭代才能学到边缘、纹理这类基础特征,训练成本高。第三是类别不均衡放大误差,可回收物样本可能比有害垃圾多好几倍,模型会倾向于预测多数类,少数类召回率惨不忍睹。

迁移学习的思路是:ImageNet 上训练的 ResNet50,浅层和中层已经学到了通用的边缘、颜色块、纹理、局部形状特征,这些特征对垃圾图像同样有效,因为垃圾识别本质上也是靠形状、材质、颜色来判断的。我们只需要替换最后的全连接分类层,让网络把已有特征重新映射到垃圾类别上。常见做法是先冻结主干只训练分类头,再解冻部分层做小学习率微调,这样既省算力又稳。

2.2 ResNet50 的残差结构为什么适合做迁移底座

ResNet50 的核心是残差块,输入通过一条跳跃连接直接加到输出上,公式上是y = F(x) + x。这个设计解决了深层网络的退化问题——没有残差时,网络越深训练误差反而可能上升。对迁移学习来说,残差结构还有个隐性好处:恒等映射让梯度能顺畅回传,微调时即使只解冻后面几个 stage,梯度也能有效传到浅层,不会出现「解冻了但学不动」的情况。

ResNet50 的 50 指带权重的层数,整体分成 conv1、conv2_x 到 conv5_x 五个阶段,最后接全局平均池化和全连接层。做迁移时我们通常保留到 conv5_x 的输出,把原来的 1000 类全连接换成自己的类别数。选 50 而不是 18 或 101,是因为 50 在精度和显存之间比较平衡:18 太浅,特征表达能力有限;101 参数量翻倍,小数据集上收益不明显还更容易过拟合。

2.3 冻结策略与学习率分层:迁移学习最关键的两个旋钮

冻结策略决定了「改多少」。我一般分三档:只训练分类头(冻结全部主干),适合数据量小于两千张、类别少于十类;解冻 conv5_x 和分类头,适合几千张的中等规模;全部解冻但用极小学习率,适合上万张、和 ImageNet 差异较大的数据。垃圾图像和 ImageNet 的自然图像分布接近,通常第二档就够。

学习率分层是配套手段。主干用 1e-4 甚至 1e-5,分类头用 1e-3,因为分类头是随机初始化的,需要更大步长快速收敛,而主干权重已经很好,大学习率会破坏预训练特征。PyTorch 里通过给不同参数组设置不同 lr 实现,下面代码会体现。

import torch import torch.nn as nn from torchvision import models def build_model(num_classes=4, freeze_backbone=True): # 加载 ImageNet 预训练权重,weights 参数在新版 torchvision 中替代 pretrained model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) # 替换分类头:ResNet50 的 fc 输入维度是 2048 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) if freeze_backbone: # 冻结主干全部参数,只让新分类头参与训练 for name, param in model.named_parameters(): if not name.startswith("fc."): param.requires_grad = False return model def build_optimizer(model, backbone_lr=1e-4, head_lr=1e-3): # 参数分组:主干和分类头用不同学习率 backbone_params, head_params = [], [] for name, param in model.named_parameters(): if not param.requires_grad: continue if name.startswith("fc."): head_params.append(param) else: backbone_params.append(param) return torch.optim.Adam([ {"params": backbone_params, "lr": backbone_lr}, {"params": head_params, "lr": head_lr}, ])

这段代码做了三件事:加载预训练权重、替换分类头、按参数名分组设置学习率。weights=models.ResNet50_Weights.IMAGENET1K_V2是较新 torchvision 的写法,老版本用pretrained=True,两者不能混用,混用会报参数冲突。freeze_backbone=True时只训练 fc 层,适合快速验证;数据量上来后把它设为 False,主干就会以 backbone_lr 参与微调。参数分组里requires_grad判断不能省,否则冻结的参数也会进优化器,白白占显存。

3. 把华为垃圾数据集喂进 ResNet50:目录组织与 Dataset 写法

3.1 数据目录怎么摆才能被 ImageFolder 直接读

PyTorch 的ImageFolder要求按类别分文件夹,每个文件夹名就是类别名,这是最省事的组织方式。华为垃圾数据集下载后常见的是压缩包或带标注文件的格式,需要先整理成下面这种结构:

dataset/ ├── train/ │ ├── recyclable/ │ ├── kitchen_waste/ │ ├── hazardous/ │ └── other/ └── val/ ├── recyclable/ ├── kitchen_waste/ ├── hazardous/ └── other/

如果原始数据只有一个大文件夹加一个 label 列表,就得写脚本按 label 把图片复制或软链到对应子目录。我一般用软链,避免复制占空间。划分比例上,数据量小于五千张时按 8:2 分训练验证,大于五千张可以 9:1。注意验证集要按类别分层抽样,否则某个类别可能一张都没进验证集,评估结果就没意义。

3.2 Dataset 与 DataLoader:预处理、增强和类别不均衡处理

图像分类的预处理分训练和验证两套。训练用随机裁剪、随机翻转、颜色抖动做增强,验证只用 resize 加中心裁剪,保证评估可复现。归一化参数用 ImageNet 的均值和标准差,因为主干是在这个分布上预训练的,不统一会拖慢收敛。

from torchvision import transforms, datasets from torch.utils.data import DataLoader, WeightedRandomSampler import numpy as np train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 随机裁剪到 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]), # ImageNet 统计量 ]) 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_set = datasets.ImageFolder("dataset/train", transform=train_tf) val_set = datasets.ImageFolder("dataset/val", transform=val_tf) # 处理类别不均衡:按类别样本数倒数给权重 targets = np.array(train_set.targets) class_count = np.bincount(targets) class_weight = 1.0 / class_count sample_weight = class_weight[targets] sampler = WeightedRandomSampler(sample_weight, num_samples=len(sample_weight), replacement=True) train_loader = DataLoader(train_set, batch_size=32, sampler=sampler, num_workers=4) val_loader = DataLoader(val_set, batch_size=32, shuffle=False, num_workers=4)

RandomResizedCrop的 scale 下限设 0.7 是经验值,太低会把垃圾主体裁掉,太高增强效果弱。WeightedRandomSampler让少数类被采样的概率提高,缓解不均衡,但它和shuffle=True互斥,用了 sampler 就不能再开 shuffle。num_workers在 Windows 上建议设 0 或 2,设大了容易在脚本入口没加if __name__ == "__main__"时反复重启进程。batch_size 32 是 8G 显存跑 ResNet50 的稳妥值,显存够可以上 64。

3.3 训练循环与验证指标:别只看准确率

训练循环里要同时记录训练损失、验证损失和验证准确率。垃圾数据集类别不均衡时,准确率会被多数类主导,必须补上每个类别的召回率和混淆矩阵。下面是一个精简的训练骨架:

import torch from tqdm import tqdm def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0.0 for imgs, labels in tqdm(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) return total_loss / len(loader.dataset) @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 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.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / len(loader.dataset), correct / total

model.train()和model.eval()必须成对出现,前者启用 BatchNorm 的批统计和 Dropout,后者用滑动平均统计量,漏写 eval 会让验证结果波动很大。torch.no_grad()在验证时省显存,不加也能跑但没必要浪费。损失函数默认用nn.CrossEntropyLoss(),如果类别极不均衡,可以传weight参数,权重按类别频率倒数算,和 sampler 二选一即可,两个一起用容易矫枉过正。

4. 训练参数怎么调:从学习率到早停的实操清单

4.1 学习率、batch size 与 epoch 的搭配关系

这三个参数不是独立的。经验上,batch size 翻倍,学习率可以相应放大,但迁移学习里主干学习率本来就小,放大空间有限。我常用的组合是:冻结阶段 batch 32、head_lr 1e-3、跑 10 个 epoch;微调阶段 batch 32、backbone_lr 1e-4、head_lr 1e-4、跑 20 到 30 个 epoch。判断学习率是否合适,看训练前几个 epoch 的损失曲线:下降太慢说明偏小,震荡甚至上升说明偏大。

epoch 不是越多越好。小数据集上通常 20 到 40 个 epoch 就收敛,再往后验证损失开始上升就是过拟合信号。配合早停,验证准确率连续 5 个 epoch 不提升就停,能省不少时间。

4.2 优化器与调度器:Adam 还是 SGD

迁移学习里 Adam 收敛快、对学习率不敏感,适合快速出结果;SGD 加动量在充分调参后泛化可能略好,但需要更细的学习率搜索。我一般先用 Adam 跑通,确认流程没问题后再考虑换 SGD 对比。调度器用CosineAnnealingLR或StepLR,让学习率随训练衰减,后期更稳。

import torch.nn as nn from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_model(num_classes=4, freeze_backbone=True).to(device) criterion = nn.CrossEntropyLoss() optimizer = build_optimizer(model, backbone_lr=1e-4, head_lr=1e-3) scheduler = CosineAnnealingLR(optimizer, T_max=10) best_acc = 0.0 for epoch in range(10): train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) scheduler.step() print(f"epoch {epoch}: train_loss={train_loss:.4f} val_loss={val_loss:.4f} val_acc={val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_resnet50_garbage.pth")

CosineAnnealingLR的T_max设成总 epoch 数,学习率会按余弦曲线从初始值降到接近 0。保存权重时存state_dict()而不是整个模型,加载时先建同结构模型再load_state_dict,这样跨设备、跨版本更稳。best_acc只在提升时更新,避免把过拟合的后期权重覆盖掉最优权重。

4.3 微调阶段怎么解冻:分层解冻与差分学习率

冻结训练收敛后进入微调,把freeze_backbone设为 False 重建优化器,或者直接改requires_grad。更细的做法是分层解冻:先解冻 conv5_x,跑几个 epoch,再解冻 conv4_x,逐层放开。这样浅层预训练特征不会被一次性大改。差分学习率上,越靠前的层学习率越小,可以按 stage 设置 1e-5、5e-5、1e-4 这样的梯度。

# 微调阶段:解冻 conv5_x 及之后,浅层保持冻结 for name, param in model.named_parameters(): if name.startswith("layer4") or name.startswith("fc."): param.requires_grad = True else: param.requires_grad = False optimizer = build_optimizer(model, backbone_lr=1e-4, head_lr=1e-4)

layer4就是 ResNet50 的 conv5_x 阶段,解冻它通常能带来几个点的准确率提升。如果验证集准确率和训练集差距很大,说明解冻太多,退回只解冻 layer4 甚至只训分类头。

5. 避坑与排查:迁移学习分类系统最常见的五个翻车点

现象一:验证准确率始终在 25% 附近,四分类等于瞎猜。原因通常是归一化参数和预训练不一致,或者标签和文件夹没对上,ImageFolder 按文件夹名字母序分配类别索引,如果训练和验证的文件夹顺序不同,标签就错位了。解决方法是打印train_set.class_to_idx和val_set.class_to_idx对比,确保一致;归一化统一用 ImageNet 统计量。

现象二:训练损失正常下降,验证损失从第一个 epoch 就很高。这是训练和验证预处理不一致导致的,常见于验证集误用了训练增强(随机裁剪、翻转)。检查两个 transform,验证必须用确定性的 resize + centercrop,不能带随机操作。

现象三:显存溢出,报 CUDA out of memory。ResNet50 在 224 输入、batch 32 下大约占 6 到 7G 显存,如果同时开了多个 dataloader worker 或没清缓存会爆。解决方法是降 batch size 到 16、torch.cuda.empty_cache()、减少 num_workers,或者用梯度累积模拟大 batch。

现象四:某个类别召回率极低,混淆矩阵里全被预测成另一类。类别不均衡的典型表现。解决方法是加WeightedRandomSampler或给 CrossEntropyLoss 传类别权重,同时检查这个类别的样本是不是本身标注有误或图片损坏。

现象五:加载保存的模型报 key 不匹配。多半是保存时用了torch.save(model)整个模型,加载时类定义变了。统一改成保存和加载state_dict(),并且保证加载前模型结构和保存时完全一致,包括 num_classes。

提示:每次改动预处理或模型结构后,先用一个 batch 的数据跑一遍前向,确认输出维度是[batch, num_classes],再开完整训练,能省下大量无效等待。

6. 把准确率再往上推一档:TTA、类别权重与混淆矩阵定位

跑通基础流程后,如果想把验证准确率再提几个点,我常用的手段有三个。第一个是测试时增强(TTA),对同一张验证图做原图、水平翻转、多尺度裁剪几次前向,把 softmax 概率平均后再取 argmax。这个技巧不增加训练成本,通常能涨 1 到 2 个点,代价是推理时间翻几倍。实现上把验证 transform 复制几份,循环前向累加概率即可。

第二个是精细调整类别权重。先用基础模型跑一遍验证集,导出混淆矩阵,看哪些类别互相混淆最多。垃圾数据集里「其他垃圾」和「厨余垃圾」经常混,因为两者外观都偏杂乱。针对混淆对,可以单独给这两个类加权,或者对混淆样本做针对性增强(比如对厨余类多加颜色抖动,模拟不同光照下的食物残渣)。

第三个是用混淆矩阵反查数据质量。我遇到过验证准确率卡在 85% 上不去,导出混淆矩阵后发现某个类别的图里混进了别的类,是标注错误。清理掉几十张错标图后,准确率直接到 91%。所以别急着调模型,先看数据。

import torch import numpy as np from sklearn.metrics import confusion_matrix, classification_report @torch.no_grad() def tta_predict(model, loader, device): model.eval() all_preds, all_labels = [], [] for imgs, labels in loader: imgs = imgs.to(device) # 原图 + 水平翻转两次前向,概率平均 prob = torch.softmax(model(imgs), dim=1) prob_flip = torch.softmax(model(torch.flip(imgs, dims=[3])), dim=1) prob = (prob + prob_flip) / 2 all_preds.extend(prob.argmax(dim=1).cpu().numpy()) all_labels.extend(labels.numpy()) return np.array(all_labels), np.array(all_preds) labels, preds = tta_predict(model, val_loader, device) print(confusion_matrix(labels, preds)) print(classification_report(labels, preds, target_names=val_set.classes))

torch.flip(imgs, dims=[3])沿宽度维翻转,对应水平镜像。TTA 的前提是模型对翻转等变,如果训练时没做翻转增强,TTA 收益会打折,所以训练增强和 TTA 要配套。classification_report直接给出每个类的精确率、召回率和 F1,比只看总体准确率有用得多。

最后说个我自己的习惯:每次实验都把配置(学习率、冻结策略、增强参数)和结果记在一个表格里,哪怕只是改了一个参数也记。迁移学习的调参很像玄学,同一个参数在不同数据划分上结果可能差好几个点,没有记录根本复现不了。我早期不信这个,结果一周后想回到某个「当时随便跑跑就很好」的配置,怎么都调不回来,血泪经验。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询