1、编码器介绍

我们现在将视野拉高,系统、全面、深入地讲解 Transformer 编码器(Encoder) 的完整结构、设计动机、工作机制、数学细节、工程实现要点,以及它在不同模型中的演化与应用。

7 个核心维度 展开,力求做到: ✅ 概念清晰|✅ 技术严谨|✅ 覆盖全面|✅ 层次分明


一、整体架构:编码器是什么?

Transformer 编码器 是一个端到端的神经网络模块,其目标是:

将一个离散的 token 序列 x = ( x 1 , x 2 , . . . , x T ) x = (x_1, x_2, ..., x_T) x=(x1,x2,...,xT) 映射为一组上下文感知的连续向量表示 H = ( h 1 , h 2 , . . . , h T ) ∈ R T × d model H = (h_1, h_2, ..., h_T) \in \mathbb{R}^{T \times d_{\text{model}}} H=(h1,h2,...,hT)RT×dmodel,其中每个 h i h_i hi 都融合了整个输入序列的语义信息。

📐 标准结构(以原始 Transformer 为基础)

Input: [x₁, x₂, ..., x_T]      ← Token IDs

Step 1: Embedding + Positional Encoding
    ↓
X ∈ ℝ^{T × d_model}            ← 初始表示(可训练嵌入 + 位置编码)

Step 2: Stack of N Encoder Layers
    ↓
Layer 1 → Layer 2 → ... → Layer N

Step 3: Final Output
    ↓
H ∈ ℝ^{T × d_model}            ← 上下文化表示(Contextualized Embeddings)
  • 典型层数 N

    • 原始 Transformer(机器翻译):6 层
    • BERT-base:12 层, d model = 768 d_{\text{model}} = 768 dmodel=768
    • BERT-large:24 层, d model = 1024 d_{\text{model}} = 1024 dmodel=1024
    • T5-small:6 层;T5-XXL:24 层
  • 所有编码器层结构相同,但参数不共享(每层有独立的 MHSA 和 FFN 权重)。


二、输入处理:从 Token 到向量(Embedding + Positional Encoding)

编码器的输入不是原始字符串,而是经过两步转换的稠密向量:

  1. Token Embedding
  • 将每个 token ID x i ∈ { 1 , 2 , . . . , V } x_i \in \{1, 2, ..., V\} xi{1,2,...,V}(V 为词表大小)映射为 d model d_{\text{model}} dmodel 维向量。
  • 实现方式:nn.Embedding(V, d_model)
  • 这是一个可学习的查找表,在预训练中与模型其他部分联合优化。
  • 注意:这不同于 Word2Vec/GloVe 等静态嵌入,它是任务自适应、上下文无关的初始表示(后续由编码器层赋予上下文)。
  1. Positional Encoding(位置编码)
  • 问题:自注意力机制对输入顺序不敏感(即 permutation-invariant),无法区分 “猫追狗” 和 “狗追猫”。
  • 解决方案:显式注入位置信息。

▶ 原始 Transformer 使用固定正弦位置编码
P E ( p o s , 2 i ) = sin ⁡ ( p o s 10000 2 i / d model ) , P E ( p o s , 2 i + 1 ) = cos ⁡ ( p o s 10000 2 i / d model ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right), \quad PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) PE(pos,2i)=sin(100002i/dmodelpos),PE(pos,2i+1)=cos(100002i/dmodelpos)

  • 优点:可泛化到训练时未见的序列长度。
  • 缺点:无法学习位置模式。

▶ 现代模型(BERT、RoBERTa、T5)普遍使用可学习位置嵌入

  • 创建一个 nn.Embedding(max_seq_len, d_model),位置索引作为输入。
  • 优点:更灵活,能捕捉特定位置的语义(如句首/句尾特殊性)。
  • 缺点:最大长度受限(如 BERT 限制为 512)。
  1. 最终输入表示

X = E token + E position ∈ R T × d model X = E_{\text{token}} + E_{\text{position}} \in \mathbb{R}^{T \times d_{\text{model}}} X=Etoken+EpositionRT×dmodel

  • 两者直接相加(而非拼接),保持维度一致。
  • 有些模型(如 ALBERT)会在输入第一层前加 LayerNorm,但非主流。

三、核心处理:N 层编码器层的级联精炼

这是编码器的“心脏”。每一层都执行相同的计算流程,但逐层深化语义

🔁 单层计算流程(注意:原始为 Post-LN,现代多用 Pre-LN)

▶ 原始 Transformer(Post-LN):

# 子层 1
attn_out = self_attn(x, x, x, mask=src_mask)
x = LayerNorm(x + Dropout(attn_out))

# 子层 2
ffn_out = ffn(x)
x = LayerNorm(x + Dropout(ffn_out))

▶ 现代主流(Pre-LN,如 T5、DeBERTa):

# 子层 1
x_norm = LayerNorm(x)
attn_out = self_attn(x_norm, x_norm, x_norm, mask=src_mask)
x = x + Dropout(attn_out)

# 子层 2
x_norm = LayerNorm(x)
ffn_out = ffn(x_norm)
x = x + Dropout(ffn_out)

⚠️ 重要澄清

  • BERT 实际使用的是 Post-LN(配合学习率 warmup)
  • Pre-LN 因训练更稳定,已成为新模型首选(无需 warmup)

🌐 信息流分析

步骤作用是否跨 token
MHSA计算 token 间相关性,聚合全局信息✅ 是(唯一跨 token 通信机制)
FFN对每个 token 独立进行非线性变换❌ 否(但输入已是上下文感知的)
残差连接保留原始信息,防止梯度消失
LayerNorm稳定激活分布,加速收敛

