news 2026/9/13 14:37:52

自动写诗与文本生成:从字符级LSTM到押韵平仄约束的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自动写诗与文本生成:从字符级LSTM到押韵平仄约束的工程实践

简介:自动写诗项目完整资源包,面向自然语言处理初学者与AI诗歌创作研究者,提供从诗歌语料准备、数据清洗、模型设计到训练生成与效果评估的闭环实现。包内共18个文件,总大小23.83MB,主要包含Python源码及编译缓存(.py/.pyc)、实验指导书与实验报告(.doc)、演示PPT、诗歌数据集(.txt/.npz)、训练日志和模型保存文件等;其中多个Python模块分别负责模型结构定义、工具函数、日志记录与训练流程,配合指导文档可快速跑通基于RNN、LSTM或Transformer的自动写诗模型。已有169人学习下载,适合用于课程实验、毕业设计或技术探索。通过源码可学习诗歌语料预处理、超参数调整、模型迭代及BLEU/ROUGE等指标评估方法;实验报告详细记录了生成实例与问题排查过程,指导书补充了完整实验流程,PPT则清晰展示实验目的、架构设计与关键结果,便于汇报与复盘。

1. 自动写诗.rar:这个压缩包背后是一整套文本生成工程

网上流传的“自动写诗.rar”,解压后通常只有几个文件:训练脚本、字符表、诗歌语料文本。不要被“自动写诗”四个字带偏,它本质上是一个字符级文本生成任务,压缩包里的代码只是整条流水线的最小切片。顺着这个切片往回推,你需要依次解决的问题包括:技术路线怎么选、语料怎么清洗、模型怎么训练、押韵和平仄这类硬约束怎么注入、最后一步生成结果怎么评估。这篇文章按这个顺序把每一环拆开讲,所有代码在普通笔记本上都能跑通。适合刚开始接触文本生成的工程师,也适合手头有通用语言模型但没想清楚怎么落成具体应用的开发者。读完你自然明白,这个压缩包打开之后该看哪里,以及自己动手时精度最该花在哪个环节。

2. 自动写诗的三条技术路线:规则模板、统计模型与深度学习

2.1 规则模板:用字典和字符串拼接撑起藏头诗

早期写诗程序,说穿了是“填空题”。程序维护几组词库——名词库、动词库、意象库,再按五言或七言的句式模板把词填进槽位。藏头诗尤其好做:输入“李白”,第一行首字固定写“李”,第二行首字固定写“白”,剩余位置从句法模板里按词性抽词,句尾再检查押韵。整个过程不涉及任何概率模型,快且稳定,几百行代码就能出一个可运行的工具。这类实现里没有一个字是“想出来”的,全是“拼出来”的。

模板方案的核心价值是可控。格律是硬规则,模板天然满足;韵脚可以用词库直接锁死;藏头字作为前缀几乎不可能写错。代价也很明显:四句话经常各说各话,前一句写到边塞,后一句就飘回闺怨,原因是模板本身不携带主题记忆。但把模板方案直接扔掉也不对,在深度学习方案里,这套逻辑被保留下来当“校验器”——生成后检查字数、逐字查平仄、核对句数,这部分能力到今天依然是必需的。所以规则模板在自动写诗里的定位不是替代者,而是兜底层。

2.2 统计语言模型:n-gram 让“接话”成为可能

n-gram 的思路是把诗看成字符或词的序列,统计转移概率。二元模型只关心 P(w_i | w_{i-1}),三元模型多看一步,这种条件概率可以从几十万行古诗里统计出来,模型文件只有几兆,CPU 上百毫秒就能滚动生成。生成时给定开头两个字,按条件概率抽样下一个字,再把新字接在末尾继续走,理论上可以无限写下去。相比模板,这类输出的句子明显更“顺”,因为概率分布来自真实语料,字与字之间的搭配不是硬凑的。

