
1. 项目全貌与核心思路拆解每年到了毕业设计季总有一大批同学盯着“基于深度学习的文本生成”这个方向不放。古诗词自动生成算是其中最有辨识度、也最容易讲清楚“技术难点”的题目之一。它既不像对话机器人那样需要海量语料和复杂的解码策略也不像机器翻译那样需要并行语料对齐同时又具备完整的“数据获取—模型训练—效果评估—系统展示”链路非常适合作为高校计算机专业本科阶段的综合实践项目。先说结论这个项目本质上是一个典型的字符级或词语级语言模型训练任务。核心目标不是让模型“背下”已有的诗而是让模型从大量古诗文本中学习到汉字之间的搭配规律、平仄节奏和语义倾向然后在你给出一个起始字或者起始的几个字之后模型能一个一个地把后续的汉字“推测”出来。LSTM在这里扮演的角色是一个带记忆的序列建模器——它能够捕捉到前文上下文对下一个字选择的约束关系。我在实际带模拟项目X的时候发现很多同学对这个项目的理解容易陷入两个误区。第一个误区是觉得“生成古诗写诗”于是拼命追求生成结果的文学性认为模型输出的每一句都必须押韵合辙、意境优美。这个期待本身就不现实。本科毕设的定位是“算法实现及系统实现”重点在“LSTM模型的构建是否规范”“训练流程是否完整”“系统功能是否可用”而不是真的要训练出一个李白。第二个误区是觉得“反正PyTorch里已经封装好了LSTM层我只要调一下接口就行”。如果抱着这种想法那答辩时老师一问“LSTM的三个门分别起什么作用”“梯度消失是怎么被缓解的”你就只能站在原地沉默场面会非常尴尬。顺着这个思路下文我会把整个项目拆成六块来讲模型选型的“为什么”、数据预处理怎么做、LSTM网络的每一层怎么设计、训练与调参的关键点、生成策略的选择、最后是Web系统的搭建与功能展示。每一块我都会结合自己实际跑过的踩坑经历来补充尽量让这篇内容能直接当做你写毕业设计文档时的“素材库”。2. LSTM模型选型为什么是它而不是RNN或Transformer2.1 从普通RNN说起短时记忆的局限性要理解为什么选LSTM得先回到最原始的循环神经网络。RNN的设计思想非常直观每一个时间步的输入不仅包含当前时刻的外部输入 (x_t)还包含上一个时间步传递下来的隐藏状态 (h_{t-1})。这样网络就具备了“记忆”能力可以处理任意长度的序列。但是RNN有一个致命的短板——梯度消失。在反向传播过程中梯度需要沿着时间步逐层回传每一步都要乘以权重矩阵。如果你用公式去推会发现在时间维度上梯度实际上是在做连乘操作。如果权重矩阵的特征值小于1连乘多次之后梯度会指数级衰减很快变成接近0的数。结果是网络无法学习到长距离依赖关系比如你写一首五言律诗第一句的“月”可能要到第八句才呼应这种跨越几十个字符的关联普通RNN基本学不到。2.2 LSTM怎么解决“记忆”问题LSTM长短期记忆网络的核心贡献是引入了一条贯穿时间步的“细胞状态”通路(C_t)。你可以把它想象成一条传送带信息在这条传送带上可以几乎没有损耗地流动。传送带旁边站着三个“门卫”它们决定信息的存、取、丢。遗忘门决定旧的细胞状态中哪些信息要被丢弃。它读取 (h_{t-1}) 和 (x_t)输出一个0到1之间的值1表示“完全保留”0表示“完全丢弃”。输入门决定新的信息中哪些部分值得写入细胞状态。它由两部分组成一个Sigmoid层决定“更新哪些值”一个Tanh层生成候选值向量。输出门决定当前细胞状态中哪些信息需要输出到当前隐藏状态 (h_t)这个隐藏状态会同时用于生成当前预测和传递给下一个时间步。正是这种“线性传送带非线性门控”的结构让LSTM能够在长序列上保持稳定的梯度流动。这也是为什么LSTM在2010年代中期到Transformer全面崛起之前一直是NLP序列建模的事实标准。2.3 为什么不用Transformer我知道很多同学会问现在大模型不都是Transformer架构吗为什么古诗词生成还要用LSTM原因有三点。第一古诗的长度很短一首五言绝句只有20个字七言律诗也才56个字LSTM的“长记忆”优势在这个长度范围内完全够用。Transformer的全局注意力机制在这里属于“杀鸡用牛刀”反而增加了实现和调参的复杂度。第二LSTM的生成过程是严格按时间步顺序解码的其序列生成逻辑天然适合“接龙式”写诗。Transformer的Self-Attention在训练时可以做并行但在推理时如果采用自回归方式本质上也需要逐步生成而且每一步都要重新计算之前所有位置的注意力权重计算开销更大。第三从毕设答辩的角度来说LSTM更容易把“门控机制”“梯度缓解原理”“序列建模思想”这些知识点讲透这是评审老师更愿意看到的“算法理解深度”。当然我也建议你在论文里加一小段“为什么不选Transformer”的比较分析这不仅能体现你调研过前沿技术还能让老师觉得你不是只会调库。3. 数据准备与预处理决定了模型的上限3.1 语料来源与规模选择古诗词语料最常用的有两个来源全唐诗、全宋词。这两个数据集在GitHub和各种开源数据平台上都能找到整理好的版本。全唐诗大约有近五万首诗体涵盖五言绝句、七言绝句、五言律诗、七言律诗、古体诗等全宋词约两万首。如果你的机器配置一般比如笔记本的GTX 1660或者1650建议不需要上全量语料抽取其中诗体格式规范的1万到2万首即可。这里要特别强调一个预处理信念语料质量远重要于语料数量。我当时做模拟项目X时吃过一次亏最开始用的是未经清洗的全量数据里面混入了不少作者的题序、注解、生僻异体字训练出来的模型经常在诗中间突然蹦出一个“右”“序”“注”之类的字显然是模型从题序里学到了错误的字符搭配。所以清洗的第一步就是过滤出纯五言或纯七言的诗句并且要求每一行完全由汉字、逗号、句号、感叹号、问号等基础标点组成剔除含字母、数字、非GBK字符的行。3.2 按字建模还是按词建模这是文本生成类项目里必须先回答的问题。中文NLP通常默认要做分词然后按词建模但对于古诗词我强烈建议按**单个汉字字符级**建模。原因有三。第一古诗的语言高度凝练很多词在现代汉语中是两个独立的字但在古诗里组合在一起才形成意境按词切分容易破坏这种语义单元。第二古诗中一字多义、词类活用的现象非常普遍词边界本身是模糊的硬切分的错误会被模型当作噪声学习。第三也是最重要的字符级建模的词表极小。一首五言绝句加上七个标点符号词表大约只有汉字三千多个。用字符级词表做Softmax分类维度低、训练快、生成时不会遇到“未登录词”的问题。3.3 构建训练样本滑动窗口生成序列有了清洗后的诗句文本下一步是把它们切成模型能“一口吃下”的序列样本。方法是滑窗切分。假设设定序列长度 (T 32)即每次喂给模型32个字符预测第33个字符那么对一首七言律诗从第一个字开始以步长为1或2依次截取长度为32的连续片段每个片段对应一个训练样本。我推荐的步长是1虽然样本会高度重叠但对于字符级语言模型来说重叠反而让每个字符都有机会出现在不同的上下文位置中有利于模型学到更稳健的转移规律。需要特别处理的是“句子边界”。训练时我习惯于在每首诗的句尾添加句号而不是用换行符分隔。例如一首五言绝句经过处理后变成一行纯文本“床前明月光疑是地上霜。举头望明月低头思故乡。”用这种一行一首诗的方式存储可以避免换行符被当作普通字符学习换行符在模型看来也是词表中的一个token如果出现在诗中间会非常奇怪。另外每首诗结束后填充一个特殊分隔符比如用“ ”标记一首诗的结束在生成阶段遇到这个标记就停止这样可以避免模型无限生成下去。3.4 构建词表与数值化词表Vocabulary是一个从“字/标点”到“整数索引”的映射表。构建时把所有清洗语料中出现的字符收集起来加上一个UNK未知字符和一个END结束标记组成你的词表。在模拟项目X里我的词表大小约为4500左右其中主体是常用汉字。数值化过程就是把文本中的每个字符替换成它在词表中的整数索引得到一串整数序列。接下来还要做One-Hot编码吗不需要。现在的做法通常是在模型里设置一个嵌入层nn.Embedding这个层的输入是整数索引输出是对应的稠密词向量。嵌入层的参数会在训练过程中同步更新它会自动学到每个字符在高维空间中的表示。4. 模型网络结构设计每一层都是什么意思4.1 输入层与嵌入层模型的第一层是嵌入层。用PyTorch的代码表示就是self.embedding nn.Embedding(num_embeddingsvocab_size, embedding_dimembed_dim)这里的embed_dim是词向量维度我常用的值是128或256。维度太小会让字的语义表示不够丰富维度太大则会增加参数量而且在这个数据规模下容易过拟合。嵌入层会把每个字符索引映射为一个稠密向量例如“月”这个字会被映射成一个128维的向量这个向量不是人手工设计的而是在训练过程中自动调整出来的。从嵌入层输出的形状是 ([batch_size, seq_len, embed_dim])也就是每个时间步的字符都被换成了一个向量。这个三维张量接下来会被送进LSTM层。4.2 LSTM层隐藏维度与层数怎么定LSTM层的核心参数有两个hidden_size隐藏状态维度和num_layers堆叠层数。我在模拟项目X中使用的是单层LSTM隐藏维度为256。你可能会问为什么不用两层三层层数多了不是能学到更复杂的特征吗理论上确实如此但前提是你的数据量足够大。古诗语料清洗后通常只有几十万字符一层LSTM的参数量已经足够拟合这些数据的转移规律。层数加到两层以上不仅训练时间翻倍还容易在小数据集上过拟合——表现就是训练集损失降得很快验证集生成效果反而越来越差生成出的句子背上训练样本的痕迹太重缺乏多样性。LSTM层的输入形状是 ([batch_size, seq_len, embed_dim])输出有两个一个是所有时间步的隐藏状态序列outputs形状为 ([batch_size, seq_len, hidden_size])另一个是最后时间步的隐藏状态和细胞状态(h_n, c_n)。在语言模型任务中我们通常只需要outputs因为我们要对每一个时间步都做一个预测。4.3 全连接输出层与SoftmaxLSTM输出的hidden\_size维向量不能直接作为字符概率还需要经过一个全连接层把它映射到词表大小维度self.fc nn.Linear(hidden_size, vocab_size)这个全连接层做的事情是把每个时间步的隐藏状态“翻译”成词表中每个字符的得分Logits。最后经过Softmax就得到每个字符的概率分布。比如在当前上下文中模型觉得下一个字是“月”的概率是0.35是“风”的概率是0.2是“花”的概率是0.12。到这里你可能发现了整个模型的结构其实非常简洁嵌入层→LSTM→全连接层。这恰恰是这个项目最值得讲清楚的地方——它的核心价值不在于网络结构的复杂而在于你如何理解数据在每一层之间的形状变化以及如何根据训练效果调整超参数。4.4 损失函数与优化器选择因为是字符级的分类任务损失函数用交叉熵损失CrossEntropyLoss。PyTorch里的nn.CrossEntropyLoss在计算时会自动把模型的原始得分Logits做Softmax所以模型最后一层不需要手动加Softmax只需要输出Logits即可。这点细节很多同学容易搞混以为需要自己先Softmax再算损失结果导致梯度计算路径重复模型训练反而出问题。优化器我推荐使用Adam学习率初始值设为0.001。Adam的自适应学习率机制在NLP任务中表现非常稳定基本不需要手动做学习率衰减。如果用SGD则需要仔细调节动量参数和学习率调试成本高对小项目不划算。5. 训练过程与调参经验从Loss曲线到生成效果5.1 训练流程与代码框架整个训练流程的伪代码如下for epoch in range(num_epochs): model.train() total_loss 0 for batch in dataloader: x, y batch # x: 输入序列, y: 目标序列右移一位 optimizer.zero_grad() logits, _ model(x) # 形状 [batch, seq_len, vocab_size] loss criterion(logits.view(-1, vocab_size), y.view(-1)) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch}: loss {total_loss / len(dataloader):.4f})这里有个关键细节构建训练样本时输入序列和目标序列是错位一位的。例如输入是“床前明月光疑”目标输出就应该是“前明月光疑是”。也就是说模型看到前T个字符目标是预测第T1个字符推理时模型输出每个位置对“下一个字符”的预测概率然后和真实的下一个字符算交叉熵损失。5.2 损失值多少算正常根据我的经验词表大小约4500时如果随机初始化权重初始损失约等于 (\ln(4500) \approx 8.41)。训练若干轮后损失会逐步下降。当损失降到6.0左右时模型已经能够生成语法上大体通顺的句子降到5.5以下时生成的诗句在字面组合上已经比较自然了有时会偶然出现一两个“像样”的句子。但如果追求更高质量通常需要继续训练到4.8~5.2区间这取决于你的语料规模。如果模型损失降不下去一直在5.8左右徘徊不要急着加层数或扩大隐藏维度。先检查三个地方一是学习率是否合适如果过大会导致loss震荡不下降二是数据是否干净有没有混入脏字符三是序列长度是否过长导致梯度难以传回。这三点是文本生成任务中最常见的收敛瓶颈。5.3 过拟合与欠拟合的判断方法古诗词语料规模不大很容易出现过拟合。判断方法很简单观察训练集损失和验证集损失或生成效果的差距。如果训练损失持续下降但验证损失回升说明模型开始“背诗”了——它记住了训练集中的特定字序而不是泛化出古诗的普遍规律。这时候应该提高Dropout比例LSTM层的Dropout常用0.3~0.5或者减小模型规模。反过来如果训练损失和验证损失都很高比如始终在7.0以上那是欠拟合需要增加模型容量提高隐藏维度或层数或者增大语料规模。训练轮次方面我一般设置20到50轮Epoch配合早停策略如果连续5轮验证损失没有下降就停止训练并保存历史最优模型。这个策略能帮你节省大量时间尤其是做实验对比不同超参数时。6. 生成策略从贪心到Beam Search再到随机采样6.1 贪心解码最保守的选择最简单的生成方式是贪心每一步都取概率最高的那个字符作为下一个字。这种方式的优点是快速稳定但缺点非常明显——生成结果平淡、容易陷入重复循环。因为模型在局部最优处容易一直自嗨比如学到“花”后面最喜欢接“落”那它就一直“花落花落花落”循环下去。6.2 Beam Search多保留几条候选路径Beam Search的思路是每一步不只保留一个最优选择而是保留前K个最优的部分序列K称为束宽。例如K5时每一步保留5条当前概率最高的序列然后基于这5条序列扩展下一个字符再从所有扩展候选中重新选择概率最高的5条。这样避免了一开始选错一个字符就“全盘皆输”的问题生成效果比贪心稳定得多。但Beam Search也有自己的问题它倾向于生成“安全但平庸”的句子因为它在最大化整个序列的概率即更偏向常用搭配导致生成的诗缺乏个性和惊喜感。不过对于毕设系统演示来说Beam Search是性价比最高的方案至少不会频繁出现明显的语法错误。6.3 温度采样给生成增加一点“意外”如果你想看到更有意思的生成效果可以尝试Temperature Sampling温度采样。它的核心是对Softmax前的Logits除以一个温度系数 (T)。当 (T 1) 时概率分布变得更尖锐高概率更容易被选中生成更保守当 (T 1) 时分布变得更平缓低概率也有机会被选中生成更多样化。在我做的模拟项目X里实际测试下来 (T 0.8) 是个不错的折中值。(T 1.2) 时生成的句子经常出现生僻字组合虽然偶尔会有惊喜但不适合直接展示给老师看。建议在系统里给用户提供一个温度滑块让使用者自己调节这个交互设计在答辩时也是一个不错的加分点。7. 系统实现把模型装进Web界面7.1 技术选型Flask还是FastAPI模型训练好之后需要把它封装成一个可以交互的系统。最常见的做法是后端用Python Web框架前端用简单的HTMLCSSJavaScript。我这里推荐FastAPI它天生支持异步接口文档自动生成而且和PyTorch模型的加载配合得很好。如果你们学校要求比较传统用Flask也完全没有问题毕竟这个项目的核心在后端模型调用Web框架只是展示层。系统的主要接口只需要两个。第一个是“生成接口”接收用户输入的主题字比如“春”和参数诗体、温度、束宽、生成长度返回一首生成的诗句。第二个是“模型信息接口”返回词表大小、LSTM层数、隐藏维度、训练损失等模型元信息方便在系统页面上展示。7.2 模型加载与推理时的关键细节模型训练和推理的代码路径要分开。训练时模型处于train模式Dropout生效推理时必须调用model.eval()切换为评估模式Dropout关闭。还有一个容易被忽略的点是推理时要确保输入张量在相同的设备上CPU或GPU——如果模型在GPU上输入必须在GPU上否则会报设备不匹配的错。生成时整个过程是一个自动回归循环。给定起始字“春”先查词表得到它的索引输入模型拿到的第一个预测分布采样得到第二个字然后把“春”和第二个字拼接起来作为新的输入继续预测第三个字。以此类推直到生成到预设长度或遇到END标记。7.3 页面展示与交互设计前端页面建议做成一个简洁的“古诗词生成器”风格一个输入框用于填主题字一个下拉框用于选择诗体五言/七言一个滑块调节“随机性”即温度一个按钮触发生成。生成结果以大号书法字体显示在页面上。还可以加一个“历史记录”功能把用户生成过的诗句保存在本地列表中方便对比不同参数下的生成效果。我不建议在系统界面里用太花哨的前端框架因为这个项目的重点是算法实现不是页面设计。一个干净整洁的单页HTML加上基础的CSS样式就足够了。如果还想加点分可以做一个简单的“生成动画”——让诗句逐字显示模拟打字机效果这会极大地提升演示时的观感。8. 常见问题与排查技巧实录8.1 训练Loss出现NaN这是初学者最常遇到的问题之一。NaNNot a Number出现的首要原因是梯度爆炸。LSTM在时间维度上做反向传播时如果权重初始化过大或学习率过高梯度值会指数级增大最终超过浮点数表示范围变成无穷大然后计算更新参数时就变成NaN。解决办法有三个第一降低学习率比如从0.001降到0.0001第二在优化器上设置梯度裁剪PyTorch中一行代码搞定torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)第三检查语料中是否有异常字符导致数值计算出现问题比如某个词表外的字符在查表时报错。8.2 生成的结果总是重复那几句这个问题大概率是训练数据分布不均导致的。如果你的语料里七言律诗占80%五言绝句只占20%模型会更倾向于生成类似七言律诗的结构。解决方法是按诗体分层采样让五言和七言的比例接近1:1。另外训练时也可以对每首诗做随机裁剪让模型见到更多样的片段组合。8.3 生成的句子“太现代”模型的输出词汇高度依赖于训练语料。如果你混合了现代的文本比如网上的评论、新闻作为补充数据那么模型很容易学到现代汉语的表达习惯导致生成结果完全没有古风感。我实际看到过某位同学因为补充了当代成语词典模型动不动就输出“与时俱进”“科技创新”这种词场面一度非常尴尬。所以语料选择必须纯净宁少勿杂。8.4 推理速度太慢如果在CPU上运行生成一首七言律诗可能需要一两秒这个速度在演示时是可以接受的。如果遇到需要批量生成或者实时响应的场景可以把模型转为ONNX格式或TorchScript格式推理速度能提升不少。但对于本科毕设这个优化不是必需的。8.5 如何让生成结果“更好看”这里分享一个小技巧生成完之后不要急着展示可以加一个简单的“后处理规则”。比如检查生成的句子是否以句号结尾如果不是就自动补上检查每句长度是否一致如果发现相邻两句完全相同重新采样一次。这些规则虽然简单但对提升演示效果非常明显。9. 整体评价与后续扩展方向站在指导者的角度评价这个项目它的技术栈覆盖面广从序列建模原理到数据工程再到Web开发都能得到锻炼难度又刚好在本科生的可控范围内。它的确是一个低风险、高收益的毕设选题。从答辩的角度来说LSTM原理、数据预处理、生成策略这三个部分是老师最常提问的环节也是最能体现你工作量和技术理解的环节务必要提前准备透彻。如果你学有余力后续可以在几个方向上做扩展。比如把LSTM替换为GRU做对比实验GRU参数更少、训练更快或者引入Attention机制让模型在生成时能“回看”更早的关键字符还可以尝试用GPT-2的中文轻量版做迁移学习对比“从零训练”和“预训练微调”两种路线的效果差异。这些扩展都能作为论文的“未来工作”章节也能体现你已经具备了独立调研和迭代实验的能力。最后再分享一个数据层面的心得这可能是我在这个项目里最深刻的一课不要迷信公开数据集的“完整版”。全唐诗全量数据虽然大但里面有大量重复、残缺、作者署名和注释信息直接拿来训练既浪费算力还会让模型学到很多噪声。我当时把数据清洗脚本反复迭代了三个版本每一次清洗后模型的生成质量都有肉眼可见的提升。数据工程在深度学习项目里的重要性往往被初学者严重低估。