其余三个零件 · LayerNorm、MLP、Block

啃完注意力,剩下三个零件轻松得像甜点。逐行读 nanoGPT 的 LayerNorm(为什么自己写)、MLP(先胀 4 倍再压回)、Block(Pre-LN + 残差怎么把注意力和 MLP 拼成一层)。

作者 David更新于 第 5 篇(共 14 篇)

其余三个零件 · LayerNorm、MLP、Block

注意力是五个零件里最硬的骨头,啃完了。剩下三个——LayerNorm、MLP、Block——简单得多。这一章一口气收拾掉,你会发现它们干净利落,读完就集齐了拼一个 GPT 的全部积木。

LayerNorm:给数据「校准量纲」

先看 nanoGPT 自己写的 LayerNorm(model.py,约 18–27 行):

class LayerNorm(nn.Module):
    """ LayerNorm but with an optional bias. PyTorch doesn't support simply bias=False """

    def __init__(self, ndim, bias):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(ndim))
        self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None

    def forward(self, input):
        return F.layer_norm(input, self.weight.shape, self.weight, self.bias, 1e-5)

它在干嘛? 归一化——把每个 token 向量,沿着特征维减均值、除标准差,拉回「零均值、单位方差」,再用一个可学习的 weight(缩放)和 bias(平移)调整回合适的范围。好处是:各层数值的「量纲」被稳住,梯度不容易爆或消,训练更顺。

逐行看:

  • self.weight = nn.Parameter(torch.ones(ndim)):缩放系数,初始化为全 1(一开始「不缩放」)。
  • self.bias = ... if bias else None:平移量,初始为全 0;bias=False 时干脆设成 None。
  • forward 里直接调 PyTorch 的 F.layer_norm,最后那个 1e-5 是防止除零的 eps。

为什么要自己写,不直接用 nn.LayerNorm? 注释第一行就抱怨了:PyTorch 自带的 nn.LayerNorm 不支持简单地关掉 bias。而 nanoGPT 想让 bias 成为一个可配的开关(我们在 GPTConfig 里见过 bias: bool),所以只好手搓一个「bias 可选」的版本。又一个「为了一个小小的可配置性,宁可自己写」的务实细节。

MLP:先胀 4 倍,再压回来

MLP 是每个 Block 里负责「逐位置深加工」的前馈网络(model.py,约 78–92 行):

class MLP(nn.Module):

    def __init__(self, config):
        super().__init__()
        self.c_fc    = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
        self.gelu    = nn.GELU()
        self.c_proj  = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x):
        x = self.c_fc(x)
        x = self.gelu(x)
        x = self.c_proj(x)
        x = self.dropout(x)
        return x

它的形状变化是一个漂亮的「先胀后缩」:

  • c_fc:把 n_embd 维放大 4 倍到 4 * n_embd(默认 768 → 3072)。
  • gelu:一个非线性激活(GELU,比 ReLU 更平滑)。非线性是关键——没有它,两个线性层叠起来还是线性,白搭。
  • c_proj:再压回 n_embd(3072 → 768)。
  • dropout:随机失活,抗过拟合。

为什么先放大再缩小? 打个比方:把信息摊到一个更大的空间里,才有足够的「工作台面」做复杂的非线性变换,加工完再收拢回原尺寸。这个「4 倍」是 Transformer 沿用至今的惯例。注意力负责「跨位置」交换信息,MLP 负责「单个位置」把自己的向量揉得更有表达力——一横一纵,分工明确。

还记得上一个专栏说「前馈层是参数大头、也是 MoE 动刀的地方」吗?看这里就懂了:c_fc 和 c_proj 两个 4× 的大矩阵,正是整个 Block 里最占参数的部分。真实大模型的 MoE,拆的就是这个 MLP——把它换成一排专家。而 nanoGPT 里,它就是老老实实的一个稠密 MLP。

Block:两行 forward,拼出一层 Transformer

最后,Block 把注意力和 MLP 组装成「一层」(model.py,约 94–106 行):

class Block(nn.Module):

    def __init__(self, config):
        super().__init__()
        self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
        self.mlp = MLP(config)

    def forward(self, x):
        x = x + self.attn(self.ln_1(x))
        x = x + self.mlp(self.ln_2(x))
        return x

__init__ 备齐四个零件:两个 LayerNorm、一个注意力、一个 MLP。精华全在 forward 那两行——模型全景那章已经点过,这里对着真实代码再钉一遍:

  • x = x + self.attn(self.ln_1(x)):先归一化 ln_1(x),再进注意力,输出加回 x。
  • x = x + self.mlp(self.ln_2(x)):先归一化 ln_2(x),再进 MLP,输出加回 x。

两个关键点:

  1. Pre-LN:归一化在子层前面(ln_1(x) 先算,再喂给 attn)。这比原始 Transformer 的 Post-LN 训练更稳,是现代 GPT 的标配。
  2. 残差 x + ...:子层输出是加到 x 上,不是替换。这条「加法高速路」让梯度能直通到底,深层网络也学得动。

这两个设计——Pre-LN 的位置、RMSNorm 还是 LayerNorm——正是《AI 大模型解构》里专门对比过的一整章。nanoGPT 用的是「LayerNorm + Pre-LN」这个 GPT-2 经典组合;而 Llama、DeepSeek 们把 LayerNorm 换成了 RMSNorm,OLMo 2 甚至把 Pre 换回了 Post。你在这里读到的每个小设计,到了真实大模型那儿,都是一个可以拨动的旋钮。

五个零件,集齐了

盘一下:LayerNorm(校准)、CausalSelfAttention(跨位置看)、MLP(单位置加工)、Block(把后两个拼成一层)。加上马上要讲的 GPT(把 Block 叠 N 层、配上入口出口),model.py 的五个类就全了。

积木备齐,下一章我们动手搭整台机器:GPT 类怎么把词嵌入、一摞 Block、输出层拼起来,前向传播怎么从 token 编号一路算到 loss,以及那个「权重绑定」和「推理期只算最后一个位置」的小机灵。👉 搭起整台机器:GPT 的 init 与 forward

参考文献

  • [1] Ba et al. “Layer Normalization.” 2016. arXiv:1607.06450. —— LayerNorm。
  • [2] Hendrycks & Gimpel. “Gaussian Error Linear Units (GELUs).” 2016. arXiv:1606.08415. —— GELU 激活。
  • [3] He et al. “Deep Residual Learning for Image Recognition.” CVPR 2016. arXiv:1512.03385. —— 残差连接。
  • [4] Xiong et al. “On Layer Normalization in the Transformer Architecture.” ICML 2020. arXiv:2002.04745. —— Pre-LN 的稳定性。
  • 源码:Andrej Karpathy, nanoGPT, model.py. https://github.com/karpathy/nanoGPT

本页目录