from collections import defaultdict import random trans = defaultdict(lambda: defaultdict(int)) for poem in poems: chars = list(poem) for i in range(len(chars) - 2): trans[chars[i], chars[i + 1]][chars[i + 2]] += 1 def sample_next(prefix, temperature=0.8): counts = trans[prefix] if not counts: return random.choice(list(all_chars)) total = sum(counts.values()) probs = {c: cnt / total for c, cnt in counts.items()} return random.choices(list(probs.keys()), weights=list(probs.values()))[0]

参数说明:temperature在这里通过对概率做幂运算实现,取对数后等价于给频次加温度指数。抽样用random.choices而不是取最大概率,是为了保持“能写出意外搭配”的探索性。代码里的trans字典是稀疏的,语料里没出现过的二元组合会直接落到空分支,这就是 n-gram 的硬伤。

用几轮你就能撞上两个问题。第一,语料再大也有未出现的二元组合,平滑技术能缓解,但诗里大量“看似突兀却出彩”的搭配会被概率统计系统性地压掉。第二,n-gram 只看前面两步,五言诗第一句和第四句之间几乎无法形成主题呼应,让模型从“月”开头写,十次有八次写到一半忘了自己写的是月。因此统计语言模型在今天很少单独承担自动写诗任务,它的合理位置是做候选重排——深度模型出候选字后,用 n-gram 打分微调一下次序,成本极低收益却稳定。

2.3 深度学习路线:从字符级 LSTM 到预训练模型的演进

深度模型把“接话”升级成“记忆”。字符级 LSTM 每读入一个字就更新一个隐状态向量,这个向量相当于前面全部内容的有损摘要,决定下一个字符怎么选。训练目标就是预测下一个字符:“床前明月”后面是“光”的概率要远高于“山”。2015 年前后兴起的自动写诗开源项目,绝大多数是这个结构。字表通常三千出头,CPU 上几小时能训完,这也是它至今仍是入门首选的原因。

到了 Transformer 时代,底座经历了两次切换:第一次是从循环结构换到自注意力,解决长程依赖;第二次是预训练加微调范式出现,模型先在通用语料上学会字词规律,再到古诗语料上小步微调。自动写诗的工程骨架却没有变,仍然是“字符级模型加采样参数加约束后处理”,换的只是中间那层网络。选型建议很直接:没有 GPU 就用 LSTM 跑通全链路,有 GPU 直接微调预训练模型,但后处理必须保留,因为押韵和平仄这类规则约束,模型学得再久也做不到 100% 正确。

路线数据需求硬件门槛可控性产出质量
规则模板词库,几百词够用最高句式僵硬,主题空洞
n-gram数万行文本局部通顺,前后不呼应
字符级 LSTM几万到几十万行CPU 可训低,靠采样参数调整体连贯,常识弱
预训练模型微调通用语料加诗歌语料GPU 更合适中,依赖 prompt 设计质量上限最高

选定路线之后,问题就落到非常实际的地方:语料怎么组织、代码怎么写、采样参数怎么调。下一章给出一个不依赖 GPU 也能完整跑通的最小实现,从数据清洗一路写到生成。

3. 用 Python 实现自动写诗的最小可行方案

3.1 数据预处理:把全唐诗切成字符级训练样本

语料是第一步。从公开渠道拿到的古诗文本通常是杂乱的,行间混着作者名、诗题、注解,标点也不统一。清洗时我习惯按行读入,用正则跳过标题行,只保留长度在五言或七言范围内的纯诗句,再把逗号句号统一成句读。每首诗的开头加<s>、末尾加</s>,这两个特殊标记让模型在训练时知道诗歌边界在哪,生成时也作为起始输入使用。

字符级优于词级的理由有两条。其一,古诗生僻字密度高,大量单字本身成词,按词建模词表会膨胀到几万维,训练变慢且容易过拟合;其二,字符级字表通常三千出头,模型体积小,CPU 上迭代快。以下代码构造训练语料和映射表:

