观文听傑

返回

上一篇用 BatchNorm 与 LayerNorm 控制中间激活的尺度,但即使每个变换内部都很稳定,信息仍要依次穿过所有权重、非线性与归一化层。网络越深,这条串行路径越长,优化器就越难找到“多加几层但至少别比浅层差”的参数。

残差连接(Residual Connection)给每个变换旁边增加一条恒等捷径(Identity Shortcut):让新层先学习“应该在输入上改多少”,再把改动加回输入。本文只讲透三个紧密相关的问题:前向为何改学残差、反向梯度如何分流,以及形状改变和预归一化时怎样保住捷径。

01 更深却有更高训练误差,不等于过拟合#

若 20 层网络已经得到可用函数,理论上给它再接若干恒等映射,新网络至少可以复现原来的输出。然而普通串行层要求优化器从随机参数中学出这些恒等映射;这并不容易。深层模型可能出现训练误差也升高的退化问题(Degradation Problem),它和“训练误差低、验证误差高”的过拟合不是一回事。

普通两层变换写成:

xl+1=Hl(xl)=ϕ(Wl,2ϕ(Wl,1xl))x_{l+1}=H_l(x_l)=\phi(W_{l,2}\,\phi(W_{l,1}x_l))

其中 xlRN×Dx_l\in\mathbb{R}^{N\times D} 是第 ll 个块的输入,NN 是 batch 大小,DD 是特征维度;Wl,1,Wl,2RD×DW_{l,1},W_{l,2}\in\mathbb{R}^{D\times D}ϕ\phi 是逐元素非线性。每加一块,前向信号和反向梯度都必须再穿过两次矩阵乘法和非线性。

残差块改成:

xl+1=xl+Fl(xl;θl)x_{l+1}=x_l+F_l(x_l;\theta_l)

FlF_l 是残差分支(Residual Branch),参数集合为 θl\theta_l;捷径分支直接复制 xlx_l。若最优选择是“不改变输入”,只需让 Fl(xl)0F_l(x_l)\approx 0,而不必让一串非线性层精确拟合恒等函数。

02 前向数据到底怎样走?#

                              残差分支 F(x) [N,D]
                         ┌─ Linear/Conv ─ Norm ─ Act ─ Linear/Conv ─┐
                         │                                           ▼
x_l [N,D] ── split ──────┤                                         (+) ──► x_{l+1} [N,D]
                         │                                           ▲
                         └──────── 恒等捷径 identity [N,D] ──────────┘

约束:相加两端必须具有相同 shape、device 和可兼容 dtype。
语义:输出 = 原信息 + 本块学到的修正。
text

这不是把两路特征拼接。相加(Addition)保持维度 DD 不变;拼接(Concatenation)会得到 [N,2D],后续层的参数量和语义都会变化。

连续堆叠时,先忽略块尾额外的非线性:

xL=xl+i=lL1Fi(xi;θi)x_L=x_l+\sum_{i=l}^{L-1}F_i(x_i;\theta_i)

浅层表示 xlx_l 可以沿捷径直接进入深层,残差分支则逐块累积修正。注意 FiF_i 的输入是不断更新的 xix_i,上式并不表示各块彼此独立或可以任意交换顺序。

03 用两个数手算一次前向叠加#

令输入与残差分支为:

x=[12],F(x)=Wx,W=[0.5000.5]x=\begin{bmatrix}1\\2\end{bmatrix},\qquad F(x)=Wx,\qquad W=\begin{bmatrix}0.5&0\\0&-0.5\end{bmatrix}

先算修正量:

F(x)=[0.51]F(x)=\begin{bmatrix}0.5\\-1\end{bmatrix}

普通串行块只输出 F(x)=[0.5,1]F(x)=[0.5,-1]^\top;残差块输出:

y=x+F(x)=[1.51]y=x+F(x)=\begin{bmatrix}1.5\\1\end{bmatrix}

第一个特征被增强 0.5,第二个特征被削弱 1,但原输入并未要求残差分支重新生成。若把 WW 初始化为零,残差块从 y=xy=x 开始;若 WW 很小,它从“接近恒等”开始。

04 反向传播为什么会多出一条直接路径?#

设损失为 L\mathcal{L},上游梯度 gl+1=L/xl+1g_{l+1}=\partial\mathcal{L}/\partial x_{l+1}xl+1x_{l+1} 同形。由链式法则:

Lxl=Lxl+1(I+Flxl)\frac{\partial\mathcal{L}}{\partial x_l} =\frac{\partial\mathcal{L}}{\partial x_{l+1}} \left(I+\frac{\partial F_l}{\partial x_l}\right)

按列向量记法写得更明确:

gl=gl+1+JFl(xl)gl+1g_l=g_{l+1}+J_{F_l}(x_l)^\top g_{l+1}

JFlRD×DJ_{F_l}\in\mathbb{R}^{D\times D} 是残差分支对输入的雅可比矩阵(Jacobian)。第一项 gl+1g_{l+1} 来自恒等捷径,不乘权重;第二项才穿过残差分支。所谓“梯度高速公路”不是梯度永远等于 1,而是每个块都提供了一项不经过该块参数变换的贡献。

回到上面的线性例子,令上游梯度 gy=[1,1]g_y=[1,1]^\top

gx=gy+Wgy=[11]+[0.50.5]=[1.50.5]g_x=g_y+W^\top g_y =\begin{bmatrix}1\\1\end{bmatrix} +\begin{bmatrix}0.5\\-0.5\end{bmatrix} =\begin{bmatrix}1.5\\0.5\end{bmatrix}

若没有捷径,梯度只有 Wgy=[0.5,0.5]W^\top g_y=[0.5,-0.5]^\top。两条路径也可能相互抵消,所以残差连接缓解而不保证消灭梯度消失或爆炸。

05 多层相乘时差异有多大?#

考虑每层只有一个标量,残差函数 Fl(x)=alxF_l(x)=a_lx。普通串行网络的梯度为:

xLx0=l=0L1al\frac{\partial x_L}{\partial x_0}=\prod_{l=0}^{L-1}a_l

残差网络则为:

xLx0=l=0L1(1+al)\frac{\partial x_L}{\partial x_0}=\prod_{l=0}^{L-1}(1+a_l)

若四层都有 al=0.1a_l=-0.1,普通路径的梯度大小是 10410^{-4},残差路径是 0.94=0.65610.9^4=0.6561。这个例子只说明恒等项如何改变乘积,不代表参数越接近零越好:若 al=1a_l=-1,残差因子反而为零;若很多 al>0a_l>0,乘积也可能爆炸。

06 不依赖框架,写出前向与反向本体#

下面直接实现线性残差块。x 的形状为 [N,D]weight[D,D],输出仍为 [N,D]

grad_weight = grad_y.T @ x 只来自残差分支,因为恒等捷径没有参数。grad_x 则是两路梯度相加;若漏掉 grad_x_identity,代码虽然形状正确,却已经退化成普通串行层。

07 训练与推理的完整伪代码#

残差连接本身没有可训练参数,也没有 train/eval 两套行为;差异来自 FlF_l 或投影 PlP_l 内部的 BatchNorm、Dropout 等模块。

08 形状改变时,恒等捷径怎么办?#

相加要求两路输出形状完全一致。卷积网络中,残差分支可能用 stride=2[N,C,H,W] 变为 [N,2C,H/2,W/2],原始输入不能直接相加。

常用投影捷径(Projection Shortcut)是一个 1×1 卷积:

P(x)=Wsx,qquadWsRCout×Cin×1×1P(x)=W_s*x,qquad W_s\in\mathbb{R}^{C_{out}\times C_{in}\times1\times1}

若步幅为 2,它同时改变通道数和空间尺寸:

x [N,64,56,56]
  ├─ residual: 3×3 conv, stride=2 ─ ... ─► [N,128,28,28] ─┐
  └─ shortcut: 1×1 conv, stride=2 ───────► [N,128,28,28] ─┤ (+)


                                                       y [N,128,28,28]
text

投影路径不再是严格恒等映射,也会引入参数和雅可比矩阵。因此只在形状必须改变时使用;同形块优先保留无参数的 nn.Identity() 或直接使用 x

场景捷径是否有参数梯度直接项
同形 [N,C,H,W] → [N,C,H,W]x / IdentityII
改通道或下采样1×1 Conv(stride)JPJ_P
拼接而不是相加concat([x,F(x)])否,但后层变宽不是标准残差加法

09 预归一化为什么更接近“干净捷径”?#

上一篇介绍了 LayerNorm。把归一化放在相加之前还是之后,会改变捷径的数据流。

后归一化(Post-Norm)常写为:

xl+1=LN(xl+Fl(xl))x_{l+1}=\operatorname{LN}(x_l+F_l(x_l))

即使走捷径,信号和梯度仍必须穿过块尾 LayerNorm。预归一化(Pre-Norm)改为:

xl+1=xl+Fl(LN(xl))x_{l+1}=x_l+F_l(\operatorname{LN}(x_l))

此时相加后的 xlx_lxl+1x_{l+1} 是真正的恒等路径,归一化只在残差分支内。它通常让很深的序列模型更容易优化,但也会改变表征尺度、最终归一化位置和训练动态,不能只移动一行代码后假设模型完全等价。

Post-Norm: x ─┬─────────────┐
              └─ F(x) ──────┴─ (+) ─ LN ─► y
                                         捷径仍穿过 LN

Pre-Norm:  x ─┬───────────────────────┐
              └─ LN ─ F(·) ──────────┴─ (+) ─► y
              相加后的主路径保持 identity
text

在原始卷积 ResNet 中还会看到“卷积—BatchNorm—ReLU”的块内顺序以及全预激活(Full Pre-activation)设计。它们和 Transformer 的 Pre-LN 共享“把变换留在残差分支、让相加路径更直接”的思想,但具体归一化轴和块结构不同,不能把名称当作同一个实现。

10 用 PyTorch 2.13 实现可检查的卷积残差块#

当前 PyTorch 2.13 官方 Conv2d 接收 [N,C,H,W]BatchNorm2d 保持同形并按通道归一化。下面显式构造残差分支和必要的投影捷径:

卷积后接 BatchNorm 时把卷积 bias=False,因为归一化层默认已有可学习偏置。nn.init.zeros_(self.bn2.weight) 把最后一次 BatchNorm 的缩放参数 γ\gamma 置零,使同形块初始残差接近零;这是一种可选初始化策略,不应误写成把所有卷积权重都置零。

11 怎样检查梯度确实走了两条路?#

给输入注册 hook,并分别保留捷径与残差输出的梯度:

这里 residual.grad 就是相加节点传入残差分支的上游梯度;x.grad 还多接收一份恒等捷径的梯度。不要期待两个范数满足简单相加,因为梯度是向量,方向不同会增强或抵消。

更严格的数值检查是把同一个 x 分别送入 x + branch(x) 与手写矩阵公式,再用 torch.testing.assert_close 比较前向和 torch.autograd.grad 结果。若自定义算子参与残差分支,可用双精度 torch.autograd.gradcheck 验证局部导数。

12 最常见的错误与最短调试路径#

  1. 相加两端 shape 不同。 先打印两路 [N,C,H,W];通道或步幅变化时再加 1×1 投影,不要靠错误广播“凑巧运行”。
  2. 原地操作破坏反向需要的值。 复杂分支中谨慎使用 inplace=True+=;出现 autograd version 报错时先换成非原地写法。
  3. 把拼接当残差连接。 torch.cat 会改变通道数;若设计确实需要 DenseNet 式拼接,应同步计算后续层输入维度与内存。
  4. 每个同形块都加投影。 这会丢掉无参数恒等路径并增加计算;只在尺寸变化时投影。
  5. 错误地把所有参数初始化为零。 同层神经元会保持对称;只将残差分支末端特定缩放置零,与上一篇的 He/Xavier 初始化配合。
  6. 忽略 BatchNorm 模式。 残差加法不区分训练和推理,但分支中的 BatchNorm 区分;验证必须同时使用 eval()no_grad()
  7. 残差分支压过主干。 记录 ||F(x)|| / (||x|| + ε);若长期极大,检查学习率、初始化、归一化与输入尺度。
  8. 只看总梯度范数。 分层记录输入梯度、分支参数梯度和投影梯度,才能定位第一个异常块。

