文献精读,第六篇,轮到ResNet了。这篇论文我想单独拿出来写,不只是因为它拿了当年的ImageNet冠军,而是因为这几年我落地过分类、检测、分割项目,翻来覆去用的backbone几乎都绕不开它。框架换来换去,但残差连接这个概念,真的像地基一样铺在几乎所有现代卷积网络下面。如果你也经常看论文,你会发现ResNet的影响已经远远超过它自己那几层卷积,它改写了深度网络的组装方式。
这篇笔记,我不会把论文从头到尾翻译一遍,而是基于我对这篇文献的理解和复现经验,讲清楚三件事:第一,ResNet到底解决了一个什么问题,而且是其他方法解决不了的问题;第二,残差块内部的细节为什么长成这样,每个设计背后都有什么考虑;第三,你真正动手去训练一个ResNet时,会遇到哪些论文里没写但实打实存在的坑。
适合谁看呢?如果你是刚读完基础CNN结构、准备认真精读经典论文的入门者,这篇能帮你把主线理顺;如果你已经跑过不少模型,但总觉得ResNet的代码和论文对不上,这篇也能帮你踩掉几个常见的坑。我尽量讲得直白,复杂的部分用计算和类比拆开说。
1. 这篇文献到底解决了什么问题
1.1 深度网络遇到的两个坎:过拟合与退化
在ResNet之前,大家普遍认为网络越深,表达能力越强,所以研究趋势就是疯狂往深了堆。但实际问题远没有这么简单。模型能力上去之后,第一个坎是过拟合,这个大家认识一致:训练集上表现很好,测试集上拉胯。所以早年的办法是加正则、加大数据集、加Dropout。
但第二个坎,当时很多人没意识到,就是退化问题。举个最简单的现象:你用一个20层的神经网络和一个56层的神经网络做同一个分类任务,训练迭代足够的轮数,20层的训练误差可能更低,56层的训练误差反而更高。这不是过拟合造成的,因为在训练集上就已经输了。
这就很反直觉了:层数多了,理论上表达能力只会更强,至少应该不比浅层差。如果层数多而效果差,那说明网络在训练过程中出了问题,特征没能被有效传递。文献里把这个叫退化现象。注意,它不是梯度消失那么简单,因为在某些阶段梯度并没有消失,但很深层的网络仍然很难被优化。
1.2 残差学习的核心思路——从“直接学映射”到“学残差”
作者团队换了一个视角来思考这个问题。他们说,与其让卷积层直接拟合一个目标映射H(x),不如把目标拆成两个部分:一个是短接过来的恒等映射x,另一个是需要学习的残差F(x)。最终输出是F(x)加x。这样做的直接好处是,如果某个模块已经足够好,理想的F(x)应该趋近于0,这时候网络就可以退化成恒等映射,至少不会比浅层网络差。
我用生活里的例子类比一下。你想把一个箱子推上台阶,整段路径很难一步到位。但残差学习的意思是,你已经站在了某一级台阶上,下一步只需要关心怎么再抬高一点点。这个“一点点”就是残差。它把学习目标从高难度的完整映射,拆解成了相对容易的小步逼近。
这个思路在数学上就是:
y = F(x, {W_i}) + x
其中F代表残差模块中间的几个卷积操作,x是输入,y是最终输出。这个结构一旦成立,网络堆多深都不会越堆越差,因为至少存在一个理论上的最优解:剩余部分全部学成0。
这是整篇文献最核心的洞察,后续所有网络设计,比如DenseNet、ResNeXt、甚至Transformer里的残差连接,底层逻辑都是从这里长出来的。
2. 残差块设计里的那些细节
2.1 两类模块:BasicBlock 与 Bottleneck
论文里其实给了两种基础模块,这也是很多人看代码时容易忽略的。第一种叫BasicBlock,就是两个3x3卷积串联,每个卷积后面跟着Batch Normalization(BN)和ReLU,最后输入输出直接相加。你去看一个18层或34层的ResNet,用的就是这种。
第二种叫Bottleneck,专门给50层以上的网络准备的。它由三个卷积组成:先是1x1卷积把通道数降下来,然后是一个3x3卷积真正做空间特征提取,最后再用1x1卷积把通道数升回去。这样设计的理由是控制计算量。以ResNet-50为例,如果直接使用两个3x3的BasicBlock堆到152层,计算量会严重膨胀,训练和推理都很难落地。Bottleneck通过降低中间通道数,把一个模块的乘法计算量压缩到原来的四分之一左右。
注意,Bottleneck里两个1x1卷积的通道不是对称的。第一个1x1把通道从256压到64,第二个1x1再恢复回来。这样把计算集中在了中间的3x3卷积上,同时也保留了足够的跨通道信息融合能力。
2.2 恒等映射的三种连接方式
残差块要做加法,就必须保证F(x)和x的形状一致。三种方式分别对应不同的网络配置。第一种最简单,输入和输出形状完全一致,直接相加,论文里叫identity shortcut。第二种,如果输出尺寸缩小了,比如通过步长为2的卷积下采样,那x也需要跟着改变形状。论文给出的方案是,用1x1卷积且步长也为2,把x变换到目标形状。第三种是通道数不一致的时候,你还可以选择补零,但作者明确指出补零的性能不如1x1卷积。
我复现的时候,这两种情况在实际代码里会写成两个分支。短接分支如果为空,加法就是原位对位相加;如果不为空,这个分支就是一个独立的1x1卷积,它也有自己的权重。很多人第一次手写ResNet时经常在这里犯迷糊,把这个1x1卷积漏掉,然后发现维度对不上,报错都不知道错在哪。
补充一点,identity branch是可以被权重衰减砸到的。实际工程里有人将shortcut单独拿出来,不放进主路径的正则里,得到的效果略有不同,但我个人测试下来,只要BN和初始化正确,常规做法就已经很稳了。
2.3 为什么不加Batch Normalization反而训练不动
在我刚上手时,对BN在残差块里的位置,理解得特别浅。总觉得BN加在哪都行。直到自己手搭了一个没有BN的残差块,简单数据集上还能跑,一到ImageNet和复杂任务,损失就像卡死一样。这时候我才真正体会到论文里反复强调内部协变量偏移这个概念的意义。
在残差块里,BN的作用是稳定激活值的分布,保证残差F(x)的数值范围足够小且稳定。恒等分支x本来就已经有一个合理的分布,如果残差分支的分布一直乱跳,加法结果就会不稳定,反向传播时梯度的尺度也很难控制。BN本质上是给残差学习提供了一个数值上的稳定平台,这也是论文作者团队在原代码的每个卷积后面都接BN的原因。
所以,我可以很负责任地讲:复现ResNet结构时,那个两个卷积夹一个BN的顺序,尽量不要自己发明。你先按原论文和官方代码的顺序跑通,再逐渐尝试调整,否则很难定位问题到底出在结构还是出在训练配置上。
3. 网络架构与层数配置
3.1 从18层到152层的配置规律
ResNet系列不是一个单独的网络,而是一组按层数划分的家族。论文里给出了从18到152层的多个配置。虽然层数不同,但它们遵循一个统一的规律:网络被分成四个阶段,每个阶段内部的残差块数量递增,每个阶段开始时,空间尺寸减半、通道数翻倍。这样设计的意图是控制计算量。深层网络不会在所有阶段有相同分辨率,而是逐步把空间信息压缩成高语义特征。
举个例子,ResNet-50后面的层数分布是3、4、6、3。这三个数字加起来等于16,再乘以Bottleneck的3层,再加上最前面的一层初始卷积和最后一层分类层,总共50层。同理,ResNet-101是3、4、23、3,152是3、8、36、3。
我在实际工程中,更常把ResNet当成一个模板,而不是固定结构。很多框架允许你定义每个阶段的残差块数量,比如检测网络用地比较多的ResNet-50带FPN,就经常只保留前三个阶段的特征拿来做多尺度预测。所以理解这个块数配置背后的逻辑,比背数字有用得多。
3.2 不同ResNet怎么选、在哪用
层数选择不仅关乎精度,更关乎显存和推理时间。我的经验是:如果数据集规模中等,比如一万到十万张图,ResNet-34或ResNet-50就够用了。盲目上ResNet-152,在数据量不足的情况下收益很小,还会增加训练时间和显存占用。如果业务里对延迟非常敏感,比如线上实时推理,我一般会降到ResNet-18或ResNet-34,再通过蒸馏等手段补偿精度。
在检测和分割任务里,ResNet-50和ResNet-101是最常见的backbone。它们提取出的特征层次分明,浅层适合定位,深层适合分类。很多项目直接拿ImageNet预训练的ResNet来初始化,然后在新任务上微调,收敛速度快很多。只有在数据量特别大且任务特别复杂时,我才会考虑ResNet-152甚至比它更深的网络。
3.3 计算量对比和感受野的变化
从计算量看,不同深度的ResNet差异非常大。ResNet-18的FLOPs大约在18亿左右,ResNet-50是38亿,ResNet-152则到了113亿。如果只用CPU推理,这个差距是灾难性的。但感受野的变化也同样重要。随着层数增多,每个输出位置能看到的输入区域变大,模型对全局信息的感知能力增强。在细粒度分类任务里,大感受野往往能帮助模型抓住整体的结构关系,但也会丢失一部分局部细节。所以你会看到一些项目在浅层引入额外的小感受野分支,来同时兼顾局部和全局。
理解感受野的增量,对搭网络很有帮助。残差块中两个3x3卷积叠加,等效于一个5x5卷积的感受野,但参数量更少。Bottleneck中的1x1、3x3、1x1组合,感受野等于一个3x3,再加上额外的跨通道映射。所以,不要只看网络层数,要看你实际需要多大的感受野,再来决定在哪个阶段加深。
4. 恒等映射为什么能缓解梯度消失——前向传播的视角
4.1 前向传播中的恒等连接
这一节,我们要把数学搞明白。一个残差块,输出可以写成:
x_{l+1} = x_l + F(x_l, W_l)
如果网络一共有L个这样的残差块,从第l层到第L层的特征,可以通过连续展开得到:
x_L = x_l + sum(F(x_i, W_i))
这个展开式说明了一个很直观的事实:深层特征x_L,不仅包含经过一系列残差变换后的信息,还包含最原始的输入x_l。只要恒等路径畅通,信息就能无损直通。这一设计意味着深层特征的全局信息不会完全依赖逐层映射,即使中间的某几个模块学习得不太好,浅层信息依然能搭上直通车。
这也是为什么在ResNet里,信息流动非常顺畅。对比一下普通网络,每一层的输出完全依赖前一层的非线性变换,一旦中间某个节点数值坍缩,后面的层都会跟着遭殃。
4.2 反向传播中的梯度直达
反向传播时,损失C对x_l求导,可以用链式法则展开:
∂C/∂x_l = ∂C/∂x_L * (1 + ∂(sum(F))/∂x_l)
括号里这个1非常关键。即使残差分支的整体导数很小,甚至趋近于0,梯度仍然能够通过这个恒等项传回到浅层,不会彻底消失。这就是ResNet能支撑超深层网络的数学原因。
我用一个更直观的方式来解释:梯度在网络里传播时,相当于一条高速公路,每一个残差块都开了一个收费站的便道,让车可以直接穿过去。即使便道落满了积雪(残差分支梯度很小),主路依然畅通。
对比一下没有残差连接的网络,梯度必须穿过每一层卷积和非线性的乘积。如果这些因子的最大值小于1,多层相乘之后,梯度就会指数级变小;如果大于1,梯度又会爆炸。ResNet通过这个加法结构,硬生生把这条乘法链变成了叠加链,梯度缩放问题就大大缓解了。
4.3 为什么“恒等”很重要
你可能想问,短接分支为什么一定要是恒等映射,而不是另一个可学习的层?论文里专门提到,如果shortcut换成别的变换,比如乘以一个可学习的标量,可能会损害梯度传播。因为任何非恒等变换都会对自己求导产生缩放因子。如果该因子小于1,多层残差堆积后,梯度仍可能衰减;如果大于1,又面临梯度爆炸风险。恒等映射的导数为1,是最安全的选择,既不增也不减。
这一点后来被很多变体实验验证。比如有的工作尝试在恒等路径上加可学习的缩放参数,往往效果并不比标准版本好多少,却引入了额外的调参成本。我个人建议,在没有充分理由的情况下,shortcut保持恒等,别乱改。
5. 复现过程中的踩坑与经验
5.1 训练配置与优化器调节细节
直接复现ResNet的过程中,最容易翻车的不是网络结构,而是训练配置。原论文里使用了一批图片大小224x224,随机裁剪和水平翻转做增强,Momentum设为0.9,权重衰减设为0.0001,初始学习率0.1,批次大小256,一共迭代60万步。我自己复现时,遇到最常见的现象是:第一代网络结构就是按论文搭的,但测试集精度始终比论文低好几个点。
排查来排查去,问题多数出在优化器上。现在的框架默认优化器实现细节不完全一样,尤其是动量缓冲的初始化和BN的momentum参数。如果你用PyTorch,注意SGD里的nesterov是否开启,权重衰减是否真的加在卷积和BN的所有参数上。很多复现精度上不去,就是因为BN层的参数也被施加了不合理的权重衰减。
5.2 几个容易踩的坑
以下坑,都是我实际踩过或在帮别人排查时见过的,逐条列出来:
- 第一个,维度匹配时1x1卷积的步长。下采样阶段shortcut的1x1卷积必须步长2,否则空间尺寸对不上。这是新手最常见的报错点。
- 第二个,BN默认初始化。BN层的gamma初始化为1,beta初始化为0。如果某个任务里你修改了初始化,会导致早期训练特别不稳定。
- 第三个,学习率太大或太小。ResNet在ImageNet这类大任务上,初始学习率0.1很合理,但如果你用了更大的批次,最好按比例换算。迁移到小数据集时,0.1就没有参考价值了,0.01或0.001更稳妥。
- 第四个,多卡同步BN。跨卡训练时BN的均值和方差如果不做同步处理,大批次下的统计量会出现偏移。小batch单卡训练时要注意本地统计和全局统计的切换。
- 第五个,预训练权重的通道顺序。从某个仓库下载的权重,如果网络代码定义的通道顺序不一致,加载时可能静默出错,精度直接崩掉。加载权重后最好先用单batch做前向比对。
5.3 从论文到实际业务的项目化落地
如果你只是做实验室复现,跑通训练、看准确率就够了。但如果要落地到实际业务,你需要多考虑几个层面。第一,模型压缩和剪枝。ResNet-50的权重约25MB,边缘设备往往扛不住,需要蒸馏或结构化剪枝。第二,推理速度优化。把BN折叠到前一层的卷积里,可以省去推理时的额外计算。这是工程上常用且安全的操作。第三,不同框架间的转换。保存成ONNX再转到边缘推理框架,往往会在BN和Resize这类算子上遇到兼容性问题。我的建议是,把ResNet当成一个标准组件,转换前后都做一次全模型输出对比,确保误差可控。
6. 实验结果该怎么读、怎么和别的网络比
6.1 ImageNet上的对比
论文的实验部分,最核心的对比是34层ResNet和34层Plain网络。这两个网络层数相同,唯一区别是有无残差连接。训练条件保持一致的情况下,34层ResNet的top-5错误率是5.71%,而34层Plain网络是8.60%。这个对比特别干净,因为它控制变量控制得非常好,证明了提升不是来自更深的层数,而是来自残差连接本身。
同时,18层ResNet和18层Plain网络都对,18层的精度差异不算特别大。这一点也非常关键,说明在浅层时,网络能力可能还没遇到瓶颈,残差的优势展现得不够充分。只有当网络加深到一定程度,退化问题出现时,残差连接的价值才真正凸显出来。
6.2 CIFAR-10上的深层实验
论文作者还在CIFAR-10上做了极限实验,将网络加到了110层,甚至尝试了1202层。在110层时,错误率可以做到6.61%左右,1202层的网络也依然能收敛,但是精度略有下降。这个结果的意义不在于那一点点错误率,而在于证明了残差连接可以支撑起1000层以上的网络训练。这对于当时的认知来说,冲击力非常大。
我自己在CIFAR-10上复现过110层的ResNet,训练时间比34层明显长很多,但收益已经很小。小数据集上深层的优势并不明显,因为模型容量早饱和了,反而是正则化手段的收益更大。
6.3 网络结构对比表的读法
论文里那张对比各网络在ImageNet上的错误率表,我建议你逐行细读。你会发现,ImageNet当时主流网络VGG-19的错误率约8.20%,参数量也大。ResNet-34的参数量小一些,但错误率已经降到5.71%左右,ResNet-50则可以直接到4.75%附近,ResNet-152能到3.57%左右。读完这张表,基本就能理解为什么后来大家的backbone都换成了ResNet家族。
读这张表的另一个技巧是,不要只看精度,还要看计算量和模型大小。有些论文在对比时故意忽略这些因素,只报最高的精度,但实际工程部署时,效率和精度需要同时权衡。
7. 对后续研究的影响,和一些个人体会
7.1 残差思想如何渗透到整个深度学习
现在去看各个领域的深度学习模型,几乎都有残差连接的身影。DenseNet把残差思想进一步发展成了密集连接,WideResNet通过加宽残差块结构来提升性能,ResNeXt则把残差里的卷积换成了分组卷积。甚至Transformer里的每个子层都有Add操作,本质上也是一种残差设计。残差思想的影响范围已经超越了图像识别,进入了自然语言处理、图神经网络等领域。
我在不同任务里反复观察到同一个模式:只要网络变深,残差连接就成了刚需。不管网络结构本身是卷积还是全连接还是注意力,只要没有明确的跨层连接,深层训练就会变得极不稳定。这个观察,让我在很多时候设计一个新的网络模型时,都会先问自己:各层之间有没有合适的残差通路?这是不是避免退化最廉价的手段?
7.2 一些个人体会和后续可以扩展的方向
最后分享一点我自己的看法。ResNet的成功让我明白一个深刻的道理:在深度学习里,设计一个让优化更容易的网络结构,有时比单纯提升模型表达能力更管用。表达能力强但没有良好的优化通道,深度带来的红利会被训练困难抵消,甚至变成负收益。
目前,我在实际项目中还会经常使用ResNet作为对比基线。每当我拿到一个新任务,不知道哪种结构更合适时,我都会先跑一遍ResNet-50,这个结果基本上可以作为后续所有实验的基准线。因为ResNet的训练已经非常成熟,代码、权重、技巧一应俱全,它能很快帮我建立对数据规模和任务难度的基本判断。
后续如果你想继续扩展,可以沿着几个方向:一是结合最新的训练技巧,比如不同的优化器、学习率调度、数据增强,再看ResNet的上限能到多少;二是把ResNet与Transformer做混合结构,比如浅层用卷积或专门的特征提取模块,深层用注意力机制,这种方式在很多任务上效果极佳;三是研究更高效的残差变体,在保持精度的同时把计算量压下来。这些方向都建立在读懂ResNet基础细节之上,并不难。