import re from collections import Counter raw_text = open("poems.txt", encoding="utf-8").read() lines = [ln.strip() for ln in raw_text.splitlines() if ln.strip()] poems = [] for ln in lines: if re.match(r"^《.+》$", ln): continue body = re.sub(r"[,。!?;]", ",", ln) body = body.replace(",", "。", 1) clean = body.replace(",", "") if len(clean) in (20, 28): poems.append("<s>" + body + "</s>") text = "\n".join(poems) counter = Counter(text) char2id = {c: i for i, (c, _) in enumerate(counter.most_common())} id2char = {i: c for c, i in char2id.items()}

逻辑说明:先跳过带书名号的标题行,统一标点后replace(",", "。", 1)把第一处逗号替换成句号,四句诗就变成“句。句。句。句”的格式。len(clean) in (20, 28)分别对应五言四句和七言四句,能顺势过滤掉残缺行和词牌。Counter.most_common()按出现频率建表,高频字排在前面,生成时更容易被采到。这段清洗逻辑隐含一个前提:原始文件是每行一首诗且不含作者名,如果你的语料格式不一样,第一步应该是先剥作者行和诗题行,再做长度过滤。

3.2 字符级 LSTM 的训练代码与参数设置

模型结构采用“嵌入层、双层 LSTM、全连接输出层”的标准骨架。输入是形状为 (batch, seq_len) 的字符 id 序列,输出是每个位置下一个字符的 logits。序列长度取 32,短于五言四句加标点后的整篇长度,但已足够覆盖跨句依赖;过长了 CPU 收敛很慢,不适合快速迭代。

import torch import torch.nn as nn class CharLM(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, x, hidden=None): embs = self.embedding(x) out, hidden = self.lstm(embs, hidden) return self.fc(out), hidden

embed_dim=128对三千字表足够,hidden_dim=256是 CPU 训练在容量和速度之间的平衡点,双层 LSTM 比单层更擅长捕捉句间关系。训练循环里的关键不在模型结构,而在数据切法和梯度裁剪:

import random def make_batches(text, char2id, seq_len=32, batch_size=64): ids = [char2id[c] for c in text] max_start = len(ids) - seq_len - 1 starts = [random.randint(0, max_start) for _ in range(1000)] for i in range(0, len(starts), batch_size): batch_starts = starts[i:i + batch_size] x = torch.stack([torch.tensor(ids[s:s + seq_len]) for s in batch_starts]) y = torch.stack([torch.tensor(ids[s + 1:s + seq_len + 1]) for s in batch_starts]) yield x, y criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=3e-3) for epoch in range(20): for x, y in make_batches(text, char2id): logits, _ = model(x) loss = criterion(logits.view(-1, logits.size(-1)), y.view(-1)) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step()

数据切法上,random.randint随机选起点,而不是顺序滑窗,这样每首诗的开头会均匀出现在不同 batch 里,模型不会过度学习“上一首结尾接下一首开头”的伪模式。梯度裁剪参数1.0指的是梯度 L2 范数上限,LSTM 在长序列上极容易梯度爆炸,不裁剪时损失会猛然跳到几十再恢复,训练曲线剧烈振荡。lr=3e-3配 Adam 和 256 维隐层是常用起点,如果 loss 在某个值附近震荡不降,把学习率降到 1e-3 重训。

3.3 生成时用 temperature 与 top-k 控制发散程度

训练完成的模型只是掌握了一组概率分布,真正决定产出物质量的是采样策略。三个参数要配合调:temperature 控制分布的尖锐程度,top-k 限制候选字池大小,top-p 按累计概率动态截断。低 temperature 让模型只选高概率字,输出稳定但容易重复;高 temperature 增加探索性,偶尔出彩但更容易跑偏。

