
我刚开始做图像分类模型迁移时遇到过一件很别扭的事把网络从18层换成34层理论上参数更多、拟合能力更强测试集精度反而掉了一截。当时第一反应是过拟合可一看训练集上的loss发现训练集上就已经涨上去了这就不太对劲——模型在训练数据上表现变差说明问题根本不在泛化而在优化。后来我才彻底弄清这正是ResNet残差网络要解决的退化现象也是残差连接为什么能成为现代深度学习地基的原因。虽然现在已经很少有人从零手写一个网络训练到收敛但理解残差连接仍然值得。你后面读几乎所有主流模型——从各类Transformer到扩散模型U-Net都会看到“跳跃连接”“恒等映射”“残差块”这些词。这篇文章我从原理讲到工程落地的细节再到后面对这个思想的继承和改造尽量把ResNet这课说透。1. 堆层数这个“常识”到底是从哪里开始翻车的1.1 深度增加后的精度饱和是在训练集上就发生的在残差网络出现之前想让模型更强最直接的做法就是把网络做得更深更大。VGG时代的思路基本就是这样几层到十几层暴力堆卷积。早期大家也确实能看到层数增加精度跟着涨。可涨到一定程度后就出现了一个非常诡异的现象——继续堆层精度不仅不涨还开始往下掉。如果只是测试集精度下降大家还能用“过拟合”来解释。但实际观察到的现象更让人头疼训练集上的误差也同步上升。也就是说深层模型连记住训练数据都做不到了这跟“泛化能力变差”完全是两码事。一个参数更多的模型理论上应该有更强的表达能力就算什么都学不会至少也该跟浅层模型打个平手。可它连浅层网络的效果都达不到逻辑上似乎说不通。这就排除了“模型容量不够”这个猜测。深层网络的问题不在容量而在优化——网络越深梯度信号在反向传播过程中越难顺利地回传到浅层参数上训练过程变得非常困难。这种层数越堆越差、训练误差不降反升的现象后来被称作退化问题degradation problem。1.2 深层网络为什么不等于复杂模型的简单叠加很多人会把“深层”和“复杂”画等号潜意识里觉得网络越深学习到的函数越复杂。但实际训练里深度带来的不仅是模型表达能力的提升还带来了优化上的巨大挑战。你仔细想一下训练过程中的参数更新路径。一个普通的多层网络每一层都在对输入做一次非线性变换。信息从输入传到输出要经过一连串的矩阵乘法和激活函数梯度从损失函数回传到前面的层同样要穿过这一连串变换。层与层之间的变换如果不太“温和”梯度要么指数级衰减要么指数级爆炸。尤其是非线性激活函数比如Sigmoid经过多层叠加之后对梯度的压缩效应非常明显——传到浅层网络时梯度可能已经近乎为零浅层参数基本收不到有效的更新信号只能随机初始化原地踏步。这就是梯度消失问题。早期很多研究试图用ReLU、用更好的初始化来缓解确实有改善但解决不了更深层网络优化上的退化问题。1.3 退化问题与传统梯度消失问题的区别以及恒等映射的启发这里的微妙处在于退化问题和分类讨论里常说的“梯度消失”不能完全画等号。梯度消失通常指梯度数值趋近于零浅层参数根本没法学。但ResNet之前的实验里中等深度的网络——大概二三十层——用合适的初始化技术已经能训练起来说明梯度没有彻底消失但继续加深之后精度的提升就反而劣化了。那是什么在拖后腿研究者们给出的一个切入点很有意思假如深层网络的深层部分学到的都是无效变换那么只要后面的层在做一个恒等映射——也就是输入什么就输出什么——整个网络的性能起码不会比浅层网络差。但普通的叠加式网络很难做到这一点。因为每个卷积层、每个激活函数都要改变一点输入信息多层叠加下来要让组合出来的函数恰好变成恒等映射这种精确构造基本上不可能通过随机初始化后的梯度下降来完成。于是核心矛盾变成了对于梯度下降这种优化手段来说学一个恒等映射竟然也很难。如果有办法让网络结构本身就把“恒等映射”这条路修好让深层部分的选项里天然包含“什么都不做”这个选择优化就会容易得多。这正是残差连接想出来的出发点。2. 残差学习的数学改写从“学完整映射”变成“补缺口”2.1 F(x) H(x) - x 反直觉在哪残差连接的核心是做了一个数学形式上的改写。普通网络里某一层区块想学一个目标映射H(x)那我们就直接让这层去拟合H(x)。残差网络不去直接拟合H(x)而是定义一个残差函数F(x) H(x) - x然后让网络这部分去学F(x)最后输出的时候再把x加回来。也就是说对应的输出变成了F(x) x。表面上看两种形式的表达能力完全一样——既然F(x)可以表示任意函数那F(x) x自然也能表示H(x)。但落到优化上差异就显现出来了。如果我们想学的是恒等映射H(x) x那残差网络只需要把F(x)的参数往零收敛也就是让残差这一支逐渐不起作用即可。压力瞬间小了很多——把权重往零推是对梯度下降来说比较容易的事而让一堆卷积核的权重精确组合出一个恒等变换则难得多。我更喜欢用一个生活方式来理解好比你要完成一篇文章普通网络是每次都要从零开始写完整段落残差网络是维护一个成品文档每次只撰写相对原稿的增删改。修改一个文档永远比重新写一个一模一样的文档容易。这个思路不复杂但它彻底改变了深层网络优化的走势。2.2 恒等映射的“兜底”作用如何体现在网络放大的过程中残差结构里有一个值得反复品味的细节当残差函数F(x)趋向于零时整个块的行为就退化成恒等映射。这意味着网络在训练过程中无论中间层数怎么增加至少有一个默认的保底选项——不让深层部分拖后腿。这能解释为什么残差网络的层数可以从少数层次直接推到非常深。想象一下你有一个已经可以工作的浅层网络想在后面再追加几十层的时候加残差结构其实等价于给网络增加了一组初始化为零附近的“候选分支”。初始训练时骨干路径继续发挥作用新增层要么学会对骨干输出做有意义的修正要么干脆保持零输出不会把原有性能破坏掉。当然真实训练时所有这些层是同时学习的并不是真的有一个网络“先训好浅层、再慢慢加深层”。但残差结构在逻辑上提供了这种可能性让优化器可以灵活地决定每一层用多少、不用多少。这也是拿残差网络和普通深层网络做对照实验时前者训练曲线明显更平稳、更不容易陷入局部不好的解的原因。2.3 反向传播视角下的梯度捷径从反向传播的角度看残差连接还做了一个相当关键的事它给梯度开辟了一条没有障碍的“快速通道”。记网络某层输入为x输出为y F(x) x。损失对x的梯度根据链式法则写成∂L/∂x ∂L/∂y * (∂F/∂x 1)这里有个恒等项“1”。如果没有残差连接梯度只能沿着∂F/∂x这条满是权重矩阵乘积的路径回传深度一宽、层数一多梯度经过连乘很容易被压缩到极小的数量级。而有了这个“1”最坏情况下梯度也能被原封不动地传给上一层避免彻底消失。这其实解释了一个我实际训练中经常观察到的现象残差网络对初始化尺度和超参数的敏感度明显低于普通深层网络。我曾经试过在共用一个模型框架的前提下把某层的权重初始化标准差调大普通深层网络很快就出现参数爆炸的迹象而残差模型只是训练曲线波动了一阵最后还能收回来收敛。那条恒等路径在中间起了很大的稳定作用。3. 工程落地BasicBlock、Bottleneck与短路连接的三种做法残差连接的原理讨论起来很清爽但要真正把网络搭出来有几个结构设计的细节是绕不开的。我按自己复现时的顺序一个个说。3.1 BasicBlock 的结构与感受野权衡残差网络中最早出现的基础残差块通常叫BasicBlock。它由两个3x3卷积组成中间夹着批量归一化BN和ReLU激活函数。输入x先经过第一层卷积再BN、ReLU再第二次卷积、BN然后把输入x从分支加回来最后再过一个ReLU输出。这里有个设计点很容易被忽略为什么用两个3x3卷积而不是一个5x5卷积在感受野上两个3x3卷积叠加等价于一个5x5卷积但参数更少而且中间多了个非线性激活分区模型的非线性表达能力反而更强。后者的这个设计思想保留为现代深度学习中的一种常见折中。BasicBlock通常用在层数比较低的残差网络里比如34层以内的。因为计算量不大通道数可以在每个块里保持比较高不会造成显存压力。# 伪代码用PyTorch风格表达BasicBlock前向逻辑 def basic_block_forward(x, conv1, bn1, conv2, bn2): identity x out conv1(x) out bn1(out) out relu(out) out conv2(out) out bn2(out) out out identity # 残差相加 out relu(out) return out3.2 Bottleneck 的通道压缩设计网络一深计算量会急剧上升。为了把ResNet做到几十层甚至上百层还能在GPU上训练后来提出了残差网络的瓶颈结构Bottleneck。它的思路很直接先用一个1x1卷积把通道数压缩到原来的四分之一再用3x3卷积做空间特征提取最后再用一个1x1卷积把通道数扩张回去。这跟PCA降维的思路有点像——把特征图先压缩到一个低维空间里去处理处理完之后再映射回原来的维度。通道压缩最直接的好处是计算量大幅下降。如果两个块都保持同样的输入输出通道数Bottleneck的速度优势非常明显而它反而可以把块的数目增加更多总体网络深度更大。在实际的模型部署里我遇到的绝大多数预训练模型都使用Bottleneck版本因为它兼顾了深度和计算效率。基础块和瓶颈块的取舍本质上是在计算预算和网络深度之间做权衡并没有绝对的好坏。3.3 降采样层的三种短路方式怎么选残差块内需要做降采样或通道数变化的时候输入x和输出F(x)的形状就不一致了不能直接相加。这时候短路连接通常有三种处理方式第一种是直接补零。对于多出来的通道直接在特征图里拼接零值不引入额外参数。实现简单但是新增通道的信息一开始都是零训练初期会吃掉一点点学习优势。第二种是使用单位映射池化。对于空间尺寸缩小的情况用平均池化把特征图缩到目标尺寸通道不够的补零通常配合全连接层前的全局平均池化使用。第三种是加入一个1x1卷积投影把输入x变换到目标通道数和空间尺寸这是实践中用得最多、效果也最稳定的方案。那个1x1卷积通常还会跟着一个BN。我个人在给比较小的项目设计网络时统一走第三种方案。省心而且1x1卷积参数开销几乎可以忽略。3.4 不同深度的网络配置对照为了验证哪些结构设计真正起作用不同深度的ResNet在资源耗费和性能上存在明显差异。我把常见的几种配置放在一起做个对照配置残差块类型大致层数适合场景ResNet-18BasicBlock18层左右快速验证、移动端、小数据集ResNet-34BasicBlock34层左右中等数据集训练速度快显存要求低ResNet-50Bottleneck50层左右通用视觉任务性价比高预训练模型多ResNet-101Bottleneck100层以上精度提升明显的复杂任务分割、检测等几个配置用同一套残差公式但残差块的内部设计不同。这意味着如果你只是想验证一个数据增强策略几十层足够了但如果要让模型具备很强的表征能力又必须做好深层训练的准备Bottleneck几乎是必然选择。4. 训练残差网络的实战细节与排坑记录原理看懂、结构写出来不意味着训练就能一帆风顺。我在复现和调整残差网络的过程中踩过几个坑拿出来说说。4.1 BN的摆放位置原始版与预激活版的差异第一个坑出在BN的摆放顺序上。原始版残差块的结构是“卷积-BN-ReLU-卷积-BN-相加-ReLU”也就是把BN放在残差函数内部相加之后再用ReLU。而后来出现的预激活残差块则是把顺序调整成“BN-ReLU-卷积-BN-ReLU-卷积”整个块的尾部不再有ReLU短路连接直接加到输出。这个调整看似只是顺序交换实际效果差别很大。预激活结构能更好地保持恒等路径畅通——相加之后不再过ReLU恒等信号的数值就不会被截断。训练非常深的网络时这种“通路更干净”的特性可以明显提升收敛速度和精度。我自己在训练超过100层的残差网络时明显感觉到预激活版本更稳训练曲线的抖动少很多。所以在有深网络需求时我倾向于用预激活风格的残差块而不是完全照搬原始论文的分支。4.2 学习率warmup和初始化策略第二个容易被忽略的问题是学习率。残差网络因为包含跳跃连接参数空间的某些方向对学习率特别敏感。尤其是使用较大batch训练的时候不设置warmup阶段训练初期loss可能会出现剧烈波动甚至直接发散。我的经验是从零训练时先用较保守的学习率跑几个epoch比如从头5个epoch把学习率从0线性上升到目标值然后再切换成衰减策略。这有点像人刚起步时先小步走两步、跑起来之后再全速前进目的是给BN统计量一个稳定阶段。权重初始化方面如果残差网络的分支里使用了BN那么初始化时把BN层gamma设成0是常见的做法。但很多人不知道在某些实现版本里最后一个BN的gamma初始化为0会让整个残差块初始状态接近恒等映射对稳扎稳打非常有效。我实测下来这种做法在很多库的源码里都能看到确实是稳定收敛的杀手锏。4.3 一个关于第一个卷积层的坑训练速度慢、精度差有时候不是残差块内部出问题而是卷积核设计埋了隐患。常见的一个做法是把普通卷积核换成一个步长为2的7x7卷积前加池化作为最初的降采样模块。这个模块在某种数据集上效果不错但在小图像任务上太早降采样会损失大量边缘和纹理细节。如果你是在64x64甚至32x32分辨率的图像上做分类直接套用面向高分辨率输入的配置往往第一层卷积加池化就把有效信息抹掉了。这时候把降采样推迟到更靠后的层或者将初始卷积核改成3x3步长1能够明显保住细节信息。这类问题数据集和网络结构之间的匹配往往比“调大学习率”更有用。5. 残差思想在后续网络结构中的演进脉络残差连接刚提出时“跳跃连接”还只是其独有的结构特征。几年之后你会发现这个思路已经蔓延到整个计算机视觉和更多领域而且被改出了不少新花样。5.1 从残差到稠密连接一个很有代表性的改写思路是稠密连接。普通残差结构每个块只有一个跳跃连接而稠密连接把前面所有层的输出都拼到当前层的输入里形成一种“每一层都能看到前面所有层特征”的结构。它的逻辑延续了残差的初衷——信息流动得越顺畅网络越容易训练。只不过它把“一条捷径”扩展成了“多条捷径”通道拼接的代价是显存和计算量增加迅猛。我在实际使用中很少直接在超大模型里采用密密麻麻的稠密连接但它那种逐层累加特征的做法在医学图像分割等需要细节保持的任务里确实很有价值。残差思想在这里被证明是可迁移的而不是只能用在分类网络里。5.2 残差与分组卷积的叠加另一个演化方向是残差块内部增加分组卷积。分组卷积把特征通道分成若干组每组独立做卷积相当于在一个块里并行了多个小网络再把结果拼在一起。这种形式把残差思想保留下来同时通过组的数量来控制模型宽度和计算量。这类结构在相同计算资源下能比单纯的深层网络容纳更多通道所以经常出现在需要大模型但功耗受限的场景里。我在做一次图像特征提取实验时在同样FLOPs预算下用分组卷积变体比直接用ResNet-101高了差不多一个点的准确率可见残差只是改造的基础内部结构有相当大的腾挪空间。5.3 残差作为一种通用设计范式再往后你会看到残差连接出现在各种各样与图像分类无关的网络里。Transformer类的模型里多头注意力子层和前馈子层之间都夹着残差连接。扩散模型的降噪U-Net中不同尺度之间的跳跃连接同样承担着把低频信息传回解码器的任务。这些使用方式说明残差连接已经从“一种网络结构”变成“一种设计范式”——只要存在信息跨层流动的需求就可以在路径上加上一个恒等捷径。我在搭建各种自定义网络时最常用的起步思路就是主体结构设计完之后在关键桥接位置手动画上几条恒等捷径构建出的训练稳定性就大幅度改善。对于做工程的人来说这可能是残差连接最宝贵的遗产。6. 最后分享几点实操体会写到这里我把原理和踩坑交错说完最后想给读者几点直接的体会。先从训练模型的角度说残差连接并不是万能保险丝。它确实能缓解梯度消失但如果你把残差网络本身就设计得很糟糕比如初始卷积核太大、BN位置摆错、残差块里叠加了太多非线性深层训练还是会出问题。恒等路径是短板不假但剩下的路仍然需要认真铺。从复现论文的角度说如果你要在自己的数据集上迁移一个残差网络第一件事不是调参而是先确认预处理和训练配置是否匹配。很多公开的复现bug都出现在输入尺寸、归一化方式、数据增强和网络第一层之间的配合上。残差模块本身很少出bug出问题往往在外部。我自己的习惯是在熟悉基础残差块之后直接去读几行预激活版本的实现对比两版forward函数的布局。当你自己动手改过一次内部顺序观察训练曲线的变化你会发现残差连接背后的敏感点其实就那么几个恒等路径要畅通残差分支要稳健非线性要分层把控。把这三个点想清楚很多模型对你来说就再也不是“黑盒调参”了。