English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

访客
1 / ?
返回课程列表

注意力 + MLP,重复

Transformer 块 Pre-Norm 结构


两个子层

Transformer 块精确包含两个子层,每个子层操作形状为 [batch, seq_len, d_model] 的令牌序列:


1. 多头注意力子层。 标记之间相互查看。活动 68 详细介绍了这一点。输出形状与输入形状匹配。

2. 前馈 MLP 子层。 每个标记独立通过一个两层感知器进行转换。没有标记间的信息流动。输出形状与输入形状匹配。


两个子层都保留 [batch, seq_len, d_model] 形状。这种保留使得块可以堆叠:层 N 的输出直接馈送到层 N+1 的输入,无需形状变换。


每个子层的贡献

注意力在位置之间移动信息:位置 17 的标记可以从位置 1 到 16 拉取信息。MLP 在每个位置内部转换信息:标记的表示通过学习到的非线性函数被重塑,但永远看不到其邻居。


堆叠块交替执行这两个操作。Layer 1 的注意力混合位置。Layer 1 的 MLP 重新塑造每个位置。Layer 2 的注意力再次混合,现在是在重新塑造的表示上。这种交替随着深度增长而增强表达能力。


ANDREA 的堆叠


Variantn_layern_headd_modelmlp_dim
ANDREA-12M6123841536
ANDREA-120M12127683072
ANDREA-480M162415366144

注意整个系列中 mlp_dim = 4 × d_model。这个比例几乎在每个现代 transformer 中都成立。第 3 节将详细解释原因。

命名子层

一个 transformer 块包含两个子层。请按顺序命名它们,并针对每个子层说明它是跨位置(token-to-token)传递信息,还是在单个位置内(每个 token 独立)转换信息。每子层一句。

为什么跳跃连接很重要

残差模式

每个子层将其计算包装在残差连接中。输出会加回输入:


x = x + Attention(LayerNorm(x))     # attention sublayer
x = x + MLP(LayerNorm(x))           # MLP 子层

在每个子层内部,函数 Attention(...)MLP(...) 会产生一个增量。块不会替换输入;它会添加一个学习到的修正。


为什么这很重要

残差连接主导深度架构的三个原因:


1. 梯度流动。 在反向传播期间,残差连接为梯度从输出直接回传到输入提供了路径,绕过了子层。没有残差的 12 层堆栈会在到达嵌入层之前很久就丢失梯度信号;有了残差,梯度幅度在数百层中保持可用。


2. 恒等初始化。 在初始化时,子层权重产生较小的输出。残差连接意味着第 N 层最初几乎不变地传递通过。训练从一个有效的起点逐步学习增量。


3. 组合学习。 每个块学习一个细化,而不是替换。层 1 可能添加位置信息。层 2 可能添加句法结构。层 7 可能添加语义关系。残差流累积这些。


层归一化

在每个子层之前,LayerNorm 将每个标记的表示重新缩放到零均值和单位方差,然后应用每个特征的学习增益和偏置:


y = gamma * (x - mean(x)) / sqrt(var(x) + epsilon) + beta

均值和方差在 d_model 维度上计算,对于每个 token 分别计算。两个可学习的向量(gammabeta,每个形状为 [d_model])恢复表达性的尺度。归一化将激活值保持在数值稳定的范围内;没有它,小的训练不稳定性会滚雪球般累积成 NaN 梯度。

Pre-Norm 与 Post-Norm

一个微妙但关键的选择

将层归一化接入残差子层的两种方式:


后置归一化(原始 2017 年论文):

x = LayerNorm(x + Attention(x))

层归一化位于残差相加之后。残差流本身在每一层都会被归一化。


Pre-norm(现代标准,用于 ANDREA):

x = x + Attention(LayerNorm(x))

层归一化位于子层之前,在残差分支内部。残差流保持未归一化;只有子层的输入被重新缩放。


为什么 Pre-Norm 胜出

Post-norm 在没有 LR 预热和仔细超参数调优的情况下训练效果差。在早期层中梯度爆炸,因为每个层归一化会打乱残差流的累积状态。2017 年的原始论文使用了 post-norm 并进行了广泛调优;后续工作(GPT-2、LLaMA、ANDREA)标准化为 pre-norm。


预归一化训练稳定。残差流在所有层中干净地累积;只有子层输入进行归一化以确保数值稳定性。现代 Transformer 默认使用预归一化,ANDREA 继承了这一选择。


最终块方程

结合残差、预归一化位置的层归一化,以及两个子层,得到 ANDREA 的完整块:


```python
def block_forward(x):
```
x = x + Attention(LayerNorm(x))   # attention 子层
x = x + MLP(LayerNorm(x))         # MLP 子层
return x

两个子层,两个残差加法,两个层归一化(注意:每个子层都有自己的层归一化;ANDREA-120M 在 12 个块中跨 24 个层归一化,加上输出处的最终一个,总共 25 个)。重复 12 次。这就是 ANDREA-120M 的主体。

为什么 Pre-Norm 能稳定训练

ANDREA 使用 pre-norm:`x = x + Attention(LayerNorm(x))`。与 post-norm:`x = LayerNorm(x + Attention(x))` 进行比较。从梯度流动的角度,给出一个为什么 pre-norm 在深层堆栈中比 post-norm 训练更稳定的原因。在答案中引用残差流。

两个线性层,一个激活函数

