news 2026/10/13 23:34:37

基于RNN的古体诗生成器实战:从数据清洗到模型调参

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于RNN的古体诗生成器实战:从数据清洗到模型调参

1. 从一句"诗"开始:这个生成器到底解决什么问题

1.1 很多人都问我,为什么不用更"潮"的模型

最近朋友圈里又刮起了一股"AI写诗"的风,各种号称大模型的平台随便输入两句话就能生成一首律诗。但说实话,这类工具跑出来的东西,要么是辞藻堆砌的空壳,要么平仄完全不对,念起来像被人掐着脖子。我决定自己动手做一个古体诗生成器,用的还是被很多人说"过时"的RNN,框架是TensorFlow 2.0。

有人可能会问:现在Transformer这么流行,为什么不用它?我的想法很简单——古体诗生成是一个序列到序列的字符级任务,数据量小、词汇表窄、依赖长距离上下文。RNN里的LSTM恰恰擅长捕捉这种几十个字范围内的时序关系,而且训练成本低,普通笔记本GPU就能跑。这个项目不是为了赶时髦,而是为了把深度学习中"序列建模"这件事掰开揉碎讲清楚。

1.2 生成器的核心玩法:字对字预测

所谓古体诗生成器,本质就是一个"字符级语言模型":给定前面的若干个字,预测下一个字是什么。比如模型看到"床前明月",就要尽量预测出"光"。等到它能连续预测几百步,你就可以自己给一句开头,让模型把后面的诗"接"下去。

这个目标听起来简单,但里面藏着的难点一点都不少:古体诗讲究字对、押韵、意象连贯,而且五言、七言的句子长度固定。要让模型学会这些规律,一个关键前提是——数据够干净,模型结构够简单,训练过程够温柔。我写这篇文章,就是想把我从数据清洗到模型训练再到生成策略的全过程记录下来,尤其那些踩过的坑,能给想动手练手的人省下几个周末。

1.3 这篇文到底适合谁

如果你是深度学习的初学者,刚学完Python和Keras的基本操作,这篇能带你跑通一个完整的文本生成项目。如果你已经会训练图像分类模型,但对RNN、序列数据的处理比较陌生,这篇能帮你建立起"序列建模"的直观认识。就算你只是对AI写诗感兴趣,也可以直接跳到后半部分,看看生成效果和调参技巧。

我写完整个项目大概花了两周业余时间,模型训练总时长不到半天,成本几乎可以忽略。所以别担心门槛,跟着思路走,你也能搞出一个能写诗的小模型。

2. 语料清洗的坑比模型还多:公开诗词库如何变成训练集

2.1 数据来源与最低要求

做文本生成,最怕的是"垃圾进、垃圾出"。我一开始从网上找了一份公开的古诗词语料库,里面杂七杂八收了几十万首,看起来量大管饱,但直接丢进模型训练,出来的东西基本是灾难。

要先想清楚我们需要什么样的诗词:第一,必须严格限制在古诗的范畴,词、曲、现代打油诗都先放一边,因为格律完全不一样;第二,最好以五言和七言为主,四言、六言、杂言诗数量少且格式特殊,容易干扰模型学习固定节奏;第三,诗句总长度要适中,一首五言绝句20个字,七言律诗56个字,超过60个字的可以暂时排除。

我最后保留下来大约8万首比较规整的古体诗,折算成字符数大概是500万到600万字。这个规模对字符级RNN来说不算大,但恰好够用——它既能保证模型见过足够多的字词搭配,又不会因为数据太庞杂导致训练时间失控。

2.2 清洗规则的具体细节

清洗这一步我反复折腾了很久,列出几条最重要的规则:

第一,去重。公开语料里同一首诗可能被收录了三四次,格式略有差异。我用正文的MD5做粗去重,再把正文中所有非中文字符删除后做一次细去重。这个操作直接让数据量缩掉了将近四成。

第二,过滤异体字和生僻字。古体诗里经常会用到已经很少出现的字,比如"閒""峯"之类。直接删掉吧,多少有点可惜;保留吧,模型会被大量低频字符带偏。我的处理方式是:先把所有字符做频率统计,保留出现次数前2500的字,其余统一替换成一个特殊的"UNK"标记。这保证了字表不会太大,也能让模型学会处理生僻字的兜底逻辑。

