简介:面向机器学习初学者与Kaggle参赛者,这份资源以CIFAR-10图像分类赛题为切入点,演示如何用PyTorch完成从数据读取、模型搭建到训练预测与提交结果的完整流程。压缩包共1017个文件,约2.34MB,其中1006张PNG图片构成精简版训练/测试图像,4个Python脚本与2个PyTorch notebook分别承担数据预处理、模型定义和训练逻辑,3个CSV文件包含标签与提交结果,另有2个pyc缓存文件便于复现。已有180人学习浏览,适合希望通过实战代码快速建立深度学习图像分类项目认知的读者。借助notebook的分步讲解和可运行脚本,使用者能对照理解卷积网络在真实比赛中的调参思路、预测结果整理及Kaggle提交格式,并基于示例图片和CSV快速跑通一个最小可用方案。
1. 别急着写模型,先弄懂 CIFAR-10 到底在考什么
我第一次打 Kaggle 的 CIFAR-10 比赛时,犯过一个特别典型的错误:花了一整天把 ResNet 代码调通、跑出 baseline,然后志得意满地点了 Submit,最后看到的准确率数字让我整个人都清醒了。说实话,这个比赛看起来非常简单,但真正想拿到一个体面的分数,远不是"把数据集丢进 CNN 里训练"这么简单。
先把这个比赛的基础信息说透。CIFAR-10 是一个 60 张 32x32 彩色图片组成的数据集,共分 10 个类别:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、轮船、卡车。其中训练集 50000 张,测试集 10000 张。Kaggle 上的比赛版本通常会把官方测试集留出一部分作为 Public Leaderboard(公开榜),另一部分作为 Private Leaderboard(私有榜),你最终的名次由私有榜决定。
这里要特别留个心眼:很多人看到 32x32 就觉得"这图片这么小,随便搞搞不就 90% 正确率了吗?" 但放大看你会发现,CIFAR-10 的图片分辨率极低,一只猫可能就只有一小坨像素,人眼都容易认错,模型更是如此。所以这个比赛本质上考的不是"你会不会搭 CNN",而是数据策略、正则化手段、训练技巧和模型集成这几项能不能有效组合。
再讲讲评价指标。CIFAR-10 用的是 Accuracy(准确率),也就是预测正确的图片占比。别看它简单,在类别分布均衡的数据集里,Accuracy 是最直观也最容易被刷的指标。纯随机猜是 10% 准确率,线性模型大概能到 40%,一个正经的 ResNet 轻松到 90% 以上,但要再往上走,每 1% 的差距都需要成倍的算力和调参功夫。
我还想强调一点:参加 Kaggle 比赛,先读明白数据说明远比先写代码重要。CIFAR-10 看起来人畜无害,但它的数据分布、类别平衡、是否包含数据噪声,都会直接影响你的方案选择。比如你必须确认测试集是否经过了与训练集相同的数据预处理,否则你本地验证准确率高,提交上去反而下降,这种事在 Kaggle 上并不罕见。
2. 环境准备与数据加载:这里卡住了 80% 的新手
2.1 用 Anaconda 创建一个干净的环境
很多初学者一上来就在全局环境里装 PyTorch,等到要装第二个项目依赖时,版本冲突能让你怀疑人生。我强烈建议用 Anaconda 或 Miniconda 为这个比赛单独建一个环境。做法很简单:
conda create -n cifar10 python=3.10 conda activate cifar10 conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia注意,pytorch-cuda=12.1这个参数取决于你的显卡驱动支持的 CUDA 版本。你可以先跑nvidia-smi看右上角的 CUDA Version,那个是驱动支持的最高 CUDA 版本,只要这个数字大于或等于你要安装的 CUDA 版本就可以。简单说:不是 CUDA 装得越高越好,而是要匹配你的驱动。
如果你没有独立显卡,或者显卡显存不够(小于 6GB 会比较吃紧),也别急着放弃。CPU 也能跑通整个流程,只是训练速度会慢上很多。环境变量设置CUDA_VISIBLE_DEVICES=""或者直接不装 CUDA 版本,PyTorch 会自动退回 CPU 模式。
2.2 理解 torchvision 内置数据集与 DataLoader 的协作逻辑
很多人上来就下载 Kaggle 上的 CIFAR-10 数据文件,然后自己写解析代码,这一步没必要。torchvision 已经内置了 CIFAR-10 数据集,你只需要告诉它"我要下载到哪里、要不要训练集"就行:
from torchvision import datasets, transforms train_dataset = datasets.CIFAR10( root='./data', train=True, download=True, transform=train_transform ) test_dataset = datasets.CIFAR10( root='./data', train=False, download=True, transform=test_transform )如果网络不好下载失败,手动去下载cifar-10-python.tar.gz放到./data下对应目录也能解决问题。
DataLoader 则是负责把 Dataset 里的数据一批一批地取出来送给模型训练,核心参数有这么几个:
train_loader = DataLoader( train_dataset, batch_size=128, shuffle=True, num_workers=4, pin_memory=True )batch_size:每次喂给模型多少张图,通常取决于显存大小,128 或 256 都算常见。shuffle:训练集必须设为 True,把打乱顺序的样本交给模型,防止模型学到顺序相关的偏差。num_workers:用几个子进程去做数据加载和预处理,Windows 上建议设为 0 或 2,Linux 上可以往上调,但过高反而会拖慢速度。pin_memory:设为 True 后,主机内存的数据搬到 GPU 显存时会走更快的通道,对训练吞吐有实际收益。
batch_size的选择其实对模型收敛有直接影响。过大时收敛慢、容易收敛到 sharp minimum(泛化差的尖底),过小则梯度噪声大、训练不稳定。CIFAR-10 场景下,128 是一个很均衡的起步值,我实测下来效果不错。
2.3 用数据集划分验证训练效果
我从不直接用官方测试集来评估模型,因为提交次数有限,每天都有提交上限,一旦提交多了,就会被排行榜惩罚(俗称"刷榜"),反而影响最终排名。所以我都会先从训练集里划一部分出来当验证集:
from torch.utils.data import random_split train_data, val_data = random_split( train_dataset, [45000, 5000], generator=torch.Generator().manual_seed(42) )分成 45000 训练、5000 验证的好处是:训练过程中可以实时监控验证准确率,据此判断模型有没有过拟合、学习率策略要不要调整。最后再拿真正留出的官方测试集提交到 Kaggle,每一分提交都花在刀刃上。
3. 数据增广:别嫌麻烦,这是 90% 与 95% 的分水岭
3.1 不再"裸奔"的预处理 Pipeline
CIFAR-10 只有 50000 张训练图片,如果直接喂给模型,哪怕是 ResNet 这种效果不错的架构,也很容易过拟合——训练集准确率蹭蹭往上涨,验证集准确率却早早封顶。数据增广(Data Augmentation)是解决这个问题最直接、最便宜的手段。
我测试过很多组增广配置,最终长期使用的一套是:
train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.Transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize( mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616) ) ]) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize( mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616) ) ])逐个解释一下:
RandomCrop(32, padding=4):先把图片四周补上 4 个像素的 0(默认模式),再做一次随机裁剪回 32x32。等于每次训练看到的图片都发生了小幅位移,模型被迫学得更稳健。RandomHorizontalFlip:以 50% 概率水平翻转。CIFAR-10 中大多数类别翻转后语义不变(比如卡车翻转后还是卡车),这是算力性价比极高的一种增广。ColorJitter:给图片亮度、对比度加一点随机扰动,提高模型对光照变化的鲁棒性。注意幅度不要太大,否则会让图片失真、反而损害性能。Normalize:把像素值从 [0,1] 区间转换为以 0 为中心、方差为 1 的分布。这个操作必须做,否则网络训练会明显变慢,甚至在深层网络中梯度爆炸。
验证集和测试集只做标准化,绝对不做随机增广,这是为了保证评估指标稳定。一个小细节:验证/测试增广中的mean/std值是全官方 CIFAR-10 数据集的统计值,不是自己拍脑袋编的,直接用上面这组数字即可。
3.2 CutOut、MixUp、CutMix 到底该不该用
基础的 RandomCrop + Flip 能帮你稳定站上 90% 以上的准确率,但想突破 95%,就得考虑更高阶的增广或训练策略了。这里我说三个常见的,以及我自己的使用感受。
CutOut(也叫 RandomErasing):随机擦除图片中的一块小区域,强迫模型不要只依赖局部信息。它对 CIFAR-10 提升效果不错,而且实现很简单,PyTorch 里torchvision.transforms.RandomErasing可以直接用。对 32x32 的输入尺寸,RandomErasing(p=0.7, scale=(0.01, 0.1))是个可靠的配置。
MixUp:按比例把两张训练图混合,同时把标签也按同样的比例混合。做法是每次随机取两个样本和一个混合系数lambda,输出变成:
mixed_x = lambda * x1 + (1 - lambda) * x2 mixed_y = lambda * y1 + (1 - lambda) * y2MixUp 在 CIFAR-10 上稳定有提升,但直观感受是训练收敛变慢,需要配合更长的 epoch 才能看到好处。它和 CutOut 不是互斥关系,有人两者都用,也有人只选其一,属于"预算充足就上,预算有限优先保住基础增广"的类型。
CutMix:把一块区域从一个样本里切下来,贴到另一个样本上,标签按面积比例混合。它在很多 Kaggle 图像比赛里是冠军常客技巧,比 MixUp 更适合 CIFAR-10 这种形状信息重要的场景。
我的建议是:初学阶段先把基础增广做好,保证模型不快速过拟合;当你能稳定跑到 93%、94% 以后,再引入 MixUp 或 CutMix 这一类高阶方案。一步到位反而容易让问题变复杂,排错难度上升,收益却说不准。
4. 模型与训练:从 ResNet18 起步的进阶路线
4.1 为什么 ResNet18 是性价比极高的第一选择
CIFAR-10 的图只有 32x32,用不着那种为 ImageNet 千分类设计的超深网络,很多强模型在这类任务上反而吃亏。ResNet18 大概几百万参数,单张图处理很快,训练时间可控,效果已经能轻松超过 90%,所以我先建议用它跑通全流程。
torchvision 里提供了 CIFAR-10 专用的 ResNet 变体,注意不要直接引通用的torchvision.models.resnet18,因为通用版本第一层是 7x7 步长 2 的卷积和最大池化,对 32x32 的输入并不合适。最省事的做法是把第一层改成kernel_size=3, stride=1, padding=1,去掉第一层后的池化。也可以直接导入现成的变体:
from torchvision.models import resnet18 model = resnet18(num_classes=10)不过更靠谱的是手动调整输入层,这是 CIFAR-10 任务的关键细节:
import torch.nn as nn model.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False) model.maxpool = nn.Identity()改完以后,模型对 32x32 输入的适应度会明显提升,且参数量几乎不变。
4.2 关于优化器:不要无脑选 Adam
这是个老生常谈的话题,但我还是要强调:CIFAR-10 这类任务里,SGD + Momentum + CosineAnnealing 往往比 Adam 训练效果更稳定,最终准确率也更高。并不是说 Adam 不行,而是对于图像分类这类计算机视觉任务,SGD 在调整得当的情况下,更容易收敛到平坦的极小值,泛化性能更好。
我常用的训练配置:
- 优化器:SGD,
momentum=0.9 - 初始学习率:
0.1(配合 batch_size=128 的场景) - 权重衰减:
5e-4 - 学习率策略:CosineAnnealingLR,从 0.1 逐步降到接近 0
- Epoch:50 左右
如果你坚持用 Adam,建议把学习率降到0.001,并配合 weight decay。但根据我个人的经验,同样 50 个 epoch,SGD 路线在 CIFAR-10 上通常能比 Adam 高出 0.5 到 1 个百分点的准确率。
4.3 训练循环模板:把 checkpoint 和日志做好
初学阶段最容易踩的坑就是训练到一半程序崩了,结果所有进度全部归零。所以我建议从一开始就建立 checkpoint 机制,每训练完一个 epoch 就保存一次模型权重和优化器状态:
torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_acc': best_acc, }, 'checkpoint.pth')恢复训练时:
checkpoint = torch.load('checkpoint.pth') model.load_state_dict(checkpoint['model_state_dict']) optimizer.load_state_dict(checkpoint['optimizer_state_dict']) start_epoch = checkpoint['epoch'] + 1另外,把每个 epoch 的训练 loss、训练准确率、验证准确率记录到一个日志文件或者直接用tensorboard可视化。刚开始觉得麻烦,等到调参时你会发现这些历史数据是判断"哪一步改动起了作用"的重要依据。
我在实际训练中还养成了一个习惯:不仅保存最优准确率的 checkpoint,还会保存最后一个 epoch 的权重。因为有些集成策略会用到最后一轮模型,或者你权重衰减策略在最后几个 epoch 带来了某种特殊状态。
5. 预测与集成:别人提分的隐藏手牌
5.1 TTA(Test Time Augmentation)几乎零成本的提分方案
TTA 的原理很简单:预测时不再对图片只做一次前向传播,而是对同一张图的多个增广版本各做一次预测,最后把预测概率平均起来作为最终输出。以水平翻转为例,一张测试图可以先正常预测一次,再水平翻转后预测一次,两条概率向量取平均,最终预测结果通常会更稳定。
model.eval() with torch.no_grad(): logits1 = model(x) logits2 = model(torch.flip(x, dims=[3])) probs = (torch.softmax(logits1, dim=1) + torch.softmax(logits2, dim=1)) / 2 pred = probs.argmax(dim=1)就这么简单的一行增强,通常能带来 0.2% 到 0.5% 的准确率提升。CIFAR-10 里有几个类别边界模糊,TTA 正好能平抑这种偶然性,而且是零成本额外训练,属于纯收益操作。
5.2 模型集成:单模 94%,三模平均可能 95%
单个模型到了 94% 左右,想再往上突破非常吃力。但把几个不同随机种子训练出来的模型做预测平均,往往能有意外之喜。
最简单的做法是 K 折交叉验证。把训练集分成 5 折,每次用其中 4 折训练、1 折验证,训练出 5 个模型。预测时,5 个模型分别对测试集预测,把 5 组概率取平均:
final_probs = np.mean(all_pred_probs, axis=0) predictions = np.argmax(final_probs, axis=1)这种集成的收益来源,一是在于不同模型看到了不同的训练数据,二是在于不同随机种子带来的模型差异。只要模型之间有一定差异,平均后准确率大概率比最好的单个模型还高。
你也可以在此基础上做加权集成:用验证集上每个模型的准确率作为依据,给高准确率的模型分配更高权重。实际操作中,简单平均通常已经能拿到大部分收益,加权平均略复杂一点,但有时能再挤一点分数出来。我的经验是,差距在 0.5% 以内的模型简单平均就够了,如果模型水平参差不齐再考虑加权。
这里额外提醒一句:Kaggle 在比赛后期会切换到你没见过的 Private 数据上评估,集成策略在这种"未知数据"上通常依然稳定,因为我们只是做了概率平均,并没有过分贴合公开榜。这一点对最终名次的影响非常关键。
6. 实战中的排错与技巧:这些坑我都踩过
6.1 训练 Loss 不降,应该先排查什么
如果你发现 Loss 一直降不下去,准确率徘徊在 10% 左右,基本等于随机水平。此时先别急着怀疑模型结构,按以下顺序排查:
- 检查数据预处理是否正常:把经过 transform 的 batch 保存成图片看一眼,确认没有出现全黑、全白或者严重失真的情况。
- 检查标签与图片是否一一对应:CIFAR-10 的悲剧在于有些图片人眼都很难辨认,如果你误把标签顺序搞错,训练再久都白搭。
- 检查学习率是否太大/太小:学习率太大会导致 Loss 震荡甚至发散,太小则收敛极慢。SGD 用 0.1 起步比较稳。
- 检查网络最后输出层数量是否为 10:很多人复制 ImageNet 代码时忘了改最后分类头,导致输出维度不匹配,报错还好,如果恰好输出 1000 类然后你只取了前 10 个输出,那坑就大了。
6.2 验证集准确率停滞在 90% 以下:大多不是模型架构问题
如果你的验证准确率卡在 80%~90% 之间,多半不是模型不够强,而是数据策略出问题了。首先检查训练集和验证集之间是否存在分布差异;其次检查是否忘了做数据增广;最后再检查 Normalize 的均值和方差是不是用错了,一旦标准化错误,模型的训练效率和最终上限都会受很大影响。
6.3 显存不够怎么办
如果你在训练时报出 "CUDA out of memory",先不要急着换显卡。有几个立刻见效的救急手段:
- 减小 batch size,比如从 128 降到 64 或 32,同时按比例调低学习率(
lr = base_lr * batch_size / 128)。 - 使用
torch.cuda.amp混合精度训练,显存占用能降低近一半,而且训练速度还可能变快。 - 关闭
pin_memory=True,有时能省下一部分瓶颈资源,但影响不是很大。 - 减少
num_workers,数据缓存对显存的影响较小,但能降低整体系统开销。
6.4 训练中途断掉的续训问题
我在一次比赛里训练了 40 个 epoch,结果机器意外重启,因为没有保存 checkpoint 的坏习惯,所有进度全部丢失。复盘之后学乖了,现在每次至少保证每个 epoch 结束时保存一次模型,并且把best_acc单独记录,训练日志和权重存在同一个目录下。
还有一个小技巧:写代码时在训练循环外预留--resume参数,这样即使某次比赛中途断线,也能在恢复到 checkpoint 后立刻从断点继续训练,不浪费任何一小时的算力。
6.5 别忽略代码的"可复现性"
Kaggle 比赛里,你可能一次跑出 93.5%,另一次只跑出 92.8%,模型结构完全相同,差别只来自随机性。为了让自己调参时不被随机性迷惑,我建议固定所有能固定的随机种子:
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注意,固定随机种子后训练速度可能略有下降,但对判断"这次改动到底有没有用"至关重要。实际比赛中 GPU 和硬件不同也会影响结果,所以在本地调试调参时固定种子,最终训练时可以不固定种子让模型探索更多可能性。
写在最后:如果你想低成本参加一次 Kaggle,CIFAR-10 是不错的起点
我个人觉得,CIFAR-10 这个比赛就像开了作弊器一样,把数据量、任务复杂度、算力需求都压到了刚好的水位:你不需要上万张图片的下载能力,不需要动辄几周的预训练时间,也不需要花一大笔钱买显卡,就能把完整的机器学习项目闭环——数据处理、模型搭建、训练调参、验证评估、预测提交——从头到尾跑一遍。
很多人在入坑机器学习时总想着"等我准备好再参加比赛",但 Kaggle 的真相是,你永远不可能"完全准备好"。打一次完整比赛,遇到报错、踩坑、调参失手、提交被拒,这些混乱时刻恰恰是成长最快的时候。CIFAR-10 就是这么一块低门槛试金石,先把这套流程走通,之后你想去打更高阶的 ImageNet 分类、目标检测,至少不会在工程层面被卡住了。
如果你有 GPU,我建议直接把 batch size 拉满,配 50 个 epoch 的 SGD + CosineAnnealing,再叠一个简单的 TTA,感受一下从 90% 冲刺 95% 的过程。如果你没有 GPU,用 CPU 跑一个小的 ResNet18 加上基础增广,也能在几小时里拿到 80% 以上的结果,用来理解整个流程绰绰有余。动起手来,跑通一个结果比看十篇教程都管用。
本文还有配套的精品资源,点击获取