
简介面向深度学习初学者和图像分类任务开发者该压缩包提供了一份基于ResNet实现CIFAR-10分类的完整算法工程。代码分为模型定义、数据加载、训练与推理四个Python脚本并附有说明文档共五个文件体积仅三KB结构精简、无额外依赖便于快速阅读和二次修改。当前已有七百余人浏览学习。实现中重点涵盖了残差块的跳跃连接、批量归一化、全局平均池化、交叉熵损失以及带动量的随机梯度下降优化器并引入了随机裁剪、水平翻转等数据增强手段。读者可以对照训练脚本直观理解ResNet如何通过恒等映射缓解深层网络的梯度消失问题并掌握针对三十二乘三十二像素小尺寸图像的分类管线设计方法。适合作为高校深度学习课程作业、竞赛入门或论文复现的参考代码。1. ResNet CIFAR10一个小数据集上的经典分类实战做深度学习图像分类的人迟早都会跟 CIFAR10 打交道。这个数据集只有 10 个类别、6 万张 32×32 的小图但想在它上面把准确率推到 90% 以上并不是随便叠几层卷积就能做到的事。这份资源不是简单的“调库跑通”而是一个从数据加载、模型搭建到训练调参都完整落地的 ResNet 实现——你拿到的是一整个能跑出结果的算法工程而不是一段粘贴即用的碎片代码。它适合两类人一类是刚学完卷积神经网络、想看看残差连接到底怎么解决网络退化问题的新手另一类是想在自己的分类任务里快速迁移 ResNet 结构的开发者。下文会按“它怎么设计 → 核心代码长什么样 → 怎么跑起来 → 踩了哪些坑 → 怎么改成自己的模型”的顺序把这份源码逐层拆开。2. 项目架构与数据流从 32×32 输入到 10 类输出2.1 目录结构一览模型、训练、数据三块分离拿到压缩包解压之后先别急着跑花两分钟把目录结构过一遍。这个项目的组织方式是典型的“单脚本训练 模块化模型定义”比把几百行代码塞进一个 main.py 要清晰得多。resnet-cifar10/ ├── model/ │ ├── resnet.py # ResNet 网络结构定义 │ └── __init__.py ├── data/ │ ├── dataset.py # CIFAR10 数据加载与增强 │ └── __init__.py ├── train.py # 训练主脚本 ├── test.py # 测试与评估脚本 ├── config.py # 全局超参数配置 └── checkpoints/ # 模型权重保存目录运行时生成我一般拿到别人的项目会先看 config.py因为超参数往往是整个工程最“玄学”的部分。这个项目的 config.py 里定义了初始学习率、batch size、训练轮数、权重衰减系数还有数据增强开关。数据部分独立成模块意味着你后续想换成自己的数据集只需要改动 dataset.py 里的数据加载逻辑模型和训练脚本都可以不动。2.2 数据加载与增强CIFAR10 的预处理细节CIFAR10 的图片分辨率只有 32×32这意味着不能直接套用 ImageNet 那套 224×224 的预处理流程。dataset.py 里做了几个关键操作先将 PIL 图像转为 Tensor再做标准化三个通道的均值是 (0.4914, 0.4822, 0.4465)标准差是 (0.2023, 0.1994, 0.2010)。这一组数值是 CIFAR10 数据集的全局统计量是官方计算好的直接拿来用就行不要自己重新算否则会引入不必要的偏差。# data/dataset.py 核心代码 transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize( mean[0.4914, 0.4822, 0.4465], std[0.2023, 0.1994, 0.2010] ) ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize( mean[0.4914, 0.4822, 0.4465], std[0.2023, 0.1994, 0.2010] ) ])这段代码里训练集和测试集的预处理差异是刻意设计的训练集做了 RandomCrop 和 RandomHorizontalFlip属于数据增强目的是让模型在有限的 5 万张训练图上学到更鲁棒的特征测试集只做标准化不做任何随机变换保证评估结果的可复现性。RandomCrop 的 padding4 表示在图片四周填充 4 个像素后再随机裁剪回 32×32这是 CIFAR10 上最经典也最有效的增强手段几乎没有之一。2.3 训练主循环学习率调度与梯度更新train.py 里的训练循环写得比较规矩每个 epoch 遍历训练集计算损失、反向传播、更新权重然后每个 epoch 结束在验证集上评估一次。值得留意的是学习率调度策略这个项目用了余弦退火CosineAnnealingLR初始学习率 0.1随着训练轮数逐步衰减到接近 0。相比固定学习率余弦退火在 CIFAR10 上通常能多涨 12 个点的准确率属于性价比很高的改动。# train.py 训练核心逻辑 for epoch in range(config.epochs): model.train() for batch_idx, (inputs, targets) in enumerate(trainloader): inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 结束后在测试集上评估 accuracy evaluate(model, testloader, device) print(fEpoch {epoch1}/{config.epochs}, Acc: {accuracy:.2f}%)这里的 device 变量通过config.device控制支持 GPU 和 CPU 两种模式。如果机器上没有 CUDA代码会自动回退到 CPU 训练但 200 个 epoch 跑下来会比较慢建议还是在有 GPU 的环境下运行。criterion用的是 CrossEntropyLoss这是多分类任务的标准损失函数它内部已经包含了 Softmax 操作所以模型最后一层不需要额外加 Softmax。3. 核心算法拆解ResNet 的残差连接与瓶颈块实现3.1 BasicBlock两层卷积的残差学习单元这个项目采用的是 ResNet 的 CIFAR 版本变体网络结构比 ImageNet 版本要轻量得多。核心组件是 BasicBlock由两个 3×3 卷积组成中间夹一个 BatchNorm 和 ReLU最后有一条恒等映射shortcut把输入直接加到输出上。残差连接的意义在于当网络层数加深时恒等映射提供了一个“高速公路”让梯度可以无损回传从而缓解梯度消失问题。# model/resnet.py BasicBlock 实现 class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super(BasicBlock, self).__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # 输入输出通道不一致时用 1x1 卷积对齐维度 self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): residual x out torch.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(residual) out torch.relu(out) return out这里的biasFalse是 BatchNorm 的搭配惯例——卷积层后面紧跟 BatchNorm 时偏置项会被 BN 层归一化掉留着只会增加参数冗余。shortcut 分支的 1×1 卷积用于两种场景一是 stride2 时特征图尺寸减半需要 1×1 卷积同时降采样并对齐通道数二是通道数翻倍时需要 1×1 卷积把恒等映射的通道数扩到和主分支一致否则两个张量无法相加。3.2 网络组装堆叠 BasicBlock 构建深度模型整个 ResNet 的组装逻辑在于各阶段的下采样。CIFAR 版本的 ResNet 输入层通常先用一个 3×3 卷积而不是 ImageNet 的 7×7MaxPool因为 32×32 的输入分辨率太小大卷积核会过早缩小特征图。项目里默认的是 ResNet20结构是一层初始卷积 三层 BasicBlock 组每组分别堆叠 3、3、3 个块通道数从 16 开始逐阶段翻倍为 16→32→64。# model/resnet.py ResNet 主体结构 class ResNet(nn.Module): def __init__(self, block, num_blocks, num_classes10): super(ResNet, self).__init__() self.in_channels 16 self.conv1 nn.Conv2d(3, 16, kernel_size3, stride1, padding1, biasFalse) self.bn1 nn.BatchNorm2d(16) self.layer1 self._make_layer(block, 16, num_blocks[0], stride1) self.layer2 self._make_layer(block, 32, num_blocks[1], stride2) self.layer3 self._make_layer(block, 64, num_blocks[2], stride2) self.avg_pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(64, num_classes) def _make_layer(self, block, out_channels, num_blocks, stride): strides [stride] [1] * (num_blocks - 1) layers [] for s in strides: layers.append(block(self.in_channels, out_channels, strides)) self.in_channels out_channels return nn.Sequential(*layers)_make_layer这个函数有一个容易看懵的细节每组第一个 block 的 stride 负责下采样后面的 block 保持 stride1。比如 layer2 传入 stride2那么这一层第一个 BasicBlock 的卷积 stride2特征图从 16×16 降到 8×8同时 shortcut 也要做一次 stride2 的 1×1 卷积来匹配尺寸。后面的两个 block 维持 8×8不断提取更深层的语义特征。3.3 参数量对比为什么 CIFAR 版本要轻如果你拿这个网络和标准的 ResNet50 对比会发现参数量差了一个数量级。ResNet50 的参数大约 2500 万而这里的 ResNet20 只有约 27 万参数。原因是 CIFAR10 的图只有 32×32信息量远小于 ImageNet 的 224×224把网络做得过深反而会过拟合。实际经验是ResNet20 在 CIFAR10 上就能达到 92% 左右的准确率ResNet32 能到 93%再往上加到 ResNet56 收益就开始边际递减了训练时间却成倍增长。网络尾部用了AdaptiveAvgPool2d((1, 1))而不是传统的 FlattenMaxPool这样不管输入分辨率怎么变全连接层的输入维度始终是 64。这个设计在你迁移到其他尺寸的数据集时非常有用——比如你想改成 48×48 的输入网络结构不用动只需要改数据加载部分。4. 运行与调参指南一次完整的训练流程复现4.1 环境准备与依赖安装在跑这个项目之前先确认环境里有 Python 3.8 以上的解释器以及 PyTorch 和 torchvision。我推荐用 conda 创建独立环境避免把系统 Python 搞乱。安装命令如下# 创建虚拟环境并安装依赖 conda create -n resnet-cifar10 python3.9 conda activate resnet-cifar10 pip install torch torchvision # 如果需要 GPU 版本按官网命令安装 pip install numpy tqdm matplotlib如果你的机器有 NVIDIA GPU建议安装 CUDA 版的 PyTorch训练速度会快 2030 倍。没有 GPU 也没关系这个项目在网络和 batch size 上都做了 CPU 友好的设定只是 200 个 epoch 可能需要跑几个小时耐心等就好。安装完成之后在项目根目录执行python train.py脚本会自动下载 CIFAR10 数据集到本地首次运行需要联网约 170MB。4.2 超参数含义与调整建议config.py 里的超参数不是随手填的每个都对应一个训练效果的关键变量。下面这个表列了最核心的几个参数以及我的调参经验参数名默认值含义调整建议epochs200训练轮数少于 100 轮模型欠拟合多于 300 轮收益极小batch_size128单次前向传播的样本数显存不够时降到 64同时可适当降低学习率learning_rate0.1初始学习率batch size 减半时学习率也应减半weight_decay5e-4L2 正则化系数过拟合加重时调大到 1e-3momentum0.9SGD 动量一般不动0.9 是默认最优值这里有一个新手常犯的错误直接套用 ImageNet 分类的初始学习率 0.01。在 CIFAR10 ResNet 的组合下0.01 的初始学习率会导致收敛极其缓慢因为 batch size 小、梯度噪声大。项目默认的 0.1 配合带动量的 SGD前几个 epoch 就能看到损失显著下降。如果你想用 Adam 替代 SGD建议把学习率降到 1e-3因为 Adam 对学习率的敏感度更高。4.3 训练输出解读与断点恢复训练过程中每个 epoch 都会打印准确率和损失值。正常的训练曲线应该是这样的前 10 个 epoch 准确率快速从 10%随机猜测水平爬到 70% 左右之后增速放缓100 个 epoch 左右达到 90% 以上随后进入平台期靠余弦退火把最后的几个点“磨”出来。Epoch 1/200, Acc: 32.45%, Loss: 2.310 Epoch 10/200, Acc: 69.87%, Loss: 0.982 Epoch 50/200, Acc: 87.23%, Loss: 0.354 Epoch 100/200, Acc: 90.18%, Loss: 0.271 Epoch 200/200, Acc: 92.37%, Loss: 0.189如果你看到 Loss 一开始就不降或者波动剧烈多半是学习率问题。此外项目里还写了断点保存逻辑——每 20 个 epoch 保存一次 checkpoint存放在 checkpoints/ 目录下。中途中断后想继续训练可以用python train.py --resume加载最近的权重不用从头开始。5. 避坑指南训练 CIFAR10 时的五个常见问题5.1 准确率卡在 85% 上不去现象训练 Loss 正常下降但测试集准确率始终在 85% 左右徘徊超过 20 个 epoch 都没有突破。原因这一般是数据增强不足导致的过拟合。CIFAR10 的训练集只有 5 万张图如果只做 ToTensor 和 Normalize模型的泛化能力会比较差。解决确认 train.py 里确实用上了 RandomCrop 和 RandomHorizontalFlip。如果已经用了还卡住可以再加一个 Cutout随机遮挡一小块区域或者 MixUp 增强这两个技巧在 CIFAR10 上往往能再涨 1~2 个点。5.2 训练 Loss 变成 NaN现象Loss 在某个 epoch 突然变成 NaN之后所有参数都跟着变成 NaN。原因学习率太大导致梯度爆炸。0.1 的初始学习率配合 SGD 在大多数情况没事但如果你把 batch size 调小了比如 32而学习率没变梯度更新的方差会变大容易把权重推到数值溢出的区域。解决检查最近改了什么参数优先把学习率降到 0.05 或者 0.01 重试。另一个技巧是给模型加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5)。5.3 从零训练比官方准确率低了 3 个百分点现象训练完成后准确率在 89% 左右但论文或者 README 里写的 baseline 是 92%。原因这种差距绝大多数来自两个地方一是训练轮数不够论文里的 200 个 epoch 是有必要的二是学习率调度没有生效。很多简化项目的代码里学的是 step decay每隔 60 个 epoch 衰减一次而这个项目用的是 CosineAnnealingLR两者在同一网络上的差距能到 1~2 个点。解决确认scheduler.step()是在每个 epoch 结尾调用而不是在每次 batch 迭代里调用。用余弦退火时学习率是平滑下降的如果写在错误的位置调度器会在一个 epoch 内就把学习率耗尽模型后半程基本白学。5.4 测试集和训练集预处理不一致现象模型训练完测试准确率很高95%但实际部署到业务场景里表现很差或者跑 test.py 时结果异常高。原因测试阶段混入了数据增强。有的同学图省事在 transform_test 里也加上了 RandomCrop 或 RandomHorizontalFlip导致评估结果虚高。解决test.py 里的 transform 必须和 transform_test 一致只包含 ToTensor 和 Normalize。另外验证的时候用model.eval()关闭 dropout 和 BatchNorm 的更新否则 BN 层的统计量会乱掉。这是新手最容易忽略的细节。5.5 GPU 显存不足现象batch_size128 时 CUDA out of memory。原因如果你改过网络结构比如把 BasicBlock 的通道数从 16 翻到 32参数量和中间特征图的显存占用会成倍增长。解决先把 batch_size 降到 64 或 32 试试。如果还想保持较大 batch可以用torch.cuda.amp混合精度训练显存占用能降低约 40%。在这个项目里把model.half()和inputs.half()加上大部分 4GB 显存的卡都能跑起来。6. 二次开发把 CIFAR10 分类器改成你的私有数据分类工具6.1 替换数据集换数据不换模型做二次开发最常见的需求是把这个分类工程迁移到自己的数据集上。CIFAR10 是 10 类的 32×32 小图而你手头的数据可能是猫狗分类2 类、工业缺陷检测5 类或者某种特定场景的图片。改动其实很小主要集中在两个位置dataset.py 的数据加载函数和 model 最后一层的输出维度。# 修改数据集加载为自定义图片文件夹结构 # 目录结构data/train/class1/*.jpg, data/train/class2/*.jpg from torchvision import datasets train_dataset datasets.ImageFolder( rootdata/train, transformtransform_train ) test_dataset datasets.ImageFolder( rootdata/test, transformtransform_test )改完之后要注意两个连带问题一是num_classes要改成你自己的类别数比如猫狗分类就是 2只需要把ResNet(block, num_blocks, num_classes10)里的 10 改成 2二是全连接层的输入尺寸不需要改因为AdaptiveAvgPool2d已经把特征压缩成固定 64 维了。如果你的图片不是 32×32比如是 128×128就在 transform 里加一个transforms.Resize((32, 32))模型完全不用动。6.2 保存并复用模型从训练到推理训练好的模型不能只活在训练脚本里最后总归要导出成可部署的形式。常见的做法是直接保存整个模型状态字典然后在推理脚本里重新构建网络结构再加载权重。# 训练完成后保存 torch.save(model.state_dict(), resnet20_cifar10.pth) # 推理时加载 model ResNet(BasicBlock, [3, 3, 3], num_classes10) model.load_state_dict(torch.load(resnet20_cifar10.pth)) model.eval() # 单张图片预测 def predict(image_path): img Image.open(image_path).convert(RGB) img transform_test(img).unsqueeze(0) with torch.no_grad(): output model(img) pred output.argmax(dim1).item() return class_names[pred]这里model.eval()再强调一遍不是可选项。加载权重后如果不切换到 eval 模式BatchNorm 层的均值和方差还是会用训练时积累的统计量但 dropout 层的行为会保持训练模式导致推理结果不稳定。上述代码里no_grad()是另一个关键点它告诉 PyTorch 不要构建计算图节省内存且加速推理。6.3 换个更大的网络从 ResNet20 到 ResNet56当你觉得当前准确率还不够卷的时候增大模型容量是最直接的思路。项目里已经预留了这个扩展点——num_blocks参数就是用来控制深度的。把实例化代码从ResNet(BasicBlock, [3, 3, 3])改成ResNet(BasicBlock, [5, 5, 5])就是 ResNet32改成[9, 9, 9]就是 ResNet56。# 在 config.py 中增加网络深度配置 model ResNet(BasicBlock, [5, 5, 5], num_classes10) # ResNet32改网络深度时其余超参数不用变但训练时间会显著增加——ResNet32 大约是 ResNet20 的 1.6 倍。另外有个经验值网络越深weight_decay 的作用越明显。如果 ResNet56 的效果反而不如 ResNet32优先检查是不是过拟合了把 weight_decay 从 5e-4 调到 1e-3同时增加一点数据增强的强度。6.4 部署到 TorchScript 获取加速最后一个技巧是把训练好的模型转成 TorchScript这样可以在没有 Python 环境的 C 端部署或者通过 LibTorch 做推理加速。转换过程很简单但建议在 eval 模式下完成避免把 BN 层的训练行为固化进脚本。# 导出 TorchScript 模型 script_model torch.jit.script(model) script_model.save(resnet20_torchscript.pt)我一般会把这一步放在模型最终确定之后。有个血泪教训是在 model.train() 模式下导出模型之后部署时输出结果总是和验证集差一点排查了很久才发现是 BN 层的 running_mean 和 running_var 被冻结成了非最优状态。从那以后我每次导出模型都强制先走一遍 eval 模式再验证一整个测试集的准确率没掉点才放心。这份资源虽然针对的是 CIFAR10但它的代码结构和模块划分方式足够撑起你后面大半年的分类任务改造希望帮到你。本文还有配套的精品资源点击获取