第三,统一标点。原语料里的标点非常乱,全角半角、中文英文混用。我干脆把所有标点删除,只保留汉字,一首诗用换行符分隔,诗句和诗句之间用空格占位符模拟停顿。你可能觉得删掉标点会影响模型对断句的学习,但经过实验,字符级模型完全可以自己学会在完整句子的边界处输出换行,不会因为少了逗号句号就胡来。

2.3 字表设计与向量化的隐性约束

字表是整个项目的"语言字典"。我用一个简单的collections.Counter统计所有字符的出现次数,然后按频率从高到低排列,生成一个char2id和id2char的映射表。

这里有一个容易被忽略的问题:字表里要不要加入空格和换行?我的答案是必须加。空格代表一句之内的停顿,换行代表句子的结束,这两个字符在生成阶段至关重要。如果字表里没有换行标记,模型就不知道怎么结束一首诗,只会一路输出到底,没法形成"五言四句"或"七言八句"的结构。

字表确定后,每一首诗就变成了一个整数序列。比如"床前明月光"会变成类似[231, 15, 47, 89, 212]的ID序列。模型输入一个定长的窗口,输出下一个字符的ID。

2.4 序列切分的滑动窗口细节

把整首诗变成训练样本,最常用的方式是滑动窗口切分。我设定sequence_length = 32,也就是说,用前32个字符去预测第33个字符,然后窗口向后滑动一个字符,再用第2到第33个字符预测第34个字符,依次类推。

一首五言绝句只有20多个字,去掉换行和空格也就30个字符左右,刚好能凑成几个窗口。但这里有个问题:窗口会跨过一首诗的结尾,把下一首诗的开头也拉进来,模型就会学到"两首诗之间可以无缝拼接",生成的文本会出现莫名其妙的"穿越"。

我的解决办法是:诗与诗之间用一个特殊的"<|endoftext|>"分隔符隔开,当滑动窗口碰到这个分隔符时就强制重置,开始新一轮切分。这个细节很多人不做,但实测下来,它对生成文本的"完整性"影响极大。

3. 模型搭建:三层结构如何把文字变成向量再变回文字

3.1 Embedding层:让模型自己找字的语义坐标

模型的第一层是一个Embedding层,它的作用是把离散的字符ID映射成稠密的向量。举例来说,字表大小是3000,Embedding维度设为128,那么每个字符都会变成一个128维的浮点向量。

为什么要这么做?因为字符之间本来没有数值大小关系,"床"是120,"前"是80,不能直接拿120和80做算术。Embedding层相当于让模型自己去学习每一个字在多维空间里的"语义坐标",语义相近的字(比如"山"和"峰")会落在相近的区域,模型学习后续模式就容易很多。

Embedding层还有一个隐性优势:它天然包含了对字频的感知。高频字会被放到相对平滑的向量空间,低频字则会被挤压到边缘地带。在训练过程中,低频字的梯度更新频率低,容易被冻住,这也是为什么我前面坚持用UNK替换过少的生僻字。

3.2 LSTM层:记忆跨过几十个字的上下文

中间层我用了LSTM。LSTM全称是长短期记忆网络,它相比普通RNN最大的改进是引入了一个"细胞状态"通道,像一条传送带贯穿整个序列。每一步都可以选择往传送带上写入新的信息,或者擦除旧的信息。

你可能会好奇,为什么LSTM能记住几十个字之前的"山"而不被中间的字干扰?秘密在于三个门控:遗忘门决定要不要忘掉旧状态,输入门决定新信息写入多少,输出门决定当前时刻的隐藏状态对外输出多少。这三个门都是由当前输入和上一时刻的隐藏状态共同计算得出的。

对于古体诗来说,最长依赖不过是一联内上下句的呼应,比如"白日依山尽,黄河入海流","白日"和"黄河"跨了大约10个字。LSTM处理这种长度的依赖绰绰有余。我选了128个LSTM单元,也就是隐藏状态维度128。这个数字不大,但搭配字表大小3000和Embedding维度128,已经能形成足够强的表达能力。

3.3 Dense输出层与损失函数搭建逻辑

