☰
CIFAR-10图像分类实战:从PyTorch环境到Kaggle冲榜完整指南
2026/9/25 23:34:21 网站建设 项目流程

简介:一份面向Kaggle CIFAR-10图像分类实战的PyTorch资源包,专为希望从代码入手学习深度学习的读者设计,也可作为数据科学竞赛入门者的参考。压缩包共1017个文件,大小约2.34MB;其中4个Python脚本用于数据加载、模型搭建与训练流程,2个Jupyter Notebook提供分步说明和运行结果,3个CSV文件对应标签及提交格式,其余大量PNG多为训练过程的可视化与中间产物,便于对照理解。目前已有180人浏览学习。整套资源围绕完整赛题展开,使用户能清晰看到从数据预处理、卷积网络构建、训练调优到生成Kaggle提交文件的完整链路,并可在原代码基础上灵活修改,体会竞赛中的常见问题与解决思路;压缩包内目录按脚本、笔记与输出数据划分,查找方便,是一份轻量、可运行、信息密度较高的入门实战素材。

1. CIFAR-10 为什么是 Kaggle 图像分类的第一块敲门砖

刚上手 Kaggle 时,我见过不少人一上来就挑医疗影像、遥感检测这类高难度赛道,结果光读数据就劝退了。如果你是想用一场真实比赛把 PyTorch 图像分类全流程走通,我的建议是先拿 CIFAR-10 练手。它只有 10 个类别、每张图 32×32,数据量小到一张普通游戏显卡就能在十分钟内跑完一轮训练,但该有的环节一个不少:平台注册、环境搭建、模型定义、训练调参、生成提交文件。这篇文章会把这条路径完整拆开,从 PyTorch 安装讲到最后的 TTA 和模型集成,中间遇到的验证码问题、显存不足、过拟合这些翻车场景也一并给出排查顺序。适合刚学完 PyTorch 基础、想用一场真实竞赛检验水平的从业者。

2. 开赛前把三件事搞定:PyTorch 环境搭建、Kaggle 注册验证码与 CIFAR-10 加载

很多人比赛没开始就死在环境上。Kaggle 上的 CIFAR-10 图像分类任务本身不复杂,但本地环境如果没理顺,后面每跑一步都会卡壳。按我的经验,赛前准备只需要解决三个问题:PyTorch 能不能用 GPU 跑、Kaggle 账号能不能顺利注册、CIFAR-10 数据能不能一次加载对。下面逐个说。

2.1 用 Anaconda 隔离 PyTorch 环境:从创建到 CUDA 验证

我一般会为每个比赛单独建一个 conda 环境,不往 base 里乱装东西。原因很简单,不同比赛的依赖经常打架,torch、torchvision 版本差一个都可能导致预训练权重加载报错。创建环境这一步没什么技术含量,但值得养成习惯:

conda create -n cifar10 python=3.9 -y conda activate cifar10 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

这里的cu118对应 CUDA 11.8,你需要先查自己显卡驱动支持的 CUDA 版本。NVIDIA 官网或者nvidia-smi右上角的 CUDA Version 都能看到,如果你的驱动支持 CUDA 12.x,就把cu118换成cu121。CPU 用户直接执行pip install torch torchvision就行,只是后面训练会慢不少。

装完后不要急着写模型,先花三十秒验证环境是否真的能用:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU")

torch.cuda.is_available()返回 True 才代表 PyTorch 正确接上了 GPU。我遇到过有人在这步返回 False,但nvidia-smi正常,原因基本是装的 CPU 版 torch,或者 conda 环境里混装了多个版本。解决方法是把虚拟环境删掉重建,严格按官网给的命令装,不要自己拼接安装源。

2.2 Kaggle 注册:验证码不显示与 captcha must be filled out 的解决顺序

Kaggle 注册是个经典坎,网上搜 CIFAR-10 比赛攻略时,这个问题出现频率极高。现象是注册页面验证码区域一直空白或转圈,点击提交后提示captcha must be filled out,账号就是创建不了。这不是你操作的问题,也不是 Kaggle 服务器故障,而是本地环境和浏览器的锅。

