
1. 任务拆解CIFAR10到底要解决什么1.1 数据集基本盘CIFAR10 可以说是计算机视觉入门必经的一个路口。它比 MNIST 更接近真实场景但又不像 ImageNet 那样动辄上百 GB刚好卡在“能跑得动”和“有一定挑战”的中间位置。这个数据集由 60000 张 32x32 的彩色图片组成分成 10 个类别飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。每个类别 6000 张其中 50000 张用于训练10000 张用于测试。说到“彩色图片识别”CIFAR10 和 MNIST 最本质的区别就在“彩色”这两个字上。MNIST 是单通道灰度图每个像素只有一个数值代表亮度CIFAR10 是三通道 RGB 图每个像素有三个值分别代表红、绿、蓝三个通道的强度。这个差异直接决定了神经网络第一层卷积核的维度输入通道数从 1 变成 3模型需要同时学习三个通道之间的关联和每个通道内部的空间结构。训练营把 CIFAR10 放在第 P2 周我理解是有意为之的。如果你已经能在 MNIST 上跑通 LeNet那 CIFAR10 就是验证“你是不是真的理解卷积网络”的试金石——同样的网络结构搬到彩色图效果可能会断崖式下跌这时候你就知道问题出在模型容量、数据增强还是训练策略上了。1.2 彩色图片识别的难点在哪32x32 的分辨率听起来很小但放到真实场景里这个尺寸恰恰是很多移动端模型的输入规格。CIFAR10 难在几个地方第一图片分辨率低细节信息有限。32x32 的尺寸意味着每个类别只有大约 1000 个像素点想靠“数像素”的方式区分猫和狗几乎不可能模型必须学到更高层的语义特征。这也是为什么简单的全连接网络在 CIFAR10 上效果很差的原因——它没有空间不变性换个位置就认不出来了。第二类别之间有语义重叠。比如“猫”和“狗”、“汽车”和“卡车”这些类别的低层特征边缘、纹理非常相似区分它们需要模型捕捉到更抽象的形态差异和上下文信息。第三训练样本相对于模型容量来说并不充裕。50000 张图要学 10 类平均每类只有 5000 张如果模型参数太多很容易就过拟合了。训练集准确率能做到 95% 以上测试集却只有 70% 多这是很多新手在 CIFAR10 上最先遇到的打击。搞清楚这三点后面的模型设计和训练策略才有方向。盲目堆卷积层数或者直接把某个预训练模型搬过来都不是这个阶段最该做的事情。2. 环境准备与数据加载2.1 依赖安装与设备判断CIFAR10 的实践代码量不大核心依赖就三个PyTorch、TorchVision、Matplotlib。TorchVision 不只是用来下载数据集的它里面的torchvision.transforms是数据预处理的标准工具后面做标准化、数据增强都靠它。pip install torch torchvision matplotlib装完之后先确认一下设备这一步很多人会跳过但后面训练速度差几倍甚至十几倍全看这里。import torch device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) print(fPyTorch version: {torch.__version__})如果你和我一样用的是 Windows 笔记本大概率会拿到cpu。这没问题CIFAR10 用一个小型 CNN 在 CPU 上也能训练就是慢一些。我的建议是先跑通全流程再去想提速的事情。训练营这一周的核心目标是理解模型结构和训练流程不是为了刷分。2.2 transform 怎么配比较稳数据加载的第一步是定义 transform。CIFAR10 原始图片的像素范围是 0 到 255值域跨度大、分布不均匀直接喂给网络会导致梯度更新不稳定。标准做法是先转成 Tensor再按通道做标准化。CIFAR10 数据集的三个通道有官方统计好的均值和标准差直接拿来用就行from torchvision import transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ])这里Normalize的公式是(x - mean) / std作用是把每个通道的数据拉到均值为 0、方差为 1 的分布。为什么要这样做拿生活里的例子类比如果你要比较两个人的身高和体重肯定先各自标准化不然体重数值大会在距离计算里占据主导。神经网络里的梯度更新也类似数值范围不一致会让优化过程变得很扭巴。提示ToTensor()会自动把像素值从 0~255 缩放到 0~1所以Normalize里的均值和标准差也是相对于 0~1 这个范围来设计的不要拿去跟原始像素值做对比。2.3 加载数据集的小细节TorchVision 提供了 CIFAR10 的直接下载接口如果网速不理想它会卡在下载阶段。我的做法是手动下载数据集压缩包放到./data目录下再把downloadFalse传进去。用 DataLoader 加载训练集和测试集from torch.utils.data import DataLoader from torchvision import datasets train_dataset datasets.CIFAR10( root./data, trainTrue, downloadFalse, transformtransform ) test_dataset datasets.CIFAR10( root./data, trainFalse, downloadFalse, transformtransform ) batch_size 64 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers2) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse, num_workers2)shuffleTrue只用在训练集目的是打乱样本顺序避免模型学到样本排列的伪规律。测试集不需要打乱。num_workers是数据加载的并行进程数Windows 上设成 2 就够了设太高反而可能报错。第一次训练前我先抽了一批数据出来可视化这一步强烈建议做。随机打印 20 张图看到图片尺寸、颜色分布是否正常比直接开训省心很多import matplotlib.pyplot as plt import numpy as np def imshow(img): img img / 2 0.5 # 反标准化把数据映射回 0~1 区间 npimg img.numpy() plt.imshow(np.transpose(npimg, (1, 2, 0))) plt.show() dataiter iter(train_loader) images, labels next(dataiter) imshow(torchvision.utils.make_grid(images[:4]))这里有个容易踩的坑标准化之后的数据已经不是 0~1 区间了直接imshow会看到一片奇怪的色块。需要先做反标准化img / 2 0.5是针对均值 0、方差 1 的近似还原再显示。3. 模型搭建从零写一个 CNN3.1 输入输出的 shape 变化CIFAR10 的输入是(3, 32, 32)即 3 个通道、宽高各 32 像素。在动手写模型之前先把张量 shape 的变化过程在脑子里过一遍这是所有 CNN 编程的基础功。一个标准的卷积模块包含三层卷积层提取特征、池化层降采样、激活函数加非线性。以我最常用的小型网络为例import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes10): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) self.pool nn.MaxPool2d(kernel_size2, stride2) self.fc1 nn.Linear(64 * 8 * 8, 256) self.fc2 nn.Linear(256, num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): x self.pool(F.relu(self.bn1(self.conv1(x)))) x self.pool(F.relu(self.bn2(self.conv2(x)))) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x我按张量 shape 的变化把流程整理成了下表第一次写模型的人对着这个表检查很少会出错层输入 shape输出 shape说明Conv2d(3, 32, 3, padding1)(N, 3, 32, 32)(N, 32, 32, 32)卷积不改变空间尺寸BatchNorm2d ReLU(N, 32, 32, 32)(N, 32, 32, 32)标准化激活MaxPool2d(2)(N, 32, 32, 32)(N, 32, 16, 16)宽高减半Conv2d(32, 64, 3, padding1)(N, 32, 16, 16)(N, 64, 16, 16)通道数翻倍BatchNorm2d ReLU(N, 64, 16, 16)(N, 64, 16, 16)同上MaxPool2d(2)(N, 64, 16, 16)(N, 64, 8, 8)宽高再减半view 展平(N, 64, 8, 8)(N, 6488)转为二维送入全连接层Linear(6488, 256)(N, 4096)(N, 256)全连接Dropout ReLU(N, 256)(N, 256)防止过拟合Linear(256, 10)(N, 256)(N, 10)输出类别分数有个细节要特别说明padding1配合kernel_size3能保证卷积输出的宽高和输入一致不需要手动计算输出尺寸。这个“same padding”的写法在搭建多层网络时非常省心。3.2 各层设计思路与参数量第一个卷积层输入通道是 3因为图片是 RGB 三通道。输出通道设成 32这是经验值——通道数太少了学不到足够的特征太多了又容易过拟合。每个卷积核都是 3x3 的窗口它在输入图上滑动时同时看到三个通道的局部区域输出一个融合了颜色和空间信息的特征值。第二个卷积层把通道数从 32 加到 64。通道数逐渐增加是 CNN 设计的通用模式浅层提取边缘、颜色等低级特征通道数不需要太多深层提取纹理、形状等高级特征需要更多通道来容纳更丰富的语义信息。这就像看一张照片先看轮廓和颜色再看局部的细节。全连接层前面的64 * 8 * 8是展平后的向量长度由最后一层卷积的输出决定。修改了卷积层配置这里也要跟着变这是新手最容易忽略的报错点。如果你改了网络结构之后报维度不匹配的错误十有八九是这里的数字忘了改。激活函数我统一用了 ReLU。它有计算简单、缓解梯度消失的优点虽然在 x0 时梯度为 0但对这个小网络来说完全够用。如果你有兴趣换成 LeakyReLU 也可以但别指望 CIFAR10 这个量级的数据集能体现出太大差别。3.3 为什么加 BatchNorm 和 DropoutBatchNorm2d是训练稳定性的关键。它的作用是在每个 batch 内把特征图的数据分布拉回到均值为 0、方差为 1。这样做的直接好处是即使前一层输出的数值范围在训练过程中发生了偏移也能被及时拉回来梯度更新不会因为数值过大或过小而抖动得厉害。Dropout(0.5)的做法是训练时随机让一半的神经元不参与计算测试时再用全部神经元。这相当于每轮都在训练一个不同的子网络最后使用时相当于把一群子网络做了集成。用大白话说就是防止网络“死记硬背”训练集——有人管这叫“断网式学习”我觉得挺形象。BatchNorm 和 Dropout 一起用没问题但要记住BatchNorm 在训练和测试时的行为不同训练时用当前 batch 的统计量测试时用累积的全局统计量PyTorch 已经处理好了不需要手动切换。Dropout 同理model.eval()会自动关闭它。4. 训练配置与完整流程4.1 损失函数、优化器、batch size 的选择分类任务的标准损失函数是交叉熵损失nn.CrossEntropyLoss()。它内部已经整合了 Softmax 和负对数似然损失所以模型最后一层不需要手动加 Softmax——直接输出 10 个类别的原始分数就行。第一次写代码的同学经常会画蛇添足地在最后一层加 Softmax导致损失函数算出来的值很奇怪这里要特别注意。优化器我选了 Adam学习率1e-3。Adam 的优势是自适应调整每个参数的学习率对新手非常友好不需要像 SGD 那样手动调动量和学习率衰减策略。但 Adam 也有个问题如果训练后期发现 loss 震荡剧烈需要手动把学习率降下来。batch size 设为 64。这个值不能太大也不能太小太小的话梯度更新方向不稳定训练过程噪声大太大会超出显存而且收敛速度反而变慢。64 在 CIFAR10 上是一个平衡点。超参数配置参数取值选择原因batch size64平衡梯度稳定性与内存占用学习率1e-3Adam 的默认推荐范围损失函数CrossEntropyLoss多分类标准选择优化器Adam自适应学习率对新手友好训练轮数20CPU 环境可跑完GPU 可适当增加4.2 训练循环的代码骨架整个训练流程可以抽象成三层循环外层是 epoch遍历整个数据集几遍中层是 batch每个 batch 更新一次参数内层是前向传播、反向传播、参数更新三步。import torch.optim as optim model SimpleCNN(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) epochs 20 train_loss_list [] train_acc_list [] for epoch in range(epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / len(train_loader) epoch_acc correct / total train_loss_list.append(epoch_loss) train_acc_list.append(epoch_acc) print(fEpoch [{epoch1}/{epochs}], Loss: {epoch_loss:.4f}, Accuracy: {epoch_acc:.4f})optimizer.zero_grad()这行代码不是可有可无的。PyTorch 的梯度是累加的不清零的话每个 batch 的梯度都会叠加上去参数更新方向就会乱七八糟。我见过不少新手在这里漏掉一行结果 loss 忽高忽低怎么调都调不好。torch.max(outputs, 1)返回每一行每个样本的最大值和对应的索引索引就是模型预测的类别。4.3 验证集评估的正确姿势每个 epoch 结束后建议跑一遍测试集这样才能知道模型是不是真的在学有用的特征而不是只在训练集上“刷分”。评估时要注意区分model.eval()和torch.no_grad()的作用。def evaluate(model, test_loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() accuracy correct / total return accuracy test_acc evaluate(model, test_loader, device) print(fTest Accuracy: {test_acc:.4f})model.eval()切换 BatchNorm 和 Dropout 到推理模式torch.no_grad()关闭梯度计算省内存也省时间。这两个必须在评估时同时使用缺一不可。如果不加model.eval()BatchNorm 会用当前 batch 的统计量而不是全局统计量批大小不同时会导致结果波动如果不加torch.no_grad()forward过程中会构建计算图白白浪费大量内存。第一轮训练结束我的训练准确率在 60% 左右测试准确率在 55% 左右。等到第 10 轮训练准确率能到 90% 以上测试准确率大约 78%。这个基线成绩说明模型架构没有问题再往后提升就需要靠技巧了。5. 提升识别准确率的实操技巧5.1 数据增强少样本情况下最有效的正则化CIFAR10 只有 50000 张训练图片对深度学习来说确实不算多。一个简单有效的应对手段是数据增强在训练时对原始图片做随机变换相当于用有限的样本生成更多样的训练数据。常用的增强方式有三种随机水平翻转、随机裁剪、色彩抖动。它们的原理都是“制造扰动”让模型学会忽略与分类无关的变化。from torchvision import transforms transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ])注意两个细节测试集的 transform 不能加数据增强因为评估时需要看到一个稳定的结果RandomCrop(32, padding4)的意思是先把图片四周各补 4 像素的 0再随机裁回 32x32这比直接旋转或缩放更符合照片的自然变化——物品在画面里的位置本来就是有偏移的。加上数据增强后同样训 20 轮我的测试准确率从 78% 提到了 82% 左右。这个提升不依赖任何模型结构的改动纯粹是数据层面的调整性价比极高。5.2 学习率的调整时机固定学习率训练 20 轮后我发现一个现象loss 在早期下降很快但后面会进入一个平台期上下震荡但不再明显下降。这是学习率过大的典型表现——参数在一个山谷附近来回跳跃就是跳不进最低点。解决办法是学习率衰减。最简单的做法是在训练到一半时手动把学习率调低一个数量级for epoch in range(epochs): if epoch 10: for param_group in optimizer.param_groups: param_group[lr] 1e-4 print(Learning rate changed to 1e-4)更省事的方案是用 PyTorch 的ReduceLROnPlateau它在 loss 连续几个 epoch 不下降时自动调低学习率。但我觉得在训练营这个阶段手动调整反而更有感觉——你能清楚地看到不同学习率下 loss 的表现差异这对培养调参直觉很重要。5.3 我踩过的损失震荡和收敛慢问题第一版代码我用的是原始像素直接输入没有任何标准化训练 loss 下降非常慢。后来检查才发现数据范围是 0~255而模型权重初始化在 0~1 附近梯度计算出来数值偏大Adam 虽然能处理但效率不高。加上标准化之后loss 下降速度明显加快。还有一次我误把shuffleFalse用在训练集上结果是模型每个 epoch 看到的样本顺序固定训练曲线出现周期性的波动。排查了半天才发现是数据打乱的问题——训练数据不打乱每个 batch 内的类别分布可能严重不均衡导致 loss 一会高一会低。如果你的模型光往前跑却不收敛先检查三个地方数据标准化做没做、训练集有没有 shuffle、学习率是不是设得太高。这三板斧能解决 80% 的收敛问题。6. 常见问题与排查技巧实录6.1 验证集一直卡着不动训练集准确率涨得好好的测试集准确率却一直卡在 50% 上下首先想到的应该是代码问题而不是模型问题。最常见的 bug 是数据预处理不一致训练时用了数据增强测试时忘了用标准化或者标准化参数写错了。这会导致模型看到的测试图片分布和训练图片不一致表现自然上不去。我建议把训练和测试的 transform 放在一起定义对比着看减少写错的可能。另一个原因是标签错位。CIFAR10 的类别索引是固定的如果在某个环节不小心做了重映射模型学到的映射关系就和测试标签对不上了。检查方法很简单随机采样几张验证集图片打印真实标签和模型预测标签肉眼看看对不对得上。6.2 显存不够怎么办很多人以为显存不够只能换显卡其实有更便宜的解决办法。最直接的是减小 batch size从 64 减到 32 或 16观察 loss 和准确率的变化。这是最简单有效的方案代价是训练速度略降。还有一个思路是减小输入图片的尺寸但 CIFAR10 本身就是 32x32没有再缩小的空间了。如果用的是自定义数据集这个方案才值得考虑。6.3 训练集准确率很高、测试集准确率很低这个现象叫过拟合在 CIFAR10 上体现得很明显。训练准确率在 95% 以上、测试准确率只有 70% 多说明模型把训练集的特征“背”了下来而不是学到了泛化能力。针对这个问题我的实验顺序是先加数据增强这是性价比最高的手段通常能带来 3~5 个百分点的提升。再加 Dropout全连接层之间的 Dropout 最能发挥作用。如果还过拟合考虑减小模型容量——把卷积通道数从 64 减到 48或减少全连接层的神经元数量。最后考虑早停策略即验证集准确率连续多个 epoch 不提升时提前结束训练。这四步做完CIFAR10 的测试准确率一般都能稳定到 85% 以上训练准确率和测试准确率的差距也能从 20 多个百分点缩小到 10 个百分点左右。6.4 做一张准确率变化曲线图训练结束后把每个 epoch 的训练准确率和测试准确率画在一张图上是判断模型状态最直观的方式。两条线同步上升说明模型在正常学习训练线上升而测试线停滞或下降说明过拟合了两条线都在震荡说明学习率偏高或数据有问题。plt.plot(train_acc_list, labelTrain Accuracy) plt.plot(test_acc_list, labelTest Accuracy) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.legend() plt.title(Training History) plt.show()这张图也是训练营周报里的必备内容比单独贴一个最终准确率数字更有说服力因为能完整地展示模型的训练过程和朋友踩坑痕迹。第 P2 周做下来我最大的感受是 CIFAR10 这个任务选得非常好——它刚好卡在“调参能力”和“对深度学习的理解”这两个维度的交界处。模型结构本身并不复杂但要把准确率从 65% 提到 85% 以上你会被迫去思考数据、优化、正则化之间如何配合这比单纯调参学到的东西多得多。如果你也正在跑这个任务建议记录每一轮实验的改动和结果哪怕只是简单记在备忘录里回头复盘的时候会发现价值很大。