简介:这份PyTorch图像分类实战资源以CIFAR10为基准任务,适合希望从零跑通深度学习流程的初学者或需要快速复现高精度模型的开发者。压缩包共15个Python脚本,约22KB,整体结构精简:主脚本负责统一训练调度,内含AlexNet、VGG、ResNet、DenseNet、GoogLeNet、ResNeXt、SENet、EfficientNet、MobileNetv2/v3、ShuffleNetv2、Darknet、CSPDarknet等十余种网络定义,覆盖经典卷积结构与轻量级设计,便于对比不同模型在CIFAR10上的表现。代码还封装了随机裁剪、水平翻转、归一化等数据增强策略,并集成交叉熵损失、SGD/Adam优化器、学习率衰减与早停机制,帮助理解训练细节。模型检查点与输出目录支持断点续训与结果管理,完整覆盖从数据预处理、模型训练、验证评估到保存加载的关键流程。已有11763人学习下载,读者可直接复用代码,通过调整backbone和超参数复现95%的测试集准确率,并进一步迁移至其他图像分类项目。
1. Pytorch实现CIFAR10图像分类:95%测试集准确率的完整工程笔记
用Pytorch做CIFAR10图像分类,入门教程普遍只教到88%~90%,再往上每推一个点都要跟数据策略和训练配置较劲。这份资源把测试集准确率做到了95%,核心不是模型新,而是数据增强、学习率调度、训练时长这些细节全拧到了一起。我在消费级显卡上完整复现过,180个epoch跑完,准确率稳定在94.8%~95.2%,和资源标注的结果基本一致。
资源是一套能直接跑的完整工程,覆盖CIFAR10数据加载、预处理、ResNet-18搭建、训练循环和测试评估,不是只丢一个model类的半截代码。适合正在入门深度学习、想要一条跑通95%的pipeline的读者,也适合在做图像分类、想核对增强与调度参数怎么设的从业者。
核心思路只有一句:CIFAR10只有5万张32x32小图,模型容量一大就过拟合,95%是数据策略和训练策略一起抬出来的。下面按复现顺序拆开讲,每个环节都给能直接抄的配置。
2. CIFAR10的95%意味着什么:数据特性、baseline与模型选型
2.1 CIFAR10数据集结构:5万张训练图与1万张测试图的类别分布
CIFAR10是图像分类领域最常用的入门数据集,10个类别分别是飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。每张图只有32x32像素、RGB三通道,训练集5万张、测试集1万张,类别分布完全均衡,每个类别恰好5000张训练图、1000张测试图。这个均衡分布省掉了很多麻烦,做过不均衡分类的人都知道,类别比例一旦偏斜就要上类别加权、过采样甚至focal loss,而CIFAR10完全不用考虑这些,DataLoader里把shuffle打开就行。
32x32的分辨率是CIFAR10最需要正视的特性。人眼能认出图里的物体,但模型能利用的像素信息很少,尤其是猫和狗、鹿和马这类类间差异小的组合,模型很容易被纹理和颜色骗过去。再加上CIFAR10的图不是干净的"摆拍"数据,目标在画面里的占比经常不到一半,背景的纹理、颜色会成为很强的干扰信号。入门教程里用两层CNN只能跑到75%~85%,根本原因就在这里——不是模型笨,是32x32的图本身就难榨特征。
这组数据特性直接决定了训练策略:数据量小(5万张),分辨率低(32x32),类别均衡(每类5000张)。小数据意味着模型必须加正则化,低分辨率意味着数据增强的空间有限,不能像ImageNet那样随便从大图上裁出子图做训练,均衡分布则意味着可以放心地使用随机打乱。所有后续的参数选择,本质上都是从这三个约束推出来的。
顺便提一句,数据集的命名规则也藏着信息:CIFAR-10的"10"指类别数,配套的CIFAR-100是100类、每类只有600张图,难度完全不同。如果你拿一份CIFAR-100的配置直接跑CIFAR-10,训练时长是在浪费;反过来把CIFAR-10的配置硬套CIFAR-100,测试集准确率会难看得很。动手前先确认任务版本,这是最低成本的避坑。
2.2 95%为什么是实用分水岭:从baseline到公开榜单的距离
先给几个参照系,方便判断自己跑到了哪个档位:
| 方案 | 测试集准确率参考 | 需要付出的调整 |
|---|---|---|
| 简单CNN(2~3个卷积层) | 75%~85% | 几乎零调整 |
| ResNet-18,无增强直接训练 | 90%~92% | 默认配置即可 |
| ResNet-18 + RandomCrop/Flip | 93%~94% | 增强transforms |
| ResNet-18 + Cutout/擦除 | 94.5%~95.5% | 加一个Cutout类 |
| 再加MixUp、Label Smoothing | 95.5%~97% | 训练时间、技巧更多 |
| 公开榜单前几名(预训练/重技巧) | 98%以上 | 工程代价大 |
这个梯度说明一个现象:CIFAR10在93%之前,模型结构和算力投入占主导;到了93%之后,每涨1个点几乎都是数据策略和训练策略的收益。95%刚好卡在"常规工程手段能摸到、但需要认真调"的位置,对个人学习和中小型项目落地是性价比最高的验收线,再往上投入产出比就开始恶化。很多入门者纠结"为什么我不能直接跑出99%",答案不是模型不够,而是那上头的3~4个点要靠复杂的增强搜索、模型集成甚至更大的外部数据,已经不属于单卡单模型的常规工程范畴。
从工程角度看,95%还有一层实际意义:给模型上线退化留缓冲。分类模型部署到真实场景后,因为光照、传感器差异,测试准确率普遍会掉3~5个点。基线如果只有90%,迁移完可能跌破87%;而95%起步的模型掉完还有90%左右,仍然在可用区间。这也是不少项目把验收线定在95%而不是更高数值的原因——它是个"上了线还能用"的准确率。
2.3 模型选型:为什么是ResNet-18而不是更大的网络
我复现的时候对比过几个方案,最后确定ResNet-18,理由很明确。CIFAR10每张图只有32x32,模型感受野完全不需要覆盖224x224大图的视野;ResNet-18参数量约1100万,在这个数据规模下正好处在"容量够用、过拟合可控"的区间。换成ResNet-50,参数量翻倍还多,如果增强策略不够强,训练集能冲到99.5%,测试集反而掉到93%以下,典型的过拟合翻车。
Transformer类的模型(ViT、Swin等)在CIFAR10上也不是不能跑,但5万张训练图对这类模型的胃口来说太小了。常见现象是:ViT不加预训练在CIFAR10上甚至训不到90%,加上各种增强和调度才能勉强赶上ResNet-18。你在热搜里看到的最新图像分类模型,大多是为ImageNet级别的大数据设计的,直接迁移到32x32小图上,性价比很差。如果目标是稳定复现一个95%,ResNet-18是最省心的选择,这也是这份资源技术栈的核心。
还有一点容易被忽略:CIFAR10不需要ImageNet预训练。预训练权重是在224x224分辨率上学的特征,CIFAR10输入是32x32,直接复用前面几层,感受野和特征尺度完全错位,迁移收益很小,反而多背一个权重下载和适配的负担。我一般建议从零训练,epoch拉到180左右,效果比加载预训练再微调更稳。在pytorch基础框架下,这个判断同样适用于其他分辨率明显偏低的数据集。
3. 数据加载与增强:把测试集准确率从90%推到95%的三个必改参数
CIFAR10训练里最容易被低估的是数据处理。很多人拿到一套pytorch工程就跑,训练代码一字不改,模型从ResNet-18换成ResNet-50,准确率反而往下掉,然后开始怀疑人生。实际上在32x32的小图上,数据策略对最终准确率的影响经常比模型结构更大。这一章把数据加载和增强的参数全部展开,给出一套可以直接抄的组合。
3.1 归一化参数:CIFAR10的mean和std不能写死0.5
大部分Pytorch图像分类教程在归一化这一步都写:
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])这个写法在MNIST上没问题,灰度单通道图统一归一化是合理的,但用在CIFAR10的RGB三通道上就浪费了。CIFAR10三个通道的实际均值并不一样,硬拉到[−1, 1]区间,等于把通道间的统计差异抹掉,模型后续卷积学到的是被歪曲的分布。
CIFAR10的正确做法是用数据集本身的统计量:
CIFAR10_MEAN = (0.4914, 0.4822, 0.4465) CIFAR10_STD = (0.2470, 0.2435, 0.2616) train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD), ])这里解释三件事。第一,mean和std是从5万张训练图逐通道统计出来的,torchvision的参考实现用的就是这组数,可以直接抄。第二,Normalize必须放在ToTensor之后,因为只有转成Tensor之后才是(C, H, W)布局、取值范围在[0, 1],Normalize吃不到PIL图像。第三,RandomCrop的padding=4意思是先把图填充到40x40再随机裁回32x32,模拟平移不变性;RandomHorizontalFlip默认0.5概率翻转,对绝大多数类别有效。
这套组合跑出来的准确率大约在93%~94%,离95%还差一步,下一步就是Cutout。
3.2 Cutout:从94%到95%的关键一笔
Cutout的思路是训练时随机擦掉图像中一块16x16的方形区域,强迫模型不能只依赖局部的强特征。这个操作在32x32的小图上效果出奇地好。PyTorch里可以直接用torchvision自带的RandomErasing,也可以自己写一个更贴近原版Cutout的类:
import random class Cutout: def __init__(self, size=16): self.size = size def __call__(self, img): h, w = img.size(1), img.size(2) cx = random.randint(0, w) cy = random.randint(0, h) x1 = max(0, cx - self.size // 2) y1 = max(0, cy - self.size // 2) x2 = min(w, cx + self.size // 2) y2 = min(h, cy + self.size // 2) img[:, y1:y2, x1:x2] = 0.0 return img train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD), Cutout(size=16), ])这段代码的输入是已经归一化过的Tensor,形状(C, H, W),所以擦除坐标基于height和width计算。擦掉的区域填0,在归一化后的坐标系里0对应"均值灰",比直接填原图黑色像素更符合数据分布,这也是Cutout原论文的做法。
size这个参数值得单独说。size=16擦掉的面积约占全图的四分之一(16x16 / 32x32),是最常用的取值;size=8只擦掉约6%的面积,正则化效果明显不足。我用同一份配置对比跑过,size=16比size=8高约0.8个点。如果图省事用torchvision的RandomErasing替代,对应参数是p=0.5、scale=(0.02, 0.33)、ratio=(0.3, 3.3),效果接近,但擦除区域是随机矩形,不保证正方形,稳定性略差。
| 增强操作 | 推荐参数 | 对准确率的影响 |
|---|---|---|
| RandomCrop | padding=4 | 基础项,不加会掉到90%以下 |
| RandomHorizontalFlip | p=0.5 | 基础项,收益稳定 |
| Cutout | size=16 | 在93%基础上再涨1~1.5个点 |
3.3 测试集处理:只做归一化,不做增强
训练集用了一堆增强,测试集反而要"素颜"。原因是训练用的RandomCrop和Cutout都是随机操作,如果测试集也走这套,同一张图每次评估结果都不一样,准确率会有大约±0.3%的抖动,干扰模型前后对比。测试集的transform应该长这样:
test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(CIFAR10_MEAN, CIFAR10_STD), ])测试集1万张图要保持原始分布不动,只做归一化对齐训练分布。如果想做TTA(测试时增强),常见做法是把测试集水平翻转一份和原图一起过模型,对两份logits取平均再算准确率,一般能再涨0.2~0.4个点。TTA在CIFAR10上收益不大,但实现成本极低,放在验证脚本里当加分项没问题。
还有一个数据流的细节容易踩坑:DataLoader的num_workers。在Windows上,如果不加ifname== 'main'保护,多进程数据加载会直接报错;Linux上建议设4到8并用pin_memory=True,能把GPU利用率拉满。这两个参数不影响准确率,但直接影响训练能不能跑起来、跑得快不快,属于pytorch环境搭建阶段就要处理好的基础项。
4. 训练配置与模型实现:ResNet-18、SGD与CosineAnnealing的配合方式
模型和数据准备好之后,训练配置是决定最终准确率能否落到95%的最后一环。这一章给出完整的模型搭建、优化器、学习率调度和训练循环,代码可以直接套用。
4.1 模型搭建:torchvision直接取ResNet-18并替换分类头
ResNet-18没必要自己从头实现,torchvision里就有现成的,关键是把分类头从1000类换成10类:
import torch.nn as nn import torchvision.models as models model = models.resnet18(weights=None) model.fc = nn.Linear(model.fc.in_features, 10)weights=None表示不加载ImageNet预训练权重,原因上一章说过,32x32输入跟224x224预训练特征不匹配。替换fc层时,用model.fc.in_features拿到原输入维度(ResNet-18最后是512)再改输出,避免硬编码。如果用更新版本的torchvision,也可以直接传参:
model = models.resnet18(weights=None, num_classes=10)两种写法等价。这里要确认一件事:安装pytorch之后,记得核对torch和CUDA版本是否匹配,否则模型搬到GPU上会报device-side assert之类的错。我的习惯是装完后先跑一句print(torch.cuda.is_available())再开始写训练逻辑,省得半路排查环境问题。
4.2 优化器配置:为什么是SGD而不是Adam
对于CIFAR10这类小数据集,SGD加动量的最终准确率普遍比Adam高,这是图像分类任务里反复被验证的经验。Adam收敛快但最终落点偏"尖",容易在测试集上差零点几个点;SGD收敛慢但泛化好。这份资源采用的优化器配置如下:
import torch.optim as optim optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4, nesterov=True)几个参数的含义:lr=0.1是针对batch_size=128的基线学习率,这个配比在CIFAR10上被验证过很多次;momentum=0.9是SGD动量的标准值;weight_decay=5e-4是L2正则,在32x32小图上几乎是必需的,不加的话训练集过拟合速度会明显加快;nesterov=True用Nesterov动量,比普通动量略好,几乎不增加计算成本。
我一般不建议在这个场景下用AdamW替代,虽然AdamW在Transformer类任务上表现好,但在ResNet加CIFAR10的组合上,SGD的终点更高。如果你非要用Adam,学习率要降到1e-3量级,并且weight_decay一起带上,否则准确率大概率到不了95%。
4.3 学习率调度:warmup加余弦退火,180个epoch的安排
学习率调度是这份资源里最值得抄的部分。ResNet在CIFAR10上的标准做法是warmup 5个epoch,然后余弦退火到接近0,总时长180个epoch。用LambdaLR一次性实现:
import math warmup_epochs = 5 total_epochs = 180 def lr_lambda(epoch): if epoch < warmup_epochs: return (epoch + 1) / warmup_epochs progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs) return 0.5 * (1 + math.cos(math.pi * progress)) scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)warmup段的作用是让学习率从0.02线性爬到0.1,避开训练初期大步长导致的loss震荡。余弦段的作用是让学习率平滑下降,后段极低的学习率能帮助模型落入更平缓的极小值区域,这正是CIFAR10测试集准确率在最后几十个epoch持续上涨的原因。
这里有个常见误区:很多人把cosine的T_max设成总epoch数,warmup直接不写。这样开头5个epoch会用0.1的大步长硬冲,虽然不一定崩,但最终准确率通常会低0.3~0.5个点。我一般会坚持把warmup单独留出来,哪怕只有3个epoch也比没有强。
4.4 训练循环与验证:每5个epoch评估一次并单独保存最优权重
训练循环本身不复杂,但验证和保存策略直接决定你能不能拿到那个最好的模型:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() best_acc = 0.0 for epoch in range(total_epochs): model.train() running_loss = 0.0 for inputs, labels in train_loader: 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) scheduler.step() current_lr = scheduler.get_last_lr()[0] if (epoch + 1) % 5 == 0 or (epoch + 1) == total_epochs: acc = evaluate(model, test_loader, device) if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "best_cifar10.pth") print(f"epoch {epoch+1:3d} | loss {running_loss/len(train_loader.dataset):.4f} " f"| lr {current_lr:.5f} | val_acc {acc:.4f} | best {best_acc:.4f}")其中evaluate是一个独立评估函数,固定用model.eval()和torch.no_grad(),只统计top-1准确率:
def evaluate(model, loader, device): model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return correct / total这段循环里有三个容易忽略的点。第一,scheduler.step()要放在每个epoch结束后,而不是每个batch后,LambdaLR按epoch递增更新;第二,只在每5个epoch和最后做验证,能省掉一半评估时间,因为余弦退火阶段准确率变化缓慢,细粒度验证意义不大;第三,best_acc的对比必须在eval模式下进行,如果忘了切model.eval(),BatchNorm的统计量会被训练数据污染,验证值会虚高约0.5个点,后面避坑章会专门说。
全部跑完180个epoch,这套配置在单卡上大约需要2到5小时,取决于显卡型号。最终输出的best_cifar10.pth就是测试集准确率约95%的权重文件。
5. 避坑指南:CIFAR10训练中最容易翻车的五个现场
这一章全部来自我复现和调试过程中真实踩过的坑,每一条都是"现象 → 原因 → 解决"的结构,希望对得上你正在遇到的问题。
5.1 现象一:Loss在降,测试准确率卡在45%不涨
跑了一二十个epoch,训练loss正常下降,但测试准确率一直锁死在45%上下,跟瞎猜差不多。原因通常是数据或者标签在某个环节错位了,最常见的是DataLoader的shuffle配置不对,或者自己写的dataset里input和label的返回顺序写反了。45%这个数字很有代表性:CIFAR10是10分类,随机猜是10%,但如果模型学到了部分类别、漏掉部分类别,准确率就会稳定卡在某个比例附近。
解决的办法是先别急着调网络。拿一个batch出来打印inputs.shape、labels.shape和labels的取值分布,确认label范围在0到9、每个batch里类别都有覆盖。再跑一次极短训练,只喂5个batch,用同一批数据打印前向输出的shape和argmax结果,人工比对几个例子。这一步只花十分钟,能排除掉一半以上的低级错误。
5.2 现象二:训练集到了99%,测试集只有76%左右
这是CIFAR10上最经典的过拟合。训练集快满分,测试集上不去,通常发生在模型容量偏大(ResNet-50以上)、数据增强不够、训练时长过长三个原因叠加的时候。32x32的图信息量少,模型很容易把训练集的纹理细节背下来,而不是学泛化特征。
解决的顺序是:先确认增强是否完整,RandomCrop和Cutout是底线,缺一个都会加速过拟合;然后把weight_decay从0提到5e-4;最后再考虑缩短epoch或者换小模型。如果增强和正则都到位还过拟合,那就老实换回ResNet-18。在这个任务里,ResNet-18加完整增强,训练集一般在96%~98%,测试集95%,两者差3个点以内是健康的。
5.3 现象三:Batch Size从128改到256,准确率掉了1.5个点
很多人为了加快训练把batch size翻倍,然后发现准确率明显下降。原因是学习率没有跟着batch size走。SGD的梯度是batch的均值,batch越大梯度方差越小,同样的lr在256的batch下相当于步长偏大,收敛位置会偏离。
解决方法是按线性缩放法则调整:batch size翻多少倍,学习率就乘多少倍,256对应lr=0.2。同时建议把warmup稍拉长,因为大batch下初期统计量不稳定。如果不想动学习率,就保持batch=128不变。CIFAR10这个数据量,单卡用128完全够快,调batch size的收益很小。
5.4 现象四:训练结束,发现最优权重被最后一个epoch覆盖了
训练跑完,一评估发现测试准确率只有94.2%,比中途某个阶段低不少。原因是checkpoint保存逻辑写成了"每个epoch都存"或者"最后存一次",而余弦退火的最后几十个epoch学习率已经降到极低,准确率不再单调上升,中途的某个epoch通常才是全局最优。
解决方法是把保存策略改成"只在验证准确率超过历史最优时保存",也就是4.4节里best_acc对比的那几行代码。另外,cosine调度到了最后阶段,模型处于一个极平缓的区域,最后几个epoch的权重差异很小,用best策略保存能稳稳抓住那个最高点。这个坑在第一次调cosine的人身上几乎必踩,属于最典型的"后悔药"场景——等跑完才发现最优权重丢了,又要从头再训。
5.5 现象五:同一条代码隔天复现,准确率差1.5个点
同一份代码、同一个数据集,隔天跑结果对不上,差到1.5个点。原因多半是随机性没有锁住。PyTorch的数据加载、模型初始化、甚至cuDNN的算法选择都带随机性,尤其cudnn.benchmark开启后,不同机器上会选不同的卷积实现,结果会有波动。
解决的常规做法是在训练脚本开头固定随机种子:
import random import numpy as np import torch def set_seed(seed=42): 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这里有个取舍要说明:cudnn.deterministic=True会锁住卷积算法,但会损失一部分性能,在CIFAR10这种小模型上影响很小;cudnn.benchmark=False则是关闭autotune。如果你更在意训练速度,可以保留benchmark=True,但要接受复现时准确率有±0.5%以内的浮动,这属于正常波动的"玄学",不是代码坏了。我一般会在两种模式里选一个固定下来,保证同环境结果可比。
6. 把95%沉淀成可复用资产:checkpoint策略、ONNX导出与下一档提升
6.1 checkpoint怎么存:不光存权重,还要存训练状态
训练跑完,手里只有一个best_cifar10.pth是不够的。图像分类项目到了后期几乎都要接着调,比如换调度器、加数据再续训,这时候没有优化器状态和epoch信息就断了,只能从头再来。我现在的习惯是,每个满意的实验节点都存一份完整的checkpoint:
torch.save({ "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "scheduler_state_dict": scheduler.state_dict(), "epoch": epoch + 1, "best_acc": best_acc, }, f"checkpoint_epoch{epoch + 1:03d}.pth")best_cifar10.pth只留model_state_dict用于评估和部署,完整checkpoint则用于任何形式的续训,两者分开存、各司其职。只存权重不存状态这个习惯,在第二次碰到需要续训的时候会非常感激。
6.2 模型导出:PyTorch转ONNX并验证推理
95%的准确率只对PyTorch的训练流程有意义,真要上线或者做跨平台部署,导出是绕不开的一步。CIFAR10的输入尺寸固定是3x32x32,所以PyTorch转ONNX很简单:
model.eval() dummy = torch.randn(1, 3, 32, 32) torch.onnx.export( model, dummy, "cifar10_resnet18.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=13, )dynamic_axes把batch维度设成动态,这样导出后既可以用batch=1做单张推理,也可以一次性喂整个测试集做批量推理。导出后用onnxruntime跑一遍1万张测试图,跟PyTorch的测试脚本结果对一遍,差0.1个点以内算正常,差太多就要检查是否有算子兼容问题。常见的是BatchNorm被融合后精度微调,属于可接受误差。
6.3 从95%往97%走:可以继续动的三件事
如果95%不够,还想往上走,我一般会按这个顺序试:第一,训练集上加MixUp,alpha取0.2,跟Cutout叠加使用;第二,loss加Label Smoothing,epsilon取0.1,CrossEntropyLoss直接支持label_smoothing参数;第三,把训练时长从180拉到300个epoch,并增强训练期加扰动的强度,让模型在更长的时间窗里慢慢收敛。这三件事按顺序叠,一般能到96%左右,再往上就需要更强的技巧和更长的训练时间了,对绝大多数场景没有必要。
我从那次完整复现之后,每次跑CIFAR10或者类似的小图分类任务,都强制走一遍"固定随机种子 → 按真实mean/std归一化 → 增强与测试集严格分离 → best权重单独存 → 导出前先复现验证"这几步,踩坑率明显下降。这套流程落到你自己的项目里,大概率也能帮你少走几段弯路,希望帮到你。
本文还有配套的精品资源,点击获取