news 2026/10/6 3:16:51

古诗自动生成与情感分析:LSTM、情感分类与韵律约束的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
古诗自动生成与情感分析:LSTM、情感分类与韵律约束的工程实践

简介:一套基于机器学习与自然语言处理的古诗自动生成与情感分析系统项目资料包,面向自然语言处理学习者、诗歌生成研究者及对中文文本分析感兴趣的开发者。资源覆盖语料爬取、数据清洗与标注、词频与情感分析、规则作诗及神经网络写诗等完整流程,提供爬虫脚本、预处理代码、SPSS分析数据、规则引擎及模型训练产物。包内共156个文件,以43个Python源码、73个纯文本语料为主,另含6个模型检查点与data文件、6个词向量文件、7张图表及说明文档,整体约337.22MB;目录结构清晰,便于按模块对照学习。已有295人学习下载,适合希望从零复现古诗生成与情感分析实验的读者,可直接基于已训练模型和文本数据开展后续改进,也可参照脚本扩展语料或调整网络结构。

1. 古诗自动生成与情感分析:一个系统里两个「黑匣子」

很多人第一次接触这个项目,是被「古诗自动生成」吸引,觉得让机器写一首像模像样的七绝很酷;真正动手做才发现,情感分析才是那个让系统从「玩具」变成「工具」的关键。一个完整的古诗自动生成与情感分析系统,实际上要解决两个完全不同性质的问题:生成是创作型任务,模型要学会的是「字与字之间的概率分布」;情感分析是理解型任务,模型要学会的是「从有限的字里推断言外之意」。前者输出的是文本,后者输出的是判断。这两件事共享同一份语料,却需要完全不同的建模思路、训练目标和评估方法,这也是为什么很多课程设计或工程 demo 做到一半就卡住——不是模型不够强,而是没想清楚这两条技术路线的分界。下文按我实际做过的一套方案来拆解:数据怎么处理、生成模型怎么选、情感分析怎么做、以及最常踩的四个坑。

2. 把古诗变成训练样本:清洗、切分与韵律标注的三个门槛

2.1 语料来源与清洗:不是拿到诗集就能用

古诗自动生成系统的第一个瓶颈永远是数据。常见做法是去开源古诗数据库拉原始文本,比如全唐诗、全宋词的整理版本,通常一个文本文件就是一首诗加标题和作者。但这类原始数据离能训练还差很远:全角半角混用、注释和序言混在正文里、生僻字用图片代替、同一个字的异体字写法不统一。我一般会先写一个清洗脚本,做三件事:去掉非正文行,统一全角标点为半角,把「『』」「【】」等注释符号连同中间内容一并删除。

import re def clean_poem(raw_text: str) -> str: # 去掉作者、标题行:通常格式为“作者·标题”或“标题 — 作者” lines = raw_text.strip().split("\n") body_lines = [] for line in lines: line = line.strip() if not line: continue # 跳过纯标题/作者行:长度小于等于8且不含句读 if len(line) <= 8 and not re.search(r"[,。!?;]", line): continue # 去掉注释符号及其内容 line = re.sub(r"[【】「」『』]", "", line) body_lines.append(line) return "\n".join(body_lines)

清洗逻辑里最容易翻车的是「跳过标题行」这一步。有些五言绝句的正文只有十个字,如果按「长度小于等于 8 就跳过」的规则,会把正文也误杀。所以还要加一个条件:不含句读符号。这个规则不是绝对可靠,但对大多数标注规范的语料已经够用。清洗后还要做一次人工抽检,每 500 首抽查 10 首,确认没有把序言当正文、没有把作者行残留在正文里。这一步的产出直接决定后续所有环节的质量,不值得省。

2.2 字级还是句级?训练样本的切分粒度决定模型上限

清洗完的语料是一首首完整的诗,下一步要决定训练样本的切分粒度。古诗生成有两类主流做法:按整首诗作为训练样本,或者按「上句-下句」的对句作为训练样本。前者适合语言模型式的续写——给它前三个字让它往后接;后者适合 Seq2Seq 式的对仗生成——给它上句让它对出下句。我做过对比,在同等数据量下,「上句-下句」的对句切分明显更容易训练,因为古诗的下半句和上半句之间有极强的对仗、平仄和语义约束,模型从对句里学到的规律比从连续文本里学到的要清晰得多。

