资讯动态

ResNet图像分类实战:CIFAR10数据管线、训练配方与过拟合排查

发布时间:2026/10/26 2:05:04 来源:尧图企业网站定制
简介这份围绕ResNet实现CIFAR-10图像分类的算法实践包面向已掌握卷积网络基础、希望动手复现残差网络的中级学习者。内容聚焦残差块与跳跃连接、批量归一化、全局平均池化分类头以及SGD/交叉熵训练流程和数据增强、学习率调度等关键环节帮助读者把抽象理论落到可运行代码。压缩包共5个文件以4个Python脚本和1份Markdown说明为主体量约3KB按模型定义、数据加载、训练与推理拆分结构轻量便于逐模块阅读和二次修改。目前已有726人学习适合用作课程实验、课题复现或小数据集分类入门参考。读者可从中获得网络结构定义、训练主循环、推理评估与数据预处理等完整实现线索并借助说明文档快速梳理运行逻辑减少从零搭建时的试错成本。1. 一个训练集 99%、测试集 60% 的落差暴露了什么前段时间有人把一份 resnet 算法实现发给我看跑的是 CIFAR10 分类代码能跑通训练集准确率冲到 99%测试集却死活卡在 60% 上下。翻开源码看了十分钟问题不在网络结构上出在数据管线归一化用错了统计量随机裁剪的 padding 又写成了 0模型再深也白搭。这份资源做的事情其实很聚焦——用 ResNet 在 CIFAR10 上做十分类从数据加载、残差网络搭建、训练循环到精度评估是一条完整可复现的链路。它适合两类人一类是刚学完卷积、想找个能跑通的残差网络项目练手的新手照着改几行参数就能看到准确率变化另一类是已经会用 PyTorch、但没认真拆过 CIFAR 版 ResNet 和 ImageNet 版差异的熟手里面有关于 stem 结构、shortcut 投影、学习率调度的取舍值得逐行看一遍。下面按「结构长什么样 → 一个 batch 怎么流过 → 训练配方怎么调 → 哪里容易翻车 → 怎么改成自己的」这条线拆。2. 网络结构拆解CIFAR 版 ResNet 的 BasicBlock 与三阶段堆叠2.1 为什么 CIFAR 版砍掉了 7×7 卷积和 MaxPool刚接触的人常有一个疑问ImageNet 上那套 ResNet-50 的骨架直接搬过来跑 CIFAR10 行不行能跑但效果会打折。原版的第一层是 7×7、stride2 的卷积后面跟一个 3×3、stride2 的 MaxPool两步下来 224×224 的输入直接缩到 56×56缩了四倍。CIFAR10 的图只有 32×32同样两步下来只剩 8×8等进到第一个残差 stage空间信息已经被压得所剩无几后面再怎么堆 block 也补不回来。所以 CIFAR 版的标准做法是把 stem 简化成一层 3×3、stride1、padding1 的卷积通道数直接给 16不做任何池化。这样 32×32 进去还是 32×32三个 stage 分别输出 32×32、16×16、8×8下采样全部交给 stage 内部第一个 block 的 stride2 完成。这份资源的网络定义基本遵循这个思路我一般也会这么写因为它让「深度」和「感受野」解耦了你想加深网络只需要加 block不用担心一开始就把特征图压没了。参数量上差距更直观。一份 CIFAR 版 ResNet-20 大概 0.27M 参数跑一张 32×32 的图计算量很小单卡 8G 显存能开到 batch size 512 还绰绰有余而同样的 20 层如果按 ImageNet 骨架堆参数和显存都会翻好几倍对小数据集来说是纯浪费。2.2 工程目录与模块边界资源包解压后的结构大致是这样不同人写会有细节差异但模块边界基本固定resnet-cifar10/ ├── main.py # 入口参数解析 训练/评估分支 ├── models/ │ ├── __init__.py # 按名字创建 resnet20/32/44/56 │ └── resnet.py # BasicBlock ResNetCIFAR 主类 ├── data/ │ └── cifar10.py # Dataset 构建、增强、DataLoader 封装 ├── utils/ │ ├── train.py # 训练一个 epoch 的循环 │ ├── eval.py # 评估与指标累计 │ ├── lr_scheduler.py # warmup cosine / multistep │ └── checkpoint.py # 保存与恢复 └── configs/ └── resnet20.yaml # 超参集中在这里避免散落在代码里这个划分方式值得学的一点是models里只放网络data里只放数据utils/train.py里只放「跑一个 epoch」这件事main.py负责把三者串起来。很多新手图省事把所有逻辑写进一个train.py两三百行之后想换个学习率调度器都要翻半天。拆开之后换骨干只需要动models/__init__.py的一行注册换数据集只需要在data/下加一个文件。2.3 BasicBlock残差相加必须发生在第二个 ReLU 之前残差网络的核心就是那个 shortcut。CIFAR 版用的是 BasicBlock两层 3×3 卷积不是 Bottleneck1×1、3×3、1×1 三层因为 32×32 的输入本身通道数就少用 Bottleneck 反而增加复杂度还降精度。核心实现如下import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): expansion 1 def __init__(self, in_planes, planes, stride1, use_projectionFalse): super().__init__() # 两层 3x3 卷积padding1 保证 stride1 时分辨率不变 self.conv1 nn.Conv2d(in_planes, planes, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(planes) self.conv2 nn.Conv2d(planes, planes, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(planes) # shortcut形状一致走恒等映射形状变化才用 1x1 卷积投影 if use_projection: self.shortcut nn.Sequential( nn.Conv2d(in_planes, planes, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(planes), ) else: self.shortcut nn.Identity() def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) # 注意这里不加 ReLU out out self.shortcut(x) # 残差相加发生在第二个 BN 之后 return F.relu(out) # 相加完再统一激活这段里最容易写错的是最后三行。conv 后面带 bias 是浪费因为紧跟着 BNbias 会被减掉所以统一设biasFalse这也是原论文的做法。use_projection的开关逻辑也很关键只要输入输出通道数不同或者 stride 不等于 1 导致分辨率变了shortcut 就无法直接相加必须走 1×1 卷积把形状对齐。常见的一个翻车写法是「通道变了但还是用恒等映射」前向传播时直接报 shape mismatch或者更隐蔽地用了广播机制把结果算错但程序不报错。out self.bn2(self.conv2(out))这一行后面绝对不能加 ReLU。残差块的设计意图是让网络学习残差F(x)然后输出F(x) x如果第二个 BN 之后先过 ReLU那负半轴的信息就被截断了恒等映射的路径上就有了非线性污染深度一上去精度反而掉。这个点在 ResNet 变体比如 pre-activation 版本里会重新排列顺序但原版 BasicBlock 就是这么写的。2.4 三个 stage 的堆叠规则与参数量对照主网络按「stem → stage1 → stage2 → stage3 → 全局池化 → 全连接」组织三个 stage 的通道数是 16、32、64每个 stage 内部的 block 数由配置决定class ResNetCIFAR(nn.Module): def __init__(self, blocks_per_stage, num_classes10, widen1): super().__init__() self.in_planes 16 # stem单层 3x3不池化保留 32x32 分辨率 self.stem nn.Conv2d(3, 16, kernel_size3, stride1, padding1, biasFalse) self.bn_stem nn.BatchNorm2d(16) # 三个 stage通道数按 16/32/64 递增后两个在首个 block 下采样 self.stage1 self._make_stage(16 * widen, blocks_per_stage, stride1) self.stage2 self._make_stage(32 * widen, blocks_per_stage, stride2) self.stage3 self._make_stage(64 * widen, blocks_per_stage, stride2) self.fc nn.Linear(64 * widen, num_classes) def _make_stage(self, planes, num_blocks, stride): blocks [] for i in range(num_blocks): s stride if i 0 else 1 # 只在首个 block 下采样 use_proj (s ! 1) or (self.in_planes ! planes) blocks.append(BasicBlock(self.in_planes, planes, s, use_proj)) self.in_planes planes return nn.Sequential(*blocks) def forward(self, x): out F.relu(self.bn_stem(self.stem(x))) out self.stage1(out) out self.stage2(out) out self.stage3(out) out F.adaptive_avg_pool2d(out, 1).flatten(1) return self.fc(out)blocks_per_stage传 3 就是 ResNet-20传 5 是 ResNet-32传 7 是 ResNet-44传 9 是 ResNet-56层数 6 × n 2。widen是通道倍率默认 1想试 WideResNet 的思路时把它调到 2 或 4通道翻倍但层数不变往往比单纯加深度收益更明显——这是我在小数据集上反复验证过的一条经验。模型blocks_per_stage参数量约CIFAR10 测试精度参考ResNet-2030.27M91.5% ~ 92.0%ResNet-3250.47M92.3% ~ 92.8%ResNet-4470.66M92.5% ~ 93.0%ResNet-5690.85M92.8% ~ 93.3%表里的精度是标准训练配方随机裁剪 翻转 余弦退火 200 epoch下的大致区间不是绝对红线。有人跑到 94% 以上通常是在配方上加了东西标签平滑、EMA、更好的增强而不是网络本身变了。反过来如果 ResNet-20 只跑到 88%先别急着换更深的模型八成是训练循环或数据管线的问题后面第 5 章会具体说。3. 数据管线与训练循环一个 batch 从磁盘到 loss 的完整链路3.1 CIFAR10 的下载、归一化与增强顺序CIFAR10 一共 60000 张 32×32 的彩色图50000 训练、10000 测试十个类别各占十分之一类别均衡这点省了不少心。数据部分看着简单但坑全在顺序上import torch from torchvision import datasets, transforms # 训练集的真实逐通道均值/标准差直接写死比每次现算省事 MEAN (0.4914, 0.4822, 0.4465) STD (0.2470, 0.2435, 0.2616) train_tf transforms.Compose([ transforms.RandomCrop(32, padding4), # 先四周补 0 到 40再随机裁回 32 transforms.RandomHorizontalFlip(p0.5), # 水平翻转CIFAR 语义不变 transforms.ToTensor(), # HWC uint8 - CHW float [0,1] transforms.Normalize(MEAN, STD), # 必须在 ToTensor 之后 ]) test_tf transforms.Compose([ transforms.ToTensor(), transforms.Normalize(MEAN, STD), # 测试集只做归一化不做任何随机 ]) train_set datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtrain_tf) test_set datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtest_tf)RandomCrop(32, padding4)是 CIFAR 上性价比最高的增强等价于先给四周补零到 40×40 再随机抠一块 32×32 出来模型见到的每张图位置都略有偏移泛化能涨一到两个点。Normalize必须放在ToTensor后面因为归一化操作的是浮点张量如果顺序反了ToTensor会把已经归一化到 [-1,1] 附近的数值再除以 255等于把数据压成一坨接近 0 的数训练直接不收敛。归一化的均值标准差要用 CIFAR 自己的统计量不要图省事抄 ImageNet 的(0.485, 0.456, 0.406)。用错统计量不会报错但相当于给每个通道乘了一个不匹配的系数BN 层虽然能吸收一部分收敛速度和最终精度都会受影响。上面这组数值是对训练集按通道算出来的我一般在项目里直接写成常量避免每次启动都遍历一遍数据集。3.2 训练循环骨架loss、优化器与梯度裁剪一个 epoch 的核心逻辑就四步前向、算 loss、反向、更新。写清楚这四步比堆一堆花活更重要import torch import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device, clip1.0): model.train() # 切到训练模式BN 用 batch 统计量 total_loss, correct, total 0.0, 0, 0 for images, labels in loader: images images.to(device, non_blockingTrue) labels labels.to(device, non_blockingTrue) optimizer.zero_grad(set_to_noneTrue) # 置 None 比置 0 省一点显存 logits model(images) loss criterion(logits, labels) loss.backward() # 梯度裁剪防止个别 batch 梯度爆炸把权重带飞 torch.nn.utils.clip_grad_norm_(model.parameters(), max_normclip) optimizer.step() total_loss loss.item() * labels.size(0) correct (logits.argmax(dim1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total几个参数值得单独说。optimizer.zero_grad(set_to_noneTrue)在新版 PyTorch 里是推荐写法把梯度张量设成 None 而不是填零省一次写内存的操作训练大模型时差别明显小模型上主要是个习惯问题。clip_grad_norm_(max_norm1.0)对 CIFAR 这种小任务来说不是必需品前几十个 epoch 梯度一般很温和但一旦你把学习率调大或者加了比较激进的增强某个 batch 突然给一个大梯度权重能被带到一个很差的区域之后几十个 epoch 都缓不过来。加上它相当于买了个后悔药代价几乎为零。model.train()这一行不能漏。BN 层在训练和评估两种模式下行为完全不同训练时用当前 batch 的均值和方差评估时用滑动平均。如果评估阶段忘了model.eval()测试集的准确率会莫名其妙地波动同一份权重量两次能差好几个点这是新手最常遇到的玄学问题之一。损失函数用标准的交叉熵优化器用带动量的 SGD 而不是 Adam这点在小数据集上差距很明显model ResNetCIFAR(blocks_per_stage3).to(device) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer torch.optim.SGD( model.parameters(), lr0.1, # 初始学习率batch size 128 时的常用值 momentum0.9, weight_decay5e-4, # CIFAR 上非常稳的一个值 nesterovTrue, )SGD momentum 在 CIFAR 上收敛到的解通常比 Adam 泛化更好同样跑 200 epochAdam 在训练集上拟合得更快测试集却经常差零点几个点。label_smoothing0.1是后来加进CrossEntropyLoss的官方参数不需要自己写标签平滑的 loss它对小数据集抑制过拟合的效果很直接代价是训练 loss 的数字看起来没那么漂亮——这是个心理门槛习惯就好。3.3 学习率调度与 checkpoint 的存取学习率调度对最终精度的影响不比网络结构小。CIFAR 上最常见的是两种多步衰减在第 100、150 epoch 各降 10 倍和余弦退火从初始值平滑降到 0。资源里两种应该都有对应的实现我一般默认用余弦退火配一个短 warmupimport math def cosine_with_warmup(optimizer, warmup_epochs, total_epochs, base_lr, step): step 是全局的迭代步数不是 epoch 数 total_steps total_epochs * steps_per_epoch warmup_steps warmup_epochs * steps_per_epoch if step warmup_steps: # 前几个 epoch 线性从 0.01 倍爬到 1 倍避免一上来就把 BN 统计量带偏 lr base_lr * (0.01 0.99 * step / warmup_steps) else: progress (step - warmup_steps) / max(1, total_steps - warmup_steps) lr 0.5 * base_lr * (1.0 math.cos(math.pi * progress)) for group in optimizer.param_groups: group[lr] lr return lrwarmup 的意义在于训练最开始的那几百步BN 的滑动统计量还没稳定如果直接用 0.1 的学习率前几个 batch 的梯度方向噪声很大权重容易被推到不好的位置。用一个从 0.001 起步的爬坡期等于给网络一点时间把统计量校准。这套写法在 transformer 圈子里更常见但放在 CIFAR 的 ResNet 上同样有效尤其是 batch size 开得比较大的时候。checkpoint 的存取容易被写残最常见的错误是只存model.state_dict()def save_ckpt(path, model, optimizer, scheduler_step, epoch, best_acc): torch.save({ model: model.state_dict(), optimizer: optimizer.state_dict(), epoch: epoch, global_step: scheduler_step, best_acc: best_acc, }, path)只保存模型权重的话中断后想接着训就得从头调学习率等于放弃了之前所有的退火进度恢复后精度会明显地先掉一截再爬回来。把 optimizer 的状态、全局步数、最好的精度一起存进去恢复时按原轨迹继续才叫真正的断点续训。optimizer.state_dict()里包含动量缓冲区这部分丢了影响尤其大SGD 的动量是跨 step 累积的重置一次要好几十步才能重新建立起来。4. 训练配方学习率曲线、正则与评估口径的取舍4.1 三条学习率曲线的实测差别同一份网络定义换学习率策略CIFAR10 上的测试精度能差出接近两个点。我用 ResNet-20 跑过三组对照都是 200 epoch、batch size 128、其余配置一致策略配置测试精度特点固定学习率lr0.1 全程不变89.5% 左右后期在最优解附近来回震荡收敛不彻底多步衰减100/150 epoch 各 ×0.191.8% 左右稳定但对衰减点的位置敏感余弦退火 5 epoch warmup平滑降到 092.3% 左右后期学习率极小相当于自动做精细搜索多步衰减的敏感点在于那两个衰减 epoch 的位置。设在 100 和 150是因为总轮数是 200如果你把总轮数改成 100 但衰减点没改后半程学习率就没降下去最后十来个 epoch 精度会一直在抖。余弦退火没有这个烦恼它只依赖总步数把总轮数改了曲线自动跟着缩放这也是我后来基本只用它的原因。余弦退火有个副作用训练末段学习率接近 0模型几乎不再更新如果这时候验证精度刚好在一个局部高点你看到的数字会比模型真实水平略乐观一点点。这不是大问题但如果你要拿这个数去和别人比最好把「最后 5 个 epoch 的平均精度」也报出来比单点更实在。4.2 权重衰减与标签平滑的配合weight_decay5e-4是 CIFAR 上的经典值改这个参数之前先确认两件事一是你用的优化器是不是 SGDAdam 系的最优权重衰减通常在 1e-4 到 1e-2 之间还牵扯到 AdamW 的解耦实现不能直接照搬二是这个值是和 batch size 绑定的batch 从 128 翻到 512等效的正则强度会变常见做法是线性放大到 2e-3 附近再微调。标签平滑设 0.1 意味着真实类别的目标概率是 0.9剩下 0.1 均摊给其他九类。它的作用和权重衰减有重叠都是抑制过拟合所以两个一起上的时候不要都往大调。我见过有人把weight_decay调到 1e-3 又加上 0.2 的标签平滑结果训练 50 epoch 后训练集精度还不到 85%模型根本没学起来——正则过强的时候现象和「学习率太小」几乎一样都是 loss 降得慢容易误判。判断到底是正则太强还是学习率不对有个简单办法把正则全部关掉跑 20 个 epoch如果训练集精度能迅速冲到 95% 以上说明模型容量和学习率都没问题是正则压太狠了如果关掉正则还是学不动那问题在前向或者数据侧跟正则没关系。4.3 评估口径best checkpoint、EMA 与 TTA评估这件事看着只是model.eval()加一个循环实际上口径不统一同一份权重量出来的数能差一个点。第一个口径是取哪个 checkpoint。训练过程中保存「验证精度最高」的那一份比起取最后一个 epoch通常能高 0.3 到 0.8 个点因为余弦退火末段虽然稳但单点波动仍然存在。代价是这份权重在验证集上被「挑选」过如果验证集和测试集划分不严谨报出来的数字会偏乐观。CIFAR10 的测试集是官方切好的不存在这个问题但换成自己的数据集时要注意验证集和测试集的隔离。第二个口径是 EMA指数滑动平均。训练时额外维护一份参数的滑动平均评估时用这份而不是当前权重对小数据集上的 ResNet 通常能稳定涨 0.3 个点左右class EMA: def __init__(self, model, decay0.999): self.decay decay # 深拷贝一份参数不参与梯度 self.shadow {k: v.detach().clone() for k, v in model.state_dict().items()} torch.no_grad() def update(self, model): for k, v in model.state_dict().items(): if v.dtype.is_floating_point: self.shadow[k].mul_(self.decay).add_(v.detach(), alpha1 - self.decay) else: self.shadow[k].copy_(v) # BN 的 num_batches_tracked 是整型整型参数BN 层里的num_batches_tracked不能做浮点滑动平均必须直接拷贝不加这个判断会报类型错误。decay0.999对应的有效窗口大概是一千步200 epoch、每 epoch 391 步的话覆盖不到半个 epoch偏短想更平滑可以调到 0.9999但要注意训练早期的参数还没稳定EMA 会把早期的垃圾权重也平均进去通常前 10% 的步数不做更新。第三个口径是 TTA测试时增强最常用的就是原图加水平翻转各跑一次两个 softmax 输出取平均。成本翻倍收益 0.2 到 0.4 个点值不值得看你的场景。竞赛里常上工程部署里基本不用因为推理延迟翻倍是实打实的代价。5. 避坑与排查五条训练现场踩出来的记录5.1 训练侧的三个高频问题现象一训练 loss 前几个 epoch 一直不降卡在 2.3 左右不动。原因通常是学习率写小了或者优化器压根没拿到参数。有一类很隐蔽的写法是先建优化器再model.to(device)如果中间做过model model.cuda()这种重新赋值优化器里持有的还是旧的参数对象梯度算在旧参数上step()更新的是另一份loss 自然不动。解决方式很简单永远在model.to(device)之后创建优化器并且打印一次len(list(model.parameters()))和len(optimizer.param_groups[0][params])对一下数量。现象二训练精度 99%测试精度 60% 出头。八成出在评估管线。常见的是测试集的 transform 里保留了RandomCrop或RandomHorizontalFlip每次评估看到的图都不一样指标自然飘另一个可能是忘了model.eval()BN 用 batch 统计量在测试集上前向小 batch 下统计量噪声极大。还有一个容易忽略的点测试集的Normalize用了和训练集不同的均值或者干脆没做归一化。逐项对一遍 train_tf 和 test_tf除了随机增强那两行其余必须完全一致。现象三训到一半 loss 突然变成 nan之后再也救不回来。先看是不是学习率太大尤其是从 warmup 切到正式学习率的那一刻如果 base_lr 设成 0.5 这种激进值一两个 batch 就能把权重炸掉。其次检查数据里有没有异常值自定义数据集常见的情况是某张图读出来全是 255 或者全是 0归一化后数值超出正常范围。最后一种可能是混合精度训练时 loss scale 没配对fp16 的梯度下溢成 0 之后下一步的更新就会出 nan。排查手段是把torch.autograd.set_detect_anomaly(True)打开跑几个 batch它会指出反向传播里第一个出问题的算子。5.2 数据与环境侧的两个坑现象四Windows 上DataLoader(num_workers4)直接卡死或者每个 epoch 结束时报一堆进程退出错误。这是num_workers 0在 Windows 上的经典问题根因是子进程会重新 import 主模块而主模块里的训练代码没有放在if __name__ __main__:下面于是每个 worker 启动时都试图重新跑一遍训练。解决办法有两个把入口逻辑放进 main guard或者干脆把num_workers设成 0 跑通再说。Linux 上因为默认用 fork不容易暴露这个问题所以「在服务器上好好的拷到本地就卡死」是常见现象。现象五换个环境跑精度掉了一个多点代码一行没改。先怀疑随机种子和数据加载顺序。torch.backends.cudnn.benchmarkTrue会让卷积算法自动选最快的实现不同显卡、不同 cuDNN 版本选出来的算法不一样浮点累加顺序不同结果就会有细微差别。追求可复现的话把它设成False并把torch.manual_seed、random.seed、numpy.random.seed三个一起设DataLoader的worker_init_fn也要固定。完全不差是不可能的多卡训练里梯度规约的顺序本身就不确定一般能控制在 0.2 个点以内就算复现成功。5.3 一个能直接用的排查脚本遇到「不知道哪一步出问题」的时候别急着改超参先把一个 batch 拆开看import torch def sanity_check(model, loader, device): model.eval() images, labels next(iter(loader)) print(输入范围:, images.min().item(), images.max().item()) # 应在 -2.5 ~ 2.5 print(标签分布:, torch.bincount(labels, minlength10).tolist()) with torch.no_grad(): logits model(images.to(device)) print(输出形状:, tuple(logits.shape)) # 应为 [B, 10] print(初始 loss:, torch.nn.functional.cross_entropy( logits, labels.to(device)).item()) # 应为 ln(10) ≈ 2.30 # 检查梯度是否真的流到了每一层 model.train() loss torch.nn.functional.cross_entropy( model(images.to(device)), labels.to(device)) loss.backward() for name, p in model.named_parameters(): if p.grad is None: print(梯度为 None:, name) # 出现即说明该层没参与计算初始 loss 接近 2.30 是个很好的健康检查点如果明显大于这个值说明输出 logits 的尺度不对可能是最后一层初始化太大如果明显小于说明标签泄漏或者模型直接读到了答案。梯度为 None 的参数列表更重要走一遍就能发现哪些层被定义了但没用上——这是拼接网络、加分支模块时最常见的低级错误。6. 二次开发换骨干、换数据集与加模块的三条改造路径改这份代码之前先想清楚你要动的是哪一层是换网络结构、换数据、还是加训练技巧。这三件事的改动范围完全不同混在一起改出了问题很难定位。改造目标需要动的文件关键改动点验证方式换骨干ResNet → WideResNet / 加 SEmodels/resnet.py、models/__init__.py改widen参数或插入 SE 模块保持 stage 输出通道约定参数量对照表前向 shape 检查换数据集CIFAR100 / 自建data/、configs/*.yaml类别数、归一化统计量、增强策略先跑 5 epoch 看 loss 是否正常下降加训练技巧EMA / 混合精度utils/train.py、main.py权重更新时机、loss scale 与反缩放与 baseline 同种子对比最后 5 epoch 均值换骨干这条路径最省事。想试 WideResNet 就动widen把 1 改成 4通道从 16/32/64 变成 64/128/256参数量从 0.27M 涨到 2.7M 左右CIFAR10 上能到 94.5% 以上代价是训练时间大概翻三倍。加 SE 模块的话在 BasicBlock 的conv2之后插一个通道注意力注意 SE 里的全连接降维比例别太小reduction16在通道数只有 64 的时候会把中间维度压到 4信息损失太大改成 8 更稳。换数据集最容易出问题的是归一化统计量和类别数这两处。CIFAR100 的均值标准差和 CIFAR10 不一样直接从data/cifar10.py复制过来改个名字前面几个 epoch 的 loss 曲线会明显更抖。类别数改了但忘了改self.fc nn.Linear(64 * widen, num_classes)训练能跑评估时argmax出来的索引对不上标签精度永远是随机水平。我一般的做法是写一个assert num_classes len(dataset.classes)让它在启动时就炸掉而不是训完两小时才发现。还有一个改动量很小但收益明显的点把torch.compile(model)加上。新版 PyTorch 里这一行对 CIFAR 上的小模型能提速 20% 到 40%代价是第一次编译要等一会儿而且遇到不支持的算子会回退到 eager 模式。加之前先确认你的 CUDA 和 PyTorch 版本匹配版本不对的时候torch.compile的报错信息很不友好能让你查一下午。那条血泪经验是这样的任何一次「只改了一点点」的改动都要拿同一个随机种子跑一遍 20 epoch 的短周期和改动前的结果并排看 loss 曲线而不是直接上 200 epoch。我有一次信心满满地把标签平滑从 0.1 调到 0.2觉得肯定涨点跑完一整轮发现掉了 0.6 个点回头查才发现是那一版代码里weight_decay忘了同步下调两个正则叠在一起把模型压死了。从那以后我每次动超参都强制走一遍 20 epoch 的对照曲线对不上就不往下跑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