从0实现并理解GPT

莎士比亚为例创建一个快速的文本生成演示

cat > shakespeare_generator.py << 'EOF'
import torch
import torch.nn as nn
import pickle
import os

print("莎士比亚风格文本生成器")
print("=" * 50)

加载数据和词汇表

data_dir = 'data/shakespeare_char'
with open(os.path.join(data_dir, 'meta.pkl'), 'rb') as f:
meta = pickle.load(f)

获取编解码函数

chars = sorted(list(set(open(os.path.join(data_dir, 'input.txt'), 'r').read())))
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}

print(f"词汇表大小: {len(chars)}")
print(f"字符集: {''.join(chars[:20])}…")

def encode(s):
return [stoi[c] for c in s]

def decode(l):
return ''.join([itos[i] for i in l])

加载训练数据

train_data = torch.frombuffer(
open(os.path.join(data_dir, 'train.bin'), 'rb').read(),
dtype=torch.uint16
).long()

print(f"📖 训练数据长度: {len(train_data):,} tokens")

超简单的字符级语言模型

class SimpleGPT(nn.Module):
def init(self, vocab_size, n_embd=128, n_head=4, n_layer=3):
super().init()
self.embedding = nn.Embedding(vocab_size, n_embd)
self.pos_embedding = nn.Embedding(1000, n_embd)
self.transformer = nn.ModuleList([
nn.TransformerDecoderLayer(
d_model=n_embd,
nhead=n_head,
dim_feedforward=n_embd * 4,
batch_first=True,
dropout=0.1
) for _ in range(n_layer)
])
self.ln_f = nn.LayerNorm(n_embd)
self.head = nn.Linear(n_embd, vocab_size)

def forward(self, x):
    b, t = x.shape
    pos = torch.arange(0, t, dtype=torch.long).unsqueeze(0)

    x = self.embedding(x) + self.pos_embedding(pos)

    # 创建因果mask
    mask = torch.triu(torch.ones(t, t), diagonal=1).bool()

    for transformer in self.transformer:
        x = transformer(x, x, tgt_mask=mask)

    x = self.ln_f(x)
    logits = self.head(x)
    return logits

创建和训练模型

print("\n 创建模型…")
model = SimpleGPT(vocab_size=len(chars))
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)

print(f"模型参数: {sum(p.numel() for p in model.parameters()):,}")

快速训练

print("\n 快速训练…")
block_size = 32
model.train()

for step in range(50): # 只训练50步,快速看效果
ix = torch.randint(len(train_data) - block_size, (8,))
x = torch.stack([train_data[i:i+block_size] for i in ix])
y = torch.stack([train_data[i+1:i+block_size+1] for i in ix])

logits = model(x)
loss = nn.functional.cross_entropy(logits.view(-1, len(chars)), y.view(-1))

optimizer.zero_grad()
loss.backward()
optimizer.step()

if step % 10 == 0:
    print(f"  Step {step:2d}: loss = {loss.item():.4f}")

print("\n 开始生成莎士比亚风格文本…")

def generate_text(prompt="", max_tokens=200, temperature=0.8, top_k=20):
model.eval()

# 编码提示词
if prompt:
    tokens = encode(prompt)
else:
    tokens = [encode("ROMEO:")[0]]  # 默认以ROMEO开始

with torch.no_grad():
    for _ in range(max_tokens):
        # 取最后block_size个tokens
        context = torch.tensor([tokens[-block_size:]])
        logits = model(context)[0, -1, :]

        # 应用temperature
        logits = logits / temperature

        # Top-k采样
        if top_k > 0:
            indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
            logits[indices_to_remove] = float('-inf')

        probs = torch.softmax(logits, dim=-1)
        next_token = torch.multinomial(probs, 1).item()
        tokens.append(next_token)

        # 如果生成了换行符,可能是一个好的停止点
        if len(tokens) > 10 and next_token == encode('\n')[0]:
            break

return decode(tokens)