🧠 为什么需要多层堆叠?

单层只能捕获浅层交互。堆叠 N 层形成层次化表征

层级学习内容实证证据
底层(1–3 层)词法、局部搭配、基本语法(如 n-gram)注意力头聚焦邻近 token
中层(4–8 层)句法结构(主谓一致、依存关系、成分边界)出现“句法头”(syntactic heads)
高层(9–12+ 层)语义角色、指代消解、任务意图、情感注意力分布稀疏且语义导向

📊 实验支持:

  • BERTology 研究(Tenney et al., 2019)通过探针实验验证了这种分层特性。
  • 注意力可视化显示底层关注局部,高层关注语义相关词(如动词-宾语)。

四、输出表示:上下文化嵌入的性质与用途

编码器的最终输出 H = [ h 1 , . . . , h T ] H = [h_1, ..., h_T] H=[h1,...,hT] 具有以下关键特性:

✅ 核心性质

  1. 上下文感知(Contextualized):同一词在不同句子中向量不同。
  2. 全局依赖(Global Dependency):每个 h i h_i hi 理论上可访问所有 x j x_j xj(通过 MHSA)。
  3. 向量分布集中(Low Anisotropy):向量倾向于聚集在低维锥体中(可通过后处理改善)。
  4. 任务通用(Task-Agnostic):预训练编码器可迁移到多种下游任务。

🛠️ 下游任务如何使用这些表示?

任务类型使用方式示例
句子级分类取特殊 token(如 BERT 的 [CLS])的输出向量情感分析、NLI
Token 级标注每个 token 的输出接分类头NER、POS tagging
问答(抽取式)用两个向量分别预测答案起止位置SQuAD
语义相似度对两个句子的 [CLS] 向量计算余弦相似度STS-B
作为解码器输入在 Seq2Seq 中,H 作为 cross-attention 的 Key/ValueT5、BART

💡 注意[CLS] 是 BERT 引入的特殊 token,原始 Transformer 编码器没有此设计。它之所以有效,是因为在每层都能通过自注意力看到所有 token。


五、关键技术细节与工程考量

  1. Masking 策略
  • Padding Mask:屏蔽 <pad> token,避免无效信息干扰。
    • 形状:(B, T) → 扩展为 (B, 1, T) 用于注意力
    • 实现:在 softmax 前将 mask 位置设为 -inf
  • 无因果掩码(Causal Mask):编码器不需要像解码器那样遮盖未来信息,因为它是双向建模
  1. 参数规模估算

以 BERT-base 为例(12 层):

  • Embedding 层: V × d model ≈ 30 k × 768 ≈ 23 M V \times d_{\text{model}} ≈ 30k \times 768 ≈ 23M V×dmodel30k×76823M
  • 每层 MHSA: 4 × d model 2 ≈ 4 × 768 2 ≈ 2.4 M 4 \times d_{\text{model}}^2 ≈ 4 \times 768^2 ≈ 2.4M 4×dmodel24×76822.4M(Q/K/V/O 投影)
  • 每层 FFN: 2 × d model × d ff ≈ 2 × 768 × 3072 ≈ 4.7 M 2 \times d_{\text{model}} \times d_{\text{ff}} ≈ 2 \times 768 \times 3072 ≈ 4.7M 2×dmodel×dff2×768×30724.7M
  • 总参数 ≈ 110M

