从零构建LLM 第4章:从零实现GPT

📅 2026-03-02📖 ~9 min readLLMGPTArchitecture
本文是 Build a Large Language Model From Scratch(Sebastian Raschka 著)第4章的学习笔记。上一章实现了注意力机制这个核心组件,本章的目标是把它和 LayerNormGELUFeedForward残差连接组装成一个完整的 GPT 模型。从一个配置字典开始,一层一层搭积木,最终得到一个能接受 token 序列并输出下一个 token 概率分布的完整神经网络
← Ch3: 注意力机制Ch5: 预训练 →

1. GPT 的整体架构

GPT (Generative Pre-trained Transformer) 的架构其实并不复杂。它是 Transformer 的仅解码器 (Decoder-Only) 变体,核心就是一堆相同结构的 TransformerBlock 堆叠在一起:

  1. Token 嵌入 + 位置嵌入(第2章实现)→ 将输入序列转为向量
  2. N 个 TransformerBlock(每个含注意力 + 前馈网络)→ 逐层提取和融合特征
  3. LayerNorm → 最终归一化
  4. 线性输出层 → 映射回词汇表大小,输出每个 token 的概率
Chapter 4 overview

第4章在整体 LLM 构建流程中的位置 (图源: LLMs from Scratch)

整个模型的行为由一个配置字典完全确定。改变配置就能得到不同规模的 GPT-2 变体:

Python — GPT-2 配置
GPT_CONFIG_124M = {
    "vocab_size": 50257,      # BPE 词汇表大小
    "context_length": 1024,   # 最大上下文长度
    "emb_dim": 768,           # 嵌入维度
    "n_heads": 12,            # 注意力头数
    "n_layers": 12,           # TransformerBlock 层数
    "drop_rate": 0.1,         # Dropout 率
    "qkv_bias": False         # Q/K/V 是否使用偏置
}
模型 参数量 嵌入维度 层数 注意力头
GPT-2 Small 124M 768 12 12
GPT-2 Medium 355M 1024 24 16
GPT-2 Large 774M 1280 36 20
GPT-2 XL 1558M 1600 48 25

观察规律:所有变体的结构完全相同,只有数字不同。这体现了 Transformer 架构的模块化优势 — 扩展模型只需要增加维度和层数。


2. 归一化与激活:LayerNorm 和 GELU

在组装 TransformerBlock 之前,需要先实现两个基础组件。

2.1 LayerNorm(层归一化)

LayerNorm 对每个 token 的所有特征维度做归一化(均值为 0,方差为 1),然后通过可学习的 scale 和 shift 参数恢复表达能力。

为什么不用 BatchNorm?BatchNorm 在 batch 维度上归一化,但在文本任务中不同序列的同一位置没有统计意义(第3个词可能是名词也可能是动词)。LayerNorm 只看「单个 token 的所有特征」,更适合序列模型。

2.2 GELU 激活函数

GELU (Gaussian Error Linear Unit) 是 GPT 的激活函数。不同于 ReLU 的硬截断(x < 0 时梯度直接为 0),GELU 用高斯分布的 CDF 做「软门控」— 小的负值被轻微抑制而非完全丢弃,有利于梯度流动。

GELU vs ReLU activation

GELU vs ReLU — GELU 对负值区域更平滑,梯度不会突变 (图源: LLMs from Scratch)

2.3 FeedForward 前馈网络

FeedForward 模块由两个线性层组成,中间维度扩大 4 倍:768 → 3072 → 768。这个「先扩展再压缩」的设计给模型提供了更大的中间表示空间来学习复杂的特征变换。

Python — LayerNorm + GELU + FeedForward
class LayerNorm(nn.Module):
    def __init__(self, emb_dim):
        super().__init__()
        self.eps = 1e-5
        self.scale = nn.Parameter(torch.ones(emb_dim))
        self.shift = nn.Parameter(torch.zeros(emb_dim))

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        norm_x = (x - mean) / torch.sqrt(var + self.eps)
        return self.scale * norm_x + self.shift

class GELU(nn.Module):
    def forward(self, x):
        return 0.5 * x * (1 + torch.tanh(
            torch.sqrt(torch.tensor(2.0 / torch.pi)) *
            (x + 0.044715 * torch.pow(x, 3))
        ))

class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),  # 768 → 3072
            GELU(),
            nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),  # 3072 → 768
        )

    def forward(self, x):
        return self.layers(x)

3. TransformerBlock:核心积木

TransformerBlock 是 GPT 的核心重复单元。它将上一章的多头注意力和刚实现的 FeedForward 组合在一起,并用 LayerNorm残差连接 (Residual Connection) 将它们串联。

TransformerBlock architecture

TransformerBlock:两个子层(注意力 + 前馈),各配 LayerNorm + 残差连接 (图源: LLMs from Scratch)

GPT 使用 Pre-Norm 架构(先归一化再计算),而非原版 Transformer 论文中的 Post-Norm(先计算再归一化)。Pre-Norm 的优势是训练更稳定,不容易出现梯度爆炸。

