ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

图像分类实战指南:从数据预处理到ResNet18训练全流程

图像分类实战指南:从数据预处理到ResNet18训练全流程 1. 为什么第五节要做图像分类1.1 从全连接到卷积质的跨越第五节了。前面几节如果都跟下来了你应该已经知道了张量是什么、梯度回传怎么运作、也亲手搭过一个几层的全连接网络。但说真的在那个阶段你可能和我当年一样心里一直有个隐隐的疑惑就这这玩意儿真能认图片答案是肯定的但需要用对结构。全连接网络处理图像的问题在于一个 32x32 的彩色小图就有 3072 个像素你把它拉平成向量喂进全连接层第一层想做透特征就得是几百上千个神经元参数量哗啦啦涨到几十万上百万。小图凑合能跑一到 224x224 或者更大分辨率的真实图片模型直接膨胀到没法训练。这个痛点就是卷积结构登场的直接原因——它用局部连接权值共享两个设计把参数量压下去几个数量级同时保留了图像的空间结构信息。所以这一节真正要解决的事情不是把准确率刷到多高而是让你亲手跑通一条完整的图像分类流水线数据读取 → 预处理 → 模型定义 → 训练 → 评估。这条流水线是后面所有视觉任务——目标检测、语义分割、图像生成——的地基你现在踩的每一个坑后面还会以变体形式反复出现。提前把这些坑摸一遍之后会顺畅非常多。1.2 图像分类在整个学习路线里的位置我见过太多人上来就想啃检测、啃分割的源码结果被一堆数据增强、锚框、特征金字塔的术语劝退。图像分类之所以被公认为计算机视觉的练习曲是因为它把所有核心科目都浓缩在了一个相对干净的框架里特征提取靠卷积网络优化靠反向传播评估靠准确率指标。没有复杂的输出结构没有多任务损失你就是把一个图塞进去出来一个类别概率分布。同时分类模型本身也是后续任务的骨干。检测网络的分类分支就是分类器分割网络的编码器也常常直接用分类网络的预训练权重初始化。所以这一节你学的不是一个孤立的小玩具而是整套视觉技术栈的核心部件。学完这节再回头看检测、分割的公式你会觉得很多地方似曾相识因为底层的逻辑你在分类实战里已经亲手实践过了。1.3 这一节完成后你应该做到什么程度我在带人的时候有一个很朴素的验收标准不查资料能从头写出一份包含数据加载、模型定义、训练循环、验证评估的完整代码并且能解释每一步为什么这么写。能独立做到说明你真的吸收了而不是照着教程敲了一遍。所以这篇文章我尽量把为什么讲透参数怎么选、代码为什么是这个顺序、出了问题从哪查起都给你捋清楚。代码只是载体背后的决策逻辑才是这五节课真正想交给你的东西。2. 数据准备与预处理是真正的分水岭2.1 数据集的目录组织和加载方式很多人一上来就急着调模型把数据随便丢进一个文件夹写了几行load_data就开始训练结果后面踩了各种莫名其妙的坑——类别对不上啦、标签偏移啦、训练集验证集混在一起啦。我个人的经验是数据准备花的时间跟后面省下来的调试时间绝对是划算的。以官方的 CIFAR-10 为例它本身已经分好了 train 和 test。用torchvision.datasets.CIFAR10可以直接下载加载适合做快速验证。但真实项目中你手里的数据往往是一堆散乱的图片我建议从一开始就按标准目录结构组织data/ ├── train/ │ ├── cat/ │ │ ├── cat_0001.jpg │ │ └── ... │ └── dog/ │ ├── dog_0001.jpg │ └── ... └── val/ ├── cat/ └── dog/这种组织方式的好处是torchvision.datasets.ImageFolder直接就能读自动按二级目录名生成类别索引不用手写标签文件。我自己到现在做项目还是会先用脚本把数据整理成这个结构因为它简单、直观、不容易错。用ImageFolder加载就这么几行train_dataset torchvision.datasets.ImageFolder(rootdata/train, transformtrain_transform) val_dataset torchvision.datasets.ImageFolder(rootdata/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue)注意验证集这里shuffleFalse不是为了省那点算力而是为了让验证结果可复现——你每轮验证跑的顺序是一致的不至于因为顺序变化产生多余的随机噪声。2.2 归一化的门道mean 和 std 是怎么来的图像本质上是 0 到 255 的整数矩阵直接喂进网络其实也能跑但收敛会很别扭。原因说到底很简单梯度更新的步幅大小受特征数值尺度影响深层次特征的数值稍微抖动一下乘起来的梯度就可能爆炸或者消失。把输入标准化到均值 0、方差 1 附近相当于给优化器一个地形平坦的起点。transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), # 把PIL图片转成Tensor数值缩放到[0,1] transforms.Normalize(mean[0.4914, 0.4822, 0.4465], std[0.2470, 0.2435, 0.2616]) ])这三组数不是随手写的是 CIFAR-10 训练集在 RGB 三个通道上各自的均值和方法。每换一个数据集mean 和 std 最好自己算一遍方法很简单把训练集全图所有像素累加取平均再用均值算方差。千万别偷懒直接拿 ImageNet 的那组(0.485, 0.456, 0.406)用在不相关的小数据集上虽然不至于模型崩掉但分布偏移总会造成收益上的损失。2.3 数据增强宁可少调参也要先做这步数据增强是图像分类里性价比最高的白嫖算力手段。我的经验是一个有增强、参数默认的简单模型往往能打过没增强但精心调参的复杂模型。因为分类任务最缺的不是算力而是数据多样性。你拍一张猫换个角度再拍一张还是猫模型应该从这两个不同的像素排列里学到同一个概念。对 CIFAR-10 这种 32x32 的小图常用的增强组合就三板斧随机裁剪、随机水平翻转偶尔加点颜色扰动。train_transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.4914, 0.4822, 0.4465], std[0.2470, 0.2435, 0.2616]) ])RandomCrop(32, padding4)的意思是先把图四周补 4 像素的零再随机裁回 32x32。这样每次训练看到的图都略有平移等于免费把数据量翻了若干倍。而RandomHorizontalFlip这种左右翻转对我们熟悉的交通标志这类具有方向性的任务是禁忌但对猫、狗、飞机这些目标方向不敏感的数据集非常友好。这里有一条铁律验证集和测试集的 transform 里绝对不能出现随机增强。最多做 resize 和归一化。我见过同学把RandomHorizontalFlip写进了验证集结果准确率忽高忽低还以为是模型不稳定折腾了两天才发现是验证的时候图被随机翻转了纯属自己跟自己过不去。2.4 加载器的细节参数Batch Size 与 Workersbatch_size的选择直接影响训练速度和稳定性。我的建议是先用你的显存上限去定它比如 8GB 显存跑 ResNet18 输入 32x3264 的 batch 完全没压力就设 64。batch 太小比如 2 或 4梯度噪声太大训练曲线抖得像心电图batch 太大单个 epoch 里的参数更新次数太少反而可能欠拟合。num_workers是数据加载用的进程数设成 0 就在主进程里加载速度慢但最兼容设成 4 或 8 能明显提高吞吐量。Linux 环境下我习惯设成 CPU 核数的一半。在 Windows 上如果报多进程相关的错先把它设回 0 排查别上来就怀疑模型写错了。pin_memoryTrue这个参数很多人忽略。它把数据加载到锁页内存GPU 拷贝的路径更短理论上能省下不少数据传输时间。代价是内存占用略高但对大多数机器来说利远大于弊我基本常开。3. 模型搭建与训练细节别急着调参3.1 第一版模型自建小网络还是 ResNet18作为第五节课的实战我强烈不建议你从零手写卷积层——不是为了偷懒而是因为经典网络的架构设计经过了无数人的验证它的层数、通道数、残差连接方式每一样都有道理。你手写的灵感版CNN在学术想象力上是合格的但在 CIFAR-10 这种任务上很可能收敛得又慢又差然后你陷入是我代码错了还是网络设计问题的迷茫非常打击信心。这个阶段的正确姿势是直接用torchvision.models里的 ResNet18官方还提供了预训练权重。ResNet18 是整个残差家族里最轻量、最适合入门的版本结构清晰显存占用小一张 32x32 的 CIFAR-10 图跑起来毫无压力。import torchvision.models as models model models.resnet18(num_classes10)但这里有个细节ResNet18 默认的输入是 3x224x224它的第一个卷积层是conv1卷积核 7x7、步长 2一下把 224x224 降成 112x112。你要是把 32x32 的 CIFAR-10 图直接塞进去原始下采样太快信息大量丢失然后全局池化层输出也跟你预想的不一样。所以用 CIFAR-10 这类小图训练 ResNet要做一处小改造把第一个卷积层的核心参数改小。import torch.nn as nn model.conv1 nn.Conv2d(3, 64, kernel_size3, stride1, padding1, biasFalse) model.maxpool nn.Identity() # 去掉7x7分支后的最大池化改完整模型的前向路径是3x32x32 → conv1(3x3) → 64x32x32 → 残差块序列 → 全局平均池化 → 10 维分类头。这样才是为 32x32 输入定制的正确结构。3.2 损失函数和优化器的选择逻辑分类任务的标准损失是交叉熵PyTorch 里就是nn.CrossEntropyLoss()。这里有一个新手经常误解的点CrossEntropyLoss其实内置了 Softmax 操作所以模型的最后一层直接输出 10 个类别的 logits 就行不要在输出层再手动加一个 Softmax。加了在数值上通常不会出错但会让你的损失计算变得绕而且万一你用到了混合精度训练数值稳定性还会受影响。我见过有人一时糊涂输出层手动加了F.log_softmax又搭配NLLLoss用效果虽然差不多但代码读起来绕了一大圈没必要。优化器我推荐直接上 SGD Momentum不是因为这玩意儿效果最好而是因为它对超参数的理解成本最低。optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4)weight_decay是 L2 正则给大权重施以惩罚配合数据增强一起压制过拟合是图像分类训练的标准搭配。为什么不用 AdamAdam 确实收敛快、对学习率不敏感但它泛化性能在这类任务上常常不如调得好的 SGD。作为教学项目用 SGD 能逼着你理解学习率衰减的作用这个理解对你以后做任何优化都有价值。3.3 训练集验证集的划分别自欺欺人CIFAR-10 官方已经分好了直接拿来做 train/test 划分就行。如果用自己的数据务必保证类别分布均衡并且绝对不能有数据泄漏——比如同一张猫的图既出现在训练集又出现在验证集那你的验证准确率就是注了水的。划分比例我默认是 8:2 或 9:1关键看数据总量。总量几万张9:1 完全够总共有几千张验证集至少得留几百张否则验证准确率的波动能大到看不出有没有改进。顺带说一下固定随机种子的习惯。开头写三行能让你的整轮实验可复现调参的时候不会因为随机性把锅甩给模型import random, numpy as np, torch random.seed(0) np.random.seed(0) torch.manual_seed(0)如果用了 GPU再加一句torch.cuda.manual_seed_all(0)。实际跑起来因为多线程、非确定性算法等因素不可能完全相同但至少能消除换一次运行换个结果这种最让人崩溃的问题。4. 训练过程实录与核心参数解读4.1 一份可直接抄的主干代码这里我先给你一份完整的训练循环骨架把注释写明白然后逐段拆解里面的关键细节import torch import torch.nn as nn from torch.utils.data import DataLoader import torchvision import torchvision.transforms as transforms import torchvision.models as models # ---------- 数据 ---------- train_transform transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.4914, 0.4822, 0.4465], std[0.2470, 0.2435, 0.2616])]) val_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.4914, 0.4822, 0.4465], std[0.2470, 0.2435, 0.2616])]) train_dataset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtrain_transform) val_dataset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue) # ---------- 模型 ---------- model models.resnet18(num_classes10) model.conv1 nn.Conv2d(3, 64, kernel_size3, stride1, padding1, biasFalse) model.maxpool nn.Identity() device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) # ---------- 训练循环 ---------- def train_one_epoch(epoch): model.train() train_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() train_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() avg_loss train_loss / total acc 100.0 * correct / total print(fEpoch {epoch} | Train Loss: {avg_loss:.4f} | Acc: {acc:.2f}%) return avg_loss, acc def validate(epoch): model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() avg_loss val_loss / total acc 100.0 * correct / total print(fEpoch {epoch} | Val Loss: {avg_loss:.4f} | Acc: {acc:.2f}%) return avg_loss, acc for epoch in range(1, 31): train_one_epoch(epoch) validate(epoch)这段代码跑在普通显卡上CIFAR-10 大约十几分钟一个 epoch30 个 epoch 大概几个小时能收完。最后验证准确率可以到 85% 到 90% 之间比瞎猜的 10% 已经高出太多了。4.2 训练循环里最容易写错的三处细节第一处是model.train()和model.eval()的切换。这两个方法控制的是 BatchNorm 和 Dropout 在训练/推理时的不同行为。训练时 BatchNorm 用当前 batch 的均值方差推理时用的是训练阶段累积的全局统计量。如果你忘了切回eval()验证时每一批数据的归一化统计量都不一样结果会忽高忽低表现很不稳定。这基本是新手报错榜第一名。第二处是optimizer.zero_grad()的位置。PyTorch 的梯度是默认累加的不清零的话每个 batch 的梯度会叠加在一起相当于用一个巨大无比的 batch 去更新参数。正确顺序永远是zero_grad()→ 前向 → 算 loss →backward()→step()。我自己的肌肉记忆就是这个顺序一次都没错过。第三处是验证阶段用with torch.no_grad():包起来。不包也能跑但会额外计算一堆根本用不上的计算图显存和速度都会受影响。更隐蔽的问题是如果你后面要做模型转换或者推理部署就会发现这个多余的计算图会成为各种报错的源头养成验证推理都包no_grad的习惯后面省很多事。4.3 学习率衰减SGD 的胜负手如果你拿着lr0.1不做衰减直接硬跑 60 个 epoch会发现前 20 个 epoch 涨得很快后面就卡住了上不去。这不是模型不行是学习率太大导致参数在最优解附近来回震荡、无法收敛到更小的损失盆地。标准做法是步进式衰减每过一定 epoch 数把学习率降为原来的十分之一。CIFAR-10 上我习惯在第 15、25 个 epoch 各衰减一次 0.1 倍scheduler torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones[15, 25], gamma0.1)然后在每个 epoch 结束时调用scheduler.step()。注意调用时机——PyTorch 的MultiStepLR是在调用step()时根据当前 epoch 数决定是否衰减所以通常放在一个 epoch 结束之后。我盯着训练曲线看的时候有个很直观的经验学习率衰减那一刻模型会有一个明显的再进一步的提升损失和准确率都突然往好了跳。如果你衰减完了毫无变化通常说明模型早就收敛了衰减不衰减都差不多这时候就应该关注是不是容量不够的问题了。4.4 Checkpoint 保存怎么存才算专业很多人训练的时候不存模型等最后跑完才发现中间某个 epoch 效果最好但没存下来只能重来。我的习惯是每轮验证结束如果验证准确率创了新高就把模型状态存成一份best_model.pth。只保存最好的那一个不用把每一轮的都存不然硬盘很快被撑爆。best_acc 0.0 for epoch in range(1, 31): train_one_epoch(epoch) val_loss, val_acc validate(epoch) if val_acc best_acc: best_acc val_acc checkpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, } torch.save(checkpoint, best_model.pth) print(f保存最佳模型验证准确率: {val_acc:.2f}%)注意这里保存的是state_dict()而不是整个model。前者的好处是文件小、跨环境兼容好、结构清晰。加载的时候先创建模型结构再load_state_dict把权重填进去。而torch.save(model)这种整模型保存方式在版本升级之后很容易加载失败我踩过这个坑后来再也没用过。我把 optimizer 的state_dict也一起存了因为真要断点续训的话光有模型权重、没有优化器动量状态恢复出来的训练过程会跟你中断之前不完全一样。完整保存才能在某个 epoch 中断后接着继续跑不用从头开始。5. 常见问题排查与避坑实录5.1 训练 loss 下不去的系统排查法这恐怕是每个人都会撞见的头号问题。我总结了一个从快到慢的排查顺序照这个顺序走绝大多数情况几分钟就能定位。第一步先看 loss 初值是否符合直觉。10 分类任务随机初始化后交叉熵应该在 ln(10) 约等于 2.30 附近。如果你设置完模型损失直接是 0 或者几百那大概率是代码有 bug而不是训练问题。比如标签没对齐、数据预处理方向错了这类基础性错误都会在初值这里暴露。第二步确认梯度有没有真的流动起来。挑一批数据手动来一次loss.backward()然后打印几层参数的gradfor name, param in model.named_parameters(): if param.grad is not None: print(name, param.grad.abs().mean().item())如果发现某层梯度全是 0要么是这一层的输入本身就断了要么是前面的层输出有问题。常见的元凶包括把ReLU错装成ReLU6导致输出被截断或者某个forward里写错了张量维度。打印梯度这一步基本能锁定。第三步拿一个极小数据集做拟合测试。比如只拿 4 张训练图片反复跑如果模型连这 4 张都记不住、loss 死活不降说明网络结构或者数据链路有 bug。反之如果极小数据能快速过拟合到 100%说明模型没问题问题在整体训练设置上——这时候就该检查学习率、数据增强是不是过猛了。5.2 过拟合的三大信号与标准操作图像分类里过拟合的识别其实很简单训练准确率一路爬升接近 100%验证准确率却停滞甚至开始掉头向下训练 loss 和验证 loss 的差距越拉越大。CIFAR-10 用小网络跑到 90% 训练准确率还看不出过拟合但如果你的网络是 ResNet50 这种大模型训练集几万张那过拟合几乎是必然的。应对手段按优先级排第一是增强数据增强强度随机裁剪的 padding 从 4 加到 8再加随机旋转或随机擦除。第二是调高weight_decay1e-4 不够就试 5e-3。第三是加 Dropout对 ResNet 来说其实不需要它本身有 BatchNorm 也有一定的正则效果。第四是换更小的网络实在不行再从数据规模上想办法。有一个很容易混淆的点过拟合和训练完了epoch 不够经常长得很像。前者是训练 loss 低、验证 loss 高后者是两个 loss 都很高。前者加大正则后者就单纯多训几个 epoch方向完全相反判断错了等于白折腾。5.3 验证准确率卡在低位的排查顺序如果训练一切正常但验证集准确率就是上不去比如 10 分类卡在 30% 甚至 20%先别急着骂模型。我见过最多次的原因是验证集的 transform 和训练集的差异处理出了问题比如验证集忘了做ToTensor()导致输入还是 PIL 格式直接进了模型这必炸。第二个常犯的错误是model.eval()忘了写。第三个可能是什么地方把标签顺序给打乱了比如用了shuffleTrue的验证集或者ImageFolder的类别排序和你预期的不是一回事。再往下排查考虑是不是数据增强设置太强。我有一回做一个小数据集分类训练集用了大角度的随机旋转加极端颜色扰动結果模型把增强后的分布学到了而对原始风格的验证集完全不适应。这时候把增强幅度调回正常水平准确率立刻恢复正常。最后一个排查方向是类别不均衡。如果你的数据里 90% 都是猫模型全猜猫就有 90% 准确率你还以为模型很强其实它对狗一无所知。这种情况不要只看整体准确率要看每个类别的召回率、F1。必要的时候用类别加权损失或者过采样。5.4 显存不足与训练速度异常的实用对策显存爆掉CUDA OOM是入门阶段跑模型最常见的技术性崩溃。标准做法是减小 batch size,但减到 1 还不够用就得换思路了。我在比较短的教程里会提torch.cuda.empty_cache()但它治标不治本。更好的办法是用torch.backends.cudnn.benchmark True让 cuDNN 自动选最快的卷积算法提升速度。数据加载num_workers加大训练循环里的 CPU 瓶颈优先解决。如果模型实在太大考虑冻结前面几层的梯度只训后面的分类头参数显存占用和计算量都会明显下降。训练速度异常慢还有一种隐蔽原因num_workers0数据加载完全串行GPU 大部分时间在空等。我见过有人用默认设置跑 224x224 的 ImageNet 子集GPU 利用率只有 30%把num_workers加到 8 之后利用率直接拉到 90% 以上训练时间缩短了一半还多。5.5 常见问题速查表现象最可能的根源首选排查动作训练 loss 完全不降代码 bug 或数据链路断裂先打印初始 loss再做极小集过拟合测试初始 loss 远偏离 2.30标签错位、预处理错误检查数据集样本和标签对应关系训练 loss 降验证不降过拟合或验证集泄露检查验证集是否被增强加大正则准确率忽高忽低验证集出现了随机增强删掉验证 transform 里所有随机操作显存不足batch size 过大减小 batch size 或冻结部分层梯度训练慢到怀疑人生num_workers 太小调大 num_workers 并确认 pin_memory 开启重启程序后结果变样随机种子未固定在开头固定 seed加载 checkpoint 报错结构对不上或格式不符只存 state_dict加载时先建模型结构6. 让模型再进一步三条务实的优化路线6.1 数据增强加码自动增强与随机擦除如果你跑完上面的基线心里有点数了最值得花时间的就是数据增强加码。CIFAR-10 上常用的梯度是三段式第一段是 RandomCrop 翻转第二段加 ColorJitter第三段上 RandomErasing 或者 Cutout——随机用一块黑色矩形遮住图中某个区域强迫模型不能只依赖局部特征做判断这个思路非常实用。# 在已有的增强链末尾追加 train_transform.transforms.append( transforms.RandomErasing(p0.5, scale(0.02, 0.2), ratio(0.3, 3.3)) )随机擦除的本质是正则化它逼着模型去利用全局信息而不是偷懒抓某一个局部特征下结论。我自己的实测里CIFAR-10 加上它一般能再涨 1 到 2 个点代价是训练时间稍微长一点收益完全值得。6.2 迁移学习用预训练权重就是降维打击如果不想在这份小数据集上默默炼丹迁移学习是一条公认的捷径。用在 ImageNet 上预训练好的 ResNet18 权重只不过 ImageNet 是 1000 分类最后的分层头是 1000 维换成 CIFAR-10 时把最后一层换成 10 维输出即可model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 10)这里要注意用预训练模型时输入图上采样到 224x224 才符合它原来的输入分布你要把Resize((224, 224))加到 transform 里。训练策略上也跟从头训不一样先用一个比较小的学习率比如 0.001把新加的最后一层训几个 epoch 预热然后可以放宽到全部层。前面的卷积层特征已经足够通用微调幅度不用太大不然容易灾难性遗忘。迁移学习真正的威力体现在小数据集上——几百张到几千张的数据从头训一个网络几乎必过拟合但用预训练模型微调往往能直接到 80% 以上。6.3 测试期增强与模型集成的小技巧最后分享两个实验阶段很好用的提分手段成本低、直接有效。测试期增强TTA的思路是推理的时候不只看原图一次而是把图做若干次轻微变换比如随机裁剪、翻转然后把所有输出的概率取平均再取 argmax。这个思路能平滑掉单次预测的偶然性。CIFAR-10 上实测可以稳定涨 0.5 到 1 个点。def predict_tta(model, image, transforms_list, device): probs [] with torch.no_grad(): for t in transforms_list: img_tensor t(image).unsqueeze(0).to(device) output torch.softmax(model(img_tensor), dim1) probs.append(output) avg_probs torch.mean(torch.cat(probs, dim0), dim0) return avg_probs.argmax().item()模型集成就更简单粗暴了训练两个不同初始化和增强配置的模型推理时把概率平均。两个模型都能到 90% 左右集成一下往往能再涨 1 到 2 个点。代价是训练时间翻倍、推理时间翻倍竞赛里很实用工程上则要看业务能不能接受这个延迟。到这里一条完整的图像分类流水线你已经亲手走通了。我个人在实际带项目的过程里最深的一点体会是图像分类看起来就这点东西但数据、模型、训练、评估四个环节没有一个是能糊弄过去的——每个环节的错误都会以各种诡异的面貌出现在结果里。如果你跑通了这份代码不妨再自己动手做一次实验把数据增强删掉跑一轮、把学习率衰减去掉跑一轮、把 batch size 改成 16 跑一轮亲眼看曲线变化。这个过程比看十篇教程都有用。等你真正理解了这些超参数在曲线上的投影再去看检测、分割那些复杂模型会发现自己已经能读懂它们在做什么了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表