简介:这是一份基于迁移学习残差网络的食物图像分类项目,使用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 为什么骨干网络用 ResNet50
ResNet 本身解决的是深层网络的退化问题。论文里的核心改动是引入残差块,也就是在卷积层旁边加一条恒等映射(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(weights=models.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 较新版本推荐的权重枚举,比字符串pretrained=True更明确,能避免版本废弃警告。
冻结策略怎么选,我整理成了下面这张对照表,按类别样本量对号入座:
| 策略 | 冻结范围 | 实际参与训练的参数 | 适用样本量 | 推荐初始lr |
|---|---|---|---|---|
| 特征提取 | 除 fc 外全部冻结 | 仅 fc 层 | 每类少于 100 张 | 1e-3 |
| 后段微调 | 冻结 layer1、layer2 | layer3、layer4、fc | 每类 100~300 张 | 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_grad=True 的参数,避免白算冻结层的梯度 trainable_params = [p for p in model.parameters() if p.requires_grad] optimizer = torch.optim.AdamW(trainable_params, lr=1e-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 与预训练权重缓存
依赖清单通常是这样的:
torch>=1.10 torchvision>=0.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 上也能训练,只是速度会慢 5~10 倍,训练轮次和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(root='data/train', transform=train_transform) val_dataset = datasets.ImageFolder(root='data/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2)这段代码里有三个点值得展开。第一,Normalize的 mean 和 std 必须使用 ImageNet 的统计量[0.485, 0.456, 0.406],因为 ResNet50 预训练权重是在这个归一化约定下训出来的,换掉数值相当于把输入分布改了,预训练特征就没法对齐。第二,验证集不做随机裁剪,只做Resize(256)加CenterCrop(224),这是为了每次验证结果可复现,不会被随机增强干扰。第三,训练集shuffle=True,验证集shuffle=False,前者保证每个 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 | 调度器 | 建议轮数 |
|---|---|---|---|---|
| 特征提取 | Adam | 1e-3 | StepLR | 15~20 |
| 后段微调 | AdamW | 1e-4 | CosineAnnealing | 20~30 |
| 全量微调 | AdamW | 3e-5~1e-4 | CosineAnnealing | 30~40 |
核心代码块长这样:
import torch from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = torch.optim.AdamW( model.parameters(), lr=1e-4, # 微调阶段典型初始值 weight_decay=1e-4 # 对食物小数据集有明显抑制作用 ) scheduler = CosineAnnealingLR( optimizer, T_max=30, # 与训练总轮数保持一致 eta_min=1e-6 # 末轮学习率的下限 )为什么选 AdamW 而不是传统 SGD?诚实说,SGD 加动量在 ImageNet 这类大数据集上泛化表现确实好,但课程设计场景下 AdamW 的调试成本低得多,它对学习率不那么敏感,而且内置的权重衰减对几百到几千张的食物数据能有效压住过拟合。weight_decay=1e-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 或 4,BN 统计量会剧烈抖动,训练 loss 看起来在下降,验证集准确率却像心电图一样上下乱窜。
显存紧张时的常见取舍顺序是:先降 batch_size 到 16,不行再到 8,保底 4。但低于 8 时建议配合梯度累积,用多个小 batch 累积梯度后再更新一次参数,等效于一个更大的 batch:
accumulation_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 数据增强参数:食物图像要慎用“重口味”增强
数据增强对食物分类是把双刃剑。用得太轻,模型容易过拟合;用得太重,会把食物原本的颜色和质感信息破坏掉。我列了这份食物场景特有的参数参考表:
| 增强操作 | 推荐参数 | 注意事项 |
|---|---|---|
| RandomResizedCrop | scale=(0.6, 1.0) | 下限 0.6 能保留食物主体 |
| RandomHorizontalFlip | p=0.5 | 食物对称性弱,翻转一般安全 |
| ColorJitter | brightness=0.2, contrast=0.2, saturation=0.2 | hue 参数保持 0,不要动色调 |
| RandomErasing | p=0.3 | 模拟食物被餐具遮挡的场景 |
| Rotation | ±15° | 超过 30° 会引入大量黑边 |
train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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 坑 1:loss 稳定下降但验证集准确率纹丝不动
现象:训练到第 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, indent=2)这个文件就是“后悔药”。推理阶段直接从 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(weights=None) model.load_state_dict(state)这里weights=None表示不加载内置预训练权重,而是从本地文件手动注入。这样做的额外好处是训练脚本完全离线可跑,答辩演示时不会因为网络问题尴尬。
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_samples=len(sample_weights), replacement=True ) train_loader = DataLoader( train_dataset, batch_size=32, sampler=sampler, num_workers=2 )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_names=val_dataset.classes, digits=3)) 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, dim=1) 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和一张完整的混淆矩阵图,然后随机挑三张验证集图片做单图推理,确认输出和图片内容一致才敢说“跑通了”。这个动作帮我避掉过太多次演示现场的尴尬,也让我养成了先看数据再谈模型的习惯。希望帮到你。
本文还有配套的精品资源,点击获取