1. 项目概述
这个教学版GPT实现是一个专为初学者设计的Transformer模型教学项目。它用纯Python编写,不依赖任何外部库,完整实现了从自动微分系统到模型训练的全流程。我特别喜欢这种"从零开始"的实现方式,因为它能让我们真正理解GPT模型的核心原理,而不是简单地调用现成的深度学习框架。
这个实现基于Andrej Karpathy的microGPT代码,但添加了大量教学注释和解释。我在实际教学中发现,很多学生对Transformer的理解停留在表面,而这个项目正好能解决这个问题。它用不到500行代码就展示了GPT的核心机制,包括:
- 字符级分词处理
- 自动微分系统
- Transformer架构(注意力机制、MLP块)
- 训练流程(前向传播、反向传播、参数更新)
- 文本生成功能
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心组件解析
2.1 自动微分系统
这个项目最精彩的部分之一就是它实现了一个简易的自动微分系统。在常规深度学习中,我们通常直接使用PyTorch或TensorFlow的自动微分功能,但这里我们自己实现了这个机制:
python复制class Value:
"""自动微分的核心类"""
__slots__ = ('data', 'grad', '_children', '_local_grads')
def __init__(self, data, children=(), local_grads=()):
self.data = data # 节点值
self.grad = 0 # 梯度值
self._children = children # 子节点
self._local_grads = local_grads # 局部梯度
def backward(self):
# 构建拓扑排序
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._children:
build_topo(child)
topo.append(v)
build_topo(self)
# 反向传播计算梯度
self.grad = 1
for v in reversed(topo):
for child, local_grad in zip(v._children, v._local_grads):
child.grad += local_grad * v.grad
这个实现虽然简单,但包含了自动微分的所有关键要素:
- 计算图构建(通过运算符重载)
- 拓扑排序
- 链式法则应用
我在教学中发现,学生通过这个实现能更好地理解反向传播的本质。比如,为什么梯度是从输出往输入反向传播?为什么需要拓扑排序?这些在常规框架中被隐藏的细节,在这里都清晰可见。
2.2 Transformer架构
项目的Transformer实现虽然简化,但保留了所有关键组件:
python复制def gpt(token_id, pos_id, keys, values):
# 1. 嵌入层
tok_emb = state_dict['wte'][token_id] # 词嵌入
pos_emb = state_dict['wpe'][pos_id] # 位置嵌入
x = [t + p for t, p in zip(tok_emb, pos_emb)]
x = rmsnorm(x)
# 2. Transformer层
for li in range(n_layer):
# 注意力机制
x_residual = x
x = rmsnorm(x)
q = linear(x, state_dict[f'layer{li}.attn_wq'])
k = linear(x, state_dict[f'layer{li}.attn_wk'])
v = linear(x, state_dict[f'layer{li}.attn_wv'])
# 多头注意力计算
x_attn = []
for h in range(n_head):
# 计算注意力分数
attn_logits = [sum(q_h[j]*k_h[t][j] for j in range(head_dim))/head_dim**0.5
for t in range(len(k_h))]
attn_weights = softmax(attn_logits)
# 加权求和
head_out = [sum(attn_weights[t]*v_h[t][j] for t in range(len(v_h)))
for j in range(head_dim)]
x_attn.extend(head_out)
# 残差连接
x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])
x = [a + b for a, b in zip(x, x_residual)]
# MLP块
x_residual = x
x = rmsnorm(x)
x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
x = [xi.relu() for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
x = [a + b for a, b in zip(x, x_residual)]
# 3. 输出层
return linear(x, state_dict['lm_head'])
这个实现有几个教学亮点:
- 清晰地展示了Transformer层的结构:注意力→残差连接→MLP→残差连接
- 实现了简化的多头注意力机制
- 使用了RMSNorm而不是常规的LayerNorm
- 保留了位置编码的关键设计
3. 训练流程详解
3.1 数据准备
项目使用了一个名字数据集,这非常适合教学目的:
python复制# 下载数据集
if not os.path.exists('input.txt'):
import urllib.request
names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt'
urllib.request.urlretrieve(names_url, 'input.txt')
# 读取并处理数据
docs = [line.strip() for line in open('input.txt') if line.strip()]
random.shuffle(docs)
这种小规模数据集有几个优势:
- 训练速度快,适合教学演示
- 字符级分词简单直观
- 生成结果容易评估
3.2 训练循环
训练过程实现了完整的反向传播和参数更新:
python复制for step in range(num_steps):
# 准备数据
doc = docs[step % len(docs)]
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
n = min(block_size, len(tokens) - 1)
# 前向传播
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
losses = []
for pos_id in range(n):
token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
logits = gpt(token_id, pos_id, keys, values)
probs = softmax(logits)
loss_t = -probs[target_id].log()
losses.append(loss_t)
loss = (1 / n) * sum(losses)
# 反向传播
loss.backward()
# 参数更新(Adam优化器)
lr_t = learning_rate * (1 - step / num_steps)
for i, p in enumerate(params):
m[i] = beta1 * m[i] + (1 - beta1) * p.grad
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
m_hat = m[i] / (1 - beta1 ** (step + 1))
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
p.grad = 0
这个训练循环包含了几个关键教学点:
- 完整的训练流程:前向→损失计算→反向→更新
- 实现了Adam优化器
- 包含学习率衰减
- 清晰地展示了梯度清零的重要性
4. 文本生成与参数调优
4.1 生成算法
项目实现了基于温度参数的采样生成:
python复制temperature = 0.5 # 控制生成多样性
for sample_idx in range(20):
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
token_id = BOS # 开始标记
sample = []
for pos_id in range(block_size):
logits = gpt(token_id, pos_id, keys, values)
probs = softmax([l / temperature for l in logits])
token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
if token_id == BOS: break
sample.append(uchars[token_id])
print(f"样本 {sample_idx+1:2d}: {''.join(sample)}")
温度参数的作用:
- temperature < 1.0:模型更保守,选择高概率token
- temperature > 1.0:模型更冒险,增加多样性
- temperature = 1.0:标准采样
4.2 参数调优经验
在实际教学中,我发现几个关键参数需要特别注意:
- 学习率:0.01对于这个小模型来说偏大,建议从0.001开始尝试
- 嵌入维度:n_embd=16可能太小,增加到32或64效果更好
- 训练步数:1000步对于名字生成足够,但复杂任务需要更多
- 温度参数:0.5-0.8之间通常能产生合理且多样的结果
5. 教学实践心得
在使用这个项目进行教学时,我总结了几个特别有效的教学方法:
-
分步演示:不要一次性展示全部代码,而是按组件逐步构建:
- 先实现自动微分
- 然后构建最简单的线性模型
- 逐步添加注意力机制
- 最后完成完整Transformer
-
可视化辅助:用图形展示计算图和注意力权重,帮助学生理解信息流动
-
调试技巧:
- 在前向传播后检查激活值的范围
- 监控梯度大小是否合理
- 使用固定的小批量数据验证过拟合能力
-
扩展练习:
- 尝试增加层数(n_layer)
- 修改为单词级分词
- 添加dropout等正则化技术
- 实现beam search生成算法
这个教学项目最宝贵的不是代码本身,而是它揭示的深度学习本质。通过从零实现,学生能真正理解现代语言模型的核心原理,而不仅仅是学会调用API。我在实际教学中发现,经过这样的训练后,学生使用PyTorch等框架时也更加得心应手,因为他们理解了底层机制。
