这三个任务放一起做,其实挺巧的——它们正好串起了CNN图片分类从入门到实战的一条完整路径。作为“从零开始的推荐系统学习之路”的第十六篇,这一章暂时把视野从用户行为序列、Embedding和打分预测,挪到像素矩阵、卷积核和类别概率上,本质上还是在学同一件事:怎么用深度学习从原始数据里自动提特征。
这篇笔记我会按自己实际动手的顺序来写:先拆解三个Kaggle任务的差异和各自难点,再讲通用的CNN建模流程,最后把训练过程中真实踩过的坑和排查思路整理成清单。代码部分只贴核心片段,完整可跑的脚本建议直接对照动手学深度学习开源教材的相应章节来实践。
1. 三个Kaggle任务放在一起,到底在练什么
这个标题初看有点杂,树叶分类、CIFAR-10、ImageNet Dogs,数据集规模、图片内容、类别数量完全不是一个量级。但真正上手跑一遍就会发现,这三个任务拼在一起恰好覆盖了CNN图片分类最常见的三类场景。
树叶分类是典型的小样本细粒度分类。每个类别样本量少,不同物种的叶片在形状、纹理、叶脉走势上差异极其细微,模型稍有不慎就过拟合,或者干脆学到背景和光照的伪特征。这个任务练的是数据增强、正则化、迁移学习这些“在样本受限时怎么把泛化能力挤出来”的本事。
CIFAR-10是通用物体识别的入门基准。32x32的彩色小图,10个类别,5万张训练图,规模不大不小,类别间界限清晰但像素信息很少。它最适合用来理解CNN的基础组件——卷积层、池化层、批量归一化、残差连接——因为模型结构够简单,改一个参数就能看到效果变化,反馈非常直接。
ImageNet Dogs是大规模预训练模型的迁移学习实战。120个犬种,图片分辨率高、场景复杂、类别间相似度极高(比如比格犬和小猎兔犬),单靠自己从头训练一个CNN基本不现实。这个任务练的是怎么用好别人训练好的模型,怎么微调,怎么在有限算力下拿到尽可能高的分数。
从学习路径的角度看,这三件事是递进的:先学会怎么调数据,再学会怎么搭模型,最后学会怎么站在巨人的肩膀上。很多初学者直接拿ImageNet级别的任务上手,结果被环境和算力劝退;也有人一辈子只跑MNIST,始终没接触过真实赛题的脏数据和类别不均衡。把这三个任务串起来,正好补上中间的梯度。
2. 动手前必看:三个任务的技术要点对照
开工之前,先把三个任务的关键差异搞清楚。我在跑的过程中发现,很多后面出现的坑,其实在数据层面就已经埋下了。
| 数据集 | 图片尺寸 | 类别数 | 训练样本量 | 核心难点 | 推荐策略 |
|---|---|---|---|---|---|
| 树叶分类 | 不定,需resize | 176类(原赛题) | 约1000张总量 | 样本极少、类间差异细微 | 迁移学习 + 强数据增强 + Label Smoothing |
| CIFAR-10 | 32x32 | 10类 | 50000张 | 分辨率低、细节少 | 自建CNN + 残差结构 + 清洗数据 |
| ImageNet Dogs | 不定,通常224x224 | 120类 | 约20000张 | 类别相似度高、背景复杂 | 预训练模型微调 + 多尺度测试 |
先说树叶分类。我拿到的这个版本的赛题是Kaggle的“Leaf Classification”,原赛题有176个类别和99个标本,赛制是每个标本的多张图片分到训练集和测试集,所以测试集里会出现训练集见过的标本的新照片。这意味着模型如果只是死记标本特征而不是学习物种本身的纹理、形状、叶脉规律,测试分数会非常难看。这个任务的数据增强要尤其小心,翻转、旋转、缩放都有用,但要保证增强后的图片仍然保留物种鉴别的核心特征。
CIFAR-10则完全相反,训练集和测试集是同一分布的,类别也少,所以核心竞争点全在模型结构和训练策略上。32x32的分辨率决定了它不需要特别深的网络——太多下采样反而会把信息丢光。动手学深度学习教材里专门给了两个CIFAR-10的实现版本,一个是用全连接层硬怼的玩具模型(效果很差但能让你理解为什么CNN有必要),另一个是带批量归一化和Dropout的类VGG结构,后者才是入门CNN的正确姿势。
ImageNet Dogs严格来说是Kaggle的“Dog Breed Identification”的简化版(实际赛题是一万两千张图和120个类别)。这个任务里迁移学习几乎是唯一现实可行的路线,因为从头训练的CNN在这么少的数据上学到的特征质量,远不如ImageNet上预训练好的模型。你需要决定的是冻多少层、调多少层、用什么学习率、怎么处理类别不均衡。
实操经验:不要急着写模型,先把三个数据集的图片各看一批。我在树叶分类上花了很长时间才意识到,很多叶片在肉眼看来几乎一模一样,模型能区分的线索可能是叶脉走向的细微差异,而不是颜色。这种直觉会直接影响你后面怎么选增强策略、怎么调输入分辨率。
3. 通用CNN建模流程:从数据预处理到训练策略
三个任务的侧重点不同,但标准的处理流程是一致的。我习惯把流程拆成四步:数据组织与预处理、基线模型建立、迭代优化、集成与后处理。下面按这个顺序展开讲,每个环节用什么工具、什么参数、为什么这样做,都会说清楚。
3.1 数据组织与预处理
建议所有图片统一走一个预处理管线,输入尺寸按任务来:树叶分类和ImageNet Dogs用224x224(为了兼容预训练模型),CIFAR-10保持32x32(原始尺寸即可,不需要放大,因为放大不会增加信息)。
# 标准的预处理流程(伪代码,适配不同框架) train_transform = Compose([ Resize((224, 224)), # 树叶和狗狗数据;CIFAR-10跳过这步 RandomHorizontalFlip(p=0.5), # 轻量增强,所有任务通用 RandomRotation(10), # 树叶分类必选,狗狗可选 ColorJitter(0.2, 0.2, 0.2), # 树叶分类要谨慎,颜色可能是鉴别特征 ToTensor(), Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])Normalize的三个通道均值和标准差,如果用的是PyTorch的torchvision预训练模型,必须保持上面这组值不变——这些模型是在ImageNet上用这组统计量归一化的图片训练的,输入分布不对齐,权重就白迁移了。
树叶分类的特殊之处在于数据量太少,增强强度需要另外加大。我试过加RandomResizedCrop(scale=(0.7, 1.0))和RandomGrayscale(p=0.1),前者模拟不同角度和距离的拍摄效果,后者强制模型不依赖颜色信息、转而关注纹理和形状。但注意,如果某些树叶的鉴别特征恰恰是颜色(比如秋天红叶和绿叶的区分),灰度增强会让模型丢掉这个线索,所以p值不要设太高。
CIFAR-10有一个小技巧:官方训练集5万张里其实混了一部分质量差的图,但实测下来清洗的效果有限,不如把精力放在数据增强上。唯一的例外是如果发现训练损失降不下去、验证集准确率异常低,可以先检查是不是有损坏图片混在数据里。
3.2 基线模型选择:先跑通,再优化
不要在第一天就追求SOTA。我的习惯是先跑一个最简单的基线,确认整个pipeline(数据载入、训练循环、评估逻辑)没有bug,再逐步升级模型。
- 树叶分类:直接用ResNet-18预训练权重做特征提取,替换最后的全连接层为176类输出。先用冻结backbone的方式训练,只更新分类头,跑10个epoch建立基线。
- CIFAR-10:用简化版VGG(动手学深度学习教材里那个结构),或者直接上ResNet-18。如果直接上ResNet-18,建议把第一层卷积的kernel size从7改成3、去掉首个池化层,因为32x32的输入不需要感受野那么大的首层。
- ImageNet Dogs:用ResNet-50或更大的模型(有GPU的话可以考虑EfficientNet或ConvNeXt),同样先冻结backbone微调分类头。
关于“冻结”的一点解释:所谓冻结,就是在训练时让backbone的参数不参与梯度更新,模型只更新最后几层。这样做的原因是预训练模型已经学到了通用特征——边缘、纹理、形状——这些特征对任何图片任务都有用,不需要重新学。只用少量数据去训练分类头,速度快、不容易过拟合。等分类头收敛了,再解冻后面几层,用小学习率微调,让高层特征慢慢适应新任务的分布。
# PyTorch冻结backbone的示意代码 model = models.resnet18(pretrained=True) for param in model.parameters(): param.requires_grad = False num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 176) optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)这个阶段的目标是验证数据加载对不对、损失函数配置对不对、评估脚本能不能算出准确率。很多初学者一上来就加载大模型,结果训练三天后才发现验证集的评估逻辑写错了,白烧了显卡。先用小模型快跑几轮把流程验证好,再上大模型。
3.3 训练策略与超参数
三个任务的共同点是都用了分阶段训练:先用较大的学习率训练分类头,再解冻部分层用小学习率微调。这不是玄学,是有明确原因的。
预训练模型的权重是在ImageNet上优化的,它们的特征提取能力很强,但分类头是换掉的。如果你一开始就用很小的学习率去训,分类头学习速度会很慢;如果一开始就用很大的学习率去解冻全部层,会把预训练权重冲坏。所以要先让新分类头快速适应,再用小步长调整backbone。
针对三个任务,我实际用下来相对稳定的参数方案如下:
| 任务 | 模型 | 阶段1(冻结) | 阶段2(微调) | 总epoch | 批大小 |
|---|---|---|---|---|---|
| 树叶分类 | ResNet-18 | lr=1e-3, 10 epochs | lr=1e-4, 15 epochs | 25 | 16 |
| CIFAR-10 | ResNet-18(改造首层) | 从头训练,lr=0.1(CosineAnnealing) | 无 | 50 | 128 |
| ImageNet Dogs | ResNet-50 | lr=1e-3, 5 epochs | lr=5e-5, 10 epochs | 15 | 32 |
关于CIFAR-10的0.1初始学习率,看起来很高,但这个数据集配合批量归一化、合适的权重初始化和余弦退火,0.1是经典配置。我试过把学习率调到0.01,收敛很慢而且最终准确率偏低。如果你用的是Adam优化器,建议学习率缩放到1e-3量级并配合权重衰减。
学习率的调度策略,三个任务我都推荐带warmup的CosineAnnealingLR:前几个epoch线性增加学习率,让模型平稳起步,然后余弦式衰减到接近零。这种方式比step decay更平滑,不容易在训练后期因为学习率骤降而震荡。
注意:权重衰减(Weight Decay)一定要加。我见过太多初学者训练CNN不设weight_decay,结果模型倒是能收敛,但验证集准确率始终上不去,这就是典型的过拟合。三个任务里,树叶分类建议设为1e-4甚至5e-4,CIFAR-10和Dogs可以设为1e-4或5e-5。代价很小,收益很稳定。
3.4 损失函数与类别不均衡
三个任务的类别分布总体均匀(树叶分类是按标本分配的,每个类别样本数接近;CIFAR-10每类5000张;Dogs每类约150张),所以用标准的CrossEntropyLoss就够了,不需要专门处理类别不均衡。
但我还是建议在树叶分类上加上Label Smoothing(通常设epsilon=0.1)。原因是树叶分类的很多类别在视觉上过于接近,模型容易对训练集产生过度自信——训练集上预测概率接近1,验证集上却错误百出。Label Smoothing相当于把one-hot标签往平滑方向推一点,鼓励模型输出更保守的概率分布,能明显抑制这种过拟合现象。
# Label Smoothing实现示例(PyTorch) class LabelSmoothingCrossEntropy(nn.Module): def __init__(self, smoothing=0.1): super().__init__() self.smoothing = smoothing def forward(self, pred, target): n_classes = pred.size(1) one_hot = torch.zeros_like(pred).scatter_(1, target.unsqueeze(1), 1) smoothed_labels = one_hot * (1 - self.smoothing) + self.smoothing / n_classes log_probs = F.log_softmax(pred, dim=1) loss = -(smoothed_labels * log_probs).sum(dim=1).mean() return loss4. 常见的坑和排查思路
这部分是我最想写的。这三个任务我前后折腾了大概三周,踩过的坑比学到的理论多,但每一次排查都让我对CNN的理解深了一层。整理成清单,按高频程度排列。
训练损失不降,准确率接近随机
先检查数据预处理:图片的三通道均值归一化对不对?标签有没有错位?DataLoader的shuffle有没有开?再用一个batch过一遍模型,确认输出shape和loss在数值上合理。如果都没问题,检查学习率——用Adam时初始学习率太高(比如大于1e-2)会导致发散,太低(小于1e-5)会导致几乎不动。
训练损失下降,但验证集准确率上不去
这是过拟合的典型症状。优先加正则化手段:数据增强、Label Smoothing、Dropout、权重衰减,按顺序一个个加。顺便检查一下模型容量是否过大——树叶分类这种小样本任务用ResNet-50反而比ResNet-18更容易过拟合,换小模型往往效果反而更好。
迁移学习时,微调后准确率反而下降
这是我在ImageNet Dogs上踩的坑。原因通常是解冻层数太多、学习率太大,把预训练权重冲坏了。解冻时先只解冻最后一个block(比如ResNet的layer4),学习率设小一倍,跑几个epoch看验证集情况再决定是否进一步解冻。另一个常见错误是微调前忘记把backbone的BatchNorm层设为training模式以外的状态——当batch size很小时,BN的统计量估计不准,特征分布会被扰动。
热力图(Grad-CAM)看起来模型没在关注目标物体
这个说起来有点伤自尊,但也是最好诊断的。输出归一化前后的一致性、预训练模型的预处理方式(Resize还是Crop)、训练时数据增强太强导致物体被裁掉,都可能导致这个问题。用可视化工具检查训练样本本身,远比盯着loss曲线猜要高效。
CIFAR-10小图大模型,显存不够
当batch size减到8还是OOM,别再硬扛了,要么换输入尺寸更小的模型,要么开混合精度训练。CIFAR-10 32x32的输入其实不需要很大的特征图,把模型首层改成小卷积核、去掉冗余的池化层,能省下不少显存。真正重要的信息大多数时候写在网络结构的约束里。
5. 集成与后处理:还能再榨一点分
如果你前三步都做完了,准确率卡在一个还不错的水平,还可以通过工程手段再往前推一推。
多模型集成(Ensemble)在三个任务上都有明显收益,CIFAR-10尤其明显,因为它的类别更容易互补——有的模型对交通工具敏感,有的模型对动物敏感,不同初始化权重的模型预测结果做平均,能降低方差。实操上最简单的做法是训练2-3个结构不同(比如ResNet-18+ResNet-34)或相同结构不同随机种子的模型,对softmax输出做算术平均。树叶分类数据量太小,我会建议在测试阶段做TTA(Test-Time Augmentation),把每张测试图经过水平翻转等几个变换分别预测,再平均结果。这个手段在树叶分类上提升非常直观——肉眼都能看到混淆矩阵的改善。
阈值调整适用于对查准率查全率有特定要求的场景。例如ImageNet Dogs如果希望最终错误预测的代价更均匀,可以用验证集统计每个类别的置信度分布来微调决策阈值。不过大多数Kaggle图像分类任务直接用argmax就行,阈值调整带来的收益一般不如TTA和Ensemble明显。
6. 到底应该按什么顺序学
题目里写了“TODO”,我猜作者可能在提醒自己这份笔记还没有结束。如果让我给后来者一个顺序建议,我会这么说:不要跳过CIFAR-10直接做后面两个任务,因为在CIFAR-10上从头训练一个CNN,是唯一一次能真正看清卷积层怎么堆叠、通道数怎么变化、skip connection为什么能让梯度更顺畅的机会。
树叶分类适合放在CIFAR-10之后,用来理解“预训练模型+冻结+微调”这套范式为什么能在小数据上work。这也是推荐系统领域常见的操作——把在大规模数据上学到的特征迁移到冷启动的小任务上,思想完全一致。
ImageNet Dogs放在最后当作综合实战,把前面学到的所有手段——迁移学习、TTA、多模型集成——组合起来用。这时候你会发现,做CV比赛和做推荐系统比赛,到了后期拼的其实都是同几样东西:对数据的理解、对偏差方差权衡的判断、对训练细节的把控。模型结构反而成了最不值得纠结的部分。
这三件事做下来,CNN图片分类这条线就算入了门。之后无论是去做目标检测、图像分割,还是回到推荐系统里做图像特征融合,底子都是通用的。