简介:这是一份面向计算机专业本科生的深度学习实战项目资源,聚焦唐诗自动生成任务,适用于课程设计、期末大作业及NLP入门实践。项目基于TensorFlow 2.x框架构建LSTM-RNN模型,完整实现数据预处理、模型训练、古诗生成与结果评估全流程,兼顾理论理解与工程落地能力培养。压缩包共11个文件,含2个核心Python源码(data_utils.py、config.py)、2个Jupyter Notebook(poem_model.ipynb含训练与推理代码)、2个文本文件(poetry.txt语料库与out.txt生成示例)、1个README.md说明文档,以及配置与缓存相关文件,整体5.32MB,结构清晰、模块解耦,便于学习者逐层理解RNN文本生成机制。已有138人下载学习,配套文档详述模型原理、参数调优策略与常见报错解决方案,可直接部署运行并支持个性化修改,是少有的高分(98分)通过、导师认可的可复现教学级项目。 期末大作业这块,我一直觉得"唐诗生成"是个被低估的好选题。它看起来像是个文科生做的文艺项目,但实际上手之后你会发现,它把深度学习中自然语言处理的整个闭环都走了一遍:数据清洗、文本张量化、Embedding、LSTM循环网络、采样策略、训练调参,一个不落。而且TensorFlow作为后端框架,生态成熟、教程多、Debug信息友好,用LSTM-RNN去逐字生成唐诗,既能讲清楚模型原理,又能跑出让人眼前一亮的demo。这个项目我前后带过几个学弟学妹复现,也自己完整重写过一版Python源码加文档,今天就把整个思路、代码设计、踩坑记录和答辩要点一次说清楚。
如果你正在纠结期末大作业选什么方向,或者说你已经在做类似题目但对模型结构和数据预处理还有疑问,这篇内容应该能帮你省掉不少走弯路的时间。我会按项目规划和数据准备、模型结构设计、训练配置、生成效果调优、答辩常见追问这几个维度去拆,尽量把一个"能跑、能讲、能拿分"的完整项目模板给你。
1. 为什么选"唐诗生成"作为深度学习期末项目:一个性价比极高的选题
先说结论:在同等时间投入下,唐诗生成是我见过的最容易做出"别人看不懂但又觉得你好厉害"效果的项目。你不需要很大的算力,不需要下载几个T的数据集,不需要部署线上服务,只要一台普通笔记本,CPU也能完成训练和推理。但它的技术覆盖面却一点都不缩水。
从技术栈的完整性看,这个项目覆盖了深度学习入门到进阶的大部分核心知识点:
- 文本数据的采集与清洗:如何处理中文古体诗文本,统一编码、去噪、去特殊字符
- 字符级(char-level)NLP建模:不再是词级别,而是直接对单个汉字建模,非常考验模型对上下文依赖的捕捉能力
- Embedding层的使用与理解:怎么把离散字符映射成稠密向量,Embedding对训练收敛的影响
- LSTM / RNN的原理与实战:为什么RNN会有梯度消失,LSTM通过门控机制怎么缓解
- 训练策略:学习率设置、序列长度选择、损失函数设计、模型保存与恢复
- 生成端(Inference)的采样策略:贪心解码、温度参数调整、Top-k采样等
从展示效果看,它的"观赏性"极强。
你想象一下答辩现场,其他同学展示的是图片分类准确率曲线,或者房价预测的MSE(平均平方误差)变化——你直接跑一句"海暗三山雨,花明五岭春"出来,老师的第一反应就会是"这真是模型自己写的?"如果再用温度参数调一调,生成一批风格各异的句子,整个项目的记忆点就直接拉满。
从期末大作业的实际约束看,唐诗语料天然地适合小规模实验。
全唐诗虽然收录了近五万首,但每首诗的长度都很短。期末项目你根本不需要把五万首全塞进去,精选几百首到两三千首,总字符量在几十万到一两百万这个量级就足够了。对比那些动辄需要长时间训练的语言模型项目,这个体量在CPU上吃两三个小时就能看到比较像样的生成效果,换成GPU或者云平台更是事半功倍。
我见过太多人选了NLP方向之后又不自量力地去跑大型语料,结果训练一周、效果全无、代码崩溃,最后只能拿个半成品去答辩。唐诗生成这个题恰恰相反,它能让你在有限的时间和算力预算内拿到完整且有趣的结果,留出充裕时间写文档、做实验对比,何乐而不为。
2. 掌握LSTM-RNN的核心原理:为什么它能写出符合语感的唐诗
要写好这个项目,光会调用TensorFlow的API不够,你至少得能用大白话把LSTM为什么适合文本生成这件事讲明白。答辩的时候老师最常问的问题就是:"你为什么要用LSTM,它比传统RNN好在哪?"
2.1 从RNN说起:循环结构如何记忆序列
文本本质上是一个序列数据。你在生成第n个字的时候,前面的n-1个字都是上下文。传统的全连接网络做不到这一点,因为你输入一个词,它只能输出一个结果,完全无法处理变长的历史信息。
RNN(循环神经网络)的核心思想是在时间步之间共享权重。它把上一步的隐藏状态 作为一个额外的输入传给当前步。数学上,每一步的隐藏状态可以简写成h_t = f(x_t, h_{t-1}),相当于网络在一步步"阅读"输入,同时把自己的理解压缩在一个隐藏向量里。
但在实践中,这种结构在处理长序列时会出现梯度消失或梯度爆炸。因为梯度在时间维度上反复相乘,步数一多,要么权重更新幅度消失,要么剧烈震荡。你用RNN去生成两句以上的诗,大概率越到后面越胡言乱语,因为"前一句的开头"根本传不到现在这一步。
2.2 LSTM的门控机制:让记忆可以"被选择性地保留和遗忘"
LSTM(长短期记忆网络)在RNN的基础上加了三个门:遗忘门、输入门和输出门。我把这三个门用生活化的方式解释:
- 遗忘门:决定上一个记忆状态里哪些信息要丢弃。比如模型读到"春"之后,可能希望把之前"冬"的某些信息冲淡,这里的权重是训练出来的。
- 输入门:决定当前输入里哪些信息值得写入记忆。相当于"我觉得'月'这个词出现了,应该把它记到长期记忆里"。
- 输出门:决定当前应该从记忆中读出多少内容用于生成当前输出。可以理解为"我要结合记忆和当前输入,决定下一个字最可能是什么"。
这套机制让信息能够跨越很长时间步被保留。诗里经常出现"上句的意象影响下句的物象"这种事情,比如"明月松间照"后面大概率接"清泉石上流",两个分句之间存在远距离的语义和结构依赖。LSTM的遗忘门和输入门协同工作,可以在一定程度上捕捉这种跨越若干字的关联。
2.3 为什么是字符级,而不是词级
中文古诗尤其是近体诗,字数高度规整,五言或七言。如果你做词级模型,分词本身就是一个麻烦——古汉语分词和现代汉语分词不完全一样,"床前明月光"这种句子怎么切?"床前"算是两个词还是一个词组?而且末级词表会变得非常大,小语料下很难训练充分。
字符级模型的优势在于:
- 字表规模小,一般一两千个字就能覆盖常用语料,Embedding矩阵也小,训练很快
- 天然规避分词错误
- 更能展现模型对韵律和搭配的学习能力——它不是在套固定词组,而是在统计字符共现模式
在代码实现中,你只需要维护两个字典:char_to_id(字符到索引)和id_to_char(索引到字符)。生成的时候输出概率分布,取一个索引,反查字典得到汉字。
然后整个模型就在做一件事:给定前面的字序列,预测下一个字的概率分布。这和当代大语言模型的next-token prediction本质上是一个思路,只不过我们用的是全连接softmax,没有用Transformer那些复杂结构。
2.4 LSTM在古诗生成任务中的实际能力边界
说完了原理,我也得泼一盆冷水提醒你:LSTM相对轻量,它学到的更多是"风格和意象的搭配",而不是真正的格律规则和主题一致性。
比如模型可能会生成出"风来花欲语,春去鸟还啼"这种意象很协调的句子,但它可能通篇都在写"花鸟风月",难以围绕一个明确的景物或情绪线索去写。所以你在期末报告里最好把这个边界写清楚——模型的本质是在学习字符级别的概率分布,不是在理解诗词内涵。这种诚实的定性能让你在技术追问环节更经得起考验。
3. 数据准备:如何把唐诗语料变成模型能吃的张量
这一步我愿称之为整个项目里最"脏活累活"但最容易出彩的部分。数据处理的质量直接决定训练曲线能不能收敛、生成结果像不像话。
3.1 语料选择与清洗
我常用的做法是去GitHub上找一个整合过的全唐诗txt语料(就是那种几百KB到一两MB的纯文本文件),里面每行可能是一首诗,或者是带标题的诗。你先别急着写模型,先写个数据探查函数看看这些原文长什么样。
大部分公开语料会包含:
- 标题、作者、序言、注释
- 全角半角混用、中文标点
- 部分HTML标签(如果你爬下来的没清理干净)
- 空行、重复诗、乱码行
我的清洗策略很简单:只保留汉字、常见中文标点(逗号、句号、感叹号、问号、顿号)、换行符。其他数字、英文、括号、空白符统统丢掉。对于这个项目,字数太少或者太长的诗也可以直接过滤,比如我一般只保留单句长度在5到7个字的五言和七言诗。清洗完毕后再做一次去重,防止语料里同一首诗出现多次导致模型背课文。
3.2 建立字表:控制Vocabulary规模
清洗完之后写个Counter统计所有字符出现次数。你会看到"之"、"人"、"山"、"水"、"风"、"月"这些高频字稳居前列。字表大小不用贪全,把出现次数低于某个阈值(比如5次或10次)的字直接归为Unknown Token,比如:
from collections import Counter chars = [ch for line in corpus for ch in line] freq = Counter(chars) vocab = [ch for ch, cnt in freq.items() if cnt >= 5] word2idx = {ch: i for i, ch in enumerate(vocab)} idx2word = {i: ch for ch, i in word2idx.items()}这样字表通常控制在1500到3000之间。字表太大,Embedding层参数爆炸,训练变慢;字表太小,低频字全被丢弃,生成时容易变成"死语料"。
3.3 构造训练样本:输入序列和目标序列
下一步是把语料转成一个超长的整数序列,然后切分成等长的样本。一个典型的做法是设置一个sequence length(我用的是30到50个字符),从超长序列里按顺序或随机切出很多个长度为seq_len的小窗口。
对每个窗口,输入是前seq_len-1个字,标签是后移一位的seq_len-1个字。也就是说:
- 输入:"床前明月光,疑是地上"
- 标签:"前明月光,疑是地上霜"
模型的任务就是看到"床前明月光,疑是地上"之后,最大化"前"的预测概率,然后继续往后推。如果你希望模型能学会换行,别忘了在诗与诗之间拼接的时候要保留换行符。
3.4 构建tf.data管道:避免一次性加载所有数据
训练样本量其实不小。假设总字符量有80万个,seq_len=40,按步长1去滑动窗口,能得到近80万个训练序列。直接转换成NumPy数组一次性塞内存可能还能扛,但加载到GPU训练时带宽会拖后腿。更优雅的方式是用TensorFlow的tf.data API:
import tensorflow as tf dataset = tf.data.Dataset.from_tensor_slices((inputs, labels)) dataset = dataset.shuffle(buffer_size=10000).batch(batch_size).prefetch(tf.data.AUTOTUNE)Buffer_size不用太大,10万左右已经够随机性了。Prefetch是必须的,它让CPU在GPU训练的同时预取下一批数据,不然训练时周期性卡顿会非常明显。
提示:如果你的语料比较小(比如只有几十万字),直接把所有训练对读入内存做shuffle也可以,不用过度设计。期末项目优先保证代码简洁、可解释。
4. 模型结构设计:一个可解释、参数可控的LSTM文本生成器
模型设计这个环节最关键的一点是"你的网络容量要和数据量匹配"。唐诗生成这个任务,数据量不大,字符表也不大,你不需要搞一个深度很大的网络。盲目堆层数只会让训练时间变长,而且在小数据上更容易过拟合——生成结果会变成背诵语料里的原句,而不是原创。
4.1 推荐的基础结构
我最后定的模型结构大概是这样的:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout model = Sequential([ Embedding(input_dim=len(word2idx), output_dim=256, mask_zero=False), LSTM(256, return_sequences=True, dropout=0.2, recurrent_dropout=0.2), LSTM(256, dropout=0.2, recurrent_dropout=0.2), Dense(len(word2idx), activation='softmax') ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics=['accuracy'] )这里有几个关键参数值得仔细斟酌:
- Embedding维度:128到256之间足够。唐诗语料大概能覆盖两三千个不重复的字,用256维的向量去表示每个字,容量上没问题。如果你用64维,训练会更快,但模型可能学不到足够的语义差异。
- LSTM层数:两层足够了。第一层建模低层字符搭配(比如常见的连绵词),第二层建模更高层级的上下文关系。再多层要么过拟合要么训练不稳定。
- Dropout:embedding 和后接层之间以及循环单元内都可以加一点dropout,我用0.2。这个值能有效降低过拟合,也不会让收敛太慢。
这里特别提醒一下:recurrent_dropout在CPU上会跑得比较慢,如果你是CPU训练,可以把它设为0或0.1,效果差别不大但速度提升明显。
4.2 为什么Embedding对生成质量至关重要
早期做法是用one-hot向量表示字符,比如字表有3000个字,每个字符就是一个3000维的稀疏向量。这样的表示有两个问题:维度灾难和向量之间没有语义关系。你在Embedding层训练结束后,可以在文档里展示几个常见汉字的余弦相似度,"山"和"水"在学习之后很可能会被映射到相近的空间位置,因为你语料里它们经常同时出现在"山水"这样的搭配中。
Embedding层实际上就是查表操作,把整数索引映射为稠密向量,然后在训练过程中不断调整向量方向,让语义相近的汉字在隐空间里靠得更近。
4.3 损失函数和评估指标的选择逻辑
预测下一个字符,本质上是一个多分类问题,类别数是字表大小。用SparseCategoricalCrossentropy是标准的做法,因为它内部做了softmax和交叉熵的组合,数值稳定更重要的一点是,labels是整数索引而不是one-hot,能省不少内存。
Accuracy在这里可以做参考,但不要迷信。因为如果有些字的出现频率极高(比如"人"、"风"、"云"),即使模型总是猜这些高频字,Acc也会显得不太低。真正有价值的评估还是去读生成出来的诗句。
- 训练过程中容易忽略的几个关键配置
这一个部分属于实操经验,你按文档敲完代码之后,会发现真正让训练顺利进行的反而是这些配置细节。
5.1 模型保存与早停
期末项目不比发论文,你不需要训练到最优才罢手。但训练过程中模型中断是常事(断电、内存爆掉、或者CPU训练时间超出预期),所以每一步都要能够恢复。
我在代码里设置的是每训练一个epoch,就把整个模型保存到checkpoint目录,用callback实现:
checkpoint_callback = tf.keras.callbacks.ModelCheckpoint( 'checkpoints/poem_{epoch:02d}.ckpt', save_weights_only=False, save_best_only=True, monitor='loss' ) early_stopping = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=5, restore_best_weights=True)如果训练损失连续好几个epoch没有下降,EarlyStopping会自动停下来,避免你用CPU继续空转。
5.2 学习率与优化器的调整
Adam配0.001是我们常见的起点,但这个任务如果你发现loss下降得太慢,可以把lr调大到0.002或0.003;如果loss震荡得很厉害,就降到0.0005。还有一个实用技巧是配合ReduceLROnPlateau自动降学习率:
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='loss', factor=0.5, patience=2)它会在loss连续两个epoch不降的时候把学习率减半,省去手动盯训练的精力。
5.3 训练时长的预估
这里给大家一个相对可参考的经验数据:我在8GB显存的入门级GPU上,用60万字左右的语料,字号表2000,seq_len=40,batch_size=64,两层LSTM(每层256单元),训练一个epoch大概需要几十秒到一两分钟。30到50个epoch之后,生成结果就比较稳定了。CPU训练的话,一个epoch可能要几分钟到十几分钟,你也别慌,反正自动保存了,随时可以停。
5.4 过一个阶段做一次"中间生成"检查
不要等训练全部结束时才去看效果。我在每个epoch或每两个epoch都会跑一个小函数,用固定种子字符(比如"春")生成一段诗,打印出来。这样你一眼就能判断模型是不是已经进入"背诵模式"了:
- 如果生成内容几乎逐字复现语料里的原句,说明过拟合了,可以考虑降低模型容量、加大Dropout或提前停止
- 如果生成内容毫无语义、随机乱喷,说明欠拟合,要么增加训练轮数,要么模型容量太小
这一步建议写进文档里,答辩的时候展示"训练过程中的生成效果变化",比单纯展示loss曲线更有说服力。
6. 生成阶段的采样策略:从"必然"到"有惊喜"的调参经验
训练完成后,进入了最有意思的部分——采样生成。很多人这里踩坑最深:模型明明loss很低,但生成出来的诗又呆板又重复,让人怀疑模型学废了。绝大多数情况是你采样策略没调好。
6.1 贪心解码 vs 随机采样
最简单的生成方式是每个时间步都选择概率最高的那一个字。这种贪心算法的结果通常非常"安全",但容易出现死循环——模型会高频重复某个常见搭配,比如连续生成"花"、"春"、"风"这种高频字,导致整首诗读起来很平。
随机采样的做法是根据概率分布采样:
probs = model.predict(context)[-1] next_idx = np.random.choice(len(probs), p=probs)随机性让生成结果更多样,但如果不加控制,容易跳出语义连贯性。解决办法是引入temperature参数:
def sample_with_temperature(probs, temperature=0.8): probs = np.log(probs + 1e-8) / temperature probs = np.exp(probs) / np.sum(np.exp(probs)) return np.random.choice(len(probs), p=probs)温度值怎么调?我的经验是:
- temperature在0.4到0.6之间,生成句子更贴近语料中的常见搭配,稳妥但可能无聊
- temperature在0.8到1.0之间,有惊喜、偶尔有错别字或不通顺,但整体基调是对的
- temperature超过1.2,基本开始乱说
我在期末作业里通常会展示同一个种子词在不同温度下的3组生成结果,用表格对比,观感非常专业。
6.2 如何控制生成长度和格式
诗不是无限生成的。你需要在两个层面做限制:
第一,长度控制。设定一个最大生成字符数,比如七言绝句的话,28个字加上标点和换行,最多生成40个字符就该停了。
第二,格式终止。这里有个小技巧:你可以指定遇到句号或感叹号时停止,确保生成的句子是一个完整分句;或者指定如果已经生成了4个分句就结束。这样能得到一首四句的"绝句",而不是一段不知道多长的口水话。
6.3 Top-k采样:防止采样走偏
Temperature让分布更尖锐或更平滑,但某些时刻概率最高的几个字之间差距很大,如果你每次都从整个字表里采样,偶尔会抽到那些模型完全没把握的尾部分布里的字。一个实用的优化是把采样范围限制在概率最高的k个字里,其余字清零再归一化:
def top_k_sample(probs, k=10, temperature=0.8): probs = np.log(probs + 1e-8) / temperature probs = np.exp(probs) / np.sum(np.exp(probs)) top_k_idx = np.argsort(probs)[-k:] mask = np.zeros_like(probs) mask[top_k_idx] = 1 probs = probs * mask probs = probs / np.sum(probs) return np.random.choice(len(probs), p=probs)我把Top-k设为10到20,配合temperature=0.8,生成的句子的"灵性"明显提升,重复率也下降不少。这个参数强烈建议你写进代码并做对照实验。
7. 期末答辩避坑指南:哪些细节能让你加分,哪些会招来追问
到了期末,项目代码能不能跑是大前提,但答辩环节能不能把老师问倒才是最关键的。以下这几个问题我基本每次都被问到,提前准备好方案,现场绝对稳住。
7.1 最容易追问的问题:"你的模型真的理解唐诗的平仄和意象吗?"
诚实回答,不硬撑:模型并不理解"平仄",也不理解"意象",它学的是字符之间的统计共现关系。比如它可能知道"月"后面经常接"明"、"落"、"照",因为语料里"明月"、"落月"、"月照"频繁出现,但它不理解月亮是思乡意象,更不理解对仗的格律规则。如果你把这个边界说清楚,然后补充"若要增强主题一致性,可以考虑引入预训练语言模型或添加格律特征"这种改进思路,反而是加分项。
7.2 为什么用字符级而不是词级?——展示思考深度
这个问题我在第2部分详细解释过:古汉语分词困难、字表规模控制、序列长度分配更均匀、模型可以更细腻地捕捉字符之间的搭配。同时也要说明代价——字符级模型没有词汇语义的先验,所以它更依赖大量语料和模型容量去自行学习词汇搭配。
7.3 如果老师说这个项目太简单,怎么承接?
可以主动展示更进阶的尝试。比如:
- 加入Beam Search,在生成时保留多个候选序列,避免局部最优
- 用词向量预训练初始化Embedding
- 加入注意力机制或Transformer层做对比实验
- 引入"藏头诗"模式,锁定前几个字的生成路径
这些不一定要全部实现,但提出来会让老师看到你对深度学习的整体理解是在线的。
7.4 训练loss不断下降但生成结果却越来越差怎么办?
这是一个非常经典的过拟合问题。模型在训练集上越"懂",生成时越倾向于复述语料原句。对策很多,比如降低模型容量、增加dropout、增强数据随机性(可以在训练时随机打乱诗的顺序)、使用更早的checkpoint。你可以直接在报告里放一张"训练Loss与生成多样性对比"的曲线图,说明你意识到了这个问题并做了处理。
7.5 数据量不足的表现和应对?
如果你只用了两三百首诗,生成的句子大概率是零散字词的拼凑。这种情况下先别急着加深度模型,可以考虑数据增强手段:把每首诗按行反转、把诗与诗随机拼接、对诗句进行随机截断。这些方法对字符级模型的多样性提升非常明显,成本也低。
8. 项目文档与代码组织心得:让期末大作业"不扣一分"
最后聊一点和代码关系不大但和分数关系很大的事情——文档结构。期末大作业拼的不只是模型效果,还有你的工程组织能力。
我的建议是项目整体结构如下:
project/ ├── data/ │ ├── raw_poems.txt │ └── processed_poems.txt ├── src/ │ ├── data_processing.py │ ├── model.py │ ├── train.py │ ├── generate.py │ └── utils.py ├── checkpoints/ ├── docs/ │ ├── 项目报告.md │ └── 答辩演示.pptx └── README.md其中README.md一定要写清楚三件事:环境依赖(Python版本、TensorFlow版本)、如何运行(数据准备命令、训练命令、生成命令)、预期结果。老师拿到你的项目第一件事就是打开README,如果它能一步步跑通,印象分直接拉满。
环境依赖这里特别提醒一个坑:TensorFlow版本差异非常大。如果你用的是TensorFlow 2.x,老教程里很多写法(比如tf.contrib)会直接报错。建议在requirements.txt里固定版本号,比如tensorflow==2.10.0或2.18,并且注明Python版本。我自己在用2.18的时候发现有些旧的keras API被移到keras.io,所以代码里统一用tf.keras官方接口是最稳的。
项目文档的写法上,我强烈建议你按"实验报告"的逻辑而不是"源码解析"的逻辑来写:先讲问题背景,再讲数据怎么处理的,然后讲模型架构为什么这么选,接着是训练曲线和生成效果展示,最后是局限与改进方向。这份文档千万不要写成代码逐行注释,老师要看的是你的思考过程。
以上就是我从选题、原理、数据、模型、训练、生成到答辩准备的全流程复盘。这个项目的"难"不在于某一步有多深奥,而在于串起整个流程时各种细节的交互。如果你从头到尾走一遍,你会对TensorFlow的Tensor、Embedding、LSTM、Softmax、采样这些概念建立非常直观的体感——这比背诵任何教科书都管用。我到现在还记得第一次看到模型自动生成出"江流天地外,山色有无中"这种句子时的震撼感,虽然它只是偶然复现了语料中的名句,但那瞬间你会真切地感受到:深度学习确实从数据里"学到"了某些关于文字的东西。这种体验,才是你做这个项目最值得收藏的部分。
本文还有配套的精品资源,点击获取