莎士比亚为例创建一个快速的文本生成演示
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()
逐步解析:
open(..., 'rb').read()
: 以'rb'
(read-binary)二进制模式读取train.bin文件,得到的是二进制原始字节torch.frombuffer(..., dtype=torch.uint16)
: 将二进制数据转换为16位无符号整数张量,uint16
= 16位无符号整数 = 0到65535的数字.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定义它为自动计算维度大小,相当于 autoy.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的本质:
- 输入:字符序列 → 数字序列
- 处理:Transformer层处理上下文信息
- 输出:预测下一个字符的概率分布
- 生成:根据概率分布采样下一个字符
关键技术:
- 嵌入:字符 → 向量
- 位置编码:告诉模型字符位置
- 自注意力:字符之间的关系
- 因果掩码:防止看到未来
- 温度采样:控制创造性
- Top-k采样:控制选择范围
这就是GPT的完整实现!每一行代码都有其特定的作用,组合起来就能生成连贯的莎士比亚文风的文本。