📌 编码器是 Transformer 中参数最密集的部分(尤其 FFN 占比超 60%)。

  1. 计算复杂度
  • MHSA O ( T 2 ⋅ d model ) O(T^2 \cdot d_{\text{model}}) O(T2dmodel)(因注意力矩阵为 T × T T \times T T×T
  • FFN O ( T ⋅ d model ⋅ d ff ) O(T \cdot d_{\text{model}} \cdot d_{\text{ff}}) O(Tdmodeldff)
  • 总复杂度:随序列长度平方增长 → 长文本瓶颈

🔜 这也是 Longformer、BigBird 等模型引入稀疏注意力的原因。


六、在不同模型中的演化与变体

模型编码器特点关键差异
原始 Transformer6 层,Post-LN,正弦位置编码用于机器翻译,需配合解码器
BERT12/24 层,Post-LN + warmup,可学习位置嵌入,含 [CLS]/[SEP]首个大规模预训练纯编码器
RoBERTa同 BERT 架构,移除 NSP,更大 batch/数据更强的 MLM 预训练
ALBERT参数共享(所有层用同一组权重)显著减少参数量
DeBERTa内容与位置向量分离,引入相对位置偏置更精准的位置建模
T5Pre-LN,可学习位置嵌入,统一文本到文本框架编码器仅用于理解输入

🌟 趋势:从 Post-LN → Pre-LN;从固定位置编码 → 可学习/相对位置;从绝对位置 → 相对位置增强。


七、常见误解澄清

误解正确理解
“编码器输出的是词向量”❌ 它输出的是上下文向量,同一词在不同句子中不同
“FFN 也能让 token 互相通信”❌ FFN 是 position-wise 的,只有 MHSA 能跨 token 通信
“位置编码只是加噪声”❌ 它是结构性信号,对模型理解顺序至关重要
“更多层一定更好”⚠️ 存在收益递减;深层模型需配合 deepnorm 等技巧
“编码器能生成文本”❌ 编码器是理解模块;生成需解码器(或仅用解码器如 GPT)

✅ 终极总结:编码器的本质

Transformer 编码器是一个由可学习嵌入、位置编码和 N 个精心设计的编码器层组成的深度神经网络。它通过多头自注意力机制建立 token 间的全局依赖,并通过前馈网络进行非线性特征提炼,在残差连接和层归一化的保障下,逐层构建从词法到语义的层次化上下文表示。其输出是一组动态、上下文感知的向量,构成了现代自然语言理解系统的语义基石。

你可以将其视为:

  • 语义蒸馏器:把原始符号转化为富含意义的向量
  • 信息整合引擎:将局部观察升华为全局理解
  • 预训练知识容器:在海量文本中学习语言的统计规律与结构

正是这个看似简单的“编码器”,支撑了从 BERT 到 T5 再到今天大模型的绝大多数 NLP 突破。理解它,就理解了现代 NLP 的半壁江山。


2、编码器数据流向

🧭 总览:数据流向的宏观路径

Raw Tokens 
    ↓ (Tokenization)
Token IDs: [w₁, w₂, ..., w_T] ∈ ℤ^T
    ↓ (Embedding Lookup)
Token Embeddings: E_tok ∈ ℝ^{T × d_model}
    ↓ (+ Positional Encoding)
Input Representation: X⁰ ∈ ℝ^{T × d_model}
    ↓ (Layer 1)
X¹ ∈ ℝ^{T × d_model}
    ↓ (Layer 2)
X² ∈ ℝ^{T × d_model}
    ⋮
    ↓ (Layer N)
Xᴺ ∈ ℝ^{T × d_model}
    ↓ (Final LayerNorm)        ← 新增这一步!
H ∈ ℝ^{T × d_model}           ← Final Contextualized Output

关键性质

  • 所有中间表示 保持相同形状(T, d_model)
  • 每一层都是 等维映射(isometric transformation)
  • 信息通过 自注意力(跨 token) + FFN(单 token 非线性) 不断精炼
  • 最终输出 H 经过顶层 LayerNorm,确保表示分布稳定

下面我们从 输入 → 嵌入 → 位置编码 → 第1层 → … → 第N层 → 输出 逐步展开。


🔹 阶段 1:原始输入 → Token IDs

  • 输入:字符串序列,如 "The cat sat on the mat."

  • 处理:经 tokenizer(如 WordPiece、BPE)切分为 subword tokens:

    tokens = ["[CLS]", "the", "cat", "sat", "on", "the", "mat", ".", "[SEP]"]
    
  • 输出:token ID 序列
    w = [ w 1 , w 2 , . . . , w T ] ∈ { 0 , 1 , . . . , V − 1 } T \mathbf{w} = [w_1, w_2, ..., w_T] \in \{0, 1, ..., V-1\}^T w=[w1,w2,...,wT]{0,1,...,V1}T

    • T T T:序列长度(含特殊 token)
    • V V V:词表大小(如 BERT: 30522)

📌 注意:此阶段无数值计算,仅为符号到整数的映射。


🔹 阶段 2:Token Embedding(词嵌入)

  • 操作:查表(embedding lookup)

  • 模块nn.Embedding(V, d_model)

  • 数学
    E tok = Embed ( w ) = [ e w 1 , e w 2 , . . . , e w T ] ⊤ ∈ R T × d model \mathbf{E}_{\text{tok}} = \text{Embed}(\mathbf{w}) = [\mathbf{e}_{w_1}, \mathbf{e}_{w_2}, ..., \mathbf{e}_{w_T}]^\top \in \mathbb{R}^{T \times d_{\text{model}}} Etok=Embed(w)=[ew1,ew2,...,ewT]RT×dmodel

    • 每个 e w i ∈ R d model \mathbf{e}_{w_i} \in \mathbb{R}^{d_{\text{model}}} ewiRdmodel 是可学习向量
  • 形状变化

    • 输入:(T,)(整数索引)
    • 输出:(T, d_model)(浮点向量)

💡 此时每个 token 向量彼此独立,无上下文、无顺序信息。


🔹 阶段 3:Positional Encoding(位置编码)

  • 目的:注入序列顺序信息(因 MHSA 本身对顺序不敏感)

  • 方式:向量加法(非拼接)

  • 数学
    X ( 0 ) = E tok + E pos ∈ R T × d model \mathbf{X}^{(0)} = \mathbf{E}_{\text{tok}} + \mathbf{E}_{\text{pos}} \in \mathbb{R}^{T \times d_{\text{model}}} X(0)=Etok+EposRT×dmodel

    • E pos = [ p e 1 , p e 2 , . . . , p e T ] ⊤ \mathbf{E}_{\text{pos}} = [pe_1, pe_2, ..., pe_T]^\top Epos=[pe1,pe2,...,peT],其中 p e t ∈ R d model pe_t \in \mathbb{R}^{d_{\text{model}}} petRdmodel

▶ 两种主流实现:

类型公式/实现特点
正弦编码(原始 Transformer) p e ( t , 2 i ) = sin ⁡ ( t / 10000 2 i / d ) pe_{(t,2i)} = \sin(t / 10000^{2i/d}) pe(t,2i)=sin(t/100002i/d), p e ( t , 2 i + 1 ) = cos ⁡ ( ⋯   ) pe_{(t,2i+1)} = \cos(\cdots) pe(t,2i+1)=cos()固定、可外推、不可学习
可学习位置嵌入(BERT/T5)nn.Embedding(max_len, d_model),输入位置索引 t可训练、更灵活、长度受限
  • 形状不变:仍为 (T, d_model)
  • 语义意义:现在每个向量同时包含 “是什么词” + “在什么位置”

⚠️ 注意:没有 LayerNorm 在此阶段(除非像 ALBERT 那样显式添加,但非常规)


🔹 阶段 4:进入第 1 个编码器层(以 Pre-LN 架构为例)

我们以 现代主流 Pre-LN 架构 为例(如 T5、DeBERTa),因其训练更稳定。
(若用 Post-LN,仅 LayerNorm 位置不同,数据流本质一致)

设当前输入为 X ( l − 1 ) ∈ R T × d model \mathbf{X}^{(l-1)} \in \mathbb{R}^{T \times d_{\text{model}}} X(l1)RT×dmodel,输出为 X ( l ) \mathbf{X}^{(l)} X(l)


🔄 子步骤 4.1:LayerNorm(MHSA 前)

  • 操作:对每个 token 向量独立做 Layer Normalization

  • 数学(对每个位置 t t t):
    x ~ t = LayerNorm ( x t ( l − 1 ) ) = γ ⋅ x t ( l − 1 ) − μ t σ t 2 + ϵ + β \tilde{\mathbf{x}}_t = \text{LayerNorm}(\mathbf{x}_t^{(l-1)}) = \gamma \cdot \frac{\mathbf{x}_t^{(l-1)} - \mu_t}{\sqrt{\sigma_t^2 + \epsilon}} + \beta x~t=LayerNorm(xt(l1))=γσt2+ϵ xt(l1)μt+β

    • μ t , σ t 2 \mu_t, \sigma_t^2 μt,σt2:在 d model d_{\text{model}} dmodel 维上计算均值/方差
    • γ , β ∈ R d model \gamma, \beta \in \mathbb{R}^{d_{\text{model}}} γ,βRdmodel:可学习缩放/偏移
  • 形状:仍为 (T, d_model)

  • 作用:稳定激活分布,加速训练

📌 注意:LayerNorm 是 position-wise 的,不跨 token。


🔄 子步骤 4.2:多头自注意力(MHSA)

  • 输入 X ~ = [ x ~ 1 , . . . , x ~ T ] ⊤ ∈ R T × d model \tilde{\mathbf{X}} = [\tilde{\mathbf{x}}_1, ..., \tilde{\mathbf{x}}_T]^\top \in \mathbb{R}^{T \times d_{\text{model}}} X~=[x~1,...,x~T]RT×dmodel

  • 投影(线性变换):
    Q = X ~ W Q ∈ R T × d k K = X ~ W K ∈ R T × d k V = X ~ W V ∈ R T × d v \begin{aligned} \mathbf{Q} &= \tilde{\mathbf{X}} \mathbf{W}^Q \in \mathbb{R}^{T \times d_k} \\ \mathbf{K} &= \tilde{\mathbf{X}} \mathbf{W}^K \in \mathbb{R}^{T \times d_k} \\ \mathbf{V} &= \tilde{\mathbf{X}} \mathbf{W}^V \in \mathbb{R}^{T \times d_v} \end{aligned} QKV=X~WQRT×dk=X~WKRT×dk=X~WVRT×dv

    • 通常 d k = d v = d model / h d_k = d_v = d_{\text{model}} / h dk=dv=dmodel/h h h h 为头数(如 h=12, d_k=64)
    • W Q , W K , W V ∈ R d model × d k \mathbf{W}^Q, \mathbf{W}^K, \mathbf{W}^V \in \mathbb{R}^{d_{\text{model}} \times d_k} WQ,WK,WVRdmodel×dk:可学习权重
  • 多头注意力计算(对每个头 i i i):
    head i = softmax ( Q i K i ⊤ d k + M ) V i \text{head}_i = \text{softmax}\left( \frac{\mathbf{Q}_i \mathbf{K}_i^\top}{\sqrt{d_k}} + \mathbf{M} \right) \mathbf{V}_i headi=softmax(dk QiKi+M)Vi

    • M ∈ R T × T \mathbf{M} \in \mathbb{R}^{T \times T} MRT×T实际实现中,padding mask 通常为 (B, 1, T),广播后作用于 (B, h, T, T) 的注意力分数
      • 有效位置:0,屏蔽位置:-∞
    • softmax 沿 最后一维(key 维度) 归一化 → 得到注意力权重
  • 拼接与输出投影
    MultiHead = Concat ( head 1 , . . . , head h ) W O ∈ R T × d model \text{MultiHead} = \text{Concat}(\text{head}_1, ..., \text{head}_h) \mathbf{W}^O \in \mathbb{R}^{T \times d_{\text{model}}} MultiHead=Concat(head1,...,headh)WORT×dmodel

    • W O ∈ R d model × d model \mathbf{W}^O \in \mathbb{R}^{d_{\text{model}} \times d_{\text{model}}} WORdmodel×dmodel
  • Dropout
    A = Dropout ( MultiHead ) ∈ R T × d model \mathbf{A} = \text{Dropout}(\text{MultiHead}) \in \mathbb{R}^{T \times d_{\text{model}}} A=Dropout(MultiHead)RT×dmodel

  • 残差连接
    X temp ( l ) = X ( l − 1 ) + A \mathbf{X}^{(l)}_{\text{temp}} = \mathbf{X}^{(l-1)} + \mathbf{A} Xtemp(l)=X(l1)+A

关键信息流

  • 跨 token 通信发生在此:每个输出 token 是所有输入 token 的加权和
  • mask 决定可见范围:编码器使用 padding mask,所有有效 token 互相可见(双向)
  • 输出形状不变(T, d_model)

🔄 子步骤 4.3:LayerNorm(FFN 前)

  • X temp ( l ) \mathbf{X}^{(l)}_{\text{temp}} Xtemp(l) 做 LayerNorm(为 FFN 准备):
    X ~ ffn = LayerNorm ( X temp ( l ) ) \tilde{\mathbf{X}}_{\text{ffn}} = \text{LayerNorm}(\mathbf{X}^{(l)}_{\text{temp}}) X~ffn=LayerNorm(Xtemp(l))

  • 形状:(T, d_model)

📌 注意:Pre-LN 架构中,每层仅有两个 LayerNorm,分别位于 MHSA 和 FFN 之前


🔄 子步骤 4.4:前馈网络(FFN)

  • 结构:两层 MLP,中间扩维
    FFN ( x ) = W 2 ⋅ GELU ( W 1 x + b 1 ) + b 2 \text{FFN}(\mathbf{x}) = \mathbf{W}_2 \cdot \text{GELU}(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 FFN(x)=W2GELU(W1x+b1)+b2

    • W 1 ∈ R d ff × d model \mathbf{W}_1 \in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}} W1Rdff×dmodel,通常 d ff = 4 × d model d_{\text{ff}} = 4 \times d_{\text{model}} dff=4×dmodel
    • W 2 ∈ R d model × d ff \mathbf{W}_2 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}} W2Rdmodel×dff
    • GELU / ReLU 引入非线性
    • 在第一层之后其实还有个 Dropout,但 Dropout 不属于模型组件,也不是核心内容,所以公式中并未体现出来
  • 计算方式对每个 token 独立应用(position-wise)
    F = FFN ( X ~ ffn ) ∈ R T × d model \mathbf{F} = \text{FFN}(\tilde{\mathbf{X}}_{\text{ffn}}) \in \mathbb{R}^{T \times d_{\text{model}}} F=FFN(X~ffn)RT×dmodel

  • Dropout + 残差
    X ( l ) = X temp ( l ) + Dropout ( F ) \mathbf{X}^{(l)} = \mathbf{X}^{(l)}_{\text{temp}} + \text{Dropout}(\mathbf{F}) X(l)=Xtemp(l)+Dropout(F)