按这个顺序排查:第一步,换 Chrome 无痕窗口重新打开注册页,无痕模式会禁掉大部分浏览器扩展的干扰;第二步,如果你装了广告拦截、隐私保护类插件,先临时停掉再刷新页面,很多拦截规则会把 Google 的 reCAPTCHA 脚本一起拦掉;第三步,核对系统时间,reCAPTCHA 依赖时间戳做会话校验,系统时间偏了会导致验证码校验永远失败;第四步,换一个网络接入点,比如从 Wi-Fi 切到手机热点。注意不要在同一个页面反复刷新,reCAPTCHA 有频率限制,刷太多次会被临时拒绝。

如果你在注册环节卡了很久,直接换浏览器整体重来往往比纠结原因更高效。Edge、Chrome 都行,重点是保持无痕、无插件、时间正确这三个条件。

2.3 CIFAR-10 加载:torchvision 数据集的一行代码背后

CIFAR-10 数据集在 PyTorch 里属于开箱即用的类型,torchvision.datasets.CIFAR10一行就能下载。首次运行时它会从服务器拉数据到root目录,之后都在本地读,不会重复下载。下面是一份可以直接跑的基础加载代码:

from torchvision import datasets, transforms from torch.utils.data import DataLoader transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) train_ds = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) val_ds = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_ds, batch_size=128, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=256, shuffle=False, num_workers=4, pin_memory=True)

这里有两处容易被忽略。第一,Normalize的参数是 CIFAR-10 整个数据集在 RGB 三个通道上的均值与标准差,直接用官方给的数据就行,不要自己算。第二,ToTensor()会把 PIL 图像从 0-255 的整数变为 0-1 的浮点数,并自动把通道维提到前面,这一步必须放在Normalize之前。num_workers=4代表用四个子进程做数据加载,能明显缓解 GPU 等待;pin_memory=True配合 GPU 训练可以减少数据从内存到显存的拷贝时间,这两个参数在后面的避坑章节还会再提到。

3. 搭第一个可提交的 Baseline:用 PyTorch 写 LeNet 并完成训练与提交

环境就绪后,我建议不要一上来就上 ResNet,先拿一个最朴素的小网络把全流程跑通。这样做的好处是:当你的代码能从数据加载走到生成提交文件,就已经解决了比赛里 80% 的工程问题。第一个出发点选 LeNet,它是图像分类里最经典的入门结构,参数量不到 6 万,在 CIFAR-10 上 CPU 都能训练。

3.1 模型定义:LeNet 的 PyTorch 写法

LeNet 的核心思路是“卷积提取特征,全连接做分类”,两层卷积加上三层全连接。针对 CIFAR-10 的 32×32 输入,我在原版基础上给第一层卷积加了padding=2,否则特征图会缩到 28×28,后面的尺寸对不上。

import torch.nn as nn class LeNet(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 6, kernel_size=5, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(6, 16, kernel_size=5), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.Linear(16 * 6 * 6, 120), nn.ReLU(inplace=True), nn.Linear(120, 84), nn.ReLU(inplace=True), nn.Linear(84, num_classes), ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x

view(x.size(0), -1)是把每个样本的特征图拉成向量,-1让 PyTorch 自动推断展平后的长度。第一层卷积的输出尺寸是 32×32,经过一次池化变 16×16,第二层卷积后变 12×12,再池化变成 6×6,所以全连接层的输入维度是16 * 6 * 6 = 576。如果你改了中间卷积的核大小或步长,这里的尺寸要重新计算,算错的话前向传播会直接报 shape 不匹配。

3.2 训练循环:损失、优化器和每轮验证

模型定义完就可以写训练循环。对 CIFAR-10 这种多分类问题,损失函数选nn.CrossEntropyLoss,它内部已经做了 softmax,所以模型最后一层输出的 10 个 logits 直接喂进去就行。优化器我习惯先用 SGD 加 momentum,这样收敛轨迹更平滑,也不容易出现 Adam 那种训练初期冲太快导致最终泛化差的问题。

import torch from torch.optim import SGD device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = LeNet().to(device) criterion = nn.CrossEntropyLoss() optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss, correct, total = 0.0, 0, 0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) correct += (out.argmax(1) == y).sum().item() total += x.size(0) return total_loss / total, correct / total def evaluate(model, loader): model.eval() correct, total = 0, 0 with torch.no_grad(): for x, y in loader: x, y = x.to(device), y.to(device) out = model(x) correct += (out.argmax(1) == y).sum().item() total += y.size(0) return correct / total

