ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python古诗生成器实战:从LSTM建模到Flask接口与前端集成

Python古诗生成器实战:从LSTM建模到Flask接口与前端集成 简介这是一套基于Python的古诗生成器完整源码并集成可直接操作的前端页面面向对自然语言处理、AI写诗和前后端一体化开发感兴趣的编程爱好者与学习者可作个人练习、课程设计或兴趣小组的实践素材。压缩包共43个文件、约10.85MB文件类型涵盖7个Python脚本、5个XML配置文件、5个CSS样式表、5个JavaScript脚本、4个文本文件以及GIF/PNG图片与字体资源其中Python脚本负责初始化、数据处理、模型训练与诗词评估XML/JS/CSS承担项目配置、页面交互和视觉样式文本与图片素材则提供诗库及界面元素。目前已有323人学习/下载。项目完整呈现了从语料处理、模型生成到前端展示的闭环并借助中文BERT预训练模型提升生成效果源码结构清晰、注释到位便于读者梳理流程也可作为二次开发、功能拓展或教学演示的参考基础。1. 古诗生成器是一个被低估的 Python 练手项目它到底解决什么问题古诗生成器是一个被低估的 Python 练手项目把它落地到能打开网页点一下按钮就生成一首五言诗是理解 Python 后端到前端完整链路的捷径。标题里的“源码”意味着你要拿到的不只是几行 demo而是一套能跑、能改、能展示的综合工程。它真正练的不是“写诗”的玄学而是中文语料处理、文本生成模型的搭建、接口封装以及前端集成设计这四件事。这套东西适合已经把 Python 基础语法过了一遍、想找个有可视化成果的项目的开发者也适合准备课程设计或毕设展示的人。难点不在模型本身而在你愿不愿意把“能出结果”打磨成“稳定可用”语料干不干净、接口容不容错、前端是否处理了请求失败。我这里按从头搭一套常见方案的路径来说所有代码都是可以直接复现的落地写法。2. 接口先行生成器的模型选型与前后端协议设计先定协议再写代码。直接打开编辑器刷模型很容易翻车因为前端不知道后端要传什么后端不知道前端要不要 loading。一个能长期改着玩的古诗生成器项目第一件事是把模型选型和接口约定定下来。2.1 三种生成方案怎么选规则、马尔可夫链、LSTM不同来源的古诗生成器源码核心思路基本逃不开三类。模板规则最稳定输出一定是“五言绝句”的格式但它没有生成能力只是填词马尔可夫链快语料小也能跑缺点是生成内容跳跃字符级 LSTM 最自然能学到“平仄对仗”的大致规律但要花时间训练。对要交作业或者做展示的场景我更建议用 LSTM。它的代码量看起来比马尔可夫链大但训练过程有损失曲线可看生成的句子也更像“诗”。如果只是想快速验证前后端调通那就先用马尔可夫链后面再换模型接口不用变。方案生成质量训练成本适合场景模板规则低但格式稳无纯页面展示马尔可夫链中语句跳跃秒级快速验证接口字符级 LSTM较高有诗感分钟到小时正式项目、课程设计2.2 定义前后端接口用 JSON 统一请求和响应前端和后端之间只走一个接口前端传“起句、生成长度、随机度”后端返回“生成的诗句”。这样做的好处是以后把 Flask 换成 FastAPI或者把前端从原生 HTML 换成 Vue都不用改生成逻辑。请求方式定为 POST/api/poem请求体是 JSON。我要特别说明为什么用 POST 而不是 GET生成参数里包含汉字GET 会有 URL 编码问题另外同一句话反复生成结果应当不同这种带副作用的操作放 POST 更合适。{ seed: 春江花月夜, length: 20, temperature: 0.9 }响应的 JSON 结构也要提前固定。除了生成结果我还会返回 seed 和 temperature方便前端展示“这是基于什么生成的”。如果生成过程抛异常响应里要带一个error字段而不是让前端拿到空白。{ poem: 春江花月夜风清月满楼。, seed: 春江花月夜, temperature: 0.9 }接口约定不一定写在文档里直接写一个api.md放项目根目录也行。重点是前后端都按同一份字段开发。2.3 项目目录怎么铺Flask 前后端一体的常见结构我做这类项目时习惯用一个 Flask 应用同时管后端接口和前端页面。目录不需要花哨但要把语料、模型、静态资源分开poem_generator/ ├── app.py # Flask 入口同时提供页面和 API ├── train.py # 训练脚本 ├── corpus/ │ └── 唐诗三百首.txt ├── models/ │ └── poem_model.h5 ├── static/ │ └── app.js # 前端逻辑 └── templates/ └── index.html # 页面模板静态文件放staticHTML 放templates是 Flask 默认约定。这样做最大的好处是前端可以和后端一起启动省掉跨域问题。如果你打算把前后端彻底分离那前端就要另起 dev server后端必须处理 CORS这个坑放到第 5 章专门说。3. 后端实现语料清洗、LSTM 生成与 Flask 接口后端是整套生成器的核心。我一般分成三步先清洗语料再训练模型最后把模型包成接口。每一步单独跑通最后合在一起不要一口气写完再调试。3.1 语料清洗把一本诗集变成模型能读的纯文本古诗文网站上的文本经常夹着全角空格、注释和 Windows 换行符。字符级模型是按字学习的一个混进去的\u3000也会被当成独立字符导致字符表变大、生成结果里出现空白。我常用的清洗函数如下import re def clean_corpus(corpus_path: str) - str: with open(corpus_path, encodingutf-8) as f: text f.read() # 去掉全角空格、制表符和普通空格 text re.sub(r[ \t\u3000], , text) # 只保留汉字、英文半角标点和换行其余全部丢弃 text re.sub(r[^\u4e00-\u9fff。\n], , text) # 统一换行符避免 Windows 的 \r\n 干扰切分 text text.replace(\r\n, \n).strip() return text这段代码的关键在第二个正则\u4e00-\u9fff是统一汉字编码范围标点只保留古诗里常用的逗号、句号、感叹号和问号。如果你把“123”和字母也保留模型可能会学到不想要的噪音。参数说明corpus_path指向你下载好的 txt 语料返回值是一个干净的长字符串。要特别提醒直接读文件时encodingutf-8必须显式指定Windows 默认编码可能是 gbk不指定会报UnicodeDecodeError。3.2 训练一个字符级 LSTM核心代码和参数怎么调字符级模型的任务是给你前 20 个字预测第 21 个字。训练前要把清洗后的字符串切成固定长度的输入和输出序列。这里有一个时间窗口seq_len和步长stride直接影响训练数据量和生成连贯性。# train.py 核心训练片段 import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Embedding from tensorflow.keras.callbacks import ModelCheckpoint text clean_corpus(corpus/唐诗三百首.txt) chars sorted(set(text)) char_to_idx {ch: i for i, ch in enumerate(chars)} idx_to_char {i: ch for i, ch in enumerate(chars)} seq_len 20 stride 3 X, y [], [] for i in range(0, len(text) - seq_len, stride): seq_in text[i:i seq_len] seq_out text[i seq_len] X.append([char_to_idx[ch] for ch in seq_in]) y.append(char_to_idx[seq_out]) X np.array(X) y np.array(y) model Sequential() model.add(Embedding(len(chars), 128, input_lengthseq_len)) model.add(LSTM(256, return_sequencesTrue)) model.add(LSTM(256)) model.add(Dense(len(chars), activationsoftmax)) model.compile(optimizeradam, losssparse_categorical_crossentropy) checkpoint ModelCheckpoint(models/poem_model.h5, save_best_onlyTrue, monitorloss) model.fit(X, y, batch_size128, epochs30, callbacks[checkpoint])几个参数一定要理解。seq_len20表示用 20 个字预测下一个字太小生成容易跑题太大训练样本急剧减少。stride3表示每 3 个字切一个窗口如果设成 1数据量会变成三倍但相邻样本高度重复训练容易过拟合。模型结构上我用两层 LSTM第一层返回完整序列第二层只返回最后一个输出这样能捕捉到前后文的短期依赖。Embedding直接设成 128 维古诗字表一般只有几千个字这个维度够用了。batch_size128在 CPU 上也能接受如果显存不足就调成 64 或 32。训练过程里loss 从 3 左右慢慢降到 1 以下是正常的不需要等到它到 0。LSTM 生成古诗本身就有玄学成分重点是模型记住常用字组合和结尾语气词。3.3 模型加载与生成函数避免每次请求都重读文件很多源码会在接口里写load_model这是最大的性能问题。模型文件几十兆每次请求都加载一次页面就会卡好几秒。正确做法是在 Flask 启动时加载一次放到全局变量。# generate.py import numpy as np from tensorflow.keras.models import load_model _model load_model(models/poem_model.h5) _char_to_idx None # 训练完把映射存成 json 后恢复 def generate_poem(seed: str, length: int, temperature: float) - str: result seed for _ in range(length): seq result[-20:] x np.array([[ _char_to_idx[ch] for ch in seq ]]) preds _model.predict(x, verbose0)[0] preds np.log(preds 1e-8) / temperature exp_preds np.exp(preds) preds exp_preds / np.sum(exp_preds) next_idx np.random.choice(len(preds), ppreds) result idx_to_char[next_idx] return result这里的temperature是控制随机度的关键参数大于 1 生成更随机但容易不通顺小于 1 则更保守但容易重复。我在服务层会把它限制到 0.1 到 1.5 之间。3.4 Flask 接口把生成函数包成 JSON 服务到这一步生成器已经能跑了现在把它挂到 HTTP 服务上。from flask import Flask, request, jsonify from generate import generate_poem app Flask(__name__) app.route(/) def index(): return app.send_static_file(index.html) app.route(/api/poem, methods[POST]) def poem_api(): body request.get_json(forceTrue) seed body.get(seed, 春江).strip() length max(1, min(int(body.get(length, 20)), 50)) temperature max(0.1, min(float(body.get(temperature, 0.9)), 1.5)) poem generate_poem(seed, length, temperature) return jsonify({poem: poem, seed: seed, temperature: temperature})forceTrue允许前端即使忘了设Content-Type也能解析 JSON联调时少踩一个坑。length限制在 50 以内防止有人调一个 1000 让服务器生成半天。返回的 JSON 和第 2 章约定完全一致前端可以直接用。4. 前端集成设计输入、展示、状态管理与请求联调前端集成设计不是写一个按钮调接口就完了。你要处理用户输入、加载状态、生成失败提示还有历史记录。这里用原生 HTML JavaScript 实现逻辑简单、没有 Node 依赖任何人拉下来都能直接跑。4.1 页面骨架输入起句与结果容器templates/index.html是 Flask 默认读取的页面模板。我把按钮、输入框、结果区写好样式尽量简单。!doctype html html langzh-CN head meta charsetutf-8 title古诗生成器/title /head body div classcontainer h1古诗生成器/h1 input idseed placeholder输入起句比如春江花月夜 value春江花月夜 button idsubmit生成古诗/button p idstatus/p div idresult classpoem/div div idhistory/div /div script src/static/app.js/script /body /html这里的value用做默认提示语用户不输入也能生成。status用来显示“生成中”或“出错了”。结果区单独占一个div方便用 CSS 控制字体和间距。4.2 用 fetch 调用后端接口串起整个链路static/app.js负责监听按钮点击、调用接口、渲染结果。这里最容易错的点是fetch 里的headers必须显式声明Content-Type: application/json否则 Flask 端的request.get_json拿到的可能是None。const submitBtn document.getElementById(submit); const seedInput document.getElementById(seed); const resultDiv document.getElementById(result); const statusDiv document.getElementById(status); async function generatePoem() { const seed seedInput.value.trim() || 春江花月夜; statusDiv.textContent 生成中...; try { const response await fetch(/api/poem, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ seed, length: 20, temperature: 0.9 }) }); const data await response.json(); if (data.error) throw new Error(data.error); resultDiv.textContent data.poem; statusDiv.textContent ; } catch (err) { statusDiv.textContent 生成失败请检查后端服务是否启动; } } submitBtn.addEventListener(click, generatePoem);我把错误处理放在catch里网络断连、后端 500、后端返回error字段都会被捕获。这样页面不会白屏。4.3 加一个 localStorage 历史功能前端集成设计的加分项课程设计只做“点按钮生成”太单薄。我把生成结果存到localStorage刷新页面后历史还在。这个功能对展示很有用也能体现前端状态管理的思路。function saveToHistory(poem, seed) { const history JSON.parse(localStorage.getItem(poemHistory) || []); history.unshift({ poem, seed, time: new Date().toLocaleString() }); localStorage.setItem(poemHistory, JSON.stringify(history.slice(0, 10))); renderHistory(history); } function renderHistory(history) { const historyDiv document.getElementById(history); historyDiv.innerHTML history .map(item p${item.time}${item.poem}/p) .join(); }在generatePoem的函数里拿到data.poem后调用saveToHistory即可。注意localStorage只能存字符串所以对象要先JSON.stringify读取时再JSON.parse。4.4 联调时先确认 URL静态页面和后端端口常见错位前端写完后第一步不是功能测试而是确认页面里所有请求地址对不对。Flask 默认跑在http://127.0.0.1:5000如果你直接用浏览器打开index.html文件fetch 的/api/poem会变成一个无效的file:///api/poem请求。正确做法是启动 Flask 后打开http://127.0.0.1:5000/。如果你用 VSCode 的 Live Server 插件打开页面那就必须给请求地址改成完整的http://127.0.0.1:5000/api/poem同时后端要处理 CORS。这属于典型的“页面能看到但接口调不通”多半是地址或跨域问题。5. 避坑从 Python 环境配置到模型推理最常见的 5 个翻车点这个项目坑不少每一个我都见过真实翻车的场景。这里挑最常见的五个按“现象、原因、解决”写清楚照着排查能省下半天时间。5.1 现象读取语料时报UnicodeDecodeError或生成结果全是乱码原因Windows 下 Python 默认编码是 gbk而语料文件是 utf-8 保存的打开时没指定编码。解决所有open都显式写encodingutf-8包括训练脚本和 Flask 服务。如果文件本身是 gbk就把encodinggbk。判断文件编码的土办法是用 VSCode 打开 txt 看右下角或者用 Python 的chardet检测。5.2 现象训练时报ResourceExhaustedErrorCPU 内存或显卡显存不够原因batch_size设得太大或者seq_len太长导致每次输入模型的矩阵过大。解决先把batch_size降到 32LSTM单元从 256 降到 128。如果还是爆就减少训练语料比如只留 200 首诗。生成器项目不需要用全唐诗语料质量比数量重要。5.3 现象生成结果永远是“春江春江春江”这种循环重复原因temperature设得太低导致采样总是选概率最大的那个字或者训练轮次太多模型过拟合只记住了特殊字连续出现的片段。解决把温度调到 0.8 到 1.2并且用np.random.choice按概率采样而不是每次都取argmax。如果过拟合就把epochs降到 20加一层Dropout。5.4 现象前端控制台报blocked by CORS policy原因前端页面运行在5500端口后端 Flask 在5000端口浏览器把这两种不同源地址的跨域请求拦截了。解决如果你一定要走前后端分离模式给 Flask 接口加一个after_request的跨域头from flask import Flask, request, jsonify app Flask(__name__) app.after_request def add_cors_headers(resp): resp.headers[Access-Control-Allow-Origin] * resp.headers[Access-Control-Allow-Headers] Content-Type resp.headers[Access-Control-Allow-Methods] POST, OPTIONS return resp这个*只适合本地开发部署上线要把Access-Control-Allow-Origin设置成你自己的域名否则任何网站都能跨域调用你的接口。5.5 现象VSCode 里跑得通换命令行跑就报ModuleNotFoundError原因VSCode 默认选中的 Python 解释器和命令行里用的不是同一个pip install tensorflow装进了系统环境而 VSCode 用的是虚拟环境或者反过来。解决在项目根目录建一个虚拟环境统一入口。我一般这样做python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install tensorflow flask numpy然后在 VSCode 里按CtrlShiftP打开“选择 Python 解释器”选venv下那个路径。这样不管从哪个终端启动依赖都是同一份。6. 进阶验证用温度与押韵检查把生成器调到可展示到这步你的古诗生成器已经能完整跑通了。最后一件事是把“能跑”变成“看着像样”。我这里的两个技巧是很多人不会注意的温度动态调整和简单的押韵校验。6.1 temperature 不只是参数按句子长度分段调短句子用低温度能保证开头不跑偏长句子用略高温度能让内容更丰富。更自然的写法是生成前 10 个字时温度设 0.7后面设为 1.0def generate_poem(seed, length, temperature): result seed for i in range(length): current_temp temperature if i 10 else temperature 0.1 next_char sample_one(result, current_temp) result next_char return result这个改动很小但生成的效果比全程固定温度更稳定尤其对五言绝句这种前几个字影响全诗的场景。6.2 用押韵检查做自动回归改完模型后怎么判断有没有变好不能只靠肉眼。一个便宜的验证方法是检查句尾是否落在常用韵脚上。古诗的用字习惯可以在语料里统计最后一句末字是否在训练集高频尾字集合里。rhyme_chars {楼, 舟, 秋, 流, 愁, 休, 洲, 头} def check_rhyme(poem): lines poem.replace(。, 。\n).split(\n) last_chars [line[-1] for line in lines if line.strip()] return sum(1 for c in last_chars if c in rhyme_chars) / max(len(last_chars), 1)每次改完训练参数跑 50 次生成算平均押韵率。这个数字如果下降说明模型在“乱写了”。它不完美但比凭感觉判断可靠得多。我自己做这个项目的习惯是先把接口文档写在项目 README 的第一段再写代码。后来前端换过一次框架后端从 Flask 换成了 FastAPI接口字段没动前端逻辑几乎没有改。这个教训让我确信古诗生成器的重点不在“生成”两个字而在于把生成能力做成了别人能调用的服务。希望帮到你。本文还有配套的精品资源点击获取
返回列表