LSTM输出的隐藏状态,需要经过一个全连接层,才能映射回字表大小为3000的分类概率。全连接层的权重矩阵形状是(128, 3000),这一步的计算量比前面几层加一起都大,但现代框架处理起来还是很快的。

输出层之后接一个Softmax,把3000个实数变成3000个概率值,概率最大的字符就是模型预测的下一个字符。损失函数我用的标准交叉熵,目标是把正确字符对应的概率不断拉高。

这里有个值得注意的小细节:我不对LSTM返回的每个时间步都做预测,只取最后一个时间步的输出做预测,因为训练样本就是"前32个字预测第33个字",模型结构要和这个设定完全对齐。如果你想一次性预测多个后续字符,就该改用return_sequences=True,但那个需求在这个项目里没有必要。

3.4 参数量与内存的粗略盘算

搭建模型之前,我很粗略地估算过参数量。Embedding层的参数量是3000 * 128 = 384000个,LSTM层的参数量大约是4 * (输入维度 + 隐藏维度) * 隐藏维度,也就是4 * (128 + 128) * 128 = 131072个,再加上偏置项大概3万。全连接层最大,是128 * 3000 = 384000个。全部加起来大概90万个参数。

90万参数对现代深度学习来说算是非常轻量了,一张几年前的独立显卡都能把模型塞进显存且绰绰有余。就算只用CPU训练,每个Epoch大约也就十几分钟,完全在可接受范围内。这也再次说明:小项目不要一上来就堆大模型,先把简单的结构跑通,效果不满意再扩规模。

import tensorflow as tf def build_model(vocab_size, embedding_dim=128, lstm_units=128, sequence_length=32): model = tf.keras.Sequential([ tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=sequence_length), tf.keras.layers.LSTM(lstm_units, return_sequences=False), tf.keras.layers.Dense(vocab_size, activation="softmax") ]) return model model = build_model(vocab_size=3000) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss="sparse_categorical_crossentropy" ) print(model.summary())

4. 训练调参全记录:loss曲线从哪一刻开始"出诗味"

4.1 第一个Epoch通常是胡言乱语

训练刚开始的时候,模型完全是在瞎蒙。我用的优化器是Adam,初始学习率0.001。第一个Epoch结束后,loss才降到5.7左右。你可能对这个数值没什么概念,简单说,如果模型完全随机预测3000个字里的一个,交叉熵会是log(3000) ≈ 8.0,所以5.7意味着模型已经学到了一点规律,但生成效果依旧像坏掉的复读机。

我当时做了一次早期测试,给模型输入"春眠不觉"四个字,它输出的后续是"处处处处处处"。这个结果特别经典——模型发现了"春眠不觉晓"这首诗里"处处"结构高频,但又不知道该怎么变化,就疯狂复制。随着训练推进,loss降到3.0以下时,这种复读现象才会慢慢减少。

4.2 学习率与优化器的拉扯

调参过程中我发现,学习率0.001在前期很合适,loss下降明显;但到后期,大概训练到8个Epoch左右,loss曲线开始出现小幅振荡,训练集上甚至偶尔不降反升。

这时候我做了两个调整。第一,把学习率降到0.0005;第二,给Adam加了一个梯度裁剪,clipnorm=1.0。梯度裁剪对RNN来说特别重要,因为LSTM在长序列上的梯度很容易出现极端大或极端小的值,裁剪一下能防止个别Batch把已经学好的权重彻底冲乱。

我也没有用什么特别复杂的衰减策略,只是每5个Epoch手动把学习率减半。这个粗暴的做法在项目后期非常好用,loss从3.0往下到2.4的过程,全靠这种手动衰减一点点磨出来的。

4.3 Batch Size与序列长度的纠缠

Batch Size我试过64、128和256。在小数据集上,64的效果最好,理由是RNN每个时间步的梯度本身就比较"跳跃",Batch Size太小会导致噪声太大,模型容易在几个高频繁词之间反复横跳;Batch Size太大又会平均掉短句和长句之间的差异,让模型学不到韵律变化。64在训练速度和稳定性之间找到了一个平衡点。

