PyTorch图像分类实战:六类经典CNN网络原理与对比

发布时间:2026/10/17 5:54:49
PyTorch图像分类实战:六类经典CNN网络原理与对比 图像分类是多数人接触深度学习时跨进的第一道门也是我把基本功打得最扎实的方向。这篇文章整理自一次完整的模型对比实践我把 PyTorch 下的 AlexNet、VGGNet、Inception、ResNet、DenseNet、MobileNet 六类经典网络放在同一套数据和训练流程里全部过了一遍从自己手写网络结构到调整训练策略最后沉淀出一套比较稳定的做法。文章没有停在“会用接口”的层面而是把每个结构的来龙去脉、选型逻辑、实现细节和实际踩坑都讲清楚。不论你是刚学会张量操作的新手还是已经能跑通几个模型、想在真实数据集上做出结果的工程师都能从里面找到可以直接落地的部分。1. 项目思路为什么用 PyTorch 把六类经典网络全部过一遍很多初学者会有一个误区图像分类就是调库torchvision里每个模型都封装好了直接resnet50()一把梭就行。这话对一半。真正做项目时模型选型从来不是“看谁最新用谁”也不是“看谁精度高就无脑上最重的”而是要理解每个网络的结构到底在解决什么问题然后根据你的数据规模、算力成本、部署环境做取舍。我把这六类网络放在一起对比就是因为它们分别代表了 CNN 发展史上的几个关键转折从暴力堆叠到小卷积核堆叠从单路径到多分支从绕路连接到直达连接最后再到高效计算。先说为什么选 PyTorch。生态是一方面更重要的是 PyTorch 的代码风格非常接近“手动推公式”的体验nn.Module的灵活度足够高方便我逐个模块去替换、打补丁、打印中间特征。相比固定模板式的框架PyTorch 更适合做这种横向对比实验。尤其是在自定义一些像 Inception 分支、残差块、深度可分离卷积这类结构时你能清楚看到每一步张量的形状变化这对理解模型非常有帮助。把六类网络放在同一套数据和流程里过一遍还有一个额外好处能摸清哪些结构差异真正影响了结果。我之前在模拟项目 X 里看到很多人用不同模型时数据增强、优化器、训练周期全都不一样最后比较出来的结论其实毫无意义。这次我统一固定了输入尺寸、数据增强策略、优化器、学习率调度、batch 大小和训练轮数只让网络结构作为变量这样得到的对比数据才具备参考价值。1.1 六类网络的选型核心差异在动手之前我先给自己做了一张选型表把每个网络的立身之本列出来网络核心思想参数量级适合场景AlexNet大卷积核 相对深的结构奠定了 CNN 基本范式约 60M教学、入门 baselineVGGNet用 3x3 小卷积核堆叠出足够深的网络约 138M迁移学习、结构规整的特征骨干Inception多分支并行提取多尺度特征1x1 卷积降维控制计算量约 24Mv3精度要求高、资源适中的场景ResNet残差连接解决深度网络的退化问题ResNet18 约 11.7MResNet50 约 25.6M通用首选、大规模分类DenseNet特征重用层与层之间密集连接DenseNet121 约 8M参数预算紧、需要做特征融合的项目MobileNet深度可分离卷积把参数和计算量压到很低MobileNetV2 约 3.4M移动端、边缘设备、端侧推理这里要特别注意一点参数量低不等于实际推理快。MobileNet 的核心是深度可分离卷积但深度卷积在部分底层加速库上不一定能跑出理论峰值有时候反而不如一个优化得很好的 3x3 卷积快。所以选型时一定要看你的目标平台不能只盯着参数字典。1.2 不是越新的网络就越好现在每年新模型一大把但经典模型依然有存在的必要。AlexNet 到今天已经远谈不上最优可它的设计几乎包含了现代 CNN 的所有基本元素卷积、池化、激活、Dropout、全连接分类。VGG 虽然重可是它的结构极度规整做迁移学习时在多个数据集上表现很稳。Inception 启发了大量关于多尺度特征的研究你会在很多检测和分割模型里看到它的影子。ResNet 更不用说残差连接几乎成了现代深度学习的基础设施连 Transformer 里都在用类似思想。DenseNet 和 MobileNet 则分别把“特征复用”和“轻量化设计”做到了极致。所以我的建议是经典不是过时而是沉淀。把它们吃透你才能在新网络出现时快速看懂它到底新在哪。这也是我为什么坚持要自己实现一遍而不是只调封装接口。2. 数据准备与训练环境先跑通一个公平的对比底座模型对比这件事最大的坑不是模型本身而是数据处理不一致。这次我用的是自己整理的一套食材分类数据集28 个类别约 9 万张图片。每张图原始分辨率在 300 到 800 像素之间训练集、验证集、测试集按 8:1:1 划分。类别数量没有做严格的完全均衡但整体分布比较均匀足够观察模型在真实分布下的表现。数据质量检查是开工前最重要的一步。我会先抽一批图出来人工看一遍重点确认三件事图片有没有翻转方向错误的、有没有加载出来是损坏的、标签和文件名对不对。这个步骤看起来简单但能避免后面训练到一半才发现数据乱了白跑好几个小时。2.1 数据增强与预处理细节图像分类的预处理基本围绕尺寸归一化、数值归一化和数据增强来展开。我在训练阶段使用了随机裁剪、随机水平翻转和颜色抖动验证和测试阶段只做 resize 和中心裁剪保证指标是可比的。具体改动用transforms.Compose组合起来from torchvision import transforms transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.65, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) transform_eval transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomResizedCrop的 scale 范围不要直接复制别人代码最好根据你的数据情况调。如果目标物体在图片里通常比较大scale 可以设到 0.7 以上如果物体经常很小那就要把下界放宽让模型有机会看到小目标。Normalize 用到的均值和方差是常见预训练统计值如果你训练的数据分布和自然图像差异特别大可以自己在训练集上算一份新的均值和方差效果会更好。2.2 DataLoader 与数据读取效率数据读取这块新手最容易忽略的是num_workers和pin_memory。我把常用配置贴在下面from torch.utils.data import DataLoader train_loader DataLoader( train_set, batch_size64, shuffleTrue, num_workers8, pin_memoryTrue, drop_lastTrue, ) valid_loader DataLoader( valid_set, batch_size64, shuffleFalse, num_workers8, pin_memoryTrue, )num_workers不是越大越好。我有一次在只有 16G 内存的机器上把 worker 开到 16结果数据加载进程吃光了内存训练直接卡死。常规做法是先看 CPU 核心数再看内存余量一般设成 CPU 核心数的一半到三分之二比较稳。pin_memoryTrue能让 GPU 拷贝数据更快前提是你的机器确实在用 GPU 训练纯 CPU 环境开了也没意义。drop_lastTrue只在训练时用可以避免最后一个 batch 太小导致 BatchNorm 统计不稳定。2.3 先跑通一个最小闭环再放大刚开始做这个对比时我没有直接上 9 万张全量数据而是先取了其中 2% 的图片把整个训练、验证、测试流程跑通。这一步非常关键能快速暴露代码里的低级错误比如张量维度不匹配、模型输入尺寸写错、标签越界等等。小数据跑通后再加全量你会省下大量试错时间。3. AlexNet、VGGNet、Inception 的原理与实现拆解这三类网络代表了一条很清晰的演进路线从“用大卷积核直接看大范围”到“用小卷积核层层叠加感受野”再到“用多个分支并行提取不同尺度特征”。我实现的时候没有完全照搬原始论文里的每一个参数而是做了一点面向工程的调整比如用 AdaptiveAvgPool 兜底全连接层的输入尺寸这样代码在不同输入分辨率下不会一上来就炸。3.1 AlexNet暴力堆叠也能赢但更值得记住的是三个惯例AlexNet 的历史地位不用多说它让业界重新认识到深度 CNN 的能力。它的结构在今天看来很朴素前面几层用大卷积核快速缩小特征图中间用 ReLU 激活加速收敛最后接两层 4096 维的全连接层中间加 Dropout。我实现时参考了常见的简化版本import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes1000): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 64, kernel_size11, stride4, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), nn.Conv2d(64, 192, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), nn.Conv2d(192, 384, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(384, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), ) self.classifier nn.Sequential( nn.Dropout(0.5), nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) return self.classifier(x)如果你自己实现时输入尺寸不是标准的 224最后的256 * 6 * 6很可能对不上。最稳妥的办法是在self.features后面加一个nn.AdaptiveAvgPool2d((6, 6))然后固定全连接层输入为256 * 6 * 6这样网络对输入尺寸的宽容度会高很多。我在项目里就是这么改的。AlexNet 的参数量有 60M 左右大头全在最后三个全连接层。这说明一个问题早期 CNN 的设计思路还比较接近传统的多层感知机特征提取部分反而相对单薄。实践上AlexNet 现在主要用来跑通流程、练手它本身已经很难在大数据集上取得特别高的精度。3.2 VGGNet用 3x3 卷积堆出来的深度VGGNet 的思想一句话就能说明白与其用一个 5x5 或 7x7 的大卷积核不如堆叠多个 3x3 卷积。两个 3x3 卷积的感受野等于一个 5x5 卷积三个 3x3 卷积等于一个 7x7 卷积但参数量和非线性都更占优。这个“以小代大”的思路后来几乎成了 CNN 设计的基本共识。VGG 的结构极度规整就是若干个“连续 N 个 3x3 卷积 一个最大池化”的模块。我封装了一个构建函数def vgg_block(in_channels, out_channels, n_convs): layers [] for _ in range(n_convs): layers [ nn.Conv2d(in_channels, out_channels, kernel_size3, padding1), nn.ReLU(inplaceTrue), ] in_channels out_channels layers.append(nn.MaxPool2d(kernel_size2, stride2)) return nn.Sequential(*layers)使用的时候可以把vgg_block(3, 64, 2)、vgg_block(64, 128, 2)、vgg_block(128, 256, 3)这样一层层串起来后面再接全连接分类器。VGGNet 的缺点是重VGG16 的参数量超过 138M训练和推理都比较吃资源。但它的优点是结构规整做迁移学习时非常好用。我在另一个项目里用它做过预训练骨干下游任务只要把最后一层换掉就能很快收敛。如果你想复现一个比较轻的 VGG也可以把每个 block 里的卷积数减少做成 VGG11 或自定义变体。实际用 VGG 时还要注意一个点它特别依赖数据增强和 Dropout。因为参数量大在小数据集上很容易过拟合。我通常会在全连接层至少加一层 Dropout并且把 weight decay 设大一些。3.3 Inception多分支多尺度让网络自己选“该看什么”Inception 的思路和 VGG 完全不同。它不去追求“用更小卷积核堆得更深”而是把 1x1、3x3、5x5 的卷积和 3x3 的最大池化放在同一个模块里并行计算最后拼接输出。这样一来同一层的特征里既有小范围的细节也有大范围的上下文网络可以自己学着组合不同尺度的信息。同时Inception 大量使用 1x1 卷积先降维再去做 3x3 或 5x5 卷积大大控制了计算量。一个简化版 Inception 块可以这样实现class InceptionBlock(nn.Module): def __init__(self, in_c, c1, c2a, c2b, c3a, c3b, c4): super().__init__() self.branch1 nn.Conv2d(in_c, c1, kernel_size1) self.branch2 nn.Sequential( nn.Conv2d(in_c, c2a, kernel_size1), nn.ReLU(inplaceTrue), nn.Conv2d(c2a, c2b, kernel_size3, padding1), ) self.branch3 nn.Sequential( nn.Conv2d(in_c, c3a, kernel_size1), nn.ReLU(inplaceTrue), nn.Conv2d(c3a, c3b, kernel_size5, padding2), ) self.branch4 nn.Sequential( nn.MaxPool2d(kernel_size3, stride1, padding1), nn.Conv2d(in_c, c4, kernel_size1), ) def forward(self, x): out1 self.branch1(x) out2 self.branch2(x) out3 self.branch3(x) out4 self.branch4(x) return torch.cat([out1, out2, out3, out4], dim1)注意几个细节。第一分支里先做 1x1 卷积之后通常要接一个 ReLU再接后面的卷积这是 Inception 论文中的标准配置。第二分支 4 的池化必须设置 padding1保证输出尺寸和其他分支一致不然到了拼接维度就报错。第三每个分支输出的通道数不同也可以拼接只要空间尺寸相同就行。Inception 的问题是实现自由度太高一不小心就会写得很乱。如果你只是想做对比实验建议直接用框架里封装好的 InceptionV3 或者 InceptionV4不要自己从零搭完整网络因为真正要调的辅助分类器、标签平滑等设置很容易出错。自己实现一个 InceptionBlock 用于理解结构就够了。4. ResNet、DenseNet、MobileNet 的原理与实现拆解如果说前面三个网络解决的是“怎么提取更丰富的特征”那后面这三个网络解决的是两个更现实的问题怎么把网络做得更深以及怎么把网络做得更轻。ResNet 的答案是残差连接DenseNet 的答案是特征复用MobileNet 的答案是深度可分离卷积。这三个思路今天依然被大量使用。4.1 ResNet残差连接让梯度少走弯路ResNet 的出现是因为研究人员发现网络堆到一定深度后效果不升反降这种现象被称为退化问题。不是过拟合而是训练时深层网络的误差反而更大。ResNet 给出的方案很优雅与其让每一层直接学习目标映射 H(x)不如让网络学习残差 F(x) H(x) - x然后把输入 x 原样加到输出上。这样一来即使某一层学不到什么有价值的东西它至少可以学成恒等映射不会让结果变差。核心的 BasicBlock 代码并不复杂class BasicBlock(nn.Module): def __init__(self, in_planes, planes, stride1): super().__init__() self.conv1 nn.Conv2d(in_planes, planes, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(planes) self.conv2 nn.Conv2d(planes, planes, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(planes) if stride ! 1 or in_planes ! planes: self.shortcut nn.Sequential( nn.Conv2d(in_planes, planes, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(planes), ) else: self.shortcut nn.Identity() def forward(self, x): out nn.functional.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) return nn.functional.relu(out)实现时最容易错的是通道数不匹配。当 stride2 下采样时特征图尺寸减半shortcut 必须用 1x1 卷积做投影把通道数也对齐否则out self.shortcut(x)会直接报错。我经常看到新手只改卷积的 stride忘了改 shortcut导致维度对不上。ResNet 还有一个升级版 Bottleneck 结构用“1x1 降维 - 3x3 卷积 - 1x1 升维”的方式把计算量压低从而支持更深的层数。ResNet50 就是用它组成的。如果你的数据集不算太小ResNet50 是一个很稳妥的默认选择既能体现深度模型的优势又不至于像 VGG 那样慢到让人崩溃。4.2 DenseNet极致的特征复用DenseNet 的形态比 ResNet 更激进在同一个 DenseBlock 里每一层的输入是前面所有层输出的拼接。假设输入通道是 C0增长率是 k那么第 L 层的输入通道就是 C0 k*(L-1)。这个设计让每一层都能直接拿到前面所有层的特征实现了极致的特征复用参数效率非常高。DenseNet 的单元层实现也比较直接class _DenseLayer(nn.Module): def __init__(self, in_channels, growth_rate, bn_size4): super().__init__() self.norm1 nn.BatchNorm2d(in_channels) self.conv1 nn.Conv2d(in_channels, bn_size * growth_rate, kernel_size1, biasFalse) self.norm2 nn.BatchNorm2d(bn_size * growth_rate) self.conv2 nn.Conv2d(bn_size * growth_rate, growth_rate, kernel_size3, padding1, biasFalse) def forward(self, x): out self.conv2(nn.functional.relu( self.norm2(self.conv1(nn.functional.relu(self.norm1(x)))))) return torch.cat([x, out], dim1)注意是torch.cat不是加法。这也是 DenseNet 与 ResNet 最大的区别ResNet 是恒等映射相加DenseNet 是通道维度拼接。拼接带来一个问题随着层数增加通道数会越来越多所以 DenseBlock 之间必须插入 Transition 层用 1x1 卷积压缩通道再用平均池化降低分辨率否则显存和计算量都会爆炸。实际训练 DenseNet 时建议把 growth_rate 设小一点常见值是 12 或 24。我之前第一次用的时候设了 48结果参数量没多多少但显存占用涨得很快。在图像分类任务上DenseNet 的精度往往和 ResNet 相当但参数量更少这让我在特征提取类任务里更喜欢用它。4.3 MobileNet深度可分离卷积让模型跑进移动端MobileNet 的核心不是“更准”而是“更划算”。它把标准卷积拆成两步先对每个通道单独做空间卷积也就是 depthwise 卷积再用 1x1 卷积把各通道的信息融合也就是 pointwise 卷积。标准卷积的计算量是乘法深度可分离卷积把它变成了加法所以在精度损失可控的前提下参数和计算量都大幅下降。我给 MobileNetV1 做一个最简单但够用的模块class DepthwiseSeparableConv(nn.Module): def __init__(self, in_planes, out_planes, stride1): super().__init__() self.depthwise nn.Conv2d(in_planes, in_planes, kernel_size3, stridestride, padding1, groupsin_planes, biasFalse) self.bn1 nn.BatchNorm2d(in_planes) self.pointwise nn.Conv2d(in_planes, out_planes, kernel_size1, biasFalse) self.bn2 nn.BatchNorm2d(out_planes) def forward(self, x): out nn.functional.relu(self.bn1(self.depthwise(x))) out nn.functional.relu(self.bn2(self.pointwise(out))) return out关键在groupsin_planes这告诉 PyTorch 这个卷积不是跨通道的而是每个通道独立处理一张特征图。如果漏掉这个参数它就退化成一个普通卷积整个 MobileNet 的轻量化优势全部归零。MobileNetV2 进一步改进了设计提出倒残差结构和线性瓶颈。所谓倒残差就是先 1x1 扩张通道再做深度卷积最后 1x1 降维回去线性瓶颈则是去掉最后一个 1x1 卷积后面的 ReLU避免在低维空间里丢失信息。这个结构后续成为很多移动端网络的基础模块。工程上如果你要部署到手机或者边缘设备MobileNetV2 几乎是绕不开的选择但要注意它和 ResNet 这类大模型的精度差距在很细粒度的分类任务上会明显放大。5. 训练流程与调参经验让每个模型发挥出应有水平网络结构只是骨架真正决定最终精度的是训练策略。我在这套对比实验里统一了所有模型的训练设置但并不意味着每个模型的最佳设置都一样。下面这套流程是我反复验证过的算是一个比较通用的起点。5.1 优化器与学习率调度图像分类任务里SGD 加动量是我默认的优化器选择。它虽然看起来没有 Adam 那么“聪明”但在 CNN 分类场景下收敛更稳定泛化性也更好。初始化动量设 0.9权重衰减设 1e-4初始学习率根据 batch size 调整。我的经验是 batch size 64 时学习率从 0.01 到 0.05 之间试探先用一小段数据跑几十个 iteration观察 loss 是升还是降再确定。学习率调度我强烈推荐 warmup 加余弦退火的组合。一开始用很小的学习率跑几个 epoch让模型权重从随机状态平稳过渡之后再用余弦退火慢慢降低学习率在训练后期不断细化。用 PyTorch 实现起来很简洁from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR, SequentialLR warmup LinearLR(optimizer, start_factor0.1, total_iters5) cosine CosineAnnealingLR(optimizer, T_maxepochs - 5, eta_min1e-5) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[5])如果不用 SequentialLR也可以手动在两个调度器之间切换但 API 在这种场景下确实省事。需要提醒的是学习率调度器更新的时机有两种每个 epoch 更新一次和每个 step 更新一次。上面这种调法适用于每个 batch 更新一次也就是把scheduler.step()放在每个 batch 结束之后。如果你的代码习惯是每个 epoch 更新那 CosineAnnealingLR 的 T_max 直接设为整体 epoch 数即可两者不要混。5.2 正则化、标签平滑与混合精度过拟合是图像分类在中小规模数据集上的头号问题。为了防止每个模型在自己的强项上跑分虚高我统一启用了重量级数据增强同时加了标签平滑。标签平滑的作用是让模型不要对训练集的标签过于自信减少过拟合尤其是数据里有标注噪声时非常有效。PyTorch 的CrossEntropyLoss在较新版本里已经支持label_smoothing参数直接设成 0.1 就行。混合精度训练现在几乎是标配尤其是跑 Inception、VGG 这种稍重的模型时收益非常明显。我在训练循环里加了自动混合精度代码片段如下scaler torch.amp.GradScaler() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with torch.autocast(device_typecuda, dtypetorch.float16): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()注意混合精度并不是单纯的“快”它还能降低显存占用。但在某些老版本 PyTorch 里如果模型里存在自定义的数值不稳定的算子要小心数值溢出。这时可以跳过 autocast 的层或者先把模型切成 float32 看看是否有精度差异。5.3 训练循环与模型保存完整的训练循环看起来很简单但有几个细节容易被忽略。第一个是model.train()和model.eval()的切换尤其在 BatchNorm 和 Dropout 存在时忘记切换会导致验证集结果忽高忽低。第二个是验证阶段要包在torch.no_grad()里否则模型参数会被自动求导记录消耗额外显存并拖慢速度。第三个是模型保存不要只看最后一个 epoch要按验证集最优结果保存。训练循环主体是这样的best_acc 0.0 for epoch in range(epochs): model.train() train_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) with torch.autocast(device_typecuda, dtypetorch.float16): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad() scheduler.step() train_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in valid_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted outputs.max(1) val_total labels.size(0) val_correct predicted.eq(labels).sum().item() val_acc 100.0 * val_correct / val_total if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)我自己在项目里记录指标时除了 Top-1 还会额外记录 Top-5。很多看起来效果一般的模型Top-5 其实表现很好这说明它的排序能力不差只是第一名的置信度没有压准。如果你做的是检索或者推荐Top-5 往往比 Top-1 更有参考价值。把六个模型都跑完后我在模拟项目 X 上的对比结果大致如下仅供趋势参考AlexNet 约 76%VGG16 约 82%InceptionV3 约 84%ResNet50 约 85%DenseNet121 约 84%MobileNetV2 约 83%。可以明显看到在同样的训练周期内ResNet 和 DenseNet 这类带连接结构的模型收敛得更快最终精度也更好。VGG 虽然参数最多但它的优势在迁移学习里才更容易体现直接在中小数据集上从头训练性价比不高。6. 踩坑记录精度、显存与复现问题速查模型对比做多了遇到的问题也越来越相似。我把这次实践里的典型问题整理成了一张速查表后面单独提几个值得展开的细节。问题常见原因排查方向训练 loss 不降学习率过大或过小、数据没有 shuffle、标签和图像错位先小数据试跑检查 loss 初始值打印前几个 batch 的标签分布验证集精度很低但训练集很高过拟合、数据增强不足、Dropout/weight decay 没配好增加增强强度调大 weight decay检查模型是否太大显存不足batch size 太大、输入分辨率太高、特征图堆积减小 batch启用梯度累积使用混合精度检查是否有持久化的中间张量训练很慢DataLoader 卡、worker 太少、模型里大量小算子检查 CPU/GPU 利用率适当增加 num_workers优先用大卷积算子加载预训练权重报尺寸不匹配分类层通道数不同、输入通道数不同先加载除了最后一层外的权重再替换分类层训练结果复现不一致缺少随机种子、数据加载顺序不稳定、增强随机性固定全套随机种子统一 worker 数量必要时关闭数据 shuffle6.1 梯度爆炸和初始 loss 异常ResNet 和 DenseNet 这类有大量 BatchNorm 的网络通常不太会遇到梯度爆炸但 AlexNet 和 VGG 如果学习率起始值设太大第一个 epoch 的 loss 就可能变成 NaN。我和同事排查过几次最后发现一半以上都是学习率的问题。解决方法是先固定随机种子把学习率调到 1e-3 级别试跑几十个 batch如果 loss 能稳定下降再加回去。另外如果你的图片没有做归一化或者 Normalize 的通道顺序不对模型的初始 loss 也会异常偏高。比如你用的是 RGB 数据但预处理写成了 BGR 顺序模型等于在看一张颜色错乱的图训练自然很吃力。这种问题日志里不一定会直接报错但 Top-1 会一直卡在很低的位置。6.2 过拟合在中小数据集上的典型表现中小数据集上最典型的过拟合信号是训练 Top-1 快速冲到 99%但验证集一直上不去。我在跑 AlexNet 时感受最明显因为它全连接层太多。解决顺序建议是先加强数据增强再调大 weight decay然后尝试 Dropout最后才是减小模型。顺序不要颠倒因为数据增强是最不伤已有结构的方案。有一个很多人不知道的小技巧Dropout 位置不同效果差别很大。放在全连接层前面和放在卷积特征图之间作用并不一样。对于 VGG 这种网络我通常在全连接层加 0.5 的 Dropout但不会在卷积层加否则会损失太多空间信息。6.3 显存不足的工程解法显存不够时第一反应不要是换更小的模型而是先看有没有写得不讲究的代码。常见浪费包括在训练循环外保留整个 dataset 的归一化中间结果、验证阶段忘记包torch.no_grad()、用太多小 batch 但没开梯度累积。把这些问题清理干净再去考虑模型层面。如果还是不够就用梯度累积模拟更大的 batch。下面是常用的写法accumulation_steps 4 for step, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad() scaler.update()注意把 loss 除以 accumulation_steps保证最终梯度量级和直接使用大 batch 一致。还有一个细节BatchNorm 的统计量在梯度累积时会受影响所以如果你真的对精度要求很高尽量用真实 batch size 而不是过分依赖这个方法。6.4 结果复现对不上的问题在对比多个模型时最烦的就是昨天跑出的结果今天复现不出来。后来我养成了一个习惯在代码开头把所有随机种子固定包括 Python、NumPy 和 PyTorch 的种子同时把 DataLoader 的 worker 数量固定。即使这样在 GPU 上由于底层算子的非确定性多次运行结果仍有微小差异这个属于正常范围大家不用过于焦虑。def set_seed(seed42): random.seed(seed) os.environ[PYTHONHASHSEED] str(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)如果希望完全可复现可以把torch.backends.cudnn.deterministic和torch.backends.cudnn.benchmark也设置好但这时训练速度会慢一点。做对比实验时我建议固定 benchmark 为 False 或者 True总之不要每次运行都不一样。7. 模型选型与实操建议把六个模型全部过完之后我对选型的判断更清晰了。如果是第一次接触图像分类我建议用 ResNet 入门尤其是 ResNet18 和 ResNet50。它结构清晰、收敛稳定、代码实现难度低能让你把注意力集中在数据流程和训练策略上。如果目标是部署到移动端那就在 MobileNetV2 和 MobileNetV3 之间选先把深度可分离卷积的原理吃透再说精度。如果手里数据集不大但又想拿一个高精度特征提取器DenseNet 的性价比很高参数利用率比同级别的 ResNet 更出色。Inception 和 VGG 不是没用只是用它们的场景更垂直。VGG 适合当迁移学习的骨干因为它各个层级的语义特征非常规整很多风格迁移和特征可视化任务到今天还在用它。Inception 适合做高精度分类特别是你的输入图片本身尺度差异很大的时候它的多分支结构能天然适应而不需要你手动调整输入分辨率。实际项目里我一般不会只选一个模型而是先让 ResNet 跑通 baseline再用 DenseNet 或 MobileNet 做对比最后看目标设备的限制来定。毕竟模型选型不是一场考试没有一个标准答案。在做这类项目时我最后还会用torchinfo打印一次模型摘要确认每一层的输出形状和参数量是否符合预期。这个小动作能帮你尽早发现代码里的维度问题比出了 NaN 再回头查要高效得多。你如果正准备开始图像分类不用追求一步到位先跑通一个小规模闭环再逐步把所有经典模型都过一遍收获会比只调库大得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询