ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

古诗生成与情感分析闭环系统:基于LSTM和BERT的NLP实践

古诗生成与情感分析闭环系统:基于LSTM和BERT的NLP实践 简介面向自然语言处理与机器学习学习者这份资源围绕古诗自动生成与情感分析完整覆盖语料爬取、文本预处理、SPSS数据分析、规则作诗和机器学习写诗五个环节适合做课程设计、毕业设计或入门NLP项目实践。压缩包内共156个文件包含43个Python脚本涵盖爬虫、分词、去停用词、模型训练与推理73个txt文件保存语料、训练数据和分析结果6组checkpoint、index和data分片对应不同训练步数的TensorFlow模型另有词向量bin、可视化png、ttf字体及docx说明文档整体约337MB。目前已有293人学习下载适合需要系统参考完整流程的学习者。资源提供了可直接参考的工程目录与训练产物既有基于格律韵脚的规则引擎也包含不同步数的机器学习写诗模型可对比两种生成方式的差异情感分析数据与词向量产物则有助于研究古诗语言风格与情感映射对理解中文NLP落地过程很有帮助。1. 把古诗生成和情感分析放进同一个系统到底图什么很多做机器学习项目的同学一上来就只跑古诗生成结果生成出来的句子通顺归通顺情感是散的上一句还“春风得意”下一句直接“泪满襟”读起来非常跳。反过来只做情感分析的那拨人把手头的古诗词打上积极、消极标签就算交差完全没有用到生成能力。拆开看这两件事都不难难的是让它们在同一个系统里闭环流转模型先把诗写出来再用自然语言处理的情感分析模块去验这首诗的情绪强度和目标不一致就扔回去重写直到交出情绪稳定的作品。这套系统就是干这件事的对正在做机器学习相关毕业设计、或者想同时把生成任务和分类任务串起来练手的人来说是很典型也很有性价比的一个方向。它不解决“写出传世名篇”的问题它解决的是“让生成结果可控、可评价、可迭代”的问题。以七言绝句生成为主场景叠加上中文文本情感分析模块既能完整演示自然语言处理从数据到模型再到服务的链路也给“机器写诗写得好不好”提供了一个相对客观的评估尺子。2. 古诗生成的模型选型LSTM还是Transformer2.1 古诗文本的建模难点以及为什么不能照搬通用模型古诗和新闻、评论、聊天记录最大的差别在于约束条件多。一首七言绝句每句固定七个字还要讲平仄、押韵、对仗语义上又要保持意象连贯。直接拿现代文本语料预训练好的语言模型来续写输出经常是“像现代散文的一句诗”因为它在训练时没见过这么强的格式约束。做这个项目时我的建议是不要一上来就套大参数模型尤其别直接依赖通用预训练模型做古诗续写原因有两个古诗语料远小于现代汉语语料预训练模型在古诗场景上的收益不稳定另外对想在机器学习方向上练手的人来说模型结构可解释、训练过程可控比“跑出个高分”更有价值。所以在选型上常见做法是先用字符级的 LSTM 把基线跑通拿到一批能看的输出再决定要不要切到 Transformer。LSTM 的优势是处理定长序列的古诗很自然每个 time step 吐一个字五个字或七个字的诗正好对应五个或七个隐状态缺点是对长距离依赖的建模能力弱于带自注意力的 Transformer。用在绝句这种短文本上LSTM 的劣势不明显反而训练快、显存省、代码也好调。2.2 数据清洗和字符级预处理只留五言和七言古诗生成的数据集不需要太大。常见做法是用全唐诗的整理文本网上有现成的版本只需要把每首诗按行拆出来过滤掉题目、作者和注释行。关键点是过滤逻辑必须严格不然模型会学到“一行七个字但夹杂着标点和空格”这种坏习惯。我一般会写这样一个预处理脚本import re from collections import Counter def load_poems(path): poems [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue # 去掉常见标点全半角都要处理 line re.sub(r[。、“”‘’\s], , line) # 只要五言或七言的整句 if len(line) not in (5, 7): continue # 只保留纯汉字行去掉带数字、英文的噪声 if re.fullmatch(r[\u4e00-\u9fff], line): poems.append(line) char_count Counter(.join(poems)) vocab [pad, unk, bos, eos] sorted(char_count.keys()) char2idx {c: i for i, c in enumerate(vocab)} idx2char {i: c for c, i in char2idx.items()} return poems, char2idx, idx2char这段代码的作用不仅仅是清洗它同时会把语料转成字符级的字典映射。注意我用bos和eos作为句子的开始和结束标记这比用s、/s更直观而且和后面情感分析模块的 tokenizer 格式不冲突。过滤标点时正则里一定要包含中文标点很多开源语料里夹着全角逗号和句号漏掉的话字符表会凭空多出一堆低频脏词。参数说明len(line) not in (5, 7)这一步看起来很粗暴但古诗数据集的现状就是这样行长短不一先按字数过滤是最可靠的做法。如果你要做的是词或曲再把长度规则改掉。字符表用sorted排序是为了保证每次运行得到相同字典避免模型训练时字符顺序不稳定。2.3 字符级 LSTM 生成模型的最小实现模型部分我用 PyTorch 写一个两层 LSTM。embedding 维度取 128隐层维度取 256两个 LSTM 层叠起来输出层接一个全连接映射到词表大小。词表大小取决于语料规模全唐诗整理完之后常见在五千到八千之间这个规模下模型参数量完全可控单卡训练也很轻松。import torch import torch.nn as nn class PoemGenerator(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim256, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, batch_firstTrue) 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训练时的核心是 teacher forcing每个 time step 的输入按一定概率用真实的下一个字而不是模型上一个时间步的预测结果。我习惯把 teacher forcing 比例设成 0.9 起步每轮训练结束后衰减 0.02最低到 0.5。这样模型先跟着真实语料学稳定再慢慢切换到预测模式不容易在早期训练阶段就崩出各种不存在的字组合。损失函数用交叉熵但要注意掩码。def compute_loss(logits, targets, pad_idx): loss_fn nn.CrossEntropyLoss(ignore_indexpad_idx) # logits: (batch, seq_len, vocab_size) # targets: (batch, seq_len) logits logits.reshape(-1, logits.size(-1)) targets targets.reshape(-1) return loss_fn(logits, targets)这里ignore_indexpad_idx很关键它让模型不需要预测 padding 位置的字符。如果不加padding 位置会贡献大量无意义的梯度训练时 loss 曲线看起来挺低但实际生成质量会莫名其妙变差。2.4 生成阶段的三个必调参数温度、重复惩罚、首字约束模型训练好后生成不能直接argmax。诗和聊天不同诗讲究一定的随机性。我用温度采样控制随机程度。import numpy as np def sample_with_temperature(logits, temperature0.8): logits logits / temperature probs torch.softmax(torch.tensor(logits), dim-1).numpy() idx np.random.choice(len(probs), pprobs) return int(idx)temperature 设 0.8 是折中值。大于 1.0 时生成结果偏散容易跑出“江水东流月如霜”这种意象拼贴感很强的句子低于 0.5 时模型过度保守反复输出训练语料里的高频套话。对五言诗我一般用 0.9因为五言字数少需要更多变化七言诗用 0.7 到 0.8因为七言本身信息量大温度太高容易导致后半句失控。除温度外还有一个容易被忽略的参数首字约束。给模型一个起始字或起始词生成质量和可控性都会提升。比如用户输入“春”字模型从“春”开始续出整句。这个实现方式是把首字作为序列的第一个 token 喂进模型然后从第二步开始采样。很多人在这一步想看“从零生成”但我建议至少给一个主题字否则模型会倾向输出训练集里出现频率最高的那几个开头例如“白日”“青山”“故人”。3. 古诗情感分析文本分类那套方法在这里要动手改3.1 古诗情感的特殊性不是简单分正负面古诗的情感表达方式比现代汉语含蓄得多。“感时花溅泪恨别鸟惊心”里没有直接写“悲伤”两个字但情感强度非常高。常规的文本情感分析模型在电商评论、社交文本上表现不错一放到古诗上就翻车原因是古诗的情感更多依靠意象组合而不是显式的情感词。所以做这个模块时我重新定义了情感标签体系不搞五档或七档只保留三分类加一个强度值积极、消极、中性。强度值是连续量范围从 0 到 1表示情感表达的强烈程度。为什么这样设计因为生成系统下游要做的是“把诗的情绪拉回目标区间”三分类用来做硬过滤强度值用来做软排序两件事分开做比混在一起好调。3.2 用预训练模型做基础情感分类我用bert-base-chinese作为情感分析的基础模型。古诗语料不多从头训练不现实在 BERT 基础上做领域微调是常规做法。先用通用情感数据集让模型学到情感的通用表示再用古诗语料微调让模型适应古典词汇。from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels3 ) def predict_sentiment(text): inputs tokenizer( text, return_tensorspt, max_length64, truncationTrue, paddingTrue ) logits model(**inputs).logits label torch.argmax(logits, dim-1).item() probs torch.softmax(logits, dim-1).tolist()[0] return label, probs这里我做了两个调整max_length设 64 而不是常见的 128因为古诗单句最长七字绝句整首也就二十八字64 个 token 足够覆盖整首诗还留有余量num_labels3和前面的三分类标签对应。输出的probs是三个类别的概率分布我把它存下来后面生成系统要用。3.3 怎么给古诗语料打情感标签半自动标注流程中文古诗可用的现成情感标签数据很少硬标几百首又费时间。我的做法是先用分词工具统计每首诗里的情感关键词把强弱关键词加权得到初步标签然后人工抽检修正。sentiment_words { 悲: -0.8, 愁: -0.7, 泪: -0.6, 孤: -0.5, 寒: -0.4, 喜: 0.8, 乐: 0.6, 春: 0.3, 明: 0.3, 暖: 0.4, } def auto_label(poem_text): score 0.0 for ch in poem_text: if ch in sentiment_words: score sentiment_words[ch] if score 0.3: label 2 # 积极 elif score -0.3: label 0 # 消极 else: label 1 # 中性 intensity min(1.0, abs(score) / 2.0) return label, intensity这个方案很粗糙但作为微调数据的起点够用了。关键是后半句每首诗自动打完标签后我会抽 200 首人工复核。复核的指标不是“标签对不对”而是“四个字的意象组合是否确实传达了这种情绪”。如果错得太多就把情感词表里权重不准的词删掉重来。3.4 微调参数怎么设学习率和训练轮数优先BERT 微调最容易犯的错是学习率设太高把预训练权重破坏掉。我习惯用learning_rate2e-5batch_size16训练 3 到 5 个 epoch。古诗语料小2-3 个 epoch 后验证集准确率就开始波动这时候继续练只会过拟合表现为情感分析在训练集上几乎全对但真实生成的诗句分类结果乱跳。另外微调时要把古诗文本按整首输入而不是按单句输入。整首诗输入能让模型学到句间的情绪递进关系按单句输入会把“窗含西岭千秋雪”这种写景句误判成中性。4. 把生成器和情感分析串成一个可用系统4.1 系统整体架构生成、校验、回滚一个完整的古诗自动生成与情感分析系统运行时的数据流是这个顺序请求进入系统后先生成一个候选诗接着调用情感分析模块对整首诗打标签和强度值。系统比较目标标签和实际标签一致就输出不一致就触发重新生成。重新生成不是简单的重跑而是带着温度变化重新采样让模型探索不同的输出。def generate_with_sentiment( generator, sentiment_model, seed_char, target_label, target_intensity0.5, max_attempts8 ): best_candidate None best_distance 1e9 for attempt in range(max_attempts): poem generate_poem(generator, seed_char, temperature0.7 attempt * 0.05) label, probs predict_sentiment(sentiment_model, poem) if label target_label: distance abs(probs[target_label] - target_intensity) if best_candidate is None or distance best_distance: best_candidate poem best_distance distance if distance 0.2: return poem, label, probs # 记录当前最佳候选避免全部失败时返回空 if best_candidate is None: best_candidate poem return best_candidate, label, probs这段代码的核心逻辑是分步放宽标准。每次生成失败后temperature 递增 0.05从 0.7 逐渐升到接近 1.05。这样前几次生成偏向稳定后几次开始引入更多变化。max_attempts8是经验值写五言诗时 8 次通常能找到目标情感七言诗因为句子长如果前 4 次失败后 4 次也大概率失败这个问题在后面避坑章节会细说。4.2 命令行接口和最小可运行示例系统要可复现直接跑命令就能体验。我不喜欢把接口做得很重一个main.py搞定生成、情感分析和对比即可。python3 main.py \ --seed 春 \ --target_label 2 \ --max_attempts 8 \ --temperature 0.8输出只打印三行生成的诗句、实际情感标签、加标签的概率分布。这样的接口设计有两个好处第一调试时能直观看到一次生成尝试的整体效果第二给后面做 Web API 留了余地命令行参数和 HTTP 请求参数可以一一对应。如果你打算做成 Web 服务常见做法是选 FastAPI 包一层把生成函数和情感分析函数挂到POST /generate接口上。参数校验要注意一点target_label必须限定在 0、1、2 三个值不能传负数或浮点数因为情感分析模型的输出层只有三个类别。4.3 串起来之后最值得关心的事不是生成质量本身系统跑通后最先要观察的指标不是我之前以为的“生成诗是否通顺”而是情感分析的稳定性。生成器每跑一次结果都不同如果情感分析模型本身的预测方差大系统就会处于反复重试状态表现为好几次生成失败后返回一个情感标签完全南辕北辙的结果。解决这个问题的常见做法是把max_attempts从 6 提到 12再不行就检查情感分析模型的微调数据是不是太偏。另外要明确一点情感分析模块和生成模型最好用不同的随机种子。两个模型的随机性独立生成结果和情感判定才不会出现“生成器只产出一种风格”的耦合偏置。我实际操作时给生成器固定seed42给情感分析模型不固定种子效果比两个都固定好。5. 避坑古诗生成与情感分析系统的 5 个常见问题5.1 现象生成的诗句反复出现同一句像是复读机原因训练数据里同一首诗出现多遍或者语料清洗时没去重。很多全唐诗数据集本身就有重复收录同一首诗在多个卷里出现。模型学到高频句后teacher forcing 训练让它在生成初期很自信导致反复输出同一句。解决清洗阶段加一步去重按行内容做哈希过滤。另外在采样时增加重复惩罚项具体做法是给已经生成过的 token 的 logits 减去一个惩罚值我习惯减 1.5效果比调温度更直接。5.2 现象情感分析把七言绝句整首诗判成中性但明显是首悲诗原因绝句后两句经常是写景收尾情感在前两句。整首诗输入时模型注意力被后两句写景内容分散。我一开始用整首诗输入觉得信息更全结果反而导致分类被“空山不见人”这类写景句带偏。解决同时输入整首诗和前两句让情感分析模型各出一个预测最后取两个预测概率的平均值。前两句集中表达情感后两句拓展意境平均后预测方差明显下降。5.3 现象模型生成到第七个字时总是不够七个字多一个或少一个原因LSTM 的batch_first配置和序列长度掩码没对齐。如果训练时序列长度设为 8但输入数据里有长度不足 8 的样本padding 位置被模型当成了有效字符去学习生成时模型会把pad当成一个可预测的字符输出来。解决训练时所有样本先按最长句 pad 到固定长度并在 loss 计算时用ignore_indexpad_idx掩掉 padding 位置。生成阶段强制约束序列长度不生成eos直接截断到目标长度。这两个改动配合长度问题基本消失。5.4 现象情感分析模型在微调数据上准确率很高但系统里实际用起来完全不对原因微调数据里积极和消极样本比例失衡。古诗里悲诗占多数积极类的样本自然少。我最初的数据集里消极样本占 60%积极只有 15%模型学成了“看见古诗就蒙消极”。解决训练前统计标签分布对积极类样本做上采样或者采用 weighted loss。我最后用的是后者把积极类的 loss 权重调成 2.0中性类调成 1.0消极类保持 1.0。微调后实际分类准确率提升了大概十个百分点。5.5 现象训练 loss 一直降但生成结果完全没有诗意只是字与字之间的局部衔接流畅原因这是最典型的“模型没崩溃但也没学到结构”的情况。古诗的格律、押韵、意象是篇章级特征字符级 LSTM 每个 time step 只看到前面的字很难把整句的目标比如押韵变成梯度信号。loss 在下降是因为它学会了高频字的分布但对“押韵”这个全局约束无能为力。解决在生成阶段加规则校验不押韵的诗句直接丢弃重来。常见做法是把每句尾字和首句尾字比较韵母韵母不一致就记为失败。对五言诗和七言诗分别维护一个简单的韵脚表。这个规则虽然机械但很可靠比让 LSTM 自己琢磨押韵效率高得多。6. 更进一步的验证方法从两个分数和一个习惯入手系统做完不是终点怎么向别人证明“这个系统有效”才是关键。我自己的验证方法是两个自动分数加一个手动检查。自动分数一个是生成诗的情感准确率另一个是 BLEU 和参考诗的相似度。情感准确率很好算系统输出 200 首诗人工标出情感标签和系统预测对比就得到准确率。BLEU 用现成库算但参考语料要选好如果拿全唐诗全集做参考BLEU 会低得离谱正确做法是拿同一个主题下的几十首诗做参考集。手动检查是看三个点字数是否符合格式、韵脚是否压上、情绪是否和目标一致。这三个点分别对应格式约束、语感约束和语义约束任何一个不满足系统就需要继续调。我建议把这三个检查项写成一行命令每次调参后批量跑 50 首样本快速对比不同参数组合的输出分布。这是我自己调参下来最有效的习惯先批量产出样本再统一看失败模式而不是每次生成一首就调一次参数。这个项目做下来我最有体会的一点是生成任务和分类任务放在同一个系统里时真正的难点不是模型结构而是怎么让两个模块的误差互相对齐。情感分析的一点点偏见会在生成系统的反复重试中被放大。希望这篇文章能帮你少走这段弯路。本文还有配套的精品资源点击获取
返回列表