
简介面向深度学习和自然语言处理初学者这份资料以TensorFlow 2.0为基础完整演示了基于LSTM的循环神经网络生成唐诗的实战过程。压缩包内共12个文件包含6个Python源码脚本、2个文本配置文件、模型权重h5文件、训练效果示意图、实验报告PDF和答辩PPT整体大小约20.18MB。其中Python脚本覆盖数据预处理、模型构建、训练与评估全流程实验报告和答辩PPT则帮助理解代码逻辑并快速呈现成果。已有1537人学习下载适合需要完成课程设计、毕业设计或入门RNN实战的读者尤其是希望从零跑通“日、红、山、夜、湖、海、月”等起始词生成唐诗任务的开发者。资料内容完整既支持直接运行调试也能作为拆解LSTM原理的参考。1. 用 TensorFlow 2.0 训一个会写唐诗的 RNN这个实战项目到底在解决什么问题如果你刚把深度学习环境配好正愁找不到一个「数据、代码、权重、报告」四件套齐全的 NLP 入门项目这个 RNN 唐诗写作的资源包会非常对你胃口。它不是那种只贴几段残缺代码的教程而是把从语料清洗、LSTM 模型搭建、训练调参到生成诗文的完整链路都收敛在一个 zip 里train.py、dataset.py、model.py、utils.py、settings.py、eval.py 外加已经训好的 best_model.h5 权重文件以及答辩 PPT 和实验报告。也就是说你拿到手就能跑通一个以 LSTM 为核心、用「日、红、山、夜、湖、海、月」这几个字作为开头词生成唐诗的完整流程。这类任务本质上是字符级语言建模给定前一个字符预测下一个字符的概率分布。听起来简单但真正落地时数据预处理怎么做、Embedding 维度设多少、LSTM 层数堆几层、采样温度怎么调每一步都藏着坑。这篇文章我就按自己拆这个项目的习惯从数据管线、模型结构、训练策略到生成推理逐层拆开讲重点放在那些你照着教程敲也会翻车的地方。2. 数据预处理与训练集构建从 poems.txt 到序列样本2.1 语料的真实形态与清洗策略项目里的 poems.txt 是原始语料包含的是全唐诗整理后的文本。常见做法是每行一首诗也可能一首诗占多行、中间夹杂标题和作者信息。第一步不是直接塞进模型而是先做清洗把全角字符转半角、去掉空白符和特殊符号、过滤掉长度异常的句子。# utils.py 中常见的清洗逻辑我按项目场景重写了一份 import re def clean_poem(line): # 去掉标题、作者等非诗内容只保留汉字和常见标点 line re.sub(r[^\u4e00-\u9fa5。、\n], , line) # 全角空格、制表符统一去掉 line line.replace(\u3000, ).replace(\t, ).strip() return line清洗逻辑的核心是正则表达式[^\u4e00-\u9fa5...]表示只保留 Unicode 范围内的汉字和几个中文标点其余字符全部丢弃。这里有个容易被忽略的点——唐诗里的「阙」「琤」这类生僻字也在\u4e00-\u9fa5区间内不会被误删但如果你额外加了繁简转换逻辑反而可能破坏原诗的用字习惯我一般不做繁简转换。2.2 字符表构建与序列化比分词更适合古诗生成古诗生成用「字」而不是「词」做最小单位。原因是古诗字数少、用词灵活分词容易把「白日依山尽」拆得支离破碎而字符级建模能保留每个字的独立性也让模型更易学到五言、七言的节奏规律。# dataset.py 中构建字符表的典型实现 def build_vocab(poems): chars sorted(list(set(.join(poems)))) char_to_idx {ch: i for i, ch in enumerate(chars)} idx_to_char {i: ch for i, ch in enumerate(chars)} vocab_size len(chars) return char_to_idx, idx_to_char, vocab_sizeset(.join(poems))把全部语料摊平成一个字符流再去重得到的字符表通常在两三千的量级。这里要注意字符表必须固定下来训练和推理用同一份否则eval.py加载best_model.h5时会出现索引错位——模型输出的其实是训练时的字索引映射表一变生成结果就全乱了。当 打 完 字 符 表下一步就是把每首诗切成长度为seq_length的输入序列和对应的目标序列。目标序列就是输入序列整体右移一位输入「床前明月」目标就是「前明月光」。这是字符级语言建模的标准做法LSTM 在每个时间步看到一个字符去预测下一个字符。# 序列化滑窗切分生成 (input, target) 对 def make_sequences(poems, char_to_idx, seq_length32): sequences [] for poem in poems: # 把诗转成索引序列末尾补一个结束符 EOP方便模型学到诗的终止 idx_seq [char_to_idx[ch] for ch in poem] [char_to_idx[EOP]] for i in range(0, len(idx_seq) - seq_length, 1): input_seq idx_seq[i:i seq_length] target_seq idx_seq[i 1:i seq_length 1] sequences.append((input_seq, target_seq)) return sequences滑窗步长设为 1意味着每往前挪一个字就生成一条样本。一首 40 字的诗大约能切出 8 条训练样本整个语料几万首诗样本量足够喂饱一个单层 LSTM。EOP结束符很关键它让模型学会在一首诗写完后停止而不是无限循环下去。2.3 数据管线的落地细节数据构建完成后最终喂给模型的是两个张量X形状为(batch_size, seq_length)y形状也是(batch_size, seq_length)。但y不能直接和交叉熵损失对接需要做 one-hot 编码或直接让模型输出层配合sparse_categorical_crossentropy使用。# train.py 中的数据装载简化逻辑 import tensorflow as tf def load_dataset(seq_length32, batch_size64): char_to_idx, idx_to_char, vocab_size build_vocab(all_poems) seqs make_sequences(all_poems, char_to_idx, seq_length) X tf.constant([s[0] for s in seqs], dtypetf.int32) y tf.constant([s[1] for s in seqs], dtypetf.int32) dataset tf.data.Dataset.from_tensor_slices((X, y)) dataset dataset.shuffle(10000).batch(batch_size, drop_remainderTrue) return dataset, char_to_idx, idx_to_char, vocab_size我习惯用tf.data.Dataset而不是手动切 batch一方面它自带 shuffle 和 batch 封装另一方面 prefetch 可以在训练时异步加载数据避免 GPU 空转。drop_remainderTrue保证每个 batch 形状一致省得最后一批形状不一致触发报错。这里有一个性能取舍滑窗步长为 1 会产生大量高度重叠的样本模型很容易记住相邻样本的规律而不是真正学到诗的结构。如果发现训练损失降得很快但生成效果差可以把步长从 1 调成seq_length // 2减少样本重叠度。3. 模型构建与训练策略LSTM 的参数到底怎么设3.1 网络结构拆解Embedding LSTM Dense项目里的 model.py 结构不复杂核心就是三层Embedding 层把字符索引映射成稠密向量LSTM 层在时间步上传递隐状态Dense 层输出每个字符的概率。这是最经典的字符级 RNN 配置也是我第一次跑通这个项目时的结构。# model.py 中 LSTM 模型的定义 import tensorflow as tf from tensorflow.keras import layers, models def build_model(vocab_size, embedding_dim128, lstm_units256, num_layers2): model models.Sequential([ layers.Embedding(vocab_size, embedding_dim), layers.LSTM(lstm_units, return_sequencesTrue, dropout0.2), layers.LSTM(lstm_units, return_sequencesTrue, dropout0.2), layers.Dense(vocab_size, activationsoftmax) ]) return modelreturn_sequencesTrue是必须的——它让 LSTM 在每个时间步都输出隐状态而不是只在最后一个时间步输出。如果这里写错Dense 层接收到的维度就不对训练时直接报 shape 不匹配。两层 LSTM 的 dropout 都设了 0.2这是防止过拟合的第一道防线。Embedding 维度 128 是我在这个数据量级下的起步值。语料只有几万首诗字表两千多128 维足够编码每个字的语义和韵律特征。LSTM 单元数 256 也是中庸选择太小学不到长距离依赖太大在 CPU 上训练会慢到怀疑人生。如果你没有 GPU建议把 LSTM 单元数降到 128单层结构训练时间能缩短一半以上。3.2 损失函数与优化器的选择逻辑模型输出层用 softmax 得到 vocab_size 个概率值损失函数用sparse_categorical_crossentropy。很多人问为什么不用categorical_crossentropy——因为后者需要把目标值转成 one-hot 向量3000 类的 one-hot 会占大量内存而 sparse 版本直接吃整数索引即可省一步转换。# train.py 中的编译与训练配置 model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losstf.keras.losses.sparse_categorical_crossentropy, metrics[accuracy] ) checkpoint tf.keras.callbacks.ModelCheckpoint( best_model.h5, monitorloss, save_best_onlyTrue, modemin ) history model.fit( train_dataset, epochs50, callbacks[checkpoint], verbose1 )Adam 学习率 0.001 是 NLP 任务的默认起点。这里有个我踩过的坑如果 loss 下降非常慢先别急着调网络结构把学习率调到 0.003 试试字符级语言建模的 loss 曲面比较平滑稍大的学习率往往收敛更快。save_best_only配合monitorloss会让模型只在 loss 创新低时保存避免训练后期因过拟合导致权重变差还被保存下来。3.3 训练过程的观察维度训练时不要只盯 accuracy字符级预测里 accuracy 到 60% 就已经说明模型学到了不少字词搭配但生成的诗依然可能狗屁不通。更要关注 loss 的变化趋势如果训练 loss 持续下降但验证集如果有划分loss 开始回升就是过拟合信号此时应增大 dropout 或提前停止。项目里的 settings.py 集中管理这些超参数包括seq_length、embedding_dim、lstm_units、batch_size、epochs、学习率等。我强烈建议你也这样组织代码——所有可调参数集中在一个文件里实验不同配置时只需改 settings.py不用翻训练脚本。# settings.py 中的超参数配置 SEQ_LENGTH 32 EMBEDDING_DIM 128 LSTM_UNITS 256 NUM_LAYERS 2 DROPOUT 0.2 BATCH_SIZE 64 EPOCHS 50 LEARNING_RATE 0.001把这些参数抽成常量而不是散落在各脚本里是让整个项目可复现的基石。答辩时老师问「你这个模型怎么调的参」你直接说「所有参数集中在 settings.py我跑了三组对比实验」比现场翻代码找定义要有说服力得多。4. 从权重到诗句eval.py 的生成逻辑与温度采样4.1 加载权重与预热隐状态训练结束后生成阶段要做的第一件事是加载 best_model.h5然后用指定的开头字构造初始输入序列。这里的关键在于模型是序列到序列的你不能只给它一个「日」字就期待它输出整首诗需要把开头字构造成(1, seq_length)的定长序列不足的部分用填充符补齐。# eval.py 中加载模型并构造初始输入 import numpy as np import tensorflow as tf model tf.keras.models.load_model(best_model.h5) def generate_poem(start_word, char_to_idx, idx_to_char, max_len64, temperature1.0): # 构造定长序列开头字放最后前面用 EOP 填充 init_seq [char_to_idx[EOP]] * (SEQ_LENGTH - 1) [char_to_idx[start_word]] input_seq np.array([init_seq], dtypenp.int32) generated [start_word] for _ in range(max_len): # 预测下一个字符的概率分布 probs model.predict(input_seq, verbose0)[0, -1, :] # 温度采样 probs np.log(probs 1e-10) / temperature probs np.exp(probs) probs probs / np.sum(probs) next_idx np.random.choice(len(probs), pprobs) next_char idx_to_char[next_idx] if next_char EOP: break generated.append(next_char) # 把新字符接到序列末尾丢掉最前面的字符保持长度不变 input_seq np.roll(input_seq, -1) input_seq[0, -1] next_idx return .join(generated)这段代码有几个细节值得说清楚。model.predict输出的是(1, seq_length, vocab_size)的三维张量[0, -1, :]取的是最后一个时间步的概率分布——也就是模型基于当前看到的所有字符对下一个字符的预测。np.roll实现滑动窗口更新把整个序列左移一位新预测的字符塞到末尾这样模型每次看到的都是最近seq_length个字符。4.2 温度参数从保守到放飞温度temperature是生成阶段最重要的旋钮。温度越低模型越倾向于选概率最高的字符生成结果保守但通顺温度越高概率分布被拉平低频字也有机会被选中结果更有创意但也更容易跑偏。我实测下来温度在 0.81.2 之间是唐诗生成的甜区。低于 0.5模型会反复输出「日日照香炉」这类高频搭配整首诗毫无新意高于 1.5句子就从唐诗变成了乱码。项目默认的 1.0 是中规中矩的起点你想让诗更有「人味」就把温度调到 0.9 左右在通顺和惊喜之间取个平衡。4.3 生成效果的自检方法生成完一首诗不要只看字面通不通顺用几个硬指标快速自检押韵是否到位——五言诗第 2、4、6 句末字应该同韵平仄是否基本合规——二四六位置的平仄交替是否明显以及是否频繁重复同一个字——重复率过高说明模型没有学到足够丰富的用字分布。# 命令行调用示例 python eval.py --start_word 日 --temperature 1.0如果生成的五言诗里连续三句都以同一个字结尾大概率是训练不充分或者语料里五言诗占比不均衡。项目中的 poems.txt 其实涵盖了五言和七言如果你只想要五言诗预处理阶段就该按句子长度过滤而不是在生成阶段硬拗。5. 避坑指南环境、数据、过拟合与权重加载的八个翻车现场5.1 TensorFlow 版本不匹配导致权重加载失败现象load_model(best_model.h5)报错提示Unknown layer: LSTM或AttributeError: module tensorflow has no attribute compat。原因项目基于 TensorFlow 2.0 训练但你的环境装的是 TensorFlow 2.10 或 1.x。Keras 层序列化时写了版本相关的标识跨版本加载权重时结构解析会失败。解决严格按项目要求创建虚拟环境固定tensorflow2.0.0。如果你用的是 2.10可以用tf.keras.models.load_model并尝试compileFalse跳过优化器状态加载model tf.keras.models.load_model(best_model.h5, compileFalse)5.2 中文编码问题导致语料读进来就是乱码现象poems.txt打开正常但open()读进来全是锟斤拷一类乱码。原因文件是 GBK 编码而你用默认 UTF-8 读取。大部分中文语料为了兼容老旧 Windows 工具会存成 GBK。解决读取时显式指定编码先探测再读取with open(poems.txt, r, encodinggbk, errorsignore) as f: poems f.readlines()更保险的做法是用chardet库先检测编码再按检测结果读取。这一步做不对后面全是白费功夫——模型吃进去的是乱码生成出来的自然也是乱码。5.3 训练 loss 不降卡在 7.5 左右现象loss 卡在 log(vocab_size) 附近也就是模型在均匀瞎猜。原因学习率太低或者 Embedding 层没有正确初始化。更隐蔽的原因是数据没 shuffle模型在按顺序学习语料里的相似诗句。解决确认dataset.shuffle(10000)生效别在 batch 之后 shuffle。把学习率从 0.001 提到 0.003 再试。如果还没动静检查输入数据里有没有大量空行——空行对应的样本全是填充符模型学不到任何东西。5.4 生成的诗五言不像五言、七言不像七言现象生成的句子长度参差不齐有时一句 6 个字。原因语料清洗时没有按诗句长度过滤。全唐诗里五言、七言、杂言混在一起模型学到了各种长度模式。解决在预处理阶段按诗歌体裁过滤。五言诗通常每行 20 个字符含标点七言诗 28 个字符。用正则按行匹配长度把不合规的句子剔除掉。5.5 训练时间过长一个 epoch 要 20 分钟现象训练速度慢到根本跑不完 50 个 epoch。原因LSTM 单元数设太大或者 batch_size 太小导致 GPU 利用率不足。更常见的是根本没在用 GPUTensorFlow 静默跑在 CPU 上。解决训练前检查设备print(tf.config.list_physical_devices(GPU))如果输出为空说明 TensorFlow 没识别到 GPU需要装 CUDA 和 cuDNN 对应版本。网络结构方面压缩到单层 LSTM 128 单元训练时间能砍掉四分之三。5.6 生成了EOP标记但没有停止现象生成的诗里直接出现EOP三个字符循环没跳出。原因生成循环里判断的是next_char EOP但字符表里EOP和实际文本中的字符混在一起预测时输出了完整的占位符文本而不是对应的索引。解决把idx_to_char[next_idx]拿到字符后先判断是否等于结束符再做拼接。更稳的方式是单独用一个end_idx变量存EOP的索引判断next_idx end_idx从索引层面拦截不依赖字符比较。5.7 权重文件缺失只有训练好的 h5 没有模型结构现象load_model报错提示文件不是合法的模型文件或者结构完整但加载后无法预测。原因best_model.h5在 TensorFlow 2.0 里默认保存的是完整模型但在某些使用model.save_weights()的版本里只保存了权重没有结构。解决用model tf.keras.models.load_model(best_model.h5)代替model.load_weights()。如果实在加载不了完整模型先手动用 model.py 里的build_model()重建结构再调用load_weights(best_model.h5)。5.8 实验报告和答辩 PPT 与代码版本不一致现象报告里写的超参数和 settings.py 里的对不上PPT 里的模型图跟实际训练的层数不一样。原因项目作者可能多次调整代码但报告和 PPT 没同步更新。解决拿到项目后先对照 settings.py 和 model.py把报告里的网络结构图、超参数表改成当前代码的实际值。答辩时老师一旦对照代码问细节前后不一致比模型效果差更致命。6. 进阶玩法温度退火与藏头诗生成把项目从「能跑」变成「能讲」基础的eval.py已经能生成完整唐诗但如果你想在答辩或作品集里多展示一些自己的思考两个小改进就能让项目上一个台阶。第一个是温度退火——生成开头时用较高的温度探索新字生成后半段降低温度保证收束。这模拟了人写诗先有灵感后收束的思路实现起来只需在生成循环里动态调整 temperaturefor i in range(max_len): current_temp 1.0 if i max_len // 2 else 0.7 probs model.predict(input_seq, verbose0)[0, -1, :] probs np.log(probs 1e-10) / current_temp probs np.exp(probs) probs probs / np.sum(probs) next_idx np.random.choice(len(probs), pprobs)第二个玩法是藏头诗生成。给定「月」「光」「千」「里」四个字作为每句的开头生成时分别在句首位置强制指定这几个字其余位置正常采样。实现方式是在生成到指定位置时直接把下一字符设为预设的字跳过采样步骤。这个功能写进 PPT 里非常讨巧因为它直观展示了「模型能受控生成」——比单纯输出一首藏头诗更能体现你对模型输入输出机制的理解。我从这个项目里学到的最重要一课是环境搭建不是「配置一下就能开始」的步骤而是整个项目的第一道生死关。TensorFlow 2.0 的.h5权重文件在 2.10 及以上版本里加载姿势完全不同Python 3.6 和 3.8 对某些依赖的兼容性也有细微差别。从那以后我每拿到一个新的深度学习项目第一件事永远是建独立虚拟环境、按项目的依赖列表逐项安装、跑通最小推理用例再开始改代码。整个过程半小时不到却避免了后续无数个小时的排错。希望这篇拆解能帮你在 RNN 唐诗写作这条路上一遍走通少踩几个我踩过的坑。本文还有配套的精品资源点击获取