1. 为什么选择PyTorch搭建小型中文GPT作为一个在个人电脑上就能跑起来的实验项目PyTorch绝对是我们的首选框架。我当年第一次尝试用TensorFlow实现语言模型时光是静态计算图就把我折腾得够呛。PyTorch的动态图机制对初学者友好得多就像用Python写普通程序一样自然。实测对比在我的GTX 1060显卡上PyTorch的CUDA加速能让训练速度比纯CPU快8-10倍。更重要的是它的调试体验——你可以像普通Python代码那样设置断点实时查看张量值。这对理解GPT的工作原理特别重要毕竟Transformer那些注意力权重的变化可不是静态图能轻易观察到的。说到中文处理这里有个坑我踩过英文的tokenizer直接用在中文上效果很差。我们得自己构建字级别的词表vocab原因很简单——中文的基本单位是字而不是空格分隔的单词。举个例子我喜欢机器学习应该拆解成[我,喜,欢,机,器,学,习]而不是像英文那样按单词分割。2. 数据预处理实战技巧2.1 语料清洗的隐藏陷阱拿到50万条中文闲聊数据时千万别直接开训我建议先用简单的规则过滤删除含特殊符号的句子如※★▶剔除长度超过30个字的对话轮次统一全角/半角标点关键技巧用jieba分词虽然方便但会引入额外依赖。实际上单字切分对小型GPT效果更好还能减少词表大小。我们的处理脚本长这样def clean_text(text): text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , text) return .join([char for char in text if char not in exclude_chars])2.2 词表构建的平衡艺术词表大小直接影响模型性能太大会导致稀疏训练我的破显卡显存直接爆炸太小又无法覆盖常用表达经过多次实验我发现2-3万的词表对闲聊场景正合适。这里有个实用技巧——统计字符频率时给对话开头和结尾的特殊token如start、sep设置最小出现次数保证from collections import Counter def build_vocab(texts, min_count5): counter Counter(char for text in texts for char in text) vocab [pad, unk] \ [char for char, count in counter.items() if count min_count] return {char: idx for idx, char in enumerate(vocab)}3. 模型搭建的省显存秘籍3.1 轻量级Transformer结构原版GPT-2有1.5亿参数我们的迷你版要精简得多层数从12层减到6层注意力头数从12减到8隐藏层维度从768减到512注意即使这样batch_size也只能设到8我的6GB显存极限。这时梯度累积技巧就派上用场了——每4个batch才更新一次参数等效于batch_size32optimizer.zero_grad() for i, (inputs, targets) in enumerate(dataloader): outputs model(inputs) loss criterion(outputs, targets) loss.backward() if (i1) % 4 0: # 每4个batch更新一次 optimizer.step() optimizer.zero_grad()3.2 位置编码的替代方案原始Transformer的位置编码需要预先计算最大长度这对长对话不友好。我改用了可学习的相对位置编码class PositionalEmbedding(nn.Module): def __init__(self, max_len, d_model): super().__init__() self.pos_embed nn.Embedding(max_len, d_model) def forward(self, x): seq_len x.size(1) positions torch.arange(seq_len, devicex.device).expand(x.size(0), seq_len) return x self.pos_embed(positions)4. 训练优化的实战经验4.1 学习率动态调整使用带warmup的Adam优化器能显著提升收敛速度。这是我的配置方案前1000步线性warmup到1e-4之后余弦衰减到1e-5from torch.optim.lr_scheduler import LambdaLR def get_scheduler(optimizer, warmup_steps, total_steps): def lr_lambda(current_step): if current_step warmup_steps: return float(current_step) / float(max(1, warmup_steps)) progress float(current_step - warmup_steps) / float(max(1, total_steps - warmup_steps)) return max(0.1, 0.5 * (1.0 math.cos(math.pi * progress))) return LambdaLR(optimizer, lr_lambda)4.2 应对过拟合的三板斧小数据训练GPT特别容易过拟合我总结的有效方法分层学习率底层参数用更小的学习率随机权重平均(SWA)训练后期使用torch.optim.swa_utils标签平滑让模型不要太自信criterion nn.CrossEntropyLoss( ignore_index0, # 忽略padding label_smoothing0.1 # 标签平滑 )5. 对话生成的实用技巧5.1 温度采样(Temperature Sampling)直接argmax会生成机械回复加入温度系数让输出更自然def generate_with_temp(logits, temperature0.7): logits logits / temperature probs F.softmax(logits, dim-1) return torch.multinomial(probs, num_samples1)5.2 上下文缓存加速多轮对话时缓存之前的KV向量避免重复计算class ConversationCache: def __init__(self): self.cache None def update(self, new_kv): if self.cache is None: self.cache new_kv else: self.cache [torch.cat([prev, new], dim-2) for prev, new in zip(self.cache, new_kv)]6. 效果调优的进阶思路当基础模型跑通后可以尝试这些提升策略数据增强用同义词替换生成更多训练样本课程学习先训练短对话再逐步增加长度混合精度训练用torch.cuda.amp节省显存最后提醒一点当损失降到2.0左右时生成效果会有明显提升。这时候可以开始人工评估重点关注回复相关性语句通顺度多轮连贯性我在项目后期专门写了个评估脚本随机采样100组对话进行人工打分。虽然这个方法很原始但对调参方向判断特别有帮助。