Python — TransformerBlock
class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.att = MultiHeadAttention(
            d_in=cfg["emb_dim"], d_out=cfg["emb_dim"],
            context_length=cfg["context_length"],
            num_heads=cfg["n_heads"],
            dropout=cfg["drop_rate"], qkv_bias=cfg["qkv_bias"]
        )
        self.ff = FeedForward(cfg)
        self.norm1 = LayerNorm(cfg["emb_dim"])
        self.norm2 = LayerNorm(cfg["emb_dim"])
        self.drop_shortcut = nn.Dropout(cfg["drop_rate"])

    def forward(self, x):
        # 子层1: LayerNorm → 多头注意力 → Dropout → 残差连接
        shortcut = x
        x = self.norm1(x)
        x = self.att(x)
        x = self.drop_shortcut(x)
        x = x + shortcut  # 残差:把输入直接加回来

        # 子层2: LayerNorm → FeedForward → Dropout → 残差连接
        shortcut = x
        x = self.norm2(x)
        x = self.ff(x)
        x = self.drop_shortcut(x)
        x = x + shortcut
        return x

⚠️ 残差连接的作用:如果没有残差连接,12 层 TransformerBlock 意味着信号需要经过 24 个非线性变换 — 梯度极容易消失。残差连接提供了一条「高速公路」,让梯度可以直接流回浅层,是深层网络能训练的关键。这也是为什么 GPT-2 XL 能堆到 48 层。


4. GPTModel:完整模型

有了所有组件,GPTModel 的实现就是一个「搭积木」的过程:

Full GPT model architecture

GPTModel 完整架构:嵌入 → N 个 TransformerBlock → LayerNorm → 线性输出 (图源: LLMs from Scratch)
Python — GPTModel
class GPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        self.drop_emb = nn.Dropout(cfg["drop_rate"])

        # 堆叠 N 个 TransformerBlock
        self.trf_blocks = nn.Sequential(
            *[TransformerBlock(cfg) for _ in range(cfg["n_layers"])]
        )
        self.final_norm = LayerNorm(cfg["emb_dim"])
        self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)

    def forward(self, in_idx):
        batch_size, seq_len = in_idx.shape
        # Token 嵌入 + 位置嵌入(第2章的内容)
        tok_embeds = self.tok_emb(in_idx)
        pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
        x = self.drop_emb(tok_embeds + pos_embeds)

        x = self.trf_blocks(x)       # 经过 12 个 TransformerBlock
        x = self.final_norm(x)       # 最终 LayerNorm
        logits = self.out_head(x)    # 映射到词汇表大小
        return logits                # [batch, seq_len, 50257]

模型实例化后,可以直接查看参数量:

Python — 参数量统计
model = GPTModel(GPT_CONFIG_124M)
total_params = sum(p.numel() for p in model.parameters())
print(f"Total parameters: {total_params:,}")
# Total parameters: 163,009,536

# 注意:实际参数量 (163M) 大于名称中的 124M
# 因为原始 GPT-2 将 token 嵌入层的权重与输出层共享 (weight tying)
# 去掉重复计算后:163M - 50257*768 = 124.4M ≈ 124M
total_params_no_tying = total_params - model.tok_emb.weight.numel()
print(f"Without weight tying: {total_params_no_tying:,}")
# Without weight tying: 124,412,160

ℹ️ Weight Tying 技巧:GPT-2 让 tok_emb(token 嵌入层)和 out_head(输出线性层)共享同一组权重。这不仅减少了 ~39M 参数,而且在语义上也合理 — 嵌入空间中「相近」的词,在输出时也应该有相似的概率分布。


5. 为什么这很重要

本章的核心收获不仅是实现了一个 GPT 模型,更是理解了它的设计哲学

  • 模块化设计:GPT 的全部复杂性被封装在 TransformerBlock 中。改变模型规模只需要调整配置参数,代码不需要改动。这种设计使得 scaling 变得简单直接。
  • Scaling Laws:从 124M 到 1558M,GPT-2 的四个变体验证了一个关键发现 — 增加参数量(特别是维度和层数)能持续提升模型能力。这后来被 Kaplan et al. 系统化为 Scaling Laws。
  • 参数量估算:理解参数量的构成(嵌入层占多少、注意力占多少、FFN 占多少)对于评估模型的计算和内存需求至关重要。FFN 的 4× 扩展是参数量的主要来源。
  • Pre-Norm vs Post-Norm:GPT-2 使用 Pre-Norm 是经过实践验证的选择。后来的 LLaMA 等模型进一步采用了 RMSNorm(去掉了 mean 的计算),更加高效。
  • 权重共享:Weight Tying 是一个巧妙的工程优化。在大模型中,嵌入层参数可能占比更高(因为词汇表大小不随模型缩放),这个技巧的价值更大。

ℹ️ 总结 — 第4章 GPT 模型组装:Token Embedding + Positional Embedding → Dropout → 12× TransformerBlock (Pre-LayerNorm → Multi-Head Causal Attention → Residual + Pre-LayerNorm → FeedForward → Residual) → Final LayerNorm → Linear Output Head。此时模型的权重都是随机初始化的 — 下一章我们将学习如何通过预训练让它真正学会语言。

本文是 Build a Large Language Model From Scratch (Sebastian Raschka) 的学习笔记。所有配图版权归原作者所有。代码基于原书示例,有简化和中文注释。

← Ch3: 注意力机制Ch5: 预训练 →

Related Posts