ARTICLE DETAIL

资讯详情

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

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

古诗自动生成与情感分析: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_dim128, hidden_dim256, num_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_dim, vocab_size) def forward(self, x, hiddenNone): emb self.embedding(x) out, hidden self.lstm(emb, hidden) logits self.fc(out) return logits, hidden这份配置里的关键参数有三个。第一个是embed_dim128古诗词的常用字大约在 3000 到 6000 之间128 维的嵌入已经足够表达字形和语义的区分调大到 256 收益很小但训练时间增加明显。第二个是hidden_dim256配合num_layers2两层 LSTM 比单层能捕捉到更高层次的句法特征但三层以上在这么短序列上容易过拟合。第三个是dropout0.3这个值是在「生成多样性」和「训练稳定性」之间折中的结果调大到 0.5 会让训练损失下降变慢但生成文本更不重复。训练时的学习率我从 0.002 起调配合余弦退火。如果发现训练损失震荡不降首选把学习率降到 0.001而不是调模型结构。优化器用 Adam 就好它的自适应步长对这种小规模文本任务几乎不需要额外调参。3.3 控制押韵与平仄解码时的硬约束与软引导模型本身不会主动押韵它只知道统计规律。要让生成的诗真的「像诗」必须在解码阶段加入韵律约束。常见做法是硬约束在生成最后一个字时只在「与上一句韵脚同韵部」的字里做采样。具体实现上decode 时维护一个当前句的韵脚字在最后一步把词汇表中不符合韵部的字概率全部置为负无穷。def constrained_sample(logits, rhyme_restrictNone, temperature0.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, dtypetorch.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_labels7 ) def predict_emotion(poem_text: str, title: str ) - dict: # 拼接输入标题 分隔符 正文 text f{title}[SEP]{poem_text} inputs tokenizer(text, return_tensorspt, max_length128, truncationTrue) 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-5epoch 数 5。数据量如果在 5000 条以上这个配置基本能在验证集上收敛如果不到 5000 条加入早停验证损失连续 3 个 epoch 不降就停止。另外要注意max_length128对这个任务来说是足够的七言律诗全文加标题也就 60 到 70 个字符不需要开更长。4.3 情感强度与演化轨迹比分类标签多一步的解读单纯输出「悲伤」标签是初级版本。完整一点的情感分析系统还会做强度和演化轨迹分析。强度方面把七类情感各分三档弱、中、强用模型输出概率的分布特征来映射如果模型对某类的概率超过 0.7 判定为强0.5 到 0.7 判定为中低于 0.5 判定为弱。这个映射虽然粗糙但实测可用。演化轨迹是针对长诗律诗、排律或词的处理按句切分后逐句做情感分类看整首诗的情感从第一句到末句是怎么流动的。常见规律是「起句平淡、承句展开、转句起伏、合句升华」情感轨迹能很直观地反映这种结构。这个功能对文学研究者来说比单独的分类标签有价值得多也更容易成为系统里区别于普通情感分析工具的亮点。实现上不复杂就是把上一节里的predict_emotion对每一句各调用一次再把结果按句号顺序拼接成一条情感曲线。情感类别示例典型意象三档强度参考常见于诗体悲伤孤、残、寒、泪出现 1 个弱 / 2 个中 / 3 个以上强晚唐诗、悼亡诗思乡归、故园、雁、月叠用且末句点题则强羁旅诗山水隐逸山、云、林、渔全诗无情绪词、纯写景则中王维山水诗讽喻朱门、酒肉、白骨反讽修辞出现则强新乐府5. 系统联调与避坑生成重复、情感错位、数据稀疏的排查记录5.1 现象模型反复生成同一句越跑越僵训练完成后的模型在生成时经常会吐出一模一样的句子尤其是同一首诗里第二句和第四句结构相同的情况。最开始我以为是模型过拟合但看训练损失明明还有下降空间。原因出在解码策略上贪心搜索每次取概率最高的字一旦某条路径在早期占优后续全靠这条路走到底。解决办法是用temperature0.8的采样替代贪心搜索同时在解码时对已经生成的 n-gram 做重复惩罚——如果某个连续两个字或三个字的组合已经出现过就在下一轮采样时把它们的概率乘一个 0.5 的折扣。这个「采样 重复惩罚」的组合是解决生成内容单一问题的最直接手段。5.2 现象七绝的第三句不押韵甚至是仄声收尾七绝的押韵规则是第二句和第四句押韵第一句可押可不押第三句按格律要求必须仄声收尾。模型不懂这套规则训练数据里虽然有规律但总有例外。于是生成结果经常出现第三句用了平声收尾、和第二句的韵脚「撞韵」的情况。解决方式是在解码阶段引入「句位感知」的硬约束生成第三句时把末字限定在仄声字表内生成第二句和第四句时限定在与第一句韵脚同韵部的字内。实现上的要点是先在词汇表里预制「平声字表」「仄声字表」「按韵部划分的韵脚字表」然后在constrained_sample里传入对应字表。这类约束本质上是对格律规则的硬编码比让模型自己学可靠得多。5.3 现象情感分析把「独钓寒江雪」的整首诗判成负面「千山鸟飞绝万径人踪灭。孤舟蓑笠翁独钓寒江雪。」柳宗元这首诗用模型跑出来经常被判成「悲伤」但实际上它更接近「山水隐逸」——虽然字面全是孤寂意象但诗人的情感取向是清高、超脱。原因在于模型只学到了字面意象和情感标签的相关性没有学到「诗人对意象的态度」。解决这个问题的常见做法是在标注环节增加一层「情感极性」标注这首诗整体是正面的欣赏、还是负面的哀叹、还是中立的写景。把「情感类别」和「情感极性」两个标签同时作为训练目标模型才有机会区分「写孤独但享受孤独」和「写孤独且痛苦」。如果只用七分类标签这类样本几乎无解。5.4 现象训练损失不降反升生成内容变成乱码我在训练 LSTM 时遇到过一种典型情况损失函数在前几个 epoch 正常下降随后突然反弹紧接着生成内容变成无意义的字排列。排查后发现是学习率设置过大加上梯度裁剪缺失导致 LSTM 隐状态在长序列反向传播时发生了梯度爆炸。LSTM 对梯度爆炸比 Transformer 敏感得多解决方案有两个一是设置梯度裁剪clip_grad_norm_(model.parameters(), max_norm5.0)二是把学习率从 0.002 降到 0.001。如果两个方案同时做效果最稳。另外一个容易被忽略的点是embedding 层的初始化不要把padding_idx0也设为可训练为随机值必须显式把 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 首诗对比模型输出标签和标注标签的宏平均 F1F1 到 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: 游} }这个返回结构既满足了展示需求又让情感分析和生成结果互相印证——用户看到「系统说这首诗有归乡情绪」的时候能立刻在逐句标签里找到依据而不是面对一个黑匣子。做这个系统到最后我最大的教训是不要试图让模型同时做生成和情感分析。两个任务共享数据但各自训练、各自调优最后在服务层合并这是最省力也最不容易互相拖累的架构。数据清洗和标注花的精力远大于模型训练但它是整个系统的地基。希望这份踩坑记录能让你少走几段弯路。本文还有配套的精品资源点击获取
返回列表