具体切分方式是按行拆分再配对:绝句的四行拆成两对(1-2 句、3-4 句),律诗的八行拆成四对。这里有个细节,不能简单按物理行切,因为有些语料把长联排成一段而不是一行。处理方式是先按句读符号(。!?;)分成单句,再按顺序两两配对。

def split_couplets(poem_body: str): # 按句读切分为单句 sentences = re.split(r"[。!?;\n]", poem_body) sentences = [s.strip() for s in sentences if s.strip()] couplets = [] for i in range(0, len(sentences) - 1, 2): couplets.append((sentences[i], sentences[i + 1])) return couplets

切分时必须保留单句内部的句读,不能因为切分把「举头望明月」和「低头思故乡」中间的语义关系统统丢掉。另外要注意:不是所有古诗都是标准偶句。个别词牌的长短句排列不符合两两配对规则,这类样本应该过滤而不是强行配对,否则模型会学到错误的「对句=相邻两行」的偏见。

2.3 给样本打上「情感标签」:一个可落地的七分类体系

情感分析模块的训练数据不是现成的,公开的古诗情感标注数据集非常少,大部分项目是自己标。我用的是一套七分类体系:喜悦、愤怒、悲伤、思乡、离别、山水隐逸、讽喻。前五个接近通用情感分类,后两个是古诗里特有且高频的情感类型。如果不用这两个类别,模型会把大量写景抒怀的诗硬塞进「喜悦」或「悲伤」,导致情感分析结果失真。

标注流程上,我采用「先规则预标注、后人工校正」的方式。规则预标注用种子词表:比如出现「愁」「泪」「孤」「寒」等字就预标为悲伤;出现「归」「故园」「乡」等字预标为思乡。预标注的准确率大约在 60% 到 70%,剩余部分人工校正。人工校正的标准很关键:以全诗整体情绪为准,不以单句情绪为准。比如「感时花溅泪,恨别鸟惊心」整体是悲伤,但单看「花溅泪」不能标成悲伤以外的类别。这个标准要在标注规范里写死,否则不同人标注的结果差异会大到无法训练。

七分类的标签分布通常极不均衡——「山水隐逸」和「悲伤」会占掉一大半,「愤怒」少得可怜。处理方式有两个:一是收集语料时有意多收边塞诗和咏史诗来补「愤怒」和「讽喻」;二是对少数类做过采样,在训练时给少数类样本更高的采样权重。这两个手段要同时用,只用过采样容易过拟合,只调整采集方向又补不了多少样本。

3. 用 Seq2Seq 还是语言模型?古诗生成的主干网络选型

3.1 从 RNN 到 Transformer:为什么古诗生成还在用带状态的结构

古诗生成的主干模型有两个主流选项:基于循环神经网络的 Seq2Seq 模型(通常用 LSTM 或 GRU)和基于 Transformer 的语言模型。很多初学者直接上 Transformer,认为越新的结构越好,但古诗生成这个任务有一个特殊性:句子长度极短(五言 10 个字、七言 14 个字),而韵律要求极高。Transformer 的自注意力机制擅长捕捉长距离依赖,但在这么短的序列上它的优势发挥不出来;反而是 LSTM 的隐状态天然带有「顺序感」,对字与字之间的衔接和平仄交替更敏感。

我自己的经验是:数据量在 10 万首以内、以绝句和律诗为主时,LSTM 的生成质量明显好于同规模 Transformer;数据量到 50 万首以上时,Transformer 才有机会反超。如果你的目标是做一个能跑通、能生成像样古诗的系统,LSTM 是最稳妥的起点。等到后续要扩展词牌生成、或者做风格迁移时,再迁移到 Transformer 也不迟。选 LSTM 还有一个工程原因:训练成本低,CPU 也能跑通小规模实验,调试迭代周期短。