生成不同风格的文本

print("\n" + "="60) print(" 生成结果展示") print("="60)

print("\n1️⃣ 保守生成 (temperature=0.5, top_k=10):")
torch.manual_seed(42)
text1 = generate_text("HAMLET: To be or not to be", max_tokens=100, temperature=0.5, top_k=10)
print(f"'{text1}'")

print("\n2️⃣ 平衡生成 (temperature=0.8, top_k=20):")
torch.manual_seed(42)
text2 = generate_text("JULIET: Romeo, Romeo", max_tokens=100, temperature=0.8, top_k=20)
print(f"'{text2}'")

print("\n3️⃣ 创意生成 (temperature=1.2, top_k=30):")
torch.manual_seed(42)
text3 = generate_text("KING: What news", max_tokens=100, temperature=1.2, top_k=30)
print(f"'{text3}'")

print("\n4️⃣ 无提示生成:")
torch.manual_seed(123)
text4 = generate_text("", max_tokens=150, temperature=0.8, top_k=25)
print(f"'{text4}'")

print(f"\n 参数解释:")
print(f"• temperature越低 → 更保守,更像训练数据")
print(f"• temperature越高 → 更有创意,但可能更乱")
print(f"• top_k越小 → 只从最可能的k个字符中选择")
print(f"• 对于65字符的词汇表,top_k=5到30比较合理")

print(f"\n 注意: 由于只训练了50步,质量不会很高")
print(f" 但您可以看到模型学习语言模式的过程!")
EOF

运行莎士比亚生成器

python shakespeare_generator.py

下面逐行详细解释这个莎士比亚生成器脚本,好理解GPT的实现原理。

🔍 逐行代码解析:莎士比亚GPT生成器

1. 导入和基础设置

import torch
import torch.nn as nn
import pickle
import os

详细解释

  • torch: PyTorch核心库,提供张量计算和神经网络功能
  • torch.nn: 神经网络模块,包含各种层(Linear, Embedding等)
  • pickle: Python序列化库,用于加载.pkl格式的数据文件
  • os: 操作系统接口,用于文件路径操作

2. 打印标题和设置

print(" 莎士比亚风格文本生成器")
print("=" * 50)

解释:简单的用户界面,"=" * 50创建50个等号的分隔线

3. 数据加载部分

# 加载数据和词汇表
data_dir = 'data/shakespeare_char'
with open(os.path.join(data_dir, 'meta.pkl'), 'rb') as f:
    meta = pickle.load(f)

详细解释

  • data_dir: 设置数据目录路径
  • os.path.join(): 跨平台的路径连接,相当于 data_dir + '/' + 'meta.pkl'
  • with open(..., 'rb'): 以二进制读取模式打开文件,with确保文件自动关闭
  • pickle.load(f): 加载.pkl文件,读取之前保存的字符对应表

4. 字符编码设置

# 获取编解码函数
chars = sorted(list(set(open(os.path.join(data_dir, 'input.txt'), 'r').read())))
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}

逐行解析

# 第1行:获取所有唯一字符
chars = sorted(list(set(open(...).read())))
  • open(...).read(): 读取整个莎士比亚文本文件
  • set(...): 创建集合,自动去除重复字符
  • list(...): 转换为列表
  • sorted(...): 按ASCII码排序,确保字符顺序固定
# 第2行:创建字符到索引的映射stoi(string2integer)
stoi = {ch: i for i, ch in enumerate(chars)}
  • enumerate(chars): 产生 (索引, 字符) 对
  • 字典推导式创建映射:{'a': 0, 'b': 1, 'c': 2, ...}
# 第3行:创建索引到字符的映射itos(integer2string)
itos = {i: ch for i, ch in enumerate(chars)}
  • 反向映射:{0: 'a', 1: 'b', 2: 'c', ...}

5. 编解码函数

def encode(s):
    return [stoi[c] for c in s]

def decode(l):
    return ''.join([itos[i] for i in l])

详细解释

