2015年前后,一篇关于深度残差学习的论文改变了视觉模型的训练方式。当时很多研究者的直觉是“网络越深精度越高”,可真正跑实验时,网络一超过几十层就各种“训不动”。这篇题为《Deep Residual Learning for Image Recognition》的论文,核心思路极其朴素:让被堆叠的层去预测相对输入的残差,而不是直接预测完整输出。论文里那一句“y = F(x, {W_i}) + x”,后来变成了无数网络结构里默认带着的“短路连接”。我会沿着问题驱动的方式,先讲清楚为什么非做残差不可,再拆解残差块的前向与反向过程,然后聊论文实验里那些容易被跳过的关键设计,结合我迁移到实际项目中的体会,最后讨论残差思想的边界。无论你是第一次读论文,还是已经在用残差网络,这篇拆解都希望给你提供一些课本之外的角度。
1. 为什么软柿子会变成硬骨头:退化才是头号敌人
1.1 退化现象:不是过拟合,而是训练不上去
在残差结构出现之前,把网络从20层加到50层,得到的往往不是精度提升,而是更差的结果。很多人第一反应是过拟合,因为模型变复杂了嘛。但论文的实验给了我们一个完全不同的结论:加深后的网络不仅在验证集上误差更高,在训练集上的误差也同样更高。过拟合的标准表现是训练误差低、验证误差高,所以眼前的问题根本不是过拟合,而是“训练阶段就没学好”。
这件事很反直觉。一个34层的网络,表达能力理论上不会弱于18层的网络。因为哪怕多出来的16层什么也不学,只是原样把上层输出传下去,34层也至少应该和18层打平。可实际训练中,优化器根本没有办法让那些多余层学会“恒等传递”。非线性层堆叠在一起时,层间的参数和激活值互相耦合,默认初始化方式下,想要让某个区块退化成恒等映射非常困难。于是层数越多,网络要搜索的解空间越大,反而越容易卡在一个糟糕的局部区域。
我当时第一次看到这个结论,觉得可以打一个工科味很重的比方:给你更多积木,理论上能搭出更复杂的建筑,但如果你手不够稳,积木越多越容易垮。深层网络就是积木,优化器就是那只手。很多年后再看,“退化”这个概念的提出,比残差结构本身还要重要,因为它把问题定义清楚了。
1.2 残差重构:把“要学的东西”换掉
论文没有发明新的激活函数,也没有搞复杂的初始化策略,而是直接改变了每个堆叠层要学习的目标。传统网络里,堆叠层要去学一个潜在映射H(x)。残差学习的做法是让堆叠层去学另一个映射F(x)=H(x)-x,也就是“输出相对于输入的差值”,然后把差值加回输入,得到完整的H(x)=F(x)+x。
这样改的直接收益是:如果最优解非常接近恒等映射,那么F(x)只需要逼近0。让权重和偏置整体向0靠拢,这件事在参数空间中很简单。反过来,如果让非线性层直接学会恒等映射,那意味着要精心调整所有卷积核,使输入经过多次非线性和加权后仍然保持不变,这几乎不可能做到。
我用一个接地气的例子来理解这件事。假设要预测明天的气温,直接让模型输出绝对值,它需要先理解当前温度、季节、日照、风力等一堆复杂背景,再给出一个数值。如果改让模型输出“相对于今天温度的增量”,这个增量的范围通常很小,模型只需要学会拟合一个小变动即可。普通深层网络是在无中生有地学一个新函数,残差网络是在学一个很小的修正量,两者在优化难度上完全不是一个量级。
这就是残差思想的核心哲学:先假设新加的网络层至少不破坏已有信息,再在已有信息基础上去做小修小补,而不是让每一层都从零开始构建一个全新函数。后来的很多网络设计,都能看到这种“保底叠加”思路的影子。
1.3 快捷连接:物理上把输入直接送过来
要支持“F(x)+x”这步计算,网络里必须有物理上的一条通道,让输入x直接绕过中间的非线性堆叠,和F(x)的输出相加。论文把这条通道称为恒等快捷连接,实现上就是一次纯粹的逐元素加法,不引入额外参数,也不增加计算量。
很多初学者以为“跳过连接”是一种学习出来的门控机制或者注意力权重,其实不是。残差块里的快捷连接就是一条直线,x原样过来,加上去,完事。整个计算路径是:输入x先经过卷积、批归一化、ReLU这些操作,得到F(x),然后在进入最后的ReLU之前,把x和F(x)相加,再一起通过激活函数。
我后来在代码里写残差块时,最常犯的错误就是在这条捷径上乱加东西。一旦给捷径配上可学习的变换,它就不再是恒等连接,“保底”这件事也就被削弱了。所以论文中的原始设计其实非常克制:能用纯加法,就绝不过度设计。
2. 一个残差块的数学与工程:从公式到训练
2.1 两层卷积块的计算路径与公式
先看数学定义。残差块要学习的残差映射F,可以写成最常见的两层形式:
y = F(x, {W_i}) + x
其中F是两层卷积的复合函数,F = W_2 · σ(W_1 · x),σ代表ReLU非线性激活,x和y是输入输出张量。如果输入输出维度不同,x上就要加一个线性投影W_s,式子变成y = F(x, {W_i}) + W_s·x。
把前向逻辑用Python伪代码写出来,大概是这个样子:
# BasicBlock 前向逻辑(示意) def forward(x): identity = x out = conv1(x) out = bn1(out) out = relu(out) out = conv2(out) out = bn2(out) out = out + identity out = relu(out) return out注意identity = x和最后out = out + identity这两行。训练时,BN放在卷积后面、ReLU之前,加法之后还会再接一次ReLU。这是论文里最基础的两层残差块。
更深网络里,论文把两层3x3卷积替换成了三个卷积的bottleneck结构:先用1x1卷积把通道数降下来,中间用3x3卷积处理空间特征,最后再用1x1卷积把通道数升回去。这样做的目的只有一个:在保证足够深度的前提下,控制计算量。原始残差块定义简单,工程上却有非常多的变形,理解基础版本之后,bottleneck只是换了个盒子,加法核心没有变。
2.2 反向传播的直觉:梯度多了一条高速公路
只看前向过程,残差连接好像只是多加了一个加法节点,没什么神秘。它的价值真正体现在反向传播时的梯度流通过程。
设最终损失为L,残差块的输出为y = F(x) + x。损失对输入x的梯度,按照链式法则展开:
∂L/∂x = ∂L/∂y · (1 + ∂F/∂x)
继续展开就是 ∂L/∂x = ∂L/∂y + ∂L/∂y · ∂F/∂x。右边第一项来自恒等捷径,它把损失梯度原封不动传回上一层;第二项来自残差分支,经过卷积等变换后传回。
这意味着,即使残差分支的梯度∂F/∂x非常小,第一项也始终存在。梯度就算不走残差分支那条难走的路,也有一条绝对通畅的“高速公路”可以一路回传到浅层。没有这条捷径时,梯度要连续乘以若干层Jacobian矩阵,一旦某些层的特征值小于1,几十层积累下来,梯度就会指数级衰减到几乎为0,前面那些层根本学不到东西。
残差连接相当于把这种“连乘路径”变成了“加法旁路”。虽然∂F/∂x那一支仍然可能很小,但保底的那一支始终稳定。这也是为什么残差网络可以用比较深的结构而不至于梯度消失的根本原因。
有人会问:如果把普通深层网络的初始化调得非常好,是不是就不需要残差了?理论上特殊初始化可以缓解,但实践中很难做到对所有深度、所有数据都稳定。残差结构的价值是让优化对超参数和网络深度更鲁棒,而不是靠运气去赌收敛。
2.3 维度不匹配时别轻易上投影
残差块的加法要求x和F(x)的通道数、空间尺寸完全一致。网络里只要出现池化或步长为2的卷积,输入输出尺度就会变化,这时候不能直接相加。论文提供了两种处理方式:一是zero padding,把x的通道补齐到目标维度;二是用一个1x1卷积做线性投影W_s,把x变换到目标维度。
论文在实验里对照过这两种方案,结论大致是zero padding不增加参数,但恒等路径被破坏了对齐关系;1x1投影表达更强,但作者也提醒不要过度使用,能保持纯恒等连接就尽量保持。
我自己写残差网络时,只在维度发生变化的临界层,用一个不带激活的1x1卷积加BN来匹配尺寸;其他所有层都保持纯加法。捷径上如果加了BN或激活函数,就不再是真正的恒等路径,训练初期稳定性和收敛速度都会明显变差。另一个值得注意的设计是“预激活”形式的残差块,它把BN和ReLU放到卷积之前,让捷径上完全没有任何变换,非常干净。原始论文的post-activation形式在中等深度下差距不大,但读后续衍生论文时,理解这个顺序的差异很有价值。
3. 把上百层网络真正训好:容易被忽略的实验设计
3.1 对照实验的逻辑:先证明“问题存在”
论文的实验不是一上来就甩出一个152层的超大模型,而是先做了一个非常清晰的对照:在同一个数据集上,分别训练18层和34层的普通网络,再训练18层和34层的残差网络。结果有两层:普通网络里,34层的训练误差和验证误差都高于18层,这复现了“退化”;残差网络里,34层反而优于18层,并且远远好于同深度的普通网络。
这样的对照设计在今天看依然非常扎实。如果只放一个很深的残差网络效果图,读者可能会怀疑是调参调得好,或者数据增强更猛。但这里除了“是否加入了残差连接”这个变量之外,其他条件完全一致。普通网络越深越差,残差网络越深越好,所以退化问题的缓解可以直接归因到残差结构上。
我当时拿到论文后,也在自己的小数据集上做过同样的事:普通深层网络卡在某个损失值上不去,加上一条shortcut之后,训练loss就从下一轮开始继续往下降。那种“纸上逻辑”和“实测结果”对上的感觉,比看一百遍公式都印象深刻。
3.2 训练设置中那些不起眼的参数
论文里图像识别模型的训练配置,放到今天依然很有参考价值。常见公开配置大概是这样:
| 配置项 | 常见取值 | 作用 |
|---|---|---|
| 批大小 | 256 | 影响BN统计量和梯度稳定性 |
| 初始学习率 | 0.1 | 配合残差保底信号,可在深层网络下较快收敛 |
| 学习率衰减 | 每30轮除以10 | 进入更精细的参数区域 |
| 动量 | 0.9 | 加速收敛,稳定梯度方向 |
| 权重衰减 | 0.0001 | 抑制过拟合,限制参数尺度 |
| 迭代次数 | 约60万次 | 保证充分训练 |
这些参数看起来很普通,却和残差结构有很强的配合关系。初始学习率0.1在老观念里对深层网络可能偏大,但残差块的输出是“F(x) + x”,即使F(x)一开始数值偏大,恒等项x也能提供一个稳定的输出底座。这样网络就不容易一上来就炸,学习率可以开得比较大,收敛速度更快。
BN在这里的作用同样关键。加入BN之后,每一层输出的尺度被拉回标准范围,F(x)的数量级受到控制,加法才足够稳定。我现在迁移这些配置到自己的数据集时,不会完全照搬,但保留了几个原则:学习率用大起点加阶段性衰减,BN参数保持在默认附近,捷径上不做任何多余变换。如果训练震荡,优先检查BN和捷径,而不是急着加梯度裁剪。
3.3 上千层模型的试探:精度没有继续涨,但也没崩
论文还把网络推到了1202层。结果多少有些出人意料:1202层的训练误差和验证误差,和1102层基本持平,没有继续带来明显收益。作者给出了几个可能性,包括数据集规模不够、网络太强容易过拟合、以及超深层网络没有配上对应的正则化策略。
这个结果非常有助于理解残差网络的边界。它解决的是“深层网络能不能训得动”的问题,而不是“越深一定越好”。1202层并不比1102层好,说明深度的收益在那个数据规模下已经饱和。模型的表达能力和训练数据是匹配的,深度红利吃完之后,继续堆层是在给过拟合创造空间。
从工程角度,这个现象提醒我:不要为了深而深。把网络从50层加到100层往往有效,但从300层加到600层在大多数任务上收益极小而代价极大。残差连接是让你敢去试更深网络的前提,但不是让你盲目叠加的理由。真正有价值的调整是配合数据规模选深度,多留一点空间给宽度、感受野和数据增强。
4. 迁移到自己的数据时,残差带给我的真实反馈
4.1 恒等分支必须真的“恒等”
我最早自己复现残差块时,不小心在shortcut路径上也加了一个BN层。当时的想法是“让主分支和残差分支都做一下归一化,应该更稳定”,结果训练曲线反复震荡,调学习率、改初始化都没用。后来逐行查代码才发现捷径上多出来的BN层就是罪魁祸首,去掉之后一切恢复稳定。
原因就是前面说的:残差结构的设计预设是x这条路径不经过任何可学习的变换。只有这样,梯度回传时才有一条固定、不随训练过程抖动的保底通道。一旦给捷径加了BN或激活函数,这条保底通道本身也在漂移,训练早期尤其脆弱。虽然理论上BN可以学习到接近恒等输出的参数,但这个过程既慢又不稳定,完全是给自己找麻烦。
这个经历后来成了我的默认习惯:写残差网络先检查捷径,除了维度匹配用的1x1卷积,不能有任何多余动作。如果loss曲线不对,优先看捷径上的操作,再往下盘卷积参数。
4.2 加了残差不等于一定能训好:调参顺序很重要
我曾在一个小型图像分类任务上,把一个普通四层CNN改成带残差的八层CNN。结果不仅没变好,反而更差。当时我的第一反应是残差结构失效了,随后逐个变量排查发现,问题根本不在于残差,而在于新加的两层让网络变深之后,训练步数和学习率策略还是按原来的节奏来做。模型还没收敛到合适的区域,学习率已经被衰减得过头了。
现在我自己调参的顺序是:先用默认学习率跑一个短训练周期,观察残差网络在前几十个step内能不能把训练loss压下来;如果能顺利下降,再放长训练时间,重新设定学习率衰减轮次;如果loss一开始就降不下去,先查数据预处理和BN状态,而不是立刻换网络结构。
残差连接只解决“梯度流动”的问题,不解决“任务本身是否容易拟合”的问题。任务复杂、数据少的时候,加深网络只会让模型更空虚地拟合噪声。这个认知帮我避免了很多次瞎堆深度的无效劳动。
4.3 结构深度之外,宽度和信息瓶颈同样在起作用
论文的bottleneck结构把计算量控制住之后猛加深度,但通道数过少依然会限制特征表达。后来出现的加宽残差块,用更浅的网络就追平甚至超过了很深的原始残差网络。换句话说,“深”不是成功的唯一维度,宽度同样关键。
我自己做过一个视频动作识别的项目,最开始照着老思路把残差网络从50层换到101层,精度几乎没动。后来把注意力放到每个阶段的通道配置和时间维采样上,反而收获明显提升。道理其实不复杂:残差能保证信号在深层仍然流动,但当下采样过程把关键信息压得面目全非时,shortcut不能“无中生有”地帮你找回信息。
这个经验让我总结出一条判断标准:如果残差网络在做某个任务时,加深之后只有一点点收益,先别急着质疑结构,先看看瓶颈是不是在数据量、通道数或者下采样方式上。把这些问题理清楚,再决定值不值得继续堆深度。
5. 残差思想在今天的定位:边界、演化与重读价值
5.1 被整个领域吸收后的样子
现在几乎很难找到一篇不含“跳过连接”的视觉骨干网络。基于注意力机制的主干结构里,每个子层都普遍带着残差连接;扩散模型的去噪网络,也在不同分辨率的模块之间大量使用残差跳跃。残差思想的适用性,早已远远超出图像识别。
比起一个具体的网络模型,“残差”更像是一种可迁移的训练辅助机制。它不依赖某个特定的卷积核大小,甚至不依赖卷积本身,只要算子支持逐元素相加,它就能用。这也是这篇论文生命力如此之强的原因:一个极其简单的加法,改变了整个领域对深层网络的看法。
5.2 残差的局限:不是所有训练问题的银弹
残差缓解了梯度消失和“退化”问题,但它不直接解决欠拟合、数据不均衡、任务逻辑不合理这些问题。在某些特殊结构里,比如对恒等路径非常敏感的孪生网络、或者需要严格保持输出分布特征的模型,加了残差如果不配合恰当的初始化和归一化,依然可能训练失败。
另外,残差的收益在极深网络中会递减,1202层实验已经给出了证据。如今大规模预训练模型里依然大量使用残差连接,但性能提升更多来自数据规模、计算量和训练策略,而不是单纯把层数堆到几千。理解这一点,就不会把残差当成一个“加了就能赢”的万能开关。
5.3 重读这篇论文,我最有价值的三个收获
第一,先把问题定义清楚,再谈创新。论文对“退化现象”的精准刻画,比直接给出残差结构更值得学习。问题定义决定了方法设计的合理性。第二,用一个极其简单的加法改变优化难度的维度,这种设计比复杂机制更持久。真正有效的东西往往可以被一两句话说清楚。第三,实验安排严谨,先用对照证明问题存在,再用消融说明方案有效,最后才扩大模型规模。这种研究习惯,在任何领域都受用。
如果你也想真正消化这篇论文,我建议别只停留在看公式。自己把基本残差块写出来,在一个小规模公开数据集上跑一组“带与不带残差”的对比,观察训练误差曲线。跑过一次,你才会明白“加法”这两个字对深度学习的意义。论文本身很薄,但反复重读的收获会越来越厚。