基于LSTM的古诗词自动生成:从模型训练到Web系统实现全攻略 每年这个季节都会有人拿着同一个题目来问基于LSTM的古诗词自动生成算法与系统实现能不能做我的回答基本始终如一——能做而且这个题目在NLP入门项目里算得上性价比极高。它本质是训练一个语言模型让模型学会“从一个字猜到下一个字”的统计规律你给它“床前明月”它要能接出“光”你给它“春江”它要能续成一首完整的长诗。更关键的是它不止停留在算法脚本层面还能包成一个Web系统后端挂训练好的LSTM模型前端留一个输入框和展示区。如果你正在做课程设计或毕业设计这篇文章基本就是一份可以直接照做的完整操作清单。我见过不少同学在这类题目上翻车翻法还很一致模型代码跑通了系统页面也做出来了但一演示就露馅。要么生成结果驴唇不对马嘴要么现场模型加载卡死要么老师问一句“为什么选LSTM而不是Transformer”就答不上来。其实这些问题都不是技术难点而是整个项目的设计顺序乱了。这篇我就按自己带做这类项目的完整流程讲一遍从数据预处理、模型训练、生成策略到Flask封装再到答辩前必须准备的东西尽量写成能直接抄作业的格式。1. 项目整体设计与思路拆解很多人的第一反应是打开编辑器就写网络结构训完模型才发现数据脏得没法看或者做完算法不知道怎么变成系统。我更建议反过来先想清楚交付物是什么再想清楚模型怎么出诗最后才是写代码。这个项目标准的交付物通常有两块。第一块是算法部分包括数据预处理脚本、模型训练脚本、生成脚本第二块是系统部分一个能跑起来的Web应用。明确了交付物之后再去看模型选型、生成策略、前后端怎么接所有问题都会变得非常具体。1.1 古诗词自动生成到底在解决什么问题从本质上讲这就是一个“猜字游戏”。模型手里捏着一本概率统计册子你输入几个字符它就按条件概率选出下一个最可能的字符再把新字符拼到末尾继续猜直到达到目标长度。用语言模型的话说就是要最大化整个序列的联合概率拆开看就是每个位置上“当前字符在前文条件下出现的概率”的连乘积。古诗有个天然优势五言七言通常只有二三十个字符这个长度范围内的前后文依赖恰好是LSTM最有把握的区域。LSTM全称长短时记忆网络可以把它理解成一个带了多扇门的记忆管家输入门决定当前看到的信息要不要写进记忆遗忘门决定旧记忆要不要倒掉输出门决定此刻放出哪些信息。相比普通循环神经网络LSTM能更好规避梯度消失问题不会因为句子长了就把开头几个字忘干净。写诗需要的就是这种前后呼应、跨句照应的能力所以它作为主力模型完全够用。1.2 为什么课程设计场景下优先选LSTM我先把常见候选模型做个对比。模型参数规模训练难度长距离依赖课程设计友好度普通RNN小低弱梯度消失明显一般效果较差LSTM / GRU中低较强高Transformer大高强低调参成本高有人会问现在主流不是都转Transformer了吗答案是可以用但在这个题目里性价比不划算。课程设计周期通常两到四周语料也就几万首古诗Transformer需要更大数据量、更久的训练时间在只有普通显卡甚至纯CPU的环境下很容易变成调参黑洞。LSTM训练快逻辑直观导师容易看懂生成效果在古诗这种短文本上也不差。拿课程设计的心态做项目选LSTM不是技术落后是性价比最优解。老师如果追问“为什么不试试Transformer”你只需要回答“规划里把Transformer作为对比实验的候选方向”这个预留空间会让方案显得更有深度。1.3 算法模块和系统模块怎么衔接最省事的做法是训练完模型后把模型文件丢给Flask后端加载后端暴露一个生成接口前端页面通过Ajax调用这个接口拿到结果后填充到页面上。这样算法和系统在代码层面只有一层接口关系两边各改各的不互相绑架。很多人容易犯的一个错误是把生成逻辑直接写死在训练脚本里到做系统时又复制粘贴一遍。正确做法是独立抽出一个生成器模块训练脚本里用来调试采样的函数Web后端也调同一个文件。这样你在Notebook里调出来的温度参数、生成效果和浏览器里演示的完全一致答辩时不管从哪个入口展示都不会出意外。2. 数据准备与模型训练要点数据预处理花两三个小时换回来的是整个模型的上限。这一点怎么强调都不过分。古诗生成任务中模型学到什么程度基本由语料质量决定网络结构反而影响相对小。2.1 数据预处理直接决定生成质量可用的开源古诗词语料并不难找常见做法是收集几千首到几万首诗每首诗占一行保存成UTF-8编码的txt文件。这里有几个细节需要专门处理。第一编码必须统一否则会出现乱码和未登录字。第二繁体转简体除非你明确想做古风效果一般课程设计都统一成简体。第三要去掉诗题、作者、注释这些干扰信息保留纯诗句。第四也是容易被忽略的一点字符集大小必须控制。把所有出现过的字符统计一遍保留频次前3000到5000的字符剩下统一映射到UNK。为什么是这个范围常用汉字量级本来就在几千这个区间字表太小模型能选的候选字太少生成内容会显得僵硬字表太大比如直接几万字全保留模型要学的东西太多很容易欠拟合训练也慢。建字表的写法很简单扫描全文用Counter统计频次按编号建立两个字典char_to_idx和idx_to_char。然后按滑动窗口切训练样本sequence_length取20就很稳。窗口太短学不到上下文窗口太长训练集膨胀有效信息反而变稀疏。每个窗口前20个字当输入第21个字当标签移动一步又生成一个新样本。2.2 LSTM网络结构怎么定入门配置一般是这样Embedding输入维度等于字符表大小、输出维度128接一层128个隐藏单元的LSTM并返回序列中间插一个Dropout层防止过拟合再接第二层128单元LSTM最后接全连接层输出每个字符的概率激活函数用softmax。Embedding层负责把一个字符的编号映射成稠密向量。比起one-hot编码它更省内存而且能从语料里学到字符之间的相似关系比如“春”“秋”这种意象相关的字在向量空间里会更接近。两个LSTM叠加是为了捕捉更抽象的上下文但如果语料规模不大单层LSTM也不会差太多。隐藏单元数取128是很多人验证过的折中值太小欠拟合生成内容很平太大训练慢小语料上还容易过拟合。Dropout层放在LSTM中间能让模型不过分依赖某一个时间步的特征对防止模型把训练集整首诗背下来很有效。2.3 训练参数和训练中的判断标准训练配置直接给一套亲测稳的损失函数sparse_categorical_crossentropy优化器Adam初始学习率0.001batch_size64到128epoch先设50配早停回调训练时不要傻等50轮跑完一定加回调。用ModelCheckpoint按验证集loss保存最优权重用EarlyStopping设patience为5验证loss连续不降就自动停。损失一般会在前几轮快速下降后面慢慢收敛。如果训练loss反弹说明学习率偏大调到0.0005再训。这里有一个常见反面现象训练loss降得很漂亮生成结果却像在拼接训练集原文。这就是过拟合。三个解决方向增加语料规模、加大Dropout比例、减小隐藏单元数。课程设计里轻度过拟合不是致命伤因为演示时给一个前缀能生成一句通顺有诗意的句子就能交差但你要心里清楚这个现象的出现位置和原因被老师问到时能说出一二。3. 实操过程与核心环节实现3.1 环境准备框架和依赖怎么选框架推荐TensorFlow/Keras。原因很实际课程设计报告和代码需要被人快速看懂Keras高层API十几行就能交代清楚模型结构用底层API写出来一屏都放不下。PyTorch当然也很好但如果导师团队熟悉的不是它Keras的阅读成本更低。环境依赖大概就是tensorflow2.13.0 flask2.3.2 numpy1.24.3训练硬件方面几万首古诗的数据量CPU也能跑GPU则快很多。我的建议是训练阶段有卡则用卡没卡就把语料缩小到八九千首同样能训出能用的模型。不要一次性把全部数据加载进内存用批次喂给模型的方式更稳内存占用也更低。3.2 可复用的LSTM训练脚本一份能跑通的最小实现下面给出一个直接可跑的最小实现为便于阅读省略了部分异常处理整体结构是完整的。import numpy as np from collections import Counter import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam # 1. 读取语料每行一首诗 with open(poems.txt, r, encodingutf-8) as f: lines [line.strip() for line in f if len(line.strip()) 0] text \n.join(lines) # 2. 统计字符频次控制词表大小 chars Counter(text) vocab [UNK] [c for c, _ in chars.most_common(4000)] char_to_idx {c: i for i, c in enumerate(vocab)} idx_to_char {i: c for i, c in enumerate(vocab)} VOCAB_SIZE len(vocab) # 3. 滑动窗口构造训练样本 seq_len 20 sentences, next_chars [], [] for i in range(len(text) - seq_len): sentences.append(text[i: i seq_len]) next_chars.append(text[i seq_len]) X np.array([[char_to_idx.get(c, 0) for c in s] for s in sentences]) y np.array([char_to_idx.get(c, 0) for c in next_chars]) # 4. 定义LSTM模型 model Sequential([ Embedding(VOCAB_SIZE, 128, input_lengthseq_len), LSTM(128, return_sequencesTrue), Dropout(0.2), LSTM(128), Dense(VOCAB_SIZE, activationsoftmax) ]) model.compile( losssparse_categorical_crossentropy, optimizerAdam(0.001), metrics[accuracy] ) # 5. 训练并保存 model.fit(X, y, batch_size128, epochs30, validation_split0.1) model.save(poem_lstm.keras)这段代码有两个细节值得单独说。第一个是UNK的处理所有不在词表里的字符统一归到0号保证X和y的维度始终对齐。第二个是X必须是整数类型Embedding层不接受浮点输入。第一次跑没有GPU也不要紧一万首诗的语料大概训二三十分钟数据量再砍一半就更快了。3.3 生成策略采样比模型本身更影响作品质量模型输出的是每个候选字的概率分布。怎么从这个分布里选字直接决定成品是“像诗”还是“像乱码”。最粗暴的是贪心采样每次都取概率最大的字符但这样容易陷入重复循环生成一两句之后开始原地打转。常用方案是带temperature的随机采样。它的核心逻辑很直观温度低概率分布被拉尖模型更保守温度高分布变平选字更大胆。古诗生成的经验温度在0.6到0.9之间低于0.5容易重复高于1.2语义基本就碎了。def sample_with_temperature(probs, temperature0.8): probs probs.astype(np.float64) log_probs np.log(probs 1e-6) / temperature exp_probs np.exp(log_probs) probs exp_probs / np.sum(exp_probs) return np.random.choice(len(probs), pprobs)实际项目中我会在这个基础上叠一个top-k过滤和重复惩罚。top-k只保留概率最高的前20到50个候选字再归一化可以避免罕见字突然跳出来重复惩罚是对已经出现过的字符的概率除以一个大于1的系数能明显减轻死循环。把这些策略封装成一个统一的生成函数def generate_poem(model, prefix, char_to_idx, idx_to_char, seq_len20, length30, temperature0.8, top_k30, repeat_penalty1.1): current prefix for _ in range(length): indices [char_to_idx.get(c, 0) for c in current[-seq_len:]] indices [0] * (seq_len - len(indices)) indices x np.array([indices], dtypenp.int32) probs model.predict(x, verbose0)[0] for c in set(current): if c in char_to_idx: probs[char_to_idx[c]] / repeat_penalty top_indices np.argsort(probs)[-top_k:] top_probs probs[top_indices].astype(np.float64) log_probs np.log(top_probs 1e-6) / temperature exp_probs np.exp(log_probs) top_probs exp_probs / np.sum(exp_probs) chosen np.random.choice(top_indices, ptop_probs) current idx_to_char.get(int(chosen), ) return current如果对比效果温度0.2时输出常是“春江春水春江流”这类单调重复温度0.8能保持语义连贯又有点变化温度1.5则会出现“春江清影夜啼乌”这种破碎感较强的句子。把这三档对比放一张表里课程设计报告里就有了一小节“生成策略分析”的素材。3.4 Web系统封装Flask加一个页面就够了模型训练好之后系统部分用一个轻量Web应用就够。Flask后端加载模型暴露一个/generate接口前端输入前缀Ajax请求后端把生成结果渲染到页面上。from flask import Flask, request, jsonify from tensorflow.keras.models import load_model app Flask(__name__) model load_model(poem_lstm.keras) app.route(/generate, methods[POST]) def generate(): data request.get_json(forceTrue) prefix data.get(prefix, 春江) temperature float(data.get(temperature, 0.8)) length int(data.get(length, 30)) result generate_poem( model, prefix, char_to_idx, idx_to_char, lengthlength, temperaturetemperature ) return jsonify({poem: result}) if __name__ __main__: app.run(port5000)前端不需要花哨一个输入框、一个温度下拉框、一个生成按钮加一个结果展示区域就够了。页面里调接口的核心代码也很短async function generatePoem() { const resp await fetch(/generate, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ prefix: document.getElementById(prefix).value, temperature: parseFloat(document.getElementById(temp).value), length: 30 }) }); const data await resp.json(); document.getElementById(result).textContent data.poem; }有一个很容易被忽略的坑模型加载之后预测时千万别开着训练模式。Keras默认predict就是推理不会反向传播但如果你在代码里手滑调用了训练相关接口内存会越跑越高。模型文件放在项目固定路径下Flask启动时加载一次不要在每次请求里重复load。4. 常见问题与排查技巧实录4.1 训练loss一直不降怎么排查先看数据这是最高频原因。语料里如果混着半角符号、乱码、异体字模型容易被带偏。处理办法是打印词表前50个高频字符一眼就能看到有没有奇怪符号有就清洗。排除了数据问题再看学习率Adam默认0.001多数情况没问题但如果loss在某个值附近反复震荡把学习率调成0.0005或0.0003。最后确认模型结构Dense层输出维度是否等于VOCAB_SIZEEmbedding输入长度和样本序列长度是否一致。按这三个位置顺序查基本能解决九成问题。4.2 生成结果重复、绕圈圈怎么办这个坑我踩过比较多次。模型短时记忆有限加上贪心采样很容易在几个字之间循环。解决方案有个优先级先把temperature提上去0.8起步放弃贪心再加top-k过滤K取30收窄候选范围如果还重复就在采样时对已经出现过的字符做重复惩罚最后是限制生成长度古诗在20到40字之间收尾别让死循环无限跑下去。这个环节建议把采样逻辑封装成独立函数所有生成调用都走它后面想调参数只改一个文件省心很多。4.3 生成的诗不符合格律、韵脚乱怎么办要明确一点纯LSTM学到的是字面统计规律不是平仄格律规则所以出律是正常现象。缓解办法分两个层次。第一层在数据端语料优先选标准的五言七言绝句把词、杂言、长短句过滤掉模型学到的句式结构更整齐出律概率明显下降。第二层在生成端加一个后处理函数按五言七言截断或补齐每句字数句尾如果指定韵脚从韵部字表里替换或重新采样。这种“模型负责语义规则负责格律”的混合思路是实际工程项目里会用的做法写进报告里反而能体现你的思考深度。4.4 Web端模型加载慢、接口卡顿怎么优化Keras保存的模型文件从几十到几百MB不等答辩现场启动时间长会很尴尬。三个应对措施第一模型启动时一次性加载进内存不要在每个请求里重新加载第二Flask启动前做预热先打一次模型生成再对外服务第三如果模型实在太大把词表降到3000、Embedding降到64体积能缩小一截生成效果不会差太多。对于课程设计而言完全不需要走到量化部署那一步别在这个环节消耗时间。4.5 答辩演示前必须处理的三个细节第一把几个生成结果提前离线跑好存成截图或文本即使现场网络抽风也有得展示。第二手动准备两三个演示前缀不要现场打一堆奇怪的字符五言就输“春风”七言就输“远上寒山”这类常见前缀模型大多见过生成质量更稳定。第三模型文件路径写清楚绝对路径或项目内固定相对路径都行不要在答辩时因为路径不对而当场报错。这些细节看着琐碎但每年真的有人摔在同一处。5. 最后分享几点实操体会5.1 语料和报告怎么写更划算关于语料永远记住“宁缺毋滥”。网上很多合集里混着注释、作者名、错字花两三个小时清洗比多调三层模型结构更值得。报告部分也不用堆砌训练曲线把数据预处理流程、模型结构说明和示意图、训练loss下降曲线、不同temperature生成对比、系统页面截图放进去结构就很完整了。我还建议放一个“失败案例”小节把生成效果最差的作品贴进去分析原因。导师会觉得你是真理解了这个流程而不是只会跑通代码。5.2 扩展方向和一个自查习惯如果做完基础版还有余力有三个方向可以选接一个韵脚字表做押韵控制把LSTM和GRU做一组对比实验或者把前端界面升级成多主题风格。无论选哪个投入产出比都很高答辩印象分会明显不一样。我自己的习惯是写完训练脚本后一定拿同一组前缀把整个生成流程完整跑一遍再交给Web端接。不要小看这个自查动作它能避开演示时几乎所有低级问题。这套流程走完你训练出的第一首完整古诗也足够成为一份挺有成就感的课设成果。