ARTICLE DETAIL

资讯详情

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

古诗自动生成与情感分析:NLP项目实战指南

古诗自动生成与情感分析:NLP项目实战指南 简介一套基于机器学习与自然语言处理的古诗自动生成与情感分析系统面向自然语言处理学习者、算法工程师和毕业设计学生完整覆盖古诗语料爬取、数据清洗、分词与词频统计、规则作诗及机器学习写诗全流程。资源包总大小337.22MB包含156个文件其中43个Python脚本用于爬虫、训练和推理73个txt文档存放语料与中间结果6组模型checkpoint和index提供训练好的权重另有词向量bin文件、可视化png和说明文档等目录结构按模块组织便于定位和使用。目前已有295人学习下载数据、代码和模型均已打包整理可直接运行验证。通过这套资源读者既能理解传统规则作诗的逻辑也能动手实操基于深度学习的古诗生成与情感分析任务适合作为课程设计或毕业设计的完整参考方案。1. 古诗自动生成 情感分析这个NLP入门级项目为什么让不少人翻车看到这个标题冲进来的人多半已经把“LSTM跑诗”的demo跑通了正准备把情感分析接上去做成毕业设计或作品集项目。真实翻车过程往往是这样模型训练倒是收敛了可生成出来的五言只有前两句像样后两句要么重复高频字要么平仄对仗全线崩溃情感模型把“遍插茱萸少一人”判成中性把“白日依山尽”判成忧愁。问题不在模型本身在于你把古诗生成当成了自由文本生成而没把它当成一个带约束的生成任务。一个能自圆其说的系统必须在格律、押韵、情感三重约束下产出句子并用情感分析模块做验收。这套方法适合课程设计、毕业设计也适合想做个中文自然语言处理作品集项目的开发者。2. 技术选型生成模型与情感模型为什么不能都无脑上BERT2.1 生成模型选择字符级LSTM为什么是我推荐的系统起点古诗自动生成与情感分析这个组合前半段任务最稳的起步方案不是预训练大模型而是字符级LSTM。原因很直接古诗字表小常用汉字八千到一万序列短五言绝句20字七言绝句28字LSTM的长程依赖足够覆盖。预训练大模型不是不行而是中文古文预训练语料稀缺微调成本高而且它对格律没有先验知识生成的字面更像诗平仄照样错。我前后对比过两条路线LSTM加格律后处理比直接让Transformer从零生成要好控得多等系统链路通了再换大模型做增量才是稳妥路径。开门见山定义生成器结构import torch import torch.nn as nn class PoemGenerator(nn.Module): def __init__(self, vocab_size, embed_size256, hidden_size512, num_layers2, dropout0.3): super().__init__() self.embed nn.Embedding(vocab_size, embed_size) self.lstm nn.LSTM(embed_size, hidden_size, num_layersnum_layers, dropoutdropout, batch_firstTrue) self.fc nn.Linear(hidden_size, vocab_size) def forward(self, x, hiddenNone): emb self.embed(x) # [B, T, D] out, hidden self.lstm(emb, hidden) # out: [B, T, H] logits self.fc(out[:, -1, :]) # 只看最后一个时间步 return logits, hiddenforward里只取最后一个时间步输出对应的训练目标是“看到前若干个字预测下一个字”。输入x维度是[B, T]的LongTensorT是句长输出logits是[B, vocab_size]直接用nn.CrossEntropyLoss()算损失。训练时用的是Teacher Forcing把真实的前一字序列喂进去而不是把自己的预测再喂回去这样收敛更稳定。这个区别容易写漏一旦推理时没拿对隐藏状态生成就会飘。字符级建模还有额外好处生成是逐字进行的模型输出和最终诗歌文本天然对齐不用处理词边界。参数我给一组能直接复用的默认值参数建议值说明embed_size256汉字表不过万256维足够承载字义hidden_size512LSTM容量性能与训练速度的平衡点num_layers2一层学不到句间结构三层以上容易过拟合dropout0.3层间dropout只对多层生效vocab_size8000~10000来自字表过大会稀释训练信号训练循环我习惯这样写optimizer torch.optim.AdamW(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, eta_min1e-5) criterion nn.CrossEntropyLoss() for epoch in range(30): for x, y in loader: # x:[B,T], y:[B] logits, _ model(x) loss criterion(logits, y) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step() if epoch % 5 0: print(fepoch {epoch}, loss {loss.item():.3f})梯度裁剪clip_grad_norm_是关键参数。LSTM在长序列上反传很容易梯度爆炸不裁或裁太大比如10.0训练到第20个epoch loss会突然变成NaN裁到5.0配合余弦退火基本能稳下来。学习率从1e-3起步按余弦降到1e-5比固定学习率少调参。如果loss卡在4.6到4.8不动这接近均匀分布的交叉熵值先别加层检查数据是否按诗篇切分以及有没有漏掉eos标识。2.2 情感分析选型规则词典为什么只能做基线第二个模块是情感分析第一反应是拿现成情感词典匹配关键词但这条路在古诗上很快撞墙。古诗表达情感靠意象叠加而不是显式情感词“感时花溅泪”没有一个“悲”字词典召回率远低于现代评论语料。词典只能当冷启动标注工具真正要做模型需要走到预训练语言模型微调这一步。我推荐bert-base-chinese而不是追新追大。原因有三古诗单句最长十几个字大模型的优势发挥不出来标注样本本来就少模型越大越容易过拟合BERT在中文基础分类任务上的能力对这个任务足够。微调只改分类头num_labels 6 # 喜、怒、哀、乐、愁、思 model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labelsnum_labels) tokenized tokenizer(text, max_length12, paddingmax_length, truncationTrue, return_tensorspt)max_length12是基于单句长度的实测选择五言单句5个字加上CLS和SEP12够用七言单句7个字也给到12。设太大只会引入无效padding拖慢推理。六分类比现代文本常用的正负二分类更贴合古诗表达但类别不均衡问题严重训练时要在损失函数里加类别权重否则模型会学成“永远输出高频类”的偷懒分类器。2.3 系统架构生成、格律校验、情感评分三条流水线两个模型训练好接下来是系统编排。常见误区是把生成模块和情感模块直接串成“生成一句→情感打分→再生成下一句”这会慢到没法在线演示而且情感模型对每个中间字打分没有意义。我一般把系统拆成三段生成器产出整首候选诗格律校验器做硬过滤情感模型对通过的诗句做软评分。格律校验要在情感打分之前因为平仄是硬约束情感分是软信号先让硬约束筛掉不合格句子能省下大量无效推理。阶段输入输出约束类型生成种子字符 可选情感标签10到30首候选软格律校验候选诗通过或淘汰硬情感评分通过的诗按情感匹配度排序软在2.1到2.3里你已经能看到这个系统的轮廓生成器负责任务主体格律校验负责兜底情感分析负责验收。后面章节就按这个顺序展开数据、训练和坑。3. 数据准备把公开古诗语料清洗成训练集的三步3.1 语料来源与清洗先按格式留再做MD5去重数据源常见做法是拿公开唐诗库的JSON文件每行一条记录字段一般有title、author、paragraphs。但直接拿原始数据训练会翻车里面有大量杂言古体、残缺句、异体字。第一步是按格式筛选只留四句且每句字数相同的绝句想做律诗就改成八句。import json poems [] with open(poet.tang.json, encodingutf-8) as f: for line in f: item json.loads(line) body item.get(paragraphs, []) if len(body) 4 and all(len(s) 5 or len(s) 7 for s in body): poems.append({ title: item.get(title, ), text: .join(body), form: 五绝 if len(body[0]) 5 else 七绝 }) print(f筛选后剩余 {len(poems)} 首)len(body) 4保证是四句all(len(s) 5 or len(s) 7 ...)保证每句要么五言要么七言杂言直接滤掉。筛选后要人工抽查几十条确认没有标点残留或断句错误。公开语料经常会收同一首诗的多版本不去重会让模型把原诗背下来生成时整句复现泛化能力差。去重我习惯用“题目首句”拼起来做MD5import hashlib seen set() dedup [] for p in poems: key hashlib.md5((p[title] p[text][:4]).encode(utf-8)).hexdigest() if key not in seen: seen.add(key) dedup.append(p) print(f去重后剩余 {len(dedup)} 首)“题目首句”去重比全文去重更稳妥因为同一首诗在不同版本里个别字可能不同但题目和首句基本一致。这套逻辑能去掉约百分之十几的重复数据。如果换数据源先print(item.keys())确认字段名不要照抄。3.2 字级Tokenization为什么不选jieba分词模型输入建议走字级不是词级。古诗分词歧义太重“明月”算一个词还是两个“床前明月光”在不同词法体系里能切出不同结果词边界注释会对生成产生干扰。字级序列天然没有这个问题而且生成任务本身就是逐字输出字级和模型输出对齐最简单。字表构建和序列化如下from collections import Counter def build_vocab(poems, min_freq2): cnt Counter() for p in poems: cnt.update(p[text]) chars [pad, unk, eos] [ c for c, n in cnt.items() if n min_freq ] char2idx {c: i for i, c in enumerate(chars)} idx2char {i: c for c, i in char2idx.items()} return char2idx, idx2char def poem_to_seq(poem, char2idx, max_len30): seq [char2idx.get(c, char2idx[unk]) for c in poem[text]] seq.append(char2idx[eos]) seq seq[:max_len] [char2idx[pad]] * (max_len - len(seq)) return seqmin_freq2会把只出现一次的生僻字并入unk把字表控制在一万以内如果设成1字表膨胀到两三万低资源下模型学不好。eos必须加在每首诗末尾否则模型不知道在哪里停。max_len对五言绝句20字加eos足够给30是为了兼容七言二十八字。训练样本用滑窗切分def make_sliding_windows(seq, window8): xs, ys [], [] for i in range(len(seq) - window): x seq[i:i window] y seq[i window] xs.append(x) ys.append(y) return xs, ys窗口设8意思是每次看前8个字预测第9个字。窗口太大对短诗没有意义反而让训练样本数变少窗口太小模型看不到句间结构。你可以在8到12之间尝试对生成质量影响不大但会影响训练收敛速度。3.3 情感标注只有几百条标注时能怎么自救情感分析最缺的是带标签的古诗句。公开情感词典覆盖现代评论还行对古诗“意象到情感”的映射几乎帮不上忙。我常用的做法是三步先拿情感词典按关键词匹配给语料打初标再把初标样本交给一个现成文本分类模型排序抽低置信样本人工修正最后迭代两轮积累几百条高质量标注后微调BERT。规则初标的示意代码emotion_keywords { 喜: [笑, 春, 花], 哀: [悲, 泪, 残], 愁: [愁, 孤, 寒], 思: [月, 归, 君], } def rough_label(text): scores {e: 0 for e in emotion_keywords} for e, words in emotion_keywords.items(): for w in words: if w in text: scores[e] 1 best max(scores, keyscores.get) return best if scores[best] 0 else None这个词典只是示意真正要做需要换更全的词表并且处理多音字和否定结构比如“不悲”不能算悲。规则初标最大的问题是噪声低置信样本要么丢要么人工改。迭代两轮之后标注质量会比词典本身可靠得多。4. 模型训练与联动生成、情感、以及怎么让它们对话4.1 生成模型训练从损失下降到能读需要动哪些配置训练生成模型的完整套路和参数我在2.1给了这里补两个容易踩的细节。第一数据集切分必须按篇切不能把同一首诗的前14字放训练、后14字放验证否则验证集全变成记忆题指标虚高。第二推理时的首字来源。我推荐的简单方案是训练时随机选一个诗内位置作为起点让模型学续写推理时用用户指定字或者用eos后的第一个字作为首字。采样配置比训练配置更影响观感def generate(model, char2idx, idx2char, seed_chars春, length24, temperature0.9, topk20): model.eval() inputs [char2idx.get(c, char2idx[unk]) for c in seed_chars] hidden None with torch.no_grad(): for _ in range(length): x torch.tensor([inputs[-1:]], dtypetorch.long) logits, hidden model(x, hidden) logits logits[0, -1] / temperature probs torch.softmax(logits, dim-1) top_probs, top_idx torch.topk(probs, topk) chosen top_idx[torch.multinomial(top_probs, 1).item()] inputs.append(chosen.item()) return .join(idx2char[i] for i in inputs)temperature控制随机性0.8到1.0之间最像诗小于0.5会频繁重复大于1.5会散得不成句。topk20把采样范围限制在概率最高的20个字能压住生僻字噪声。这两个参数一调生成质量可以从“读不懂”变成“像诗”值得多做几轮对比。生成器内部是黑匣子但采样配置是你最能直接控制的旋钮。4.2 情感分析模型训练BERT在古诗上的微调细节情感模型的训练评估不能只看准确率。古诗情感类别严重不均衡“怒”可能只有几十条样本“乐”可能几百条模型会把所有样本都判成高频类。我训练时会在每个epoch算混淆矩阵重点看少数类召回率。给损失函数加类别权重class_weight torch.tensor([1.0, 2.0, 3.0, 0.8, 1.0, 1.5]) criterion nn.CrossEntropyLoss(weightclass_weight.to(device))weight越大模型对漏判这个类别的惩罚越重。但放大太多会反向过头模型又过度输出那个类别需要拿验证集F1来调。微调BERT的常用配置是batch_size16到32学习率2e-5epoch数3到5。古诗文本短一个epoch很快一张消费级显卡几分钟能跑完。训练结束后把低置信样本打印出来人工看一遍通常能发现标注错得离谱的数据这步叫标注清洗比调结构更值得投入。4.3 条件生成把情感标签灌进生成器的三种做法想把目标情感写进诗里有三种做法条件向量、后过滤、推理时打分。我建议先做后过滤因为它不动生成器链路最简单等效果不够再做条件向量。条件向量是给生成器的embedding拼一个情感类别embeddingclass EmotionPoemGenerator(nn.Module): def __init__(self, vocab_size, num_emotions, embed_size256, hidden_size512): super().__init__() self.embed nn.Embedding(vocab_size, embed_size) self.emo_embed nn.Embedding(num_emotions, embed_size) self.lstm nn.LSTM(embed_size * 2, hidden_size, num_layers2, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_size, vocab_size) def forward(self, x, emotion_id): emb self.embed(x) emo self.emo_embed(emotion_id).unsqueeze(1).expand(-1, x.size(1), -1) emb torch.cat([emb, emo], dim-1) out, hidden self.lstm(emb, None) logits self.fc(out[:, -1, :]) return logits, hidden训练时给每条样本传入一个情感标签模型就要学习在给定情感的前提下续写。注意这个方案的前提是训练语料有情感标签没标签就只能走纯后过滤。条件向量和后过滤也可以混用训练用条件向量让生成器提高目标情感相关字的概率推理再叠加情感分排序效果最稳。5. 避坑手册古诗生成与情感分析常见的5个真实案例5.1 现象损失降到4.0以下生成却全是“春春春春春”原因模型学会了输出高频字。按字频统计“春”“月”“风”出现概率极高采样时topk每次都包含它们最终形成循环。这是古诗生成最常见的翻车点。解决训练时给损失按字频加权把高频字权重压低、低频字权重抬高推理时把topk从20降到10并打开重复n-gram过滤禁止同一个字连续出现三次以上。如果你用4.1的生成函数可以在循环里加一个判断如果待选字已经出现在最近两个生成位置就从候选里去掉。5.2 现象生成的诗平仄完全随机二四六字位置混乱原因字符级LSTM只建模字面统计规律模型完全没有“平”和“仄”的概念自然学不到“二四六分明”这类格律规则。很多课程设计止步于此就是没意识到格律是音韵学知识不在字符序列里。解决两步走。第一步在embedding后拼一个平仄特征用pypinyin把每个字标出声调阴平阳平记为“平”上声去声记为“仄”作为二值特征拼进embedding第二步在推理后做格律校验不合格候选直接丢弃。我的经验是格律后处理对“像诗”的提升比换模型更明显先做后处理再回头看模型是否要改。5.3 现象情感分析模型把一切句子都判成“中性”或“思”原因训练语料里“思”类别占比过高BERT又是在现代文本上预训练的对古诗意象表达不敏感于是模型把“思”当成了安全答案。表面看准确率还行一查混淆矩阵少数类召回率全是0。解决对类别权重做重配把“思”的weight压低到0.5同时抽低置信样本人工修正扩充“喜”“怒”标签。还有一个实用技巧推理时不看argmax而是给每个类别的输出概率设一个阈值低于阈值就判“中性”避免模型硬挑一个类别。5.4 现象生成模块与情感模块串联后情感分和目标情感相反原因你先让生成器随意生成再调情感模型打分但生成器在训练时没见过“写开心诗”的约束候选里根本没有多少目标情感的好句子。情感模型只是在一堆矮子里拔将军。解决要么改用4.3的条件生成器把情感标签作为输入要么扩大候选池一次生成30首再打分。常见做法是两者结合候选池调大之后情感匹配率能提升至少两成。这个坑提醒我串联两个模块时先确认上游有没有能力产出下游想要的信号否则下游再准也没用。5.5 现象Windows下运行训练脚本DataLoader报BrokenPipeError原因Windows上PyTorch的DataLoader默认num_workers 0时多进程spawn和Jupyter的运行方式不兼容子进程还没初始化完就报管道断开。这个坑跟模型无关但会很磨人。解决把DataLoader(..., num_workers0)如果一定要多worker把训练代码包进if __name__ __main__:并在脚本里调用multiprocessing.freeze_support()。线上服务器一般没事本地Windows跑课程设计十有八九会碰到提前设好能省半天时间。6. 落地验证从自动指标到人工测评的三层验证法6.1 自动评估指标困惑度、格律符合率、情感准确率指标说明参照值困惑度模型在验证集上的平均负对数似然古诗任务里低于350算正常格律符合率生成句中二四六字平仄正确的比例后处理前达到60%以上情感准确率情感模型在人工标注测试集上的F1六分类F1超过0.35说明信号可用困惑度只看模型有没有学会语言结构不能反映格律格律符合率能直接判断后处理是否生效情感准确率验证情感模块是否在工作。三个指标分开看不要合成一个总分否则出问题不好定位。6.2 人工盲评三个人五首诗四个评分项自动指标没法判断意境古诗生成项目最终还是要人看。常见做法是找三个人每人给五首生成诗按“格律”“通顺”“意境”“情感吻合度”四项各打1到5分。评分前把诗随机打乱不要让评审看到哪个版本、哪台机器生成的。人工评分方差大至少三个人取平均如果同一首诗两个评审分差超过2分再拉第四个人仲裁。这套盲评流程比任何自动指标都更能说服答辩评委或面试官。6.3 一个可复现的推理串联脚本最后给一个把生成、格律校验、情感打分串起来的推理脚本。格律校验是示意级实现不要拿去当严格的音韵判定。from pypinyin import pinyin, Style TONE_MAP {1: 平, 2: 平, 3: 仄, 4: 仄} def rough_tone(chars): tones [] for c in chars: s pinyin(c, styleStyle.TONE3, heteronymFalse)[0][0] if s[-1].isdigit(): tones.append(TONE_MAP[s[-1]]) else: tones.append(平) # 轻声按平处理 return tones def pipeline(seed春, target_emotion喜, num_candidates10): results [] for _ in range(num_candidates): poem_text generate(model, char2idx, idx2char, seed_charsseed, length20) # 五言绝句 lines [poem_text[i * 5:(i 1) * 5] for i in range(4)] # 简易格律过滤二四句末字应为平声 if not (rough_tone(lines[1])[-1] 平 and rough_tone(lines[3])[-1] 平): continue sent tokenizer(poem_text, max_length28, paddingmax_length, truncationTrue, return_tensorspt) probs torch.softmax(emotion_model(**sent).logits, dim-1) emotion_score probs[0][label2id[target_emotion]].item() results.append((poem_text, emotion_score)) results.sort(keylambda x: x[1], reverseTrue) return results[0] if results else None这段代码体现了我一直在强调的流程先粗格律硬过滤再情感打分排序。generate、tokenizer、emotion_model都来自前面训练好的模块label2id是情感类别映射。先生成整首候选再做格律校验最后情感打分避免逐字调用情感模型的性能灾难。num_candidates设10到15个足够大于30个会让情感模型推理变慢。我最早做这套系统时把格律校验写得太严导致大部分候选被滤掉系统不是没有结果就是永远输出同一个模板后来把校验改成评分子项只看断句错误、字表外字符、重复超限这几类硬条件平仄和情感都做软评分生成质量反而自然多了。这个原则现在成了我的习惯硬约束只保留必要的那几种其余交给模型和数据说话。希望帮到你。本文还有配套的精品资源点击获取
返回列表