ARTICLE DETAIL

资讯详情

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

CIFAR-10图像分类实战:基于PyTorch的CNN模型训练与调优

CIFAR-10图像分类实战:基于PyTorch的CNN模型训练与调优 简介面向深度学习初学者的课程实验资源利用卷积神经网络实现十类彩色图像数据集CIFAR-10的分类器。压缩包包含十七个文件其中两个Python脚本负责模型构建与训练数据的保存一个Markdown文档说明环境配置与运行步骤另有十四张PNG图展示网络结构、损失变化和分类效果整体大小约二百四十千字节便于快速下载。核心代码涵盖数据预处理、卷积层/池化层/批量归一化设置、训练与验证流程配套CSV生成脚本可记录每轮损失和准确率图片则直观呈现训练过程中的指标曲线和混淆矩阵帮助学习者评估模型表现。适用于课程作业和实验报告撰写已有一百五十三人学习参考。通过实践可以掌握图像分类任务的完整建模思路、正则化与数据增强技巧以及基于测试集反馈调整超参数的方法为后续更复杂的视觉任务提供基础。1. CIFAR-10 分类器到底难在哪一张 32×32 小图的前世今生用卷积神经网络实现 CIFAR-10 数据集的分类器是很多人的第一道“彩色门槛”从 MNIST 黑白手写体切换到 6 万张 32×32 的彩色图片输入从单通道变成三通道分辨率还低物体小纹理和背景常常纠缠在一起。你眼里是“一只趴在树上的猫”模型眼里往往是“一团分布在 1024 个像素里的噪声”。这个任务能让你练熟数据集加载、卷积结构、正则化、训练曲线管理这一整条链路也适合刚端起 PyTorch、写过几轮线性回归的从业者确认自己不是只会调包。CIFAR-10 的十个类别里汽车和卡车、猫和狗这类混叠非常明显所以网上很多教程跑 MNIST 能刷到 99%换到这里立刻打回原形。这篇笔记不打算只贴一个网络结构而是把从数据到训练、排错、导出模型的完整路径走一遍让你不仅跑得通还能在换数据集时少把旧坑再踩一遍。2. 把 CIFAR-10 数据喂给卷积神经网络加载、归一化和增强的落地写法CIFAR-10 数据集的加载在 PyTorch 生态里已经有现成封装真正需要你决策的是三件事数据进来时是什么形态、像素值怎么归一化、训练和测试是否共用同一条 transform 流水线。很多人直接复制一份训练用的 transform 去跑测试结果验证集准确率忽高忽低还以为是模型随机性太大其实问题出在数据入口。2.1 从 torchvision 加载 CIFAR-10标签含义和 DataLoader 参数from torchvision import datasets, transforms from torch.utils.data import DataLoader transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize( mean(0.4914, 0.4822, 0.4465), std(0.2470, 0.2435, 0.2616), ), ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize( mean(0.4914, 0.4822, 0.4465), std(0.2470, 0.2435, 0.2616), ), ]) trainset datasets.CIFAR10( root./data, trainTrue, downloadTrue, transformtransform_train ) testset datasets.CIFAR10( root./data, trainFalse, downloadTrue, transformtransform_test ) trainloader DataLoader( trainset, batch_size128, shuffleTrue, num_workers4, pin_memoryTrue ) testloader DataLoader( testset, batch_size256, shuffleFalse, num_workers4, pin_memoryTrue )datasets.CIFAR10 下载后会把数据组织成trainset.data和trainset.targets前者形状是 (50000, 32, 32, 3)像素值范围 0 到 255通道排在最后后者是 0 到 9 的整数标签。你要记住这个形状ToTensor 会把通道维从末尾挪到前面变成 (3, 32, 32)同时把像素除以 255 压到 0 到 1 区间。如果你后续自己写数据加载最容易犯的错就是拿到 (32, 32, 3) 直接喂给卷积层模型能训练但准确率永远上不去。shuffleTrue是训练集必须维持的不洗牌的话前几百个 batch 可能全是同一类别优化器会被带偏。测试集反而要shuffleFalse这样你每轮评估的结果可以在固定顺序上做对比。batch_size128是一张 4G 显存显卡上比较划算的起点显存吃紧就降到 64学习率也要跟着动。num_workers4通常够用在机械硬盘和老 CPU 上开 8 个 worker 反而可能让每个 epoch 更慢。pin_memoryTrue只在用 GPU 训练时有意义纯 CPU 训练开了也白开。为了确认数据没坏加载后我习惯先看一眼标签分布from collections import Counter print(trainset.data.shape) # (50000, 32, 32, 3) print(Counter(trainset.targets))CIFAR-10 天然是均衡的每个类正好 5000 张训练图所以分类器不需要做重采样。但你要把十个类名记清楚顺序是 airplane、automobile、bird、cat、deer、dog、frog、horse、ship、truck。后面画混淆矩阵时类型顺序错位会让结果完全没法读。2.2 归一化与数据增强训练集和测试集为什么不能共用一条流水线训练集 transform 里比测试集多了两个操作RandomCrop(32, padding4) 和 RandomHorizontalFlip()。先说归一化的数学含义再说增强为什么只给训练集加。Normalize 执行的是逐通道的(x - mean) / std这套 mean 和 std 是 CIFAR-10 训练集的全局统计量。(0.4914, 0.4822, 0.4465) 是 RGB 三通道各自的均值(0.2470, 0.2435, 0.2616) 是标准差。这些数不是我拍脑袋写的是 torchvision 社区里跑 CIFAR-10 时的通行配置你也可以用trainset.data.mean(axis(0,1,2))/255自己算一遍结果基本一致。如果你明明加载的是 0 到 1 区间却用 125 和 255 去做归一化输入分布会整体偏移模型得花更多 epoch 才能把 BN 层的统计量拉回来。RandomCrop 在 32×32 图像边缘补 4 像素再随机裁回 32×32等于把物体的位置抖动显式教给网络防止模型只对“正中间一辆汽车”响应。RandomHorizontalFlip 对猫、狗、鸟这种对称生物很友好对数字这种不对称图形反而不合适CIFAR-10 里没有必须保持朝向的类别所以放心用。测试集不能加 RandomCrop也不能加 HorizontalFlip因为评估必须在一个确定性分布上做。你每次跑测试都随机裁掉不同区域得到的准确率自然忽上忽下。这是我见过最隐蔽的数据坑训练集和测试集用了同一段 transform代码没报错指标却不讲道理。测试集唯一保留的是 ToTensor 和 Normalize保证数值范围跟训练时一致。2.3 关于 batch、num_workers 和 shuffle 的三个边界问题第一个边界是 batch 大小对归一化和 BN 的影响。BatchNorm 用的是当前 batch 内的均值和方差batch 越小统计量越抖。如果你显存只够跑 batch32学习率要从 0.1 降到 0.025 左右否则 loss 曲线会像锯齿一样来回翻。如果 batch 开得很大比如 512学习率适当上抬但 128 到 256 之间已经能覆盖大多数场景没必要硬冲。第二个边界是num_workers。在 Windows 上开太多 worker 容易触发重复 fork 的报错在 Linux 上则要留意内存占用。我一般遵循“先 2 后 4再试 8取最高且不崩的那个”。如果训练时数据加载变成了瓶颈你会在 GPU 利用率上看到一段段空白这时候调大num_workers立竿见影如果 GPU 利用率已经 90% 以上调大 worker 没有任何意义。第三个边界是 shuffleFalse 的使用场景。除了测试集有时候你想用整个训练集做一次可视化把某个类别的图片全部拿出来画网格这时 shuffle 会把你想要的有序数据打乱反而麻烦。更常见的是你想检查模型的错误样本这时在 DataLoader 里保留原始索引会很有用可以用dataset.targets配合预测结果做对齐别依赖 batch 内顺序。3. CNN 结构选型与实现从 VGG 式堆叠到 ResNet 风格模型结构是整个分类器里最容易被“抄作业”带偏的部分。很多人一上来就搬 ImageNet 用的 ResNet18结果第一层 7×7 卷积加 stride 2 直接把 32×32 的输入砍成 16×16后面的池化层再一压图像早早就变成 4×4细粒度特征全部丢失。CIFAR-10 的输入太小必须选择为小图设计的结构。3.1 为什么 32×32 小图不适合直接搬大模型选型先选 stemCIFAR-10 的完整图像只有 32×32一个 7×7 卷积的有效感受野几乎覆盖整张图的一半这等于让网络第一层就看到全图丧失局部纹理建模能力。常见做法是像 ResNet-20 那样第一个卷积用 3×3、stride 1、padding 1尽量保持 32×32 的空间分辨率让前几层还有机会学习边角和细节。我自己会先想清楚一个问题到底要证明“我能调出一个 CNN”还是“我要刷高 CIFAR-10 准确率”。如果只是前者VGG 风格的三段 3×3 堆叠加全局平均池化就够训练集准确率能上 95%测试准确率在 85% 上下。如果要后者残差结构值得上靠 shortcut 把梯度从最后一层直通到前面能让网络在几十层深度上依然稳定训练测试准确率可以摸到 90% 以上。VGG 风格和 ResNet 风格的差异不在单层能力而在训练过程中的梯度传播。VGG 一旦堆到十几层中间某层梯度消失前几层参数就学不动ResNet 的 shortcut 让每一层都可以选择“什么都不做”或者“只学残差”网络退化问题被明显缓解。CIFAR-10 上的任务是图像分类不是超分辨率不需要太宽的网络所以我给每个阶段的通道数按 16、32、64 翻倍而不是一开始就上 64 起步。3.2 ResNet 风格模型的最小实现BasicBlock 与三个阶段import torch import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): expansion 1 def __init__(self, in_ch, out_ch, stride1): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_ch) self.conv2 nn.Conv2d(out_ch, out_ch, 3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_ch) if stride 1 and in_ch out_ch: self.shortcut nn.Identity() else: self.shortcut nn.Sequential( nn.Conv2d(in_ch, out_ch, 1, stridestride, biasFalse), nn.BatchNorm2d(out_ch), ) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out out self.shortcut(x) return F.relu(out) class CifarResNet(nn.Module): def __init__(self, num_classes10): super().__init__() self.in_ch 16 self.conv1 nn.Conv2d(3, 16, 3, stride1, padding1, biasFalse) self.bn1 nn.BatchNorm2d(16) ch [16, 32, 64] block_nums [3, 3, 3] self.stage1 self._make_stage(ch[0], block_nums[0], stride1) self.stage2 self._make_stage(ch[1], block_nums[1], stride2) self.stage3 self._make_stage(ch[2], block_nums[2], stride2) self.gap nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(64, num_classes) def _make_stage(self, out_ch, n_blocks, stride): layers [] for i in range(n_blocks): s stride if i 0 else 1 layers.append(BasicBlock(self.in_ch, out_ch, s)) self.in_ch out_ch return nn.Sequential(*layers) def forward(self, x): x F.relu(self.bn1(self.conv1(x))) x self.stage1(x) x self.stage2(x) x self.stage3(x) x self.gap(x) x torch.flatten(x, 1) return self.fc(x)BasicBlock 的 shortcut 设计是整个结构的关键当 stride 等于 1 且输入输出通道相同时shortcut 直接用 Identity不引入额外参数当 stride 等于 2 或通道数变化时用 1×1 卷积把输入投影到目标通道数同时完成空间下采样。这个写法还能避免一种常见错误有人把 shortcut 写成nn.Conv2d但 stride 忘记对齐相加时空间尺寸不匹配直接报 RuntimeError。两段卷积都是 3×3、padding 1第一段卷积使用 stride 控制下采样。第一个 stage 不动分辨率32×32 保持到 stage1 结束stage2 的第一个 block 用 stride2空间降到 16×16stage3 再降一次到 8×8最后由全局平均池化压到 1×1。整个模型参数量只有约 27 万比动辄上千万参数的 ImageNet 模型小一个数量级在单张消费级显卡上十几分钟就能训完。注意我在__init__里先写了self.in_ch 16后面_make_stage才能正确记录上一阶段输出通道。这种状态变量在递推建层时很简单但也很容易被忽略结果就是 stage2 的输入通道对不上。写完代码建议立刻做一次形状自检model CifarResNet() x torch.randn(2, 3, 32, 32) print(model(x).shape) # torch.Size([2, 10])3.3 结构参数表从 32×32 到 8×8 的输出尺寸变化网络部件输出空间尺寸输出通道说明Conv1 BN32×32163×3 卷积stride1不降分辨率Stage132×32163 个 BasicBlock全部 stride1Stage216×1632第一个 block stride2之后两个 stride1Stage38×864第一个 block stride2之后两个 stride1GlobalAvgPool1×164AdaptiveAvgPool2d(1)FC1×110输出 10 个类别的 logits这张表值得你盯一下的数值是通道数 16、32、64 的增长速度。通道翻倍的同时空间分辨率减半理论计算量大致保持不变这是卷积网络里很经典的“宽高减半、深度加倍”设计。32×32 的小图走到 8×8 后已经不能再压了再往下连 4×4 会丢掉太多空间信息。如果你想加宽模型把ch改成 [32, 64, 128] 即可但要预计显存占用和训练时间翻倍。有个可选优化是移除最后的 GlobalAvgPool 前再接一层 stride2 的卷积让特征图直接进 FC但这种做法在 CIFAR-10 上没有明显收益反而增加过拟合风险。我的判断是先跑通这个最小残差结构确认它能在验证集上稳定逼近 90%再考虑加宽加深。4. 训练一个 CIFAR-10 分类器优化器、学习率和 checkpoint 的完整配置网络结构只占成功的一半另一半在训练配置。很多人结构抄得一模一样准确率却低五六个点差别基本都在学习率、weight decay 和模型切换 train/eval 的时机上。这一章给的配置是按 200 epoch、batch 128、单卡环境校准过的你可以直接抄但要知道每个参数为什么这样设。4.1 训练循环的关键词是 modetrain/eval 要分开def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 correct 0 total 0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) correct (out.argmax(1) y).sum().item() total y.size(0) return total_loss / total, correct / total def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for x, y in loader: x, y x.to(device), y.to(device) out model(x) correct (out.argmax(1) y).sum().item() total y.size(0) return correct / totalmodel.train()和model.eval()切换的是 BatchNorm 的工作方式。训练时 BN 使用当前 batch 的均值和方差评估时应该使用训练阶段累计的运行统计量。如果你忘了切回 eval测试时 BN 又拿当前 batch 重新算统计量小 batch 下结果会抖得非常厉害。torch.no_grad()必须包住评估循环否则模型会为每一张测试图构建计算图显存白白翻倍。训练循环里的loss.item() * x.size(0)是我比较坚持的写法。最后一个 batch 的样本数可能不足 128如果直接对所有 batch 的 loss 求平均小 batch 就会被高估。按样本数加权后得到的平均 loss 才跟准确率是同一套口径。out.argmax(1)取每个样本预测概率最大的类别和 y 比得到正确个数再除以总样本数。4.2 优化器与学习率SGD、momentum、weight_decay 和余弦退火怎么配import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR criterion nn.CrossEntropyLoss() optimizer optim.SGD( model.parameters(), lr0.1, momentum0.9, weight_decay5e-4, ) scheduler CosineAnnealingLR(optimizer, T_max200)在 CIFAR-10 上SGD 加 momentum 的效果通常比 Adam 更加牢靠。Adam 收敛前期很快但后期容易出现精度上不去的现象SGD 配合余弦退火到训练后半段时学习率降得很低能让模型在局部最优点附近把细节磨得更好。这不是玄学是很多图像分类实验里的普遍观察。lr0.1是 batch 128 下的常用起点。你如果换到 batch 64我一般会同步把 lr 折半到 0.05换到 batch 256可以试 0.2 但未必更好。学习率大了训练一开始 loss 不降反升或者卡在 2.3 附近不动学习率小了前十个 epoch 的 loss 下降像爬坡。momentum0.9是默认值一般不动。weight_decay5e-4在 CIFAR-10 的小模型上非常常见它等价于对每个参数加一个小的 L2 惩罚能明显压住测试集和训练集准确率之间的差距。调大 weight decay 会让训练集准确率掉到 90% 以下调小则过拟合回来。余弦退火T_max200表示 200 个 epoch 内把学习率从 0.1 一路平滑降到接近 0。如果你只打算训 100 epochT_max 一定要改成 100否则学习率还没降到位训练就结束准确率会差一截。CosineAnnealingLR 的调用位置在每轮 epoch 结束后不能在每个 batch 后调用否则学习率会被断成碎片。4.3 checkpoint不只存 model把 optimizer 和 val_acc 一起留下best_acc 0.0 epochs 200 for epoch in range(epochs): train_loss, train_acc train_one_epoch( model, trainloader, optimizer, criterion, device ) val_acc evaluate(model, testloader, device) scheduler.step() print(fepoch {epoch:3d} | loss {train_loss:.4f} | ftrain_acc {train_acc:.4f} | val_acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save( { epoch: epoch, model: model.state_dict(), optimizer: optimizer.state_dict(), val_acc: val_acc, }, cifar10_best.pth, ) torch.save(model.state_dict(), cifar10_last.pth)保存成字典而不是只存model.state_dict()是为了让 checkpoint 具有“后悔药”功能。中断训练重启时你可以把 optimizer 的状态也恢复学习率调度器的位置也能接着算只存权重就得从零开始调学习率。cifar10_best.pth记录验证集准确率最高的那个时刻cifar10_last.pth保留最后一个 epoch 的结果两个文件配合能让你判断“早停点”和“终点”到底哪个更好。我在实验里发现有时候最后一个 epoch 的准确率反而比 mid 阶段最高点低 0.3 到 0.5 个百分点这是正常的因为余弦退火后期学习率太小模型会在非常小的邻域内徘徊。最终对外演示时直接加载 best 即可不用纠结 last 的曲线。5. CIFAR-10 分类器常见问题排查五条踩坑记录跑通这条链路不难难的是跑到一半发现指标不对还不知道该查代码还是查参数。下面是五条我反复见过的坑全部按“现象”、“原因”、“解决”的顺序讲每条都值得你在项目里做个记号。5.1 Loss 卡在 2.30 附近不动看起来像在猜现象训练刚开始loss 一直保持在 2.3026 左右十几个 epoch 过去仍然纹丝不动训练准确率始终在 10% 附近。原因交叉熵对 10 分类的随机猜的期望值就是 log(10)约等于 2.3026。loss 卡在这里说明模型完全没在学最常见的是学习率太大导致梯度在参数空间里来回震荡或者学习率太小导致参数几乎不更新还有一种可能就是输入归一化写错图像数值范围不对卷积层的输出全被 BN 拉成了同一个分布。解决先打印optimizer.param_groups[0][lr]确认学习率确实是你设置的值再用一小批数据过一遍网络打印model(x)的数值范围如果 logits 全部接近 0可能是初始化或 BN 有问题。最直接的排除法是拿一个 batch 反复过拟合清空 DataLoader 的 shuffle固定 128 张图训 20 步如果 loss 降到 0.5 以下说明反向传播链路是通的问题出在数据入口或学习率策略。5.2 训练集 99%、测试集七成出头直接过拟合现象训练准确率七八轮就冲到 95% 以上测试集准确率却一直贴在 70% 到 75%涨得非常慢。原因CIFAR-10 总共才五万张训练图模型容量足够把训练集背下来。如果数据增强只有 ToTensor 和 Normalize网络很容易把“某一张图属于某类”当成特征泛化自然崩塌。另一种可能是 weight_decay 设成了 0模型对高频噪声完全不过滤。解决优先把 RandomCrop 和 RandomHorizontalFlip 加进训练 transform这两个操作能直接让有效样本量成倍增加。然后把 weight_decay 设为 5e-4配合 BN 通常能把测试准确率拉到 85% 以上。如果还要继续压过拟合可以试 CutOut 或 RandomErasing在 32×32 的小图上随机遮掉一块区域让模型不能依赖单个局部特征。5.3 测试准确率忽高忽低和训练曲线对不上现象训练时每个 epoch 打印的 val_acc 在 89% 到 91% 之间来回跳中间没有明显的上升趋势提交到独立脚本里复算时准确率又比训练时低了两个点。原因最常见的是评估时没有调用model.eval()BatchNorm 使用了当前 batch 的统计量batch 越小抖动越大另一种可能是测试集也保留了数据增强每次评估的结果都建立在随机裁剪上指标自然没法复现。解决在evaluate()函数开头强制model.eval()并把torch.no_grad()包住完整循环。测试 transform 只保留ToTensor和Normalize任何增强操作都不要放进去。如果这两个地方都对了val_acc 曲线就会变得平滑很多同一份代码跑两次的准确率也应该在 0.1% 以内。5.4 训练中途 CUDA out of memory调到 batch16 还是爆现象训练启动正常几个 epoch 后突然报RuntimeError: CUDA out of memory把 batch 降到 16 仍然会炸。原因显存爆炸往往不是 batch 一个因素决定的。激活值、BN 的中间统计、优化器状态都会占显存如果num_workers开得过高数据预取队列也会挤占显存。另一个隐藏杀手是训练循环里忘了optimizer.zero_grad()梯度不断累加导致计算图失控。解决先在训练循环里确认zero_grad()放在backward()之前。然后检查显存占用可以使用torch.cuda.max_memory_allocated()看峰值。真到需要压缩显存的时候我建议把 batch 降到 64学习率同步折半再把torch.cuda.amp混合精度打开显存通常能砍掉约三分之一。如果这些做完还爆那要考虑你的模型是不是把通道数从 16、32、64 手动改成了 128、256、512。5.5 同一个代码跑两次结果差异大到没法比较现象固定了所有超参数第一次跑测试集准确率 90.2%第二次只有 89.4%连训练 loss 曲线都不同。原因PyTorch 的默认行为里包含随机初始化、数据加载顺序、CUDA 卷积算法的自动选择。torch.backends.cudnn.benchmarkTrue时cuDNN 会根据输入形状选择最快的卷积算法但不同算法在不同运行里可能产生微小浮点差异累积后影响最终准确率。解决在训练脚本最前面固定随机源。import random import numpy as np import torch torch.manual_seed(42) np.random.seed(42) random.seed(42) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False def seed_worker(worker_id): worker_seed torch.initial_seed() % 2**32 np.random.seed(worker_seed) random.seed(worker_seed) trainloader DataLoader( trainset, batch_size128, shuffleTrue, num_workers4, pin_memoryTrue, worker_init_fnseed_worker, )cudnn.deterministicTrue会牺牲一点速度换取可复现性cudnn.benchmarkFalse则关闭自动算法搜索。由于每个 DataLoader worker 是独立进程光设主进程的种子不够还要通过worker_init_fn给每个 worker 单独播种。这套配置做完相同代码跑两次的验证集准确率差应该控制在 0.1% 以内。6. 把分类器做厚混淆矩阵、单文件推理与模型导出模型训到 90% 之后分类器这个“demo”还不算结束真正要用起来得能回答两个问题它到底错在哪、它能不能脱离训练脚本独立运行。我自己每次在 CIFAR-10 上折腾新结构都会固定把 best 和 last 两个 checkpoint 留好再导出一个单独推理文件方便后续对比和部署。6.1 用混淆矩阵找最容易混的类别import torch from sklearn.metrics import confusion_matrix, classification_report all_y, all_pred [], [] model.load_state_dict(torch.load(cifar10_best.pth)[model]) model.eval() with torch.no_grad(): for x, y in testloader: out model(x.to(device)).argmax(1).cpu() all_y.extend(y.tolist()) all_pred.extend(out.tolist()) classes [airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck] print(classification_report(all_y, all_pred, target_namesclasses)) cm confusion_matrix(all_y, all_pred)混淆矩阵能明确告诉你模型把“汽车”和“卡车”混成了什么比例也能看“猫”和“狗”的边界到底在哪。我见过最典型的结果是 bird 和 deer 互相串因为 CIFAR-10 里这两类样本的背景都是大片绿色植被模型最后很可能依赖的是背景色而不是目标轮廓。看到这种模式你的下一步改进方向就不该是继续调学习率而是去做更好的目标定位或更针对性的数据增强。6.2 导出 TorchScript 并写一个单文件推理脚本model.eval() traced torch.jit.trace(model.cpu(), torch.randn(1, 3, 32, 32)) traced.save(cifar10_classifier.pt)用 TorchScript 导出的模型不依赖原来的类定义也不依赖训练脚本里的字典结构换机器时只要 torch 环境匹配就能直接跑。推理脚本保持最小化只保留图像读取、预处理和输出import torch from PIL import Image from torchvision import transforms classes [airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck] transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) def infer(path): model torch.jit.load(cifar10_classifier.pt, map_locationcpu) model.eval() img transform(Image.open(path).convert(RGB)).unsqueeze(0) with torch.no_grad(): logits model(img) idx logits.argmax(1).item() prob torch.softmax(logits, dim1)[0, idx].item() print(f{classes[idx]}: {prob:.4f}) if __name__ __main__: infer(test.png)推理时Resize((32,32))是为了兼容外部传进来的任意尺寸图片如果输入本来就是 32×32这段操作是幂等的。convert(RGB)统一通道数避免 RGBA 图片带透明通道导致形状不匹配。TorchScript 导出后再转 ONNX 或 TensorRT 也只是多加一步这条路径打通后分类器才算真正落到了一个能交付的状态。我自己每次都会用一张测试图走完“加载 best 模型、trace、独立脚本推理”三步确认脚本不依赖 Jupyter 环境也不依赖训练时定义的变量。这比记一堆 API 更可靠也让后续换网络结构时有了一条不变的基准线。希望帮到你。本文还有配套的精品资源点击获取
返回列表