news 2026/10/13 8:29:38

ResNet18在CIFAR-10上的实战:从训练到95%准确率

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ResNet18在CIFAR-10上的实战:从训练到95%准确率

简介:这份资源面向深度学习入门与进阶学习者,聚焦图像识别方向,提供在CIFAR10数据集上训练ResNet18模型的完整工程。ResNet18通过残差短路结构缓解深层网络的梯度消失问题,配合PyTorch框架可清晰实践模型定义、数据预处理与训练优化全流程。压缩包共26个文件,约341.16MB,包含3个Python脚本用于模型与训练逻辑、1个pt权重文件、CIFAR10的data_batch与test_batch数据分片、若干jpg预测效果图及字体等辅助文件,目录结构便于按模块查阅。已有1384人学习下载,适合希望理解残差网络原理、复现训练过程或直接加载权重进行预测的读者,可从中掌握数据增强、交叉熵损失、学习率调度与模型保存加载等关键环节,对夯实深度学习实践能力具有较高参考价值。

1. 从 ResNet18_CIFAR10.rar 说起:一个压缩包背后到底藏着什么

很多人第一次看到ResNet18_CIFAR10.rar这个文件名,第一反应是「这不就是个作业压缩包吗」。但如果你正在找它,大概率不是想交作业,而是想搞清楚一件事:怎么用 ResNet18 在 CIFAR-10 上跑出一个能看的准确率,并且把训练、验证、推理这条链路完整走通。这个压缩包通常包含的是数据集加载脚本、模型定义、训练入口、配置文件,可能还有预训练权重和日志。它解决的核心问题是:给你一个可复现的最小工程骨架,让你不用从零搭训练框架,直接改参数就能跑。

适合谁?三类人:一是刚学完卷积网络、想找一个标准数据集练手的学生;二是需要快速验证某个改进点(比如换损失函数、加注意力模块)的算法工程师;三是想把 CIFAR-10 当作教学案例的讲师。CIFAR-10 只有 60000 张 32×32 的彩色图,10 个类别,单卡几十分钟就能跑完一轮,是验证想法性价比最高的数据集之一。ResNet18 则是残差网络里最轻的一档,参数量约 1100 万,既能体现残差连接的价值,又不会让你等太久。这两者组合,几乎是深度学习入门到进阶的必经之路。

2. ResNet18 与 CIFAR-10 的匹配逻辑:为什么不是 ResNet50 也不是 VGG

2.1 残差连接在 32×32 小图上的真实收益

ResNet 的核心是残差块:输出等于输入加上经过两层卷积后的残差。公式上写y = F(x) + x,但落到 CIFAR-10 这种小图上,收益点跟 ImageNet 上不太一样。ImageNet 上残差主要解决深层网络梯度消失,而 CIFAR-10 上 ResNet18 只有 18 层,梯度消失本来就不严重。真正起作用的是两点:一是残差块让浅层特征可以直通到深层,小图本身空间信息少,这种直通能保留更多边缘和纹理细节;二是 CIFAR-10 的 32×32 分辨率下,下采样次数不能太多,ResNet18 在 CIFAR 版本里通常把第一层 7×7 卷积换成 3×3,并且去掉 maxpool,这样特征图在进入残差阶段前保持 32×32,避免过早丢失空间信息。

如果你直接拿 torchvision 里 ImageNet 版的 ResNet18 来跑 CIFAR-10,第一层 7×7 步长 2 加 maxpool 会把 32×32 直接压到 8×8,后面再下采样三次,最后只剩 1×1,空间信息几乎丢光,准确率会卡在 70% 出头。所以常见做法是改第一层为 3×3 步长 1,去掉 maxpool,这样初始特征图保持 32×32,经过四个 stage 后变成 4×4,再全局池化,信息保留合理。

2.2 把 ResNet18 改成 CIFAR 版:三个必须改的地方

下面是一个最小可用的 CIFAR 版 ResNet18 定义,基于 PyTorch,只保留关键结构,方便你直接抄。