3.2 一个能跑的生成模型配置:嵌入、隐层、Dropout 与学习率

下面是一份我实际用过的 LSTM 古诗生成模型配置。它不是最大最强的参数组合,而是「性价比」最高的:在单张消费级显卡上能几个小时完成训练,生成质量已经足够做演示和进一步的趣味分析。

import torch import torch.nn as nn class PoemLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2, dropout=0.3): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, x, hidden=None): emb = self.embedding(x) out, hidden = self.lstm(emb, hidden) logits = self.fc(out) return logits, hidden

这份配置里的关键参数有三个。第一个是embed_dim=128:古诗词的常用字大约在 3000 到 6000 之间,128 维的嵌入已经足够表达字形和语义的区分;调大到 256 收益很小,但训练时间增加明显。第二个是hidden_dim=256配合num_layers=2:两层 LSTM 比单层能捕捉到更高层次的句法特征,但三层以上在这么短序列上容易过拟合。第三个是dropout=0.3:这个值是在「生成多样性」和「训练稳定性」之间折中的结果,调大到 0.5 会让训练损失下降变慢但生成文本更不重复。

训练时的学习率我从 0.002 起调,配合余弦退火。如果发现训练损失震荡不降,首选把学习率降到 0.001,而不是调模型结构。优化器用 Adam 就好,它的自适应步长对这种小规模文本任务几乎不需要额外调参。

3.3 控制押韵与平仄:解码时的硬约束与软引导

模型本身不会主动押韵,它只知道统计规律。要让生成的诗真的「像诗」,必须在解码阶段加入韵律约束。常见做法是硬约束:在生成最后一个字时,只在「与上一句韵脚同韵部」的字里做采样。具体实现上,decode 时维护一个当前句的韵脚字,在最后一步把词汇表中不符合韵部的字概率全部置为负无穷。

def constrained_sample(logits, rhyme_restrict=None, temperature=0.8): # logits: (vocab_size,) 模型输出的原始得分 # rhyme_restrict: list,当前韵脚允许的字列表;None 表示不约束 if rhyme_restrict is not None: mask = torch.full_like(logits, float("-inf")) allowed_idx = torch.tensor(rhyme_restrict, dtype=torch.long) mask[allowed_idx] = 0 logits = logits + mask probs = torch.softmax(logits / temperature, dim=-1) return torch.multinomial(probs, 1).item()

温度参数temperature控制多样性:调到 0.6 以下,生成内容趋于保守、重复度高;调到 1.0 以上,会出现大量不通顺的组合。我一般固定在 0.8 做演示,如果有人想玩「更随机」的版本就调到 1.1。平仄约束比押韵更难做,因为平仄是声调属性而不是字形属性,需要额外维护一个「字-平仄」对照表。我的做法是软引导:在解码时给符合平仄要求的候选字乘以 1.2 的权重,而不是硬性屏蔽,否则字表太窄时会出现选不出字的情况。硬约束押韵、软引导平仄,这个组合是生成效果和鲁棒性之间最稳的平衡点。

4. 情感分析模块:从「愁」字到「愁绪」的分类路径

4.1 关键词词典之外:为什么古诗情感不能直接用现代情感词典

情感分析这块,最容易走的路也最容易翻车:直接调用现成的现代中文情感词典,比如把「难过」「开心」这类词的情感极性映射到古诗上。古诗的表达方式决定了这条路走不通——诗人极少直接写「我很悲伤」,而是写「孤舟」「寒江」「残月」。这些字词在现代情感词典里可能完全没有极性标注,但在古诗语境里是强烈的悲伤信号。反过来说,「东风」在现代语境常被联想到温暖或希望,但在「东风无力百花残」里是颓丧的意象。词典方法的问题不是准确率低,而是它对古诗的「意象隐喻」完全无感。

所以这个模块的完整技术路径是:先做一个基于标注语料训练的文本分类模型,把整首诗或整句映射到七类情感之一;在此基础上,可以叠加一个意象词表做解释性输出——告诉用户「模型判断为悲伤,依据是出现了‘孤’‘残’‘寒’三个高相关意象」。这样既保证了分类能力,又让系统输出可解释。

