简介ResNet18_CIFAR10.rar 面向希望动手复现图像分类完整流程的 PyTorch 学习者与深度学习入门者围绕在 CIFAR10 十类数据集上训练 ResNet18 展开兼顾残差结构理解、数据预处理与训练调优等实践需求。压缩包共 26 个文件、约 341.16MB以 3 个 py 训练与推理脚本为核心配合预训练权重文件、CIFAR10 原始 batch 数据与压缩包、若干预测对比 jpg 图像及字体、缓存等辅助文件可直接对照脚本梳理模型定义、损失与优化器配置、学习率调度和精度监测的完整链路。已有 1391 人学习下载说明该案例在图像识别练手中具有一定参考价值。读者可据此复现训练与测试流程观察残差短路结构如何缓解梯度消失并借助保存的权重直接做推理验证也可在现有脚本上替换数据增强、优化器或轮数做对照实验形成可复用的实验记录与排错思路。1. 一份把图像识别链路跑通的 ResNet18 CIFAR-10 工程前阵子有个朋友拿着自己写的 PyTorch 训练脚本问我loss 打印出来是数字可准确率从头到尾卡在 10%一点不动。我看了十分钟问题出在标签和图片的顺序对不上——图像识别这行里最费时间的往往不是卷积核怎么算而是这些“跑起来了但没跑对”的环节。这份 ResNet18_CIFAR10 工程解决的正是这类问题用 PyTorch 实现完整的 ResNet18 残差网络在 CIFAR-10 十类数据集上从零训练数据下载、增强、训练、验证、权重保存、单张推理全部打通。刚学完卷积、想找一个能真正跑出 90% 以上准确率的深度学习工程练手的人适合拿它手里有自己小数据集、想拿它当迁移学习起点的人更适合因为改数据管线和分类头这两个地方就够了。2. 工程骨架与数据管线解压之后先看目录再谈模型很多人拿到压缩包第一反应是pip install然后直接python main.py跑不起来再回头翻代码这是最容易翻车的顺序。我的习惯是先花五分钟看目录分层搞清楚入口在哪、配置在哪、权重落在哪个文件夹再决定装什么依赖。这份工程的目录结构不复杂但分层很清晰看得出作者是按“以后还要改”的思路组织的不是把所有东西塞进一个文件里。ResNet18_CIFAR10/ ├── main.py # 训练入口解析参数、串联各模块 ├── predict.py # 单张/批量推理入口 ├── config.py # 默认超参、路径常量、类别名 ├── dataset/ │ └── cifar10.py # 数据下载、增强、DataLoader 构造 ├── models/ │ └── resnet18.py # BasicBlock 与 ResNet18 定义 ├── utils/ │ ├── trainer.py # 单 epoch 训练、验证、指标记录 │ ├── meters.py # 平均损失/准确率累加器 │ └── seed.py # 随机种子固定 ├── checkpoints/ # best.pth / last.pth 落盘位置 └── data/ # CIFAR-10 解压目录三层职责划分得很干脆main.py和predict.py是入口层只负责参数解析和流程编排不写任何算法逻辑models/和dataset/是算法层模型定义和数据变换都在这里改网络结构只动resnet18.pyutils/是工具层训练循环、指标统计、种子固定这些和具体任务无关的东西放这边。这种分法有个直接好处——你之后想把 CIFAR-10 换成自己的数据集只需要改dataset/和config.py里的类别数模型文件一行不用碰。2.1 数据管线CIFAR-10 从 .gz 到张量的四道加工CIFAR-10 是 32×32 的三通道彩色图训练集 50000 张、测试集 10000 张、10 个类别。工程里用torchvision.datasets.CIFAR10直接下载第一次运行会在data/下生成cifar-10-batches-py目录之后自动走缓存。数据增强这块是准确率能不能上 94% 的分水岭工程里的做法是训练集做随机裁剪加水平翻转测试集只做归一化。# dataset/cifar10.py from torchvision import transforms from torchvision.datasets import CIFAR10 from torch.utils.data import DataLoader, Subset import torch # CIFAR-10 三个通道的均值和标准差按训练集统计得来 CIFAR_MEAN (0.4914, 0.4822, 0.4465) CIFAR_STD (0.2470, 0.2435, 0.2616) train_tf transforms.Compose([ transforms.RandomCrop(32, padding4), # 四周补 4 像素零再随机裁回 32 transforms.RandomHorizontalFlip(p0.5), # 一半概率水平翻转 transforms.ToTensor(), # HWC uint8 - CHW float[0,1] transforms.Normalize(CIFAR_MEAN, CIFAR_STD), ]) eval_tf transforms.Compose([ transforms.ToTensor(), transforms.Normalize(CIFAR_MEAN, CIFAR_STD), # 验证/测试只归一化不增强 ])RandomCrop(32, padding4)这个组合不是随便写的CIFAR 原图只有 32×32如果不先补零直接裁就只能裁出更小的图再缩放回去细节会糊。先补 4 圈零得到 40×40再随机裁回 32×32等于让模型在训练时见到目标出现在画面不同位置的样本测试时目标居中也能认。Normalize的均值和标准差必须和训练集统计值一致验证集、测试集、线上推理三处都要用同一组数这是后面避坑章要专门讲的一个点。划分验证集的时候有个容易忽略的细节训练集和验证集得用同一批索引但走不同的 transform。工程里的写法是构造两份 dataset 对象一份挂train_tf、一份挂eval_tf然后用同一个随机排列切索引。def build_loaders(data_root, batch_size, num_workers, val_ratio0.1, seed42): train_full CIFAR10(rootdata_root, trainTrue, downloadTrue, transformtrain_tf) val_full CIFAR10(rootdata_root, trainTrue, downloadTrue, transformeval_tf) val_size int(len(train_full) * val_ratio) # 50000 * 0.1 5000 g torch.Generator().manual_seed(seed) perm torch.randperm(len(train_full), generatorg).tolist() val_idx, train_idx perm[:val_size], perm[val_size:] train_set Subset(train_full, train_idx) # 45000 张带增强 val_set Subset(val_full, val_idx) # 5000 张不增强 train_loader DataLoader(train_set, batch_sizebatch_size, shuffleTrue, num_workersnum_workers, pin_memoryTrue, drop_lastTrue) val_loader DataLoader(val_set, batch_sizebatch_size, shuffleFalse, num_workersnum_workers, pin_memoryTrue) test_loader DataLoader( CIFAR10(rootdata_root, trainFalse, downloadTrue, transformeval_tf), batch_sizebatch_size, shuffleFalse, num_workersnum_workers, pin_memoryTrue) return train_loader, val_loader, test_loader几个参数值得单独说。drop_lastTrue只在训练集上开因为最后一个 batch 可能只剩几张图里面只要有 BatchNorm 层单张样本算出来的方差是 0会污染 running_mean 和 running_var训练后期表现为验证准确率莫名抖动。pin_memoryTrue配合 GPU 训练能减少 Host 到 Device 的拷贝等待但前提是机器有独立显卡纯 CPU 环境开着没坏处也没收益。num_workers在 Linux 上一般设成 CPU 核数的 1/4 到 1/2Windows 上必须配合if __name__ __main__:使用否则子进程会重复导入主模块直接报递归创建进程的错。2.2 环境准备与第一条训练命令依赖不多PyTorch 加 torchvision 就够其余都是标准库。Python 版本建议 3.9 以上PyTorch 用 1.13 到 2.x 都能跑只是 2.x 之后混合精度的 API 位置有变化代码里做了兼容判断。python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install torch torchvision pip install pillow tqdm装完先跑一个最小规模的冒烟测试确认环境和数据管线没问题再上全量训练。这一步能省掉大量“训练到第 20 个 epoch 才发现路径错了”的时间。# 只跑 2 个 epoch、用小 batch验证整条链路能走通 python main.py --data-root ./data --epochs 2 --batch-size 64 --lr 0.01 # 正式训练60 epochbatch 128初始学习率 0.1开混合精度 python main.py --data-root ./data --epochs 60 --batch-size 128 --lr 0.1 --amp单卡 2080Ti 级别的卡跑完 60 个 epoch 大概两小时出头开混合精度能压到一小时半。ResNet18 在 CIFAR-10 上的常规水平是 94%95% 测试准确率如果跑完只有 90% 左右多半是学习率调度或者数据增强的细节没调对不是模型本身的问题。2.3 配置入口argparse 里哪几个参数真正常改入口参数集中在main.py顶部用argparse定义。看起来参数不少实际调参时高频改的只有五六个其余基本保持默认。# main.py 参数定义节选 import argparse def get_args(): p argparse.ArgumentParser(descriptionResNet18 on CIFAR-10) p.add_argument(--data-root, typestr, default./data) p.add_argument(--ckpt-dir, typestr, default./checkpoints) p.add_argument(--epochs, typeint, default60) p.add_argument(--batch-size, typeint, default128) p.add_argument(--lr, typefloat, default0.1) # SGD 初始学习率 p.add_argument(--weight-decay,typefloat, default5e-4) # L2 正则 p.add_argument(--momentum, typefloat, default0.9) p.add_argument(--num-workers, typeint, default4) p.add_argument(--amp, actionstore_true) # 开启混合精度 p.add_argument(--resume, typestr, defaultNone) # 断点续训路径 p.add_argument(--seed, typeint, default42) return p.parse_args()参数默认值什么时候需要改--batch-size128显存不够降到 64同时把 lr 按比例降到 0.05--lr0.1换优化器或换数据集时必须重调Adam 场景下 0.001 起步--weight-decay5e-4过拟合明显时加到 1e-3欠拟合时降到 1e-4--num-workers4数据加载成为瓶颈时往上加Windows 下先设 0 验证--amp关闭显卡支持 Tensor Core 时打开省显存也提速--resumeNone训练被中断后指向checkpoints/last.pth续跑--lr和--batch-size得一起看这是血泪经验batch 从 128 降到 32学习率还保持 0.1前几个 epoch 的 loss 会直接飞掉。线性缩放规则在这里基本成立batch 减半学习率也减半。--resume这个参数在长时间训练里是后悔药没有它机器一断电六十个 epoch 白跑。3. ResNet18 主干改造把 ImageNet 的骨架接到 10 类任务上原始 ResNet18 是为 ImageNet 的 224×224 输入和 1000 类输出设计的直接搬到 CIFAR-10 的 32×32 上会出问题——第一次 7×7 卷积 stride 2 加 3×3 最大池化两下就把 32×32 降成 8×8后面四个 stage 再降三次特征图只剩 1×1空间信息基本被压没了。这份工程做了两处关键改造把 stem 换成 3×3 stride 1 的卷积并去掉最大池化把分类头从 1000 类换成 10 类。改完之后参数量约 11.17M比原版小一点因为全连接层从 51 万参数降到了 5 千。3.1 BasicBlock残差连接到底把什么加了回去ResNet18 由 4 个 stage、每个 stage 2 个 BasicBlock 组成总共 8 个 block、16 个卷积层加上 stem 的卷积和最后的全连接正好 18 层带权重。BasicBlock 的核心是那条 shortcut输入 x 经过两个卷积层得到 F(x)再和原始输入 x 相加输出 ReLU(F(x) x)。这条路径让梯度在反向传播时有一条不经过卷积权重的高速通道深层网络的退化问题因此缓解。# models/resnet18.py import torch.nn as nn class BasicBlock(nn.Module): expansion 1 # 输出通道相对输入通道的倍数18/34 用 150 以上用 4 def __init__(self, in_ch, out_ch, stride1, downsampleNone): super().__init__() # 第一个 3x3 卷积负责降采样stride2 时 self.conv1 nn.Conv2d(in_ch, out_ch, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_ch) self.relu nn.ReLU(inplaceTrue) # 第二个 3x3 卷积保持分辨率不变 self.conv2 nn.Conv2d(out_ch, out_ch, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_ch) # 通道数或分辨率变化时shortcut 需要用 1x1 卷积对齐维度 self.downsample downsample def forward(self, x): identity x out self.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) if self.downsample is not None: identity self.downsample(x) # 维度对齐否则无法相加 out out identity # 残差相加必须在第二个 BN 之后、ReLU 之前 return self.relu(out)相加的位置是有讲究的必须在第二个 BatchNorm 之后、ReLU 之前。如果放在 ReLU 之后等于把恒等映射也过了一遍非线性shortcut 就不再是恒等映射梯度高速通道的效果会打折扣。biasFalse是因为卷积后面紧接 BatchNormBN 自带可学习的偏移参数卷积再加 bias 属于冗余参数白白多占显存。downsample只在每个 stage 的第一个 block 传入用 1×1 卷积加 stride 把通道数和分辨率对齐其余 block 的downsample是 None走纯恒等映射。3.2 CIFAR 版 stem7×7 卷积换成 3×3 之后少掉了什么原版 ResNet 的 stem 是Conv2d(3, 64, 7, stride2, padding3)接MaxPool2d(3, stride2, padding1)输入 224 出来是 56×56。CIFAR 只有 32×32如果照搬这套输出直接变成 8×8后面四个 stage 再各降一次最后只剩 1×1AdaptiveAvgPool 等于没起作用。所以工程里的 stem 改成Conv2d(3, 64, 3, stride1, padding1)并且整个去掉最大池化32×32 进、32×32 出四个 stage 分别输出 32、16、8、4最后自适应池化到 1×1空间信息保留得刚好。class ResNet18(nn.Module): def __init__(self, num_classes10): super().__init__() self.in_ch 64 # CIFAR 版 stem3x3 卷积stride1不做最大池化 self.conv1 nn.Conv2d(3, 64, kernel_size3, stride1, padding1, biasFalse) self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) self.layer1 self._make_layer(64, 2, stride1) # 32x32 self.layer2 self._make_layer(128, 2, stride2) # 16x16 self.layer3 self._make_layer(256, 2, stride2) # 8x8 self.layer4 self._make_layer(512, 2, stride2) # 4x4 self.avgpool nn.AdaptiveAvgPool2d(1) # 任意输入都能池到 1x1 self.fc nn.Linear(512 * BasicBlock.expansion, num_classes) def _make_layer(self, out_ch, num_blocks, stride): downsample None if stride ! 1 or self.in_ch ! out_ch: # 1x1 卷积对齐通道stride 与主分支保持一致 downsample nn.Sequential( nn.Conv2d(self.in_ch, out_ch, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_ch), ) blocks [BasicBlock(self.in_ch, out_ch, stride, downsample)] self.in_ch out_ch for _ in range(1, num_blocks): blocks.append(BasicBlock(self.in_ch, out_ch)) # 后续 block 不降采样 return nn.Sequential(*blocks)_make_layer这个函数负责建一个 stage逻辑是第一个 block 可能带 stride 和 downsample后一个 block 保持维度不变。self.in_ch在每层结束后更新供下一层判断是否需要 downsample。这个写法比把四个 stage 手写四遍干净得多也方便你后面把num_blocks改成[2,2,2,2]之外的配置比如升到 ResNet34 只要改成[3,4,6,3]。3.3 分类头与权重初始化AdaptiveAvgPool2d(1)把 512×4×4 压成 512×1×1再拉平接全连接层输出 10 个 logits。这里不用固定尺寸的AvgPool2d(4)是因为自适应池化对输入尺寸不敏感你之后把输入改成 64×64 或者做多尺度测试模型不用改。def forward(self, x): x self.relu(self.bn1(self.conv1(x))) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) # [N, 512, 1, 1] - [N, 512] return self.fc(x)初始化这部分工程里对卷积层用 Kaiming 正态初始化对 BN 层把权重设成 1、偏置设成 0全连接层用较小的正态分布。从零训练时初始化影响很大尤其是带残差的结构初始化不当会让前面几个 epoch 的 loss 剧烈震荡。def init_weights(m): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, modefan_out, nonlinearityrelu) elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) nn.init.constant_(m.bias, 0) model ResNet18(num_classes10) model.apply(init_weights)fan_out模式是按输出通道数算缩放因子配合 ReLU 使用如果网络里换成 LeakyReLU 或 SiLUnonlinearity参数也要跟着改否则方差估计会偏。3.4 参数量核对11.17M 是怎么来的跑起来之前先核一遍参数量能提前发现结构写错。原版 ResNet181000 类约 11.69M 参数这份 CIFAR 版把 stem 的 7×7 卷积换成 3×3省了约 7.7K 参数分类头从 512×1000 降到 512×10省了约 507K。一加一减最终落在 11.17M 左右。模块输出尺寸参数量约stem 3×3 卷积 BN64×32×321.9Klayer12 个 block64×32×32148Klayer22 个 block128×16×16525Klayer32 个 block256×8×82.1Mlayer42 个 block512×4×48.4M全连接 512→10105.1K合计—≈ 11.17M用一行代码就能验证sum(p.numel() for p in model.parameters())。如果打印出来是 11.69M说明 stem 没改或者分类头没换回__init__里对着看。如果差了整整一倍多半是_make_layer里in_ch没更新导致某个 stage 的通道数算错。4. 训练与推理链路一个 epoch 里到底发生了什么训练脚本的结构决定了后面调参方不方便。工程的utils/trainer.py把单个 epoch 的训练和验证拆成两个函数main.py里只留一个 epoch 循环负责调度、记录、存盘。这样拆的好处是你之后想加早停、加 TensorBoard、加梯度裁剪都只动main.py里那十几行不用碰训练核心逻辑。整个链路按“前向 → 算损失 → 清梯度 → 反向 → 更新参数 → 更新学习率”的顺序走顺序错了就是黑匣子现象奇怪但查不出原因。4.1 训练循环前向、损失、反向、更新的四步顺序# utils/trainer.py import torch def train_one_epoch(model, loader, criterion, optimizer, scheduler, scaler, device, use_amp): model.train() # 切训练模式BN 用 batch 统计量Dropout 生效 total_loss, total_correct, total_num 0.0, 0, 0 for images, labels in loader: images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) with torch.autocast(device_typecuda, enableduse_amp): logits model(images) # [N, 10] loss criterion(logits, labels) optimizer.zero_grad(set_to_noneTrue) # 清上一轮梯度 if use_amp: scaler.scale(loss).backward() # 损失放大后反向防梯度下溢 scaler.step(optimizer) scaler.update() else: loss.backward() optimizer.step() scheduler.step() # 按 step 更新的调度器放这里 # 用 item() 取出标量避免把计算图一直挂在显存里 total_loss loss.item() * images.size(0) total_correct (logits.argmax(1) labels).sum().item() total_num images.size(0) return total_loss / total_num, total_correct / total_num这段代码里有三个地方值得单独拎出来。optimizer.zero_grad(set_to_noneTrue)比默认的置零更省内存PyTorch 2.0 之后官方也推荐这个写法它把梯度张量直接设成 None反向时再重新分配避免了每个 batch 都做一次全量置零。loss.item()这个调用看着不起眼但漏掉它就是显存泄漏——total_loss loss会让每轮的损失张量都保留计算图几十个 batch 之后显存就爆了而且报错信息完全指不到这里。scheduler.step()的位置取决于调度器类型按 epoch 更新的放外层循环按 step 更新的放这里CosineAnnealingLR两种都支持混用会导致学习率曲线对不上预期。验证函数要包torch.no_grad()并且切model.eval()这两步缺一不可。torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() # BN 用 running 统计量Dropout 关闭 total_loss, total_correct, total_num 0.0, 0, 0 for images, labels in loader: images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) logits model(images) loss criterion(logits, labels) total_loss loss.item() * images.size(0) total_correct (logits.argmax(1) labels).sum().item() total_num images.size(0) return total_loss / total_num, total_correct / total_num漏掉model.eval()的现象很典型训练时验证准确率看着还行一换到测试集就掉三四个点原因是 BatchNorm 还在用当前 batch 的统计量batch 小的时候方差估计噪声很大。4.2 优化器与学习率调度SGD cosine 的搭配CIFAR-10 上从零训练 ResNet18SGD 加动量还是最稳的组合Adam 系列收敛快但最终准确率通常低零点几个点除非你把 weight decay 单独调。工程里用 SGD动量 0.9weight decay 5e-4配上余弦退火把学习率从 0.1 平滑降到接近 0。import torch.optim as optim criterion nn.CrossEntropyLoss(label_smoothing0.1) # 轻微标签平滑抑制过拟合 optimizer optim.SGD(model.parameters(), lrargs.lr, momentumargs.momentum, weight_decayargs.weight_decay, nesterovTrue) # 按 epoch 更新的余弦退火T_max 等于总 epoch 数 scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxargs.epochs, eta_min1e-5) scaler torch.amp.GradScaler(cuda, enabledargs.amp) # PyTorch 2.x 写法label_smoothing0.1是个便宜又有效的正则把 one-hot 标签从 [0,0,1,0...] 变成 [0.01,0.01,0.91,...]模型不会对单个样本过度自信测试集准确率一般能涨 0.3 到 0.5 个点。nesterovTrue在动量基础上再做一次前瞻修正收敛曲线通常更平滑。eta_min1e-5保证最后几个 epoch 学习率不会真的降到 0否则末尾几轮等于白跑。混合精度的GradScaler在 PyTorch 2.x 里从torch.cuda.amp挪到了torch.amp如果你用的还是 1.13把导入路径换回去就行功能一样。4.3 验证与权重落盘什么时候存 best存权重的策略直接影响你后面能不能挑到好模型。工程里同时存两份last.pth每个 epoch 覆盖用于断点续训best.pth只在验证准确率创新高时写入用于最终推理。best_acc 0.0 for epoch in range(start_epoch, args.epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, scheduler, scaler, device, args.amp) val_loss, val_acc evaluate(model, val_loader, criterion, device) state { epoch: epoch, model: model.state_dict(), optimizer: optimizer.state_dict(), scheduler: scheduler.state_dict(), best_acc: best_acc, } torch.save(state, os.path.join(args.ckpt_dir, last.pth)) # 每轮覆盖续训用 if val_acc best_acc: best_acc val_acc torch.save(state, os.path.join(args.ckpt_dir, best.pth)) # 只存最好的 print(f[epoch {epoch}] val_acc - {val_acc:.4f}, saved best)存optimizer.state_dict()和scheduler.state_dict()是断点续训能真正接上的关键。很多工程只存模型权重续训时优化器的动量缓冲全丢学习率也从初始值重新开始曲线会出现明显的断层。best_acc一起存进去续训时能接着比较不会把之前的好模型覆盖掉。4.4 单张图推理从 PIL 到 logits 的完整链路推理脚本和训练脚本最容易脱节的地方是预处理。训练时用了归一化推理时忘了模型输出会完全乱掉而且不报错只是预测结果离谱。# predict.py from PIL import Image import torch from torchvision import transforms CIFAR_CLASSES [airplane,automobile,bird,cat,deer, dog,frog,horse,ship,truck] def predict_one(img_path, ckpt_path, devicecuda): ckpt torch.load(ckpt_path, map_locationdevice) model ResNet18(num_classes10).to(device) model.load_state_dict(ckpt[model]) model.eval() tf transforms.Compose([ transforms.Resize((32, 32)), # 非 32x32 的图先缩放到训练尺寸 transforms.ToTensor(), transforms.Normalize(CIFAR_MEAN, CIFAR_STD), # 必须和训练时一致 ]) img Image.open(img_path).convert(RGB) # 强制三通道兼容灰度图 x tf(img).unsqueeze(0).to(device) # [3,32,32] - [1,3,32,32] with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1)[0] conf, idx prob.max(0) return CIFAR_CLASSES[idx.item()], conf.item()convert(RGB)这行不能省用户丢进来的图可能是 PNG 带 alpha 通道、可能是灰度图、可能是 CMYK统一转 RGB 之后再走 transform能挡掉一大类形状不匹配的报错。Resize((32,32))会拉伸长宽比对分类影响不大如果在意形变可以换成CenterCrop加 padding 的组合。unsqueeze(0)补的是 batch 维度模型 forward 里所有 BN 都假设输入是四维的少这一维会直接报维度错误。5. 避坑与排查准确率卡 10%、显存爆掉、结果复现不了的三类现场前四个 epoch 跑得顺不代表整条链路是对的。ResNet18 加 CIFAR-10 这套组合太经典网上能抄的代码一大把但每份代码里都埋着几个只在特定条件下才炸的坑。下面这几条是我在拆这类工程时反复遇到的现象、原因、解决办法按顺序写遇到问题时可以直接对号入座。5.1 准确率从第一个 epoch 到最后一个 epoch 都是 10%现象loss 有数值也在一缓慢下降但验证准确率始终在 10% 上下正好是十类随机猜的概率。原因通常有三个。第一个是标签和图片没对齐Subset切索引时训练集用了一份排列、验证集用了另一份或者自定义 Dataset 里__getitem__返回的顺序写反了。第二个是归一化用了 ImageNet 的均值方差0.485/0.456/0.406和 CIFAR 的统计分布差得远特征被压得几乎没有区分度。第三个最隐蔽DataLoader的shuffleTrue开在了验证集上同时又只取了前几百个 batch 算指标那边界样本刚好全是一个类。解决先固定一个小 batch 做过拟合测试取 32 张图反复训练 200 步如果训练准确率上不到 100%说明模型或损失函数有问题跟数据量无关。再把Normalize的六个数字打印出来核对确认训练、验证、推理三处完全一致。最后检查Subset的索引来源训练和验证必须来自同一次randperm只是切片位置不同。5.2 训练到一半显存爆掉报 CUDA out of memory现象前十几个 epoch 正常显存占用缓慢上涨某个 epoch 突然 OOM报错堆栈指向loss.backward()。原因基本是累加损失时保留了计算图。total_loss loss这个写法会把每个 batch 的损失张量连同它的计算图一起挂在一个 Python 浮点数上虽然数值上没问题但图不会被释放显存就一路涨。另一个可能是验证阶段忘了加torch.no_grad()验证集的图也被保留下来。解决所有累加都用loss.item()取标量验证函数加上torch.no_grad()装饰器。如果还爆把batch_size降一半同时按比例降学习率。开混合精度也能省三到四成显存但要注意GradScaler的step和update顺序不能反。5.3 同样的代码跑两次结果差好几个点现象不改任何代码两次训练的最终准确率能差 1 到 2 个点有时候连收敛速度都不一样。原因是没有固定随机种子或者只固定了torch.manual_seed漏掉了 NumPy、Python 内置 random 和 cuDNN 的非确定性算子。DataLoader开多进程时每个 worker 的种子是独立派生的主进程固定了也没用。解决在入口最前面统一固定并且给 worker 写一个worker_init_fn。import random, numpy as np, torch, os def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True # 强制确定性算法 torch.backends.cudnn.benchmark False # 关闭自动选最快算法 os.environ[PYTHONHASHSEED] str(seed) def worker_init_fn(worker_id): # 每个 worker 用主种子派生出的独立种子 np.random.seed(42 worker_id) random.seed(42 worker_id)cudnn.deterministicTrue会让训练慢 10% 左右换来的是可复现调试阶段值得开最终冲指标时可以关掉。5.4 Windows 下 num_workers 大于 0 直接报错现象Linux 上跑得好好的换到 Windows 上num_workers4就抛RuntimeError提示不能递归创建进程或者__main__模块被重复导入。原因Windows 用 spawn 方式启动子进程子进程会重新导入主模块如果DataLoader的构造写在模块顶层而没有if __name__ __main__:保护就会无限递归。解决把训练入口全部收进main()函数模块底部写if __name__ __main__: main()。调试阶段先把--num-workers设成 0确认逻辑没问题再往上加。5.5 验证集准确率比测试集高出一大截现象验证集能到 95%测试集只有 92%中间差了三个点。原因验证集是从训练集里切出来的虽然索引不重叠但和训练集来自同一批采集条件分布更接近测试集的分布略有偏移。另一个更常见的原因是验证集错误地用了训练时的增强 transform随机裁剪让验证指标虚高而且每次评估结果都在抖。解决验证和测试统一走eval_tf只做ToTensor加Normalize。评估时shuffleFalse保证每次跑出来的数字可比。如果确认 transform 没问题差距还在那就是过拟合加 weight decay 或者把label_smoothing从 0.1 提到 0.15。6. 二次开发换数据集、换主干、导模型的三条改造路径拿到这份工程之后真正有价值的改造方向是把它变成你自己任务的起点。最常见的需求是换数据集操作集中在三个地方config.py里的类别列表、ResNet18(num_classesN)的构造参数、dataset/下的数据加载逻辑。改完先别急着全量训练拿几百张图跑一次过拟合测试损失能降到接近 0 再放开这一步能挡掉八成数据管线的问题。想用 ImageNet 预训练权重加速收敛会撞上一个形状不匹配的坑官方torchvision的 ResNet18 是 7×7 stride 2 的 stemCIFAR 版是 3×3 stride 1load_state_dict直接报 size mismatch。实用的做法是按层过滤形状一致才加载。import torch, torchvision.models as tvm def load_imagenet_partial(model): backbone tvm.resnet18(weightstvm.ResNet18_Weights.IMAGENET1K_V1) src, dst backbone.state_dict(), model.state_dict() # 只保留 key 存在且形状一致的层stem 和 fc 会被自动跳过 matched {k: v for k, v in src.items() if k in dst and dst[k].shape v.shape} dst.update(matched) model.load_state_dict(dst) print(floaded {len(matched)}/{len(dst)} tensors) return model打印出来的匹配比例大概在 90% 左右stem 的第一层卷积和最后的全连接会被跳过这两部分本来也该重新训。加载完之后把初始学习率降到 0.01训 20 个 epoch 就能追上从零训 60 个 epoch 的水平。导出部署时ONNX 是通用性最好的选择。注意model.eval()和固定输入尺寸这两步动态轴可以留给 batch 维度。model.eval() dummy torch.randn(1, 3, 32, 32).to(device) torch.onnx.export( model, dummy, resnet18_cifar10.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version12, )导出后拿onnxruntime跑一遍和 PyTorch 输出的 logits 做逐元素对比误差在 1e-4 以内才算通过。改造方向改动位置验证方式换数据集config.py类别数、dataset/加载逻辑500 张图过拟合loss 应降到 0.01本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
ResNet18+CIFAR-10图像识别训练链路拆解与二次开发 简介:面向深度学习入门与进阶读者的 PyTorch 图像识别实践包,围绕 ResNet18 在 CIFAR10 上的训练与评估展开,适合想理解残差结构、数据预处理、模型保存加载及分类预测流程的学习者。压缩包共 26 个文件,约 341.16MB,包… · 2026/11/5 6:55:33
【LTE L3 笔记 01】LTE空口特点 课程来源:B站《【2022】华为HCIA-LTE无线工程师认证学习中级视频》 视频地址:https://www.bilibili.com/video/BV1P3411F7XX/?p1 笔记为个人整理,含课件截图,仅供学习交流。 目录一、Uu 接口是什么二、空口上只跑两种东西三、空口… · 2026/11/5 6:54:33
嵌入式八股—C语言入门学习(一) 本章学习:C语言的运行机制、常见的库文件、代码框架、标识符的命名规则、C语言关键字、常量学习、输入输出函数、局部变量与全局变量一、C语言运行机制整个过程:编写.C文件、编译成.obj的二进制机器指令、结合C语言的库文件链接形成可执行的二进制.exe、… · 2026/11/5 6:54:33
ADS122U04+PIC18F46K42:高精度PT1000测温模块设计与实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:36:44
基于ADS122U04IPWR与STM32F765ZI的工业及HVAC高精度温度测量方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:36:44
ADS122U04+PIC32MZ:HVAC高精度温度采集系统设计与实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:36:44
ADS122U04IPWR+STM32F031C6工业温度测量方案解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:36:44
基于ADS122U04与PIC32MZ的高精度温度采集系统设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:36:44
ADS122U04与STM32F410RB工业温度采集方案:UART接口ADC实战详解 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 7:35:44
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38