def encode(s):
    return [stoi[c] for c in s]
  • 输入:字符串 "hello"
  • 过程:['h', 'e', 'l', 'l', 'o'][104, 101, 108, 108, 111]
  • 输出:数字列表
def decode(l):
    return ''.join([itos[i] for i in l])
  • 输入:数字列表 [104, 101, 108, 108, 111]
  • 过程:[104, 101, 108, 108, 111]['h', 'e', 'l', 'l', 'o']
  • 输出:字符串 "hello"

6. 加载训练数据

train_data = torch.frombuffer(
    open(os.path.join(data_dir, 'train.bin'), 'rb').read(), 
    dtype=torch.uint16
).long()

逐步解析

  1. open(..., 'rb').read(): 以'rb'(read-binary)二进制模式读取train.bin文件,得到的是二进制原始字节
  2. torch.frombuffer(..., dtype=torch.uint16): 将二进制数据转换为16位无符号整数张量,uint16 = 16位无符号整数 = 0到65535的数字
  3. .long(): 转换为长整型张量(64位),long() = 64位长整数,训练时常用

为什么这样做?

  • train.bin是预处理好的数字化文本数据
  • 每个字符已经被转换为对应的索引数字
  • 直接加载比重新编码要快得多
  • train.bin文件 → 读出字节 → 变成数字列表 → 转换成PyTorch能用的格式

7. GPT模型定义

class SimpleGPT(nn.Module):
    def __init__(self, vocab_size, n_embd=128, n_head=4, n_layer=3):
        super().__init__()

详细解释

  • nn.Module: PyTorch中所有神经网络模块的基类
  • super().__init__(): 调用父类构造函数
  • 参数:
    • vocab_size: 词汇表大小(65个字符)
    • n_embd=128: 嵌入维度(每个字符用128维向量表示)
    • n_head=4: 注意力头数量
    • n_layer=3: Transformer层数

嵌入层

self.embedding = nn.Embedding(vocab_size, n_embd)
self.pos_embedding = nn.Embedding(1000, n_embd)

详细解释

self.embedding = nn.Embedding(vocab_size, n_embd)
  • 创建一个查找表:vocab_size × n_embd 的矩阵
  • 每个字符索引对应一个128维向量
  • 例如:字符 'a' (索引0) → 128维向量 [0.1, -0.2, 0.3, ...]
self.pos_embedding = nn.Embedding(1000, n_embd)
  • 位置嵌入:告诉模型每个字符在序列中的位置
  • 支持最大1000个位置
  • 位置0 → 向量1,位置1 → 向量2,...

Transformer层

self.transformer = nn.ModuleList([
    nn.TransformerDecoderLayer(
        d_model=n_embd,
        nhead=n_head,
        dim_feedforward=n_embd * 4,
        batch_first=True,
        dropout=0.1
    ) for _ in range(n_layer)
])

详细解释

  • nn.ModuleList: 存储多个神经网络层的容器
  • nn.TransformerDecoderLayer: PyTorch内置的Transformer解码器层
  • 参数详解:
    • d_model=n_embd: 输入维度(128)
    • nhead=n_head: 多头注意力的头数(4)
    • dim_feedforward=n_embd * 4: 前馈网络维度(512)
    • batch_first=True: 维度顺序以批次维度在前 (batch, seq, feature),先选句子,再选词元,数据排列像 [句子1][句子2][句子3]
    • 数据的三个维度batch = 同时处理几个句子;seq = 每个句子有多少个词元;feature = 每个词元用多少个数字表示(例如128个数字)
    • dropout=0.1: 10%的dropout防止过拟合

输出层

self.ln_f = nn.LayerNorm(n_embd)
self.head = nn.Linear(n_embd, vocab_size)

详细解释

  • nn.LayerNorm(n_embd): 层归一化,数据清洗,稳定训练。数据'洗干净' - 平均值接近0,标准差接近1,避免数字太大或太小,给数据做标准化处理。
  • nn.Linear(n_embd, vocab_size): 线性层把特征变成字符概率,将128维特征映射到65个字符的概率