序列长度同样值得反复试验。一开始我用24,模型生成的句子偏短,经常在一句之内就断掉;后来改成32,明显变得更连贯。改成48之后,效果没有继续变好,反而训练时间长了将近一倍,生成的句子偶尔还会忘掉开头吟咏的主题。最终定格在32。

4.4 验证集上看到的"背诵"风险

文本生成模型很容易陷入"背诵"而不是"创作":如果训练充分过头,模型会把训练语料里的原句原封不动地输出。我留出了大约5%的数据作为验证集,每个Epoch结束后观察验证集上的loss。如果训练loss持续下降、验证loss开始回升,就说明模型开始背诵了。

我在第12个Epoch左右观察到了这个现象。解决办法不是提前停止这么简单,而是返回去看生成结果——如果模型生成的固定搭配频繁出现,比如"长江东流去"这种被反复复用的句子,我会在前面的语料清洗阶段就把某些过度重复的模板句直接删掉。数据层面的干预,往往比调参更有效。

Epoch 1/15 - loss: 5.71 - val_loss: 5.02 Epoch 3/15 - loss: 4.15 - val_loss: 3.88 Epoch 6/15 - loss: 3.42 - val_loss: 3.35 Epoch 10/15 - loss: 2.86 - val_loss: 2.98 Epoch 12/15 - loss: 2.61 - val_loss: 2.93

5. 生成诗句的旋钮:温度、Top-k和束搜索如何决定"诗的性格"

5.1 为什么不直接取概率最大的字符

模型训练好之后,下一步是写生成代码。最容易犯的错误是"每次选概率最大的字符"——如果真这么干,生成结果会很快陷入无限循环。原因也很直观:模型在单个字符的位置上并不存在一个绝对最优解,一旦选中某个高频字符,前面的隐藏状态会把这个选择记录进去,下一个位置的最优解又倾向于同样的字符,循环就跑起来了。

我展示一个真实的"贪心生成"输出:"白日依山尽尽尽尽尽..."。这就是典型的一路取最大概率导致钻进死胡同。

为了让生成结果有变化、有意外、也有美感,必须引入随机采样。但随机不是乱来,需要在"可读性"和"多样性"之间找到平衡点。

5.2 温度参数调法:从保守到发疯

温度参数(Temperature)是一种对Softmax输出进行改造的手段。具体做法是把模型输出的logits除以一个温度值,再送入Softmax。

温度越低,概率分布越尖锐,高概率字符更容易被选中,生成结果保守、稳定、重复率高;温度越高,概率分布越平坦,低概率字符也有更多机会被选中,生成结果多样、跳跃、但容易语法崩坏。

我实际做了一批温度=0.4、0.7、1.0的对比生成。温度0.4的时候,每句都结构工整,但内容空洞;温度0.7的时候,偶尔出现让人眼前一亮的句子;温度1.0的时候,会出现大量生造的词组,比如"月落星稀人独立"还能看,"风急天高猿啸哀"就变成了"风急天高花满楼"这种让人哭笑不得的搭配。

最终我把温度设为0.75,既保留了足够的随机性,又不至于让句子散架。

def sample_with_temperature(logits, temperature=0.75): logits = logits / temperature probs = tf.nn.softmax(logits).numpy() return np.random.choice(len(probs), p=probs)

5.3 Top-k与Top-p:如何压制叠字怪癖

温度参数能控制"胆量",但控制不了"跑题倾向"。模型特别喜欢在句首输出"春风""明月""青山"这类高频起兴词,连续几句都以相同意象开头,读起来就很单调。

我的解决方案是Top-k过滤:每一步只从概率最大的前50个字符里采样,把那些极低概率的干扰项直接排除在外。Top-k的作用相当于给模型划定一个"候选区",让采样不至于从3000个字的全局范围里乱抽。Top-p同理,也叫核采样,它从累积概率达到0.9的最小字符集合里采样,这个方案的自适应性更强。

实测下来,Top-k=50配合温度0.75效果最稳定。选50而不是20的原因是,古体诗用字丰富,候选区太小会让句子过早撞上常见模板;选50而不是100,则能挡住大量生僻字和噪声字符。

5.4 束搜索:让生成结果稳而不呆

