简介这份资源面向深度学习与自然语言处理方向的初学者及课程实践者围绕循环神经网络LSTM构建唐诗生成模型解决从零搭建文本生成项目的入门难题。压缩包共12个文件、约20.18MB包含6个Python源码文件模型定义、训练、评估与工具模块、2个txt文本诗词语料与说明、1个h5权重文件、1个png模型结构图以及答辩PPT和实验报告PDF覆盖代码、数据、演示与文档全流程。实验要求以“日、红、山、夜、湖、海、月”等词作为起始词生成诗句可帮助读者理解序列建模、词向量与采样策略。已有1537人学习下载适合作为课程设计或毕业设计的参考方案读者可据此掌握TensorFlow2.0下RNN/LSTM的完整实现路径并借助实验报告与幻灯片快速梳理思路、完成展示。1. 用 RNN 写唐诗一个能跑通的深度学习入门项目长什么样很多人学完 RNN 循环神经网络的理论手推完公式却卡在“怎么把它变成一个能跑、能出结果的东西”上。唐诗生成恰好是个理想的练手场景语料干净、任务直观、模型不复杂还能直接看到“自己写的诗”这种正反馈。这个项目的核心是用 TensorFlow 2.0 搭建一个字符级 RNN 语言模型喂给它《全唐诗》这类语料让它学会平仄、押韵和句式规律最后能续写出五言或七言。它适合刚学完深度学习基础、想找一个完整项目串起数据预处理、模型搭建、训练、推理全流程的人。你不需要 GPU 也能跑但有一块会舒服很多。下面我按实际动手顺序把这条链路拆开讲清楚。2. 数据准备与字符级建模把唐诗变成 RNN 能吃的张量2.1 为什么选字符级而不是词级唐诗的词汇高度凝练分词本身就有歧义比如“白日依山尽”里“白日”是一个词还是“白”加“日”不同分词工具结果不一样反而引入噪声。字符级建模把每个汉字当成一个 token语料里出现过的所有汉字构成词表模型学的是“给定前 n 个字下一个字最可能是什么”。这样做的好处是词表小、无需分词、对未登录词天然友好缺点是序列更长、模型需要记住更远的依赖。对唐诗这种短文本五言 20 字、七言 28 字字符级完全够用也是常见做法。2.2 语料清洗与词表构建我一般会先准备一个纯文本文件每行一首诗去掉标题、作者、标点只保留汉字。下面这段代码完成读取、去重、构建字符到 id 的映射。import numpy as np # 读取语料每行一首诗已去除标点和非汉字字符 with open(tangshi.txt, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] # 只保留长度在 20 到 30 之间的诗句过滤掉残句和超长排律 poems [p for p in lines if 20 len(p) 30] print(f有效诗数量: {len(poems)}) # 构建字符表 text .join(poems) chars sorted(set(text)) char2idx {c: i for i, c in enumerate(chars)} idx2char {i: c for c, i in char2idx.items()} vocab_size len(chars) print(f词表大小: {vocab_size}) # 把整段文本转成 id 序列 data np.array([char2idx[c] for c in text], dtypenp.int32)逻辑说明poems过滤掉长度异常的句子避免模型学到残缺模式。chars用sorted保证每次运行词表顺序一致方便复现。data是展平的一维数组后面按固定长度切窗口。参数上长度范围 20 到 30 是我根据五言和七言常见格式定的你可以按自己语料调整。词表大小通常在 3000 到 6000 之间如果超过 8000说明语料里混入了生僻字或非汉字需要再清洗。2.3 用 tf.data 构造训练批次TensorFlow 2.0 推荐用tf.data.Dataset做输入管道比手动 feed 更高效。核心是把长序列切成seq_length 1的窗口前seq_length个字符作输入后移一位作标签。import tensorflow as tf seq_length 20 batch_size 64 buffer_size len(data) - seq_length # 构造滑动窗口数据集 dataset tf.data.Dataset.from_tensor_slices(data) dataset dataset.window(seq_length 1, shift1, drop_remainderTrue) dataset dataset.flat_map(lambda x: x.batch(seq_length 1)) dataset dataset.map(lambda x: (x[:-1], x[1:])) dataset dataset.shuffle(buffer_size).batch(batch_size, drop_remainderTrue) dataset dataset.prefetch(tf.data.AUTOTUNE)逻辑说明window按步长 1 滑动切窗flat_map把嵌套的窗口展平。map里x[:-1]是输入x[1:]是标签这就是语言模型的自监督构造。shuffle的buffer_size设大一些打乱更充分但别超过数据总量。prefetch让 CPU 准备数据和 GPU 计算重叠能明显提速。参数上seq_length取 20 覆盖五言全句七言可以调到 28batch_size根据显存调8G 显存跑 64 到 128 没问题。3. 搭一个能收敛的 RNN 模型Embedding GRU Dense 的参数怎么定3.1 模型结构选型SimpleRNN、LSTM 还是 GRU理论上 SimpleRNN 最贴近教科书但实际训练时梯度消失严重超过 10 个时间步就记不住东西写出来的诗会重复同一个字。LSTM 和 GRU 都引入门控机制GRU 参数更少、训练更快在唐诗这种短序列任务上效果和 LSTM 差不多。我一般直接用 GRU省显存也省时间。如果你要对比实验可以三个都跑一遍但别指望 SimpleRNN 能出好结果这是血泪经验。3.2 用 Keras 函数式 API 搭建模型TensorFlow 2.0 里tf.keras是官方推荐的高层接口下面用函数式 API 搭一个 Embedding GRU Dense 的结构。from tensorflow.keras import layers, Model embedding_dim 256 rnn_units 512 inputs layers.Input(shape(None,), dtypetf.int32) x layers.Embedding(vocab_size, embedding_dim)(inputs) x layers.GRU(rnn_units, return_sequencesTrue, recurrent_initializerglorot_uniform)(x) outputs layers.Dense(vocab_size)(x) model Model(inputs, outputs) model.compile(optimizeradam, losstf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue), metrics[accuracy]) model.summary()逻辑说明Input的shape(None,)表示可变长度推理时可以喂任意长度前缀。Embedding把离散 id 映射到稠密向量embedding_dim取 256 是常见起点语料大可以加到 512。GRU的return_sequencesTrue保证每个时间步都有输出recurrent_initializer用 glorot 均匀分布避免初始状态过大导致梯度爆炸。Dense输出维度等于词表大小from_logitsTrue表示输出未经过 softmax数值更稳定。参数量上这个配置大约 300 万到 500 万普通显卡都能跑。3.3 训练循环与 checkpoint 保存训练时用ModelCheckpoint回调定期保存权重方便中断后恢复也方便后面加载推理。checkpoint_dir ./training_checkpoints checkpoint_prefix checkpoint_dir /ckpt_{epoch} checkpoint_callback tf.keras.callbacks.ModelCheckpoint( filepathcheckpoint_prefix, save_weights_onlyTrue, save_best_onlyTrue, monitorloss, verbose1) EPOCHS 50 history model.fit(dataset, epochsEPOCHS, callbacks[checkpoint_callback])逻辑说明save_weights_onlyTrue只存权重文件小、加载快。save_best_only配合monitorloss保留损失最低的那次避免过拟合后保存了差模型。EPOCHS设 50 是起步语料大可以到 100但要看验证集损失是否还在降。训练时如果 loss 降到 1.5 以下生成的句子基本通顺降到 1.0 左右平仄和押韵会明显改善。如果 loss 震荡不降先检查学习率Adam 默认 1e-3 通常够用不行就降到 5e-4。4. 生成与调优温度参数、重复惩罚和几个必调的推理技巧4.1 用温度采样控制生成多样性模型训练完推理时不是直接取 argmax那样每次结果都一样。常用做法是按概率分布采样并用温度参数temperature调节分布陡峭程度。def generate_text(model, start_string, char2idx, idx2char, num_generate20, temperature0.8): input_eval [char2idx[s] for s in start_string] input_eval tf.expand_dims(input_eval, 0) text_generated [] for _ in range(num_generate): predictions model(input_eval) predictions tf.squeeze(predictions, 0) # 温度缩放 predictions predictions / temperature predicted_id tf.random.categorical( predictions, num_samples1)[-1, 0].numpy() input_eval tf.expand_dims([predicted_id], 0) text_generated.append(idx2char[predicted_id]) return start_string .join(text_generated)逻辑说明temperature小于 1 让分布更尖锐生成更保守、更通顺大于 1 更随机、更有创意但容易出乱码。唐诗生成我一般用 0.6 到 0.8太低会重复太高会跑偏。tf.random.categorical按概率采样num_samples1每次取一个字符。注意这里每次只喂最后一个字符因为 GRU 内部状态没有跨调用保留如果要保留上下文需要把状态传出来但那样代码复杂对短诗没必要。4.2 重复惩罚与强制押韵实际生成时经常遇到“一句里同一个字出现三次”的情况可以在 logits 上做重复惩罚。def generate_with_penalty(model, start_string, char2idx, idx2char, num_generate20, temperature0.8, penalty1.2): input_eval [char2idx[s] for s in start_string] input_eval tf.expand_dims(input_eval, 0) text_generated [] recent_chars list(start_string) for _ in range(num_generate): predictions model(input_eval) predictions tf.squeeze(predictions, 0) predictions predictions / temperature # 对最近出现过的字符降低 logit for c in set(recent_chars[-5:]): predictions[char2idx[c]] / penalty predicted_id tf.random.categorical( predictions, num_samples1)[-1, 0].numpy() input_eval tf.expand_dims([predicted_id], 0) char idx2char[predicted_id] text_generated.append(char) recent_chars.append(char) return start_string .join(text_generated)逻辑说明penalty大于 1 时最近 5 个字符的 logit 被除概率降低减少重复。recent_chars维护最近字符列表只惩罚短窗口内的重复不影响长距离用字。押韵方面可以在生成到句尾时从候选字里筛选韵母相同的字但这需要额外韵表属于进阶玩法。参数上penalty取 1.1 到 1.3 比较温和超过 1.5 会导致句子不通顺。4.3 用验证集困惑度判断模型好坏生成结果主观性强需要一个客观指标。困惑度perplexity是语言模型常用指标等于交叉熵损失的指数。def compute_perplexity(model, dataset): total_loss 0.0 total_batches 0 for x, y in dataset: loss model.evaluate(x, y, verbose0) total_loss loss[0] total_batches 1 avg_loss total_loss / total_batches return np.exp(avg_loss)逻辑说明困惑度越低模型对下一个字的预测越准。唐诗字符级模型困惑度通常在 20 到 50 之间低于 20 说明学得不错高于 80 基本是欠拟合。注意要在验证集上算不能拿训练集否则指标虚高。如果训练集困惑度低但验证集高就是过拟合需要加 dropout 或减少参数量。5. 避坑与排查训练不收敛、生成乱码、显存爆炸的 5 个真实记录5.1 现象loss 一直停在 5.5 左右不降原因词表太大或数据没打乱模型每次看到相同顺序的样本梯度方向单一。解决检查shuffle的buffer_size是否足够大至少设为数据总量的十分之一同时确认data里没有大量重复句子去重后再训练。5.2 现象生成的诗全是同一个字原因温度设得太低或者模型过拟合到只预测高频字。解决把temperature调到 0.8 以上加重复惩罚如果还不行说明模型容量不够或训练不足把rnn_units从 512 加到 1024多跑 20 个 epoch。5.3 现象训练到一半显存爆了原因batch_size太大或者seq_length设得过长导致激活值占用高。解决把batch_size减半seq_length从 28 降到 20如果还爆把embedding_dim和rnn_units各降一半先跑通再调大。5.4 现象加载 checkpoint 后生成结果和训练时不一样原因保存的是save_weights_onlyTrue加载时只恢复了权重但模型结构需要重新定义且必须完全一致。解决先实例化同样的模型再model.load_weights(ckpt_50)确保vocab_size、embedding_dim、rnn_units和训练时一模一样差一个参数都会报错或结果错乱。5.5 现象生成的句子有标点或英文原因语料清洗不彻底混入了非汉字字符。解决在构建词表前加一层过滤只保留\u4e00到\u9fff范围内的字符其他一律替换成空格或删除。这个坑很隐蔽因为训练时不会报错只有生成时才会暴露。6. 从能跑到好用把唐诗生成接进实际工作流的两个进阶技巧第一个技巧是用前缀约束做“半自动创作”。你给模型一个开头比如“春江”让它续写但限制每句字数。实现方式是在生成循环里维护当前句长度到 5 或 7 就强制插入换行符并把换行符作为下一个输入。这样生成的就是规整的五言或七言而不是一长串。换行符需要提前加入词表训练语料里每首诗末尾也加上换行模型才能学会句读。第二个技巧是用多个模型做集成采样。训练 3 到 5 个不同初始化或不同超参的模型生成时每个模型给出概率分布取平均后再采样。这样能明显降低单模型跑偏的概率诗句更稳。代价是推理时间翻倍但唐诗生成本来就不要求实时完全可接受。我一般用 3 个模型temperature统一 0.7重复惩罚 1.2效果比单模型好一截。验证方法上除了困惑度我习惯人工看 20 首生成的诗统计“通顺句占比”和“押韵句占比”。通顺句占比超过 70%、押韵句占比超过 50%这个模型就值得保留。如果低于这个线回去调rnn_units或加数据。最后说个习惯每次实验都固定随机种子tf.random.set_seed(42)和np.random.seed(42)都加上不然你复现不了自己的结果调参就是玄学。希望帮到你。本文还有配套的精品资源点击获取