import torch import torch.nn as nn import torch.nn.functional as F class BasicBlock(nn.Module): expansion = 1 def __init__(self, in_planes, planes, stride=1): super(BasicBlock, self).__init__() # 3x3 卷积,padding=1 保持空间尺寸 self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(planes) self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(planes) self.shortcut = nn.Sequential() # 如果输入输出通道不一致或需要下采样,用 1x1 卷积调整 if stride != 1 or in_planes != self.expansion * planes: self.shortcut = nn.Sequential( nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(self.expansion * planes) ) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += self.shortcut(x) # 残差相加 out = F.relu(out) return out class ResNet18CIFAR(nn.Module): def __init__(self, num_classes=10): super(ResNet18CIFAR, self).__init__() self.in_planes = 64 # CIFAR 版:第一层 3x3 卷积,步长 1,不接 maxpool self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(64) # 四个 stage,每个 stage 两个 BasicBlock self.layer1 = self._make_layer(BasicBlock, 64, 2, stride=1) self.layer2 = self._make_layer(BasicBlock, 128, 2, stride=2) self.layer3 = self._make_layer(BasicBlock, 256, 2, stride=2) self.layer4 = self._make_layer(BasicBlock, 512, 2, stride=2) self.linear = nn.Linear(512 * BasicBlock.expansion, num_classes) def _make_layer(self, block, planes, num_blocks, stride): strides = [stride] + [1] * (num_blocks - 1) layers = [] for s in strides: layers.append(block(self.in_planes, planes, s)) self.in_planes = planes * block.expansion return nn.Sequential(*layers) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.layer1(out) out = self.layer2(out) out = self.layer3(out) out = self.layer4(out) out = F.avg_pool2d(out, 4) # 32x32 经过四次下采样变成 4x4 out = out.view(out.size(0), -1) out = self.linear(out) return out

逻辑说明:BasicBlock里两个 3×3 卷积,第二个卷积后不接 ReLU,先跟 shortcut 相加再激活,这是 ResNet 原始设计。shortcut在通道数变化或步长不为 1 时用 1×1 卷积加 BN 调整。ResNet18CIFAR第一层是 3×3 步长 1,没有 maxpool,四个 stage 的通道数分别是 64、128、256、512,每个 stage 两个 block,总共 1+2×2×4=17 层卷积加一个全连接,符合 ResNet18 的层数定义。

参数说明:num_classes=10对应 CIFAR-10 的十个类别。stride=2出现在 layer2、layer3、layer4 的第一个 block,负责下采样。avg_pool2d(out, 4)里的 4 是因为输入 32×32 经过三次步长 2 下采样变成 4×4,如果输入尺寸变了这里要跟着改。bias=False是因为后面接了 BN,卷积偏置会被 BN 的偏移吸收,省掉能减少参数量。

2.3 数据加载与归一化参数:别小看那几个小数

CIFAR-10 的均值和标准差是固定的,用错会明显掉点。常见做法是mean=(0.4914, 0.4822, 0.4465),std=(0.2023, 0.1994, 0.2010)。如果你用 0.5 和 0.5 这种通用值,准确率大概会低 1 到 2 个百分点。数据增强方面,训练集用随机裁剪加水平翻转就够了,CIFAR-10 图片小,过度增强反而有害。

import torchvision import torchvision.transforms as transforms transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), # 先 padding 再随机裁剪 transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train) trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=4) testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test) testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=4)

逻辑说明:RandomCrop(32, padding=4)先把 32×32 图 padding 到 40×40,再随机裁回 32×32,相当于让物体位置有轻微偏移。RandomHorizontalFlip对 CIFAR-10 里的飞机、汽车、动物都合理,但如果你做的是文字方向敏感的任务就不能用。Normalize的四个参数按通道顺序 R、G、B 对应。

参数说明:batch_size=128是单卡 8GB 显存下的稳妥值,再大可能 OOM,再小训练会慢。num_workers=4取决于你的 CPU 核数,设成 0 会在主进程加载数据,训练时 GPU 利用率上不去。download=True只在第一次运行时下载,后面会直接读本地。

3. 训练脚本怎么写:从优化器到学习率调度

3.1 优化器与超参:SGD 还是 Adam