关键信息流

  • 无跨 token 通信:FFN 是“单兵作战”
  • 但输入已是上下文感知的,所以 FFN 是在“已有共识”基础上做个人决策
  • 非线性能力主要来自此处(MHSA 是线性加权)

🔁 阶段 5:重复 N 次 → 层间信息演化

上述过程在 每一层重复,但参数不共享,因此每层学习不同的表示模式。

📈 信息演化示例(以句子 “The cat sat on the mat.” 为例)

“cat” 的表示变化“mat” 的表示变化
X⁰(输入)仅含 “cat” 词义 + 位置2仅含 “mat” 词义 + 位置6
X¹(第1层后)知道邻近词 “The”, “sat”知道邻近词 “the”, “.”
X³(第3层后)知道主语是 “cat”,谓语是 “sat”知道 “mat” 是宾语,被 “on” 修饰
X⁶(第6层后)理解 “cat” 是动作发起者理解 “mat” 是地点载体
X¹²(最终)向量可用于判断情感/实体类型向量可用于问答/指代消解

🔬 实证支持:通过 probing classifier 或 attention rollout 可观测到这种层次化。


🔚 阶段 6:最终输出 H \mathbf{H} H

  • 计算
    H = LayerNorm ( X ( N ) ) \mathbf{H} = \text{LayerNorm}(\mathbf{X}^{(N)}) H=LayerNorm(X(N))
    其中 X ( N ) \mathbf{X}^{(N)} X(N) 是第 N N N 个编码器层的输出。

  • 形状 H ∈ R T × d model \mathbf{H} \in \mathbb{R}^{T \times d_{\text{model}}} HRT×dmodel

  • 性质

    • 每个 h t \mathbf{h}_t ht 都融合了整句话的信息
      → 不再是孤立的词向量,而是根据整个输入序列动态生成的上下文表示。
      (例如:“bank” 在 “river bank” 和 “bank account” 中会得到不同的 h t \mathbf{h}_t ht
    • 向量空间具有 语义连续性:相似语境下的 token 会产生相近的向量
    • 已通过最终 LayerNorm 归一化,数值分布稳定,可直接用于下游任务(如分类、问答、解码器输入等)

💡 注:虽然某些简化实现可能省略最终 LayerNorm,但 原版 Transformer 及绝大多数标准库(如 PyTorch nn.TransformerEncoder)均包含此步骤。它有助于稳定后续模块(如池化或解码器)的输入分布。

✅ 补充说明

📌 关于最终 LayerNorm 的澄清
在 Post-LN 架构中,每个 EncoderLayer 内部以 LayerNorm 结尾,因此第 N 层输出 Xᴺ 已是归一化的,此时“最终 LayerNorm”就是第 N 层的一部分。
在 Pre-LN 架构中,每个层以残差连接结尾,因此需在所有层后显式添加一个 LayerNorm。
无论哪种架构,最终交付给下游的表示 H 都应是经过 LayerNorm 的——这是实践中的共识。

💡 如果采用的是 Post-LN 架构(即每个 EncoderLayer 内部以 LayerNorm 结尾),那么“Final LayerNorm”本质上就是第 N 层的一部分,但为了流程完整性与 Pre-LN 架构统一表述,显式列出仍是最佳实践。


🧪 补充:Batch 维度与实际代码中的数据流

在实际训练中,输入是 batched 的:

  • 输入 token IDs(B, T)
  • Token Embedding(B, T, d_model)
  • Positional Encoding可学习位置嵌入为 (max_len, d_model),通过索引选择前 T 个并广播加到 (B, T, d_model);正弦编码可直接生成 (T, d_model) 并广播
  • 每层计算:所有操作支持 batch 并行
    • MHSA:(B, T, d_model) → Q/K/V → (B, h, T, d_k) → attn → (B, T, d_model)
    • FFN:(B, T, d_model)(B, T, d_ff)(B, T, d_model)

关键:batch 维度 不参与任何交互,各样本独立处理。


🧩 特殊情况:Padding 与 Mask 处理细节

  • Padding token(如 ID=0)在 embedding 后仍为向量,但会被 mask 屏蔽

  • Padding Mask 生成

    # input_ids: (B, T)
    src_key_padding_mask = (input_ids == pad_token_id)  # (B, T), True for padding
    
  • 在 MHSA 中使用

    • 通常扩展为 (B, 1, T)(用于 key mask,在注意力分数 (B, h, T, T) 上广播)
    • 在 softmax 前:attn_scores.masked_fill(mask.unsqueeze(1).unsqueeze(2), -inf)(PyTorch 风格)

✅ 结果:padding 位置的输出向量理论上应接近输入(因无有效信息流入),但实践中仍会受残差影响。


📊 数据流向总结表

阶段输入形状操作输出形状是否跨 token是否可学习
Tokenizationstr分词(T,)
Token Embedding(T,)查表(T, d_model)
Positional Encoding(T, d_model)加法(T, d_model)✅(若可学习)
LayerNorm (Pre-MHSA)(T, d_model)归一化(T, d_model)✅(γ, β)
MHSA(T, d_model)QKV + attn + proj(T, d_model)✅(W^Q/K/V/O)
Residual + Dropout(T, d_model)加法 + dropout(T, d_model)✅(dropout 随机)
LayerNorm (Pre-FFN)(T, d_model)归一化(T, d_model)✅(γ, β)
FFN(T, d_model)两层 MLP(T, d_model)✅(W₁, W₂)
Residual + Dropout(T, d_model)加法 + dropout(T, d_model)
(重复 N 次)
Final LayerNorm(T, d_model)归一化(T, d_model)✅(γ, β)
Final Output(T, d_model)

✅ 终极结论:数据在编码器中的本质流动

数据以 (T, d_model) 的张量形式贯穿整个编码器。它首先被赋予词义和位置身份,然后在每一层中经历两次精炼:第一次通过自注意力机制“倾听整个句子”,实现全局信息融合;第二次通过前馈网络“独立思考”,进行非线性特征转换。残差连接确保原始信息不丢失,层归一化保障训练稳定性。经过 N 次这样的“集体讨论 + 个人内化”循环,初始的孤立符号最终升华为富含上下文、句法与语义的动态向量表示。

这不仅是数据的流动,更是意义的生成过程——而 Transformer 编码器,正是这一过程的自动化引擎。


3、nn.ModuleList

📘 一、nn.ModuleList 是什么?

nn.ModuleList 是 PyTorch 提供的一个 特殊容器类(container class),继承自 torch.nn.Module,用于存储多个 nn.Module 子模块的有序列表

它在功能上类似于 Python 内置的 list,但具有深度集成到 PyTorch 模块系统的能力:

  • 自动注册子模块(submodules)
  • 自动收集参数(parameters)和缓冲区(buffers)
  • 支持设备迁移(.to(device))、训练/评估模式切换(.train() / .eval()
  • 可被 torch.save() / torch.load() 正确序列化

核心定位
nn.ModuleList 是 PyTorch 中实现可变数量层堆叠(如 N 层 Transformer 编码器)的标准工具。注意:这里的“动态”指结构在初始化时确定但层数可配置,而非训练过程中动态改变网络拓扑。


🎯 二、为什么需要 nn.ModuleList?普通 list 不行吗?

❌ 使用普通 Python list 的致命问题

class BadNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = []  # 普通 list
        for _ in range(3):
            self.layers.append(nn.Linear(10, 10))
    
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

问题分析:

问题后果
参数未注册model.parameters() 返回空迭代器 → 优化器无法更新权重
设备不迁移model.to('cuda') 不会将 Linear 层移到 GPU
状态丢失torch.save(model.state_dict()) 不包含这些层的参数
训练模式失效model.train() 不会递归设置子模块为训练模式

🔥 根本原因:PyTorch 的模块注册机制依赖于 __setattr__ 魔术方法。只有通过 self.xxx = module 或使用 nn.ModuleList/nn.ModuleDict 等注册容器,才能触发子模块注册。


✅ 使用 nn.ModuleList 的正确方式

class GoodNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.ModuleList([
            nn.Linear(10, 10) for _ in range(3)
        ])
    
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

此时:

  • 所有 Linear 层被自动注册为 GoodNet 的子模块
  • list(model.named_modules()) 包含它们
  • 参数、设备、模式等全部正常工作

📜 三、函数签名与初始化参数详解

torch.nn.ModuleList(modules=None)

参数说明:

参数类型默认值说明
modulesiterable of nn.Module or NoneNone一个可迭代对象(如 list, tuple),其中每个元素应为 nn.Module 或其子类。若为 None,则创建空的 ModuleList

⚠️ 重要约束:

  • 所有非 None 元素必须是 nn.Module。尝试放入非模块对象会立即报错:

    nn.ModuleList([1, 2, 3])  # RuntimeError: module must be a Module subclass
    
  • None 值是否允许?
    PyTorch ≥ 1.8 中,ModuleList 允许包含 None(例如用于占位),但强烈不推荐,因为:

    • None 不是 nn.Module,无参数、无 forward 方法;
    • forward 中需额外判空,易出错;
    • 序列化/反序列化可能引发问题。
    # 技术上可行(PyTorch ≥ 1.8),但不推荐
    mlist = nn.ModuleList([nn.Linear(10, 10), None])
    

🔧 四、常用操作(方法与行为)

nn.ModuleList 实现了 Python list 的大部分接口,但仅限于模块操作

  1. 创建
# 空列表
mlist = nn.ModuleList()

# 从可迭代对象初始化
mlist = nn.ModuleList([nn.Linear(10,5), nn.ReLU()])
  1. 添加模块
方法说明示例
.append(module)在末尾添加一个模块mlist.append(nn.Dropout(0.5))
.extend(modules)添加多个模块(接受可迭代对象)mlist.extend([nn.BatchNorm1d(10), nn.Linear(10,2)])
.insert(i, module)在索引 i 处插入模块mlist.insert(0, nn.Embedding(100, 10))

💡 注意:.extend() 内部会遍历输入并逐个调用 .append(),确保每个模块都被正确注册。

  1. 访问与遍历
操作说明
mlist[i]获取索引为 i 的模块(支持负索引)
len(mlist)返回模块数量
for m in mlist:遍历所有模块(顺序保持)
mlist.index(module)查找模块首次出现的索引(基于 is 比较)
  1. 修改与删除
方法说明示例
mlist[i] = new_module替换索引 i 处的模块mlist[0] = nn.Conv1d(1, 16, 3)
del mlist[i]删除索引 i 处的模块del mlist[1]
mlist.pop(i=-1)弹出并返回索引 i 的模块last_layer = mlist.pop()
mlist.clear()清空所有模块(PyTorch ≥ 1.4 即支持,非 1.8)mlist.clear()

⚠️ 注意:替换或删除模块后,原模块将不再被父模块管理(参数不会出现在 state_dict 中)。

  1. 查询与检查
方法说明
mlist.count(module)统计某模块出现次数(基于 is
module in mlist判断模块是否在列表中(基于 is

🧠 五、内部机制:PyTorch 如何管理 ModuleList

理解这一点至关重要。

  1. 子模块注册原理

当你执行:

self.layers = nn.ModuleList([layer1, layer2])

PyTorch 会:

  1. 调用 nn.Module.__setattr__('layers', module_list)

  2. 触发子模块注册逻辑

  3. module_list 本身注册为子模块

  4. 同时ModuleList__init__append 等方法会递归注册其内部每个非 None 模块

  5. 参数收集流程

调用 model.parameters() 时:

  • 遍历所有直接子模块(包括 ModuleList
  • ModuleList,递归遍历其每个None 元素
  • 收集所有 Parameter 对象
  1. 设备迁移

model.to('cuda') 会:

  • 递归调用所有子模块的 .to('cuda')
  • ModuleList 会对其每个None 元素调用 .to('cuda')
  1. 序列化

torch.save(model.state_dict()) 包含:

  • layers.0.weight, layers.0.bias
  • layers.1.running_mean, etc.

命名规则:{parent_name}.{list_name}.{index}.{param_name}


🆚 六、与其他容器的详细对比

特性nn.ModuleListnn.ModuleDictnn.Sequential普通 list
是否继承 nn.Module
自动注册子模块
保留顺序✅(索引访问)✅(Python 3.7+ 插入顺序)
支持任意控制流✅(需手动调用)❌(固定顺序执行)✅(但参数不注册)
命名访问❌(仅索引)✅(字符串 key)
自动 forward
典型用途Transformer 层数组、可配置层数多任务头、命名分支简单链式结构禁止用于可训练模块

何时选择哪个?

  • ModuleList:需要有序、可索引、自定义调用逻辑的模块集合(如 N 层相同结构)
  • ModuleDict:需要按名称访问不同功能模块(如 heads['cls'], heads['reg']
  • Sequential:简单串行,无分支、无跳连、无条件逻辑
  • 普通 list:仅用于存储非参数对象(如 loss 函数、配置字典)

🧪 七、典型应用场景(附代码)

场景 1:Transformer 编码器堆叠

encoder_layers = nn.ModuleList([
    EncoderLayer(d_model, num_heads) for _ in range(num_layers)
])

场景 2:多尺度特征提取(U-Net)

down_layers = nn.ModuleList([
    nn.Conv2d(in_ch, out_ch, 3),
    nn.MaxPool2d(2),
    ...
])

场景 3:动态深度网络(根据超参数调整)

class ConfigurableNet(nn.Module):
    def __init__(self, num_layers):
        super().__init__()
        self.layers = nn.ModuleList([
            nn.Linear(10, 10) for _ in range(num_layers)
        ])
    
    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

⚠️ 注意:这里“动态”指初始化时根据 num_layers 构建,而非在 forward 中改变结构。

场景 4:残差连接中的跳跃点

class ResNetBlock(nn.Module):
    def __init__(self, num_blocks):
        super().__init__()
        self.blocks = nn.ModuleList([
            BasicBlock() for _ in range(num_blocks)
        ])
    
    def forward(self, x):
        for block in self.blocks:
            x = x + block(x)  # 残差
        return x

⚠️ 八、常见误区与陷阱

误区 1:以为 ModuleList 会自动 forward

# 错误!
model = nn.ModuleList([nn.Linear(10,1), nn.ReLU()])
output = model(x)  # ❌ ModuleList 没有 forward 方法!

✅ 必须手动遍历:

for layer in model:
    x = layer(x)

误区 2:用 +=list 拼接

layers = nn.ModuleList([nn.Linear(10,5)])
layers += [nn.Linear(5,1)]  # ❌ 这会创建新 list,破坏注册!

✅ 正确做法:

layers.extend([nn.Linear(5,1)])

误区 3:在 forward 中动态创建 ModuleList 或层

def forward(self, x):
    layers = nn.ModuleList([nn.Linear(10,10)])  # ❌ 每次 forward 都新建!
    return layers[0](x)

→ 参数不会被优化器跟踪!所有模块必须在 __init__ 中创建

误区 4:放入非 Module 对象

nn.ModuleList([lambda x: x * 2])  # ❌ 报错

→ 若需存储函数,用普通 listdict


🧩 九、高级技巧

from copy import deepcopy
base_layer = EncoderLayer(...)
layers = nn.ModuleList([deepcopy(base_layer) for _ in range(N)])

✅ 确保每层参数独立(原始论文做法)

技巧 2:与 torch.jit.script 兼容

ModuleList 支持 TorchScript,但需注意:

  • 不能在 forward 中修改 ModuleList(如 append
  • 索引必须是常量或张量(不能是动态 Python 变量在 script 模式下)

技巧 3:自定义命名(通过父模块)

虽然 ModuleList 自身不支持命名,但可通过父模块间接实现:

class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer_0 = nn.Linear(10, 10)
        self.layer_1 = nn.Linear(10, 10)
        # 但这样失去了“列表”的便利性

→ 更推荐用 ModuleDict 实现命名。


📊 十、性能与内存考量

  • 内存开销ModuleList 本身开销极小(只是一个 Python list + 注册逻辑)
  • 速度:遍历 ModuleList 与普通 list 几乎无差别(因为只是引用)
  • GPU 利用率:无影响,因为计算发生在子模块内部

结论ModuleList 在性能上几乎零成本,放心使用。


✅ 十一、总结:最佳实践清单

场景推荐做法
存储多个相同/不同 nn.Module✅ 使用 nn.ModuleList
需要在 forward 中自定义调用顺序✅ 使用 nn.ModuleList
模块数量由超参数决定✅ 在 __init__ 中根据参数构建 ModuleList
需要命名访问➡️ 改用 nn.ModuleDict
简单串行无分支➡️ 优先考虑 nn.Sequential
存储非 Module 对象❌ 不要用 ModuleList,用普通 list/dict
训练中动态增删层⚠️ 避免! PyTorch 不支持训练时改变计算图结构(除非使用 torch.fx 等高级工具)

🌟 最后一句话

nn.ModuleList 是 PyTorch 中实现“可编程神经网络结构”的基石。它不是简单的列表,而是 PyTorch 模块系统的“第一公民”,让你既能享受 Python 的灵活性,又不丢失深度学习框架的自动化能力。


4、代码

class Encoder(nn.Module):
    def __init__(self, encoder_layer: nn.Module, num_layers: int):
        """
        :param encoder_layer: 已构建好的编码器层
        :param num_layers: 编码器的层数
        """
        super().__init__()

        # num_layer 个编码器
        self.layers = nn.ModuleList([copy.deepcopy(encoder_layer) for _ in range(num_layers)])

        # 最终输出需通过规范化层
        self.norm = LayerNorm(d_model=encoder_layer.d_model)

    def forward(self, x, mask=None):
        # 经过多个编码器层
        for layer in self.layers:
            x = layer(x, mask)

        # 最终输出经过规范化层
        output = self.norm(x)

        return output

经典代码:

import copy  # 必须显式导入 copy 模块

class Encoder(nn.Module):
    """
    Transformer 编码器:由多个 EncoderLayer 堆叠而成。
    
    输入应为已经过词嵌入(Embedding)和位置编码(Positional Encoding)的张量,
    形状为 (batch_size, seq_len, d_model)。
    """
    def __init__(self, encoder_layer, num_layers):
        """
        Args:
            encoder_layer (EncoderLayer): 一个配置好的编码器层实例(将被深拷贝 num_layers 次)
            num_layers (int): 编码器层数
        """
        super().__init__()
        # 使用 ModuleList 存储多层,确保参数被正确注册
        self.layers = nn.ModuleList([
            copy.deepcopy(encoder_layer) for _ in range(num_layers)
        ])
        # 最终的 LayerNorm(使用你自定义的 LayerNorm 类)
        self.norm = LayerNorm(d_model=encoder_layer.d_model)

    def forward(self, x, mask=None):
        """
        Args:
            x (Tensor): 输入张量,形状 (B, T, d_model)
            mask (Tensor, optional): 注意力掩码,形状通常为 (B, 1, T) 或 (B, T, T)
        
        Returns:
            Tensor: 编码后的输出,形状 (B, T, d_model)
        """
        for layer in self.layers:
            x = layer(x, mask)
        # 应用最终的 LayerNorm
        x = self.norm(x)
        return x

关键点说明

  • 必须导入 copy 模块copy.deepcopy 不是内置函数,需显式 import copy,否则会报错;
  • 使用深拷贝而非引用:确保每一层拥有独立的参数,避免所有层共享同一组权重;
  • 依赖已有的 LayerNormEncoderLayer:假设 EncoderLayer 实例具有 .d_model 属性(已在 __init__ 中保存);
  • 输入假设明确x 应已完成词嵌入和位置编码,本模块仅负责上下文建模;
  • 结构符合标准 Transformer:N 个编码器层 + 顶层 LayerNorm(与《Attention Is All You Need》一致)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