4.2 基于情感标签微调的分类模型:输入与输出设计

我建议直接用预训练中文语言模型(如 BERT 类模型)做情感分类,然后在前面提到的人工标注数据集上微调。古诗文本短,不需要分句做长文本处理,直接把整首诗拼接成一句话输入即可。输入格式上有个小技巧:在诗的关键位置插入分隔符,让模型能区分「题目」「上句」「下句」。我常用的输入格式是「[CLS] 诗题 [SEP] 全诗文本 [SEP]」,诗题作为先验信息往往能提供强烈的情感线索。

from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-roberta-wwm-ext") model = AutoModelForSequenceClassification.from_pretrained( "hfl/chinese-roberta-wwm-ext", num_labels=7 ) def predict_emotion(poem_text: str, title: str = "") -> dict: # 拼接输入:标题 + 分隔符 + 正文 text = f"{title}[SEP]{poem_text}" inputs = tokenizer(text, return_tensors="pt", max_length=128, truncation=True) outputs = model(**inputs) probs = torch.softmax(outputs.logits, dim=-1).squeeze() label = torch.argmax(probs).item() return {"label": label, "probabilities": probs.tolist()}

微调时的超参数我固定这么设:batch size 16,学习率 2e-5,epoch 数 5。数据量如果在 5000 条以上,这个配置基本能在验证集上收敛;如果不到 5000 条,加入早停,验证损失连续 3 个 epoch 不降就停止。另外要注意max_length=128对这个任务来说是足够的,七言律诗全文加标题也就 60 到 70 个字符,不需要开更长。

4.3 情感强度与演化轨迹:比分类标签多一步的解读

单纯输出「悲伤」标签是初级版本。完整一点的情感分析系统还会做强度和演化轨迹分析。强度方面,把七类情感各分三档(弱、中、强),用模型输出概率的分布特征来映射:如果模型对某类的概率超过 0.7 判定为强,0.5 到 0.7 判定为中,低于 0.5 判定为弱。这个映射虽然粗糙,但实测可用。

演化轨迹是针对长诗(律诗、排律或词)的处理:按句切分后逐句做情感分类,看整首诗的情感从第一句到末句是怎么流动的。常见规律是「起句平淡、承句展开、转句起伏、合句升华」,情感轨迹能很直观地反映这种结构。这个功能对文学研究者来说比单独的分类标签有价值得多,也更容易成为系统里区别于普通情感分析工具的亮点。实现上不复杂,就是把上一节里的predict_emotion对每一句各调用一次,再把结果按句号顺序拼接成一条情感曲线。

情感类别示例典型意象三档强度参考常见于诗体
悲伤孤、残、寒、泪出现 1 个弱 / 2 个中 / 3 个以上强晚唐诗、悼亡诗
思乡归、故园、雁、月叠用且末句点题则强羁旅诗
山水隐逸山、云、林、渔全诗无情绪词、纯写景则中王维山水诗
讽喻朱门、酒肉、白骨反讽修辞出现则强新乐府

5. 系统联调与避坑:生成重复、情感错位、数据稀疏的排查记录

5.1 现象:模型反复生成同一句,越跑越僵

训练完成后的模型在生成时经常会吐出一模一样的句子,尤其是同一首诗里第二句和第四句结构相同的情况。最开始我以为是模型过拟合,但看训练损失明明还有下降空间。原因出在解码策略上:贪心搜索每次取概率最高的字,一旦某条路径在早期占优,后续全靠这条路走到底。解决办法是用temperature=0.8的采样替代贪心搜索,同时在解码时对已经生成的 n-gram 做重复惩罚——如果某个连续两个字或三个字的组合已经出现过,就在下一轮采样时把它们的概率乘一个 0.5 的折扣。这个「采样 + 重复惩罚」的组合是解决生成内容单一问题的最直接手段。

5.2 现象:七绝的第三句不押韵,甚至是仄声收尾