8. 前向传播函数

def forward(self, x):
    b, t = x.shape
    pos = torch.arange(0, t, dtype=torch.long).unsqueeze(0)

详细解释

  • 标量(0维),向量(1维),矩阵(2维),张量(n维向量)
  • x.shape: 输入张量的形状,例如 (batch_size=8, seq_len=32)
  • b, t = x.shape: 解包得到批次大小和序列长度
  • torch.arange(0, t): 创建位置索引 [0, 1, 2, ..., t-1]
  • .unsqueeze(0): 增加一个维度,变成 (1, t)
x = self.embedding(x) + self.pos_embedding(pos)

详细解释

  • self.embedding(x): 字符嵌入,形状 (b, t, n_embd)
  • self.pos_embedding(pos): 位置嵌入,形状 (1, t, n_embd)
  • 相加得到最终嵌入:字符信息 + 位置信息
# 创建因果mask
mask = torch.triu(torch.ones(t, t), diagonal=1).bool()

详细解释

  • torch.ones(t, t): 创建全1的 t×t 矩阵
  • torch.triu(..., diagonal=1): 取上三角矩阵(对角线上方)
  • .bool(): 转换为布尔值
  • 作用:防止模型"偷看"未来的字符

举例:如果t=4,mask矩阵是:

[[False, True,  True,  True ],
 [False, False, True,  True ],
 [False, False, False, True ],
 [False, False, False, False]]
for transformer in self.transformer:
    x = transformer(x, x, tgt_mask=mask)

详细解释

  • 循环通过每个Transformer层
  • transformer(x, x, tgt_mask=mask):
    • 第一个x: 查询(query)
    • 第二个x: 键值(key, value)
    • tgt_mask=mask: 应用因果掩码
x = self.ln_f(x)
logits = self.head(x)
return logits

详细解释

  • self.ln_f(x): 最终层归一化
  • self.head(x): 线性变换,输出每个字符的未归一化概率(logits)
  • 返回形状:(batch_size, seq_len, vocab_size)

9. 模型创建和训练

model = SimpleGPT(vocab_size=len(chars))
optimizer = torch.optim.Adam(model.parameters(), lr=3e-4)

详细解释

  • SimpleGPT(vocab_size=len(chars)): 创建模型实例
  • torch.optim.Adam: Adam优化器,自适应学习率
  • model.parameters(): 获取所有可训练参数
  • lr=3e-4: 学习率 0.0003
block_size = 32
model.train()

详细解释

  • block_size = 32: 序列长度(训练窗口大小),模型一次处理32个字符
  • model.train(): 设置模型为训练模式(启用dropout等)

10. 训练循环

for step in range(50):
    ix = torch.randint(len(train_data) - block_size, (8,))
    x = torch.stack([train_data[i:i+block_size] for i in ix])
    y = torch.stack([train_data[i+1:i+block_size+1] for i in ix])

逐行解析

ix = torch.randint(len(train_data) - block_size, (8,))
  • 随机选择8个起始位置
  • 确保不超出数据边界
x = torch.stack([train_data[i:i+block_size] for i in ix])
  • 从每个起始位置取32个字符作为输入
  • torch.stack: 将列表转换为张量
y = torch.stack([train_data[i+1:i+block_size+1] for i in ix])
  • 取下一个字符作为目标(预测目标)
  • 这是语言模型的核心:预测下一个字符

举例

  • 输入x: "To be or not to be, that is th"
  • 目标y: "o be or not to be, that is the"
logits = model(x)
loss = nn.functional.cross_entropy(logits.view(-1, len(chars)), y.view(-1))