CIFAR-10 上 ResNet18 的经典配置是 SGD 加 momentum,学习率 0.1,weight decay 5e-4,momentum 0.9。Adam 也能用,但通常需要更小的学习率和更长的训练轮数,最终准确率不一定比 SGD 高。如果你只是想快速看结果,Adam 配 1e-3 学习率跑 50 轮也能到 90% 左右;如果要冲 94% 以上,还是老老实实用 SGD 加 cosine 退火。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = ResNet18CIFAR(num_classes=10).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4) scheduler = CosineAnnealingLR(optimizer, T_max=200) # 200 轮周期

逻辑说明:CrossEntropyLoss内部已经包含 softmax,所以模型最后一层不要加 softmax。CosineAnnealingLR让学习率从 0.1 按余弦曲线降到接近 0,比阶梯下降更平滑,后期收敛更稳。

参数说明:lr=0.1是 SGD 在 CIFAR-10 上的常用起点,如果你把 batch size 改成 256,学习率可以线性放大到 0.2。weight_decay=5e-4是 L2 正则系数,太大欠拟合,太小过拟合。T_max=200表示 200 轮完成一个余弦周期,如果你只跑 100 轮就改成 100。

3.2 训练循环与验证:每个 epoch 该看什么

训练循环本身不复杂,但有几个细节容易翻车:一是记得在验证时切model.eval()并关掉梯度;二是每个 epoch 结束后要调scheduler.step();三是最好记录训练损失和验证准确率,方便判断是否过拟合。

def train(epoch): model.train() train_loss = 0 correct = 0 total = 0 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() train_loss += loss.item() _, predicted = outputs.max(1) total += targets.size(0) correct += predicted.eq(targets).sum().item() print(f'Epoch {epoch}: Loss {train_loss/(batch_idx+1):.3f}, ' f'Acc {100.*correct/total:.2f}%') def test(epoch): model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, targets in testloader: inputs, targets = inputs.to(device), targets.to(device) outputs = model(inputs) _, predicted = outputs.max(1) total += targets.size(0) correct += predicted.eq(targets).sum().item() acc = 100. * correct / total print(f'Test Acc: {acc:.2f}%') return acc for epoch in range(1, 201): train(epoch) test(epoch) scheduler.step()

逻辑说明:optimizer.zero_grad()必须在loss.backward()之前,否则梯度会累加。model.eval()会关闭 BN 的批量统计和 dropout,验证时必须切。torch.no_grad()省显存也加速,验证阶段不需要反向传播。

参数说明:outputs.max(1)返回每个样本最大 logit 的索引,就是预测类别。predicted.eq(targets).sum().item()统计预测正确的数量。scheduler.step()放在 epoch 末尾,如果放在 batch 里会变成每步都调,学习率降太快。

3.3 保存与加载:别只存 state_dict

保存模型时,只存state_dict()是最省空间的,但加载时必须先有模型结构定义。如果你想把结构也带上,可以用torch.save(model, path),但这样加载依赖原始类定义,换环境容易出问题。常见做法是存 state_dict 加一个配置字典。

# 保存 torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'acc': acc, }, 'resnet18_cifar10.pth') # 加载 checkpoint = torch.load('resnet18_cifar10.pth', map_location=device) model.load_state_dict(checkpoint['model_state_dict']) optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

逻辑说明:把 epoch、optimizer 状态和准确率一起存,方便断点续训。map_location=device在 CPU 上加载 GPU 保存的权重时必加,否则会报错。

参数说明:resnet18_cifar10.pth是文件名,你可以按实验编号命名,比如resnet18_cifar10_sgd_cosine.pth,避免覆盖。acc是验证准确率,用来快速筛选最好的 checkpoint。

4. 避坑与排查:那些让准确率卡在 85% 的细节

4.1 现象:训练准确率涨但验证准确率不涨

原因通常是数据增强太强或者 BN 在验证时没切 eval。CIFAR-10 图片只有 32×32,如果你加了旋转、颜色抖动、Cutout 叠加,训练集分布和测试集差异变大,模型学到的特征泛化不过来。另一个常见原因是验证时忘了model.eval(),BN 继续用 batch 统计,而验证 batch 只有 100 张,统计量波动大。

解决:先去掉所有增强只留归一化,跑一轮看验证准确率是否正常。如果正常,再逐个加回增强,每次只加一种。验证前确认model.eval()和torch.no_grad()都写了。

4.2 现象:loss 变成 nan