def generate(model, start_text, char2id, id2char, max_len=32, temperature=0.8, top_k=50, top_p=0.9, seed=None): if seed is not None: torch.manual_seed(seed) model.eval() chars = list(start_text) input_seq = torch.tensor([[char2id[c] for c in chars]]) hidden = None with torch.no_grad(): for _ in range(max_len): logits, hidden = model(input_seq, hidden) logits = logits[:, -1, :] / temperature probs = torch.softmax(logits, dim=-1) if top_k is not None: top_probs, top_ids = torch.topk(probs, top_k) probs = torch.zeros_like(probs).scatter_(-1, top_ids, top_probs) if top_p is not None: sorted_probs, sorted_ids = torch.sort(probs, descending=True) cumsum = torch.cumsum(sorted_probs, dim=-1) keep = cumsum - sorted_probs <= top_p sorted_probs[~keep] = 0.0 probs = torch.zeros_like(probs).scatter_(-1, sorted_ids, sorted_probs) probs = probs / probs.sum(dim=-1, keepdim=True) next_id = torch.multinomial(probs, 1) next_char = id2char[next_id.item()] chars.append(next_char) input_seq = torch.tensor([[next_id.item()]]) if next_char in "。;": break return "".join(chars)

这段代码最容易出错的是 top-p 过滤后的归一化。cumsum - sorted_probs <= top_p标记的是累计概率还没超过阈值的候选位置,过滤后概率和小于 1,必须重归一化,否则 multinomial 的行为会偏向排序靠前的字。另一个细节是每次把整个历史序列重新送入模型,让 LSTM 的隐状态持续接收前文信息;加速写法是保留 hidden 只输入新字符,两种写法结果等价。temperature 范围的经验值如下:

参数区间表现
temperature0.5~0.7保守,常见字多,适合七言长句
temperature0.8~1.0平衡,偶尔出彩,适合五言
temperature>1.1随机性强,十首里能出两三首有趣的
top-k20~50候选少,更稳,五言常用
top-k100~200探索性强,配高 temperature 使用
top-p0.8~0.95动态截断,比 top-k 更平滑

调参习惯是先固定 top_k=50,再调 temperature,最后用 top_p 收尾。top-k 是硬截断,容易把低概率但有诗意的字一刀切掉;top-p 会根据实际分布自动缩放候选集合,对风格变化更宽容,所以最终成型时多半会让 top_p 生效而把 top_k 调大或关掉。

注意:top_p 和 top_k 同时启用时,先做 top-k 再做 top-p,顺序反过来会让 top-p 失去动态截断的意义。

4. 让自动写诗更像“诗”:押韵、平仄与风格控制的三个参数细节

4.1 押韵约束:维护韵脚表,生成时对 logits 打掩码

深度模型能学会字词搭配,但学不会“第一、二、四句句尾必须同韵”这种韵书规则。解决思路是强制后处理:准备一张韵部映射表,把常用汉字分组,生成到句尾位置时只允许从目标韵部挑选。目标是“算出来的韵”而不是“学出来的韵”,因为语料里押韵规律不是绝对标准,模型统计出的押韵概率只能做到六七成,上不了生产标准。

韵部表有两种来源。严格的《平水韵》有 106 个韵部,颗粒太细,很多生僻韵部在自动生成时根本用不上;更接地气的是按普通话韵母分组,比如 ang、iang、uang 归一个大韵部,用起来简单,代价是部分古音相押会被漏掉。工程上先按普通话韵母跑通,再逐步补古音特例。过滤函数如下:

end_positions = {4, 9, 14, 19} def rhyme_mask(logits, current_pos, rhyme_char_ids): if current_pos not in end_positions: return logits mask = torch.full_like(logits, -1e9) mask[rhyme_char_ids] = 0 return logits + mask

逻辑说明:end_positions是五言四句每句句尾的字符下标,rhyme_char_ids是当前韵部内所有汉字对应的 id 列表。当前位置不是句尾时不干预;是句尾时把非韵部的 logits 压成 -1e9,softmax 之后这些字的概率趋近于零。掩码值不能用 0 或者 -10,logits 的尺度通常在 -5 到 10 之间,只有 -1e9 这种量级才能做到“硬过滤”。rhyme_char_ids要覆盖多音字的多个韵部,否则一个常用字因标错韵部会导致整首生成失败。

