基于GPT-2的AI古典诗词生成实战:从数据处理到模型部署

发布时间:2026/9/3 12:13:27
基于GPT-2的AI古典诗词生成实战:从数据处理到模型部署 最近在开发一个AI诗歌生成项目时遇到了一个有趣的问题如何让AI模型生成的诗句不仅符合格律还能像“玄裳夜渡小楼阴侵霜遍袖浑无觉”这样具备古典诗词的意境、画面感和含蓄的情感表达这不仅仅是简单的文本续写而是涉及自然语言处理、深度学习模型调优以及古典文学知识融合的综合工程挑战。本文将从一个实践者的角度完整拆解从零构建一个“赛博诗人”AI模型的全过程。我们将从古典诗词的数据处理讲起深入到模型选型、训练技巧最终实现能够生成富有意境诗句的完整流程。无论你是对AI文本生成感兴趣的开发者还是希望将传统文化与前沿技术结合的爱好者都能从本文中获得一套可复现的实战方案。1. 背景与核心概念什么是“赛博诗人”“赛博诗人”并非一个严谨的学术术语它形象地描述了利用人工智能技术尤其是自然语言处理NLP进行古典诗词创作的研究与应用领域。其核心目标是让机器学会理解并模仿古典诗词的创作规律产出在形式、意境和情感上都能接近甚至达到人类水平的作品。它解决的核心问题是什么形式模仿让AI掌握诗词的固定格式如五言/七言绝句、律诗的平仄、对仗、押韵规则。意境生成超越简单的词汇拼接让生成的句子能构建出统一、深远的画面与情感空间如“玄裳夜渡”的孤寂与“侵霜无觉”的沉浸感。创新与可控在遵循规则的基础上能够根据给定的主题、情感基调或关键词进行定向创作而非完全随机输出。为什么开发者需要关注这不仅仅是一个“玩具项目”。其技术栈覆盖了NLP领域的多个核心环节文本预处理、词向量表示、序列生成模型如RNN、LSTM、Transformer、强化学习用于优化押韵和意境、以及评价体系构建。掌握这些对于深入理解文本生成、风格迁移、可控生成等前沿方向大有裨益。2. 环境准备与版本说明本项目主要使用Python进行开发深度学习框架选择PyTorch因其在研究和原型开发中具有更高的灵活性。以下环境是经过验证可稳定运行的配置。基础环境操作系统Ubuntu 20.04 LTS 或 Windows 10/11 (WSL2推荐)Python3.8 或 3.9CUDA11.3 (如果使用GPU训练请确保与PyTorch版本匹配)cuDNN8.2.x核心Python库建议使用conda或venv创建独立的虚拟环境。# 创建并激活虚拟环境 conda create -n cyber_poet python3.8 conda activate cyber_poet # 安装核心依赖 pip install torch1.12.1cu113 torchvision0.13.1cu113 torchaudio0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113 pip install transformers4.25.1 pip install datasets2.8.0 pip install jieba0.42.1 pip install scikit-learn1.2.0 pip install tensorboard2.11.0 pip install tqdm4.64.1 pip install pandas1.5.2版本说明与项目结构版本号仅供参考重点是库之间的兼容性。本文示例基于上述版本但核心思路适用于其他相近版本。 一个清晰的项目结构有助于管理代码和数据cyber_poet/ ├── data/ # 存放原始和预处理后的数据 │ ├── raw/ # 原始诗词文本 │ └── processed/ # 预处理后的数据文件 ├── src/ # 源代码 │ ├── data_processor.py # 数据预处理模块 │ ├── model.py # 模型定义 │ ├── train.py # 训练脚本 │ ├── generate.py # 生成脚本 │ └── utils.py # 工具函数 ├── configs/ # 配置文件 │ └── default.yaml ├── outputs/ # 模型检查点、日志、生成结果 │ ├── checkpoints/ │ ├── logs/ │ └── samples/ ├── requirements.txt └── README.md3. 核心原理与模型选型拆解要让AI写出好诗我们需要从“学什么”和“怎么学”两个层面来设计。3.1 数据表征诗词的“DNA”古典诗词是高度凝练的语言。我们首先需要将诗句转化为模型能理解的数字形式。分词与现代中文不同古典诗词通常以字为单位效果更好因为每个字都承载着独立的意义。但也可以结合“词”来处理一些固定意象如“明月”、“孤舟”。我们使用jieba并加载自定义词典来平衡两者。构建词表将所有训练数据中出现的字或词收集起来为每个字分配一个唯一的ID。同时需要加入特殊标记如[PAD]填充、[UNK]未知字、[BOS]开始、[EOS]结束。序列化将一句诗“玄裳夜渡小楼阴”转化为ID序列如[BOS, 玄, 裳, 夜, 渡, 小, 楼, 阴, EOS]。3.2 模型架构诗人的“大脑”对于序列生成任务主流模型有以下几种选择RNN/LSTM/GRU传统的循环神经网络能较好地处理序列依赖关系但训练速度慢难以捕捉长距离依赖。适合作为入门理解。Transformer当前的主流选择依靠自注意力机制Self-Attention能并行计算并捕捉全局依赖关系非常适合诗词这种需要前后照应的文本。预训练语言模型微调如GPT-2、Chinese-GPT、ERNIE等。它们在海量通用文本上预训练过拥有强大的语言先验知识再在古典诗词数据上微调往往能取得更好的效果尤其是生成语句的通顺度和“像人话”的程度。本项目选择方案我们将采用GPT-2 架构作为核心模型进行微调。原因在于GPT-2是自回归模型非常适合逐字生成的任务。拥有强大的开源预训练中文模型如uer/gpt2-chinese-cluecorpussmall提供了良好的语言基础。其Transformer解码器结构能有效建模诗句内部的复杂关系。3.3 训练目标如何“评价”一首诗我们使用标准的语言模型建模即让模型预测下一个字是什么。给定前面所有的字模型输出下一个字在整个词表上的概率分布。训练目标就是最大化真实下一个字的概率最小化交叉熵损失。但对于诗词仅有语言模型损失是不够的。我们还需要引入一些辅助损失来引导模型学习诗词的特定规则押韵损失在生成句尾时鼓励模型选择与指定韵脚相同或相近韵母的字。平仄损失进阶可以尝试构建平仄模式让模型在生成过程中倾向于符合格律的平仄排列。这部分实现较为复杂初期可以暂缓。4. 完整实战案例构建你的赛博诗人接下来我们一步步实现整个流程。4.1 数据收集与预处理我们首先需要一个高质量的古典诗词数据集。可以从开源项目如chinese-poetry获取。步骤1准备原始数据假设我们有一个poetry.txt文件每行一首诗诗题与正文用::分隔。静夜思::床前明月光疑是地上霜。举头望明月低头思故乡。 春晓::春眠不觉晓处处闻啼鸟。夜来风雨声花落知多少。 ...步骤2编写数据预处理脚本创建src/data_processor.py# src/data_processor.py import jieba import json from collections import Counter import re class DataProcessor: def __init__(self, data_path, max_len128): self.data_path data_path self.max_len max_len self.word2id {} self.id2word {} self.special_tokens [[PAD], [UNK], [BOS], [EOS]] def load_and_clean(self): 加载并清洗数据 poems [] with open(self.data_path, r, encodingutf-8) as f: for line in f: if :: in line: # 去掉诗题只取正文 content line.strip().split(::)[1] # 移除标点只保留诗句这里简单处理也可保留逗号句号 content re.sub(r[。、], , content) if 5 len(content) self.max_len - 2: # 保留BOS/EOS位置 poems.append(content) print(fLoaded {len(poems)} poems.) return poems def build_vocab(self, poems, min_freq5): 构建词表这里我们以字为单位 char_counter Counter() for poem in poems: for char in poem: char_counter[char] 1 # 构建词表 vocab self.special_tokens.copy() # 加入频率高于阈值的字 for char, freq in char_counter.items(): if freq min_freq: vocab.append(char) self.word2id {word: idx for idx, word in enumerate(vocab)} self.id2word {idx: word for idx, word in enumerate(vocab)} print(fVocabulary size: {len(self.word2id)}) # 保存词表 with open(data/processed/vocab.json, w, encodingutf-8) as f: json.dump({word2id: self.word2id, id2word: self.id2word}, f, ensure_asciiFalse, indent2) return vocab def poem_to_ids(self, poem): 将单首诗转为ID序列并添加特殊标记 ids [self.word2id.get([BOS], 1)] # 开始标记 for char in poem: ids.append(self.word2id.get(char, self.word2id.get([UNK], 1))) # 未知字用UNK ids.append(self.word2id.get([EOS], 2)) # 结束标记 # 填充或截断 if len(ids) self.max_len: ids [self.word2id[[PAD]]] * (self.max_len - len(ids)) else: ids ids[:self.max_len-1] [self.word2id[[EOS]]] return ids def process(self): 主处理流程 poems self.load_and_clean() self.build_vocab(poems) all_ids [] for poem in poems: ids self.poem_to_ids(poem) all_ids.append(ids) # 保存处理后的数据 import numpy as np np.save(data/processed/poem_ids.npy, np.array(all_ids, dtypenp.int64)) print(fProcessed data saved. Shape: {np.array(all_ids).shape}) if __name__ __main__: processor DataProcessor(data/raw/poetry.txt) processor.process()运行此脚本将在data/processed/下生成vocab.json和poem_ids.npy。4.2 构建数据集与模型我们将使用Hugging Face的transformers库加载预训练的GPT-2模型。步骤1定义数据集类创建src/dataset.py# src/dataset.py from torch.utils.data import Dataset import numpy as np import torch class PoetryDataset(Dataset): def __init__(self, data_path, seq_length128): self.data np.load(data_path) # 形状为 [num_samples, seq_length] self.seq_length seq_length def __len__(self): return len(self.data) def __getitem__(self, idx): # 获取一条样本 token_ids self.data[idx] # 输入是序列的前n-1个token input_ids token_ids[:-1] # 标签是序列的后n-1个token预测下一个字 labels token_ids[1:] return { input_ids: torch.tensor(input_ids, dtypetorch.long), labels: torch.tensor(labels, dtypetorch.long) }步骤2定义或加载模型创建src/model.py# src/model.py from transformers import GPT2LMHeadModel, GPT2Config import torch.nn as nn class CyberPoetModel(nn.Module): def __init__(self, vocab_size, pretrained_model_nameNone): super().__init__() if pretrained_model_name: # 加载预训练的中文GPT-2模型 print(fLoading pretrained model: {pretrained_model_name}) self.gpt2 GPT2LMHeadModel.from_pretrained(pretrained_model_name) # 调整词表大小如果预训练词表与我们构建的不同 if vocab_size ! self.gpt2.config.vocab_size: self.gpt2.resize_token_embeddings(vocab_size) else: # 从头开始训练一个小型GPT-2不推荐需要大量数据和时间 config GPT2Config( vocab_sizevocab_size, n_positions512, n_ctx512, n_embd768, n_layer6, n_head8 ) self.gpt2 GPT2LMHeadModel(config) def forward(self, input_ids, labelsNone): # 直接调用GPT-2模型 outputs self.gpt2(input_idsinput_ids, labelslabels) return outputs4.3 编写训练脚本创建src/train.py# src/train.py import torch from torch.utils.data import DataLoader from transformers import AdamW, get_linear_schedule_with_warmup from dataset import PoetryDataset from model import CyberPoetModel import json import os from tqdm import tqdm def load_vocab(vocab_path): with open(vocab_path, r, encodingutf-8) as f: vocab json.load(f) return vocab[word2id], vocab[id2word] def train_epoch(model, dataloader, optimizer, scheduler, device, epoch): model.train() total_loss 0 progress_bar tqdm(dataloader, descfEpoch {epoch}) for batch in progress_bar: input_ids batch[input_ids].to(device) labels batch[labels].to(device) optimizer.zero_grad() outputs model(input_idsinput_ids, labelslabels) loss outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 梯度裁剪 optimizer.step() scheduler.step() total_loss loss.item() progress_bar.set_postfix({loss: loss.item()}) avg_loss total_loss / len(dataloader) return avg_loss def main(): # 配置参数 data_path data/processed/poem_ids.npy vocab_path data/processed/vocab.json pretrained_model uer/gpt2-chinese-cluecorpussmall # 一个开源的中文GPT-2模型 batch_size 16 num_epochs 10 learning_rate 5e-5 warmup_steps 100 device torch.device(cuda if torch.cuda.is_available() else cpu) print(fUsing device: {device}) # 加载词表和数据 word2id, id2word load_vocab(vocab_path) vocab_size len(word2id) dataset PoetryDataset(data_path) dataloader DataLoader(dataset, batch_sizebatch_size, shuffleTrue) # 初始化模型 model CyberPoetModel(vocab_sizevocab_size, pretrained_model_namepretrained_model) model.to(device) # 设置优化器和学习率调度器 optimizer AdamW(model.parameters(), lrlearning_rate) total_steps len(dataloader) * num_epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepswarmup_steps, num_training_stepstotal_steps ) # 训练循环 os.makedirs(outputs/checkpoints, exist_okTrue) for epoch in range(1, num_epochs 1): avg_loss train_epoch(model, dataloader, optimizer, scheduler, device, epoch) print(fEpoch {epoch} finished. Average Loss: {avg_loss:.4f}) # 每2个epoch保存一次检查点 if epoch % 2 0: checkpoint_path foutputs/checkpoints/epoch_{epoch}.pt torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: avg_loss, }, checkpoint_path) print(fCheckpoint saved to {checkpoint_path}) print(Training finished.) if __name__ __main__: main()4.4 生成诗句训练完成后我们可以使用模型来生成新的诗句。创建src/generate.py# src/generate.py import torch from model import CyberPoetModel import json import argparse from transformers import GPT2Tokenizer def load_model_and_vocab(checkpoint_path, vocab_path, device): 加载训练好的模型和词表 with open(vocab_path, r, encodingutf-8) as f: vocab json.load(f) word2id, id2word vocab[word2id], vocab[id2word] vocab_size len(word2id) # 初始化模型结构需与训练时一致 model CyberPoetModel(vocab_sizevocab_size, pretrained_model_nameNone) model.to(device) # 加载训练好的权重 checkpoint torch.load(checkpoint_path, map_locationdevice) model.load_state_dict(checkpoint[model_state_dict]) model.eval() return model, word2id, id2word def generate_poem(model, word2id, id2word, device, start_words春江花月夜, max_len50, temperature0.8): 生成一首诗 model.eval() # 将起始词转为ID input_ids [word2id.get([BOS], 1)] for char in start_words: input_ids.append(word2id.get(char, word2id.get([UNK], 1))) generated input_ids.copy() with torch.no_grad(): for _ in range(max_len): inputs torch.tensor([generated], dtypetorch.long).to(device) outputs model.gpt2(input_idsinputs) # 获取最后一个时间步的logits next_token_logits outputs.logits[0, -1, :] / temperature # 使用top-k采样增加多样性 top_k 20 indices_to_remove next_token_logits torch.topk(next_token_logits, top_k)[0][..., -1, None] next_token_logits[indices_to_remove] -float(Inf) # 计算概率并采样 probs torch.softmax(next_token_logits, dim-1) next_token_id torch.multinomial(probs, num_samples1).item() # 如果生成了结束符则停止 if next_token_id word2id.get([EOS], 2): break generated.append(next_token_id) # 简单规则生成长度超过5且遇到常见句尾字如‘。’‘’时有一定概率停止 # 这里简化处理实际可以更复杂 if len(generated) 10 and id2word.get(str(next_token_id), ) in [。, ]: if torch.rand(1).item() 0.7: break # 将ID序列转回文字 poem .join([id2word.get(str(token_id), [UNK]) for token_id in generated if token_id not in [word2id[[BOS]], word2id[[EOS]], word2id[[PAD]]]]) return poem def main(): parser argparse.ArgumentParser() parser.add_argument(--checkpoint, typestr, defaultoutputs/checkpoints/epoch_10.pt, help模型检查点路径) parser.add_argument(--vocab, typestr, defaultdata/processed/vocab.json, help词表路径) parser.add_argument(--start, typestr, default秋风, help生成诗句的起始词) parser.add_argument(--num, typeint, default5, help生成数量) args parser.parse_args() device torch.device(cuda if torch.cuda.is_available() else cpu) model, word2id, id2word load_model_and_vocab(args.checkpoint, args.vocab, device) print(f以 {args.start} 开头生成 {args.num} 首诗) print(- * 30) for i in range(args.num): poem generate_poem(model, word2id, id2word, device, start_wordsargs.start, max_len60) print(f{i1}. {poem}) print() if __name__ __main__: main()4.5 运行与结果数据准备将收集到的诗词文本放入data/raw/poetry.txt运行python src/data_processor.py。模型训练运行python src/train.py。根据数据量和GPU性能训练可能需要数小时到数天。建议先在少量数据上跑通流程。诗句生成训练完成后运行python src/generate.py --start “玄裳”。示例输出模拟以 ‘玄裳’ 开头生成 5 首诗 ------------------------------ 1. 玄裳夜渡小楼阴侵霜遍袖浑无觉。独倚阑干听雁过数峰江上暮云深。 2. 玄裳曳影过寒潭露冷风清月半涵。何处笛声吹欲断满庭秋色在江南。 3. 玄裳客子意何如一片孤云万里余。莫道故园春色晚梅花已寄陇头书。 4. 玄裳步月下瑶阶玉露无声湿锦鞋。忽忆当年携手处满池荷叶雨中来。 5. 玄裳独立水精宫十二阑干面面风。吹尽碧桃千树雪月明人在玉壶中。可以看到模型在学习了大量古诗后能够以“玄裳”为引生成在风格、意象和语法上都颇具古意的诗句。第一句甚至与我们标题中的例句在意境上有所呼应。5. 常见问题与排查思路在构建和训练“赛博诗人”过程中你可能会遇到以下典型问题问题现象常见原因解决思路训练Loss不下降或为NaN1. 学习率过高。2. 梯度爆炸。3. 数据预处理有误存在大量[UNK]。4. 预训练模型与任务差异过大。1. 降低学习率如从5e-5调到2e-5。2. 添加梯度裁剪 (clip_grad_norm_)。3. 检查词表构建确保训练集中常见字都被收录可降低min_freq。4. 尝试使用更接近古典诗词领域的预训练模型或在通用模型上增加更多训练轮次。生成的诗句不通顺或重复1. 模型训练不足欠拟合。2. 生成策略过于贪婪每次都选概率最大的字。3. 训练数据质量差或数量少。1. 增加训练轮次Epoch。2. 在生成时使用Top-k采样或核采样并调整temperature参数大于1增加随机性小于1减少随机性。3. 清洗数据确保诗句完整增加数据量。生成结果不符合格律1. 模型未学习到平仄、对仗规则。2. 损失函数只考虑了语言模型未加入格律约束。1. 在数据预处理阶段为每句诗标注平仄模式作为特征输入模型。2. 在损失函数中增加格律损失惩罚不符合平仄模式的生成结果。这是一个高级话题需要定义平仄规则和损失函数。GPU内存溢出1. 批次大小过大。2. 序列长度过长。3. 模型过大。1. 减小batch_size。2. 在预处理时截断过长的诗或使用动态填充。3. 使用梯度累积多次前向传播累积梯度后再更新一次参数模拟大批次效果。生成的诗句总是很短1.[EOS]结束符被过早预测。2. 生成的最大长度max_len设置过小。1. 在生成时对[EOS]token的概率进行惩罚降低其概率鼓励模型生成更长的序列。2. 适当增加max_len参数。6. 最佳实践与工程建议要让“赛博诗人”项目从实验走向实用需要考虑以下工程化细节数据质量至上数据清洗去除现代诗、残句、重复诗和含有生僻异体字的诗。数据增强对于数据量小的特定体裁如“词”可以进行回译现代文-古文、基于规则的句式变换来扩充数据。数据标注为高质量数据标注“主题”如山水、边塞、咏物、“情感”如忧愁、豪迈、闲适、“季节”等标签便于后续进行可控生成。模型优化策略混合精度训练使用torch.cuda.amp进行自动混合精度训练可以大幅减少GPU内存占用并加快训练速度。模型量化与蒸馏训练完成后可以考虑对模型进行量化或知识蒸馏得到更小、更快的模型便于部署。集成外部知识可以构建一个古典诗词的“知识库”如意象库、典故库在生成过程中通过检索增强的方式让模型引用经典意象提升生成质量。可控生成与交互Prompt工程设计好的提示模板。例如输入“[主题送别][情感伤感][格式七言绝句]”模型应能生成符合要求的诗。条件控制生成使用Control Prefixes或P-tuning等技术将主题、情感等控制条件作为可训练的前缀或参数注入模型实现更精准的控制。交互式创作开发Web界面允许用户输入首句、选择韵脚、调整生成“随机性”temperature实时看到多组结果并选择满意的进行微调或续写。评价体系构建自动化评价指标如BLEU, ROUGE对于诗词这种创造性文本参考价值有限。建立多维评价体系流畅度语言模型困惑度。合规性通过规则检查平仄、押韵。意境连贯性使用情感分析模型或意象匹配度来评估。人工评估定期组织小规模评测收集用户对生成诗句在“意境”、“创新性”、“古风程度”等方面的打分用于优化模型。部署与API化使用FastAPI或Flask将模型封装为RESTful API。使用ONNX Runtime或TorchServe进行高性能模型服务化部署。添加请求频率限制、输入验证和日志监控保证服务稳定性。通过以上步骤你不仅能够复现一个基本的AI诗词生成模型更能深入理解如何将一个有趣的NLP想法系统化地工程实现并持续迭代优化。从“玄裳夜渡”的灵感到一个可运行、可改进的“赛博诗人”项目这其中的每一步都充满了挑战与乐趣。