原因多半是学习率太大或者梯度爆炸。SGD 配 0.1 在 ResNet18 上一般不会炸,但如果你把第一层改成 7×7 步长 2 又没加 BN,或者用了自定义损失函数没做数值稳定,就可能出 nan。另外,CIFAR-10 标签是 0 到 9 的整数,如果你不小心把标签做成了 one-hot 又用 CrossEntropyLoss,也会报错或出 nan。

解决:先把学习率降到 0.01 跑几步看 loss 是否正常。检查模型第一层卷积后是否有 BN。确认标签是torch.long类型且范围在 0 到 9。如果用了混合精度,初期可以先用 FP32 排除数值问题。

4.3 现象:GPU 利用率低,训练速度慢

原因通常是数据加载成了瓶颈。num_workers=0时数据在主进程加载,GPU 算完一个 batch 要等 CPU 读下一批。另外,如果你把数据集放在机械硬盘上,随机读取 60000 张小图也会慢。

解决:把num_workers设成 CPU 核数的一半到全部,比如 8 核设 4 或 8。加pin_memory=True让数据从 CPU 到 GPU 的拷贝更快。如果内存够,可以把整个数据集先读到内存里,或者用 SSD 存数据。

4.4 现象:最终准确率只有 88% 到 90%,上不去

原因可能是训练轮数不够、学习率调度不对、或者模型结构有细微错误。ResNet18 在 CIFAR-10 上正常能到 93% 到 95%,如果卡在 90% 以下,先检查第一层是不是 3×3 步长 1、有没有 maxpool。另一个常见错误是 shortcut 的 stride 和主分支不一致,导致相加时尺寸对不上,PyTorch 会报错,但如果你用了自动广播,可能悄悄算错。

解决:打印每个 stage 的输出尺寸,确认是 32、16、8、4。检查_make_layer里self.in_planes的更新逻辑,确保每个 block 的输入通道数正确。把训练轮数加到 200,用 cosine 退火,通常能再涨 2 到 3 个点。

4.5 现象:加载预训练权重后准确率反而下降

原因通常是预训练权重来自 ImageNet,第一层是 7×7 卷积,而你改成了 3×3,形状不匹配。如果你强行加载,PyTorch 会跳过不匹配的层,但可能连 BN 的 running stats 也一起跳过了,导致初期训练不稳定。

解决:加载时用strict=False,并且打印哪些层被跳过。对于第一层,要么保持 7×7 但改步长和 padding 适配 32×32,要么就从头训练。CIFAR-10 上从头训练 200 轮的成本并不高,预训练权重带来的收益有限。

5. 进阶技巧:把 ResNet18 在 CIFAR-10 上推到 95%

如果你已经跑通基础版,想再往上走,有几个经过验证的方向。第一个是标签平滑,把硬标签换成 0.1 的平滑标签,能减少过拟合,通常涨 0.5 到 1 个点。第二个是 Cutout,在训练图上随机挖一个 8×8 的方块置零,模拟遮挡,配合标签平滑能到 95% 左右。第三个是混合精度训练,用torch.cuda.amp把显存占用降下来,batch size 可以开到 256 甚至 512,训练速度提升明显,但要注意 loss scaling 防止梯度下溢。

下面是一个带标签平滑和 Cutout 的训练片段,你可以直接替换前面的对应部分。

