尧图精选

迁移学习实战:用ResNet50微调预训练模型实现食物图像分类

🕒 发布时间:2026/10/1 16:23:26 📁 来源:尧图网络
简介这是一份基于迁移学习残差网络的食物图像分类项目使用Python编写适合作为大学课程设计或期末大作业已获导师指导并通过高分评价项目完整确保可以运行。压缩包内共有两千个文件包含一千九百八十三张食物图片作为数据集另有十二个Python源码文件以及配置文件、说明文档等整体大小约两百三十三兆目录结构清晰便于查找。项目覆盖了图像分类的完整流程从数据预处理、模型构建到训练评估均有对应脚本无需修改即可直接运行能帮助学习者快速掌握迁移学习实战方法。附带的文档详细介绍了项目背景、使用方式与实验结论方便答辩展示。目前已有156人学习下载既可作为深度学习课程设计的高分参考也能作为图像分类任务的基础研究项目。1. 迁移学习做食物分类先想明白这个97分项目好在哪解压这份“python项目源码-实现迁移学习ResNet网络的食物图像分类”课程设计以后我建议你先别急着双击train.py而是把技术路线在脑子里过一遍。这个项目不是让你白手起家搭网络而是用迁移学习把ResNet预训练权重搬到自己的食物数据集上微调。食物图像分类有几个特点类别多、每类样本少烤面包和法棍这种“同门兄弟”容易混所以在有限数据下随机初始化的ResNet很难稳定迁移学习正好对症。这份资源自带源码、图片样本和文档说明适合期末大作业、课程设计以及第一次接触ResNet的开发者环境搭好后可以完整复现训练。后面会从选型逻辑、代码结构、参数配置讲到显存、权重下载和类别映射这几个实际坑让你下载后少走弯路。2. 迁移学习与 ResNet 选型两个决定成败的配置决策2.1 为什么食物分类场景优先选迁移学习先看数据现实。课程设计里的食物数据集一般来自 Food-101 的子集或自采爬取训练集几百到两三千张不等分配到十几个类别后每个类可能只有几十上百张。这个规模对 ResNet 这种深网络意味着什么如果从随机初始化开始训练模型要先从零学习边缘、纹理、形状这些底层特征数据量根本不够它“吃饱”结果就是训练集准确率能涨到 90% 以上验证集却停在一个低点这是典型的过拟合。迁移学习解决的正是这个问题ImageNet 上的预训练权重相当于让模型在 1400 万张图里先看过了“世界长什么样”它已经把边缘、纹理、部件级的特征固化在卷积核里你要做的只是在它头顶换一个适合你类别数量的分类头然后用食物数据把最后几层微调一遍。这样做的收敛速度明显更快达到同样精度需要的数据量也少一个量级。在学术语境里这种做法属于归纳式迁移学习区别于直推式迁移和域适应课程设计绝大部分就是归纳式微调不需要往复杂方向上想。怎么快速判断这个项目到底该用“特征提取”还是“微调”我一般先做一次小实验把最后一层之外的参数全部冻结只训练分类头跑 5 个 epoch 看验证集 Top-1。如果准确率能到 70% 以上说明预训练特征对当前食物数据集足够好用如果卡在 50% 以下且 loss 下不去说明数据分布和 ImageNet 差异较大需要把后面几个 block 也解冻做微调。这个“先冻结试跑再解冻”的操作就是我们在下一个小节里要展开的策略选择。2.2 为什么骨干网络用 ResNet50ResNet 本身解决的是深层网络的退化问题。论文里的核心改动是引入残差块也就是在卷积层旁边加一条恒等映射skip connection让梯度在反向传播时多一条“高速通道”。这条通道带来的直接收益是网络可以安心加深到 50 层、101 层而不出现梯度消失训练过程也比 VGG 那种直筒结构稳定得多。对食物图像分类来说通常不需要像 ImageNet 分类那样动用 152 层ResNet50 是一个很均衡的落点。和 VGG16 相比ResNet50 参数量处在相近量级但计算量更小训练和推理都快不少和 EfficientNet 相比虽然 EfficientNet 同等精度下更省参数但对预训练权重的获取和数据增强的配合更挑剔在课程设计这种“求稳”的场景里ResNet50 的预训练权重最容易拿到、资料最多、踩坑也最少。还有一个隐性优势ResNet 的 layer3、layer4 输出通常被当作粗粒度特征和细粒度特征的来源如果你后续想加入注意力机制或特征金字塔对输出做拼接ResNet 的分层结构改起来很顺手这也是很多改进版食物分类论文选它做骨干的原因。食物这个场景里细粒度特征格外关键。食物的类别界限往往不在“有没有轮子”这种大结构差异上而在表面纹理、局部配料这些细小区别上比如披萨和馅饼远看都是圆形带馅的面食只有注意到底部饼皮和边缘褶皱才能分开。所以用 ResNet50 拿多尺度输出比直接用单层向量更有潜力这一条可以作为你往文档说明里写的“改进方向”显得不是照抄代码。2.3 特征提取与微调冻结策略与代码落点现在落实到代码。常见做法是使用 torchvision 的预训练 ResNet50替换全连接层。import torchvision.models as models # 加载在ImageNet上预训练好的ResNet50 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) in_features model.fc.in_features # 把原1000类分类头替换成自己的类别数 model.fc torch.nn.Linear(in_features, num_classes)这里有两个细节容易忽略第一一定要先取model.fc.in_features因为 ResNet50 最后的全连接层输入是 2048 维不同系列 backbone 这个数值不一样硬编码 2048 虽然 ResNet50 没错但换模型就要改不如用代码取第二IMAGENET1K_V2是 torchvision 较新版本推荐的权重枚举比字符串pretrainedTrue更明确能避免版本废弃警告。冻结策略怎么选我整理成了下面这张对照表按类别样本量对号入座策略冻结范围实际参与训练的参数适用样本量推荐初始lr特征提取除 fc 外全部冻结仅 fc 层每类少于 100 张1e-3后段微调冻结 layer1、layer2layer3、layer4、fc每类 100300 张1e-4全量微调全部解冻所有参数每类 300 张以上1e-4 或更低后段微调是我在这个项目里最常用的中间档既能保留 ImageNet 的低中层级特征又能让高层语义适配食物数据。# 先全部冻结再按需解冻后段 for name, param in model.named_parameters(): if layer3 not in name and layer4 not in name and fc not in name: param.requires_grad False # 优化器只接收 requires_gradTrue 的参数避免白算冻结层的梯度 trainable_params [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.AdamW(trainable_params, lr1e-4)这段代码是整套迁移学习流程的“分水岭”。requires_grad控制的是反向传播时要不要计算梯度而不是前向传播要不要跑——所以冻结层依然会参与计算只是权重不再更新。这也解释了一个常见疑问为什么冻结了大部分层显存占用并没有显著下降因为中间层的特征图仍然要存在内存里供反向传播使用。3. 把项目跑起来目录结构、环境安装与数据准备3.1 解压后先看项目结构确认每一份文件干什么用课程设计项目下载后最容易翻车的地方不是代码写错而是不知道哪份文件是入口、哪份是文档、数据放在哪个目录。解压后我建议先按下面的清单对一遍把文件名和用途对应上。文件/目录作用说明train.py训练入口包含模型加载、优化器、训练循环model.py定义 ResNet 迁移模型与分类头替换逻辑data.py数据增强、Dataset 构建与 DataLoader 配置predict.py对单张图片做推理的小工具requirements.txt依赖库清单课程设计文档项目说明、实验设计、结果分析data/train训练集按类别子目录存放图片data/val验证集子目录结构必须与训练集一致这个项目自带的数据目录里是常见的 jpg 图片样本像 17052.jpg、img_151.jpg、17443.jpg 这类文件名直接铺在类别子目录下没有额外的标注文件。如果你自己换数据集也要保持这种“文件夹名即标签”的布局因为后面加载用的ImageFolder只认这种结构。一个小建议把data/train和data/val两个目录打开各抽一个类别对比看看。训练集和验证集的类别子目录命名必须一字不差大小写也要一致。很多同学在这个环节偷懒结果训练时类别数对不上运行时报错让人摸不着头脑。3.2 环境安装Python、PyTorch 与预训练权重缓存依赖清单通常是这样的torch1.10 torchvision0.11 numpy matplotlib pillow scikit-learn tensorboard安装命令在命令行里执行pip install -r requirements.txtPython 版本建议 3.8 以上PyTorch 1.10 到 2.x 的版本都能跑通这个项目。注意torchvision要和torch版本配套不能随意装最新版否则可能出现算子不匹配。装完后用一段脚本来确认基础环境import torch print(torch.__version__) print(torch.cuda.is_available())cuda.is_available()返回True表示显卡可用。如果这里显示False可能是 PyTorch 装成了 CPU 版也可能是驱动版本过旧。这个项目在 CPU 上也能训练只是速度会慢 510 倍训练轮次和batch_size都要相应下调这个后面参数章节会细说。权重缓存这里先留个心眼第一次运行models.resnet50(weights...)时torchvision 会自动联网下载预训练权重到~/.cache/torch/hub/checkpoints/。这一步在你手动跑通之前最好先验证一次下载成功后再开始正式训练。我的习惯是先去这个缓存目录确认文件存在否则训练启动到一半卡在下载进度条上很影响心情。3.3 数据加载从 ImageFolder 到类别映射的完整流程数据加载这一块项目里最可能用到的方案是torchvision.datasets.ImageFolder它按目录结构批量读取图片并自动生成类别索引。常规写法如下from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集不做随机增强只用固定缩放 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]) ]) train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootdata/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2)这段代码里有三个点值得展开。第一Normalize的 mean 和 std 必须使用 ImageNet 的统计量[0.485, 0.456, 0.406]因为 ResNet50 预训练权重是在这个归一化约定下训出来的换掉数值相当于把输入分布改了预训练特征就没法对齐。第二验证集不做随机裁剪只做Resize(256)加CenterCrop(224)这是为了每次验证结果可复现不会被随机增强干扰。第三训练集shuffleTrue验证集shuffleFalse前者保证每个 batch 的类别分布尽量随机后者保证验证指标稳定可对比。这里还要提前说一个重要机制ImageFolder的类别顺序是按目录名字母排序的不是按你创建文件夹的顺序。也就是说如果你把“汉堡”文件夹建在前面但它的字母序在“披萨”后面索引就会被排在后面。训练结束后保存预测结果时一定要用train_dataset.classes来对应类别名不要自己手写一份列表。我每次跑完训练还会顺手打印一下每个类别的样本数from collections import Counter counts Counter([label for _, label in train_dataset.samples]) print(counts)这一行输出对后面判断样本不均衡很有用。如果有的类别只有 30 张、有的类别有 300 张验证曲线会出现明显的波动这就是第 5 章要讲的坑之一。4. 参数调优实战lr、batch_size 与数据增强在食物数据上的配置4.1 学习率与优化器微调阶段的两个第一顺位参数食物图像分类用迁移学习最优先调的不是网络结构而是学习率和优化器的组合。我把这个项目在不同策略下的推荐配置整理成了一张速查表。训练策略优化器初始lr调度器建议轮数特征提取Adam1e-3StepLR1520后段微调AdamW1e-4CosineAnnealing2030全量微调AdamW3e-51e-4CosineAnnealing3040核心代码块长这样import torch from torch.optim.lr_scheduler import CosineAnnealingLR optimizer torch.optim.AdamW( model.parameters(), lr1e-4, # 微调阶段典型初始值 weight_decay1e-4 # 对食物小数据集有明显抑制作用 ) scheduler CosineAnnealingLR( optimizer, T_max30, # 与训练总轮数保持一致 eta_min1e-6 # 末轮学习率的下限 )为什么选 AdamW 而不是传统 SGD诚实说SGD 加动量在 ImageNet 这类大数据集上泛化表现确实好但课程设计场景下 AdamW 的调试成本低得多它对学习率不那么敏感而且内置的权重衰减对几百到几千张的食物数据能有效压住过拟合。weight_decay1e-4是一个经验值太大会让模型欠拟合太小又起不到正则作用。学习率的设置逻辑要区分场景特征提取阶段fc 层是随机初始化的可以给稍大的 lr 让它快速收敛后段微调阶段解冻的 layer3、layer4 承接了预训练特征lr 必须降下来否则会把已经学好的高层特征“冲坏”。全量微调更敏感一般从 1e-4 起步如果 loss 前几个 epoch 出现反弹直接降到 3e-5 重新跑。4.2 batch_size、输入分辨率与 BN 层的联动关系batch_size的选择不能只看显存还和 ResNet 里的 BatchNorm 层有直接关系。ResNet 每一层卷积后面都跟着 BN它统计的是一个 batch 内的均值和方差来归一化特征。如果batch_size太小比如只有 2 或 4BN 统计量会剧烈抖动训练 loss 看起来在下降验证集准确率却像心电图一样上下乱窜。显存紧张时的常见取舍顺序是先降 batch_size 到 16不行再到 8保底 4。但低于 8 时建议配合梯度累积用多个小 batch 累积梯度后再更新一次参数等效于一个更大的 batchaccumulation_steps 4 # 等效batch_size 单卡batch_size * 4 for i, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()参数accumulation_steps的含义是把多少个 batch 的梯度累积后再做一次参数更新。这里有个容易算错的地方loss 一定要除以accumulation_steps否则等效 batch 变大后梯度绝对值也会变大相当于隐式放大了学习率训练行为会和预期差很多。输入分辨率方面ResNet 默认是 224×224。如果显存还有富余可以尝试把输入放大到 256 或 288食物细节会有一定提升但训练时间也线性增加。我的建议是课程设计用默认 224 就够了把调分辨率的时间花在数据增强上更划算。4.3 数据增强参数食物图像要慎用“重口味”增强数据增强对食物分类是把双刃剑。用得太轻模型容易过拟合用得太重会把食物原本的颜色和质感信息破坏掉。我列了这份食物场景特有的参数参考表增强操作推荐参数注意事项RandomResizedCropscale(0.6, 1.0)下限 0.6 能保留食物主体RandomHorizontalFlipp0.5食物对称性弱翻转一般安全ColorJitterbrightness0.2, contrast0.2, saturation0.2hue 参数保持 0不要动色调RandomErasingp0.3模拟食物被餐具遮挡的场景Rotation±15°超过 30° 会引入大量黑边train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), 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]) ])RandomResizedCrop里scale(0.6, 1.0)的意思是裁剪区域占原图面积的 60% 到 100%下限设太高会让模型只看食物局部而失去整体判断设太低又会裁掉关键部位。食物和普通物体不同一道菜的整体摆盘和餐具通常也是识别线索所以我把下限压到 0.6 而不是 ImageNet 常用的 0.08。ColorJitter这里特别强调一点hue参数保持为 0不要调。食物颜色对分类非常关键烤鸡的焦褐色、蔬菜的翠绿色都是强判别特征色调偏移会直接把这些信息毁掉。亮度、对比度、饱和度小幅扰动就够了幅度控制在 0.2 以内。5. 避坑与排查食物图像分类训练中的 5 个实际踩坑记录5.1 坑 1loss 稳定下降但验证集准确率纹丝不动现象训练到第 5 个 epoch训练 loss 从 2.3 降到 0.9但验证集 Top-1 准确率一直卡在 0.2 左右像是模型在“瞎猜”。原因最常见的是训练集和验证集的类别映射不一致。ImageFolder按文件夹名字母序生成索引如果你在训练时用了一套数据集验证时换了另一套目录结构两边的class_to_idx顺序对不上验证标签就是错的。另一种可能是验证集 transform 里用了随机增强导致每次验证结果波动很大且均值偏低。解决训练开始前先打印train_dataset.class_to_idx和val_dataset.class_to_idx确认两个字典完全一致。我习惯把映射关系存成 json 文件后面推理阶段也用它保证全流程用的是同一套索引。import json with open(class_names.json, w) as f: json.dump(train_dataset.class_to_idx, f, indent2)这个文件就是“后悔药”。推理阶段直接从 json 读索引既不会搞混顺序也方便别人理解你的数据约定。5.2 坑 2预训练权重下载失败训练卡在启动阶段现象运行训练脚本后控制台停在Downloading: ...的进度条数字长时间不动最后抛超时或FileNotFoundError。原因torchvision 的预训练权重托管在境外服务器国内网络环境下下载不稳定是常态不是代码逻辑的问题。第二次运行时如果缓存没写完整还会反复触发同样的问题。解决手动把权重文件拿下来放到~/.cache/torch/hub/checkpoints/目录训练脚本检测到缓存文件存在就不会再联网。具体做法是浏览器或其他下载工具先下载resnet50-0676ba61.pth然后放进缓存目录。torchvision 加载时按文件名加哈希匹配文件名必须保持原样。如果团队协作可以把权重放到项目weights/目录下用torch.load加state_dict手动加载不走自动下载逻辑。state torch.load(weights/resnet50-0676ba61.pth) model models.resnet50(weightsNone) model.load_state_dict(state)这里weightsNone表示不加载内置预训练权重而是从本地文件手动注入。这样做的额外好处是训练脚本完全离线可跑答辩演示时不会因为网络问题尴尬。5.3 坑 3显存不足一启动训练就 OOM 崩溃现象RuntimeError: CUDA out of memory训练刚进入第一个 batch 就崩了。原因最常见的是batch_size设得太大或者输入分辨率被人为调高。注意一个误区冻结层并不会减少显存占用因为前向传播的特征图仍然要计算和存储只是不存梯度而已。解决先把batch_size降到 16 试跑不行再降 8。如果必须用大 batch就用 4.2 小节里的梯度累积方案。还可以检查一下 DataLoader 的num_workers设置过大会复制多份数据到内存也会推高显存压力。另外 PyTorch 提供了显存清理手段在验证阶段临时释放占用with torch.no_grad(): model.eval() for images, labels in val_loader: ...torch.no_grad()关闭了自动求导验证时不会为中间特征保存梯度显存占用能降一半以上。这行代码写不写对 OOM 的临界状态影响很大。我见过好几个项目训练正常、一进验证就崩原因就是验证循环忘了包这层。5.4 坑 4验证集准确率大幅震荡训练曲线像锯齿现象验证集准确率相邻两个 epoch 可以相差 8 到 10 个百分点训练 loss 曲线每轮都上下跳完全看不出收敛趋势。原因最常见是初始学习率偏大。迁移学习模型在微调阶段对 lr 极其敏感预训练特征已经在一个很平滑的损失谷里lr 太大就会在谷壁来回弹跳验证集表现自然不稳定。另一个可能性是batch_size太小BN 统计量在训练和验证之间存在显著差异。解决先把 lr 从 1e-4 降到 1e-5 跑 10 个 epoch如果曲线稳定再逐步回升。同时检查调度器CosineAnnealingLR在训练中段就会把 lr 降下来比 StepLR 的阶梯式下降更平滑更适配微调场景。还有一个容易被忽略的点验证频率建议固定为每个 epoch 结束跑一次完整验证不要用训练过程中的某几个 batch 去算准确率那样只会得到高方差噪声。5.5 坑 5预测结果永远指向同一个类别现象模型训练完随便拿哪张测试图片推理输出概率最高的永远是同一个类别比如所有图片都是“面包”。原因通常是训练集类别样本严重不均衡某类图片数量占绝对优势模型学到“无脑预测多数类”的捷径。少数类在交叉熵里贡献的梯度太小根本翻转不了多数类的先验概率。另一个次要原因是全连接层的 bias 初始化偏向样本多的一侧。解决在CrossEntropyLoss里按类别样本数反比设置权重或者给 DataLoader 换用WeightedRandomSampler让每个 batch 里少样本类也有出场机会。from torch.utils.data.sampler import WeightedRandomSampler labels [label for _, label in train_dataset.samples] class_counts torch.bincount(torch.tensor(labels)) weights 1.0 / class_counts.float() sample_weights torch.tensor([weights[label] for label in labels]) sampler WeightedRandomSampler( sample_weights, num_sampleslen(sample_weights), replacementTrue ) train_loader DataLoader( train_dataset, batch_size32, samplersampler, num_workers2 )sample_weights的含义是每个样本被抽中的概率权重等于其类别样本数的倒数。少样本类别的样本会被重复抽中多数类样本相对少出现从而让训练过程的类别分布更均衡。注意设置了sampler之后DataLoader 的shuffle参数必须保持False两者重复会导致运行时报错。6. 进阶把课程设计变成可演示系统的最后两步6.1 用混淆矩阵和 classification_report 验证模型短板课程设计文档里如果只放一条准确率曲线答辩老师大概率会觉得“这项目没做透”。一个低成本高说服力的做法是跑一次完整验证输出混淆矩阵和分类报告看看模型到底在哪些食物类别上犯混。from sklearn.metrics import classification_report, confusion_matrix val_labels, pred_labels [], [] for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) val_labels.extend(labels.numpy()) pred_labels.extend(preds.cpu().numpy()) print(classification_report(val_labels, pred_labels, target_namesval_dataset.classes, digits3)) cm confusion_matrix(val_labels, pred_labels) print(cm)classification_report会同时给出每个类别的 precision、recall 和 f1-score比全局准确率更能暴露问题。比如“烤面包”和“法棍”这两个类如果 f1 只有 0.6说明它们确实容易混——这就是你文档里可以深入写的“细粒度分类难点”而不是笼统说“模型效果不错”。6.2 用单图推理脚本提升演示说服力答辩现场最怕的是临时跑测试代码控制台报错找不到文件。我建议把推理封装成一个独立的脚本输入任意本地图片路径输出预测类别和置信度演示时一个命令出结果。def predict_single_image(model, image_path, transform, device): from PIL import Image model.eval() img Image.open(image_path).convert(RGB) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(img_tensor) probs torch.softmax(logits, dim1) conf, idx torch.max(probs, 1) return val_dataset.classes[idx.item()], conf.item() # 使用示例 class_name, conf predict_single_image(model, data/val/17052.jpg, val_transform, device) print(f预测类别{class_name}置信度{conf:.4f})convert(RGB)这一行很关键它保证灰度图或带透明通道的 PNG 都被统一转成三通道 RGB否则维度对不上会直接抛错。torch.softmax把 logits 转成了概率分布这里取max而不是直接argmax是为了顺带拿到置信度方便在演示时展示“模型不仅判对了而且很有把握”。从那次以后我每次把课程设计项目移交出去都会在根目录留下class_names.json和一张完整的混淆矩阵图然后随机挑三张验证集图片做单图推理确认输出和图片内容一致才敢说“跑通了”。这个动作帮我避掉过太多次演示现场的尴尬也让我养成了先看数据再谈模型的习惯。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →