残差连接与 LayerNorm 数值推导:接续 bank 词向量的完整计算过程
步骤 0 --- 衔接上一步
上一步自注意力结束后,bank 产生了两个不同的输出向量(句子 A/B)。现在这两个向量要经过残差连接 再经过 LayerNorm,才算完成编码器一层的第一个子层。
论文公式写法(每个子层都长这样):
scss
输出 = LayerNorm( x + Sublayer(x) )
其中 x 是子层的输入,Sublayer(x) 是子层(这里是自注意力)的计算结果。
我们手头的三个向量(d_model = 4)
| d₀ | d₁ | d₂ | d₃ | 说明 | |
|---|---|---|---|---|---|
| x(输入) | 0.80 | 0.40 | 0.60 | -0.10 | ← bank 的原始 embedding |
| Attn_A | 0.44 | 0.12 | 0.12 | 0.31 | ← 句子 A 注意力输出(river) |
| Attn_B | 0.32 | 0.19 | 0.28 | 0.23 | ← 句子 B 注意力输出(street) |
两句 bank 的注意力输出已经不同了。 但还没做残差和 Norm。接下来两步才把这些向量真正"定型"。
步骤 1 --- 残差连接:x + Sublayer(x)
做法极简:把注意力输出 和原始输入逐元素相加。
ini
residual = x + Attn(x)
句子 A 的计算
ini
x = [0.80, 0.40, 0.60, -0.10]
Attn_A = [0.44, 0.12, 0.12, 0.31]
─────────────────────────────
res_A = [1.24, 0.52, 0.72, 0.21] ← 残差结果
句子 B 的计算
ini
x = [0.80, 0.40, 0.60, -0.10]
Attn_B = [0.32, 0.19, 0.28, 0.23]
─────────────────────────────
res_B = [1.12, 0.59, 0.88, 0.13]
注意: 残差加法之后,两个向量的差距缩小了------因为两句共享了同一个 x。这不是坏事,LayerNorm 之后的输出还是会保留足够的差异用于消歧。残差的核心价值在下一步讲。
步骤 2 --- 为什么需要残差连接?
这是一个设计选择,不是数学必须。但它解决了一个训练层数越多越容易失败的根本问题。
问题:梯度消失
神经网络靠反向传播来学习:计算损失对每个参数的梯度,然后更新参数。梯度从最后一层往前传,每经过一层都要乘以那一层的导数。
如果导数普遍小于 1,乘 6 次之后梯度就会变得极小------趋近于 0。参数几乎不更新,网络学不动了。这叫梯度消失。
没有残差 vs. 有残差的梯度对比
没有残差连接
r
输出 = F(x)
∂Loss/∂x = ∂Loss/∂F · ∂F/∂x
梯度只能走"穿过 F"这一条路。如果 F 的导数小,梯度就衰减。6 层叠下来,第 1 层收到的梯度可能已经乘了六次小数 → 接近 0。
| 层 | 梯度保留比例 |
|---|---|
| 层 6 | 1.000 |
| 层 5 | 0.600 |
| 层 4 | 0.360 |
| 层 3 | 0.216 |
| 层 2 | 0.130 |
| 层 1 | 0.078 |
第 1 层梯度只剩 8% --- 几乎学不动
有残差连接
scss
输出 = x + F(x)
∂Loss/∂x = ∂Loss/∂输出 · (1 + ∂F/∂x)
导数里多了一个 +1。即使 ∂F/∂x 趋近于 0,整个导数还剩 1------梯度可以"原封不动"地穿过这一层继续往前传。
| 层 | 梯度保留比例 |
|---|---|
| 层 6 | 1.000 |
| 层 5 | 0.970 |
| 层 4 | 0.941 |
| 层 3 | 0.913 |
| 层 2 | 0.885 |
| 层 1 | 0.858 |
第 1 层梯度还有 86% --- 正常学习
另一个角度理解残差: 每一层不用"从头学一个变换",只需要学"原来的基础上改哪里"------即学习残差(residual = 差量) 。从 0 出发学一个复杂变换很难;在已有结果上学一个小修正容易得多。这也是"residual"这个名字的来历。
残差连接由 ResNet(2015,图像识别)引入,Transformer 直接借用了这个设计。它是让网络能堆到 6 层、甚至后来几百层的关键。
步骤 3 --- Layer Normalization
残差之后,向量要过 LayerNorm 。它把向量重新拉到均值 0、方差 1 附近,然后用两个可学习参数微调。
scss
LayerNorm(z) = γ · (z − μ) / (σ + ε) + β
符号含义
| 符号 | 含义 |
|---|---|
| z | 输入向量(这里是残差结果) |
| μ | z 各维度的均值 |
| σ | z 各维度的标准差 |
| ε | 极小数(防止除以 0,取 1e-6) |
| γ | 可学习缩放参数(初始化为 1) |
| β | 可学习偏移参数(初始化为 0) |
对句子 A 的 res_A = [1.24, 0.52, 0.72, 0.21] 完整计算
第一步:算均值 μ
ini
μ = (1.24 + 0.52 + 0.72 + 0.21) / 4
= 2.69 / 4
= 0.6725
第二步:算方差 σ²,再开方得 σ
ini
各分量减均值: [0.568, -0.153, 0.048, -0.463]
各分量平方: [0.323, 0.023, 0.002, 0.214]
方差 σ² = (0.323 + 0.023 + 0.002 + 0.214) / 4 = 0.1405
标准差 σ = √0.1405 ≈ 0.3748
第三步:归一化 (z − μ) / (σ + ε)
ini
z_norm = [0.568, -0.153, 0.048, -0.463] / 0.3748
≈ [1.516, -0.408, 0.128, -1.235]
第四步:乘 γ 加 β(初始 γ=1,β=0,训练后会变)
ini
LN_A = 1 × [1.516, -0.408, 0.128, -1.235] + 0
= [1.516, -0.408, 0.128, -1.235]
可以拖动滑块感受 γ 和 β 的效果(原 HTML 中有交互滑块,γ 范围 0.1
3,β 范围 -22)当 γ=1.0,β=0.0 时:LN 输出 = [1.516, -0.408, 0.128, -1.235],均值 = 0.000,标准差 = 1.000
步骤 4 --- 为什么需要 LayerNorm?
归一化是为了稳定训练。我们来看不 Norm 会发生什么。
问题:Internal Covariate Shift(内部协变量偏移)
每一层的输出分布会随着训练变化------前一层参数一更新,后一层收到的输入分布就变了,后一层又得重新适应新分布。层数越多,这种"地基不停动"的现象越严重,导致训练不稳、需要极小的学习率、收敛慢。
归一化把每一层的输入强制拉到稳定的分布(μ≈0,σ≈1),让后面的层不用操心"输入的尺度变了"这件事,可以安心学习真正有用的特征变换。
LayerNorm vs BatchNorm --- 为什么 Transformer 选 LayerNorm
| BatchNorm(CNN 常用) | LayerNorm(Transformer 用) | |
|---|---|---|
| 归一化方向 | 同一维度、跨 batch 内所有样本 | 同一个样本、跨所有维度 |
| batch 依赖 | 需要足够大的 batch 才统计稳定 | 和 batch 大小无关 |
| 序列长度 | 序列长度不固定时,跨样本对齐很麻烦 | 和序列长度无关 |
| 推理阶段 | 需要维护"移动均值" | 训练推理行为完全一致,无移动统计量负担 |
一句话区别: BatchNorm 是"纵向看"(同一维度跨样本),LayerNorm 是"横向看"(同一样本跨维度)。序列模型天然偏爱 LayerNorm。
γ 和 β 为什么要可学习?
纯归一化会破坏向量原有的信息------比如某个维度本来就应该比其他维度大。γ 和 β 让模型在归一化之后自己决定把哪个维度缩放到多大、偏移多少。初始化成 γ=1,β=0(相当于"先不做任何额外变换"),然后在训练中学出最合适的值。
一个常见混淆: LayerNorm 里的均值和标准差,是针对一个向量的所有维度算的,不是针对一批样本算的。4 维向量就只用 4 个数来算均值和方差。
步骤 5 --- 合起来:完整子层公式
把三步串成一条流水线,就是论文里那行公式的完整含义:
scss
输出 = LayerNorm( x + Attention(x) )
bank 经过完整子层后的最终向量
| d₀ | d₁ | d₂ | d₃ | 说明 | |
|---|---|---|---|---|---|
| 输入 x | 0.80 | 0.40 | 0.60 | -0.10 | 两句相同 |
| 句子 A | 1.516 | -0.408 | 0.128 | -1.235 | ← river 语境 |
| 句子 B | 1.312 | -0.201 | 0.447 | -1.558 | ← street 语境 |
这两个向量会作为输入进入同一层的第二个子层(FFN) ,再经过同样的"+ LayerNorm"包装,然后传到第 2 层......一共重复 6 次。
一层的完整数据流
scss
x ──→ [Multi-Head Attention] ──→ Attn(x)
x ──────────────────────────────────┐
↓ 相加
x + Attn(x)
↓ LayerNorm
z = LN(x + Attn(x)) ← 传给 FFN
z ──→ [Feed-Forward Network] ──→ FFN(z)
z ──────────────────────────────────┐
↓ 相加
z + FFN(z)
↓ LayerNorm
本层最终输出
残差 + LayerNorm 的分工:
- 残差连接解决梯度消失------让梯度有条"高速公路"跨层直传,使 6 层堆叠成为可能。
- LayerNorm 解决训练不稳------把每层的输入分布固定在 μ=0、σ=1,让各层安心学习而不必追赶分布漂移。
- 两者各司其职,缺一不可。
接下来可以深入哪里
- 下一步:FFN 数值推导
- 转成 PyTorch 代码
- 多头拼接过程