简介:面向深度学习与自然语言处理初学者,一套基于Keras框架实现RNN+LSTM古诗自动生成模型的完整代码包,适合想从零搭建序列文本生成任务、并希望复现训练流程的读者。压缩包共包含8个文件、3.86MB,以4个Python脚本为核心,分别承担模型构建、数据工具、配置与主流程,另附语料文本、说明文档、许可证和忽略文件,整体为可直接运行的工程结构。已有601人学习下载,内容具备一定的实践参考价值。代码覆盖了从语料加载与预处理、模型定义与训练,到古诗文本生成与保存的全链路,且作者记录了一些其他教程未讲清的实现细节与排错思路,能帮助读者减少重复踩坑、更快理解RNN与LSTM在文本生成上的应用。 最近整理旧硬盘,翻出一个压箱底的项目:用Keras实现的RNN+LSTM古诗词自动生成模型。当年为了这个demo,前前后后折腾了两个礼拜,踩了不少坑,也积累了不少经验。现在回头看,这个项目依然是理解RNN、LSTM乃至序列生成任务的最佳入门实践之一。借着这个话题,我把完整思路和代码细节重新梳理一遍,给想入门自然语言处理和文本生成的朋友一份可以直接“抄作业”的参考。
1. 整体设计思路:为什么要用RNN+LSTM写古诗
1.1 从文本生成角度理解古诗创作
古诗和日常语言最大的区别在于——它既是语言,又是音乐。五言绝句也好,七言律诗也罢,都有着严格的字数和韵律结构。但在深度学习视角下,写古诗本质上是一个序列生成任务:给定若干个已经写出来的字(比如“床前明月”),预测下一个最合适的字(比如“光”),然后把这个字拼到已有序列后面,继续预测下下个。
这个任务天然适合循环神经网络来处理。RNN的核心理念是带有记忆的模型——在时间步t,隐藏状态h_t不仅由当前的输入x_t决定,还受到上一个时间步的隐藏状态h_{t-1}影响。这个隐藏状态就像写作者脑中“最近写了什么”的短期记忆,让模型在预测下一个字时,能参考已经写出的上下文。
1.2 为什么选Keras而不是纯TensorFlow或PyTorch
项目用了Keras,核心原因就是快。当年Keras作为TensorFlow的高级API,书写模型像搭积木,Sequential接口几行代码就能把Embedding、LSTM、Dense堆叠起来。对于文本生成这种任务,模型结构并不复杂,复杂的反而是数据预处理和采样策略。Keras能让研究者把精力集中在数据清洗和生成逻辑上,而不是和底层算子搏斗。
公平地说,PyTorch在这种动态图任务上也很顺手,但如果你只是想快速验证RNN/LSTM做文本生成的效果,Keras的体验仍然是最丝滑的。尤其是模型调试,在Keras里打印每层输出形状、监控loss变化、保存权重,几乎零成本。
1.3 模型结构设计的取舍
这个项目的模型结构参考了2016年Andrej Karpathy那篇经典博客《The Unreasonable Effectiveness of Recurrent Neural Networks》里用字符级RNN生成文本的思路,并做了两个关键升级:
- 用LSTM替换vanilla RNN,解决长距离依赖问题。
- 用Embedding层替换one-hot编码,降低输入维度,让模型学到的不再是孤立的字,而是字之间的语义相关性。
模型整体只有四层:Embedding层、一个或两个LSTM层、Dense输出层。这个结构放到今天的大模型时代看起来简单得近乎简陋,但对理解序列建模的核心机制来说,它恰到好处——所有应该有的部件都在,但又没有多余的tricks掩盖本质。
2. 数据准备与预处理
2.1 语料选择与清洗:喂给模型的“饭”很重要
做古诗生成,语料的质量直接决定生成结果的下限。当年我用的数据集是从GitHub上一个开源仓库找来的全唐诗,包含了大约5.7万首唐诗。如果找不到完整数据集,用《唐诗三百首》也完全可以跑通,只是生成多样性会差一些。
拿到了原始语料,处理的步骤是这几步:
- 去重:同一首诗在不同来源里重复出现很多次,需要按全文hash去重。
- 噪音去除:古诗文本里常见的括号注释(“(唐)李白”)、多餘空格、生僻标点等,统统去掉。
- 格式过滤:只保留五言或七言的绝句和律诗。这个过滤很重要,因为四言诗、杂言诗的节奏感完全不同,混在一起训练会让模型学到乱七八糟的断句方式。
- 清洗后归档:把过滤后的诗句按每行一首的格式保存为纯文本文件。
2.2 分词与序列化:用“字”做单位而不是“词”
这个项目用的是字级别模型。中文分词在古诗场景下尤其麻烦——古汉语里单字词占绝大多数,但“琵琶”“葡萄”这种双音节词又真实存在。与其引入分词工具引入噪音,不如直接用字作为建模单位。
处理方式:
- 遍历全部文本,建立字符到ID的映射表(vocab)。
- 模型输入不是完整的句子,而是固定长度的字序列,比如设定
maxlen = 20,表示模型每次看20个字来预测第21个字。 - 每个样本就是一组“输入序列 + 真实下一个字”的配对。
2.3 训练样本的构造细节
这里有个容易被新手忽略的细节:一个字也不能浪费。训练诗“床前明月光,疑是地上霜。”时,不是只有“床前明月光”这5个字作为一次训练样本,而是要把所有可能的等长子序列都挖出来。
比如maxlen=5时:
- “床前明月” → 光
- “前明月光” → ,
- “明月光,” → 疑
- “月光,疑” → 是
这样一首20字的诗可以生成15个训练样本。这个技巧叫滑窗采样,能极大扩充训练集规模,同时让模型学到不同起点的预测能力。
构造完样本后,再把输入序列全部转换成ID向量,这样一个可以喂给模型的数据集就准备好了。整个过程其实半小时就能完成,但很多教程里这段被一笔带过,实际做的时候却最容易出错。
3. 模型搭建与训练
3.1 Embedding层:把汉字变成向量
在Embedding层出现之前,字符特征通常用one-hot编码:每个字对应一个高维稀疏向量,维度是词表大小(几千甚至几万),但只有一个位置是1。这种编码有两个大问题:维度爆炸和无法表达字间关系。
Embedding层做的事情,本质上是一个可学习的查表操作。每个字对应一个固定长度的稠密向量(比如128维),这个向量会随着训练不断更新,最终使得语义相近的字在向量空间里彼此靠近。
在Keras里搭建:
from keras.models import Sequential from keras.layers import Embedding, LSTM, Dense, Dropout model = Sequential() model.add(Embedding(input_dim=vocab_size, output_dim=128, input_length=maxlen))这里input_dim是词表大小,output_dim是Embedding向量维度,input_length是输入序列长度。训练过程中层会自动学习每个字的128维向量表示,这比手动设计任何语言特征都要高效。
3.2 LSTM层:真正“写诗”的大脑
LSTM是RNN的改良版,它通过三个门(遗忘门、输入门、输出门)来控制信息的流动。简单理解:遗忘门决定“之前的记忆要保留多少”,输入门决定“新信息有多少要写进记忆”,输出门决定“当前记忆有多少要展示为输出”。
这种结构让LSTM能记住更长距离的信息。写古诗时,模型在生成“月”字时,可能需要参考很早期的“床前”来保证意境一致。Vanilla RNN会有梯度消失问题,长距离信息传到后面时已经衰减得所剩无几;而LSTM通过细胞状态这条传送带,让关键信息能一路畅通地传递下去。
在Keras里搭建:
model.add(LSTM(256, return_sequences=True)) model.add(Dropout(0.3)) model.add(LSTM(256)) model.add(Dropout(0.3))这里用了两层LSTM,第一层设置了return_sequences=True,表示每个时间步的输出都传给下一层,而不是只保留最后一个时间步的输出。这样做的好处是让两层LSTM形成层级特征提取——第一层学到的是较底层的字词搭配模式,第二层学到的是更高层的语义结构。
3.3 训练配置与参数选择
输出层是Dense层加softmax激活函数,输出维度等于词表大小,每个位置的值表示下一个字为对应字的概率:
model.add(Dense(vocab_size, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])选categorical_crossentropy因为这个任务本质上是多分类——在词表所有字里选出最合适的那个。优化器用Adam,它是目前综合效果最好的通用优化器,无需调参就能达到不错的收敛效果。
关键参数参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| maxlen | 20~30 | 输入序列长度,越长信息越多,但训练越慢 |
| Embedding维度 | 128 | 常见的起步值,资源充足可设256 |
| LSTM隐层维度 | 256 | 隐层维度越大,模型容量越大,但过拟合风险增加 |
| Dropout | 0.3~0.5 | 防止过拟合,尤其是训练轮次较多时 |
| batch_size | 128 | 显存(内存)允许范围内尽量大,训练更稳定 |
训练代码:
model.fit(X, y, batch_size=128, epochs=50, validation_split=0.1)validation_split=0.1表示预留10%数据做验证集,判断模型是否过拟合。一般训练30~50轮就能看到loss明显下降,生成的古诗开始有模有样。实测在GTX 1060上,全唐诗5.7万首语料训练50轮,大约需要2~3小时。
4. 古诗生成与调优
4.1 从概率到文字:采样策略决定了生成质量
训练完成后,模型的输出是每个字的概率分布。但“概率最大的字”并不总是最优选——如果每次都选概率最高的那个,模型会陷入重复循环,生成的诗字字句句都相似。
常用的策略是随机采样,即按概率分布抽样,让概率高的字有更大机会被选中,但概率低的字也有机会“露脸”。实现起来很简单:
import numpy as np def sample(preds, temperature=1.0): preds = np.asarray(preds).astype('float64') preds = np.log(preds + 1e-7) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) probas = np.random.multinomial(1, preds, 1) return np.argmax(probas)4.2 温度参数:控制“创意”与“保守”的旋钮
这里的temperature就是控制创意的关键参数,原理是让概率分布变得更尖锐或更平滑:
- 低温(0.3~0.5):概率分布变得更尖锐,高概率字几乎必选。生成结果更保守、更通顺,但缺少惊喜。
- 中等温度(0.8~1.2):自由度和通顺度平衡得最好,推荐从1.0开始调。
- 高温(1.5以上):概率分布被压平,几乎所有字都有可能被选中。生成内容天马行空,但大概率语句不通。
对一个“能看”的古诗生成器来说,0.8~1.0是比较合适的区间。低温适合生成结构严格的绝句,高温适合寻找灵感式的“打油诗”。
4.3 实操中积累的生成技巧
生成时的环境和训练时并不完全一样。训练时模型每次只预测一个字,但生成时模型要充分调用自己的“既有输出”来预测下一个字。这会导致错误逐渐累积,一个不合适的字可能会带偏整首诗。
我的做法是引入一种强制前缀机制:给一个起始字或起始短语,比如“春”“明月”“长江”等,让模型从这个seed开始续写。实现时把seed转换成ID序列,输入模型,得到下一个字的概率分布,采样后拼接到输入序列末尾,再去掉最早的那个字,保持输入长度不变。这样一句一句地“滚”出全诗。
生成完一整首后,我还习惯做一个简单的格式后处理:五言诗每5个字断一句,七言诗每7个字断一句,加上标点符号,输出格式更像真正的诗。
5. 常见问题与排查技巧
5.1 loss不下降或下降极慢
这个是我的老熟人了。排查顺序:
先确认数据预处理没有bug。检查X和y的形状对不对,确保maxlen个字的输入对应正确的第maxlen+1个字作为标签。
降低学习率。Adam默认学习率是0.001,在小数据集上有时偏高。降到0.0005甚至0.0001,loss往往就能平稳下降。
减少序列长度。如果maxlen=30但你的语料本身短句居多,模型很难学到有效信息,适当减到15~20。
5.2 生成结果大量重复
这个几乎是所有RNN/LSTM文本生成项目的通病。如果整首诗只重复一两个字,或反复输出相同句子,直接原因往往不是结构问题,而是训练不充分或模型容量不够。
排查方案:
- 增大LSTM隐层维度,从128提到256或512。
- 增加训练轮次,并观察验证集loss是否仍在下降。
- 生成阶段适当提升温度参数,缓解“过于保守”的重复。
- 增大Dropout可能有帮助,也可能是帮倒忙——先用0.3,确认过拟合再往上加。
5.3 显存(内存)不足怎么办
如果batch_size=128太激进,会直接导致OOM报错。这时候先调小batch_size到32或16。如果仍然不足,把maxlen缩短、Embedding维度降到64都是立竿见影的方案。
还有一个更tricky的方法:设置model.add(LSTM(128, unroll=True))。unroll=True会把LSTM的时间步展开成独立计算图,训练速度更快,但更耗显存。反过来unroll=False(默认)更省显存,适合长序列。不过只有在GPU上训练时才有意义,CPU上默认反而更好。
5.4 古诗不押韵,读起来没有诗意
这个在字级别模型里几乎无解,因为模型完全没有“音调”的概念。一个朴素的改进思路是在数据预处理时做邻近字押韵约束——但实操成本高,而且效果有限。更现实的期望是:这个模型能生成通顺、有基本意境的诗句,但和真正的格律诗有差距。想要更进一步,需要用到更大的模型和更精细的训练策略,那就超出这个demo的范畴了。
6. 进一步优化方向与扩展思路
这个项目做完之后,我陆续做了几个方向的扩展,这里一并说给感兴趣的读者。
6.1 从全唐诗扩展到其他文体
同一套代码,换掉语料就能生成宋词、元曲,甚至现代诗。只需注意不同文体的字数和断句规则不同,可能需要调整maxlen和后处理逻辑。
6.2 加入注意力机制
LSTM的生成是基于固定长度上下文向量的,古诗长度还算好,但如果想生成更长的内容——比如对联或长律——注意力机制能让模型在生成每个字时“回头看”输入序列的关键部分,效果提升非常明显。
6.3 尝试Transformer
我在完成这个项目大半年后,用相同的语料试了Transformer的解码器结构。同样是字级别生成,Transformer在生成长度超过20~30字的序列时明显更稳,但训练显存占用也高了很多。如果想系统性对比RNN系和Transformer系在文本生成上的差异,这个古诗生成项目是一个绝佳的对照实验场景。
6.4 把“韵”和“调”编码进模型
预处理时给每个字标注平仄和韵部,拼接到Embedding向量后面,理论上能让模型学会吟诗作对的平仄规则。这个方向我做过初步实验,效果有提升但不稳定,有兴趣的朋友可以继续深入。
回到这个项目本身,用Keras实现RNN+LSTM自动编写古诗,工程量不大,但麻雀虽小五脏俱全——数据清洗、序列建模、参数调优、生成策略、错误排查,文本生成任务的所有关键环节都覆盖了。我至今仍认为它是理解循环神经网络最好的实操项目,没有之一。建议想入坑NLP的朋友亲手跑一遍,卡在任何一个环节都别着急,回头看看这篇博客,也许就能少走很多弯路。
本文还有配套的精品资源,点击获取