简介:这份资源面向具备一定深度学习基础、希望动手实践序列生成模型的开发者与学习者,核心是用Keras框架搭建RNN与LSTM网络,实现自动编写古诗的完整流程,帮助读者理解循环神经网络在文本生成任务中的建模思路与训练细节。压缩包共8个文件,约3.86MB,以4个Python脚本为主体,分别承担模型定义、数据预处理与配置管理等职责,另含诗词语料文本、说明文档、开源协议及忽略配置等辅助文件,结构清晰,便于直接运行与二次修改。目前已有602人学习下载,说明该案例在入门序列生成方向具有一定参考价值。读者可从中获得可运行的完整代码、古诗数据集、模型构建与训练脚本,以及作者在学习过程中整理的关键问题记录,适合作为RNN与LSTM实战练手项目,也可在此基础上调整网络结构或更换语料,探索更丰富的文本生成效果。
1. 用 Keras 搭一个会写古诗的 RNN+LSTM:这份源码能让你从零跑通文本生成
有个挺有意思的现象:很多开发者学完 RNN 和 LSTM 的理论,梯度消失、门控机制、时间步展开都能讲得头头是道,但一到“给我一个能跑起来的序列生成项目”,就卡住了。这份《用 Keras 实现 RNN+LSTM 的模型自动编写古诗》源码包,解决的就是这个断层——它把“古诗文本 → 字符级序列建模 → 训练 → 按首句续写”整条链路完整落到了一个可运行的工程里。技术栈是 Python + Keras(TensorFlow 后端),核心是字符级 LSTM 语言模型,适合刚学完深度学习基础、想找一个完整 NLP 小项目练手的开发者,也适合想拿它当模板改造成歌词生成、对联生成、代码补全等场景的熟手。下面我按“它是什么 → 怎么跑 → 怎么改”的顺序,把这份源码拆开讲。
2. 项目架构与数据流:从古诗文本到字符级序列样本
2.1 整体目录结构与模块职责
拿到源码包解压后,常见的目录组织大致是这样(不同版本可能略有差异,但核心文件不会少):
poetry_lstm/ ├── data/ │ └── poems.txt # 古诗语料,每行一首 ├── model/ │ └── poetry_model.h5 # 训练后保存的权重 ├── config.py # 超参数集中配置 ├── data_loader.py # 语料读取与字符映射表构建 ├── model.py # 网络结构定义 ├── train.py # 训练入口 ├── generate.py # 生成/续写入口 └── requirements.txt # 依赖清单这个划分是典型的“配置、数据、模型、训练、推理”五分离。我一般拿到这类项目,第一件事不是急着跑train.py,而是先看config.py,因为所有能调的东西都集中在那里,改起来不用满项目翻。
2.2 数据预处理:字符级建模的关键一步
古诗生成属于字符级语言模型,不是词级。原因很直接:古诗用词凝练、生僻字多,分词反而会破坏韵律结构,而字符级建模能让模型学到“平仄”“对仗”这种位置规律。核心预处理逻辑如下:
# data_loader.py import numpy as np def load_poems(path): with open(path, 'r', encoding='utf-8') as f: poems = [line.strip() for line in f if line.strip()] return poems def build_vocab(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()} return chars, char2idx, idx2char def make_sequences(poems, char2idx, seq_len=20): X, y = [], [] for poem in poems: # 每首诗转成索引序列 idxs = [char2idx[c] for c in poem if c in char2idx] # 滑动窗口切样本:前 seq_len 个字符预测第 seq_len+1 个 for i in range(len(idxs) - seq_len): X.append(idxs[i:i + seq_len]) y.append(idxs[i + seq_len]) return np.array(X), np.array(y)逻辑说明:build_vocab构建字符到整数的双向映射,这是所有文本生成任务的地基。make_sequences用滑动窗口把长诗切成“输入 20 个字符、预测下一个字符”的监督样本。参数seq_len决定模型一次能“看到”多长的上下文,默认 20 对五言/七言绝句够用,改大能捕捉更长依赖,但显存和训练时间会涨。
注意:语料里如果有标点、空格、英文,要么在预处理阶段过滤掉,要么让它们进词表,否则
char2idx[c]会直接抛 KeyError,这是新手最常见的翻车点。
2.3 模型结构:Embedding + LSTM + Softmax 三段式
网络本身不复杂,但每一层的参数都值得说清楚:
# model.py from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense def build_model(vocab_size, seq_len, embedding_dim=128, lstm_units=256): model = Sequential([ # 把字符索引映射成稠密向量 Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=seq_len), # 单层 LSTM,返回最后一个时间步的输出 LSTM(lstm_units, return_sequences=False), # 输出层:vocab_size 个字符的概率分布 Dense(vocab_size, activation='softmax') ]) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) return model逻辑说明:Embedding把离散字符索引变成embedding_dim维向量,让模型能学字符间的语义相似性。LSTM是核心,return_sequences=False表示只取最后一个时间步的隐状态,因为我们要预测的是“下一个字符”这一个输出。Dense + softmax输出整个词表上的概率分布。参数上,embedding_dim=128、lstm_units=256是这类小语料的稳妥起点;如果语料只有几千首诗,层数再堆深容易过拟合,单层 LSTM 反而更稳。
3. 训练与生成:把模型跑起来并让它写出第一句诗
3.1 训练脚本与回调配置
训练入口通常长这样,重点在回调:
# train.py from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping from data_loader import load_poems, build_vocab, make_sequences from model import build_model import config poems = load_poems(config.DATA_PATH) chars, char2idx, idx2char = build_vocab(poems) X, y = make_sequences(poems, char2idx, seq_len=config.SEQ_LEN) model = build_model(len(chars), config.SEQ_LEN) model.summary() callbacks = [ # 只保存验证损失最低的权重,避免最后一轮反而变差 ModelCheckpoint(config.MODEL_PATH, save_best_only=True), # 连续 3 轮验证损失不降就停,省时间 EarlyStopping(patience=3, restore_best_weights=True) ] model.fit(X, y, batch_size=config.BATCH_SIZE, epochs=config.EPOCHS, validation_split=0.1, callbacks=callbacks)逻辑说明:ModelCheckpoint配合save_best_only=True是血泪经验——文本生成模型训练后期很容易过拟合,最后一轮的权重未必最好,保存最优权重能省掉手动回滚的麻烦。EarlyStopping的patience=3表示容忍 3 轮不进步。validation_split=0.1从训练集里切 10% 做验证,语料小的时候这个比例够用。
3.2 生成逻辑:温度参数与逐字符采样
生成不是一次性输出,而是“预测一个字符 → 拼回输入 → 再预测下一个”的循环:
# generate.py import numpy as np def sample(preds, temperature=1.0): # 温度越低越保守,越高越随机 preds = np.asarray(preds).astype('float64') preds = np.log(preds + 1e-8) / temperature exp_preds = np.exp(preds) preds = exp_preds / np.sum(exp_preds) return np.argmax(np.random.multinomial(1, preds, 1)) def generate_poem(model, start_text, char2idx, idx2char, seq_len=20, length=50, temperature=0.8): generated = start_text for _ in range(length): # 取最后 seq_len 个字符作为输入 seq = generated[-seq_len:] x = np.zeros((1, seq_len)) for i, c in enumerate(seq): x[0, i] = char2idx.get(c, 0) preds = model.predict(x, verbose=0)[0] next_idx = sample(preds, temperature) generated += idx2char[next_idx] return generated逻辑说明:sample函数里的温度参数是控制生成质量的关键。temperature=0.5时输出保守、重复多但通顺;temperature=1.2时用词新奇但容易跑偏。generate_poem每次只预测一个字符,然后把它追加到已生成文本末尾,再截取最后seq_len个字符作为下一轮输入。参数length控制生成总长度,start_text是给定的首句。
提示:如果生成结果全是重复字,先别怀疑模型,八成是温度太低或训练轮数不够。把
temperature调到 0.8~1.0 之间再试。
3.3 运行步骤与依赖安装
完整跑通流程如下:
# 1. 建虚拟环境(推荐,避免依赖冲突) python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 2. 装依赖,核心就两个 pip install tensorflow keras numpy # 3. 训练(首次跑建议先把 EPOCHS 调小验证流程) python train.py # 4. 生成 python generate.py --start "床前明月光" --length 40依赖上,TensorFlow 2.x 已经内置 Keras,不需要单独装keras包,但有些老代码会import keras,这时装一个兼容版本即可。训练时间取决于语料规模,几千首诗在普通 CPU 上跑几十轮大概几十分钟到几小时,有 GPU 会快很多。
4. 避坑与常见问题排查
4.1 报错 KeyError:字符不在词表里
现象:生成时输入首句,程序直接抛KeyError。原因:首句里出现了训练语料中没出现过的字符,char2idx里查不到。解决:在generate_poem里用char2idx.get(c, 0)兜底,或者干脆在生成前过滤掉生僻字。我一般会在预处理阶段就把非汉字字符全部剔除,从源头避免。
4.2 生成结果全是同一个字
现象:不管输入什么首句,输出都是“啊啊啊啊”或“春春春春”。原因:模型没训练充分,或者学习率过高导致输出分布坍缩到单个字符。解决:先确认训练轮数和损失是否正常下降;再把temperature调高到 1.0 以上增加随机性;如果还不行,检查make_sequences的滑动窗口是否切错了,导致标签和输入错位。
4.3 显存/内存爆掉
现象:训练到一半OOM被杀。原因:seq_len或batch_size设太大,或者make_sequences一次性把所有样本堆进内存。解决:把batch_size降到 64 或 32;语料特别大时改用生成器逐批喂数据,而不是np.array全量加载。
4.4 模型保存后加载失败
现象:model.load_weights报维度不匹配。原因:加载时的vocab_size或seq_len和训练时不一致。解决:把词表和超参数一起保存,加载时先读配置再建模型。常见做法是把char2idx用pickle存下来,生成脚本直接读,别每次重新构建。
4.5 训练损失不下降
现象:loss 卡在某个值不动。原因:学习率太大导致震荡,或者数据没做归一化(字符级建模一般不需要,但索引越界会出问题)。解决:换用Adam默认学习率通常够用;检查X里的索引是否都小于vocab_size,越界会让 Embedding 层输出异常。
5. 二次开发:把古诗模型改成你自己的生成器
这份源码最大的价值不是“会写古诗”,而是它提供了一个字符级序列生成的通用骨架。想改成歌词生成、对联生成、甚至代码补全,改动集中在三个地方。
第一,换语料。把data/poems.txt换成你的目标文本,每行一条。歌词就一行一句,对联就一行上联一行下联。注意保持“每行一个独立样本”的格式,load_poems不用动。
第二,调seq_len。古诗五言七言短,20 够用;歌词句子长,可能要 40~60;代码补全的上下文更长,可以到 100 以上。但seq_len越大,LSTM 要记的依赖越长,训练越慢,建议从 30 起步逐步加。
第三,改生成策略。默认是逐字符贪心/采样,如果你想让生成更可控,可以加“首句约束”或“关键词引导”。比如对联生成,可以固定上联,只让模型生成下联:
# 固定前半段,只生成后半段 def generate_with_prefix(model, prefix, suffix_len, char2idx, idx2char): generated = prefix for _ in range(suffix_len): seq = generated[-config.SEQ_LEN:] x = np.zeros((1, config.SEQ_LEN)) for i, c in enumerate(seq): x[0, i] = char2idx.get(c, 0) preds = model.predict(x, verbose=0)[0] generated += idx2char[sample(preds, temperature=0.7)] return generated验证改造是否成功,我一般用两个指标:一是生成结果里目标领域的词占比,二是人工读几句看通顺度。别只看 loss,loss 低不代表生成好看,这是文本生成任务最容易踩的坑。
改造时还有个实用技巧:把config.py里的超参数全部参数化,用命令行传入,这样同一份代码能跑多组实验,不用每次改文件。我习惯在train.py里加argparse,把seq_len、lstm_units、epochs都做成可选参数。
从那以后我每次拿到这类生成项目,都强制先跑一遍小规模训练(把EPOCHS设成 2、语料只取 100 首),确认整条链路通了再上全量数据——这样能省掉大量“训练几小时才发现数据格式错了”的时间。希望这份拆解能帮你顺利跑起来,并改成你自己想要的样子。
本文还有配套的精品资源,点击获取