从零构建GPT-1:Transformer解码器实战指南与PyTorch完整实现
当我们在2023年谈论大语言模型时,ChatGPT、GPT-4这些名字已经家喻户晓。但回溯到2018年,GPT-1的诞生才是这场革命的真正起点。与现在动辄千亿参数的大模型相比,GPT-1的1.17亿参数显得微不足道,但它确立的预训练-微调范式却成为了整个行业的黄金标准。本文将带你深入GPT-1的核心——Transformer解码器架构,通过PyTorch代码实现,让你不仅理解其原理,更能亲手搭建这个改变NLP历史的模型。
1. 环境准备与基础组件
在开始构建GPT-1之前,我们需要配置合适的开发环境。建议使用Python 3.8+和PyTorch 1.12+版本,这些版本在兼容性和性能之间取得了良好平衡。
conda create -n gpt1 python=3.8 conda activate gpt1 pip install torch==1.12.1 torchtext==0.13.1 numpy tqdmGPT-1的核心是Transformer解码器堆叠,我们先实现几个关键组件:
import torch import torch.nn as nn import math class LayerNorm(nn.Module): def __init__(self, hidden_size, eps=1e-12): super().__init__() self.weight = nn.Parameter(torch.ones(hidden_size)) self.bias = nn.Parameter(torch.zeros(hidden_size)) self.eps = eps def forward(self, x): mean = x.mean(-1, keepdim=True) std = x.std(-1, keepdim=True) return self.weight * (x - mean) / (std + self.eps) + self.bias注意:GPT-1使用的是后归一化(Post-LayerNorm),这与后来Transformer常用的前归一化(Pre-LayerNorm)不同。这个细节对模型训练稳定性有重要影响。
2. 自注意力机制实现
自注意力是Transformer的核心,让我们分解实现这个关键组件:
class SelfAttention(nn.Module): def __init__(self, hidden_size=768, num_heads=12): super().__init__() assert hidden_size % num_heads == 0 self.num_heads = num_heads self.head_dim = hidden_size // num_heads self.query = nn.Linear(hidden_size, hidden_size) self.key = nn.Linear(hidden_size, hidden_size) self.value = nn.Linear(hidden_size, hidden_size) self.proj = nn.Linear(hidden_size, hidden_size) self.attn_dropout = nn.Dropout(0.1) def forward(self, x, mask=None): batch_size, seq_len, hidden_size = x.size() # 线性变换得到Q,K,V q = self.query(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) k = self.key(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) v = self.value(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # 计算注意力分数 scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim) if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) attn = torch.softmax(scores, dim=-1) attn = self.attn_dropout(attn) # 应用注意力权重到V上 out = torch.matmul(attn, v).transpose(1, 2).contiguous() out = out.view(batch_size, seq_len, hidden_size) return self.proj(out)关键参数说明:
| 参数名 | 值 | 说明 |
|---|---|---|
| hidden_size | 768 | GPT-1的隐层维度 |
| num_heads | 12 | 多头注意力头数 |
| head_dim | 64 | 每个注意力头的维度(768/12) |
| attn_dropout | 0.1 | 注意力权重dropout率 |
3. Transformer解码器层构建
有了自注意力机制,我们现在可以构建完整的Transformer解码器层:
class TransformerBlock(nn.Module): def __init__(self, hidden_size=768, num_heads=12, ff_dim=3072): super().__init__() self.ln1 = LayerNorm(hidden_size) self.attn = SelfAttention(hidden_size, num_heads) self.ln2 = LayerNorm(hidden_size) self.mlp = nn.Sequential( nn.Linear(hidden_size, ff_dim), nn.GELU(), nn.Linear(ff_dim, hidden_size), nn.Dropout(0.1) ) def forward(self, x, mask=None): # 后归一化架构 x = x + self.attn(self.ln1(x), mask) x = x + self.mlp(self.ln2(x)) return x提示:GPT-1使用的GELU激活函数与后来流行的ReLU不同,它更接近生物神经元的激活模式,在数学上可以表示为GELU(x) = xΦ(x),其中Φ是标准正态分布的累积分布函数。
4. 完整GPT-1模型实现
现在我们将所有组件组合成完整的GPT-1模型:
class GPT1(nn.Module): def __init__(self, vocab_size=40478, max_len=512, hidden_size=768, num_layers=12, num_heads=12, ff_dim=3072): super().__init__() self.token_emb = nn.Embedding(vocab_size, hidden_size) self.pos_emb = nn.Parameter(torch.zeros(1, max_len, hidden_size)) self.drop = nn.Dropout(0.1) self.layers = nn.ModuleList([ TransformerBlock(hidden_size, num_heads, ff_dim) for _ in range(num_layers) ]) self.ln_f = LayerNorm(hidden_size) self.head = nn.Linear(hidden_size, vocab_size, bias=False) # 权重绑定:输出层与输入嵌入共享权重 self.head.weight = self.token_emb.weight # 初始化 self.apply(self._init_weights) def _init_weights(self, module): if isinstance(module, (nn.Linear, nn.Embedding)): module.weight.data.normal_(mean=0.0, std=0.02) if isinstance(module, nn.Linear) and module.bias is not None: module.bias.data.zero_() elif isinstance(module, LayerNorm): module.bias.data.zero_() module.weight.data.fill_(1.0) def forward(self, x, mask=None): batch_size, seq_len = x.size() assert seq_len <= self.pos_emb.size(1), "序列长度超过最大位置编码长度" # 获取token嵌入并添加位置编码 tok_emb = self.token_emb(x) pos_emb = self.pos_emb[:, :seq_len, :] x = self.drop(tok_emb + pos_emb) # 通过所有Transformer层 for layer in self.layers: x = layer(x, mask) x = self.ln_f(x) logits = self.head(x) return logits模型架构关键点:
- 权重绑定:输出层的权重与输入嵌入层共享,这减少了参数量并提高了训练稳定性
- 位置编码:GPT-1使用可学习的位置嵌入,而非原始Transformer的正弦余弦编码
- 初始化:所有线性层和嵌入层使用均值为0、标准差为0.02的正态分布初始化
5. 预训练目标实现
GPT-1的核心是语言模型预训练,我们实现其训练逻辑:
def train_step(model, batch, optimizer, device): model.train() inputs, targets = batch inputs = inputs.to(device) targets = targets.to(device) # 创建因果掩码 seq_len = inputs.size(1) mask = torch.tril(torch.ones(seq_len, seq_len)).to(device) # 前向传播 logits = model(inputs, mask) loss = nn.CrossEntropyLoss()( logits.view(-1, logits.size(-1)), targets.view(-1) ) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() return loss.item() def generate_text(model, tokenizer, prompt, max_len=20, device='cpu'): model.eval() tokens = tokenizer.encode(prompt) input_ids = torch.tensor([tokens]).to(device) for _ in range(max_len): # 创建因果掩码 seq_len = input_ids.size(1) mask = torch.tril(torch.ones(seq_len, seq_len)).to(device) # 获取下一个token的logits with torch.no_grad(): logits = model(input_ids, mask) # 取最后一个token的logits并采样 next_token = torch.argmax(logits[0, -1, :]).unsqueeze(0) input_ids = torch.cat([input_ids, next_token.unsqueeze(0)], dim=1) return tokenizer.decode(input_ids[0].tolist())注意:GPT-1使用标准的自回归语言模型目标,即预测下一个token。因果掩码确保模型在预测时只能看到前面的token,不能看到未来的token。
6. 下游任务微调策略
GPT-1的强大之处在于其能够通过微调适应各种下游任务。我们实现其微调逻辑:
class GPT1ForClassification(nn.Module): def __init__(self, base_model, num_classes): super().__init__() self.transformer = base_model self.classifier = nn.Linear(base_model.hidden_size, num_classes) def forward(self, x, mask=None): # 获取最后一个token的表示 hidden_states = self.transformer(x, mask) last_token = hidden_states[:, -1, :] return self.classifier(last_token) def fine_tune(model, train_loader, val_loader, optimizer, num_epochs, device): best_acc = 0.0 for epoch in range(num_epochs): model.train() for batch in train_loader: inputs, labels = batch inputs, labels = inputs.to(device), labels.to(device) # 创建因果掩码 seq_len = inputs.size(1) mask = torch.tril(torch.ones(seq_len, seq_len)).to(device) # 前向传播 outputs = model(inputs, mask) loss = nn.CrossEntropyLoss()(outputs, labels) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() # 验证集评估 val_acc = evaluate(model, val_loader, device) if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pt') return best_acc不同任务的输入转换策略:
文本蕴含:将前提和假设用分隔符
$连接[前提]$[假设]语义相似度:对两种顺序分别处理并相加
def similarity_forward(model, text1, text2): input1 = tokenize(f"{text1}${text2}") input2 = tokenize(f"{text2}${text1}") out1 = model(input1)[:, -1, :] out2 = model(input2)[:, -1, :] return (out1 + out2) / 2问答任务:将上下文、问题和每个答案组合
[上下文]$[问题]$[答案1] [上下文]$[问题]$[答案2] ...
7. 模型训练技巧与优化
训练GPT-1这样的大型语言模型需要特别注意以下技巧:
学习率调度:使用带热启动的线性衰减学习率
def get_lr(step, warmup_steps, total_steps, max_lr): if step < warmup_steps: return max_lr * step / warmup_steps return max_lr * (1 - (step - warmup_steps) / (total_steps - warmup_steps))梯度裁剪:防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)混合精度训练:节省显存并加速训练
scaler = torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): logits = model(inputs, mask) loss = criterion(logits, targets) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()关键超参数设置参考:
| 超参数 | 预训练值 | 微调值 |
|---|---|---|
| 批量大小 | 64 | 32 |
| 学习率 | 6e-4 | 5e-5 |
| 预热步数 | 4000 | 100 |
| 总步数 | 1M | 10k |
| Dropout率 | 0.1 | 0.1 |
| 权重衰减 | 0.01 | 0.01 |
8. 模型评估与结果分析
在实现完整模型后,我们需要评估其性能。GPT-1论文中报告了在多个任务上的结果:
自然语言推理任务表现:
| 数据集 | Accuracy | 提升幅度 |
|---|---|---|
| MNLI | 82.1% | +4.6% |
| QNLI | 88.1% | +5.8% |
| RTE | 56.0% | +0.2% |
问答与常识推理任务:
| 数据集 | Accuracy | 提升幅度 |
|---|---|---|
| RACE | 59.0% | +7.6% |
| COPA | 78.6% | +9.2% |
语义相似度任务:
| 数据集 | Accuracy | 提升幅度 |
|---|---|---|
| STS-B | 80.0% | +15.0% |
| MRPC | 82.3% | +6.7% |
可视化分析注意力模式可以帮助我们理解模型的工作原理:
def plot_attention(model, text, layer=0, head=0): tokens = tokenizer.encode(text) inputs = torch.tensor([tokens]).to(device) # 获取注意力权重 with torch.no_grad(): outputs = model.transformer(inputs) attn = model.transformer.layers[layer].attn.attn # 绘制热力图 plt.figure(figsize=(10, 8)) sns.heatmap(attn[0, head].cpu().numpy(), annot=True, fmt=".2f") plt.xticks(range(len(tokens)), tokenizer.decode(tokens), rotation=45) plt.yticks(range(len(tokens)), tokenizer.decode(tokens), rotation=0) plt.show()通过这样的分析,我们可以观察到模型如何建立长距离依赖关系,以及不同注意力头如何专注于不同类型的语言模式。