AI 技术演进与核心算法实战 | 第五篇:Transformer 架构深潜:Positional Encoding、Layer Norm 与前馈网络的手写实现(NanoGPT 复现)
/author/zhaohuan.jpg)
/author/zhaohuan.jpg)
Self-Attention 是 Transformer 的灵魂,但只有灵魂还不够,它还需要一副强大的躯壳。今天,我们就来拼装这副躯壳。
在上一篇中,我们解密了 Self-Attention 的核心机制,明白了词与词之间是如何通过 Q、K、V 进行动态交流的。然而,纯粹的 Attention 机制有两个致命的缺陷:
- 它是“色盲”加“脸盲”的(丢失位置信息):Self-Attention 将输入视为一个“无序的词袋”,无论“狗咬人”还是“人咬狗”,计算出的 Attention 分数是一样的,因为它完全不知道词的顺序。
- 它容易“营养不良”或“走火入魔”(梯度问题与特征表达能力不足):仅靠线性变换的 Attention,无法拟合复杂的非线性语言规律,同时深层网络极易出现梯度消失或爆炸。
为了解决这些问题,Transformer 引入了三大法宝:位置编码(Positional Encoding)、层归一化(Layer Normalization)和前馈网络(Feed-Forward Network, FFN)。
本篇是 《AI 技术演进与核心算法实战》第一模块的第五篇。我们将结合 Andrej Karpathy 的 NanoGPT 项目,用手写代码和 SVG 图解,带你彻底搞透这三大组件的底层逻辑,并最终拼装出一个完整的 Transformer Block!
1. 注入时间之魂:Positional Encoding
为什么 Transformer 那么快?因为它用“并行计算”取代了 RNN 的“串行计算”。 但代价是什么?代价是丢失了时序信息。
为了让模型知道每个词在句子中的位置,我们需要给每个词贴上一个“位置标签”。这就是位置编码(Positional Encoding)。
1.1 绝对位置编码的直觉
最简单的想法是:第一个词贴上标签 1,第二个词贴上 2…… 但这样做有问题:句子很长时,数字会非常大,导致特征在数值上被位置标签“淹没”;而且模型很难泛化到比训练集更长的句子。
Google 在论文中给出了一个极其优雅的解法:使用不同频率的正弦和余弦函数。
想象一个二进制计数器:
- 个位:
0 1 0 1 0 1...(变化极快) - 十位:
0 0 1 1 0 0...(变化中等) - 百位:
0 0 0 0 1 1...(变化极慢)
Transformer 的位置编码也是类似原理,只是把离散的 0 和 1 换成了连续的 sin 和 cos 曲线。向量的第一维变化最快(高频),最后一维变化最慢(低频)。
1.2 图解位置编码
1.3 手写实现(PyTorch)
在目前的实践中(如 GPT 系列),通常采用更简单的可学习的位置嵌入(Learned Positional Embedding),而不是固定的正余弦。NanoGPT 采用的正是这种方案:
import torch
import torch.nn as nn
class PositionalEncoding(nn.Module):
def __init__(self, block_size, n_embd):
super().__init__()
# block_size 是最大序列长度,n_embd 是词向量维度
# 直接创建一个可学习的 Embedding 矩阵 (block_size, n_embd)
self.pos_emb = nn.Embedding(block_size, n_embd)
def forward(self, x):
# x 的 shape: (batch_size, seq_len, n_embd)
B, T, C = x.shape
# 生成 0 到 T-1 的位置索引
pos = torch.arange(0, T, dtype=torch.long, device=x.device) # (T)
# 获取位置向量并与输入相加
pos_emb = self.pos_emb(pos) # (T, C)
return x + pos_emb # 利用广播机制 (B, T, C) + (T, C) -> (B, T, C)
物理意义: 词的最终表达 = “它是什么意思”(Token Embedding) + “它在哪个位置”(Positional Embedding)。这两者在向量空间中完美融合。
2. 驯服狂暴的梯度:Layer Normalization
当我们在堆叠多层 Self-Attention 时,数值会变得非常大,导致梯度爆炸,网络根本无法收敛。 为了让每一层的输出“冷静”下来,我们需要 Layer Normalization(层归一化)。
2.1 为什么是 Layer Norm 而不是 Batch Norm?
在图像领域(CNN),大家习惯用 Batch Norm(批归一化),它是跨越 Batch 对同一个通道进行归一化。 但在自然语言中,句子的长度是不一样的(有长有短),如果跨句子求平均,长句后面的词只能和填充(Padding)的 0 一起算,这毫无意义。
因此,NLP 中采用 Layer Norm:在每一个词的词向量内部进行归一化。不管句子多长,每个词管好自己的向量就行。
2.2 图解 Layer Norm
2.3 手写实现(核心逻辑)
Layer Norm 的公式很简单:先减去均值,再除以标准差,最后乘上缩放参数 gamma,加上平移参数 beta。
class LayerNorm(nn.Module):
def __init__(self, ndim, bias=True):
super().__init__()
# 可学习的缩放参数 gamma 和平移参数 beta
self.weight = nn.Parameter(torch.ones(ndim))
self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None
def forward(self, input):
# input shape: (Batch, SeqLen, EmbeddingDim)
# 在 Embedding 维度(最后一步)上计算均值和方差
mean = input.mean(dim=-1, keepdim=True)
var = input.var(dim=-1, keepdim=True, unbiased=False)
# 归一化:减均值,除以标准差 (加 1e-5 防止除零)
out = (input - mean) / torch.sqrt(var + 1e-5)
# 应用可学习的仿射变换
out = out * self.weight
if self.bias is not None:
out = out + self.bias
return out
3. 赋予思考的深度:前馈网络(FFN)
Self-Attention 虽然强大,但它只做了一件事:让词与词之间交换信息。这本质上只是一个复杂的加权求和(线性变换)。 如果全是线性变换,多层网络就会退化成一层。我们需要非线性激活函数,这就是前馈网络(Feed-Forward Network, FFN)的作用。
3.1 放大与压缩的艺术
在 Transformer 中,FFN 的结构非常经典:
- 升维:先将维度放大 4 倍(
n_embd -> 4 * n_embd)。 - 非线性:通过激活函数(如 GELU 或 ReLU)进行非线性映射。
- 降维:再将维度压缩回原来的大小(
4 * n_embd -> n_embd)。
物理意义: Self-Attention 负责“收集资料”(看上下文),而 FFN 负责“闭门思考”(在单个词内部进行复杂的非线性逻辑推理)。升维的目的是提供更大的空间去记忆和组合特征。
3.2 手写实现
class FeedForward(nn.Module):
def __init__(self, n_embd):
super().__init__()
self.net = nn.Sequential(
# 1. 升维:放大 4 倍
nn.Linear(n_embd, 4 * n_embd),
# 2. 非线性激活:GPT 系列通常使用 GELU
nn.GELU(),
# 3. 降维:还原回 n_embd
nn.Linear(4 * n_embd, n_embd),
# Dropout 防止过拟合
nn.Dropout(0.1),
)
def forward(self, x):
return self.net(x)
4. 拼装神迹:构建完整的 Transformer Block
现在,我们有了 Self-Attention(上篇)、Layer Norm 和 FFN。是时候把它们组装成一个真正的 Transformer Block 了!
在早期的 Transformer(如原始论文)中,Layer Norm 放在 Attention 之后(Post-LN)。但实践证明,将 Layer Norm 放在 Attention 之前(Pre-LN)能让训练极其稳定,GPT 家族全部采用了 Pre-LN 架构。
4.1 架构图解(Pre-LN)
4.2 NanoGPT 源码复现
结合图解,我们用代码将它们串联起来。注意残差连接(x = x + ...)的运用,它是深层网络能够训练成功的另一个关键。
class Block(nn.Module):
def __init__(self, n_embd, n_head):
super().__init__()
# 1. 归一化层 1
self.ln_1 = LayerNorm(n_embd)
# 2. 多头自注意力机制 (我们在上一篇详细讲过)
# 这里假设 CausalSelfAttention 已经实现好
self.attn = CausalSelfAttention(n_embd, n_head)
# 3. 归一化层 2
self.ln_2 = LayerNorm(n_embd)
# 4. 前馈网络
self.mlp = FeedForward(n_embd)
def forward(self, x):
# 注意这里的结构是 Pre-LN:先做 LayerNorm,再输入到模块,最后加上残差
x = x + self.attn(self.ln_1(x))
x = x + self.mlp(self.ln_2(x))
return x
至此,一个完整的 Transformer 积木块就诞生了!现代的 GPT 模型,比如 GPT-3 或 LLaMA,本质上就是把这个 Block 重复堆叠了几十层。
5. 总结
在这篇文章中,我们为裸奔的 Self-Attention 穿上了神装:
- Positional Encoding 赋予了模型时间的概念,让模型不再脸盲。
- Layer Norm 稳定了数值波动,降伏了狂暴的梯度。
- FFN 提供了非线性的推理空间,让模型拥有了真正的“思考”能力。
- 残差连接 (Residual Connection) 搭建了信息的高速公路,使得几十上百层的网络依然能够顺畅反向传播。
在下一篇中,我们将进入实战环节:预训练与微调。我们将揭秘大模型是如何通过 Masked LM 或 Causal LM 目标函数“读遍天下书”的,并深入探讨 LoRA/P-Tuning 等让普通玩家也能玩转大模型的高效微调黑科技。敬请期待!
参考文献与延伸阅读
- Attention Is All You Need (Vaswani et al., 2017):Transformer 架构的开山之作。
- NanoGPT by Andrej Karpathy:用极其简洁优雅的代码复现了 GPT,是学习大模型底层实现最好的开源项目。
- Layer Normalization (Ba et al., 2016):详细阐述了为什么在 RNN 和 NLP 任务中 Layer Norm 优于 Batch Norm。
- On Layer Normalization in the Transformer Architecture (Xiong et al., 2020):论证了 Pre-LN 相对于 Post-LN 的稳定性优势。