model.train()和model.eval()对 LeNet 来说影响不大,但如果后面换到带 Dropout 或 BatchNorm 的模型,漏掉这两个切换会导致训练和验证结果双双失真。loss.backward()之后必须执行optimizer.step(),同时要记得在每个 batch 前调用optimizer.zero_grad(),否则梯度会在多个 batch 间累加,损失曲线会像心跳一样剧烈震荡。

用这份代码跑 20 个 epoch,验证集准确率大约在 70% 到 75%。不用加花哨的 trick,这就是 LeNet 在该任务上的真实水平,也是你的第一个有效分数。

3.3 生成 submission.csv 并完成第一次 Kaggle 提交

Kaggle 的 CIFAR-10 图像分类竞赛通常要求提交一个 CSV 文件,里面是ImageId,Label两列,ImageId从 1 开始递增,Label是 0-9 的类别编号。很多新手在本地验证集上算了半天准确率,结果提交文件格式不对,平台直接判 0 分。

import numpy as np import pandas as pd import torch model.eval() preds = [] with torch.no_grad(): for x, _ in val_loader: x = x.to(device) preds.append(model(x).argmax(1).cpu().numpy()) preds = np.concatenate(preds) submit = pd.DataFrame({ "ImageId": np.arange(1, len(preds) + 1), "Label": preds, }) submit.to_csv("submission.csv", index=False)

这里argmax(1)取的是每个样本概率最大的类别索引,正好对应 Kaggle 要求的 Label 格式。cpu().numpy()是把 GPU 上的张量搬回内存再转数组,否则直接对 CUDA 张量调用numpy()会报错。把submission.csv拖到 Kaggle 提交页面,如果代码没有 bug,你会立刻拿到一个约 75% 的分数。这份提交也许排名靠后,但它证明整条链路已经打通。

4. 从 75% 到 90%:换 ResNet-18 并调好数据增强与学习率火候

拿到 baseline 之后,下一步就是把准确率往上拉。目标先定到 88% 到 90%,这个区间在 CIFAR-10 上只需要做三件事:换更强的模型、加数据增强、换更稳的优化器与学习率调度。每一步都能带来 5% 到 8% 的提升,合起来就是从 75% 到 90% 的跨越。

4.1 换 ResNet-18:迁移学习写法与参数冻结策略

LeNet 的瓶颈在于结构太浅,特征提取能力有限。ResNet-18 是 CIFAR-10 上性价比最高的模型,既有足够的深度,又因为残差连接不容易出现梯度消失。直接用 torchvision 加载预训练权重是常见做法,不必自己从头训练 ImageNet 那么久。

import torchvision.models as models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 10) model = model.to(device)

预训练模型原本是针对 ImageNet 的 1000 类设计的,所以最后一层全连接必须替换成输出 10 类的新层。model.fc.in_features拿到的是原模型该层的输入维度,ResNet-18 是 512。新加的fc层是随机初始化的,而前面的卷积层已经有很好的特征提取能力,因此一个稳妥的训练策略是先用较小学习率全量微调,而不是冻结主干只练分类头。冻结主干只适合数据量极小的任务,CIFAR-10 有 5 万张训练图,全量微调的效果明显更好。

4.2 数据增强:RandomCrop 与 RandomHorizontalFlip 的参数怎么选

数据增强是 CIFAR-10 上提升准确率最狠的一招。合理的增强相当于免费把数据集扩大好几倍。CIFAR-10 的经典增强组合是随机裁剪加随机水平翻转,配合之前的归一化一起放在 transform 里。

train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) val_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ])

RandomCrop(32, padding=4)会把原始 32×32 图片四周各填充 4 个像素,再随机裁剪回 32×32,等于每张图都有了 9 种不同的平移位置。RandomHorizontalFlip以 50% 概率左右翻转,对 CIFAR-10 的 10 个类别都没有破坏语义。关键是验证集上面尽量不要加这些随机增强,最多加一个RandomHorizontalFlip做 TTA,否则验证分数会不稳定。很多人在这一环节翻车,把训练用的增强直接套到验证集上,导致每个 epoch 的验证准确率忽高忽低。