如果想让模型自动续写成完整的四句,一个更高级的做法是束搜索(Beam Search)。它每一步保留概率最高的多个候选序列,最后从中选整体概率最优的,而不是一步一决策。

但直接套用束搜索在诗歌生成上有个副作用:它倾向于选择整体概率最高的路径,而这个路径往往是语义最"安全"的路径,结果是重复和套路化。所以我的建议是:如果只看一两句的局部续写,用随机采样更有灵气;如果要整篇生成,可以在束搜索的得分函数里加入一个多样性惩罚项,鼓励不同句子之间用词有所差异。

6. 翻车现场与救火方案:六种经典病句及其根源

6.1 重复循环与叠字轰炸

最常遇到的病句就是"山山山山"或者"花开花落花开花"。这类问题通常出在采样策略太贪心或温度太低。调整温度到0.7以上,配合Top-k截断,能解决大半。

如果还是循环,那么问题可能出在训练数据——某个高频词组在语料里大量出现,模型对这个模式产生了过拟合。我会在清洗阶段统计最高频的五十个三字组合,把语料里包含它们的句子部分删除,让模型不再死盯着这些搭配。

6.2 主题漂移:上一句还在咏雪,下一句开始写雨

模型经常出现"前半句写冬天、后半句写夏天"的情况。这跟LSTM的长距离遗忘有关,也可能跟训练数据中很多诗篇本身包含了四季交错的意象有关。

我的处理方法是:延长输入上下文,把序列长度从24提升到32,让模型在生成后面几个字时仍"记得"开头的意象。同时,可以在生成时给模型一个更明确的开头提示,比如输入"雪落群山"而不是"雪"一个字,这样模型等于被锁定在一个主题范围内续写。

6.3 平仄完全不搭

严格分类的话,平仄是古体诗最核心的格律之一。但字符级模型从字面上很难自动学会平仄的交替规律,因为训练数据里没有直接标注平仄信息。

我做过一个折中方案:把每个字的平仄编码成单独的输入特征,和字符ID一起喂给模型。具体做法是在Embedding之后把平仄特征向量拼接上去,让LSTM能同时看到"这个字"和"这个字是平声还是仄声"两组信息。这个改动能让生成句子的平仄贴合度明显提升,但代价是模型结构稍微复杂一点,训练时间也变长。

6.4 乱用换行与断句

如果不做换行标记的处理,模型经常一口气生成六七个分句,完全不知道在哪里收尾。我在序列切分阶段用<|endoftext|>分隔诗与诗,同时保留换行符作为字符,模型才有机会学会"每五或七个字输出一个换行"的规则。

但即使这样,偶尔还是会出现"七言变成六言再接一个空格再继续"的怪句子。这是因为模型把空格当成了一种弱停顿符,这在语义上是可以接受的;如果你严格要求字数工整,后处理阶段可以写一个脚本,按空格和换行切分,手动修正不齐的断句。

6.5 意象单薄,全是高频词

模型特别喜欢"春风""明月""天涯""故人"这些高频意象,导致生成的诗千篇一律。要解决这个,除了调整采样参数,还可以从语料层面刻意增加意象的多样性——比如在数据清洗时对那些"高频起兴词+固定场景词"的高度模板化句子多删一些。

我在清洗时加了一个简单策略:统计所有五言句的前二字组合,如果某个组合出现次数超过全体的0.5%,就把这些句子按比例剔除。这样可以强制模型去探索更丰富的起兴方式。

6.6 预训练思路不能解决问题的部分"

网上有人尝试用大模型预训练,再在小规模古诗语料上微调,效果确实不错。但我们在本地上没有那么大算力,所以更实际的方案是:先训练一个相对通用的中文语言模型(如果条件允许),再在古诗语料上微调。如果没有这个条件,直接在8万首诗的规模上从零训练RNN,也已经能得到不错的结果。

我个人的体会是,从零训练到一个能看得过去的水平,关键在于"数据去重+可控采样"这两件事,它们对最终结果的影响可以占到八成。模型结构本身反而不是瓶颈。

7. 几次实验后的个人体会与项目扩展方向

7.1 "诗歌感"的涌现点:loss值2.5附近