提示:如果押韵约束不是强制,而是希望保留一点意外,把掩码值从 -1e9 改成 -10,模型仍有小概率跳出韵部,适合做“半押韵”风格。

4.2 平仄校验:按声调库逐字检查格式

押韵约束解决句尾,平仄决定句子读起来的音乐性。近体诗的平仄是硬模板,五言四句的标准格式为“仄仄平平仄,平平仄仄平。平平平仄仄,仄仄仄平平”。逐字判断平仄用声调字典即可:普通话一二声视为平,三四声视为仄,再补一张常用入声字表做修订,因为部分入声字在普通话里读平声但按古音属仄。生僻字查不到时按平声处理,并打印日志方便后续补录。

def validate_pingze(line, tone_map, pattern, tolerated=0): if len(line) != len(pattern): return False errors = 0 for ch, p in zip(line, pattern): tone = tone_map.get(ch, 1) if (p == "平" and tone != 1) or (p == "仄" and tone != 2): errors += 1 return errors <= tolerated

tolerated是工程上很关键的参数。严格模式设 0,任何一个字不合模板就判定失败;宽松模式设 2,允许两个位置灵活处理,这对应律诗里的“一三五不论”经验。校验时机放在整首生成之后,不合格直接重生成,最多重试十次。这样做牺牲少量速度,换格式正确性;如果每生成一个字都即时校验,速度更快,但候选空间被过度压缩,句子会变得异常平庸。两种写法的取舍,本质上是你优先要格式完美,还是优先要语言灵动。普通话声调判断平仄是近似方案,真要严格按《平水韵》来,还得维护一份独立的入声字表,不能照搬拼音声调。

4.3 风格控制:用风格标记和领域词表应对“不像诗”的问题

默认训练出来的模型输出的是语料的平均风格。唐诗全集的“平均”,既不是边塞也不是田园,而是面目模糊。常见的做法是在每个训练样本前加风格标记,比如<边塞><田园>,生成时也以这个标记开头;模型把标记当作普通字符,通过序列建模让后续生成整体偏向该风格。这种做法改动最小,但要求语料有风格标签,没有标签时也可以用意象词聚类自动打标。

领域词表加权是更细的控制手段。统计边塞诗里“月”“剑”“城”等词的出现频率,生成时把这些词的 logits 加上一个正值。幅度控制有讲究:加 0.2 只产生微弱偏向,加到 0.5 能让词的出现频率明显上升,超过 1.0 会让同一首诗中反复出现该词。这个参数与 temperature 耦合明显——temperature 越低,logits 加权的效果越显著,因为低 temperature 下模型本来就倾向高概率字,一点点偏向都会被放大。

控制手段作用层面推荐设置副作用
风格标记输入序列每首加 1 个 token语料需要标注
领域词加权logits 相加0.4~0.8幅度过高会重复
句尾押韵掩码logits 掩码-1e9 硬过滤候选字骤减
平仄回退输出后校验容忍 0~2 字重生成增多

多个约束的执行顺序经常被忽略。同时使用押韵掩码和领域词加权时,先做掩码再做加权能让两者都生效;先加权后掩码,领域词的提升可能因为韵部限制被压掉。风格标记作用于输入序列,与所有 logits 级操作天然兼容,可以理解为最先执行的一层。实际调参顺序建议:固定风格标记、调 temperature、加押韵约束、最后用领域词加权做微调。顺序反过来的话,一旦输出跑偏,你很难判断是哪个环节引入的问题。

5. 验证自动写诗质量:从困惑度到人工评审的可用指标

5.1 用困惑度判断模型是否收敛

训练过程中,验证集困惑度是第一个要盯的门禁指标。公式就是exp(loss),直观含义是“下一个字符的平均候选数”。字符级模型下,困惑度 150 意味着模型平均在 150 个字里选择,接近乱猜;训练到 50 左右说明字词搭配已经比较确定;低于 30 时往往不是好事,多半是语料太少导致模型开始背句子,泛化反而变差。困惑度只回答“模型学得如何”,不回答“诗写得如何”,但它能判断该不该加训练轮数、该不该降学习率。训练日志里直接记录困惑度而不是裸 loss,会让收敛判断直观得多。