详细解释

  • model(x): 前向传播得到预测
  • view/reshape = 重新排列相同的数据
  • 为什么要reshape:交叉熵函数期望输入格式:
    • logits: (N, C) - N个样本,码本中的C个类别
  • logits.view(-1, len(chars)): 重塑为 (batch*seq, vocab_size),在形状参数中,-1 作为维度大小本来就无意义,PyTorch定义它为自动计算维度大小,相当于 auto
  • y.view(-1): 重塑为 (batch*seq,)
  • cross_entropy: 计算交叉熵损失
optimizer.zero_grad()
loss.backward()
optimizer.step()

详细解释

  • zero_grad(): 清零之前的梯度
  • backward(): 反向传播计算梯度
  • step(): 更新模型参数

11. 文本生成函数

def generate_text(prompt="", max_tokens=200, temperature=0.8, top_k=20):
    model.eval()

详细解释

  • model.eval(): 设置为评估模式(关闭dropout)
if prompt:
    tokens = encode(prompt)
else:
    tokens = [encode("ROMEO:")[0]]  # 只要'R',让模型自由发挥

详细解释

  • 如果有提示词,编码为数字列表作为上文(为了预测下一个token)
  • 否则用"ROMEO:"的第一个字符开始编码为上文,也可以不加[0]:则用"ROMEO:" 开始生成
with torch.no_grad():
    for _ in range(max_tokens):
        context = torch.tensor([tokens[-block_size:]])
        logits = model(context)[0, -1, :]

详细解释

  • torch.no_grad(): 这是推理阶段不是训练阶段,禁用梯度计算(节省内存),只要结果,不存历史
  • tokens[-block_size:]: 取最后32个字符作为上下文
  • logits = model(context)[0, -1, :]:
    • [0, -1, :]: 取第一个批次的最后一个位置的所有词汇概率,为了 next token prediction 采样,next token 即最后一个位置。
# 应用temperature
logits = logits / temperature

详细解释

  • temperature < 1: 让分布更尖锐,更保守
  • temperature > 1: 让分布更平坦,更随机
  • temperature = 1: 不改变分布
# Top-k采样
if top_k > 0:
    indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
    logits[indices_to_remove] = float('-inf')

详细解释

  • torch.topk(logits, top_k): 找到最大的k个值
  • indices_to_remove: 标记要移除的索引
  • logits[indices_to_remove] = float('-inf'): 设置为负无穷(概率为0)
probs = torch.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, 1).item()
tokens.append(next_token)

详细解释

  • torch.softmax: 将logits转换为概率分布
  • torch.multinomial: 根据概率分布随机采样
  • .item(): 将张量转换为Python数字
  • tokens.append(): 添加到生成序列

12. 生成展示

torch.manual_seed(42)
text1 = generate_text("HAMLET: To be or not to be", max_tokens=100, temperature=0.5, top_k=10)

详细解释

  • torch.manual_seed(42): 设置随机种子确保可重现
  • 不同参数组合展示不同的生成风格

核心概念总结

GPT的本质

  1. 输入:字符序列 → 数字序列
  2. 处理:Transformer层处理上下文信息
  3. 输出:预测下一个字符的概率分布
  4. 生成:根据概率分布采样下一个字符

关键技术

  • 嵌入:字符 → 向量
  • 位置编码:告诉模型字符位置
  • 自注意力:字符之间的关系
  • 因果掩码:防止看到未来
  • 温度采样:控制创造性
  • Top-k采样:控制选择范围

这就是GPT的完整实现!每一行代码都有其特定的作用,组合起来就能生成连贯的莎士比亚文风的文本。

发布者

立委

立委博士,多模态大模型应用高级咨询。出门问问大模型团队前工程副总裁,聚焦大模型及其AIGC应用。Netbase前首席科学家10年,期间指挥研发了18种语言的理解和应用系统,鲁棒、线速,scale up to 社会媒体大数据,语义落地到舆情挖掘产品,成为美国NLP工业落地的领跑者。Cymfony前研发副总八年,曾荣获第一届问答系统第一名(TREC-8 QA Track),并赢得17个小企业创新研究的信息抽取项目(PI for 17 SBIRs)。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理