1 参数
n_head = 4 # 多头注意力的头数
n_layer = 4 # transformer层数
dropout = 0.0 # dropout率
context_size = 32
embd_dim = 64
batch_size = 16
lr = 1e-3
epochs = 5000
eval_epochs = 200
eval_interval = 100
device = "cuda" if torch.cuda.is_available() else "cpu"
2 模型结构
import torch
import torch.nn as nn
import torch.nn.functional as F
from constant import *
class Attention(nn.Module):
def __init__(self, head_dim, dropout=0.1):
super().__init__()
# QKV 线性变换层(无偏置项)
self.Q = nn.Linear(embd_dim, head_dim, bias=False) # 输入维度embd_dim → 头维度head_dim
self.K = nn.Linear(embd_dim, head_dim, bias=False)
self.V = nn.Linear(embd_dim, head_dim, bias=False)
# 三角掩码(用于自注意力的因果掩码)
self.register_buffer("mask", torch.tril(torch.ones(context_size, context_size))) # 下三角掩码
# Dropout层
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B, T, C = x.shape # B=批量大小, T=时间步长, C=嵌入维度
# 计算QKV向量
q, k, v = self.Q(x), self.K(x), self.V(x) # (B, T, head_dim)
# Attention计算
weights = q @ k.transpose(-1, -2) # 点积注意力分数 (B, T, T)
weights *= C ** -0.5 # 缩放因子(防止梯度爆炸)
# 应用掩码(将未来位置置为-inf)
weights = weights.masked_fill(self.mask[:T, :T] == 0, float("-inf"))
# 计算softmax得到权重分布
weights = F.softmax(weights, dim=-1)
weights = self.dropout(weights) # Dropout
# 加权求和得到输出
out = weights @ v # (B, T, head_dim)
return out
class MultiAttention(nn.Module):
def __init__(self, n_head, dropout=0.1):
super().__init__()
head_dim = embd_dim // n_head # 每个头的维度
# 创建多头注意力层列表
self.heads = nn.ModuleList([
Attention(head_dim, dropout) for _ in range(n_head)
])
# 输出投影层(将多头结果合并为原始维度)
self.proj = nn.Linear(embd_dim, embd_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# 并行计算所有头的注意力输出
out = torch.cat([head(x) for head in self.heads], dim=-1) # (B, T, embd_dim)
# 投影合并并添加Dropout
out = self.dropout(self.proj(out))
return out
class FeedForward(nn.Module):
def __init__(self, hidden_dim, dropout=0.1):
super().__init__()
# 前馈网络结构:Linear -> ReLU -> Linear -> Dropout
self.ffn = nn.Sequential(
nn.Linear(embd_dim, hidden_dim), # 输入到隐藏层
nn.ReLU(),
nn.Linear(hidden_dim, embd_dim), # 隐藏层到输出
nn.Dropout(dropout)
)
def forward(self, x):
return self.ffn(x)
class Block(nn.Module):
def __init__(self, n_head, dropout=0.1):
super().__init__()
# 多头注意力层
self.ma = MultiAttention(n_head, dropout)
self.ln1 = nn.LayerNorm(embd_dim) # 第一个层归一化
# 前馈网络层
self.ffn = FeedForward(embd_dim * 4, dropout) # 隐藏层维度通常是4倍embd_dim
self.ln2 = nn.LayerNorm(embd_dim) # 第二个层归一化
def forward(self, x):
# 使用Pre-Normalization(先归一化后计算)
# 多头注意力分支
x = x + self.ma(self.ln1(x)) # 残差连接
# 前馈网络分支
x = x + self.ffn(self.ln2(x)) # 残差连接
return x
class GPT(nn.Module):
def __init__(self, vocab_size, n_layer, n_head, dropout=0.1):
super().__init__()
# 词嵌入和位置嵌入层
self.token_embedding_table = nn.Embedding(vocab_size, embd_dim) # 词表到嵌入空间
self.position_embedding_table = nn.Embedding(context_size, embd_dim) # 位置编码
# transformer块堆叠
self.blocks = nn.Sequential(*[
Block(n_head, dropout) for _ in range(n_layer)
])
self.ln = nn.LayerNorm(embd_dim) # 最终归一化层
# 预测层(将嵌入维度映射到词表大小)
self.liner = nn.Linear(embd_dim, vocab_size)
def forward(self, idx, y=None):
B, T = idx.shape
device = idx.device
# 嵌入层组合(词嵌入+位置嵌入)
token_emb = self.token_embedding_table(idx)
pos_emb = self.position_embedding_table(torch.arange(T, device=device))
x = token_emb + pos_emb
# 通过所有Block层
x = self.blocks(x)
x = self.ln(x) # 最终归一化
# 预测层输出logits
logits = self.liner(x)
# 计算损失(如果标签y存在)
if y is not None:
B, T, C = logits.shape
logits = logits.view(B * T, C) # 展平为(B*T, C)
y = y.view(B * T) # 展平为(B*T)
loss = F.cross_entropy(logits, y)
else:
loss = None
return logits, loss
def generate(self, idx, max_new_tokens):
"""生成文本的推理函数"""
for _ in range(max_new_tokens):
# 截断输入到context_size长度(防止超出位置编码范围)
idx_cond = idx[:, -context_size:]
# 前向计算得到最后一个token的logits
logits, _ = self(idx_cond)
logits = logits[:, -1, :] # 取最后一个时间步
# 生成概率分布并采样
probs = F.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1) # 随机采样
# 拼接新生成的token
idx = torch.cat((idx, idx_next), dim=1)
return idx # 返回完整序列(包含原始输入和生成部分)
3 工具
3.1 数据预处理
with open('../data/tinyshakespeare.txt', 'r', encoding='utf-8') as f:
text = f.read()
print("数据集中的总字符数: ", len(text))
# 文本中出现的所有的字符
chars = sorted(list(set(text)))
vocab_size = len(chars)
print("文本中出现的字符: ", ''.join(chars))
print("文本中出现的字符数量: ", vocab_size)
## 数据集划分以及数据分批
data = torch.tensor(encode(text), dtype=torch.long)
# 90%划分训练集和验证集
n = int(0.9 * len(data))
train_data = data[:n]
val_data = data[n:]
3.2 简易分词
# 简单的tokenizer实现
# 这是一个简单的tokenizer实现,就是简单的把文本中出现过的所有字符按顺序给予一个整数0~vocab_size-1
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}
# 简单的编码和解码规则
# 编码:给定一个字符串输出一个list的整数
encode = lambda s: [stoi[c] for c in s]
# 解码:给定一个list的整数输出一个字符串
decode = lambda l: ''.join([itos[i] for i in l])
3.3 数据切片
def get_batch(split):
# 生成一个batch的数据,x为输入,y是target
data = train_data if split == 'train' else val_data
ix = torch.randint(len(data) - context_size, (batch_size,))
# y相对于x移位一个token,因为训练的目标就是预测下一个token
x = torch.stack([data[i:i + context_size] for i in ix])
y = torch.stack([data[i + 1:i + context_size + 1] for i in ix])
x, y = x.to(device), y.to(device)
return x, y
3.4 模型验证
@torch.no_grad()
def estimate_loss(model):
out = {}
model.eval()
for split in ["train", "val"]:
losses = torch.zeros(eval_epochs)
for it in range(eval_epochs):
x, y = get_batch(split)
logits, loss = model(x, y)
losses[it] = loss
out[split] = losses.mean()
return out
4 模型训练
model = GPT(vocab_size, n_layer, n_head, dropout)
model = model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
train_losses = []
val_losses = []
for epoch in range(epochs):
# 训练
model.train()
x, y = get_batch("train")
logits, loss = model(x, y)
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
# 评估
if epoch % eval_interval == 0 or epoch == epochs - 1:
losses = estimate_loss(model)
print(f"step {epoch}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
train_losses.append(losses["train"])
val_losses.append(losses["val"])
plt.figure(figsize=(10, 5))
plt.plot(train_losses, label='Train Loss')
plt.plot(val_losses, label='Validation Loss')
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.savefig("loss.png")
plt.show()
# 保存模型
torch.save(model.state_dict(), "output/nanoGPT_5000_epochs.pt")
5000 epoch 的 loss 曲线如下:

5 测试生成
model_path = "./output/nanoGPT_5000_epochs.pt"
model = GPT(65, 4, 4, 0)
model.load_state_dict(torch.load(model_path))
model.eval()
model = model.to("cuda")
input = torch.zeros((1, 1), dtype=torch.long).to("cuda")
print(decode(model.generate(input, 1000)[0].tolist()))
生成效果展示,至少能拼单词了👀️ :
