Heng's Blog
03 04

Transformer Encoder 将输入序列编码为上下文化表示。每个 block 包含两个子层:Multi-Head Self-Attention 和逐位置 Feed-Forward Network(FFN),并用残差连接、LayerNorm 与 Dropout 稳定训练。

本文整理自 hengproject/ML_recall 中的 transformers/encoder.ipynb。注意力与输入表示分别见注意力机制输入嵌入

Encoder Block 的数据流#

原始 Transformer 使用 Post-LN,即先执行子层和残差相加,再归一化。本文实现 Pre-LN,将 LayerNorm 放在每个子层之前:

对应公式为:

x=x+Dropout(MHA(LN(x))),y=x+Dropout(FFN(LN(x))).\begin{aligned} x' &= x + \operatorname{Dropout}(\operatorname{MHA}(\operatorname{LN}(x))), \\ y &= x' + \operatorname{Dropout}(\operatorname{FFN}(\operatorname{LN}(x'))). \end{aligned}

Pre-LN 为残差分支提供更直接的梯度路径,通常比深层 Post-LN 模型更容易优化。不过两种结构都仍在使用,具体选择应与模型架构和训练方案保持一致。

Multi-Head Self-Attention#

在 Encoder 的 self-attention 中,Query、Key、Value 都来自同一个归一化后的输入:

Q=K=V=LN(x).Q = K = V = \operatorname{LN}(x).

Encoder 不使用 causal mask,因为每个非 padding 位置都允许关注完整输入序列。Padding mask 的形状通常为 (B,n)(B,n),它只遮蔽 Key 侧的 padding 位置。

Feed-Forward Network#

FFN 独立作用于每个序列位置,参数在所有位置之间共享:

FFN(x)=W2ϕ(W1x+b1)+b2.\operatorname{FFN}(x) = W_2\,\phi(W_1x+b_1)+b_2.

第一层把最后一维从 dmodeld_{model} 扩展到 dffd_{ff},非线性激活后再投影回 dmodeld_{model},从而能够与残差分支相加。

Encoder Block 实现#

下面的 MultiHeadAttention 沿用前一篇的接口,输入与输出形状均为 (B,n,dmodel)(B,n,d_{model})

残差连接要求每个子层输出都保持 (B,n,dmodel)(B,n,d_{model})dim_ff 只在 FFN 内部使用,不会改变 block 的外部接口。

堆叠多个 Block#

nn.ModuleList 可以注册多个独立 block。每层拥有自己的参数,但接收相同的 padding mask:

对 Pre-LN 架构,在整个 stack 末尾增加一次 LayerNorm 是常见做法。它不是单个 block 的第三个子层,而是对堆叠后的最终表示做归一化。

接入输入嵌入#

完整 Encoder 先把 token ID 转换为带位置信息的向量,再送入 block stack:

形状测试#

实现注意点#

  • Mask 遮蔽 Key 位置后,padding 对应的 Query 位置仍可能产生非零输出;下游若要求这些位置严格为零,需要在 block 外再次清零或在损失中忽略。
  • 原论文 FFN 使用 ReLU,现代模型也常用 GELU、SwiGLU 等激活与门控结构。
  • d_model 必须能被 num_heads 整除;dim_ff 则是独立的容量超参数。
  • 位置编码、Dropout 和注意力内部的投影偏置都会影响完整复现,不能只比较 block 的主干公式。
Transformer - Transformer Encoder
https://heng-blog.pages.dev/blog/transformer-encoder
Author 杨苏恒
Published at April 21, 2025