ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Keras实战:用LSTM自动生成古诗的完整指南

Keras实战:用LSTM自动生成古诗的完整指南 简介一套基于Keras框架使用循环神经网络RNN与长短期记忆网络LSTM自动生成古诗的完整项目代码面向深度学习入门者及自然语言处理爱好者。项目内置数据预处理脚本、模型定义与训练逻辑、配置文件以及说明文档可以清晰理解文本序列从清洗、切分到输入模型并生成诗句的完整流程。压缩包共8个文件其中4个Python脚本为核心实现搭配1个中文古诗语料txt、1个Markdown说明文档及许可证等辅助文件整体体积仅3.86MB轻量易用。目前已有601人学习下载是实践序列生成任务的实用参考作者还记录了其他教程未讲清楚的踩坑与排错思路随资源一并提供对初学者尤其友好。通过阅读和运行这些代码读者可以快速复用数据预处理与模型搭建模块训练属于自己的古诗生成模型同时也能迁移相关思路到文本分类、语音识别等序列任务中。 从接触深度学习开始文本生成就是最吸引我的一类任务。而用 Keras 实现 RNN LSTM 来让模型自动写古诗几乎是每个入门 NLP 的人都绕不开的实战项目。这个项目看起来简单就是喂进去一堆诗让它学会写诗但真正动手做的时候从数据预处理到训练调参再到最终的文本采样生成每一步都有不少细节坑。这篇文章是我自己从零跑通这个项目的完整记录包括了核心原理的直观解释、训练过程中的各种问题排查以及最后生成效果的调优思路希望对正在做类似项目的朋友有实际参考价值。1. 项目认知自动写古诗的本质是什么1.1 从字面复读机到序列学习先说一个很多人刚接触时的误区让模型写古诗不是让它背诗也不是让它复制粘贴。模型的本质任务只有一个——给定前面已经生成的字预测下一个最可能出现的字。比如输入床前明月模型需要计算光的概率最高这就是语言模型的核心思想。这个任务看起来简单但难点在于汉字组合的合法空间极其巨大而训练数据相对有限。五言绝句20个字七言绝句28个字古诗的句式结构、平仄韵律、语义连贯这些都不是简单的统计能搞定的。这正是需要 RNN 和 LSTM 这类循环结构的地方。如果把模型比作一个人RNN 给他的能力是记忆让他能记住前面读过的字而 LSTM 则是给这份记忆加上了工作记忆的机制能决定哪些信息要长期保留、哪些信息要立刻忘掉。没有这一步模型只能记住最近两三个字写出来的诗前言不搭后语。1.2 为什么选择 Keras 搭建这个项目现在的深度学习框架选择很多PyTorch 也很流行但如果你只是想快速验证 RNN LSTM 做文本生成这条路子Keras 依然是非常合适的选项。原因有三点API 设计直觉Keras 的Sequential模型把层一层层叠起来对初学者极其友好能把精力集中在理解网络结构上而不是纠结底层张量运算。内置组件完整嵌入层Embedding、LSTM 层、Dropout、Softmax 输出层全都是现成的几行代码就能搭出一个完整的模型。和 TensorFlow 的兼容性现在 Keras 已经深度整合进 TensorFlow 中tf.keras既保留了简洁性训练的性能也有保障。我自己当时的选择是 TensorFlow 2.x 自带的 Keras 模块不需要单独安装直接导入就能跑。这个项目用 CPU 也能训练但如果有入门级 NVIDIA 显卡训练速度会有质的提升。1.3 项目整体工作流整个项目可以拆成四大块后面每一章我都会展开细讲数据准备获取古诗语料清洗构建字符到数字、数字到字符的映射表。样本构建把古诗文本切分为输入序列-目标字符的训练对。模型搭建与训练Embedding LSTM Dense定义损失函数和优化器迭代训练。文本生成用训练好的模型通过采样策略逐字生成。这一步决定了最终的诗像不像人写的。2. 数据准备比模型更值得花时间的部分2.1 语料来源与清洗策略数据决定了模型的上限模型只是在逼近这个上限。我用的古诗语料是公开的全唐诗数据集包含约 5 万多首诗涵盖五言、七言、绝句和律诗。原始文本里混了大量作者名、诗名、标点和换行符这些在训练前都要处理掉。清洗的逻辑看起来简单但有几个决策点值得单独说标点符号中文标点和英文标点全部删除。生成阶段需要标点的话可以在后处理时按长度和句式自动补上。作者和诗题对生成任务来说作者名和诗题是噪声。我选择了剔除只保留诗的正文。长度过滤我保留了 15 到 60 个字符之间的诗。太短的可能是残句太长的如排律会增加序列长度导致训练变慢且效果未必更好。空格与换行所有空白字符统一去除让诗变成一行连续的字符流用特殊标记\n来区分诗的结尾。清洗完之后把每首诗用\n拼接起来形成一个巨大的纯文本文件。这里有个细节如果你要生成多首诗保留换行符非常关键它让模型学到这首诗写完了要换一首的边界概念。2.2 字符映射表一切模型输入的基础模型不认识汉字只认识数字。所以需要把每个唯一的字符映射到一个整数索引。这一步骤在代码上很简单import numpy as np from collections import Counter with open(poems_clean.txt, r, encodingutf-8) as f: text f.read() # 统计所有字符并建立映射 chars sorted(list(set(text))) char_to_idx {ch: idx for idx, ch in enumerate(chars)} idx_to_char {idx: ch for ch, idx in char_to_idx.items()} print(f总字符数: {len(chars)}) print(f总字数: {len(text)})实操心得字符集合的大小直接决定了模型最后一层 Dense 的神经元数量。我的语料清洗后大约有 5000 多个不同的汉字所以输出层是 5000 多分类每个时间步都要从这 5000 多个字里选一个。如果语料太少导致生僻字过多可以考虑把出现次数少于某阈值的字替换为UNK标记但古诗领域我并不建议这样做——因为诗里常有用典和古体字删掉它们反而损害内容丰富度。这个度需要自己权衡。2.3 训练样本构建滑动窗口切分模型训练的基本单元是序列到下一个字符。具体做法是设定一个序列长度SEQ_LENGTH比如 20 或者 30然后用滑动窗口在整篇语料上切分。SEQ_LENGTH 30 step 1 sentences [] next_chars [] for i in range(0, len(text) - SEQ_LENGTH, step): sentences.append(text[i: i SEQ_LENGTH]) next_chars.append(text[i SEQ_LENGTH]) print(f样本总数: {len(sentences)}) print(f示例输入: {repr(sentences[0])}) print(f示例目标: {repr(next_chars[0])})我用的滑动步长是 1表示每移动一个字符就切一个样本。这样做的结果是样本之间有大量重叠数量会达到上百万个。训练量是大了但模型见过的上下文更密集生成效果会更好。关键决策序列长度 30 意味着模型每次只看前面 30 个字。对七言绝句来说28 个字加上结尾正好是完整一首的长度。如果序列太长LSTM 的记忆负担加大训练速度变慢而且实际效果未必更好太短则模型学不到跨句的语义关系生成的句子前后逻辑容易断裂。实测下来 20 到 40 是合理区间。3. 模型结构Embedding LSTM Dense 的设计逻辑3.1 为什么不能直接用 One-hot 编码构建训练样本时最朴素的做法是把字符转成 one-hot 向量再输入模型。但 one-hot 编码的维度等于字符表大小5000并且没有任何语义信息——月和明在 one-hot 空间里的距离和月与煮完全一样模型学不到字与字之间的关联。Embedding 层解决的问题正是这个。它把每个字符映射到一个低维稠密向量中比如 128 维在训练过程中不断调整让语义相近的字在向量空间中距离更近。从直观经验看Embedding 维度设置在 128 到 256 之间效果比较好。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense from tensorflow.keras.optimizers import Adam EMBEDDING_DIM 128 LSTM_UNITS 256 VOCAB_SIZE len(chars) model Sequential([ Embedding(VOCAB_SIZE, EMBEDDING_DIM, input_lengthSEQ_LENGTH), LSTM(LSTM_UNITS), Dense(VOCAB_SIZE, activationsoftmax) ]) model.summary()3.2 单层 LSTM 还是多层 LSTM很多初学者喜欢堆多层 LSTM觉得层数越深模型越聪明。但在这个项目里我强烈建议从单层开始。原因有两个数据量匹配5 万首诗的语料规模并不算大单层 256 个单元的网络容量已经足够学习古诗的句法结构。盲目加深层数训练参数量暴涨但数据量跟不上模型很容易过拟合。训练效率LSTM 的循环结构无法像卷积层那样高度并行化每加一层训练时间接近线性增长。我在 CPU 上训练单层模型跑一个 epoch 大约 20 分钟加了第二层之后直接翻倍效果却没有明显提升。单层 LSTM 合适的 Dropout 在这个任务上是性价比最高的配置。等后续语料扩充了再考虑加深也不迟。3.3 输出层的设计一个多分类器模型的最后一层是Dense(VOCAB_SIZE, activationsoftmax)本质是做一个全连接层的多分类给定当前输入序列的最终隐藏状态计算每个汉字作为下一个字的概率分布。这里有个细节值得注意模型只在序列的最后输出一个预测而不是每个时间步都输出。这是因为我们选择的 LSTM 默认只返回最后一个时间步的隐藏状态。可以用return_sequencesTrue让模型在每个时间步都输出预测但那样做通常用于序列到序列的任务对预测下一个字来说没必要。3.4 损失函数与优化器选择多分类问题首选交叉熵损失categorical_crossentropy配合 Adam 优化器。Adam 自带自适应学习率不需要手工调太多参数对小规模数据集尤其友好。model.compile( losscategorical_crossentropy, optimizerAdam(learning_rate0.001), metrics[accuracy] )这里有个容易踩的坑目标值next_chars需要用to_categorical转为 one-hot 向量也就是从字符索引变为一个长度等于字符表大小的向量对应位置为 1其余为 0。我一开始偷懒直接传整数标签结果报错维度不匹配。如果你用的是sparse_categorical_crossentropy则可以直接传整数标签省去 one-hot 编码的环节两种方式等价记住别混用。4. 训练过程从 Loss 不降到生成有点意思4.1 训练数据预处理X 和 y 的构造上一章已经切好了sentences和next_chars接下来要转成模型能吃的格式。Embedding 层要求输入是整数索引所以先把sentences里的每个字符映射为char_to_idx中的编号得到一个形状为(样本数, SEQ_LENGTH)的整数矩阵。X np.array([[char_to_idx[ch] for ch in sentence] for sentence in sentences]) y np.array([[char_to_idx[ch]] for ch in next_chars])注意这里X是一个二维数组但 Embedding 层实际期望的输入形状是(batch_size, sequence_length)二维完全符合要求。最后训练时一次性把所有样本载入内存大约占 3 到 4 GB 内存百万样本 × 30 序列长度 × 4 字节需要留意一下机器配置。4.2 Loss 不降的排查过程第一次训练时我遇到了 loss 前几个 epoch 下降正常、后面就不动了的情况。当时我一度以为模型卡在局部最优试了好几种优化器都不见效。后来静下心排查终于定位到问题训练样本的顺序性太强了。滑动窗口切分出来的样本前后存在大量重叠如果不打乱就直接按 batch 训练相当于模型用几乎相同的数据反复学习很快就饱和了。解决方式是在拟合时指定shuffleTruehistory model.fit( X, y_onehot, batch_size128, epochs50, shuffleTrue, verbose1 )另一个坑是学习率衰减。Adam 默认学习率为 0.001但在训练后期 loss 在某个值附近震荡时手动把学习率降到 0.0003 或 0.0001往往能让 loss 明显再降一截。我是通过ReduceLROnPlateau回调实现的当 loss 连续三轮不下降时自动把学习率减半。4.3 观察训练曲线过拟合还是欠拟合我在训练过程中记录了 loss 和 accuracy。前 15 个 epoch训练集的 accuracy 从 10% 左右升到 40% 出头到第 30 个 epoch 时训练 accuracy 达到 60% 以上但验证集的表现提升非常有限这是典型的过拟合信号。对策主要有两个增大 DropoutLSTM 层之后加一个 Dropout(0.2)在输出层之前加一个 Dropout(0.2)。我当时在 LSTM 后加了 Dropout(0.3)过拟合的情况明显缓解。提前停止设定EarlyStopping回调监控验证集 loss连续多轮不降就停止训练。这样既省时间也能保住一个相对泛化的模型。必须说明的是文本生成任务的最终评价标准不是 accuracy。模型在 5000 类上做预测accuracy 能达到 30% 已经是一个看起来不太离谱的水平了。真正要判断模型好不好还得看跑出来的诗是否像句人话这也体现了为什么这个项目有时候被称作玄学调参。5. 古诗生成采样策略决定诗的灵气5.1 从概率分布到具体汉字模型训练完成之后生成阶段的逻辑是给定一个种子文本模型预测下一个字的概率分布从中采样一个作为下一个字然后把这个字拼到序列末尾再预测下一个。循环往复直到生成指定长度或遇到终止标记。核心代码如下def sample(preds, temperature1.0): preds np.asarray(preds).astype(float64) preds np.log(preds 1e-8) / temperature exp_preds np.exp(preds) preds exp_preds / np.sum(exp_preds) probas np.random.multinomial(1, preds, 1) return np.argmax(probas)5.2 Temperature 是左右生成质量最关键的一步生成效果好不好很大程度取决于一个叫temperature温度的参数。它的原理是在取 softmax 之前先把 logits 除以 temperature然后才计算概率分布。temperature越小概率分布越尖锐模型越倾向于选最可能的那个字生成结果更保守、更像模板不容易出错但也缺乏新意。temperature越大概率分布越平缓低概率字被选中的机会增加句子会更放飞自我但胡言乱语的风险也飙升。我之前分别用 0.5、0.8、1.0 和三档参数做了生成实验直观感受差异非常明显。0.5 生成的诗用词工整但频繁出现床前明月光疑是地上霜这类高度重复的句子1.0 则经常冒出风月何须问山河独自愁这种出人意料的搭配但成功率低经常整句语义不通。综合来看0.6 到 0.8 是古诗生成的最佳区间既保持基本的语感又保留一定的创造性。5.3 种子文本的选择与长度生成时需要给定一段种子文本作为起点相当于给模型一个灵感。种子文本可以是任意长度的连续字符但建议不要超过序列长度 30。实践中我给了多种类型的种子seed_text 江上月 # 短种子模型自由发挥空间大 seed_text 白日依山尽 # 完整五言句模型顺着续写 seed_text 床前明月光 # 名句开头看模型如何接生成的时候有个小技巧为了模拟原创性可以每次从全诗中随机选一个切点作为种子避免每次都从同一句开始。这样能多试几轮挑出效果最好的一组。6. 项目中的高频问题与最终效果观察6.1 高频报错与解决思路这个项目我带了几个朋友一起跑过汇总了一下大家最容易遇到的问题报错信息出现场景解决办法IndexError: index X is out of bounds for axis 0 with size Y字符索引超出范围检查语料预处理时是否出现未知字符统一用UNK替换ValueError: Shapes (None, 1) and (None, 5000) are incompatible目标值维度不匹配确认y是否已做 one-hot 编码或改用sparse_categorical_crossentropyResourceExhaustedError: OOM when allocating tensor训练时显存/内存不足降低batch_size或减少LSTM_UNITSLoss 不降上一章提到的顺序性样本问题打开shuffleTrue检查学习率6.2 我实测过的生成效果模型训练 50 个 epoch 之后我用青山作为种子文本做了一组生成temperature 设为 0.7。生成结果拿出来举个例子青山不可极万里见孤舟。 日暮风吹叶相思在故楼。说实话这句和真正唐诗比依然有差距平仄对仗也没有完全掌握但已经做到了每句话都像在说一件事比 N-gram 模型生成的拼凑句子要好太多了。让人意外的是模型竟然学到了一些意象搭配比如青山和孤舟、日暮和风吹叶这些共现关系不是靠硬编码实现的而是 LSTM 在数据里自己学到的。6.3 进阶优化注意力机制与模型结构改进跑通基础版之后如果还想继续深入有两条自然的进阶路线。第一条是引入注意力机制让模型在生成当前字时不只依赖最后一个隐藏状态而是有选择地关注之前每个时间步的隐藏状态这能有效缓解长序列的信息遗忘问题。第二条是换用 Transformer 结构利用自注意力机制并行处理整个序列这也是现代文本生成的主流方案。不过 Transformer 的训练数据和算力门槛比 LSTM 高不少不推荐作为入门第一步。最后分享一个常规文档里不会写的经验不要一上来就训练全量数据。先用 1000 首诗做一个冒烟测试确认整个流程跑通、loss 能降、生成功能正常然后再扩到全量语料。这样做能帮你在几十分钟内排除大多数代码问题而不是等几个小时后才发现预处理阶段就有 bug。这个方法救过我很多次希望对你也有用。本文还有配套的精品资源点击获取
返回列表