5.2 押韵率与格律合规率的自动化统计

诗写得好不好是主观的,押韵、平仄、字数是否合规却可以客观统计。每次改动算法后批量生成 200 首,统计三个数:句尾押韵率、平仄合规率、长度分布标准差。押韵率低于 30% 说明跟随机差不多,加上韵部掩码后应当超过 85%,这个跳变是约束代码生效的最直接证据。平仄合规率受声调库质量影响,严格模式下 50% 算正常,因为普通话声调跟古音有出入;放宽到允许 2 处出律后应达到 80% 以上。把这几个统计写进--eval子命令,生成完自动打印,比每次人工翻十首判断可靠得多。

5.3 人工评审表的设计与打分口径

自动指标再全,也覆盖不了“这首诗读完有没有画面感”,所以人工评审不能省。评审表不建议超过四个维度:主题相关性、意象统一性、格律规范性、语言新颖度,每项 1 到 5 分。每首诗至少两个人独立打分,同一首诗分差超过 1 分的要讨论归因。长期迭代时,绝对分数会随评审者状态漂移,更稳的做法是两首诗放一起强制二选一,用排位而不是孤立分数。最后落一个具体技巧:给 generate 函数加 seed 参数,并把 seed、temperature、top_k 一起写进生成日志。

gen_kwargs = {"seed": seed, "temperature": temperature, "top_k": top_k} json.dump(gen_kwargs, open("generation_log.json", "a"))

固定 seed 后,改押韵约束前后的两组输出可以在完全相同的采样轨迹上对比,任何质量差异都能归因到代码改动,而不是采样噪声;日常调试固定seed=42,同一首诗的生成结果在不同机器上完全一致,自动回归测试才真正有可重复性。这也是“自动写诗”这类生成程序从玩具走向可用工具时,最容易被漏掉却最值得先做的一步。

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

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

Steam 2024春季更新真相:客户端、Frame、18+验证与成就预加载解析

1. 项目概述&#xff1a;一次被误读为“系统性变革”的Steam常规运营节奏 最近几天&#xff0c;不少玩家在社区刷到类似“Steam突然迎来四大事件”的标题&#xff0c;点进去发现内容零散、信息混杂&#xff0c;有的说Frame预约曝光是重大技术升级&#xff0c;有的把18验证流程调…

作者头像 李华
网站建设 2026/9/13 14:34:46

Gymnasium + MuJoCo 连续控制环境从零跑通与避坑指南

Gymnasium MuJoCo 连续控制环境从零跑通与避坑指南 【免费下载链接】Gymnasium A standard API for single-agent reinforcement learning environments, with popular reference environments and related utilities (formerly Gym) 项目地址: https://gitcode.com/GitHub_…

作者头像 李华
网站建设 2026/9/13 14:34:37

PLC新手入门实战指南:从梯形图到工业现场故障排查

1. 这不是“学PLC”而是“进工厂”的入场券&#xff1a;一个干了12年自动化现场的老工程师的真心话“PLC培训&#xff0c;新手最关心的7个问题”——这句话我每天在招聘群、技术论坛、甚至车间休息室里听不下十遍。不是学员问&#xff0c;就是HR转述&#xff0c;再不就是产线班…

作者头像 李华
网站建设 2026/9/13 14:34:16

P值与α的关系:显著性检验的底层逻辑与实操避坑指南

别急着从教科书目录开始啃。我在实际做数据分析这几年&#xff0c;发现“显著性检验”几乎是每个入门者绕不过去的第一道坎&#xff0c;而其中最让人头疼的&#xff0c;就是P值和α这两个符号。大家最常问的一句话是&#xff1a;“P值到底要小于多少才算显著&#xff1f;为什么…

作者头像 李华