news 2026/9/26 3:59:48

从零构建五言绝句生成器:预训练模型微调与解码约束实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建五言绝句生成器:预训练模型微调与解码约束实战

简介:这是一套面向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', encoding='utf-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', encoding='utf-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:i+5] for i in range(0, 20, 5)] text = '[BOS]' + '[SEP]'.join(lines) + '[EOS]' return tokenizer(text, return_tensors='pt', padding='max_length', max_length=32, truncation=True) sample = '床前明月光疑是地上霜举头望明月低头思故乡' encoded = encode_poem(sample) print(encoded['input_ids'].shape)

这里max_length=32是留了余量,因为加上特殊符号后长度会超过 25。[SEP]的作用是让模型在生成时知道「该断句了」,否则它可能写出 20 个连续的字,读起来没有停顿。参数上,padding='max_length'保证 batch 内长度一致,truncation=True防止异常长样本撑爆显存。

3. 微调预训练模型:让底座学会「唐诗味」

3.1 训练目标怎么设:自回归语言建模

微调阶段的目标很简单:给定前 n 个字,预测第 n+1 个字。这就是标准的因果语言建模(Causal LM)。损失函数用交叉熵,只计算预测位置的 loss,不计算 padding 位置。我一般会用 HuggingFace 的Trainer或自己写训练循环,核心是构造labels时把输入右移一位。

import torch from torch.utils.data import Dataset class PoemDataset(Dataset): def __init__(self, path, tokenizer, max_len=32): self.samples = [] with open(path, 'r', encoding='utf-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:i+5] for i in range(0, 20, 5)] text = '[BOS]' + '[SEP]'.join(lines) + '[EOS]' enc = self.tokenizer(text, padding='max_length', max_length=self.max_len, truncation=True, return_tensors='pt') 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_len=32要和 tokenizer 对齐,padding='max_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_epochs=4, per_device_train_batch_size=16, learning_rate=2e-5, warmup_steps=200, weight_decay=0.01, logging_steps=50, save_steps=500, evaluation_strategy='no', fp16=True, ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, ) trainer.train()

warmup_steps=200是让学习率从 0 慢慢升到设定值,避免一开始就把预训练权重冲乱。fp16=True在支持半精度的卡上能省一半显存,但如果你用的是老卡,开了可能 loss 变 NaN,那就关掉。weight_decay=0.01是常规正则,防止过拟合。训练时盯着 loss,如果降到 1.5 以下还在降,但生成的诗开始重复训练集原句,那就是过拟合了,该早停。

3.3 怎么判断模型「学会了」:看生成样本而不是看 loss

loss 低不代表生成好。我一般每训练 500 步就手动跑一次生成,看三件事:字数对不对、句子通不通、有没有「唐诗味」。如果生成的是「今天天气真好呀」这种现代口语,说明底座太强、微调不够,得加大学习率或增加数据。如果生成的是「明月照高楼流光正徘徊」这种,基本就对了。下面是一个快速验证脚本:

def generate(model, tokenizer, prompt='[BOS]', max_new_tokens=25): model.eval() input_ids = tokenizer(prompt, return_tensors='pt')['input_ids'] with torch.no_grad(): output = model.generate( input_ids, max_new_tokens=max_new_tokens, do_sample=True, top_k=50, top_p=0.9, temperature=0.8, repetition_penalty=1.2, ) text = tokenizer.decode(output[0], skip_special_tokens=False) return text print(generate(model, tokenizer))

top_k=50和top_p=0.9是采样策略,控制生成的多样性。temperature=0.8稍微降低随机性,让诗句更稳。repetition_penalty=1.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 logits

rhyme_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_tokenize=False,确保每个汉字独立成 token。如果还不行,检查generate的logits_processor列表,把你的 processor 放在最后。

5.3 韵脚字选对了但读起来不押韵

现象:韵脚字确实在同一个韵部,但用现代汉语读起来不押韵,比如「东」和「风」。

原因:平水韵是古代韵书,和现代普通话发音有差异。用户用普通话读,自然觉得别扭。

解决:如果产品面向现代用户,建议用「中华新韵」代替平水韵,或者同时支持两套韵书,让用户选。我一般默认用新韵,因为大部分用户不懂古韵。

5.4 训练 loss 降到很低但生成全是乱码

现象:loss 降到 0.5 以下,但生成出来的是「啊啊啊啊啊」或者重复同一个字。

原因:学习率太大,把预训练权重冲崩了,模型退化成「只会预测高频字」。或者labels构造错了,把 padding 也算了 loss。

解决:把学习率降到 1e-5 重新训练,检查labels里 padding 位置是否设为-100。另外,fp16=True在某些卡上会导致梯度溢出,关掉试试。

5.5 生成速度太慢,单首超过 10 秒

现象:每次生成要等好几秒,用户体验差。

原因:逐字约束时遍历了整个词表,词表几万字,每次都循环一遍,CPU 直接跑满。

解决:提前把每个平仄、每个韵部对应的 token id 列表算好,存成字典,生成时直接查表,不要每次遍历词表。另外,把模型放到 GPU 上,max_new_tokens设成 25 就够,别设太大。

6. 进阶技巧:用「模板 + 改写」把生成质量再提一档

纯靠模型从零生成,质量天花板有限。我后来用了一个更稳的办法:先让模型生成一个「草稿」,再用模板做二次改写。具体做法是,准备一批五言绝句的「骨架模板」,比如「[名词][动词][名词],[名词][动词][名词]」,然后让模型只填名词和动词,这样平仄和押韵更容易控制。

另一个技巧是「两阶段生成」:第一阶段用高 temperature 生成 10 首候选,第二阶段用一个小的打分模型(可以用 RoBERTa 微调一个「诗句通顺度」分类器)给每首打分,选最高分的那首返回。这样既保留了多样性,又过滤掉了明显不通的。打分模型的数据可以用人工标注的几百条样本,标注标准就一条:读起来像不像唐诗。

def two_stage_generate(model, scorer, tokenizer, n_candidates=10): candidates = [] for _ in range(n_candidates): text = generate(model, tokenizer, temperature=1.0) score = scorer(text) candidates.append((score, text)) candidates.sort(reverse=True) return candidates[0][1]

n_candidates=10是经验和速度的平衡点,再多就慢了。scorer可以是一个简单的文本分类模型,输入是 20 个字的诗,输出是 0 到 1 的分数。训练这个打分模型不需要太多数据,几百条就够,关键是标注要一致。

我自己的习惯是:每次改完约束逻辑,先跑 50 首生成,人工读一遍,把明显翻车的记下来,回头调参数。古诗生成这件事,参数没有银弹,全靠反复试。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 3:58:32

OpenSpec 安装与使用步骤:用 TaoToken 统一 Key 打通 AI 工具配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华