三个操作

MLP 子层是一个两层感知器,层与层之间有一个非线性激活函数:


def mlp_forward(x):
h = x · W_1 + b_1        # 扩展:d_model → mlp_dim
h = GELU(h)              # 非线性激活
y = h · W_2 + b_2        # 收缩:mlp_dim → d_model
return y

三个操作。两个线性,一个非线性。第一个线性扩展宽度;第二个线性收缩回来。


4× 扩展比率

现代 transformer 设置 mlp_dim = 4 × d_model。ANDREA-120M:


d_model = 768
mlp_dim = 4 × 768 = 3072
W_1 形状 = [768, 3072]      # ~236万参数
W_2 形状 = [3072, 768]      # ~236万参数
每个块的 MLP 参数 = 472万(忽略偏置)

两个 MLP 位于每对注意力子层之间(每个块一个)。12 个块 × 472万 ≈ 5660万 MLP 参数总计在 ANDREA-120M 中,约占所有参数的一半。


为什么是 4×

4× 比率是经验性得出的。更小的比率会降低模型容量。更大的比率在每个参数上的回报递减。经过数十年的架构搜索,4× 一直经得起考验;它出现在 GPT、BERT、T5、LLaMA 和 ANDREA 中。


最近的工作(PaLM、Chinchilla)发现,门控机制(SwiGLU)可以使用 8/3× 扩展,以更低的成本实现相当的容量;ANDREA 为了简单起见,仍使用经典的 GELU + 4×。

GELU:平滑激活函数

GELU 计算的内容

GELU(Gaussian Error Linear Unit)是现代 Transformer 中 MLP 层之间标准激活函数。其公式:


GELU(x) = x · Φ(x)

Φ(x) 是标准正态分布的累积分布函数:标准高斯随机变量落在 x 或以下的概率。通过数值计算:


Φ(x) ≈ 0.5 × (1 + tanh(sqrt(2/π) × (x + 0.044715 × x³)))

按区域的行为

- 对于大的正值 x:Φ(x) ≈ 1,因此 GELU(x) ≈ x。与 ReLU 类似。

- 对于大的负值 x:Φ(x) ≈ 0,因此 GELU(x) ≈ 0。与 ReLU 类似。

- 接近 x = 0:Φ(x) ≈ 0.5,因此 GELU(0) = 0 精确。通过原点的平滑过渡。


与 ReLU 不同,GELU 允许一些负输入通过,由 Φ(x) 加权。一个小的负输入仍然贡献一个小负输出,只是比完整输入少。


为什么 GELU 优于 ReLU

经验上,使用 GELU 训练的 transformer 在相同参数数量下,能达到比使用 ReLU 训练的 transformer 更低的损失。零点附近的平滑性很重要:ReLU 在零点的硬截断会产生梯度不连续;GELU 的平滑曲线为反向传播提供了更干净的梯度。


ANDREA 的训练引擎 microgpt_cuda.cu 附带了一个手写的 GELU CUDA 内核。该内核使用了上面的 tanh 近似;现代 GPU 将 tanh 作为单指令操作包含在内。

计算 MLP 参数数量

ANDREA-120M 有 `d_model=768`、`mlp_dim=3072` 和 `n_layer=12`。计算所有 12 个块中 MLP 权重矩阵(`W_1` 和 `W_2`)的总参数数量。忽略偏置。展示你的计算过程。然后说明这占 ANDREA-120M 约 120M 总参数的什么比例(四舍五入到一位小数)。

十二个块组成 ANDREA-120M

从块到模型

ANDREA-120M 的完整前向传播:


def model_forward(token_ids):
x = token_embed(token_ids) + position_embed(positions)
for block_idx in range(n_layer):       # 12 个块
x = block_forward(x)               # 注意力 + MLP 带残差连接
x = LayerNorm(x)                       # 最终归一化
logits = x · token_embed.T             # 输出投影使用共享权重
return logits

六层。主要部分位于 block_forward 中,被调用十二次。嵌入启动管道;系结反嵌入(用于输入查找的同一矩阵,转置用于输出投影)结束它。


深度作为组合

每个块读取残差流,计算一个增量,并将其加回。经过十二次传递后,流包含来自每个块的累积贡献。内部,层趋向于专业化:


- 早期层 (1-3):句法模式,位置结构

- 中间层 (4-8):词语关系,短语边界

- 晚期层 (9-12):语义内容,事实回忆


这种特化源于训练压力,而不是架构选择。相同的统一块设计在语言训练时会产生特化层。


总块参数


组件每个块12个块总计
注意力投影 (4×W)2.36M28.3M
MLP 权重 (W_1 + W_2)4.72M56.6M
层归一化 (gamma, beta)~3K (可忽略)~37K
每个块总计~7.1M~85M

主干中有 85M 参数。加上 ~13M 的令牌嵌入 (8449 词汇 × 768 d_model × 2 用于绑定的输入/输出) 以及最终的层归一化,ANDREA-120M 的参数总数约为 120M。块设计占三分之二;嵌入占其余部分。

追踪一个令牌通过一个块

一个 768 维的令牌向量进入 ANDREA-120M 的块 7。在预归一化结构中,逐步说明它在块内发生什么。提及:两个层归一化、两个子层、两个残差加法,以及最终形状。至少说明一个残差流未被触及的位置,以及一个被修改的位置。