注意力 + MLP,重复
两个子层
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 的堆叠
| Variant | n_layer | n_head | d_model | mlp_dim |
|---|---|---|---|---|
| ANDREA-12M | 6 | 12 | 384 | 1536 |
| ANDREA-120M | 12 | 12 | 768 | 3072 |
| ANDREA-480M | 16 | 24 | 1536 | 6144 |
注意整个系列中 mlp_dim = 4 × d_model。这个比例几乎在每个现代 transformer 中都成立。第 3 节将详细解释原因。
命名子层
为什么跳跃连接很重要
残差模式
每个子层将其计算包装在残差连接中。输出会加回输入:
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 分别计算。两个可学习的向量(gamma、beta,每个形状为 [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 能稳定训练
两个线性层,一个激活函数
三个操作
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
从块到模型
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.36M | 28.3M |
| MLP 权重 (W_1 + W_2) | 4.72M | 56.6M |
| 层归一化 (gamma, beta) | ~3K (可忽略) | ~37K |
| 每个块总计 | ~7.1M | ~85M |
主干中有 85M 参数。加上 ~13M 的令牌嵌入 (8449 词汇 × 768 d_model × 2 用于绑定的输入/输出) 以及最终的层归一化,ANDREA-120M 的参数总数约为 120M。块设计占三分之二;嵌入占其余部分。