最短路径:固定随机种子与一个 batch → 打印相加前两路形状、均值和范数 → 暂时令 F(x)=0F(x)=0 验证同形块是否近似恒等 → 对一个标量损失跑 backward → 从输出向输入逐块检查梯度 → 最后再恢复完整训练。

13 残差连接会在哪里失败?#

残差网络仍可能因为学习率过大、归一化统计失配、低精度溢出或分支尺度持续增长而不稳定。恒等捷径也不会自动提升泛化:模型容量增加后,数据不足或正则化不当仍会过拟合。

它与相近结构的区别在于信息合并方式:

  • 高速网络(Highway Network)用可学习门控制变换分支和携带分支,灵活但多出门参数;
  • DenseNet 把此前层输出沿通道拼接,鼓励特征复用,却会让通道数和显存增长;
  • U-Net 的长跳连常连接编码器与解码器的同尺度特征,主要服务空间细节恢复,不等于每个块的局部残差;
  • ReZero 用可学习标量从零缩放残差分支,显式让深层网络从恒等映射开始;
  • 随机深度(Stochastic Depth)训练时随机跳过部分残差分支,是正则化和缩短有效训练路径的方法,不改变推理时完整网络结构。

14 今天真正需要记住什么?#

  1. 残差块学习 F(x)=H(x)xF(x)=H(x)-x,输出 x+F(x)x+F(x);当额外变换不必要时,让残差接近零比让深层串行函数精确学出恒等映射更容易。
  2. 反向梯度为 gx=gy+JFgyg_x=g_y+J_F^\top g_y;恒等项提供直接路径,但仍可能与残差项抵消或一起爆炸。
  3. 相加两路必须同形;尺寸改变时才使用投影捷径,同形块应保留无参数恒等路径。
  4. Pre-Norm 把归一化留在残差分支内,使块间相加路径更直接;它与 Post-Norm 的训练动态并不等价。
  5. 调试残差网络要同时观察形状、分支尺度、逐块梯度和归一化状态,不能只看“用了 ResNet”这个名字。

15 思考题与小练习#

  1. x=[2,1]x=[2,-1]^\topW=[0.250.500.25]W=\begin{bmatrix}-0.25&0.5\\0&0.25\end{bmatrix}y=x+Wxy=x+Wx,手算 yy;若 L/y=[1,2]\partial\mathcal{L}/\partial y=[1,2]^\top,再手算 L/x\partial\mathcal{L}/\partial x,标出两条路径各自的贡献。
  2. 输入为 [N=4,C=32,H=28,W=28],残差分支输出 [4,64,14,14]。设计最小投影捷径,写出卷积核、步幅、权重张量形状和输出形状,并说明为何 nn.Identity() 不能使用。
  3. 各实现一个 PostNormBlockPreNormBlock,共享同一残差分支和初始参数。堆叠 24 层后,对同一标量损失记录每层输入梯度范数;比较曲线前先确认两种块的最终归一化位置。

相关工作#

16 下一篇预告#

残差连接让更深的特征提取器更容易训练,但本文的卷积仍只在局部窗口内共享权重。下一篇将从图像平移出发,拆解卷积的局部连接、权重共享、感受野与输出尺寸,说明 CNN 如何把二维空间结构写进模型。

网络加深后训练误差为何反而升高?残差连接的恒等捷径与梯度分流
https://zwjcode.cn/blog/residual-connection-identity-gradient-path
作者
发布于 2026年9月2日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。