
简介这是一套面向AI爱好者与古诗词编程初学者的AI作诗完整项目基于Keras框架采用LSTM与RNN算法学习并预测古诗、唐诗及五言绝句。它解决了从零搭建文本生成模型的难题支持藏头诗、随机写诗、给定首句或首字作诗等多种生成方式并可自由控制诗歌形式、长度与生成概率兼顾创作灵感启发与诗歌风格探索。资源包共11个文件包含5个Python脚本训练、随机生成、藏头诗预测等核心逻辑、2个txt数据集与日志文本、1个h5预训练模型、1个ipynb交互式笔记本、1个log训练记录及1个md说明文档压缩包约110.97MB目录结构清晰便于按模块学习与二次开发。目前已有1792人学习下载。读者可直接加载预训练模型体验作诗效果也可替换数据集重新训练逐步掌握数据预处理、模型构建与调参的完整流程适合作为NLP入门与文本生成实战的参考案例。1. 从零搭一个五言绝句生成器为什么我不建议你直接上大模型去年冬天一个做语文教育产品的朋友找到我说想给 App 加一个「AI 写古诗」的入口用户点一下就能生成一首五言绝句要求平仄大致对、押韵别太离谱、意境别像机翻。他第一反应是接一个大模型 API我让他先别急因为古诗生成这件事大模型反而容易「翻车」——它太懂现代汉语了写出来的东西经常是「披着古诗外衣的现代散文」字数对、韵脚错、平仄全乱而且每次调用都要花钱离线场景直接歇菜。这就是「AI古诗生成器唐诗五言绝句自动生成包含预训练模型数据集全套代码」这个标题真正要解决的问题用一套可控、可离线、可复现的方案把五言绝句的生成拆成「数据 → 预训练模型 → 微调 → 解码约束」四步而不是把希望全押在一个黑盒 API 上。它适合两类人一类是想把古诗生成塞进自己产品里的工程师另一类是想拿它当 NLP 入门项目练手的学生。整套东西的核心不是「模型多大」而是「约束多细」——五言绝句只有 20 个字容错率极低一个字的平仄错了整首诗就废了。我最终给他的方案是用中文预训练模型做底座在唐诗数据集上微调再在解码阶段加平仄和押韵约束。下面我把这套东西从头到尾讲清楚包括数据集怎么处理、预训练模型怎么选、代码怎么写、参数怎么调以及我踩过的那些坑。2. 数据集与预训练模型先把「唐诗」变成模型能吃的格式2.1 唐诗数据集从哪来、怎么清洗公开的唐诗数据常见做法是从《全唐诗》整理版里抽网上能搜到 JSON 或 CSV 格式的合集一般包含「标题、作者、正文」三个字段。但直接拿来用会出问题正文里混着七言、五言律诗、绝句、乐府还有大量生僻字和异体字。五言绝句生成器只需要四句、每句五字的样本所以第一步是过滤。我一般会写一个清洗脚本逻辑是按标点切句只保留恰好四句、每句恰好五字的样本同时把异体字统一成常用字。下面是我用的过滤代码import json import re def clean_poem(text): # 去掉括号注释和多余空白 text re.sub(r[(].*?[)], , text) text re.sub(r\s, , text) # 按中文标点切句 lines re.split(r[。], text) lines [l for l in lines if l] return lines def is_wuyan_jueju(lines): # 五言绝句四句每句五字 if len(lines) ! 4: return False return all(len(l) 5 for l in lines) def build_dataset(raw_path, out_path): samples [] with open(raw_path, r, encodingutf-8) as f: data json.load(f) for item in data: lines clean_poem(item.get(content, )) if is_wuyan_jueju(lines): samples.append(.join(lines)) # 去重 samples list(set(samples)) with open(out_path, w, encodingutf-8) as f: for s in samples: f.write(s \n) print(f保留样本数: {len(samples)}) build_dataset(raw_poems.json, wuyan_jueju.txt)这段代码的关键在is_wuyan_jueju这个函数它把「四句五字」作为硬条件不符合的直接丢掉。参数上raw_path是原始 JSON 路径out_path是输出纯文本每行一首诗、共 20 个字。清洗完一般能剩下几万首足够微调一个小模型。注意别用「按字数过滤」这种偷懒办法因为七言诗里也可能出现五字句必须按句切分后再判断。2.2 预训练模型选 RoBERTa 还是 GPT 类看你的生成方式标题里提到「预训练模型」这里有个选型分叉如果你要做「自回归生成」一个字一个字往外蹦底座应该选 GPT 类的中文预训练模型如果你要做「掩码填充」挖空让模型填才用 RoBERTa 这类。五言绝句生成我推荐自回归路线因为用户要的是「从无到有写一首」而不是「补全一首」。RoBERTa 中文预训练模型在热词里被频繁提到它强在理解不强在生成。如果你拿 RoBERTa 直接做生成得反复掩码、迭代填字速度慢且容易陷入局部最优。我一般会选参数量在 1 亿左右的中文 GPT 底座显存占用可控单卡 8G 就能微调。选型时看三个指标词表是否覆盖常见汉字、最大序列长度是否 ≥ 64、是否支持自定义 tokenizer。五言绝句加上分隔符也就 25 个 token 左右序列长度完全够用。提示不要一上来就下载最大的预训练模型。古诗生成的数据量通常只有几万首模型太大反而过拟合生成的诗会「背」训练集里的句子。2.3 把诗转成模型输入tokenizer 与特殊符号模型不认识「诗」只认识 token。我一般会在每首诗的字之间不加空格但用特殊符号标记句边界比如用[SEP]隔开四句开头加[BOS]结尾加[EOS]。这样模型能学到「一句结束该换句」的节奏。转换代码如下from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(your-chinese-gpt-base) def encode_poem(poem): # poem 是 20 个字的字符串 lines [poem[i:i5] for i in range(0, 20, 5)] text [BOS] [SEP].join(lines) [EOS] return tokenizer(text, return_tensorspt, paddingmax_length, max_length32, truncationTrue) sample 床前明月光疑是地上霜举头望明月低头思故乡 encoded encode_poem(sample) print(encoded[input_ids].shape)这里max_length32是留了余量因为加上特殊符号后长度会超过 25。[SEP]的作用是让模型在生成时知道「该断句了」否则它可能写出 20 个连续的字读起来没有停顿。参数上paddingmax_length保证 batch 内长度一致truncationTrue防止异常长样本撑爆显存。3. 微调预训练模型让底座学会「唐诗味」3.1 训练目标怎么设自回归语言建模微调阶段的目标很简单给定前 n 个字预测第 n1 个字。这就是标准的因果语言建模Causal LM。损失函数用交叉熵只计算预测位置的 loss不计算 padding 位置。我一般会用 HuggingFace 的Trainer或自己写训练循环核心是构造labels时把输入右移一位。import torch from torch.utils.data import Dataset class PoemDataset(Dataset): def __init__(self, path, tokenizer, max_len32): self.samples [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if len(line) 20: self.samples.append(line) self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.samples) def __getitem__(self, idx): poem self.samples[idx] lines [poem[i:i5] for i in range(0, 20, 5)] text [BOS] [SEP].join(lines) [EOS] enc self.tokenizer(text, paddingmax_length, max_lengthself.max_len, truncationTrue, return_tensorspt) input_ids enc[input_ids].squeeze(0) # 自回归labels 就是 input_ids 右移最后一位忽略 labels input_ids.clone() labels[:-1] input_ids[1:] labels[-1] -100 # 忽略最后一个预测 return {input_ids: input_ids, labels: labels}关键在labels[:-1] input_ids[1:]这一行它把「下一个字」作为监督信号。-100是 PyTorch 交叉熵的忽略索引用来屏蔽 padding 和最后一个无效位置。参数上max_len32要和 tokenizer 对齐paddingmax_length保证每个样本长度一致否则 batch 拼接会报错。3.2 训练参数学习率、batch size 和早停微调预训练模型最怕两件事学习率太大把底座学崩学习率太小半天不收敛。我的血泪经验是学习率设在 1e-5 到 5e-5 之间batch size 根据显存尽量大通常 16 或 32。训练轮数不要多3 到 5 个 epoch 就够因为数据量小多了必过拟合。下面是我常用的训练配置from transformers import TrainingArguments, Trainer training_args TrainingArguments( output_dir./poem_model, num_train_epochs4, per_device_train_batch_size16, learning_rate2e-5, warmup_steps200, weight_decay0.01, logging_steps50, save_steps500, evaluation_strategyno, fp16True, ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset, ) trainer.train()warmup_steps200是让学习率从 0 慢慢升到设定值避免一开始就把预训练权重冲乱。fp16True在支持半精度的卡上能省一半显存但如果你用的是老卡开了可能 loss 变 NaN那就关掉。weight_decay0.01是常规正则防止过拟合。训练时盯着 loss如果降到 1.5 以下还在降但生成的诗开始重复训练集原句那就是过拟合了该早停。3.3 怎么判断模型「学会了」看生成样本而不是看 lossloss 低不代表生成好。我一般每训练 500 步就手动跑一次生成看三件事字数对不对、句子通不通、有没有「唐诗味」。如果生成的是「今天天气真好呀」这种现代口语说明底座太强、微调不够得加大学习率或增加数据。如果生成的是「明月照高楼流光正徘徊」这种基本就对了。下面是一个快速验证脚本def generate(model, tokenizer, prompt[BOS], max_new_tokens25): model.eval() input_ids tokenizer(prompt, return_tensorspt)[input_ids] with torch.no_grad(): output model.generate( input_ids, max_new_tokensmax_new_tokens, do_sampleTrue, top_k50, top_p0.9, temperature0.8, repetition_penalty1.2, ) text tokenizer.decode(output[0], skip_special_tokensFalse) return text print(generate(model, tokenizer))top_k50和top_p0.9是采样策略控制生成的多样性。temperature0.8稍微降低随机性让诗句更稳。repetition_penalty1.2是防止同一个字反复出现古诗里重复字太多会显得很怪。这些参数不是固定的你可以根据生成效果微调比如想要更「规矩」的诗就把 temperature 降到 0.6。4. 解码约束五言绝句的平仄和押韵怎么用代码卡住4.1 平仄约束在生成时屏蔽不合律的字五言绝句有固定的平仄格式常见的有「仄仄平平仄」「平平仄仄平」等四种基本句式。模型生成时如果只靠它自己学平仄错误率不低。我的做法是在每一步解码时根据当前句式和位置计算哪些字符合平仄然后把这些字的 logits 保留其余设为负无穷。这样模型只能在合律的字里选。# 简化版平仄表1 代表平0 代表仄-1 代表可平可仄 PING_ZE { 平: 1, 仄: 0, } # 五言绝句四种基本句式平仄序列 PATTERNS [ [0, 0, 1, 1, 0], # 仄仄平平仄 [1, 1, 0, 0, 1], # 平平仄仄平 [1, 0, 0, 1, 1], # 平仄仄平平 [0, 1, 1, 0, 0], # 仄平平仄仄 ] def apply_pingze_constraint(logits, position, pattern): # position: 当前生成到第几个字0-4 # pattern: 当前句的平仄序列 target pattern[position] for token_id, char in tokenizer.get_vocab().items(): if len(char) ! 1: continue if char in PING_ZE and PING_ZE[char] ! target: logits[token_id] -float(inf) return logits这段代码的核心是apply_pingze_constraint它遍历词表把不符合当前平仄要求的字全部屏蔽。实际使用时你需要在model.generate的logits_processor里挂上这个函数。注意词表里有多字 token要跳过len(char) ! 1的项。平仄表可以做得更细比如考虑「一三五不论二四六分明」但五言绝句字数少我建议严格卡。4.2 押韵约束韵脚字必须同韵五言绝句一般二、四句押韵第一句可押可不押。押韵约束比平仄简单先确定韵脚位置第 2 句和第 4 句的最后一个字然后从韵书里查同韵字。我一般会预加载一个「平水韵」的韵部表生成到韵脚位置时只保留同韵部的字。# 简化韵部表key 是韵部名value 是常用字集合 RHYME_DICT { 东: set(东同铜桐筒童僮瞳中衷忠虫终戎崇嵩弓躬宫融雄熊穹穷冯风枫丰充隆空公功工攻蒙笼聋珑洪红鸿虹丛翁聪通蓬烘潼胧砻峒螽梦讧冻忡酆恫总侗窿懵庞种盅芎倥艨绒葱匆骢), 冬: set(冬农宗钟龙舂松冲容蓉庸封胸雍浓重从逢缝踪茸峰锋烽蛩慵恭供淙侬松凶墉镛佣溶邛共憧喁邕壅纵龚枞脓淞匈汹禺蛹榕), # ... 其他韵部省略 } def apply_rhyme_constraint(logits, rhyme_group): allowed RHYME_DICT.get(rhyme_group, set()) for token_id, char in tokenizer.get_vocab().items(): if len(char) ! 1: continue if char not in allowed: logits[token_id] -float(inf) return logitsrhyme_group由第一句的韵脚决定一旦确定就锁定后面所有韵脚位置都用同一个韵部。注意韵书里有些字是多音字可能跨韵部我一般只保留最常用的读音对应的韵部避免生成时出现「看似押韵实则不押」的情况。4.3 把约束串起来一个完整的生成流程实际生成时平仄和押韵要同时生效。我一般会写一个LogitsProcessor类在每一步先应用平仄约束再应用押韵约束最后交给模型采样。流程是生成第一句 → 确定韵脚 → 生成第二句 → 生成第三句 → 生成第四句每句内部按平仄序列逐字约束。from transformers import LogitsProcessor class PoemLogitsProcessor(LogitsProcessor): def __init__(self, tokenizer, patterns, rhyme_dict): self.tokenizer tokenizer self.patterns patterns self.rhyme_dict rhyme_dict self.step 0 self.rhyme_group None def __call__(self, input_ids, scores): # 根据 self.step 判断当前是第几句第几字 # 应用平仄和押韵约束 # 更新 self.step return scores这个类需要维护生成进度因为generate是逐 token 调用的。self.step从 0 开始每生成一个字加一到 5 的倍数时切换下一句。韵脚位置step 为 4、9、14、19要额外应用押韵约束。实际写的时候注意scores是 batch 维度的如果 batch size 大于 1每个样本的约束可能不同我一般把 batch size 设为 1 来简化。5. 避坑与排查古诗生成器最容易翻车的 5 个地方5.1 生成结果全是「明月」「清风」过拟合了现象不管输入什么生成的诗里反复出现「明月」「清风」「白云」这几个词换 temperature 也没用。原因训练集里这些意象出现频率太高模型学到了「偷懒」——只要写这几个字loss 就低。加上训练轮数过多模型直接记住了高频组合。解决在数据清洗阶段做词频统计对高频意象字做下采样或者训练时加repetition_penalty。更彻底的办法是增加数据多样性别只用一本《全唐诗》把唐宋诗都混进来。5.2 平仄约束加了但生成还是不合律现象明明挂了apply_pingze_constraint生成的诗读起来平仄还是不对。原因大概率是词表里有「多字 token」比如「明月」是一个 token你按单字平仄去卡它直接跳过了。另外logits_processor的执行顺序可能被其他 processor 覆盖。解决在约束函数里强制跳过len(char) ! 1的 token并且在 tokenizer 初始化时设置do_basic_tokenizeFalse确保每个汉字独立成 token。如果还不行检查generate的logits_processor列表把你的 processor 放在最后。5.3 韵脚字选对了但读起来不押韵现象韵脚字确实在同一个韵部但用现代汉语读起来不押韵比如「东」和「风」。原因平水韵是古代韵书和现代普通话发音有差异。用户用普通话读自然觉得别扭。解决如果产品面向现代用户建议用「中华新韵」代替平水韵或者同时支持两套韵书让用户选。我一般默认用新韵因为大部分用户不懂古韵。5.4 训练 loss 降到很低但生成全是乱码现象loss 降到 0.5 以下但生成出来的是「啊啊啊啊啊」或者重复同一个字。原因学习率太大把预训练权重冲崩了模型退化成「只会预测高频字」。或者labels构造错了把 padding 也算了 loss。解决把学习率降到 1e-5 重新训练检查labels里 padding 位置是否设为-100。另外fp16True在某些卡上会导致梯度溢出关掉试试。5.5 生成速度太慢单首超过 10 秒现象每次生成要等好几秒用户体验差。原因逐字约束时遍历了整个词表词表几万字每次都循环一遍CPU 直接跑满。解决提前把每个平仄、每个韵部对应的 token id 列表算好存成字典生成时直接查表不要每次遍历词表。另外把模型放到 GPU 上max_new_tokens设成 25 就够别设太大。6. 进阶技巧用「模板 改写」把生成质量再提一档纯靠模型从零生成质量天花板有限。我后来用了一个更稳的办法先让模型生成一个「草稿」再用模板做二次改写。具体做法是准备一批五言绝句的「骨架模板」比如「[名词][动词][名词][名词][动词][名词]」然后让模型只填名词和动词这样平仄和押韵更容易控制。另一个技巧是「两阶段生成」第一阶段用高 temperature 生成 10 首候选第二阶段用一个小的打分模型可以用 RoBERTa 微调一个「诗句通顺度」分类器给每首打分选最高分的那首返回。这样既保留了多样性又过滤掉了明显不通的。打分模型的数据可以用人工标注的几百条样本标注标准就一条读起来像不像唐诗。def two_stage_generate(model, scorer, tokenizer, n_candidates10): candidates [] for _ in range(n_candidates): text generate(model, tokenizer, temperature1.0) score scorer(text) candidates.append((score, text)) candidates.sort(reverseTrue) return candidates[0][1]n_candidates10是经验和速度的平衡点再多就慢了。scorer可以是一个简单的文本分类模型输入是 20 个字的诗输出是 0 到 1 的分数。训练这个打分模型不需要太多数据几百条就够关键是标注要一致。我自己的习惯是每次改完约束逻辑先跑 50 首生成人工读一遍把明显翻车的记下来回头调参数。古诗生成这件事参数没有银弹全靠反复试。希望帮到你。本文还有配套的精品资源点击获取