10_add_&_norm_residual_conn

1881 字
9 分钟
10_add_&_norm_residual_conn

残差连接与层归一化

  • 在 Transformer 的每个编码器和解码器层中,所有子模块(如多头注意力和前馈网络)都被一个 Add & Norm 操作包裹。这个组合是为了保证 Transformer 能够稳定训练。
  • 这个操作由两个部分组成:
    • 残差连接 (Add):该操作将子模块的输入 x 直接加到该子模块的输出 Sublayer(x) 上。这一结构解决了深度神经网络中的梯度消失 (Vanishing Gradients) 问题。在反向传播时,梯度可以绕过子模块直接向前传播,从而保证了即使网络层数很深,模型也能得到有效的训练。其公式可以表示为:Output=x+Sublayer(x)Output=x+Sublayer(x) .
    • 层归一化 (Norm):该操作对单个样本的所有特征进行归一化,使其均值为 0 ,方差为 1 。这解决了模型训练过程中的内部协变量偏移 (Internal Covariate Shift) 问题,使每一层的输入分布保持稳定,从而加速模型收敛并提高训练的稳定性。

我们下面从代码层面看看 add&norm 的实现。

0. 初探#

import torch
from transformers.models.bert import BertModel, BertTokenizer
model_name = 'bert-base-uncased'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name, output_hidden_states=True) # 把每一个 layer 的输出都返回出来
model.config

输出为:

BertConfig {
"add_cross_attention": false,
"architectures": [
"BertForMaskedLM"
],
"attention_probs_dropout_prob": 0.1,
"bos_token_id": null,
"classifier_dropout": null,
"dtype": "float32",
"eos_token_id": null,
"gradient_checkpointing": false,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"is_decoder": false,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"output_hidden_states": true,
"pad_token_id": 0,
"position_embedding_type": "absolute",
...
"transformers_version": "5.9.0",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30522
}

这里我们需要格外关注 intermediate_size,它指的是 BERT 每层中 Feed-Forward Network(FFN,前馈网络)内部扩展维度


0.1 在 Transformer Encoder 中的位置#

回顾一下 BERT layer 的结构(我们之后也会看到):

graph LR Input[输入 768维] --> MHA[多头注意力] MHA --> Add1[Add & Norm] Add1 --> FFN[🔶 Feed-Forward Network] FFN --> Add2[Add & Norm] Add2 --> Output[输出 768维]

intermediate_size 就是 FFN 那个橙色块内部的”膨胀宽度”。


0.2 FFN 内部的维度变化#

FFN 由两层全连接网络组成:

输入 (768) ──→ 扩展层 (3072) ──→ 压缩层 (768)
W₁(768→3072) W₂(3072→768)
+ GELU激活函数
# FFN 的伪代码
def feed_forward(x):
# x: (batch, seq_len, 768)
h = x @ W1 # (batch, seq_len, 3072) ← intermediate_size
h = gelu(h) # 非线性激活
output = h @ W2 # (batch, seq_len, 768) 回到 hidden_size
return output

0.3 为什么是 3072 = 768 × 4?#

BERT 的 FFN 使用 4 倍扩展的设计:

BERT-base通用规律
hidden_size768dmodeld_{model}
intermediate_size30724×dmodel4 \times d_{model}
扩展比4x4x

先”膨胀”到 4 倍宽度(给模型更大的容量做非线性变换),再”压缩”回原宽度,这是 Transformer 的标准设计。


0.4 一张图总结#

hidden_size = 768 intermediate_size = 3072
┌──────────┐ ┌──────────────────────────────┐
│ 词向量 │ ── W₁(768×3072) ──→ │ 扩展后的中间表示 │
│ 768维 │ │ 3072维 │
└──────────┘ └──────────────────────────────┘
│ GELU 激活
┌──────────────────────────────┐
┌──────────┐ │ 非线性变换后的结果 │
│ 输出 │ ←── W₂(3072×768) ── │ 3072维 │
│ 768维 │ └──────────────────────────────┘
└──────────┘
中间膨胀到 4 倍 → 给模型更大的"思考空间" → 然后压缩回原维度

💡 通俗理解:intermediate_size 就是 FFN 的”肚子”——先把 768 维撑大到 3072 维来充分运算,再缩回 768 维传给下一层。

model

输出为:

BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(30522, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(token_type_embeddings): Embedding(2, 768)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(encoder): BertEncoder(
(layer): ModuleList(
(0-11): 12 x BertLayer(
(attention): BertAttention(
(self): BertSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(output): BertSelfOutput(
(dense): Linear(in_features=768, out_features=768, bias=True)
(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): BertIntermediate(
...
(pooler): BertPooler(
(dense): Linear(in_features=768, out_features=768, bias=True)
(activation): Tanh()
)
)

回顾我们在 03 节中介绍过的关于 BERT 模型的架构,我们知道 BertLayer 的架构如下:

  • attention: BertAttention
    • self: BertSelfAttention
    • output: BertSelfOutput
  • intermediate: BertIntermediate, 768 -> 4×7684\times 768
  • output: BertOutput, 4×7684\times 768 -> 768

再看看我们一开头给出的 Transformer 架构图,实际上:

  • attention 中的 selfoutput 共同构成了 encoder 中 Multi-Head Attention + Add & Norm 的部分;
  • intermediateoutput 共同构成了 encoder 中 Feed Forward + Add & Norm 的部分;
MLP

MLP(Multi-Layer Perceptron,多层感知机)就是由多层全连接层(Linear / Dense)堆叠而成的前馈神经网络,每层之间夹一个非线性激活函数。

这里的 intermediateoutput 实际上是两层 MLP. 我们可以把它们拆开来看:

第 1 层: BertIntermediate 第 2 层: BertOutput
┌──────────────────────┐ ┌──────────────────────────┐
│ Linear(768 → 3072) │ │ Linear(3072 → 768) │
│ ↓ │ │ ↓ │
│ GELU 激活函数 │ │ Dropout │
│ ↓ │ │ ↓ │
│ 输出: (batch, 3072) │ ───→ │ Add (残差: + attention_out)│
└──────────────────────┘ │ ↓ │
│ LayerNorm │
│ ↓ │
│ 输出: (batch, 768) │
└──────────────────────────┘

所以:

MLP(两层)
┌─────────────────────────────────────────────┐
│ │
│ attention_out ──→ Linear(768→3072) ──→ GELU ──→ Linear(3072→768) ──→ ...
│ (输入 x) ↑ BertIntermediate ↑ ↑ BertOutput ↑
│ └── 第 1 层 ────────┘ └── 第 2 层 ───┘
│ │
└─────────────────────────────────────────────┘
FFN = 两层 MLP

FFN 用两层 MLP 的主要原因是,如果只有一层 Linear(768→768),那就是一个简单的线性变换,没有任何非线性能力。模型学到的只是输入的线性组合,表达能力很弱。

而如果用 768 -> 3072 -> 768 的两层结构:

  1. 先膨胀:768 → 3072,给模型更大的”容量”去学习复杂模式
  2. 加非线性:GELU 激活,让模型能学到非线性关系
  3. 再压缩:3072 → 768,把信息压缩回原维度,传给下一层

1. model output#

test_sentence = 'this is a test sentence'
model_inputs = tokenizer(test_sentence, return_tensors='pt')
model.eval()
with torch.no_grad():
outputs = model(**model_inputs)
outputs.keys()

输出为:

odict_keys(['last_hidden_state', 'pooler_output', 'hidden_states'])
outputs[2][0] # embeddings layer

输出为:

tensor([[[ 0.1686, -0.2858, -0.3261, ..., -0.0276, 0.0383, 0.1640],
[-0.6485, 0.6739, -0.0932, ..., 0.4475, 0.6696, 0.1820],
[-0.6270, -0.0633, -0.3143, ..., 0.3427, 0.4636, 0.4594],
...,
[ 0.6010, -0.6970, -0.2001, ..., 0.2960, 0.2060, -1.7181],
[ 0.8323, 0.2878, 0.0021, ..., 0.2628, -1.1310, -1.2708],
[-0.1481, -0.2948, -0.1690, ..., -0.5009, 0.2544, -0.0700]]])

关于 BERT 模型的 outputs,我们在 07 节中进行了详尽的探索,此处不再赘述。总之,outputs[2] 表示的是所有 hidden layers 的输出(包括第 0 层 embedding layer 与 12 个 transformer layers).

outputs[2][1] # first transformer layer

输出为:

tensor([[[ 0.1556, -0.0080, -0.0707, ..., 0.0786, 0.0213, 0.0616],
[-0.5333, 0.5799, 0.1044, ..., 0.0241, 0.4888, 0.0161],
[-1.0609, -0.3058, -0.5043, ..., 0.1874, 0.2874, 0.4032],
...,
[ 0.8206, -0.6656, -0.7054, ..., 0.1347, 0.1117, -1.9040],
[ 1.1128, 0.6603, -0.1509, ..., 0.3253, -1.0006, -1.9106],
[-0.0736, 0.0346, 0.0376, ..., -0.4506, 0.6585, -0.0502]]])

看完了我们获得的结果,下面我们来从零把 BERT layer 串起来。

2. from scratch#

  • BertLayer
    • attention: BertAttention
      • self: BertSelfAttention
      • output: BertSelfOutput
    • intermediate: BertIntermediate, 768 -> 4×7684\times 768
    • output: BertOutput, 4×7684\times 768 -> 768
embeddings = outputs[2][0]
layer = model.encoder.layer[0]

2.1 第一次 add & norm,发生在 MHA 内部#

MHA_output = layer.attention.self(embeddings)
MHA_output

输出为:

(tensor([[[ 0.2979, 0.0801, -0.0037, ..., -0.0142, 0.1290, 0.0828],
[ 0.3935, 0.1356, -0.0920, ..., 0.0211, 0.1677, 0.0011],
[ 0.1696, 0.1449, -0.1039, ..., 0.1604, 0.2172, 0.0310],
...,
[-0.0617, 0.1968, -0.0669, ..., 0.1126, 0.1933, -0.0204],
[-0.2835, 0.1495, -0.0021, ..., 0.0973, 0.1865, -0.0636],
[ 0.2575, 0.1120, -0.1008, ..., 0.0175, 0.1508, 0.0878]]],
grad_fn=<ViewBackward0>),
None)

这里的 MHA_output 对应的就是架构图中 encoder 第一个 Multi-Head Self-Attention 的输出。

attention_output = layer.attention.output(MHA_output[0], embeddings) # 注意力输出 + 输入的残差连接
attention_output

输出为:

tensor([[[ 0.6143, -0.2061, -0.5682, ..., 0.2557, 0.1224, 0.3388],
[-0.6751, 1.0598, -0.2210, ..., 0.6263, 1.1682, 0.1998],
[-0.5969, 0.1463, -0.5549, ..., 0.5272, 0.5071, 0.6439],
...,
[ 1.3291, -1.1190, -0.1960, ..., 0.6164, 0.4293, -2.4704],
[ 1.8784, 0.5158, 0.1902, ..., 0.5298, -1.3812, -2.0257],
[ 0.7376, -0.7000, -0.0992, ..., -0.9356, 0.5831, -0.4610]]],
grad_fn=<NativeLayerNormBackward0>)
MHA_output[0]

MHA_output[0] 实际上是一个 tuple,包含两个元素:

MHA_output = layer.attention.self(embeddings)
# MHA_output = (
# tensor([[...]]), ← [0] 注意力计算结果,形状 (1, seq_len, 768)
# None ← [1] 注意力权重矩阵,None 因为没请求
# )

这里的 attention_output 就是结构图中经过第一个 Add & Norm 块后的输出。

2.2 第二次 add & norm,发生在 MLP 内部#

MLP1 = layer.intermediate(attention_output)
MLP1.shape # torch.Size([1, 7, 3072])
MLP2 = layer.output(MLP1, attention_output) # MLP 输出 + 输入的残差连接
MLP2.shape # torch.Size([1, 7, 768])
MLP2

输出为:

tensor([[[ 0.1556, -0.0080, -0.0707, ..., 0.0786, 0.0213, 0.0616],
[-0.5333, 0.5799, 0.1044, ..., 0.0241, 0.4888, 0.0161],
[-1.0609, -0.3058, -0.5043, ..., 0.1874, 0.2874, 0.4032],
...,
[ 0.8206, -0.6656, -0.7054, ..., 0.1347, 0.1117, -1.9040],
[ 1.1128, 0.6603, -0.1509, ..., 0.3253, -1.0006, -1.9106],
[-0.0736, 0.0346, 0.0376, ..., -0.4506, 0.6585, -0.0502]]],
grad_fn=<NativeLayerNormBackward0>)

可以看到这里的结果与之前 outputs[2][1] 的结果是一模一样的。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

10_add_&_norm_residual_conn
https://github.com/chunhuizhang/bilibili_vlogs/
作者
HAC
发布于
2026-06-22
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
HAC
观之非易,行且克难
Greetings
欢迎来到我的博客!这里主要分享我的学习笔记与兴趣爱好。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
5
标签
13
总字数
79,889
运行时长
0
最后活动
0 天前

文章目录