自动写诗实战:从马尔可夫链到语言模型的格律约束生成 📅 发布时间:2026/9/13 18:23:24 👁 浏览次数: 简介一份以自动写诗为核心的人工智能实验资源包面向自然语言处理初学者、深度学习开发者及相关课程学员涵盖数据准备、模型训练到效果评估的完整流程。压缩包共含十八个文件包括六个Python编译缓存文件、四个源代码脚本、两份实验文档、两份文本数据、一份演示文稿以及模型、数据和日志文件各一个整体大小约23.83MB。已有169人学习下载适合作为课程设计或自学项目的参考。通过源码可了解循环神经网络、长短期记忆网络、Transformer等模型在诗词生成中的实际应用文档则详细说明实验步骤、参数调整与BLEU、ROUGE等评价指标PPT便于答辩或课堂展示。附带的数据文件可直接用于训练帮助读者快速复现实验并深入理解人工智能在艺术创作中的技术实现。1. 从「自动写诗.rar」说起先解决约束再谈生成「自动写诗.rar」这个文件名透露的信息比表面多它打包的不是在线 API而是一个能本地解压、命令行直接运行的项目。这类包里通常是一份古诗语料、一段生成脚本运气好再配一个预训练模型权重。把脚本跑通不难难在让生成结果「像诗」——自动写诗的本质是带约束的文本生成除了语句通顺还得满足句数、字数、押韵甚至平仄这些硬规则而通用文本生成模型恰恰不擅长硬约束。下文按我实际做这类项目的顺序展开先立住两条技术路线再给一个不依赖 GPU 的最小实现然后讲预训练模型怎么用最后落到韵律校验和评估这些决定能否落地的细节。2. 自动写诗的两种路线从转移概率到语言模型2.1 先拆解「生成什么」格律约束的工程表示自动写诗和通用文本生成的分水岭在约束。以最常见的七言绝句为例一次完整的生成要同时满足四类条件恰好四句每句七字第二、四句末字押同一韵部第一句末字可押可不押相邻句在关键位置平仄相对。单独看每一条都不难合在一起就变成了组合约束问题因为生成器每吐一个字都可能让后面的选择空间收窄甚至走到死路。工程上通常把这四类约束拆成可控的表示。句数是生成器的外层循环次数字数是单句生成的截断长度押韵和平仄则作为后验校验器挂在生成结束后。我在做这类工具时会先列一张约束表把「规则是什么」和「代码里怎么表示」对应起来避免在写生成逻辑时把约束散落得到处都是。约束类型规则内容工程表示句数绝句固定四句外层 for 循环 4 次字数五言 5 字、七言 7 字单句字符数截断押韵二、四句末字同韵首句可押韵部映射表 尾字校验平仄相邻句相应位置声调交替声调标注 按位交替校验这张表的价值在于无论底层用哪种生成算法约束接口都能保持一致。后面从马尔可夫链换到预训练语言模型改的只是「下一字概率从哪来」规则校验层基本可以原样复用。2.2 统计式路线一阶马尔可夫链为什么仍是合格起点先看最早在 CPU 上就能跑通的做法。一阶马尔可夫链把写诗看成「接着上一个字选下一个字」的随机过程核心是一个转移表 T其中 T[a][b] 表示语料中字符 a 后面出现字符 b 的次数。生成时从起始字出发按转移概率采样下一字直到凑满一句。这条路线的好处很实际语料要求低几千首绝句就能构建像样的转移表生成在毫秒级完成不需要 GPU每一步的概率来源可以打印出来排查「为什么会写出这个字」非常直观。代价同样明显——模型只看前一个字句子内部局部通顺但前后句之间几乎没有语义关联生成多句后容易陷入「你方唱罢我登场」的车轱辘循环。针对语义弱的问题常见的升级是二阶马尔可夫链把键从「当前字」换成「当前二字」。转移表从 T[a][b] 变成 T[ab][c]数据量需求指数上升但局部语感会明显改善。自动写诗类小工具里一阶和二阶混合使用的占大多数因为对绝句这种短句场景七言句内真正需要建模的上下文也就三四个字。2.3 神经式路线语言模型把「像诗」编码进参数统计方法的问题在于它不理解「意象」。当人类读到「孤舟」「寒江」时会自动联想到萧瑟的冬景但马尔可夫链只当它们是普通字符两个词之间的搭配完全靠语料计数维持。预训练语言模型改变了这一点。以自回归语言模型为例它通过在海量文本上学习 P(c_n | c_1, c_2, …, c_{n-1})把「在什么语境下出现什么词」的规律压进模型参数。用于自动写诗时通常会在古诗语料上继续训练让模型记住五言七言的节奏和韵部搭配这就是常说的「微调」。神经式路线虽然语义质量高但引入了两个新问题模型体积和推理耗时决定它不能轻量分发采样策略直接影响生成质量——同一个模型采样温度设在 0.8 还是 1.2产出的风格差异巨大。后文第四部分会专门展开参数调节这里先把结论放出来统计式保底神经式提质两者共用一套韵脚校验接口是自动写诗工程里最稳妥的组合方式。3. 不依赖 GPU 的自动写诗最小实现马尔可夫链生成绝句这一章给一个完整的、能跑的最小实现。语言用 Python 3除标准库外不依赖任何第三方包——这也是很多「自动写诗.rar」解压后的实际形态一个脚本、一份语料、一段说明就够了。3.1 语料清洗把原始文本变成句子池语料是生成的根基。无论从哪个渠道收集古诗拿到的文本几乎都是整首排列夹杂标点、作者名、注释。第一步是把文本切分成独立句子并按长度归类因为五言句和七言句的转移表应该分开建。import re from collections import defaultdict def load_sentences(path, max_len20): sentences [] with open(path, r, encodingutf-8) as f: text f.read() # 按常见句读标点切分中文古诗以逗号句号为主 parts re.split(r[。\n], text) for part in parts: s re.sub(r[^\u4e00-\u9fff], , part) if 4 len(s) max_len: # 过滤太短和太长的残句 sentences.append(s) return sentences corpus load_sentences(poems.txt) print(f句子总数: {len(corpus)})这段代码干了三件事按标点和换行切句把所有非汉字字符剔除最后滤掉长度异常的行。4 len(s)的过滤条件会把「床前明月光」这类五言句留下同时排除单个字或过长的散文式片段。实际操作中语料质量比数量更重要宁可保留三千句干净的也不要混入一万句带噪声的。3.2 构建转移表带温度的加权采样切好句后构建一阶转移表。为了后续生成可控我在采样时加入 temperature 参数温度越低候选字集中在高频项上生成越保守温度越高低频字被选中的概率越大句子越有「意外感」。import random from collections import defaultdict def build_transition(sentences, order1): trans defaultdict(lambda: defaultdict(int)) for s in sentences: for i in range(len(s) - order): key s[i:i order] nxt s[i order] trans[key][nxt] 1 return trans def sample_next(trans, key, temperature1.0): counter trans.get(key) if not counter: return None # 对频次做温度变换再按权重随机 weights [cnt ** (1.0 / temperature) for cnt in counter.values()] chars list(counter.keys()) return random.choices(chars, weightsweights, k1)[0] def generate_line(trans, start, length7, temperature1.0): chars list(start) key start[-2:] if len(start) 2 else start while len(chars) length: nxt sample_next(trans, key, temperature) if nxt is None: break chars.append(nxt) key nxt # 一阶模型新键就是刚生成的字符 return .join(chars[:length])build_transition里order1表示一阶模型键是单个字符把order改成 2 就是二阶模型。sample_next中的温度处理是这类小工具里最值得调的参数设 0.6 时高频字占绝对优势设 1.4 时会频繁冒出不常见的字。如果同时维护一阶和二阶两张表可以先用二阶采样没命中时回退到一阶这种多退少补策略在实战中很常用。3.3 韵脚校验让最后一句落回同一韵部马尔可夫链对韵脚完全无感生成到句末时可能随便收尾。我的做法是先生成一句完整的候选再校验末字是否落在目标韵部不通过就重新生成设置最大重试次数防止死循环。韵部判断用简化的韵母映射。# 把常见韵母归并成宽韵部覆盖大多数普通话押韵场景 RHYME_GROUP { an: an, ian: an, uan: an, ang: ang, iang: ang, uang: ang, a: a, ia: a, ua: a, ou: ou, iu: ou, } def get_final(ch): # 取尾字拼音的韵母部分实际工程中可用 pypinyin或维护一张常用字字典 return final_map.get(ch, ) def rhyme_group(ch): final get_final(ch) return RHYME_GROUP.get(final) def check_rhyme(sentence, target_group): last_char sentence[-1] return rhyme_group(last_char) target_group这段代码把韵部判断收敛到一个表里避免在生成主流程里写死具体韵母。需要说明的是这里走的是「宽韵」路线只要普通话读音相近就视为押韵如果追求严格的平水韵韵部表会膨胀到三十个以上但那时马尔可夫链生成器会因为数据稀疏导致重试率过高实践中反而不可用。3.4 主流程生成一首完整的四句绝句最后把语料、转移表、韵脚校验串起来。首句由命令行传入的主题字开头后三句逐句生成第二、四句做韵脚约束第三句不强制押韵但要求不与已有句子完全重复。def generate_poem(trans, first_char, rhyme_choiceNone, max_trials100): first generate_line(trans, first_char, length7) # 未指定韵部时沿用首句末字的韵部作为全诗韵脚 rhyme rhyme_choice or rhyme_group(first[-1]) lines [first] for need_rhyme in [True, False, True]: # 对应第二、三、四句 line None last lines[-1][-2:] # 取上句末二字作起始参考 for _ in range(max_trials): cand generate_line(trans, random.choice(last), length7) if (not need_rhyme) or check_rhyme(cand, rhyme): if cand not in lines: line cand break if line is None: # 兜底重试耗尽时放弃韵脚至少要保证输出完整 line generate_line(trans, last, length7) lines.append(line) return \n.join(lines) if __name__ __main__: print(generate_poem(trans, 春, rhyme_choicean))主流程里有一个值得注意的设计每一句的起始字符取上一句末二字之一这能让相邻句在字面上产生少许承接感缓解马尔可夫链各句割裂的问题。下面是这个最小实现的核心参数清单按建议值和效果说明整理参数位置建议值效果orderbuild_transition1 或 2阶数越高越依赖语料规模temperaturesample_next0.8 ~ 1.2低值平稳高值跳脱max_trialsgenerate_poem50 ~ 200约束严时需加大句子长度generate_line5 或 7五言七言切换到这里一个不依赖 GPU 的自动写诗工具已经成型。运行python write_poem.py就能看到输出性能瓶颈基本只出现在重试次数上。4. 用预训练语言模型把诗写通微调与采样控制马尔可夫链的瓶颈在上限不在实现。当语料质量足够高、你想让生成结果真正具备「诗意」时就该切到预训练语言模型。这一章讲的不是从零训练一个模型而是站在已有中文预训练权重的基础上做继续训练和推理控制。4.1 微调的本质让模型重新学习格律统计直接拿通用语言模型做自动写诗最典型的问题是内容偏现代、句子长度失控、完全不押韵。原因在于预训练语料以新闻、百科、社区文本为主模型权重里「古诗该怎么写」的统计信息占比太低。微调的目的是把模型在通用语料上学到的语言能力迁移到古诗领域让它重新记住五言七言的节奏密度和韵部使用频次。我一般会在古诗语料上做自回归语言建模也就是让模型根据前文预测下一个字符。数据准备阶段把每首诗按句切分句与句之间用换行符连接模型只负责学会「下一个字是什么」from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments # 示意换成你本地准备好的中文 GPT-2 权重目录 tokenizer AutoTokenizer.from_pretrained(path/to/local_chinese_gpt2_checkpoint) model AutoModelForCausalLM.from_pretrained(path/to/local_chinese_gpt2_checkpoint) def encode(texts, block_size128): for t in texts: ids tokenizer.encode(t) for i in range(0, len(ids) - block_size, block_size): yield ids[i:i block_size] args TrainingArguments( output_dir./poet-ft, num_train_epochs5, per_device_train_batch_size4, learning_rate5e-5, save_strategyepoch, ) # DataCollator 会把 input_ids 同时作为 label完成语言建模训练 trainer Trainer(modelmodel, argsargs, train_datasetencoded_dataset) trainer.train()这段示意代码里值得说明的是两个细节block_size128表示按 128 个 token 切块对绝句这种短文本绰绰有余learning_rate5e-5比通用预训练小一个量级目的是微调权重而不是破坏原有语言能力。Trainer在语言建模任务上会自动把输入同时作为标签无需手动构造 label。训练的另一个常见陷阱是过拟合。古诗语料通常只有几万首模型参数动辄上亿训练两三轮后 loss 会快速下降但生成结果可能出现整句照搬训练集的现象。我一般会把验证集单独留出每轮结束后跑几条生成样例做人工观察比只看 loss 曲线可靠得多。4.2 生成参数怎么调temperature、top-k、top-p 与重复惩罚微调之后质量的下一个关键变量在推理阶段。同一个 checkpoint采样策略不同效果天差地别。下面是自动写诗场景下的参数经验值参数作用建议初值调高/调低效果temperature控制概率分布锐度0.9低则保守重复高则散乱top_k只从概率最高的 k 个词中采样50调高增加多样性top_p按累积概率截断候选集0.9调低更稳定调高更多变repetition_penalty惩罚已出现过的 token1.1过高会导致语句别扭这四者不是并列关系而是叠加关系先按 top-k 截断再按 top-p 做累积概率过滤最后乘上重复惩罚重新归一化。对七言绝句这种短文本temperature0.9、top_p0.9、repetition_penalty1.15是我常用的组合既保留了意象跳跃的空间又不会出现大段回环。用 transformers 的generate方法参数直接传入inputs tokenizer(春风, return_tensorspt) output model.generate( inputs.input_ids, max_new_tokens28, # 四句七言共 28 字 do_sampleTrue, temperature0.9, top_p0.9, repetition_penalty1.15, num_return_sequences16, # 一次生成多首再筛选 )num_return_sequences16是实践中的关键技巧与其调一轮就期望拿到好诗不如一次生成 16 份候选再用规则做后筛选。这不是偷懒而是基于「解码是随机的规则是确定的」这一分工逻辑。4.3 约束解码用 logit 掩码强制韵脚微调模型虽然比马尔可夫链更懂格律但也不能保证句末落韵。想保证 100% 押韵需要在解码时干预概率分布这就是约束解码。方法是在采样前把 logits 中不合法的词位改成负无穷让它们不可能被选中。def mask_logits_for_rhyme(logits, tokenizer, target_group): # 属于 target_group 的 token id 集合由韵部映射表预先计算 ok_ids tokens_in_rhyme_group(target_group, tokenizer) mask torch.full_like(logits, float(-inf)) mask[:, ok_ids] 0 return logits mask这个函数在生成句子最后一个字时调用先算出模型对下一个字的原始 logits再把不属于目标韵部的 token 全部掩掉最后做 softmax 采样。因为掩码是加性的不会破坏模型本身的概率分布形状只会把非法选项清零。约束解码的代价是速度每生成一句都要额外查询一次韵部映射批次内还要逐句处理。对自动写诗这种低吞吐场景完全可接受真正要注意的是「掩码集合不能为空」——若目标韵部在词表中找不到任何 token整句生成会报错。提示掩码集合为空会让整句生成直接报错上线前务必对目标韵部做一次词表覆盖率检查并加一层「空集合回退到普通采样」的兜底逻辑。5. 韵律校验与可用性打磨让自动写诗从「能生成」到「能交付」5.1 三条硬指标验收一批生成结果批量生成只能证明「能跑」能不能交付要看指标。我给自动写诗工具配置过三条自动验收规则押韵率、字数合规率、句内重复率。押韵率统计所有句末字中落在预期韵部的比例低于 50% 说明解码端的韵律约束没生效字数合规率检查每句字符数是否等于设定值七言诗中单句多一字少一字都算不合格句内重复率计算单句内重复字符占比超过 30% 直接判废。三条规则挨个过滤一轮16 首候选通常能剩下 5 首左右可进入人工挑选。5.2 三个高频翻车点与对应排查最常见的三个问题按排查优先级排列第一生成不押韵优先检查韵部映射表是否过严宽韵表里ian和an归并后命中率会明显上升第二车轱辘话反复出现检查repetition_penalty低于 1.0 时模型倾向于重复已出现的 token建议设在 1.1 到 1.2 之间第三字数忽长忽短既看max_new_tokens是否给足也看语料里有没有混入过长残句——数据清洗时遗漏的一句残句会在解码时被放大成十几处格式错误。5.3 收尾技巧打包成带参数的命令行工具最后一步把流程固化。我用 argparse 封装一个入口脚本让使用者不需要理解内部算法细节--theme指定主题字开头--rhyme指定韵部--style切换马尔可夫链或模型推理--lines控制句数。打包成 .rar 分发时保持「一个入口脚本、一个语料目录、一个 README」的结构运行时直接执行python write_poem.py --theme 春 --rhyme an --style markov输出四句诗后自动附带一份校验报告标出每句的押韵状态和字数状态。运行完一轮生成先把结果丢进 5.1 的校验脚本过了线的再进入人工挑选环节这一步能省下大量逐首检查的时间。本文还有配套的精品资源点击获取