最适合入门的 GPT 训练代码nanoGPT 项目
nanoGPT是一个用于训练和微调中等规模GPT(Generative Pre-trained Transformer)模型的代码库。它的作者 Andrej Karpathy(前特斯拉AI总监,OpenAI创始成员之一)设计这个项目的核心目的是教学。它摒弃了大型框架(如Hugging Face Transformers)中的大量抽象和复杂性,用几百行纯粹的、易于阅读的PyTorch代码,为你揭示G
我非常乐意为你深入讲解 Andrej Karpathy 的 nanoGPT 项目(https://github.com/karpathy/nanoGPT)。这是一个极好的学习资源,因为它用最简洁、最核心的代码实现了一个完整的GPT模型。
我会从高层次的概述开始,然后深入到代码的每个关键部分,解释其工作原理和设计思想。
1. 高层次概述:nanoGPT是什么?
nanoGPT 是一个用于训练和微调中等规模GPT(Generative Pre-trained Transformer)模型的代码库。它的作者 Andrej Karpathy(前特斯拉AI总监,OpenAI创始成员之一)设计这个项目的核心目的是教学。
它摒弃了大型框架(如Hugging Face Transformers)中的大量抽象和复杂性,用几百行纯粹的、易于阅读的PyTorch代码,为你揭示GPT模型内部的每一个细节。
核心特点:
-
简洁性 (Simplicity): 核心模型定义在 model.py 一个文件中,代码量小,可读性极强。
-
教育性 (Educational): 它是学习Transformer和GPT架构的最佳起点之一。Karpathy 还有一个配套的 YouTube教学视频,手把手从零构建它。
-
完整性 (Complete): 项目包含了数据预处理、模型定义、训练、生成(推理)的所有环节。
-
可复现性 (Reproducible): 它能够复现OpenAI GPT-2 (124M) 模型的性能,证明了这个小巧的代码库是正确且有效的。
2. 代码库结构解析
我们先来看一下这个项目的目录结构,了解每个文件是做什么的:
code Code
downloadcontent_copy
expand_less
nanoGPT/
├── model.py # 核心!定义了GPT模型架构的所有组件。
├── train.py # 训练脚本,负责加载数据、模型、优化器,并执行训练循环。
├── sample.py # 生成脚本,加载一个训练好的模型来生成文本。
├── data/ # 数据处理相关
│ └── shakespeare_char/
│ ├── prepare.py # 下载莎士比亚数据集并进行分词(tokenize)的脚本。
│ └── input.txt # 原始数据。
├── config/ # 存放不同实验的配置文件。
├── gpt2.py # 用于加载OpenAI官方GPT-2权重的辅助脚本。
└── ... (其他文件如 .gitignore, LICENSE, README.md)
我们的讲解将主要聚焦于 model.py、train.py 和 data/shakespeare_char/prepare.py。
3. 深入 model.py:GPT模型的心脏
这是整个项目最核心的文件。它从零开始构建了一个GPT模型。我们自底向上地来看它的构成。
3.1 CausalSelfAttention (因果自注意力)
这是Transformer模型最关键的创新点。它让模型在处理一个词(token)时,能够“关注”到句子中其他相关的词,并根据这些词来调整当前词的表示。
code Python
downloadcontent_copy
expand_less
IGNORE_WHEN_COPYING_START
IGNORE_WHEN_COPYING_END
class CausalSelfAttention(nn.Module):
def __init__(self, config):
# ...
# K, Q, V 的线性投影层 (一个大的线性层完成,效率更高)
self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
# 输出的线性投影层
self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
# ...
# 因果掩码 (causal mask)
self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
.view(1, 1, config.block_size, config.block_size))
def forward(self, x):
# ...
# 1. 计算 Q, K, V
q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
# ... (reshape for multi-head)
# 2. 计算注意力分数: (Q * K^T) / sqrt(d_k)
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
# 3. 应用因果掩码
att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
# 4. Softmax
att = F.softmax(att, dim=-1)
# 5. 用注意力分数加权 V
y = att @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs)
# ... (reshape back)
# 6. 输出投影
y = self.c_proj(y)
return y
核心思想解读:
-
Q, K, V (Query, Key, Value): 输入的每个 token 都会被一个线性层 (c_attn) 映射成三个向量:Query、Key 和 Value。
-
Query (Q): 代表当前 token "想要查询"什么信息。
-
Key (K): 代表其他 token "携带"什么信息(的关键标识)。
-
Value (V): 代表其他 token "实际"的信息内容。
-
-
注意力分数: 通过计算当前 token 的 Q 与所有其他 token 的 K 的点积(q @ k.transpose(...)),来衡量当前 token 对其他 token 的"关注度"。
-
因果掩码 (Causal Mask): 这是GPT与BERT等模型的一个关键区别。GPT是一个自回归模型,预测下一个词时只能看到前面的词,不能看到未来的词。这个掩码 (self.bias) 是一个下三角矩阵,它将所有未来的位置设置为负无穷大,这样在 softmax 之后,这些位置的注意力权重就变成了0。
-
加权求和: 将计算出的注意力分数(权重)与所有 token 的 V 进行加权求和,得到当前 token 融合了上下文信息后的新表示。
-
多头 (Multi-Head): 代码中通过 reshape 操作将Q, K, V 分成多个“头”(n_head)。这允许模型在不同的“表示子空间”中同时学习不同类型的关系。比如一个头可能关注语法关系,另一个头关注语义关系。
3.2 MLP (多层感知机)
在自注意力层之后,每个 token 的输出会经过一个简单的前馈神经网络(Feed-Forward Network)。
code Python
downloadcontent_copy
expand_less
IGNORE_WHEN_COPYING_START
IGNORE_WHEN_COPYING_END
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.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
self.dropout = nn.Dropout(config.dropout)
# 使用 GELU 激活函数
self.gelu = nn.GELU()
def forward(self, x):
x = self.c_fc(x)
x = self.gelu(x)
x = self.c_proj(x)
x = self.dropout(x)
return x
作用: 这部分可以被看作是模型的"思考"或"处理"单元。自注意力层负责信息的融合,而MLP层则对融合后的信息进行更复杂的非线性变换和加工。通常,中间层的维度会扩大4倍,然后再压缩回来。
3.3 Block (Transformer 块)
一个标准的Transformer块由一个自注意力层和一个MLP层组成。
code Python
downloadcontent_copy
expand_less
IGNORE_WHEN_COPYING_START
IGNORE_WHEN_COPYING_END
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):
# 残差连接 (Residual Connection)
x = x + self.attn(self.ln_1(x))
x = x + self.mlp(self.ln_2(x))
return x
关键设计:
-
LayerNorm (层归一化): 在进入注意力层和MLP层之前,对数据进行归一化。这有助于稳定训练过程。
-
Residual Connection (残差连接): x = x + ... 这种结构。它允许梯度在反向传播时直接流过,极大地缓解了深度神经网络中的梯度消失问题,使得训练非常深的模型成为可能。
3.4 GPT (最终模型)
最后,GPT 类将所有组件组装在一起。
code Python
downloadcontent_copy
expand_less
IGNORE_WHEN_COPYING_START
IGNORE_WHEN_COPYING_END
class GPT(nn.Module):
def __init__(self, config):
super().__init__()
# ...
self.transformer = nn.ModuleDict(dict(
wte = nn.Embedding(config.vocab_size, config.n_embd), # Token 嵌入
wpe = nn.Embedding(config.block_size, config.n_embd), # Position 嵌入
drop = nn.Dropout(config.dropout),
h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]), # 多个 Block 堆叠
ln_f = LayerNorm(config.n_embd, bias=config.bias),
))
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False) # 输出层
# ...
def forward(self, idx, targets=None):
# ...
tok_emb = self.transformer.wte(idx) # (B, T, n_embd)
pos_emb = self.transformer.wpe(torch.arange(0, T, dtype=torch.long, device=device)) # (T, n_embd)
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:
# 推理时,只计算最后一个 token 的 logits
logits = self.lm_head(x[:, [-1], :]) # (B, 1, vocab_size)
loss = None
return logits, loss
组装流程:
-
输入 (idx): 一个由 token ID 组成的序列,形状为 (batch_size, sequence_length)。
-
嵌入层:
-
wte (Word Token Embedding): 将每个 token ID 转换为一个高维向量。
-
wpe (Word Position Embedding): 为每个位置(0, 1, 2...)创建一个向量。Transformer本身没有顺序概念,位置编码将序列的顺序信息注入模型。
-
将两者相加,得到既包含内容又包含位置信息的初始输入。
-
-
Transformer Blocks (h): 将嵌入后的向量输入一个由 n_layer 个 Block 堆叠而成的网络中。每一层都会对输入序列进行更深层次的特征提取和信息融合。
-
输出层 (lm_head): 经过所有Block处理后,最终的输出向量通过一个线性层 (lm_head) 映射到词汇表的大小。这个输出称为 logits,它代表了在当前位置,词汇表中每个词出现的可能性得分。
-
损失计算: 在训练时,使用 cross_entropy (交叉熵) 损失函数来比较模型预测的logits和真实的下一个词 targets。这个损失值会用于反向传播,更新模型的所有参数。
4. 深入 train.py:模型的训练师
这个脚本是训练模型的总指挥。
主要步骤:
-
配置加载: 从 config/ 目录加载超参数,如学习率、批大小、层数等。
-
数据加载:
-
调用 get_batch 函数从预处理好的数据 (train.bin, val.bin) 中随机采样一小批数据。
-
get_batch 的巧妙之处在于,它取一个数据块 x 作为输入,然后取同一个数据块向右移动一位的 y 作为目标。这就是在教模型:看到 x 时,要预测出 y。
-
-
模型初始化: 根据配置实例化一个 GPT 模型。
-
优化器: 通常使用 AdamW 优化器,它在训练Transformer时表现很好。
-
训练循环 (Training Loop):
-
for iter in range(max_iters):
-
X, Y = get_batch('train'):获取一批训练数据。
-
logits, loss = model(X, Y):前向传播 (Forward Pass),计算模型的预测和损失。
-
optimizer.zero_grad(set_to_none=True):清空上一轮的梯度。
-
loss.backward():反向传播 (Backward Pass),计算损失函数对模型每个参数的梯度。
-
optimizer.step():参数更新,优化器根据计算出的梯度来更新模型的权重。
-
学习率衰减 (Learning Rate Decay): 随着训练的进行,逐渐降低学习率,有助于模型收敛到更好的点。
-
评估 (Evaluation): 定期在验证集上计算损失,以监控模型是否过拟合。
-
保存模型 (Checkpointing): 定期保存模型的权重,以便中断后可以继续训练,或者用于后续的推理。
-
5. data/shakespeare_char/prepare.py:数据的准备
这个脚本展示了语言模型训练的第一步:数据预处理。
主要步骤:
-
下载数据: 下载莎士比亚作品集 input.txt。
-
构建词汇表 (Vocabulary):
-
对于这个char-level(字符级)模型,它会统计文本中所有出现过的独立字符。
-
创建一个 stoi (string to integer) 字典和一个 itos (integer to string) 字典,用于字符和整数ID之间的相互转换。
-
-
编码 (Encode): 遍历整个文本,使用 stoi 将每个字符转换成对应的整数ID。
-
划分数据集: 将编码后的数据按90%/10%的比例划分为训练集和验证集。
-
保存为二进制文件: 使用numpy.memmap将处理好的数据保存为 .bin 文件。这种格式可以快速地从磁盘加载数据,而不需要把整个数据集都读入内存。
总结与学习建议
nanoGPT 的价值在于它揭示了 "魔法" 背后的数学和代码。
-
从 model.py 开始: 通读并理解 CausalSelfAttention, Block, GPT 这几个类的forward方法,这是理解数据如何在模型中流动的关键。
-
连接 train.py: 理解 train.py 中的训练循环是如何调用 model 并利用 loss 来更新参数的。
-
动手实践:
-
克隆仓库: git clone https://github.com/karpathy/nanoGPT.git
-
准备数据: cd nanoGPT/data/shakespeare_char && python prepare.py
-
开始训练: cd ../.. && python train.py config/train_shakespeare_char.py
-
生成文本: python sample.py --out_dir=out-shakespeare-char
-
当你能成功运行这个流程,并理解每一步背后的代码逻辑时,你就已经对GPT模型的工作原理有了非常扎实的理解。从这里出发,再去学习更复杂的模型和框架就会事半功倍。
可以结合这篇数学思维来看的《损失函数 L(W, b) ,与nanoGPT项目的源码对应起来说》
更多推荐
所有评论(0)