PyTorch CIFAR-10图像识别实战:从环境配置到CNN迁移学习

发布时间:2026/9/26 8:52:20
PyTorch CIFAR-10图像识别实战:从环境配置到CNN迁移学习
简介这是一份面向计算机视觉初学者的PyTorch图像识别实战压缩包聚焦图像识别与机器学习核心定位是利用CIFAR-10数据集快速上手CNN模型构建与训练。包体共5个文件、约7.15MB主要包含两个Python脚本、一个已训练好的模型权重、一个元数据文件和一个Markdown说明文档分别承担模型训练、数据读取预处理、权重加载与项目指导等任务。其中图像识别脚本完整展示了网络结构定义、损失函数与优化器配置及训练流程数据读取脚本则覆盖标准化、数据增强等预处理环节有助于提升模型泛化能力。这5个文件构成了一条从数据读取、模型训练到推理验证的完整链路代码可直接运行便于边学边练。预训练权重可直接加载用于预测或微调免去从头训练的繁琐说明文档对使用方法和常见问题给出指引。目前已有52人浏览学习适合机器学习初学者或希望快速掌握PyTorch图像识别流程的研究者参考实践。1. CIFAR-10图像识别项目拆分这份资源到底在帮你解决什么如果你和我一样最开始是拿 MNIST 练手跑通的 PyTorch那第一次把同样的代码搬到 CIFAR-10 上大概率会翻车——32×32 的彩色小图十类物体五万张训练图随便一个两层卷积网络跑出来测试集准确率卡在 70% 上下不去loss 曲线还一跳一跳的。这套「基于PyTorch的CIFAR-10图像识别」资源就是把从环境搭建、数据加载、模型训练到调参排查的完整过程拆开给你看每一步都有能直接跑的代码和对应的参数说明。它不是那种只给一个 ipynb 让你自己琢磨的压缩包而是一份能照着复现的实战记录。适合刚学完 PyTorch 基础、想拿真实数据集做第一个完整项目的同学也适合已经在做图像识别、但每次换数据集都要重新踩一遍环境坑的从业者。接下来我按自己拆这个项目的顺序把关键节点和踩过的坑都过一遍。2. 环境搭建与 PyTorch 安装从 Anaconda 到 CUDA 的一次到位2.1 为什么我先用 Anaconda 建虚拟环境装 PyTorch 之前我强烈建议先装 Anaconda并用它创建一个独立的虚拟环境。很多初学者图省事直接pip install torch装到系统 Python 里后面装 OpenCV、装 transformers、装其他依赖时互相冲突最后只能重装系统解释器。代码写得好不如环境稳虚拟环境就是你的后悔药。常见的做法是创建一个专门的环境Python 版本固定PyTorch 和 CUDA 工具包都装在里面跟其他项目的依赖彻底隔离。创建命令如下conda create -n cifar10 python3.8 -y conda activate cifar10创建环境时指定python3.8是因为 PyTorch 对 3.8–3.11 的支持都比较稳定3.8 兼容性最广尤其是你要用老版本 CUDA 或者某些编译包时不容易出问题。-y参数表示自动确认省去交互输入。激活后命令行前缀会变成(cifar10)这时候你再装任何包都只影响这个环境系统 Python 不受干扰。2.2 分平台安装命令与 GPU 验证PyTorch 的安装命令在不同硬件平台上有明显区别。最常见的是 NVIDIA GPU 平台进入 PyTorch 官网的 Get Started 页面选择对应的 CUDA 版本会生成一行pip或conda命令。我这台机器是 CUDA 11.8所以用的是pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果电脑没有 NVIDIA 显卡或者暂时不想折腾 CUDA直接装 CPU 版pip install torch torchvision torchaudio安装完成后第一时间验证 GPU 是否真的被 PyTorch 识别到。这一步不能省因为 pip 默认装的版本可能不带 CUDA 支持你后面训练时完全感觉不到模型在跑 GPU 还是 CPUimport torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU mode)如果torch.cuda.is_available()返回False问题通常出在 PyTorch 版本和 CUDA 驱动不匹配。注意区分两个概念你的电脑只需要装好 NVIDIA 驱动PyTorch 自带的 CUDA 运行库是跟驱动配合使用的不需要单独安装完整 CUDA Toolkit。torch.__version__如果带有cu118后缀说明装的是 CUDA 11.8 版本这时候驱动版本不能太低否则即使is_available()为 True实际跑起来也会报驱动错误。2.3 CUDA 版本和 PyTorch 版本怎么配对这可能是环境搭建里玄学最多的地方。我的经验是先看nvidia-smi显示的驱动版本和它支持的 CUDA 版本再决定装哪个 PyTorch。nvidia-smi输出右上角的 CUDA Version 表示你的驱动最高支持的版本比如 12.2。注意这不是说你必须装 CUDA 12.2 的 PyTorch而是说你可以装任意低于或等于 12.2 的版本。我一般选稳定性优先CUDA 11.8 或 12.1 的 PyTorch 都行。如果你用 WSL2 环境驱动在 Windows 宿主上WSL 内部直接pip install torch torchvision通常就能识别 GPU这一点在加载资源里的代码时也有体现。还有一个容易忽略的点torchvision必须和torch版本配套。比如torch2.0.1对应torchvision0.15.2版本不匹配会在 import 时报错。不要分别单独安装最新版要一起指定版本否则from torchvision import datasets很有可能会因为 ABI 不匹配直接崩溃。3. CIFAR-10 数据加载与预处理从下载到 DataLoader3.1 torchvision 自带数据集还是手动下载CIFAR-10 这个数据集被 torchvision 内置了最省事的方式是直接用torchvision.datasets.CIFAR10。它会自动下载、缓存、按训练集和测试集切分还自带了 transform 接口。但这里有个网络坑国内网络环境下下载源https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz经常超时卡在 Downloading 进度条半天不动。我建议第一次使用前先手动下载数据集放到项目目录下再用downloadFalse加载。这样能避免在训练脚本里重复下载也方便换机器复现import torch import torchvision import torchvision.transforms as transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) trainset torchvision.datasets.CIFAR10( root./data, trainTrue, downloadFalse, transformtransform ) testset torchvision.datasets.CIFAR10( root./data, trainFalse, downloadFalse, transformtransform )root./data是数据集存放目录目录下需要有cifar-10-python.tar.gz文件或者已经解压好的cifar-10-batches-py文件夹。Normalize里的四个数字分别是三个通道的均值和标准差CIFAR-10 官方图像的整体统计值直接引用即可不需要你自己重新算。这一步的核心是让像素值从 0–255 归一化到均值 0、方差 1 的分布对收敛速度有明显帮助。3.2 数据增强翻转、裁剪、标准化参数CIFAR-10 只有五万张训练图直接硬训很容易过拟合。我的做法是在训练集上做数据增强测试集只做标准化不增强。这是图像分类任务的标准流程。train_transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) test_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ])RandomCrop(32, padding4)的意思是先把图像四周补 4 个像素再随机裁剪回 32×32。这样每张图在每次 epoch 看到的都是略微不同的版本相当于把数据集扩充了几倍。RandomHorizontalFlip随机水平翻转对飞机、汽车这类对称物体效果好但对数字、文字这类方向敏感的类别要慎用——好在这份资源里只是 CIFAR-10不涉及文本识别场景。值得注意ToTensor必须放在RandomCrop和RandomHorizontalFlip之后因为这两个操作需要 PIL 图像格式ToTensor之后的数据是 Tensor再调用会直接报错。3.3 DataLoader 的 batch、num_workers 和 shuffle 参数有了数据集下一步就是构建 DataLoader。参数设置看起来简单但num_workers设不好会出现训练时卡死或进程崩溃。trainloader torch.utils.data.DataLoader( trainset, batch_size128, shuffleTrue, num_workers4 ) testloader torch.utils.data.DataLoader( testset, batch_size256, shuffleFalse, num_workers4 )batch_size128是显存和收敛速度的折中。我的显卡是 8GB 显存128 的 batch 配合一个中等规模的 CNN 完全够用。如果你显存只有 4GB建议调到 64。shuffleTrue只用于训练集测试集不需要打乱否则评估结果不稳定每次跑出来的准确率会有波动。num_workers4开启四个子进程加载数据让 GPU 在计算时 CPU 并行做图像解码和增强能明显提升训练吞吐。但 Windows 环境下num_workers大于 0 有时会报BrokenPipeError这是因为 Windows 的进程启动机制和 Linux 不同解决方案是训练主逻辑放在if __name__ __main__:里或者把num_workers改回 0。加载完后可以用一个简单的可视化检查数据是否正确import matplotlib.pyplot as plt import numpy as np dataiter iter(trainloader) images, labels next(dataiter) img images[0].permute(1, 2, 0).numpy() img np.clip(img, 0, 1) plt.imshow(img) plt.title(fLabel: {labels[0].item()}) plt.show()permute(1, 2, 0)是因为 PyTorch 的 Tensor 是(channel, height, width)顺序而 matplotlib 需要(height, width, channel)。np.clip把像素值裁剪到 0–1 区间防止Normalize之后出现的负值导致图像显示发黑或发白。这一步虽然简单但能最直观地确认整个数据链路是通的。4. CNN 模型构建与训练用结构换精度的三个关键选择4.1 基础卷积块设计Conv-BN-ReLUCIFAR-10 图像只有 32×32不适合上来就用 VGG 或 ResNet 的深度结构容易过拟合而且训练慢。我在这套资源里用的是一种精简 CNN结构上用「卷积 批归一化 ReLU」作为基本单元叠加池化层降分辨率。批归一化是关键它让每一层的输入分布稳定能让你用更大的学习率收敛速度也快很多。import torch.nn as nn import torch.nn.functional as F class Cifar10CNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) ) self.conv2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) ) self.conv3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) ) self.fc nn.Linear(128 * 4 * 4, 10) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x x.view(x.size(0), -1) x self.fc(x) return xpadding1让卷积不改变特征图尺寸所以 32×32 的输入经过三次 MaxPool2d(2) 后变成 4×4。BatchNorm2d的参数是通道数不能填错否则会报维度不匹配。全连接层输入维度的计算逻辑是128 个通道 × 4 × 4 的空间尺寸这个数字是在模型结构确定后手动算出来的改网络结构时最容易忘改这里。view(x.size(0), -1)是把每个 batch 内的特征图拉平成向量x.size(0)是 batch 大小-1自动推断展平后的长度。4.2 训练循环、损失函数和优化器模型定义好后训练循环是核心。我习惯把每一轮 epoch 的 loss 和准确率打出来方便观察趋势。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model Cifar10CNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) for epoch in range(30): model.train() running_loss 0.0 for inputs, labels in trainloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) epoch_loss running_loss / len(trainset) model.eval() correct 0 total 0 with torch.no_grad(): for inputs, labels in testloader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total print(fEpoch {epoch1}/30, Loss: {epoch_loss:.4f}, Test Acc: {acc:.4f})CrossEntropyLoss内部已经包含了 Softmax所以模型的最后一层不需要额外加 Softmax直接在 forward 里输出原始 logits 即可。optimizer.zero_grad()每次迭代前清零梯度不然后一次反向传播的梯度会累加到前一次之上loss 就会乱跳。train/eval 模式切换是很多人容易忽略的细节model.eval()会把BatchNorm和Dropout切换到推理行为如果忘了加训练和测试时的分布不一致测试准确率会偏低。测试阶段包在torch.no_grad()里不计算梯度内存占用和速度都会好很多。4.3 训练曲线怎么看训练不是把代码跑通就完事。我拆这个资源时反复强调一个观点必须看曲线判断模型状态。正常情况是训练 loss 稳步下降测试准确率逐步上升。如果 loss 降得很慢可以尝试把学习率从 0.001 调到 0.01但如果调大后 loss 反而震荡甚至变成 NaN说明学习率过大需要降回来。另一种常见情况是训练 loss 降得不错但测试准确率停滞不动这是典型的过拟合。解决顺序是先加weight_decay上面代码里已经加了1e-4再做数据增强最后才是缩小模型。不要一上来就换 ResNet先把当前模型的训练曲线看懂再决定下一步动作。这条经验在整套资源里都有贯穿前面的每一次调参都是基于曲线反馈不是凭感觉。5. CIFAR-10 训练常见问题排查五个翻车现场与处理5.1 Loss 不降或降着降着又弹回去现象训练 10 个 epoch 之后loss 还在 1.5 左右徘徊或者某几个 epoch 突然从 0.5 弹回 1.2。原因多数情况是学习率设置不合理或者 batch 太小导致梯度不稳定。CIFAR-10 配合 Adam 优化器lr0.001是一个比较稳的起点但如果你用的是 SGD同样学习率可能就偏大了。解决先看 loss 弹跳幅度如果每次都在同一点附近震荡把学习率降十倍试试。如果降到1e-4还是不稳检查batch_size——太小的 batch 会让梯度的噪声变大建议不低于 64。换个思路也可以用学习率预热前几个 epoch 用小学习率让模型先稳定后面再慢慢加上去。5.2 训练集准确率很高但测试集上不去现象训练集准确率接近 95%测试集只有 72%–75%而且随着训练继续测试集准确率不再增长。原因这是典型的过拟合。CIFAR-10 五万张图对一个百万参数的 CNN 来说完全有能力「背下来」但背下来不等于学到了泛化特征。解决按这个顺序来先加weight_decay然后加数据增强RandomCrop Flip最后加 Dropout 层。在资源里我实测过同样的模型结构增强前测试准确率约 75%增强后可以到 82% 以上。如果这三招都用了还是过拟合再考虑换成更小的网络不要硬扛。5.3 GPU 没吃满甚至根本没调用现象训练时nvidia-smi显示 GPU 利用率只有 10% 出头或者 PyTorch 完全跑在 CPU 上速度慢得离谱。原因最常见的是 PyTorch 装了 CPU 版torch.cuda.is_available()返回 False。另一种是data_loader的num_workers设置为 0GPU 每算完一个 batch 就要干等 CPU 加载数据利用率自然上不去。解决先执行torch.cuda.is_available()验证若不是 True按第 2 章的步骤重新安装 GPU 版 PyTorch。如果 GPU 可用但利用率低调大num_workers到 4 或 8同时把batch_size调大让 GPU 每次处理的批量更大减少等待时间。还要注意DataLoader里的pin_memoryTrue可以缩短数据从 CPU 到 GPU 的拷贝时间在资源里的脚本中我默认开着。5.4 数据加载崩溃num_workers 引发的进程错误现象Windows 下训练到第一个 epoch 结束、开始第二个 epoch 时弹出BrokenPipeError主进程直接崩掉。原因Windows 的多进程数据加载机制和 Linux 不同num_workers 0时每个 worker 都会尝试重新导入主模块如果没有if __name__ __main__:保护子进程会递归地创建子进程最终导致管道断裂。解决训练主逻辑一律放进if __name__ __main__:里这是最彻底的解法。另外如果只是临时跑一下把num_workers改成 0 也能绕过但训练速度会明显下降不推荐。5.5 数据集下载超时或卡住现象downloadTrue时下载进度条走到一半就不动了或者报ConnectionError。原因CIFAR-10 的数据托管在多伦多大学的服务器上国内访问不稳定偶尔还会被重置连接。解决手动下载cifar-10-python.tar.gz放到./data目录下然后downloadFalse加载。如果你的网络连手动下载都很慢可以用国内镜像站下载同一份文件校验一下 MD5 确认完整再放进去。这套资源里的代码我特意把download参数留成可配置就是方便你按网络环境切换。6. 白嫖预训练模型用 ResNet 迁移学习把准确率推到 90% 以上6.1 加载预训练权重并替换分类头自己从头训练一个 CNNCIFAR-10 准确率到 85% 已经需要不少调参功力。想再往上走迁移学习是目前最稳的方案。torchvision.models里提供了在 ImageNet 上预训练好的 ResNet18虽然 ImageNet 的图片分辨率和 CIFAR-10 不一样但底层的边缘、纹理特征是可以通用的。做法是保留前面的卷积层只替换最后的全连接层让模型输出 10 类。import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 10)weightsmodels.ResNet18_Weights.IMAGENET1K_V1表示加载在 ImageNet-1K 上训练过的权重首次运行会自动下载大约 45MB。model.fc.in_features是 ResNet18 最后一层全连接的输入维度也就是 512我们不需要硬编码用in_features自适应读取就好。替换fc层后模型的冻结情况暂时不用管直接整套网络用较小学习率微调。6.2 分阶段微调冻结卷积层避免灾难性遗忘直接对整个网络做训练也可以但我更推荐分阶段微调。第一阶段冻结所有卷积层只训练新替换的fc层让分类头先适应 CIFAR-10 的类别分布。第二阶段解冻部分深层卷积层用很小的学习率微调让高层特征也能贴合新数据。for name, param in model.named_parameters(): if name.startswith(fc): param.requires_grad True else: param.requires_grad False optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr0.0005)requires_grad设置为 False 后反向传播会跳过这些参数更新时也只更新fc层的权重。这一点比手动传一个参数子集更可靠因为filter配合requires_grad能确保你不会漏掉某些应该更新的层。训练 5 个 epoch 后再把所有层解冻学习率降到1e-4继续训练。6.3 验证与保存用 BatchNorm 陷阱检验最终效果微调完成后验证阶段一个常见的坑是BatchNorm层的统计量没有跟上。因为预训练模型的 BN 层是在 ImageNet 数据上统计的均值方差如果你只训练了fc层BN 层没更新测试时输入的分布和模型期望的分布会不一致导致准确率虚低。torch.save(model.state_dict(), resnet18_cifar10.pth)保存模型建议只存state_dict()不要直接torch.save(model)。前者只保存参数文件小、可复用后者连同整个模型结构和依赖一起打包后续加载时容易因为类定义位置变化而反序列化失败。加载时注意先实例化一个相同的 ResNet18 结构再load_state_dict如果报 missing key 或 unexpected key优先检查fc层的维度是否一致。这套迁移学习流程跑完测试集准确率能稳定到 90% 以上比从头训练提高了接近 10 个百分点。从那以后我每次做新的图像分类任务都会先写一个脚本把 baselines 跑出来先从头训练一个小模型拿到下限再加载预训练模型微调拿到上限之后所有优化都以这个上限为目标。这样不仅能看到每个改动带来的真实增量也避免了盲目堆网络结构。希望这套拆解过的流程能帮你少走些弯路。本文还有配套的精品资源点击获取