4.3 训练配置:AdamW、权重衰减与余弦退火

换到 ResNet 后,SGD 仍然能训练,但 AdamW 在微调阶段更好驾驭。它对学习率不那么敏感,配合权重衰减能有效抑制过拟合。学习率调度我推荐余弦退火,它能让模型在训练后期逐步收敛到稳定点,避免在损失曲面边缘震荡。

from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=5e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-6) criterion = nn.CrossEntropyLoss()

T_max最好等于计划的总训练轮数,这样学习率会从一个较高的初始值平滑下降到eta_min。如果训练 30 个 epoch 后还想继续,可以把学习率重置回初始值再训一轮,这套循环训练的做法在后续章节会提到。weight_decay=5e-4是 CIFAR-10 上比较通用的值,太大会让模型欠拟合,太小则起不到约束作用。

当训练轮数跑到 30 轮左右,验证集准确率应该能到 88% 到 90%。如果发现训练集已经超过 95% 而验证集还停在 85% 上下,说明过拟合了,优先检查数据增强是否生效以及权重衰减是否被不小心设成 0。

5. 避坑手册:Kaggle 图像分类训练里五个最容易翻车的地方

围绕这个标题做比赛,有一批坑几乎每个新手都会踩一遍。我把高频的五个写下来,每条按“现象、原因、解决”梳理,遇到同类问题可以直接按这个顺序排查。

5.1 注册页验证码不显示,报 captcha must be filled out

现象是注册 Kaggle 时页面上的验证码区域一直空白,点击注册按钮后提示captcha must be filled out。我在 2.2 节给过排查顺序,这里再补充一个细节:如果系统时间不对,即使你刷出了验证码并勾选通过,提交时仍会报同样的错。因此先查系统时间是否自动同步,再查浏览器插件,最后换网络接入点。不要猜测是 Kaggle 封了你的 IP,绝大多数情况与账号、IP 无关,就是本地环境问题。

5.2 训练中突然报 CUDA out of memory

现象是代码在第一个 epoch 正常运行,跑到一半或者第二个 epoch 时直接崩溃,报错信息里有CUDA out of memory。原因通常有三个:batch size 太大、模型输出张量堆积、或者验证和训练交替时没有开torch.no_grad()。最直接的解决方法是把 batch size 从 128 降到 64 或 32,同时把pin_memory=True保留但检查是否在 DataLoader 里设置了num_workers过大导致显存备份。在训练代码里,验证阶段务必包上with torch.no_grad():,否则验证过程也会计算梯度并占用大量显存,这是很多人没注意到的隐性泄漏点。

5.3 DataLoader 启动慢或进程反复崩溃

现象是训练每轮开始时卡住十几秒,有时还会报DataLoader worker (pid ...) exited unexpectedly。原因大多是 Windows 环境下num_workers设置过高,或者代码没有包在if __name__ == '__main__':保护里。解决方式是先把num_workers改成 0 跑通流程,确认代码没问题后再调回 4 或 8。Linux 下这个问题少见,Windows 下几乎是必踩的坑。另外,如果使用 Jupyter Notebook,多进程加载可能与内核产生冲突,改用python train.py直接执行脚本会更稳定。

5.4 训练集准确率很高,验证集和提交分数却上不去

现象是训练最后阶段准确率已经 96% 以上,验证集只有 85%,提交到 Kaggle 的分数更低。核心原因就是过拟合,模型把训练集里的噪声也背下来了。按优先级处理:一是在 transform 里确认RandomCrop和RandomHorizontalFlip真的生效,不要在后续调试中不小心把 transform 替换成只有ToTensor的版本;二是适当增大weight_decay到 1e-3;三是训练轮数不要硬跑到底,在验证集连续 5 个 epoch 不提升时就提前停止,保存最佳模型权重。我在代码里会加一句torch.save(model.state_dict(), "best_model.pt"),每个 epoch 后比较验证准确率,只保留最高分对应的参数,这才是最后提交的依据。

5.5 提交 CSV 后平台判 0 分