class LabelSmoothingCrossEntropy(nn.Module): def __init__(self, smoothing=0.1): super().__init__() self.smoothing = smoothing def forward(self, pred, target): log_probs = F.log_softmax(pred, dim=-1) nll_loss = -log_probs.gather(dim=-1, index=target.unsqueeze(1)) nll_loss = nll_loss.squeeze(1) smooth_loss = -log_probs.mean(dim=-1) loss = (1 - self.smoothing) * nll_loss + self.smoothing * smooth_loss return loss.mean() def cutout(img, size=8): h, w = img.size(1), img.size(2) y = torch.randint(h, (1,)) x = torch.randint(w, (1,)) y1 = torch.clamp(y - size // 2, 0, h) y2 = torch.clamp(y + size // 2, 0, h) x1 = torch.clamp(x - size // 2, 0, w) x2 = torch.clamp(x + size // 2, 0, w) img[:, y1:y2, x1:x2] = 0 return img criterion = LabelSmoothingCrossEntropy(smoothing=0.1)

逻辑说明:LabelSmoothingCrossEntropy把真实类别的概率从 1 降到 0.9,其余 0.1 均匀分给其他类别,防止模型对训练标签过度自信。cutout在每张图上随机选一个中心点,挖掉 8×8 的区域,模拟遮挡,强迫模型学习更鲁棒的特征。

参数说明:smoothing=0.1是常用值,太大欠拟合,太小没效果。size=8对 32×32 的图来说约等于挖掉 6% 的面积,再大可能丢关键信息。Cutout 只在训练时用,验证和测试时不要加。

还有一个容易被忽略的点是权重衰减和 BN 的配合。BN 的 weight 和 bias 通常不参与 weight decay,如果你用统一的 optimizer 参数,BN 的 weight 也会被衰减,长期训练可能让 BN 失效。常见做法是把 BN 参数单独分组,不设 weight decay。

bn_params = [] other_params = [] for name, param in model.named_parameters(): if 'bn' in name or 'bias' in name: bn_params.append(param) else: other_params.append(param) optimizer = optim.SGD([ {'params': other_params, 'weight_decay': 5e-4}, {'params': bn_params, 'weight_decay': 0}, ], lr=0.1, momentum=0.9)

逻辑说明:把 BN 的 weight、bias 和所有层的 bias 归到一组,weight decay 设为 0,其余卷积权重保持 5e-4。这样 BN 的缩放和平移参数不会被正则化拉向零,训练更稳定。

参数说明:'bn' in name匹配所有 BatchNorm 层的参数,'bias' in name匹配卷积和全连接的偏置。如果你的模型里有其他需要排除的参数,可以继续加条件。

最后说一个我自己的习惯:每次改完结构或超参,先跑 5 个 epoch 看 loss 下降趋势,如果 5 轮内 loss 没降到 2.0 以下,大概率是学习率或初始化有问题,不用等 200 轮。这个习惯帮我省过很多次通宵。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 8:26:05

《认知觉醒》93页深度读书笔记PPT:大脑机制到行动落地的完整拆解

做读书笔记PPT这件事,我前后整理了将近两个月。市面上讲《认知觉醒》的内容很多,但大多停留在金句摘抄和概念复述,真正能落到行动上的少。所以我做这套93页的深度读书笔记时,给自己定了一个标准:每一页都必须能回答三个…

作者头像 李华
网站建设 2026/10/13 8:25:35

宠物健康咨询系统毕业设计全解:SpringBoot+Vue+MySQL实现

毕业设计选“宠物健康咨询系统”这个题目的人,十有八九是看中它题材讨喜、贴近生活,又带着完整的业务闭环。真做起来才发现,这件事并没有想象中那么轻松:用户端要管宠物档案,医生端要写问诊记录,管理端还要…

作者头像 李华
网站建设 2026/10/13 8:25:34

AI短视频怎么做?4分钟搞定历史题材成片的全流程拆解

1. 先别急着问“怎么做”,先搞清楚历史短视频为什么天然适合AI 先说结论:我这条历史短视频从确定选题到发布,满打满算用了不到4分钟的制作时间,其中还有一小半花在等软件响应上。成品发出去之后,播放和互动都超出我最初…

作者头像 李华
网站建设 2026/10/13 8:25:11

剪叉式举升机设计原理与工程实践解析

1. 项目概述:当机械设计遇上立体拼图第一次见到直立式剪叉举升机时,我正蹲在汽修厂角落观察师傅检修底盘。那个缓缓展开的金属结构瞬间抓住了我的眼球——这分明就是把学生时代的伸缩圆规放大成了工业级工具!这种利用平行四边形变形原理实现垂…

作者头像 李华
网站建设 2026/10/13 8:24:57

COMSOL模拟原油电脱水:液滴聚结机理与优化

1. 项目背景与核心价值原油脱水是石油工业中至关重要的处理环节。在油田开采过程中,原油通常与地层水形成乳化液,这种油水混合物会显著增加运输成本、加速管道腐蚀并影响后续精炼工艺。传统重力沉降法处理高粘度原油时效率低下,而电脱水技术虽…

作者头像 李华