退化,梯度,以及那个小箭头
头回见到残差连接,是在ResNet那篇论文里——说实话,第一反应是:就这?不就是在输出上硬加了个输入吗?也太粗暴了。但是,当我自己撸了个30多层的CNN,看着训练loss不降反升,再切到ResNet-34跑一遍,那收敛速度…心里只剩下两个字:服气。
故事得从深度网络的退化(degradation)说起。很多人以为深层网络精度饱和是因为过拟合,其实不然。何恺明他们做了个实验:用CIFAR-10,一个20层的网络和一个56层的网络。不加残差的话,56层的训练误差和测试误差都比20层的高——不是持平,是更差。这可不叫过拟合,这叫优化都搞不定。深层网络里,梯度传着传着就没了,或者炸了,要不就是拐进了让人头大的鞍点。残差连接,就是给梯度一条高速通道:H(x) = F(x) + x。如果恒等映射是最优的,那直接把F(x)的权重推到零就行。学习残差,比学原始映射容易得多——恒等映射那可是零基础就能干的活。

这个结构,也叫短路(shortcut)。你可以把它想象成一条信息高速公路。主路是F(x),学的是变化量;x这条辅路直接把原始信号送过去,不参与任何变换。两层卷积堆起来,该学的学,学不好的地方,x直接补位。梯度反向走的时候,分两条路:一条经过F(x)的复杂函数,一条直接流过加法操作——梯度1.0倍无损传递。这就把深层网络的梯度消失问题给物理绕过了。很暴力。很有效。
数据不说谎:残差带来的碾压级提升
口说无凭,咱们上硬货。ImageNet 2012的分类任务,当时还是AlexNet的天下。后来VGG往深里堆,堆到19层就推不动了。ResNet带着残差连接一出来,直接干到152层,效果还炸裂。看下面这组对比——我用的是论文里原话数据:
| 模型 | 层数 | Top-1 error | Top-5 error |
|---|---|---|---|
| Plain-18 | 18 | 27.94% | 9.24% |
| Plain-34 | 34 | 28.54% | 9.96% |
| ResNet-18 | 18 | 27.88% | 9.26% |
| ResNet-34 | 34 | 25.03% | 7.76% |
看出问题没?Plain-34比Plain-18层数多了快一倍,错误率反而涨了0.6个百分点——典型的退化。而ResNet-34直接比Plain-34低了3.5个点,甚至比自己的18层版本还猛。这就是残差的魔力:堆得越深,越能榨出性能,而不是让网络智障。
更夸张的在后面:ResNet-152在ImageNet top-5错误率干到了5.71%(单模型,没整啥多尺度融合)。要知道之前的冠军GoogLeNet是6.67%,VGG是7.3%。这不仅是超越,是断层式领先。后来DenseNet、ResNeXt,甚至Transformer里的Add & Norm,骨子里都是同一种思想——残差连接,成了现代深度网络的标配暗器。

落地踩坑实录:三个让你头大的陷阱
轮子好用,但自己造的时候,坑可一个不少。我这几年在生产环境里练残差网络,结结实实栽过三次跟头。你且听我道来。
陷阱一:维度不匹配的加法雷区
这是新手最爱犯的错。残差要求F(x)和x的形状严丝合缝。一旦你用了个stride=2的卷积,或者改了通道数,直接相加,TensorFlow/PyTorch立马甩你一脸shape不匹配的报错。更坑的是,有些框架会悄悄做广播,结果你训了半天,出来的权重全是乱的。
解药:老老实实上投影shortcut(Projection Shortcut)。一个1×1卷积,stride和主分支对齐,通道数也调到一致。比如你主分支用了3×3 stride=2,输出通道64,那shortcut就接一个1×1 stride=2, 输出64的卷积。别嫌麻烦,这点参数省不得。另外,能用identity shortcut(无参数)的时候尽量用,别啥都上projection——平白无故多出参数,过拟合风险高不说,推理还慢。
陷阱二:归一化层放错位置
残差块里,BN放哪儿?我见过有人把BN塞在加法后面,或者把ReLU放在加法前。结果:训练曲线巨抖,loss疯狂震荡。原因?加法和归一化的顺序直接影响信号和梯度的分布。原版设计是:Conv → BN → ReLU → Conv → BN → Add → ReLU。后来何恺明他们又搞了个预激活(Pre-activation):BN → ReLU → Conv → BN → ReLU → Conv → Add。这种变动微小的调整,却让1001层的ResNet训得稳稳当当。
牢记:加法操作之前,两个分支的输出必须经过同样尺度的归一化。捷径那头是x,它可能没经过BN;而主分支最后是BN输出。两边一加,数值分布不一致,优化器内心是崩溃的。所以,要么shortcut那头也带BN(projection里得加BN),要么像预激活那样把BN移到权重之前,让数据流先归一化再双路合流。
陷阱三:维度过早压缩,信息瓶颈
ResNet-50开始用了bottleneck结构:1×1降维 → 3×3卷积 → 1×1升维。这设计很省参数,但降维倍数不能太狠。我试过在某个早期层直接把256维降到32维,再用shortcut加回去——结果模型收敛到一半,准确率卡在锅盖顶,上不去也下不来。原因?低维空间信息压缩太厉害,残差分支根本学不到有用东西。
解法:控制压缩比,一般取输出通道的1/4。比如输出是256,中间就降到64。如果输入输出维度差得远,projection别省,保证信息通路。还有,bottleneck最后升维那一步,BN要加在加法前,不要加在加法后——很多框架会帮你做,但自定义的时候一不留神就写错。
残差连接这东西,真是大道至简的典范。一个加法,解决了困扰学界好几年的深度退化。何恺明当年写代码时,恐怕也就是在forward里加了一句out += residual,但背后的工程直觉,够我们琢磨好一阵子。现在你闭上眼睛想,网络设计里,还有多少这种“四两拨千斤”的机会?