从零实现GPT模型:LLMs-from-scratch第四章实战指南
你是否曾好奇那些能生成流畅文本的AI模型背后的原理?想亲手构建一个属于自己的GPT模型却不知从何下手?本文将带你通过LLMs-from-scratch项目的第四章内容,一步步实现一个简化版GPT模型,无需深厚的AI背景,只需基础的Python知识就能轻松上手。读完本文后,你将掌握GPT模型的核心架构、关键组件实现以及文本生成的基本流程,能够运行自己的小型语言模型并生成简单文本。
项目准备与环境配置
在开始实现GPT模型之前,我们需要先获取项目代码并确保开发环境正确配置。本项目基于PyTorch框架开发,需要安装的主要依赖包括PyTorch、tiktoken和matplotlib等。
首先,克隆项目仓库到本地:
git clone https://gitcode.com/GitHub_Trending/ll/LLMs-from-scratch
cd LLMs-from-scratch
项目的第四章代码位于ch04/01_main-chapter-code目录下,主要文件包括:
- ch04.ipynb:包含章节所有代码的Jupyter笔记本
- gpt.py:独立的GPT模型实现脚本
- previous_chapters.py:包含前几章实现的辅助模块
建议使用Jupyter Notebook打开ch04.ipynb文件,以便交互式地学习和运行代码。在运行代码前,可以先检查环境依赖是否满足:
from importlib.metadata import version
print("matplotlib version:", version("matplotlib"))
print("torch version:", version("torch"))
print("tiktoken version:", version("tiktoken"))
GPT模型架构概览
GPT(Generative Pre-trained Transformer)模型基于Transformer架构的解码器部分构建,主要由嵌入层、多个Transformer块和输出层组成。其核心思想是通过自注意力机制捕捉文本序列中的长距离依赖关系,从而实现高质量的文本生成。
THE 0TH POSITION OF THE ORIGINAL IMAGE
一个典型的GPT模型包含以下几个关键部分:
- 嵌入层(Embedding Layer):将输入的文本 token 转换为向量表示
- 位置编码(Positional Encoding):为模型提供序列中 token 的位置信息
- Transformer块:由多头自注意力机制和前馈神经网络组成
- 输出层:将模型的隐藏状态映射到词汇表大小的输出空间
在本章中,我们将实现一个类似GPT-2的模型架构,具体配置如下:
GPT_CONFIG_124M = {
"vocab_size": 50257, # 词汇表大小
"context_length": 1024, # 上下文长度
"emb_dim": 768, # 嵌入维度
"n_heads": 12, # 注意力头数量
"n_layers": 12, # Transformer块数量
"drop_rate": 0.1, # Dropout比率
"qkv_bias": False # 是否使用QKV偏置
}
这个配置近似于GPT-2的小型版本(124M参数),适合在普通电脑上进行实验和学习。
核心组件实现
1. 层归一化(Layer Normalization)
层归一化是LLM中的关键技术,它通过将每一层的输入标准化处理,加速模型训练并提高稳定性。与批归一化不同,层归一化是对每个样本的特征维度进行归一化,而不是对批次维度。
THE 1TH POSITION OF THE ORIGINAL IMAGE
层归一化的实现代码如下:
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
这里需要注意的是,我们添加了两个可学习的参数scale和shift,允许模型在归一化后调整特征分布。同时,为了避免除零错误,我们在计算平方根时添加了一个小的epsilon值。
2. GELU激活函数
GPT模型中使用了GELU(Gaussian Error Linear Unit)作为激活函数,它比传统的ReLU函数更加平滑,有助于改善模型的训练效果和泛化能力。
GELU的数学表达式为:$GELU(x) = x \Phi(x)$,其中$\Phi(x)$是标准正态分布的累积分布函数。在实际实现中,通常使用以下近似公式:
class GELU(nn.Module):
def __init__(self):
super().__init__()
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))
))
3. 前馈神经网络
Transformer块中的前馈神经网络(Feed Forward Network)由两个线性层和一个激活函数组成,用于对注意力机制的输出进行非线性变换。
class FeedForward(nn.Module):
def __init__(self, cfg):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
GELU(),
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
)
def forward(self, x):
return self.layers(x)
前馈神经网络首先将输入特征维度扩展4倍,经过GELU激活后再压缩回原来的维度,这样的设计有助于模型学习更复杂的特征表示。
4. Transformer块
Transformer块是GPT模型的核心组件,每个块包含一个多头自注意力子层和一个前馈神经网络子层,每个子层都配有残差连接和层归一化。
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):
# 注意力子层的残差连接
shortcut = x
x = self.norm1(x)
x = self.att(x)
x = self.drop_shortcut(x)
x = x + shortcut
# 前馈子层的残差连接
shortcut = x
x = self.norm2(x)
x = self.ff(x)
x = self.drop_shortcut(x)
x = x + shortcut
return x
注意这里采用了"预归一化"(Pre-LayerNorm)的设计,即在每个子层之前应用层归一化,而不是原始Transformer论文中的"后归一化"设计。这种修改可以使模型训练更加稳定。
GPT完整模型实现
将上述组件组合起来,我们就可以构建完整的GPT模型了。GPT模型的整体结构包括嵌入层、位置编码、多个Transformer块和输出层。
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"])
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
tok_embeds = self.tok_emb(in_idx)
pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
x = tok_embeds + pos_embeds # token嵌入与位置嵌入相加
x = self.drop_emb(x)
x = self.trf_blocks(x)
x = self.final_norm(x)
logits = self.out_head(x)
return logits
模型的前向传播过程如下:
- 将输入的token索引转换为token嵌入
- 添加位置嵌入以提供序列位置信息
- 通过多个Transformer块进行特征提取
- 应用最后的层归一化
- 通过输出层映射到词汇表大小的logits
文本生成实现
有了完整的GPT模型后,我们需要实现文本生成功能。文本生成的基本原理是从初始文本(提示词)开始,不断预测下一个token并将其添加到输入序列中,重复这一过程直到生成指定长度的文本。
def generate_text_simple(model, idx, max_new_tokens, context_size):
# idx是当前上下文的(B, T)数组
for _ in range(max_new_tokens):
# 如果上下文长度超过模型支持的最大长度,截断为最后context_size个token
idx_cond = idx[:, -context_size:]
# 获取预测结果
with torch.no_grad(): # 禁用梯度计算以加速推理
logits = model(idx_cond)
# 只关注最后一个时间步的预测结果
logits = logits[:, -1, :]
# 选择概率最高的token作为下一个token(贪婪解码)
idx_next = torch.argmax(logits, dim=-1, keepdim=True)
# 将新生成的token添加到序列中
idx = torch.cat((idx, idx_next), dim=1)
return idx
上述实现使用了贪婪解码策略,即每次都选择概率最高的token。虽然这种方法简单高效,但生成的文本多样性可能不足。在实际应用中,还可以使用温度采样、Top-K采样等更高级的解码策略来改善生成效果。
运行与测试
现在我们可以测试我们实现的GPT模型了。首先创建模型实例并加载配置:
GPT_CONFIG_124M = {
"vocab_size": 50257, # 词汇表大小
"context_length": 1024, # 上下文长度
"emb_dim": 768, # 嵌入维度
"n_heads": 12, # 注意力头数量
"n_layers": 12, # Transformer块数量
"drop_rate": 0.1, # Dropout比率
"qkv_bias": False # 是否使用QKV偏置
}
torch.manual_seed(123) # 设置随机种子以确保结果可复现
model = GPTModel(GPT_CONFIG_124M)
model.eval() # 将模型设置为评估模式(禁用dropout)
接下来准备输入文本并进行编码:
import tiktoken
start_context = "Hello, I am"
tokenizer = tiktoken.get_encoding("gpt2") # 使用GPT-2的tokenizer
encoded = tokenizer.encode(start_context)
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # 添加批次维度
最后生成文本并解码输出结果:
out = generate_text_simple(
model=model,
idx=encoded_tensor,
max_new_tokens=10,
context_size=GPT_CONFIG_124M["context_length"]
)
decoded_text = tokenizer.decode(out.squeeze(0).tolist())
print("生成的文本:", decoded_text)
需要注意的是,我们这里使用的是随机初始化的模型权重,因此生成的文本可能没有实际意义。在实际应用中,需要先在大规模文本语料上预训练模型,或者加载预训练好的权重(如GPT-2的权重)才能获得有意义的生成结果。项目的后续章节将介绍如何训练模型和加载预训练权重。
总结与扩展
通过本章的学习,我们从零开始实现了一个简化版的GPT模型,包括核心组件如多头自注意力机制、层归一化、前馈神经网络等,以及完整的文本生成流程。这个实现虽然简单,但涵盖了GPT模型的基本原理和关键技术。
如果想进一步改进模型,可以考虑以下几个方向:
- 实现更高级的解码策略以提高生成文本质量
- 添加KV缓存(Key-Value Cache)以加速长文本生成
- 实现模型并行以支持更大规模的模型
- 添加LoRA等参数高效微调方法
项目的ch04/03_kv-cache目录提供了KV缓存的实现示例,可以显著提高长序列生成的效率。此外,ch05和后续章节将介绍模型训练、微调等更高级的主题。
希望通过本文的指南,你能够深入理解GPT模型的工作原理,并能够基于LLMs-from-scratch项目进行进一步的学习和探索。动手实践是学习AI最好的方式,不妨尝试修改模型配置或实现新的功能,看看它们如何影响模型的行为和性能。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



