简介:基于PyTorch的CIFAR-10图像识别压缩包,面向深度学习初学者与计算机视觉入门者,围绕经典CIFAR-10数据集,完整演示如何借助卷积神经网络(CNN)解决图像分类任务。包内共5个文件,包括2个Python脚本、1个预训练模型权重文件、1个数据元数据文件和1个说明文档,整个压缩包仅7.15MB,小巧但不失完整性。其中,CNN实现脚本详细给出网络结构、损失函数、优化器定义以及从训练到验证的完整流程;数据读取脚本则覆盖图片加载、批次组织、标准化处理和数据增强等关键预处理环节,为后续建模提供高质量输入。预训练权重文件可直接用于预测或进一步微调,说明文档则提供项目结构、使用方法和常见问题指引,帮助读者快速上手。已有52人学习浏览,适合希望从零开始理解PyTorch图像识别建模流程,并快速完成CNN实践项目的用户。
1. 这个 zip 值不值得解压:CIFAR-10 是深度学习的第一道实战门槛
如果你是刚把 PyTorch 装好、准备找个项目练手的开发者,会发现一个尴尬的现实:MNIST 太简单,跑通一次手写数字识别,对理解图像识别几乎没有增量价值;而 ImageNet 又太大,一张消费级显卡根本养不起。CIFAR-10 恰好卡在中间——32×32 的彩色小图,10 个类别,6 万张样本,单张 3090 就能在几小时内跑完一轮实验。它的难度也恰到好处:用朴素 CNN 能到 80% 出头,用 ResNet 能摸到 93% 以上,但想再往上挤 2 个点,就要开始动数据增强、学习率调度和正则化的心思。这个标题里的 zip 包,本质上是把这条完整的落线——数据加载、模型训练、评估调优——整理成了可以直接解压运行的最小工程。我建议你不要把它当成一个"跑个准确率数字"的玩具,而是当成理解 PyTorch 训练管线的最小骨架:从 Dataloader 到反向传播,从 checkpoint 到混淆矩阵,每一步都值得对照自己的需求改一遍。
2. PyTorch 环境搭建:版本匹配比安装本身更容易翻车
很多人拿到代码包的第一反应是直接pip install torch,然后跑训练脚本,接着在 import 阶段或者第一个 cuda 调用上报错。这个环节的坑和 PyTorch 本身没关系,而是版本匹配问题——Python 版本、CUDA 驱动版本、cuDNN 版本、PyTorch 版本,四者有一套松散但真实的兼容矩阵。CIFAR-10 这种规模的任务对算力要求不高,CPU 也能跑,只是慢;但如果你想在 GPU 上跑出 93% 以上的准确率,环境就要认真配。
2.1 先定 Python 版本和虚拟环境:Anaconda 是懒人最优解
我一般建议用 Anaconda 创建独立环境,而不是直接往系统 Python 里装。原因很实际:PyTorch 的依赖(numpy、PIL、torchvision)版本敏感,你手上可能有其他项目用着不同版本的 numpy,直接装会把全局环境搞成一团乱麻。Conda 环境的好处是删了重建成本极低,出问题不用重装系统。
# 创建 Python 3.10 环境,CIFAR-10 项目不需要更高版本 conda create -n cifar10 python=3.10 -y conda activate cifar10 # 验证 Python 版本 python --version # 如果已经有 conda 环境,用这个命令查看当前 env 列表 conda env list这里有个新手的常见误区:conda create -n后面的 python 版本不是你机器上原有的 Python,而是 conda 为你新拉一个独立的解释器。所以执行完这一步,which python应该指向 conda 目录下的环境路径,而不是/usr/bin/python。如果你后续在命令行里pip install装了一堆包,却发现 import 时报 ModuleNotFoundError,大概率是 shell 还停在 base 环境——用conda activate cifar10切回来即可。
2.2 CPU 与 GPU 的安装命令差异:不要无脑 pip install torch
如果你只有 CPU 机器,也想把这个 zip 里的代码跑起来,那安装命令很直接。但要注意 PyTorch 官方默认的 pip 包自带 CUDA 依赖,体积大且占空间,CPU 场景建议走 CPU 专用版本:
# CPU 版本:适合笔记本和老款 Mac(Intel 芯片) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # 验证安装,能 import 且 cuda 不可用是正常的 python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"GPU 机器的安装命令是大多数人卡住的地方。核心经验:先确认你的 CUDA 驱动版本支持哪个 CUDA 运行时版本,再去选对应的 PyTorch 轮子,而不是装最新版。NVIDIA 驱动是向后兼容的,但 PyTorch 编译时绑定的 CUDA 运行时不能高于驱动支持的版本。举例来说,驱动版本 535.x 支持 CUDA 12.2,那你装cu121或cu118的 PyTorch 都没问题;如果驱动是 470.x(只支持 CUDA 11.4),就只能装cu113或cu111的旧版 PyTorch——这类情况通常发生在老工作站或品牌整机上。
# 查看显卡驱动支持的 CUDA 版本 nvidia-smi # 右上角 "CUDA Version" 是驱动支持的最大版本,不是当前运行版本 # 以 CUDA 12.1 为例的安装命令(2024 年后官方主推的版本) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 验证 GPU 可用 python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"在安装时你可能会看到conda install pytorch torchvision torchaudio cudatoolkit=11.8 -c pytorch这类旧教程的写法。Conda 方式的优点是自动处理 CUDA 工具链,但缺点是 conda 的软件源同步速度不如 pip,而且新版 PyTorch 已经不再把cudatoolkit作为独立 conda 包分发。2024 年之后的 PyTorch 官方安装文档里,已经默认用 pip + index-url 路线,如果你不想折腾,直接照抄这一节的两条命令即可。
2.3 一个容易忽略的细节:torchvision 版本要和 torch 严格匹配
CIFAR-10 的数据集加载通常依赖torchvision.datasets.CIFAR10,而 torchvision 对 torch 的版本有严格依赖——两者不像其他 Python 包那样可以随便混用。常见的报错是AttributeError: module 'torch' has no attribute 'ops',这类问题的根因基本都是 torch 和 torchvision 版本不匹配。为了避免这种玄学错误,我习惯把四个包的版本一起固定在 requirements 文件里:
# requirements.txt 内容示例 torch==2.1.2 torchvision==0.16.2 numpy==1.26.4 pillow==10.2.0 matplotlib==3.8.3 tqdm==4.66.2 # 安装 pip install -r requirements.txt这里补充一个匹配原则:torch 2.1.x 对应 torchvision 0.16.x,torch 2.2.x 对应 torchvision 0.17.x,torch 2.3.x 对应 torchvision 0.18.x。升级 torch 时务必连带升级 torchvision,否则 import 阶段就报错。如果你在没有任何 CUDA 的纯 CPU 机器上跑,torch 和 torchvision 仍然要版本匹配,这个规则和 GPU 无关。
3. 数据加载和预处理:别小看 32×32 的图
环境配好之后,下一步是数据。CIFAR-10 数据集本身不大——60,000 张图,每张 32×32×3,打包后约 163MB。这个规模对现代机器来说非常友好,即使不预处理也能直接塞进内存。但也正因图小,很多人会忽略数据加载阶段的细节,导致后续训练准确率低、收敛慢。数据搞对了,训练就成功了一半。
3.1 CIFAR-10 的数据结构:类别是互斥的,标签是数字
CIFAR-10 包含 10 个类别:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。每类 6,000 张,训练集 50,000 张,测试集 10,000 张。标签是 0-9 的整数,没有类别不平衡问题,也没有遮挡、模糊等复杂干扰。这些特性决定了它的处理方式:你不需要像处理医学影像那样做重采样,也不需要做类别权重。
from torchvision import datasets # 第一次运行会自动下载,注意观察下载路径 train_dataset = datasets.CIFAR10(root='./data', train=True, download=True) test_dataset = datasets.CIFAR10(root='./data', train=False, download=True) # 查看数据结构 print(len(train_dataset)) # 50000 print(len(test_dataset)) # 10000 print(train_dataset[0][0].size) # <PIL.Image.Image image mode=RGB size=32x32> print(train_dataset[0][1]) # 6 (标签,对应 frog)download=True会在root目录下创建cifar-10-batches-py文件夹,里面是五个训练批次文件和一个测试批次文件。每个批次是用 pickle 序列化的字典,包含data(形状 [10000, 3072] 的 numpy 数组)和labels(长度为 10000 的列表)。
如果下载速度慢,常见做法是去官网手动下载cifar-10-python.tar.gz放到root目录,PyTorch 会自动检测到压缩包并解压。这里有一个需要注意的边界:测试集和训练集是官方固定的,不会混在一起,所以每次实验的指标有可比性,不像你自己爬的数据还要操心划分。在写代码时,train=True和train=False务必不要写反——否则会出现"测试集比训练集还好"这种违背直觉的结果。
3.2 Dataloader 的四个关键参数:batch_size、shuffle、num_workers、pin_memory
数据准备好后,要用DataLoader把它包装成可迭代对象。这个类看起来简单,但参数的设置直接影响两个东西:训练速度和数据分布的随机性。
from torch.utils.data import DataLoader from torchvision import transforms # 定义数据变换:先转 Tensor,再标准化 transform = transforms.Compose([ transforms.ToTensor(), # 将 PIL Image 或 numpy 数组转成 [0,1] 范围内的 Tensor transforms.Normalize( mean=(0.4914, 0.4822, 0.4465), # 每个通道的均值 std=(0.2470, 0.2435, 0.2616) # 每个通道的标准差 ), ]) train_dataset = datasets.CIFAR10(root='./data', train=True, download=False, transform=transform) test_dataset = datasets.CIFAR10(root='./data', train=False, download=False, transform=transform) train_loader = DataLoader( train_dataset, batch_size=128, # 每次迭代采样 128 张图 shuffle=True, # 每个 epoch 重新打乱顺序 num_workers=4, # 用 4 个子进程预加载数据 pin_memory=True, # 锁页内存,加速 CPU 到 GPU 的传输 ) test_loader = DataLoader( test_dataset, batch_size=256, # 测试时 batch 可以更大 shuffle=False, # 测试不需要打乱 num_workers=2, pin_memory=True, )这里说三个容易踩坑的参数:
batch_size不是越大越好。CIFAR-10 单张图只有 3KB 左右,显存占用不大,但过大的 batch 会让梯度估计过于平滑,导致收敛变慢。做实验建议从 64 或 128 起步,显存不够时再往下调。num_workers是数据预加载的子进程数。Windows 上设过大会报BrokenPipeError,这是 Python 多进程在 Windows 下的已知限制,把值降到 0 或 2 即可;Linux 设 4-8 都正常。shuffle在训练时必须为True,否则每个 epoch 的数据顺序一样,模型会学到一个虚假的样本顺序,更容易过拟合。测试时则不需要。
3.3 数据增强:把 50,000 张图变成"用不完"的训练集
CIFAR-10 本身只有 5 万张训练图,对于 ResNet 这类大模型来说很容易过拟合。数据增强是解决这个问题的第一道防线。PyTorch 官方的经典组合是"随机水平翻转 + 随机裁剪 + 标准化",这看似简单,但能带来 1-2% 的准确率提升,效果比换模型结构还明显。
# 训练集的数据增强(测试集只用 ToTensor + Normalize,不做增强) train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), # 先扩边 4 像素,再随机裁剪回 32×32 transforms.RandomHorizontalFlip(p=0.5), # 50% 概率水平翻转 transforms.ToTensor(), transforms.Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616)), ]) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616)), ])RandomCrop(32, padding=4)是这套增强里贡献最大的操作:先把图片周围补 4 个像素的 0(黑色),再随机裁剪回原尺寸。之所以 padding 选 4,是 CIFAR-10 官方推荐值——32×32 的图太小,padding 超过 8 会引入过多无效边界,低于 2 则基本没效果。RandomHorizontalFlip对于"汽车""船"这类左右对称的类别有效,但要注意:如果你做的是手写数字识别,就不能用这个增强——数字"6"翻转后会变成"9",标签就错了。CIFAR-10 的 10 个类别没有这种镜像歧义,所以可以放心用。
提示:数据增强是分训练集和测试集的。测试集永远只做标准化,不做随机裁剪和翻转——因为测试集模拟的是模型在真实场景中的表现,不能引入随机性。
4. 模型训练:从自定义 CNN 到 ResNet,逐步逼近 93% 准确率
环境就绪、数据加载跑通后,就进入了这个 zip 包的核心部分——训练。CIFAR-10 图像识别的模型选择有一条清晰的路径:先搭一个简单的卷积网络理解训练流程,再用 ResNet 作为主力模型追求精度。很多人喜欢一步到位直接上 ResNet,但我的建议是先花半小时把简单 CNN 跑通,这样你才能区分"代码问题"和"模型问题"。
4.1 先写一个能到 75% 的简单 CNN:理解卷积特征提取的基本套路
第一个模型不需要复杂,一组"卷积+池化+全连接"就能达到 70% 以上。关键是理解每一层输出的形状变化,这决定了全连接层的输入维度。
import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super().__init__() # 输入: [batch, 3, 32, 32] self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1) # 输出: [batch, 32, 32, 32] self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1) # 输出: [batch, 64, 16, 16],经过一次 MaxPool2d(2) 后从 32×32 变 16×16 self.pool = nn.MaxPool2d(kernel_size=2, stride=2) # 展平后: 64 * 16 * 16 = 16384 self.fc1 = nn.Linear(in_features=64 * 16 * 16, out_features=256) self.fc2 = nn.Linear(in_features=256, out_features=10) self.dropout = nn.Dropout(p=0.5) def forward(self, x): x = F.relu(self.conv1(x)) # [batch, 32, 32, 32] x = self.pool(x) # [batch, 32, 16, 16] x = F.relu(self.conv2(x)) # [batch, 64, 16, 16] x = self.pool(x) # [batch, 64, 8, 8] x = x.view(x.size(0), -1) # 展平为 [batch, 64*8*8=4096] x = F.relu(self.fc1(x)) # [batch, 256] x = self.dropout(x) x = self.fc2(x) # [batch, 10] return x这段代码里的kernel_size=3, padding=1组合可以保证特征图尺寸不变(32×32 输入经 3×3 卷积后仍是 32×32),然后靠 MaxPool2d 减半。这种"卷积保尺寸、池化降分辨率"的模式是 CNN 设计的通用套路。self.dropout放在全连接层之前,是为了抑制全连接层的过拟合——卷积层的参数共享机制已经天然有正则化效果,而全连接层的参数量大,更容易记住训练集。
训练这个模型时,有两个参数需要格外关注:学习率和优化器选择。对于 CIFAR-10 这种小数据集,Adam 是省心的选择(默认 lr=0.001),SGD 需要更仔细地调学习率(常见 0.01 到 0.1),但最终收敛精度通常略高于 Adam。如果你追求速度、不追求极限准确率,直接用 Adam。
4.2 训练循环的骨架:5 个 epoch 就能看到趋势
模型定义好了,训练代码才是真正花时间的地方。一个合格的训练循环至少包含:前向传播、计算 loss、反向传播、权重更新、验证集评估、保存 checkpoint。
import torch import torch.optim as optim import torch.nn.functional as F from tqdm import tqdm device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleCNN().to(device) optimizer = optim.Adam(model.parameters(), lr=0.001) criterion = nn.CrossEntropyLoss() def train_one_epoch(model, train_loader, optimizer, criterion, device): model.train() # 重要:切换到训练模式,启用 dropout running_loss = 0.0 correct = 0 total = 0 for images, labels in tqdm(train_loader): images, labels = images.to(device), labels.to(device) # 前向传播 outputs = model(images) loss = criterion(outputs, labels) # 反向传播 optimizer.zero_grad() # 梯度清零,否则会累加 loss.backward() optimizer.step() # 更新权重 # 统计 running_loss += loss.item() _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() epoch_loss = running_loss / len(train_loader) epoch_acc = correct / total return epoch_loss, epoch_acc for epoch in range(10): train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion, device) print(f"Epoch {epoch+1}: Loss = {train_loss:.4f}, Acc = {train_acc:.4f}")代码里最容易漏的是optimizer.zero_grad()。PyTorch 的梯度是累加模式,如果不清零,每个 batch 的梯度会叠在一起,导致更新方向完全错误,loss 表现为剧烈震荡。另外model.train()和model.eval()的切换要成对出现——Dropout和BatchNorm在训练和推理时的行为不同,用错会导致验证集准确率偏低,很多新手在评估时忘了切回eval(),看到的结果比实际差一大截。
4.3 上 ResNet18:预训练迁移 vs 从零训练的选择
简单 CNN 的上限在 80% 左右,想冲击 90% 以上就要用残差网络。ResNet 的核心贡献在于跳跃连接:把前面层的输出直接加到后面层的输入上,缓解深层网络梯度消失的问题。PyTorch 官方torchvision.models里提供了现成的 ResNet18,但直接用会报错,因为它的原始输出是 ImageNet 的 1000 类——需要替换最后一层。
import torchvision.models as models # 方式一:使用 ImageNet 预训练权重(迁移学习) resnet18 = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层全连接,输出改为 10 类 resnet18.fc = nn.Linear(in_features=512, out_features=10) # 方式二:不用预训练,随机初始化(从头训练) resnet18_scratch = models.resnet18(weights=None) resnet18_scratch.fc = nn.Linear(in_features=512, out_features=10)这两种方式的实验结论值得记住:CIFAR-10 上预训练权重并不一定能提升最终准确率,因为 ImageNet 的图像是 224×224,而 CIFAR-10 是 32×32。预训练模型的前几层卷积核是为大图设计的,直接用在 32×32 小图上会损失很多细节。如果你的训练时间有限(比如只有一张卡,想快速出结果),迁移学习效果更好;如果你想深入理解训练过程,用weights=None从头训练更能看清数据增强和优化器的作用。
从零训练 ResNet18 的关键超参策略:batch_size 从 64 起步,初始学习率 0.1(配合 SGD 的 momentum=0.9),权重衰减(weight_decay)设 5e-4,学习率在 epoch 40、60、80 时乘 0.1。这个配置是 CIFAR-10 社区里被反复验证过的经典设置,你不需要自己从头摸索。全流程训练 100 epoch 在单张 3090 上大约 30 分钟。
5. CIFAR-10 训练避坑指南:四个让新手翻车的典型场景
训练代码写完,真正的问题才开始暴露。我整理了四个出现频率最高的坑,每一个我都亲手踩过,按"现象→原因→解决"的方式列出来,供你排查时对照。
5.1 数据集下载卡在 0% 不动,或者短暂下载后报错
现象:
download=True后,进度条一直停在 0KB,等待几分钟后报ConnectionError或URL fetch failure。
原因:CIFAR-10 的下载源在服务器,国内网络环境经常访问超时。这不是代码问题,是网络环境问题。
解决:不要反复重试下载。用浏览器或第三方下载工具先下载
cifar-10-python.tar.gz(约 170MB),手动放到项目目录下的./data文件夹中(没有就新建),然后再次运行代码,PyTorch 会检测到压缩包并自动解压,不再走网络下载。
5.2 GPU 显存 OOM,报CUDA out of memory
现象:训练刚开始,显存占用飙升,报错里带有
RuntimeError: CUDA out of memory。
原因:最常见的有三类——batch_size 设置过大;输入图片尺寸不规范导致中间特征图爆炸;或者在同一 session 里反复创建模型没有释放显存(PyTorch 的显存缓存机制会导致碎片化)。
解决:优先把 batch_size 减半(128→64,64→32);检查数据预处理里
Resize步骤,CIFAR-10 必须保持 32×32;训练结束后用torch.cuda.empty_cache()手动释放缓存。如果你用的是 Windows 且开着多个 Jupyter Notebook,建议先把其他 kernel 关掉。
5.3 训练 loss 不降,或者降到 1.0 附近就卡住
现象:loss 从 2.3 开始下降,到 1.0-1.2 之间就不动了,准确率停在 50%-60% 之间上不去。
原因:CIFAR-10 有 10 个类别,随机猜测的准确率是 10%(对应 loss ≈ 2.30)。loss 停在 1.0 附近说明模型学到了一些模式但远不够。绝大多数情况下这是学习率偏高,模型在最优解附近震荡;少数情况是数据增强太强,比如
RandomCrop的 padding 设到 16,引入了过多无效边界。
解决:学习率从 0.001 降到 0.0001,或者加入学习率预热(warmup)——前 5 个 epoch 让学习率从 0 线性增加到目标值,可以避免初始阶段的大幅震荡。如果数据增强过强,把 padding 调回 4,或者暂时去掉
RandomHorizontalFlip做对照实验。
5.4 加了预训练权重反而掉点
现象:用
weights=IMAGENET1K_V1的 ResNet18 比别人从零训练的准确率低 2-3 个百分点,尤其在测试集上差距明显。
原因:CIFAR-10 的分辨率 32×32 和 ImageNet 的 224×224 差距过大。预训练模型的权重的底层特征(边缘、纹理)对 CIFAR-10 有效,但高层特征(物体的部件、类别语义)完全不匹配,强行迁移会产生负迁移。同时,预训练模型还带着 ImageNet 数据集的偏置,比如背景丰富度远高于 CIFAR-10 的纯色底图。
解决:在 CIFAR-10 上从零训练是主流选择。如果一定要用预训练,建议冻结前两层卷积的权重(
requires_grad=False),只微调后面的层,相当于把 CIFAR-10 的特征提取器当成低层特征抽取器来用。这个技巧在小数据集的迁移学习中普遍适用。
5.5 验证集准确率和训练集差太多
现象:训练集准确率已经到 92%,验证集还停在 80% 附近。
原因:这是典型的过拟合信号。CIFAR-10 训练集只有 5 万张,模型参数量一旦上来(比如 ResNet18 的 1100 万参数),很容易把训练集细节背下来,而不是学到可泛化的模式。
解决:按顺序排查——确认测试时用了
model.eval()(漏掉这行会导致 dropout 仍然生效,结果看起来更差);增大数据增强强度,比如加入RandomRotation(degrees=15)或ColorJitter;增加Dropout比例;把 weight_decay 从 0 提高到 5e-4。做过这些之后,训练集和验证集的差距通常会从 12 个百分点缩到 3-4 个百分点。
6. 从"能跑"到"能用":混淆矩阵、迁移学习和 ONNX 导出的进阶之路
准确率数字刷到 90% 以上后,很多人的项目就停在"训练完了"这一步。但如果你想把 CIFAR-10 当做一个完整项目积累下来,还有三件让我觉得整个包真正值回票价的事:看混淆矩阵、做一次迁移学习对比实验、把模型导出成 ONNX 部署格式。这三件事能分别回答三个问题:模型错在哪、预训练到底有没有用、以及训练完的模型怎么给别人用。
6.1 混淆矩阵:看看模型把哪个类别搞混了
import numpy as np from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # 收集所有测试集的预测结果 all_preds = [] all_labels = [] model.eval() with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 计算混淆矩阵 cm = confusion_matrix(all_labels, all_preds) class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] # 可视化 plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.xlabel('Predicted') plt.ylabel('True') plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')观察输出矩阵时,通常会看到一个明确的规律:猫和狗最容易互混,汽车和卡车也容易互混。前者是语义相近的动物类别,后者是外形相似的交通工具。如果你的模型在这两类上准确率特别低,不要惊讶,这是 CIFAR-10 上所有模型的通病。一个值得注意的细节是:混淆矩阵的量纲是"样本数",不需要用百分比画热力图时,fmt='d'与你样本量是否整除无关,只会完整展示每个格子的实际数量。
6.2 迁移学习的对比实验:同一个超参数,预训练 vs 从零训练
与其听说预训练在 CIFAR-10 上效果不佳,不如自己跑一次对比实验。固定数据和训练超参数(batch_size=64,lr=0.01,epoch=50),只改变weights=None还是weights=IMAGENET1K_V1,你会看到两条训练曲线的差异规律:预训练模型在第一个 epoch 的 loss 就远低于从零训练(因为前几层已经收敛),但第 20 个 epoch 之后曲线会变得平缓,最终准确率可能反而不如随机初始化的模型。如果你做这个实验,建议同时记录每个 epoch 的验证集准确率,最后画成曲线图——这比任何文字解释都有说服力。
如果实验结果和你预期相反(预训练比从零训练高),不要急着下结论:先检查学习率是否偏小,预训练模型在低学习率下收敛慢,容易停留在局部最优。
6.3 导出 ONNX:让 PyTorch 模型和部署框架接轨
PyTorch 模型本身是 Python 世界的东西,如果要把训练好的模型集成到服务端推理框架(如 TensorRT、ONNX Runtime),格式转换是必经之路。ONNX 是目前最通用的中间格式,PyTorch 官方提供了专门的导出 API:
import torch import torchvision.models as models model = models.resnet18(weights=None) model.fc = torch.nn.Linear(512, 10) model.load_state_dict(torch.load("cifar10_resnet18.pth")) # 加载训练好的权重 model.eval() # 构造一个随机输入,导出的 ONNX 需要知道输入形状 dummy_input = torch.randn(1, 3, 32, 32) torch.onnx.export( model, dummy_input, "cifar10_resnet18.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, opset_version=17, )导出之后,用 ONNX Runtime 加载做一次推理验证,确保导出的模型和 PyTorch 原模型输出一致。
常见的坑是导出时报TracerWarning或RuntimeError: Unsupported operator。前者是因为模型里有torch.nn.functional中的动态操作,不影响结果但会提示你检查输出;后者是模型里有自定义操作,ONNX 算子集不认识。对标准的 ResNet 来说,这两个问题基本不会出现。检查是否导出成功的方式:先看 ONNX 文件大小是否和 PyTorch 权重文件相近,再用 ONNX Runtime 跑一次推理对比输出差异。
我自己的习惯是,CIFAR-10 这个项目跑完后,把训练好的模型同时保存.pth和.onnx两份,前者用于继续训练和调优,后者用于快速部署到任何环境。这个习惯延续到了后来所有深度学习项目——输出格式永远不只留自己熟悉的那个,因为你不知道接手项目的人用什么环境。
上面说的这些步骤,从解压 zip 包到跑通 ResNet18,再到导出 ONNX,一整套流程下来,你对 PyTorch 的训练管线、数据增强、部署链路就有了实打实的体感。CIFAR-10 的价值不在于这个数据集本身——按今天的标准它分辨率太低、类别太简单——而在于它是你能在最短时间内完整跑通"数据→训练→评估→部署"全流程的最低成本载体。如果你手头正缺一个能验证 PyTorch 环境、又不会因为数据问题卡住你两天的项目,从这个 zip 开始是值得的。希望这篇笔记能帮你少走几段弯路。
本文还有配套的精品资源,点击获取