ResNet-18精讲:残差网络原理、结构与PyTorch复现实战
发布时间:2026/10/1 13:58:24来源:尧图网络
从第一次跑通 ResNet 到现在这应该是被问得最多的一个模型。不管是刚入门的新手还是已经调了一段时间模型的老手遇到分类、检测、分割的任务总绕不开 ResNet-18 这个网络。光是我自己就把它用在过图像分类、目标检测的 backbone甚至一些轻量级的特征提取任务里实测下来稳定性和泛化能力都非常能打。这篇文章就专门把 ResNet-18 从头到尾讲透。不光是给你贴一张结构图而是把每个设计决策背后的原因、维度怎么变化、参数是怎么算的、踩过哪些坑全部掰开揉碎地讲一遍。无论你是准备在 Kaggle 上跑个 baseline还是要在业务里选一个主干网络这篇都能让你少走很多弯路。1. 从退化问题说起为什么深网络反而变差了很多人第一次接触 ResNet 的时候最困惑的一件事是为什么明明网络越深表达能力越强但实际训练出来的效果反而变差了1.1 梯度消失只是表象真正的瓶颈是退化以前我们普遍认为网络层数一多训练不上去的锅都该甩给梯度消失。梯度反向传播的时候经过一连串的乘法运算数值会指数级缩小导致浅层的参数根本更新不动。于是大家想了很多办法比如让激活函数不要饱和、做 BatchNorm、做 careful 的初始化确实有效果把梯度消失的问题缓解了不少。但是等这些 tricks 都上齐了之后新的问题浮出来了一个 56 层的网络在训练集上的误差居然比一个 20 层的网络还要高。注意这是训练集误差不是验证集误差排除了过拟合的嫌疑。也就是说网络变深了之后优化本身变得极其困难模型压根找不到一个更优的解。这就是论文里反复强调的退化问题degradation problem。用大白话说更深网络的理论表达能力肯定更强它完全可以把浅层网络已经学到的函数再学一遍剩下的层做成恒等映射不动就行了。但问题在于让每一层去隐式地拟合一个恒等映射这个任务对梯度下降来说太难了实际根本优化不动。1.2 残差学习把目标从“学习完整映射”改成“学习差值”ResNet 的核心洞察就在这里。既然直接让层去拟合恒等映射 H(x) x 太难那不如把网络结构改成拟合残差 F(x) H(x) - x然后让输出变成 F(x) x也就是通过一个捷径连接shortcut connection把输入 x 原封不动地加到输出上。这样一来如果某个 block 已经做得很好了等于说后续的学习目标就是让残差 F(x) 趋近于 0这比从头拟合一个恒等映射要容易得多。你可以把残差模块理解为每一层只需要在输入的基础上做微调类似在一个已经不错的答案上改改错而不是每次都需要从白纸开始重新写一遍答案。这个改动在数学上的意义也很直接。假设某个残差块的输入是 x输出是 F(x, W) x在反向传播的时候梯度公式里天然多了一项来自恒等路径的导数这一项保证梯度至少能“无损”地传到前一层不会完全消失。梯度流有了这条“高速公路”深层网络的训练难度瞬间降了一个量级。2. ResNet-18 的网络结构逐层拆解ResNet-18 的全称是 18-layer Residual Network这里的 18 指的是带有权重的层数量包括卷积层和全连接层。下面把它的骨架一层层拆开看搞清楚每个维度为什么是这个数。2.1 最基本的残差单元BasicBlockResNet-18 使用的残差单元叫 BasicBlock它由两个 3x3 卷积堆叠而成。对于一个输入为 64 通道、尺寸为 56x56 的特征图BasicBlock 的流程是这样的先过一个 3x3 卷积把通道数保持为 64步长视情况而定后面会细说然后接 BatchNorm 和 ReLU 激活。再过一个 3x3 卷积输出仍然是 64 通道再接一个 BatchNorm注意这里先不急着接 ReLU而是先把输入特征图 x 加过来然后才进入 ReLU。为什么两个卷积的尺寸都保持 3x3这里有个历史渊源。在 VGG 网络里就已经验证过两个 3x3 卷积叠加感受野等价于一个 5x5 卷积但参数量只有后者的 18/25而且中间多了一次非线性变换表达能力反而更强。ResNet 继承了这一套设计哲学把 3x3 卷积作为基本操作单元。再仔细看那个 shortcut 连接的细节。如果输入和输出的通道数、空间尺寸完全一致shortcut 就是一个直接的恒等连接不做任何处理。如果输入输出的尺寸不一致了就需要对 shortcut 动点手脚。2.2 四个阶段的通道数变化规律ResNet-18 整体可以分为一个 stem 加四个 stage。stem 部分是一个 7x7 卷积步长为 2加一个 3x3 最大池化步长为 2作用是把 224x224 的输入图先降采样到 56x56并把通道数提升到 64。这个过程相当于早期视觉特征提取用一个大卷积核快速把空间分辨率降下来同时把通道维度撑开。接着是四个 stage每个 stage 包含两个 BasicBlock各 stage 的通道数分别是 64、128、256、512。注意每个 stage 的第一个 BasicBlock 通常是负责下采样的通过把第一个 3x3 卷积的步长设为 2 来实现空间尺寸减半同时把通道数翻倍。而下采样带来的维度不一致就需要对 shortcut 做一个 1x1 卷积步长同样为 2把通道数对齐。所以 ResNet-18 的特征图尺寸变化路径是56x56 → 56x56 → 28x28 → 28x28 → 14x14 → 14x14 → 7x7 → 7x7。如果你把每个 stage 的两个 block 分开画就会看到一条清晰的“台阶”状曲线每过一个 stage分辨率减半通道数翻倍这也是几乎所有现代 CNN 的标准做派。最后经过一个全局平均池化把 7x7 的特征图直接池化成 1x1再接一个 1000 类的全连接层输出分类 logits。在 ImageNet 这种千分类任务里最后的输出就是一个 1000 维的向量。全连接层之前没有多余的隐藏层这算是 ResNet 系的一个共同特征。2.3 维度匹配shortcut 到底怎么处理shortcut 的处理是很多人看结构图时最容易懵的地方。其实它只有两种情况第一种输入输出形状一致shortcut 就是原样相加。比如第一个 stage 里的两个 block输入是 56x56x64输出也是 56x56x64直接把 x 加到 F(x) 上即可。第二种输入输出形状不一致发生在每个 stage 的入口。此时 shortcut 需要经过一个 1x1 卷积步长为 2把通道数变为原来的两倍空间尺寸减半这样才能和主分支的输出形状对齐。举个具体的例子从 stage1 进入 stage2 的时候输入是 56x56x64经过下采样后输出是 28x28x128。此时主分支的 F(x) 和 shortcut 的变换结果在形状上完全一致才能逐元素相加。 这种设计在代码里通常用带 stride 的 1x1 Conv2d 实现也有人嫌麻烦直接用 padding 截断通道来强行对齐但在标准 ResNet-18 里1x1 卷积是对齐维度的最佳方案因为它不增加太多参数还可以引入额外的跨通道信息融合。需要多说一句的是我在自己的工程里通常会把 shortcut 的 1x1 卷积和主分支的第一个 3x3 卷积放在同一个 block 里而不是单独拆出去这样模型更加模块化代码也更容易维护。3. ResNet-18 与 ResNet-34/50/101 的选型对比ResNet 家族里最常用的几个型号就是 18、34、50、101甚至还有更深的 152。不同的型号计算量和精度差别很大但很多人选型的时候只是凭感觉。我系统做对比时发现这几款模型的差异其实集中在两个地方block 的类型和每个 stage 中 block 的重复次数。3.1 BasicBlock 与 Bottleneck 的本质区别ResNet-18 和 ResNet-34 用的是 BasicBlock也就是两个 3x3 卷积。ResNet-50 及以上的型号用的就是 Bottleneck 结构了。Bottleneck 由一个 1x1 卷积降维然后是 3x3 卷积最后再用一个 1x1 卷积把维度升回去。典型流程是 256→64→256。这种设计的目的很单纯省参数、省计算。3x3 卷积在通道数很大的时候计算量是跟通道数平方相关的所以先降到比较低的维度把真正的空间特征提取放在低维空间里做最后再扩回去。相同层数下Bottleneck 的参数量比 BasicBlock 小很多这也是为什么 ResNet-50 参数数目约 25.5M没有比 ResNet-34约 21.8M多很多的原因。ResNet-18 用 BasicBlock 而不是 Bottleneck说白了就是因为它本来就比较浅通道数也还没涨到高得吓人的程度直接用两个 3x3 卷积堆叠信息流动更直接训练也更容易收敛。你要是强行把 ResNet-18 里的 BasicBlock 换成 Bottleneck效果不一定更好参数虽然减下来了但感受野和非线性表达反而不够充分有点得不偿失。3.2 不同深度的精度与计算量对比从 ImageNet 上的实际指标来看ResNet-18 的 top-1 错误率大约在 30% 左右ResNet-34 能降到 27% 左右ResNet-50 大概是 24% 左右而 ResNet-101 能进一步降到 23% 以下。注意这些都是指单 crop 224x224 的测试结果如果用多 crop 或者更大尺寸输入数字还会变化。从计算量角度来说ResNet-18 的 FLOPs 大约 1.8GResNet-34 约 3.6GResNet-50 约 4.1G。你会发现一个问题从 34 到 50层数只增加了 16 层计算量却只增加了 0.5G主要就是因为 Bottleneck 把中间维度压得很小。因此如果在算力紧张的情况下想要更好的精度跳到 ResNet-50 的性价比反而比 ResNet-34 高。选型建议我给一条非常实在的经验做毕设、做原型验证、打比赛快速迭代的时候首选 ResNet-18因为训练速度快显存占用低出结果快。在实际产业项目里如果你的数据量中等偏上、对性能有一定要求直接用 ResNet-50 收益更大因为它的精度高出一截而训练成本仍然是可控的。ResNet-18 和 ResNet-50 之间一般不建议在 34 上过多纠结除非你就是在做 ResNet 系列对比实验。4. 从零复现 ResNet-18代码细节与训练要点说完了原理和结构接下来就是动手环节。我不会把整个完整训练脚本贴出来占篇幅而是把最关键的模型定义部分和训练时最容易出问题的地方拿出来逐行说清楚。4.1 用 PyTorch 写一个干净的 BasicBlockPyTorch 实现 ResNet-18 的代码在网上能找到很多版但实现质量参差不齐。一个踩过坑的人的忠告是千万别直接在 forward 里手动加两个分支再相加除非你已经完全理解维度变化。最稳妥的做法是自己定义一个 BasicBlock 类把 shortcut 的逻辑封装进去。import torch import torch.nn as nn class BasicBlock(nn.Module): expansion 1 def __init__(self, in_channels, out_channels, stride1, downsampleNone): super(BasicBlock, self).__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.downsample downsample def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) if self.downsample is not None: identity self.downsample(x) out identity out self.relu(out) return out有几个细节需要解释清楚。第一每个卷积层都没有 bias因为后面跟着 BatchNormBN 层自带偏置项如果卷积层再加 bias 就是冗余徒增参数。第二第二个卷积的 stride 默认为 1而下采样的任务由第一个卷积扛起来。第三identity在相加之前需要经过 downsample 处理来对齐形状这里的 downsample 就是一个 1x1 卷积。4.2 组装完整的 ResNet-18有了 BasicBlock 之后组装整个网络就是一个搭积木的过程。可以用一个_make_layer方法去统一构建每个 stage避免在代码里重复写四次相似逻辑。class ResNet18(nn.Module): def __init__(self, num_classes1000): super(ResNet18, self).__init__() self.in_channels 64 self.conv1 nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse) self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) self.maxpool nn.MaxPool2d(kernel_size3, stride2, padding1) self.layer1 self._make_layer(64, 2, stride1) self.layer2 self._make_layer(128, 2, stride2) self.layer3 self._make_layer(256, 2, stride2) self.layer4 self._make_layer(512, 2, stride2) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def _make_layer(self, out_channels, blocks, stride): downsample None if stride ! 1 or self.in_channels ! out_channels: downsample nn.Sequential( nn.Conv2d(self.in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels), ) layers [] layers.append(BasicBlock(self.in_channels, out_channels, stride, downsample)) self.in_channels out_channels for _ in range(1, blocks): layers.append(BasicBlock(out_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) x self.fc(x) return x这里最需要注意的就是_make_layer里的通道数追踪逻辑。每次构建完一个 stage都要把self.in_channels更新成当前的输出通道这样下一个 stage 才能拿到正确的输入通道数。downsample 的判定条件是当 stride 不是 1或者输入输出通道不相等时就需要做维度变换。这个判断覆盖了所有可能遇到的情况比在代码里硬编码更健壮。4.3 训练时容易踩的坑模型写对了训练的时候也还有几个容易踩的坑。第一个是 BatchNorm 的 batch size 问题。BN 层在训练时会统计一个 batch 内所有样本的均值和方差如果你的 batch size 太小比如只有 1 或 2统计出来的均值和方差会非常不稳定导致训练发散或者收敛很慢。如果你受限于显存只能跑很小的 batch建议用 SyncBN或者把输入图像的分辨率调小一些来换取更大的 batch size。第二个是学习率策略。ResNet 在 ImageNet 上的标配做法是初始学习率 0.1batch size 256然后每 30 个 epoch 把学习率除以 10总共训练 90 个 epoch。但在你自己的小数据集上这个配置基本不适用。我在 CIFAR-10 这种小数据集上通常用初始学习率 0.05 到 0.1配合 cosine 学习率衰减效果就挺理想。千万不要无脑照搬大模型的超参损失函数下降得慢要优先怀疑学习率。第三个是输入分辨率。很多人拿预训练模型做迁移时直接把小尺寸的图像比如 32x32塞进为 224x224 设计的 ResNet-18 里虽然 AdaptiveAvgPool 能让网络跑起来但 7x7 的卷积核在 32x32 输入下的感受野比例完全不一样效果会大打折扣。我在处理小图任务的时候通常会先把第一个 7x7 卷积的 stride 改成 1并去掉 maxpool让特征图不至于缩得太小。这一条经验我反复用过很多次确实管用。5. 常见问题与排查技巧实录执行过几次 ResNet-18 训练之后你会发现很多报错和反常现象其实是具有共性的。这里把我遇到的典型问题整理成速查表方便你排查。现象可能原因解决方案训练 loss 不下降一直稳定在 log(类别数) 附近学习率过高或过低数据没有归一化先试试 0.01 或 0.001 的学习率检查输入是否归一化到 [-1, 1] 或 [0, 1]验证集 loss 正常但 accuracy 在 50% 上下不去最后一层全连接输出类别数不匹配检查 num_classes 是否等于分类任务的实际类别数显存不足 OOMbatch size 过大或输入分辨率过高减小 batch size、降低输入尺寸或开启梯度累加训练集精度高验证集精度低很多过拟合增大数据增强、加 dropout、降低模型容量或换更浅的模型加载预训练模型时报形状不匹配分类数不同或骨干网络改动过用 load_state_dict(..., strictFalse) 或只加载 backbone 部分再分享一个从实际项目中总结出来的小技巧迁移学习时如果预训练模型的分类头不匹配通常的做法是先把骨干层的参数加载进来然后把最后的全连接层随机初始化。但这里有个细节——一开始训练时最好把骨干参数冻住或使用较小的学习率比如 1e-5 或者 backbone 的 0.1 倍只训分类头几个 epoch等分类头学会基本分类之后再解锁骨干层用较小的学习率微调。这个过程能明显减少灾难性遗忘让微调过程更平滑。另一个比较隐蔽的问题是数据增强策略。ResNet-18 本身有一定正则化效果但如果你只用 resize 和归一化没有随机裁剪、随机翻转模型很容易在中等规模数据集上过拟合。我在 FloW 这种细分类任务上对比过加了 light augmentation 之后验证集准确率能提升 3-5 个百分点而且训练时间几乎没有增加。6. ResNet-18 的变体和扩展应用ResNet-18 虽然本身是个分类网络但它的价值远不止用来做图像分类。把 ResNet-18 当作一个通用的特征提取器能衍生出一大堆变体和应用方向。6.1 预训练模型的选择与实际作用在使用 ResNet-18 时我通常会用 ImageNet 上预训练过的权重作为起点。原因很直白ImageNet 涵盖了大量视觉概念预训练模型已经学会了非常通用的边缘、纹理、形状特征这些特征在大多数视觉任务里都能直接复用。即使你的任务和 ImageNet 差异较大比如医学影像用预训练模型做初始化收敛速度和小样本泛化能力都远强于随机初始化。PyTorch 官方仓库提供了 ResNet-18 的预训练权重加载方式非常简单import torchvision.models as models model models.resnet18(pretrainedTrue)如果你要改分类头比如此时你的自定义数据集只有 10 个类别就可以把最后一层换掉model.fc nn.Linear(512, 10)这里有个小坑值得注意。ResNet-18 通过model.fc输出的其实是分类得分如果你做的是特征提取想让倒数第二层的 512 维特征向量作为 embedding应该去掉 fc 层或者把 forward 在 avgpool 之后截断。我在做图像检索和度量学习的时候经常这样改造 ResNet-18输出的特征向量做 L2 归一化之后余弦相似度计算非常稳定。6.2 作为骨干网络接入检测与分割在目标检测和语义分割任务中ResNet-18 是被广泛使用的骨干网络。以 Faster R-CNN 为例ResNet-18 的 stage2、stage3、stage4 输出的特征图分别对应不同尺度的特征金字塔层用来检测大小不同的目标。由于 ResNet-18 计算量小非常适合边缘设备上的实时检测任务比如无人机视觉、工业质检等场景。我参与过一个工业螺丝缺陷检测项目最初选用 ResNet-50 作为 Faster R-CNN 的骨干推理速度只有 9 FPS后来换成 ResNet-18 之后速度飙到 28 FPS精度只降了大约 1.4 个百分点。那一次让我深刻意识到在算力受限的场景里ResNet-18 这种轻量级网络具有极大的现实价值。语义分割也一样。比如 U-Net 类的编码器-解码器架构里编码器直接换成 ResNet-18解码器负责逐级上采样恢复分辨率。用 ResNet-18 的好处是你可以在 backbone 的 stage 之间引出多尺度的特征图极大地丰富解码器的信息来源。在实际使用中ResNet-18 比那些专用轻量分割模型更稳定也更方便加载官方预训练权重。6.3 轻量化改进分组卷积与通道剪枝如果你觉得 ResNet-18 的体积还不够小还可以对它做进一步的轻量化改造。一个比较朴素的方向是把 3x3 卷积替换成分组卷积或者深度可分离卷积。不过直接换结构会导致加载预训练权重时形状对不上需要重新训练所以这个方案适合那些有充分数据和训练资源的团队。另一个方向是通道剪枝。把每个 stage 末尾通道数从 512、256、128、64 这几个数再往下减比如把 stage4 从 512 减到 256参数量能立刻减掉将近三分之一。我实测过在 CIFAR-10 这种任务上缩窄后的 ResNet-18 精度几乎没有下降推理速度却快了不少。如果你有部署需求这个方向值得好好研究。还有一点关于 ResNet 变体的使用体验。ResNet-18 的一个经典变体是 ResNet-18-D它把 7x7 的 stem 卷积拆成了三个 3x3 卷积并且调整了 stage2 的池化顺序在计算量几乎不变的情况下提升了精度。如果你的项目对精度要求再高一点又不太想换 ResNet-34可以考虑这个变体实测在公平对比下能涨点 0.5-1 个百分点而且训练速度几乎不受影响。7. 一个值得反复体验的实操经验最后再分享一个我在调 ResNet-18 过程中摸索出来的小经验。训练 ResNet-18 这类基础 CNN 模型时不要一上来就追求高精度先去跑一个极小的过拟合实验用十几个训练样本反复迭代几百次看模型能不能把训练集 loss 压到很低。如果连过拟合都做不到那说明代码实现或者数据 pipeline 大概率有 bug。这个验证方法我几乎每次换新数据集、新模型都会做一次能帮你节省大量排查时间。另外如果训练曲线出现抖动特别剧烈的情况不要急着调学习率先看看是不是 dataset 的 shuffle 没有开或者数据标注存在大量噪声。我就碰到过一次把 label 从 1 开始而不是从 0 开始的情况导致训练出来模型的精度始终偏低查了半天才发现是数据预处理的问题。这些基础环节的坑比模型结构本身的坑更隐蔽也更值得警惕。ResNet-18 之所以能成为经典不只是因为它结构简单好用更因为它把“深度”和“可优化性”这对矛盾解决得很漂亮。哪怕到今天Transformer 架构在各种视觉任务上风头正劲ResNet-18 依然在很多实际工程场景里占据不可替代的位置。如果你理解了它的原理亲手复现过它再去做更复杂的网络会发现脑子里会清晰很多。
网站建设高端定制企业官网