ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

图像分类入门:用PyTorch从零构建CNN的完整实践指南

图像分类入门:用PyTorch从零构建CNN的完整实践指南 1. 图像分类到底在解决什么问题做深度学习的人无论搞视觉、语音还是NLP迟早都会碰图像分类。甚至可以说图像分类就是深度学习入门的第一道坎。我见过不少同学学完前馈神经网络和反向传播之后跃跃欲试地想拿网络去识别猫狗结果一跑就发现全连接网络遇到稍微大一点的图片就崩溃了显存直接爆掉。这就是我为什么要把图像分类单独拎出来写一篇的原因它是理解CNN、迁移学习、数据增强等一系列核心概念的最佳载体。1.1 一个容易跑偏的理解误区很多人一上来就喜欢研究各种花哨的网络结构什么注意力机制、Transformer、ViT结果连最基础的分类任务都没跑通。这事我特别有感触因为早期我也是一个追新不追旧的人总觉得CNN那套卷积池化太老套了结果回头补基础补了很久。图像分类本质上就是一个函数映射的问题输入一张图片输出一个类别标签。就以最经典的猫狗二分类为例网络要做的事情就是学习一个函数 f让 f(image) 能够输出“猫”或“狗”的标签。你可能会觉得这没什么大不了但实际上这个函数极其复杂因为图片在计算机眼里就是一串数字矩阵一个 224x224 的彩色图片就有 224x224x3150528 个像素值每个像素值从 0 到 255 不等。要让网络从这一个巨大的数字空间里找到规律是一件规模很大的事。更麻烦的是同一个物体在不同光照、角度、遮挡条件下像素值差异巨大但语义上仍然是同一个东西。这就是所谓的语义鸿沟问题。传统方法要手工提取特征比如SIFT、HOG然后再喂给SVM分类器这套组合拳在深度学习之前是很流行的但泛化能力始终有限。而卷积神经网络能够自动学习从底层边缘纹理特征到高层语义特征的多层次表示这也是它能在图像任务上全面碾压传统方法的核心原因。1.2 一条完整的学习路径我给新手推荐的路径是先用小数据集跑通一个简单的CNN理解卷积、池化、全连接这些基本模块的输入输出维度变化然后逐步尝试经典网络结构如VGG、ResNet理解为什么加深网络会导致退化问题、残差连接为什么有效接着可以尝试用迁移学习在预训练模型上微调最后再考虑用更先进的结构比如EfficientNet、ViT来刷新精度。这篇文章就沿着这条路径走先把数据集选好再手写一个能跑的CNN然后我带你踩一遍训练过程中的坑最后再逐层拆解优化的思路。全程我会以CIFAR-10和猫狗数据集为例代码用PyTorch来写毕竟现在PyTorch已经是学术界和工业界的事实标准了。2. 先选对数据集再谈其他2.1 入门三件套MNIST、Fashion-MNIST、CIFAR-10我见过很多新手犯一个错误一上来就用ImageNet这种超大尺度数据集练手结果GPU跑了几个小时也没有等到一个epoch结束最后心态崩了放弃了。入门阶段图的就是一个“快”字数据集分辨率不能太大类别数量不能太多这样才能快速验证模型结构是否写对了。MNIST是手写数字数据集28x28灰度图10000张测试图60000张训练图属于是深度学习界的HelloWorld。它的优点是训练极快CPU跑模型也就是几分钟的事特别适合用来验证梯度计算、损失函数下降趋势、卷积层维度变化这些基础逻辑。Fashion-MNIST是MNIST的替代品同样28x28但内容是衣服鞋子等10类物品难度稍微高一点点也更接近真实场景。CIFAR-10可以说是最经典的图像分类入门数据集了60000张32x32彩色小图分成飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车10个类别。别看分辨率只有32x32它的难度比MNIST高了一个量级因为彩色图像的纹理信息更丰富类别之间的外观差异也没那么大。这个数据集特别适合用来做网络结构的快速对比实验我在后面跑代码示例用的就是它。2.2 进阶数据集怎么选当你把CIFAR-10跑到90%以上准确率之后就可以考虑换更硬核的数据集了。CIFAR-100是CIFAR-10的升级版100个类每类600张图类别间有超类关系难度上了不止一个台阶。ImageNet-1K是图像分类领域公认的试金石128万张训练图1000个类别分辨率224x224以上几乎所有经典网络结构的论文都会在ImageNet上报告结果。但这个数据集全量下载需要几百GB个人电脑基本没法跑完整训练一般只在预训练和论文复现时使用。如果做细粒度分类比如区分不同鸟的种类、不同汽车的型号CIFAR那类粗粒度数据集就不够用了可以考虑Stanford Dogs、CUB-200-2011鸟类数据集这些细粒度分类数据集。另外还有一个方向的趋势是场景分类和遥感图像分类像一些公开的森林遥感图像分类数据集或者有人专门做的POI兴趣点数据集结合卫星图做土地利用分类这些在实际项目中都很有价值。2.3 自己造数据集必须注意的坑实际工程项目里更多情况是没有现成数据集可用需要自己采集和标注。之前就有朋友问我做占道经营检测需要什么数据其实这种场景数据网上不好找往往得自己拍或者接入城市管理摄像头的视频流截帧。自己做数据集有四个大坑必须避开。第一每一类图片数量要均衡。假设你做了两个类A类有5000张B类只有50张那网络完全可以在训练时偷懒把能100%识别成A类整体准确率也有99%。这就是类别不平衡问题解决思路包括过采样、欠采样、以及损失函数层面加类别权重。第二划分训练验证测试集时要按类别分层划分保证每个类在不同集合里的比例大致一致。第三训练集和测试集不能有重叠这是老生常谈但我真遇到过有人图省事直接复制了同一批图。数据集标注是另一件费时费力的事。自己写标注脚本容易出低级错误建议直接用LabelImg、Labelme这些开源标注工具支持Pascal VOC和COCO格式方便后续转成分类或者检测的格式。分类任务其实还需要注意一点背景要多样化。如果所有训练图的背景都是一种颜色那么模型可能学的根本不是目标物体本身而是背景颜色一换环境就失效。这就是所谓的领域漂移问题。3. CNN为什么能搞定图像分类3.1 从全连接网络到CNN核心动机在哪里全连接网络处理图像有个致命缺陷参数数量爆炸。拿一张224x224的RGB图片为例展平之后是150528个输入特征如果第一层设置1024个神经元那么这一层的权重就有 150528x1024约1.5亿个参数这在显存里根本放不下更别说训练收敛了。而且全连接网络对空间结构毫无感知把图片像素随机打乱网络训练出来的结果不会有什么区别这说明它完全没有利用图像的空间局部性。CNN的设计恰恰就针对这两个痛点。卷积操作使用局部感受野每个神经元只连接输入图像的一个局部区域参数通过权值共享大大减少。还是224x224输入的例子里假设用3x3卷积核输出通道64那参数就是3x3x3x641728个权重。这种局部连接和权值共享的设计背后隐含了一个很强的先验图像的特征具有局部性和平移不变性一个特征在一处被学习到在另一处也能被识别出来。这就是为什么图像处理不选前馈神经网络而选CNN的核心原因。前馈神经网络学习到的是全局特征组合而CNN天然具有局部特征提取能力和参数效率优势。经常有人问能不能用Transformer做图像分类ViT确实可以而且大模型上效果超越了CNN但它需要海量训练数据或者大规模预训练在中小规模数据集上反而不如CNN稳。3.2 卷积层、池化层、全连接层各司其职CNN三个基本模块各有分工。卷积层做的事情是特征提取用一个卷积核在输入图上滑动每次计算局部区域的加权和加上偏置再过非线性激活函数。多个不同的卷积核可以提取不同的特征比如有的关注水平边缘有的关注垂直边缘有的关注纹理。越深的卷积层提取的特征越抽象这也是为什么CNN最后几层可视化出来往往是脸、轮子、眼睛这些高层的语义部件。池化层的目的是降维和增强平移不变性最常用的是最大池化在2x2的窗口里取最大值输出尺寸减半。池化不引入额外参数计算量几乎为零但能有效降低后续层级的计算复杂度同时让网络对小范围的像素位移更鲁棒。注意现在很多新网络已经用stride2的卷积代替池化了效果类似但信息保留更多。经过一堆卷积池化之后特征图被展平喂给全连接层最后由类别的概率分布输出。CIFAR-10的话输出10个神经元再过Softmax得到概率配合交叉熵损失函数训练。整个过程就像一条流水线底层卷积负责从像素中找出边缘纹理中层卷积把这些线索组合成局部部件高层卷积整合出全局语义全连接层做最终的决策。3.3 经典结构快速盘点学习CNN绕不开几个经典结构每个结构都解决了一个核心问题。VGG告诉我们小卷积核堆叠比大卷积核更有效用多个3x3卷积串联可以得到与大卷积核相同的感受野但参数量更少、非线性更强。ResNet解决了网络加深之后出现的退化问题即训练集准确率反而下降这是梯度消失和优化困难共同导致的结果残差连接让梯度可以跨层传播让几百层的网络训练成为可能。GoogLeNet的Inception模块让网络在同一个层面上用不同尺寸的卷积核并行提取特征然后再拼起来网络可以做宽。MobileNet用深度可分离卷积大幅减少参数是移动端部署的基石。EfficientNet则通过复合缩放方法统一调节深度、宽度和分辨率三个维度达到了当时的最优精度-效率平衡。我不是反对你一上来就学ViT这类新结构但我的建议是先把这些经典CNN结构吃透。原因很简单图像分类后续几乎所有技巧比如数据增强、正则化、学习率调度、迁移学习在CNN上验证成本最低理解得最透彻。等这些驾轻就熟之后再去看Transformer在图像上的工作你才会明白它到底改进了什么、牺牲了什么。4. 手把手实现一个CNN图像分类器4.1 写代码前的准备工作选PyTorch做实验主要是因为调试太方便了。TensorFlow 2.x也不错但我个人觉得PyTorch的eager模式写起来更像写Python程序出问题可以随时打断点看中间张量。建议用1.8以上版本新版本对MPS和CUDA支持都更完善。开发环境这里不做过多建议直接说三条务实的经验Windows上建议直接用Anaconda建一个独立环境避免不同项目依赖打架Linux服务器上要用虚拟环境尽量用conda或者venvPyTorch的安装命令去官网按CUDA版本生成不要用默认的pip源不然下载容易卡住。conda create -n image_classification python3.9 conda activate image_classification pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install matplotlib tensorboard我习惯同时装上tensorboard因为训练到后期到底是不是在过拟合光看终端打印那几行数字是感受不到的直接可视化train loss和val loss曲线最直观。还有一个建议第一次跑通之前千万不要开混合精度训练也不要开分布式这些花活会掩盖很多基础bug等代码跑通了再加不迟。4.2 加载数据集与DataLoader配置PyTorch的torchvision.datasets内置了CIFAR-10和MNIST这些常用数据集下载之后会自动解压到指定目录免去手动找数据源的麻烦。这里要特别说明一下transform这个参数训练集和测试集的transform通常不一样训练集往往带随机翻转、随机裁剪这些数据增强测试集只做标准化。import torch import torchvision import torchvision.transforms as 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)) ]) trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) testset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform_test) trainloader torch.utils.data.DataLoader(trainset, batch_size128, shuffleTrue, num_workers2) testloader torch.utils.data.DataLoader(testset, batch_size128, shuffleFalse, num_workers2)这里的归一化均值方差是CIFAR-10官方统计好的RGB三个通道各一组。不同数据集的归一化参数不一样如果用ImageNet预训练权重那就必须用ImageNet的均值和方差。这是个很隐蔽的坑迁移学习中加载了预训练权重输入数据归一化却还在用MNIST的那套参数结果模型性能怎么都上不去。DataLoader的num_workers设置和batch_size都要根据机器来调整。num_workers太小会拖慢数据加载速度训练时GPU老是在空等num_workers太大容易造成内存溢出。个人经验是CPU核数和内存允许的情况下num_workers设成4到8比较舒服。batch_size则要考虑显存上限和收敛速度的平衡CIFAR-10上128是一个比较稳妥的值。4.3 搭建一个极简但完整的CNN新手搭CNN最容易踩的坑是维度算错。因此强烈建议先把PyTorch的nn.Sequential写到结构里并且在编写网络之后打印一个示例输出的shape用这种方式验证维度对不对。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(2, 2) self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(128) self.fc1 nn.Linear(128 * 4 * 4, 256) self.fc2 nn.Linear(256, num_classes) def forward(self, x): x F.relu(self.bn1(self.conv1(x))) x self.pool(x) x F.relu(self.bn2(self.conv2(x))) x self.pool(x) x F.relu(self.bn3(self.conv3(x))) x self.pool(x) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.fc2(x) return x model SimpleCNN() # 验证维度 sample torch.randn(2, 3, 32, 32) print(model(sample).shape) # 期望输出 [2, 10]这个网络结构很轻量三个卷积块加两个全连接层CIFAR-10上跑不到80轮准确率就能到70%以上。BatchNorm层虽然简单但对于收敛速度帮助巨大特别在CNN里我这里直接加上了效果会比不加好很多。网络输出的10个值叫logits表示每个类别的得分。计算损失的时候直接用nn.CrossEntropyLoss它内部已经帮你做了Softmax和one-hot编码不需要在模型输出后再手动加Softmax再去算损失这样既省事又数值稳定。4.4 训练循环的完整代码与关键细节训练循环本身并不长但有几个细节直接决定你训练是否顺利。第一个是优化器和学习率的选择SGD带动量是CNN训练里的经典配置动量设0.9初始学习率0.1搭配退火策略很稳。Adam收敛快但往往精度略逊于调好的SGD新手拿来做快速验证是可以的。第二个是loss必须写到tensorboard里最好同时记录train/val的loss和acc。import torch.optim as optim from torch.utils.tensorboard import SummaryWriter device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay5e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) writer SummaryWriter(runs/cifar10_cnn) def train_one_epoch(epoch): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in trainloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() train_loss running_loss / total train_acc 100.0 * correct / total writer.add_scalar(train_loss, train_loss, epoch) writer.add_scalar(train_acc, train_acc, epoch) return train_loss, train_acc def validate_one_epoch(epoch): model.eval() running_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in testloader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) running_loss loss.item() * inputs.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() val_loss running_loss / total val_acc 100.0 * correct / total writer.add_scalar(val_loss, val_loss, epoch) writer.add_scalar(val_acc, val_acc, epoch) return val_loss, val_acc for epoch in range(50): train_loss, train_acc train_one_epoch(epoch) val_loss, val_acc validate_one_epoch(epoch) print(fEpoch {epoch1}/50, Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, Val Acc: {val_acc:.2f}%) scheduler.step()model.train()和model.eval()这个细节很容易被忽略但极其重要。训练模式下BatchNorm会使用当前的batch统计量更新running mean和running var而eval模式下会使用训练阶段累积的running统计量。如果你忘了切到eval模式就去验证验证结果会异常波动尤其是batch_size比较小的时候。4.5 推理与模型保存训练完之后保存模型建议不要只保存state_dict把整个参数和优化器状态都保存下来这样如果训练中断还能恢复。模型保存成完整checkpoint后面继续训练就不用从头再来。torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, val_acc: val_acc, }, checkpoint_cifar10.pth)推理的时候如果需要看模型预测的类别输出通过Softmax转成概率然后取最大值对应的索引。唯一要注意的问题是推理阶段的transforms必须和训练时保持一致特别是Normalize参数。我见过有人训练时统一做了Normalize推理时忘了写或者写错最终预测结果全乱套。5. 图像分类的优化手段5.1 数据增强训练集扩军的正确姿势数据增强是提升图像分类性能投入产出比最高的一种手段成本几乎为零只需要在读取数据阶段加一些随机变换。核心思想很简单通过随机扰动让模型见过更多样的数据降低对某些无关属性的过拟合。比如猫的照片无论左右翻转都是猫那左右翻转就是一种合理的数据增强。CIFAR-10上常用的加强策略是RandomCrop加Padding和RandomHorizontalFlip。RandomCrop(padding4)表示先把32x32的图四周各补4个像素的0然后随机裁剪回32x32相当于引入了一点平移变化。RandomHorizontalFlip以50%概率做水平翻转。这两招组合下来CIFAR-10准确率的提升幅度可能高达5个百分点以上。进阶一点的增强还有Cutout和Mixup。Cutout的做法是在训练图上随机挖掉一块正方形区域强迫网络不要只依赖某一个局部特征。Mixup做法是把两张训练图按比例混合、标签也做线性插值实验结果比较强也让模型更平滑。这两类增强在分类任务上效果都不错但要注意Mixup会改变损失函数的计算方式标签不再是one-hot而是软标签。另外AutoAugment这类自动搜索增强策略的论文效果很好但在小数据集上不一定划算计算成本较高。我一般的建议是先用经典三件套把基线跑出来如果还欠拟合或过拟合再考虑加复杂增强。5.2 迁移学习站在预训练模型肩膀上从头训练一个CNN在ImageNet上跑出好效果需要很大的算力和数据个人基本不现实。更务实的做法是使用在ImageNet上预训练好的模型然后在自己数据集上微调。比如用torchvision里现成的ResNet18、ResNet50将最后全连接层替换成符合自己类别数量的新层然后只训练新层或者全量微调。import torchvision.models as models model models.resnet18(pretrainedTrue) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 10)微调时关键的选择是冻结之前层只训练分类头还是全部层都更新。数据量少就冻结前面的卷积基只训练分类头因为浅层特征在通用图像上已经足够好数据集太小会破坏这些特征。数据量中等或比较多可以全量微调但初始学习率要调小比如0.001或更低否则容易灾难性遗忘。预训练模型的输入尺寸通常是224x224和数据集原始尺寸可能不符。两种做法一是直接用transform.Resize把数据集变成224x224二是修改模型的avgpool和fc来适配小尺寸输入。对于CIFAR-10这种小图我自己更倾向于调整网络结构使输入保持32x32但效果不一定比Resize到224好需要实测。保存预训练模型文件很大一般200MB到600MB不等。国内访问官方下载源经常不稳定如果下载失败或者太慢建议先在能访问的地方把权重下载下来然后放到~/.cache/torch/hub/checkpoints目录下torchvision的pretrainedTrue会自动去找。有同学私信问过cnn explainer离线包的事其实那些可视化工具也都是需要先有模型权重才跑得起来权重下载问题先解决工具才能用。迁移学习对个人项目的意义很大没有强大算力、没有海量数据借力ImageNet预训练模型往往能在几天内把一个小型分类任务做到实用水平。这也是我文章里反复强调要先理解CNN的原因只有理解了网络结构如何替换、冻结、微调迁移学习才不是单纯调用别人模型的简单拼装。5.3 训练策略学习率、正则化与早停学习率调度对CNN的训练影响非常大。固定学习率时前期可能收敛太慢后期又可能在最优解附近震荡。我常用的策略是训练初期用一个稍大的学习率快速探索训练后期用余弦退火把学习率降到很低让网络在损失曲面底部精细收敛。CosineAnnealingLR是PyTorch内置的调度器T_max设成训练的总epoch数就行。还有一种常见做法是StepLR比如每隔30个epoch把学习率乘以0.1搭配SGD动量效果很稳定。学习率的初始值取决于具体网络和batch_sizebatch_size翻倍时学习率一般也可以适当翻倍这是线性缩放规则尤其对于SGD最优解有效。正则化主要防过拟合。weight_decay是SGD优化器里的L2正则项通常设1e-4到5e-4效果立竿见影。Dropout在CNN全连接层前加一个p0.5的dropout也能有效防过拟合但CNN里卷积层不用加因为参数共享本身已经有正则化作用了。早停机制也很实用。训练时记录验证集最优准确率如果连续多个epoch没有刷新记录就停止训练并恢复保存的最优模型。节省时间也防止训练过久导致过拟合。我自己写训练脚本时经常会设置一个patience10的EarlyStopping类代码虽然简单但能在调参阶段节省大量时间。权重初始化是一个容易被忽略但影响很大的细节。PyTorch的nn.Conv2d和nn.Linear默认使用Kaiming均匀初始化如果使用ReLU激活这种初始化是搭配好的。如果用ImageNet预训练权重做初始化那就不需要再额外处理了。5.4 从72%到91%一个完整的调参记录这里我给大家一个真实参考我用前文那个SimpleCNN跑CIFAR-10基线初始版本什么都不做准确率大概72%。加入BatchNorm到了75%。加上RandomCropFlip增强直接跳到82%。加CosineAnnealing学习率调度到84%。加weight_decay和更大的epoch数量到50精度达到87%。把网络从SimpleCNN升级成ResNet18同时使用预训练权重微调准确率轻松到了91%以上。这个记录想说明一件事图像分类的优化是叠加态每项技术都能带来一点提升最终性能是全部手段叠加的结果。如果某个环节出问题了性能提升曲线会卡在某一点上这时候不要盲目改网络结构先从数据增强、归一化、学习率这些最基础的环节回头排错。6. 常见问题与排查技巧实录6.1 模型不收敛怎么办训练loss不下降是新手最常遇到的问题而且原因花样百出。第一条查数据流把DataLoader里的图片用matplotlib显示出来亲眼确认标签和图片是一一对应的这一步能排除很多低级错误。第二条查归一化输入图片的值域如果不在预期范围比如像素值还是0到255没除以255训练会非常慢且不稳定。第三条查损失函数CrossEntropyLoss的输入是logits而MSE需要过Softmax用错了会坑到自己。如果loss直接变成NaN大概率是学习率太大了。这类情况的排查办法是把学习率调小一个数量级试试如果NaN消失一般就是这个问题。还有可能是数据里有脏值当自定义数据集里引入一些损坏的图片时训练到某个batch会直接触发NaN。6.2 过拟合和欠拟合怎么判断训练集准确率很高但验证集准确率低这是明显的过拟合。解决优先级依次是加数据增强、加dropout、加weight_decay、减小模型规模。验证集和训练集差距不大但都低那就是欠拟合需要加深网络或者增加特征提取能力。欠拟合不是靠无限增加epoch能解决的增加epoch只能把训练损失压更低一旦模型容量不够验证集准确率会停滞。判断过拟合和欠拟合不能光看最后的准确率数值最好盯着训练过程中的曲线。如果训练loss一直降、验证loss先降后升那么拐点就是最佳停止点。前面说的早停机制在这种情况下很有价值。6.3 显存不够与服务端部署的坑Batch size太大、分辨率太高都容易爆显存。出现OOM时先降batch_size这是最常见最直接的解决办法。其次检查代码是否忘了设置torch.no_grad()的验证阶段或者后向传播时忘掉清零梯度导致历史计算图累积存储。再不行就开梯度累积即多个小batch加起来更新一次梯度这样能模拟大batch的效果但显存消耗小很多。训练完成之后如果要去服务端做推理部署ONNX导出是最常用的跨平台方案。PyTorch模型转ONNX时需要注意固定输入尺寸因为ONNX对动态shape的支持在各种推理引擎里兼容性不一。对于只需要分类的场景推理阶段的预处理最好用OpenCV而不是PIL因为服务端并发场景对速度要求高PIL在某些平台上性能比较差。6.4 可视化工具帮你快速定位问题很多人训练半天准确率不涨第一反应是改网络结构其实最应该做的是先看中间特征图。torchvision里可以注册hook把某个卷积层的输出特征图提取出来保存成图片观察网络到底学到了什么。比如第一层卷积输出应该是边缘纹理的特征如果全部是噪声或者全黑说明网络可能在初始化或者梯度传播上有问题。TensorBoard在可视化训练曲线方面绝对值得用起来。loss曲线上有些规律可以辅助判断训练loss下不去可能是模型太小或者学习率太低训练loss震荡很大可能是batch_size太小或者学习率太大验证loss持续不降甚至上升说明可能要调正则化或者减小模型容量。写代码的时候把tensorboard相关的几行加上训练过程中时不时刷新看两眼能省很多时间。我个人的经验是任何训练任务跑通代码的那一刻只是开始真正花时间的地方全在分析和调整上。做图像分类优化不需要一开始就把所有高级技巧拉满先把曲线看清楚再针对具体现象一一对症下药它的收益远大于盲目堆数据、堆参数量。
返回列表