训练过程中有一个非常奇妙的节点:当验证集loss降到大约2.5时,生成的诗句开始具备一种模糊的"诗歌感"——它可能是病句,但读起来有韵律、有画面。2.5这个数字对应的其实是"平均每个字符有约12个合理的候选",这给了模型足够的多样性,让它不像机器。我建议你在训练时盯住这个水位线,不要一味追求loss越低越好,低过2.0之后,诗句会变得异常保守和套话化,反而不有趣。

7.2 后续还能怎么玩

这个项目的扩展空间比我想象中大得多。最简单的是改成词牌生成——只要把训练语料换成词,模型结构不变,序列长度稍微调到64,就能生成"虞美人""浣溪沙"格式的词。

还可以做藏头诗生成:拿到一个用户指定的开头字,先用这些字约束模型输出的第一个字,再用随机采样接续后面的内容,本质上就是把生成过程的候选集做一次条件过滤。

更有意思的是"跨句debate":让两个分别用不同温度采样的模型轮番续写,一句来自保守模型,一句来自激进模型,生成的文本常常会出现意想不到的意象碰撞。这种玩法已经超出了"技术正确"的范畴,更像是用代码在探索语言的边界。

7.3 写给想复现的人几句大实话

如果你准备照着这个思路自己搞一个,我能给的最实用的建议有三条:第一,清洗数据的时间一定要给够,别急着跑模型;第二,先在小规模数据上跑通完整链路,再上全量数据,省下的调试时间是以小时计的;第三,生成阶段的采样代码要尽早写,因为它能帮你快速判断模型训练方向对不对,不要等到训练全部结束才动手。

古体诗生成器这个项目,技术栈并不高深,但它把自然语言处理中"序列建模、字符表示、随机采样"这些最核心的概念全部串了起来。如果你也能动手跑通,我保证你对RNN的理解会比读十篇教程都扎实。

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

Agent Skills实战指南:从技能拆解到工程化落地与避坑

做Agent开发的人&#xff0c;最近应该都躲不开“agent-skills”这个词。我最早看到这个概念是在某海外大模型团队发布新功能时&#xff0c;当时没太当回事&#xff0c;但后来在实际项目中反复踩坑、不断重构之后才回过味来&#xff1a;这套东西解决的不是某个API好不好用的问题…

作者头像 李华
网站建设 2026/10/13 23:33:44

泰坦尼克生存分析:从数据清洗到可解释建模的完整实战

1. 项目概述&#xff1a;这不是一场关于沉船的怀旧&#xff0c;而是一次用数据重演历史决策的实战“泰坦尼克号生存数据分析及建模”——这八个字背后&#xff0c;藏着一个被反复验证却始终鲜活的数据科学经典入口。它不是教科书里冷冰冰的案例编号&#xff0c;而是我带过的三届…

作者头像 李华
网站建设 2026/10/13 23:33:04

408数据结构能力诊断:5道进阶层真题精解

1. 这不是普通习题集&#xff0c;而是一套数据结构能力诊断工具“考研408数据结构”这六个字背后&#xff0c;藏着一个被无数考生低估的真相&#xff1a;它从来不是考你背了多少定义、默了多少代码&#xff0c;而是考你能不能在3小时内&#xff0c;用链表、栈、队列、树、图这五…

作者头像 李华
网站建设 2026/10/13 23:33:01

CS2更新后Valve界面闪退与Overflow溢出错误修复指南

CS2更新后Valve界面闪退、Overflow溢出错误怎么办&#xff1f;一场大更新后&#xff0c;社区里求助帖的密度比平时翻了好几倍。这次讨论热度最高的组合就是“CS2更新后Valve界面闪退、Overflow溢出错误”&#xff0c;不少玩家在游戏启动阶段就被卡住&#xff0c;连主界面都进不…

作者头像 李华
网站建设 2026/10/13 23:30:01

2026年软件测试趋势:人工智能与机器学习重塑智能测试与模型测试

1. 2026年行业风向&#xff1a;软件测试、人工智能和机器学习正在合流这两年在一线做测试&#xff0c;最大的感受是“软件测试”这个词的边界正在被快速改写。传统的测试岗位要求我们写脚本、搭用例、跑回归&#xff0c;但到了2026年&#xff0c;需求方提的不再是“把这个功能测…

作者头像 李华