现象是本地验证集准确率 90%,但 Kaggle 提交后分数显示 0。这不是模型问题,是提交格式问题。Kaggle CIFAR-10 类比赛对提交文件的要求非常严格:列名必须是ImageId和Label,ImageId必须从 1 开始连续递增,不能有索引列,Label 必须是整数而不是浮点数。最常见的翻车是pd.DataFrame.to_csv()时没有写index=False,导致 CSV 多出一列索引,Kaggle 解析时直接判错。另一个隐蔽问题是顺序,预测结果要与测试文件夹里图片的排列顺序一一对应,不能自己打乱。提交前自己打印 CSV 前五行确认一下,一分钟能省下三十分钟返工。

6. 冲 95% 的收官技巧:TTA、多模型集成和余弦退火

当你能稳定到 90% 左右,再往上每提一个点都需要更精细的手段。最后的这一公里我通常会做三件事:测试时增强、多模型平均和循环训练。

6.1 TTA:推理时也做随机翻转让预测更稳

测试时增强的思路很简单:训练时用翻转和裁剪增强了数据多样性,推理时不要只对原图预测一次,而是对翻转后的图也各预测一次,再把概率取平均。类别概率经过多次扰动后更稳定,通常能带来 0.5% 到 1% 的提升。CIFAR-10 上最轻量的 TTA 版本是同时预测原图和水平翻转图各一次:

model.eval() prob_sum = np.zeros((len(val_ds), 10)) with torch.no_grad(): for idx, (x, _) in enumerate(val_loader): x_f = torch.flip(x, dims=[3]) out1 = F.softmax(model(x.to(device)), dim=1).cpu().numpy() out2 = F.softmax(model(x_f.to(device)), dim=1).cpu().numpy() prob_sum[idx * x.size(0): (idx + 1) * x.size(0)] = (out1 + out2) / 2

torch.flip(x, dims=[3])在宽维上做镜像翻转,dims=[3]对应 NHWC 布局下的 W 维。这里必须用softmax把输出变成概率再平均,而不是直接平均 logits,否则不同模型或不同增强之间的数值尺度和置信度无法对齐。

6.2 多模型平均:让三个普通模型互相纠错

多模型集成是冲榜的常规手段。几个 90% 准确率的模型,如果错误分布不太重叠,平均之后往往能到 93% 以上。操作上我会把 ResNet-18、ResNet-34 和一个稍加宽度的 ResNet-18 各自训到收敛,推理时把它们的 softmax 概率加和:

# models 是已加载权重并处于 eval 模式的模型列表 avg_prob = np.zeros((len(val_ds), 10)) with torch.no_grad(): for i, (x, _) in enumerate(val_loader): for model in models: out = F.softmax(model(x.to(device)), dim=1).cpu().numpy() avg_prob[i * x.size(0): (i + 1) * x.size(0)] += out avg_prob[i * x.size(0): (i + 1) * x.size(0)] /= len(models)

要注意的是集成模型之间差异越大,效果越好。如果三个模型结构完全相同、随机种子也相同,那它们学到的几乎是一样的错误,平均后提升微乎其微。我会在训练时给每个模型设置不同的随机种子,或者一个用 SGD 一个用 AdamW,让它们各自寻找不同的收敛区域。

6.3 余弦退火重置和早停:最后一公里别松懈

CIFAR-10 上还有一个小技巧是循环训练法。训练到第 30 个 epoch 学习率退到最低点后,把学习率重新设回初始值,再训练 30 个 epoch。这个过程相当于在已经收敛的权重上做二次精修,往往能再挤出 0.5 个点。配合早停策略,当验证准确率连续 8 个 epoch 没有刷新最高记录时停止训练,用保存的最佳权重提交。

这套组合拳打完后,CIFAR-10 测试集准确率应该能稳定在 93% 到 95%。走到这里回头看,你会发现这个比赛最难的不是某个模型或某个参数,而是把一整套工程流程串起来的能力。每次打完比赛我都会把按这段代码整理成模板,新比赛来了直接改数据加载和输出维度就能复用。Kaggle 图像分类的套路是相通的,CIFAR-10 跑通了,后面做任何分类任务心里都有底。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询