七绝的押韵规则是第二句和第四句押韵,第一句可押可不押,第三句按格律要求必须仄声收尾。模型不懂这套规则,训练数据里虽然有规律,但总有例外。于是生成结果经常出现第三句用了平声收尾、和第二句的韵脚「撞韵」的情况。解决方式是在解码阶段引入「句位感知」的硬约束:生成第三句时,把末字限定在仄声字表内;生成第二句和第四句时,限定在与第一句韵脚同韵部的字内。实现上的要点是先在词汇表里预制「平声字表」「仄声字表」「按韵部划分的韵脚字表」,然后在constrained_sample里传入对应字表。这类约束本质上是对格律规则的硬编码,比让模型自己学可靠得多。

5.3 现象:情感分析把「独钓寒江雪」的整首诗判成负面

「千山鸟飞绝,万径人踪灭。孤舟蓑笠翁,独钓寒江雪。」柳宗元这首诗用模型跑出来经常被判成「悲伤」,但实际上它更接近「山水隐逸」——虽然字面全是孤寂意象,但诗人的情感取向是清高、超脱。原因在于模型只学到了字面意象和情感标签的相关性,没有学到「诗人对意象的态度」。解决这个问题的常见做法是在标注环节增加一层「情感极性」标注:这首诗整体是正面的欣赏、还是负面的哀叹、还是中立的写景。把「情感类别」和「情感极性」两个标签同时作为训练目标,模型才有机会区分「写孤独但享受孤独」和「写孤独且痛苦」。如果只用七分类标签,这类样本几乎无解。

5.4 现象:训练损失不降反升,生成内容变成乱码

我在训练 LSTM 时遇到过一种典型情况:损失函数在前几个 epoch 正常下降,随后突然反弹,紧接着生成内容变成无意义的字排列。排查后发现是学习率设置过大加上梯度裁剪缺失,导致 LSTM 隐状态在长序列反向传播时发生了梯度爆炸。LSTM 对梯度爆炸比 Transformer 敏感得多,解决方案有两个:一是设置梯度裁剪clip_grad_norm_(model.parameters(), max_norm=5.0),二是把学习率从 0.002 降到 0.001。如果两个方案同时做,效果最稳。另外一个容易被忽略的点是:embedding 层的初始化不要把padding_idx=0也设为可训练为随机值,必须显式把 0 号位置的嵌入向量置为零向量,否则训练早期会不稳定。

6. 把这个系统用到能交付:生成质量评估与参数收敛的验证方法

6.1 自动评估指标:困惑度、BLEU 与重复率的配合使用

生成类系统的评估不能靠肉眼感觉。我习惯同时看三个自动指标。第一个是困惑度(perplexity):模型对验证集的平均负对数似然做指数化,数值越低越好。古诗任务里困惑度降到 20 以下,生成文本基本通顺;高于 50 就意味着模型还没学好。第二个是 BLEU:拿生成的句子和训练集里相似主题的句子做 n-gram 重叠度对比,这个指标不能高,太高说明模型在背训练集,太低说明生成内容和古诗的句法差距大。古诗生成的 BLEU 在 15 到 30 之间算合理。第三个是重复率:统计生成诗里连续两字组合的出现频次,超过 15% 就需要调高重复惩罚。

6.2 人工评估维度与样例打分的操作表

自动指标代替不了人工判断。我设计过一套人工打分表,每首诗打三个维度:通顺度(0-5 分,句子是否完整、符合语法)、押韵度(0-5 分,韵脚是否符合规则)、意象连贯度(0-5 分,整体意境是否统一、有没有前后断裂)。评估时找三个人各打一遍取平均值。一个能交付的生成系统,三个维度的平均分应该分别达到 4.0、4.5、3.5。低于 3.5 的生成结果不应该上线展示。情感分析模块的人工验证则是抽样 200 首诗,对比模型输出标签和标注标签的宏平均 F1,F1 到 0.8 以上才是可信水平。

6.3 从模型到系统:接口设计与返回结构的建议

最后的落地形态通常是一个 Web 服务:用户输入一个主题词或一句上联,系统返回一首生成的诗和每句的情感分析结果。接口返回结构我建议做成「诗文本 + 逐句情感标签 + 整体情感分布 + 韵律分析」四段式。

{ "poem": "孤舟夜泊寒江畔,独对青山忆旧游。", "overall_emotion": {"label": "思乡", "confidence": 0.72}, "line_emotions": [ {"line": "孤舟夜泊寒江畔", "label": "悲伤", "confidence": 0.81}, {"line": "独对青山忆旧游", "label": "思乡", "confidence": 0.66} ], "rhyme": {"pattern": "仄仄平平平仄仄,仄仄平平仄仄平", "rhyme_char": "游"} }

这个返回结构既满足了展示需求,又让情感分析和生成结果互相印证——用户看到「系统说这首诗有归乡情绪」的时候,能立刻在逐句标签里找到依据,而不是面对一个黑匣子。做这个系统到最后,我最大的教训是:不要试图让模型同时做生成和情感分析。两个任务共享数据但各自训练、各自调优,最后在服务层合并,这是最省力也最不容易互相拖累的架构。数据清洗和标注花的精力远大于模型训练,但它是整个系统的地基。希望这份踩坑记录能让你少走几段弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:16:41

Git全流程操作手册:从安装配置到分支合并与SSH认证实战

写这篇 Git 操作全流程手册&#xff0c;是因为我在各种团队和项目里见过太多因为 Git 使用不当而浪费时间的事故。提交信息乱写、分支合并一团糟、SSH 认证失败后什么都连不上&#xff0c;这些坑几乎每个人都会踩一遍。Git 是分布式版本控制系统&#xff0c;它真正解决的核心问…

作者头像 李华
网站建设 2026/10/6 3:16:23

从吼英语到流利口语:高阻力训练与肌肉记忆的实操指南

先说一个可能没人信的事实&#xff1a;把我从大山里带出来的那张车票&#xff0c;不是火车票&#xff0c;是一口在楼顶喊出来的英语。我爸至今不明白&#xff0c;为什么我每天傍晚要爬上自家平房顶&#xff0c;对着对面的山坡吼半个钟头。他只知道后来我考上了外语专业&#xf…

作者头像 李华
网站建设 2026/10/6 3:16:22

前后端实时通信选型:短轮询、长轮询、SSE与WebSocket对比

前阵子做内部工单系统&#xff0c;需求是后端任务状态一变&#xff0c;前端要立刻弹红点提示&#xff0c;顺便把消息卡片推出来。同事第一反应是“直接上WebSocket”&#xff0c;我翻了翻需求&#xff0c;最后选的却是SSE。这类选型分歧在前后端实时通信里太常见了——很多人把…

作者头像 李华
网站建设 2026/10/6 3:15:42

无模型自适应控制(MFAC)原理与Matlab/Simulink仿真实现

刚开始接触无模型自适应控制&#xff08;MFAC&#xff09;的时候&#xff0c;大部分人第一反应是“不依赖模型&#xff0c;那参数到底从哪来”&#xff1f;这个问题我当年也问过自己。MFAC的核心逻辑&#xff0c;是对被控系统在相邻采样点之间的动态变化做紧格式动态线性化&…

作者头像 李华
网站建设 2026/10/6 3:15:10

数据结构绪论全解析:逻辑结构、存储结构与复杂度分析

我当年考研复习数据结构&#xff0c;翻开王道的书&#xff0c;第一页就是绪论。说实话&#xff0c;第一遍我基本没看懂&#xff1a;这一章不就是在介绍“什么是数据结构”吗&#xff1f;后面写代码的时候不就知道了吗&#xff1f;后来我才发现&#xff0c;这一章是整个数据结构…

作者头像 李华
网站建设 2026/10/6 3:14:18

Xcode调试时搜索不到iOS模拟器?常见原因与完整排查步骤

很多iOS开发者第一次遇到“Xcode调试时搜索不到iOS模拟器”这个问题时&#xff0c;第一反应都是怀疑自己是不是把Xcode弄坏了。尤其是照着教程走&#xff0c;到了选设备那一列&#xff0c;发现上面空空如也&#xff0c;只有一个“Other”选项&#xff0c;或者是明明项目支持模拟…

作者头像 李华