参考 nanoGPT 学习 GPT 的基本原理 参考 nanoGPT 學習 GPT 的基本原理 参考 nanoGPT 学习 GPT 的基本原理
这篇文章整理自自然语言处理课程实验报告,主要结合 GPT 原始论文和 src/gpt/gpt_reference.py 的实现,理解 GPT 的模型结构、关键模块与生成流程。这里的实现可以看作一个单文件版本的 decoder-only Transformer:模型接收前文 token,并预测下一个 token。
一、实验目的
- 理解 GPT 模型的基本概念、发展背景及其在自然语言生成任务中的应用意义。
- 掌握自回归语言模型的基本原理,理解基于上下文预测下一个 token 的建模思想。
- 掌握 Transformer 解码器结构的核心组成,包括词嵌入、位置编码、多头自注意力机制、前馈神经网络、残差连接和层归一化。
- 理解因果注意力掩码在 GPT 模型中的作用,掌握它防止模型利用未来信息的基本原理。
- 能够阅读 nanoGPT 的核心代码结构,理解模型定义中各模块的功能及其相互关系。
- 能够将源码实现与 GPT 原始论文中的描述对应起来,形成对 GPT 基本原理的系统性理解。
二、实验内容
- 了解 nanoGPT 项目的基本结构和主要功能。
- 学习 GPT 自回归语言模型和 Transformer 解码器的基本原理。
- 阅读 GPT 参考源码,理解词嵌入、位置编码、注意力、前馈网络、残差连接和层归一化等模块。
- 结合 GPT 原始论文,对源码中的关键实现进行对照分析。
- 整理源码阅读笔记和理论总结,不要求实际训练或运行模型。
三、实验要求
- 能够理解 GPT 模型的主要结构和基本工作流程。
- 能够阅读 nanoGPT 的核心代码并说明其主要作用。
- 能够将源码实现与 GPT 原始论文中的概念进行对应说明。
- 能够记录源码阅读过程、关键理解点和个人总结。
四、实验步骤
- 下载并查看 nanoGPT 项目,熟悉主要文件结构。
- 阅读 GPT 原始论文,梳理模型提出的背景、目标和核心方法。
- 阅读数据处理、模型定义和推理相关代码,重点分析 GPT 的关键组成部分。
- 将源码中的实现与论文中的概念逐项对应,整理阅读笔记。
- 总结 GPT 的基本原理、结构特点和个人理解。
五、代码实现及配置
本次实验主要结合 GPT 原始论文与 src/gpt/gpt_reference.py 的实现,梳理 GPT 的模型结构、关键模块和生成流程。学习过程中还参考了 The Illustrated GPT Transformer。
5.1 模型整体结构
GPT 的网络结构包含词嵌入层、位置嵌入层、多层 Transformer Block 和输出层,其中多层 Transformer Block 是最核心的部分。每一个 Block 本质上就是 Transformer Decoder,从前到后分别是多头注意力层、层归一化、MLP 层和层归一化;与 Encoder 相连的交叉注意力层在 GPT 中被省略。

上图从输入开始,经过 Embedding 和 Positional Encoding,依次通过多层 Decoder Layer,最后由 Linear 与 Softmax 得到输出概率。

第二张图把 GPT 标在完整 Transformer 结构的右侧:它只保留了解码器路径,并使用 masked multi-head attention 保证自回归预测。
5.2 因果自注意力
GPT 的关键在于因果自注意力。在 CausalSelfAttention 中,输入首先通过一个线性层一次性生成 q、k、v,然后拆分成多个头并行计算注意力权重。由于 GPT 是自回归语言模型,任意位置只能利用当前位置左侧的历史信息,因此注意力矩阵必须施加因果掩码。

多头注意力把同一个输入分别映射为多个 Q、K、V 子空间,在每个头中计算 scaled dot-product attention,再把各头结果拼接并线性变换。

因果掩码把允许关注的位置保留为 0,把未来位置设为 -inf;经过 softmax 后,未来位置的权重就变成了 0。报告中的记号写作“让 Q >= K 的 Attention 不变,把 Q <= K 的 Attention 置为负无穷”,核心含义是每个 token 只能看到自己和之前的 token。
这也解释了 GPT 与 BERT 的侧重点差异:GPT 不能利用下文信息,在需要理解全文的任务中可能错失一部分信息;BERT 因此更适合理解型任务,而 GPT 在生成型任务上表现更好。即使在大模型时代,一些倚重文本理解的任务仍可以优先考虑 BERT 一类的双向编码器。
nanoGPT 中因果注意力的核心实现如下:
class CausalSelfAttention(nn.Module):
def forward(self, x):
B, T, C = x.size()
q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
if self.flash:
y = torch.nn.functional.scaled_dot_product_attention(
q, k, v, attn_mask=None,
dropout_p=self.dropout if self.training else 0,
is_causal=True
)
else:
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
att = F.softmax(att, dim=-1)
att = self.attn_dropout(att)
y = att @ v
y = y.transpose(1, 2).contiguous().view(B, T, C)
y = self.resid_dropout(self.c_proj(y))
return y
5.3 Transformer Block 当中的其他部分
除了多头注意力层之外,每个 Transformer Block 还有两个层归一化层和一个 MLP 层。层归一化把隐藏状态调整到更稳定的分布,帮助训练过程保持数值稳定。经过 Attention 后,结果会通过残差连接加回隐藏状态;因此进入 MLP 时,表示已经包含了上下文信息。
MLP 的作用是在已有上下文中进一步学习特征,对每个 token 的隐藏向量执行“升维 -> GELU -> 降维”,通过非线性变换提取更复杂的特征:
class MLP(nn.Module):
def forward(self, x):
x = self.c_fc(x)
x = self.gelu(x)
x = self.c_proj(x)
x = self.dropout(x)
return x
5.4 前向传播与语言建模目标
GPT.forward() 是整套模型的主流程。输入 idx 先经过 token embedding 和 position embedding 相加,再送入多层 Block,最后通过 ln_f 和 lm_head 得到词表上的 logits。训练时如果传入 targets,使用交叉熵损失;推理时只取最后一个位置的 logits,用于下一 token 预测。
def forward(self, idx, targets=None):
device = idx.device
b, t = idx.size()
pos = torch.arange(0, t, dtype=torch.long, device=device)
tok_emb = self.transformer.wte(idx)
pos_emb = self.transformer.wpe(pos)
x = self.transformer.drop(tok_emb + pos_emb)
for block in self.transformer.h:
x = block(x)
x = self.transformer.ln_f(x)
if targets is not None:
logits = self.lm_head(x)
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)),
targets.view(-1),
ignore_index=-1
)
else:
logits = self.lm_head(x[:, [-1], :])
loss = None
return logits, loss
5.5 自回归生成
generate() 实现了 GPT 的逐 token 生成过程。每一步都会把当前上下文截断到 block_size 范围内,前向得到最后位置的分布后,再通过温度缩放和可选的 top-k 采样选出下一个 token,最后把新 token 拼接回输入序列继续生成。
@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
for _ in range(max_new_tokens):
idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature
if top_k is not None:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = -float('Inf')
probs = F.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
idx = torch.cat((idx, idx_next), dim=1)
return idx
总结
从 nanoGPT 的实现回看 GPT,可以把它概括为一条清晰的链路:token embedding 与 position embedding 提供输入表示,多层 decoder-only Transformer Block 逐层融合上下文,因果掩码保证训练目标符合自回归约束,最后由语言模型头输出下一个 token 的概率分布。阅读源码时,把每个类和函数与论文中的概念逐项对应,比单独记忆公式更容易形成对 GPT 工作流程的整体理解。