做毕设最怕的就是题目看起来很大、很难,最后做出来却没什么亮点。今天要聊的这个题目——Python知识图谱下的中华古诗词可视化、古诗词情感分析、古诗词智能问答、AI自动写诗,前期我一度以为要堆出好几个系统才能过查重,后来拆开一看,其实就是一个“数据采集+知识图谱+NLP+Web展示”的标准组合。这篇文章把整个设计思路、技术选型、核心实现和踩坑记录全部梳理出来,适合正在纠结毕业设计方向、想自己动手做一个能打的东西的人参考。
这个项目能做什么,一句话概括就是把古诗词变成一张可检索、可探索的知识网,再在知识网之上叠加三个应用:可视化让用户看结构,问答让用户问答案,AI写诗让用户得到自动生成的新诗句。而情感分析更像一个贯穿全流程的“滤镜”,统计整体情绪偏向,给每一首诗贴上情感标签。整个系统的技术栈不复杂,难在细节处理,比如古诗词的清洗、实体关系的设计、中文语境下的情感判断。下面我把每个模块怎么落地讲清楚。
1. 项目拆解与整体架构
1.1 古诗词知识图谱能解决什么问题
传统古诗词网站大多按诗人、朝代、诗词标题做列表展示,想找“含有月亮的诗”“写离别的词句”,基本靠搜索引擎。而知识图谱把实体和关系显式建模,诗人、作品、朝代、意象、风格等都被连成一张图。用户搜索一个关键词,系统不仅能返回对应诗词,还能沿着关系拓展出“还有哪些诗人和这位诗人同属一个朝代”“这首诗里出现了哪些常见意象”这类深层信息。
这个项目的核心价值就落在“关系”上。比如用户搜索“月亮”,知识图谱返回的不是一堆句子,而是一组诗、每个诗对应的作者、作者所属的朝代、这些诗共同使用的情感标签。视觉呈现时,节点和连线直接展示这种多维关系,比传统列表漂亮得多,答辩时也很容易讲出亮点。
1.2 五大核心模块的功能边界
系统拆成五个模块,每个模块职责清晰:
| 模块 | 输入 | 输出 | 核心技术 |
|---|---|---|---|
| 数据采集与清洗 | 开源古诗词数据集/网页HTML | 结构化JSON/CSV | requests、BeautifulSoup、正则 |
| 知识图谱构建 | 清洗后诗文数据 | Neo4j图数据库 | jieba、LTP、Cypher |
| 可视化大屏 | Neo4j查询结果 | 关系图、统计图、词云 | Flask、ECharts、pyecharts |
| 古诗词情感分析 | 单篇诗文 | 情感分值加正负中性标签 | 自定义情感词典、TF-IDF、朴素贝叶斯 |
| 智能问答与写诗 | 用户自然语言问题/关键词 | 答案文本/生成诗句 | 模板匹配、Cypher查询、LSTM+Attention |
后端统一用Flask提供REST接口,前端只有三个页面。第一个页面是可视化图谱,展示整体网络;第二个页面是问答交互框;第三个页面是写诗工具。这三个页面共享同一套知识图谱数据,所以数据层设计得好不好,直接影响后面三个功能的开发量。
1.3 技术选型背后的取舍
很多人一上来就想上最新的大模型,我用到的其实是“轻量级大模型方案”。真正的AI自动写诗,毕业设计周期很难从零训练一个大模型,所以我先做了一版基于LSTM+Attention的生成模型,再在展示时保留了一个“智能续写”接口,后续如果有时间可以替换成本地部署的开源中文模型。这个选择的好处是,即使没有高端显卡,训练一个几十MB的小模型也够了,答辩时也能解释清楚“为什么用轻量模型而不是直接调API”。
另一个关键选择是用Neo4j而不是MySQL。诗词数据的核心是实体关联,如果用关系型数据库存储,做多跳查询会很痛苦,SQL写起来又长又难维护。Neo4j的Cypher查询天然适合这类“从一个节点找邻居的邻居”的操作。做可视化时,Neo4j返回的JSON结构也接近前端关系图的数据格式,处理成本极低。
2. 数据准备与知识图谱构建
2.1 古诗词语料采集与清洗
数据是知识图谱的地基,我直接找了个包含几十万首诗词的开源数据集,涵盖唐、宋、元、明、清多个朝代。但原始数据格式混乱,有繁体、异体字、错别字,还有不少不算诗的口水话,所以必须认真清洗。
清洗流程我总结成四步:
- 去重。同一首诗可能在不同诗集里重复出现,按“标题+作者+正文”做MD5去重。
- 转换。把繁体统一转简体,标点统一转成中文全角标点,去掉无关的注释和网页格式符。
- 过滤。保留五言、七言绝句和律诗为主,丢弃残缺的长短句,避免知识图谱里出现残缺数据。
- 分字段。诗词元数据拆成编号、标题、作者、朝代、正文、类型。作者和朝代单独建字典,避免后续实体抽取时反复解析。
这里有一个容易踩的坑:直接用开源数据集时,作者字段经常有“李白”“李 白”“李白(唐)”这类变体。我写了一个作者归一的规则,先按“()”去掉注释,再删除所有空格和标点,最后做同名字典映射。没有这一步,图谱里会出现大量重复作者节点。
2.2 实体识别与关系抽取
知识图谱的实体我定义了五类:作者、诗词、朝代、意象、风格。这五类实体的来源不同:作者、朝代、诗词都来自结构化字段,直接解析即可;意象和风格需要从正文中通过规则抽取。
意象是古诗词里最值钱的实体。我用了一个人工整理的“核心意象词典”,包含月、雪、梅、竹、舟、雁、酒、剑、花、风、云等常见意象,每个意象还带了典型情感倾向标签。比如“月”常关联“思乡”,“梅/竹”常关联“高洁”,“酒/剑”常关联“豪迈”或“愁绪”。抽意象时,我用jieba分词后再做词典匹配。要注意分词可能把“明月”切成一个自定义词,所以需要往jieba词典里补充“明月”“孤舟”“寒山”这类组合词,否则“明”和“月”会被分开,导致匹配不到。
关系抽取更依赖规则。我设计了五类关系:
- 作者创作诗词:
作者 -[:创作]-> 诗词 - 诗人属于朝代:
作者 -[:属于]-> 朝代 - 诗词使用意象:
诗词 -[:包含意象]-> 意象 - 诗词包含情感:
诗词 -[:情感倾向]-> 情感标签 - 同朝代作者关联:
作者 -[:同朝代]-> 作者
同朝代关联不需要单独抽取,建完朝代节点后统一执行一次Cypher批量创建即可。这样能让图谱更密,可视化效果也更好。
2.3 Neo4j批量导入与索引设计
数据量到几万首诗词后,逐条用Cypher插入会很慢。推荐做法是把实体和关系分别导出为CSV,然后用LOAD CSV批量导入。
实体文件poems.csv大概长这样:
id,title,author,dynasty,content,type p001,静夜思,李白,唐代,床前明月光疑是地上霜举头望明月低头思故乡,五言绝句然后执行Cypher:
LOAD CSV WITH HEADERS FROM 'file:///poems.csv' AS row FIELDTERMINATOR ',' MERGE (a:Author {name: row.author}) MERGE (d:Dynasty {name: row.dynasty}) MERGE (p:Poem {id: row.id}) SET p.title = row.title, p.content = row.content, p.type = row.type MERGE (a)-[:创作]->(p) MERGE (a)-[:属于]->(d);注意两点:file:///poems.csv指向Neo4j安装目录下的import文件夹,文件路径不对会一直报找不到;另一个是如果作者名存在重复,一定要先对CSV做唯一性清洗。MERGE不是CREATE,会检查节点是否存在,所以重复执行同一批导入不会产生重复节点。
建完节点后,给常用属性建索引,比如作者姓名、诗词标题、朝代名称。没有索引,几万节点下查询速度还可以忍,到了可视化拖拽和问答多跳查询时会明显卡顿,索引能显著提升体验。
3. 可视化与情感分析模块实现
3.1 基于ECharts的关系图谱可视化
可视化我选了ECharts的关系图,基础数据结构是nodes和links。Flask后端封装一个查询函数,从Neo4j查询所有节点和关系,转成如下JSON:
{ "nodes": [ {"id": "李白", "category": "作者", "symbolSize": 60}, {"id": "静夜思", "category": "诗词", "symbolSize": 30} ], "links": [ {"source": "李白", "target": "静夜思", "relation": "创作"} ] }前端用Ajax拿到数据后直接myChart.setOption(option),一张可拖拽、可缩放、可点击的关系图就出来了。我加了一个筛选功能,用户可以直接在搜索框输入“明月”,系统只展示包含该意象的子图,避免整张大图太密看不清。
这里有个体验层面的细节:节点数量超过300个后,ECharts关系图会非常卡。解决办法是可视化页面只展示“核心节点+一级邻居”,完整网状图放到后台分析页面。我实际做的时候,把朝代、意象这类节点做了颜色分组,作者节点放大一圈,整体视觉层次立刻出来了。
3.2 古诗词情感分析的整体建模思路
现代情感分析大多直接用BERT这一类预训练模型,但对古诗词效果一般,因为古诗语言高度凝练、情感表达含蓄,现代文语料训练出来的模型很难理解“枯藤老树昏鸦”的悲凉感。所以我采用了“词典规则+机器学习”结合的方案,既保证可解释性,又能在少量标注样本下达到不错的效果。
第一步是构建古诗词情感词典。我把情感分为五类:豪迈、悲愁、闲适、思乡、愤懑。每首诗词可以同时包含多种情感,但为了简化项目,我只给每条诗词打一个主情感标签。词典来源是自己标注的2000首常见诗词,加上从开源情感词典里筛选出的适合古汉语的词条。
第二步是情感特征计算。对一首诗,把每个词的情感分累加,再考虑程度副词和否定词。比如“不”会让情感反转,“极”“甚”会加重程度。
3.3 情感分值计算与分类实现
我写了一个情感打分函数,核心逻辑是遍历诗句的词,查询情感词典,累加分数并记录情感标签。示意代码如下:
def sentiment_score(text): words = jieba.lcut(text) total = 0 label_counts = {} for word in words: if word in neg_words: total = -total # 否定翻转 elif word in degree_words: total += degree_words[word] * 0.5 elif word in sentiment_dict: score, label = sentiment_dict[word] total += score label_counts[label] = label_counts.get(label, 0) + 1 if total >= 0: final_label = max(label_counts, key=label_counts.get, default='闲适') else: final_label = min(label_counts, key=label_counts.get, default='悲愁') return total, final_label实际测试发现,单纯靠词典打分准确率只有60%左右,后来我做了两个改进:一是给五言和七言诗句加权重,诗人往往在最后两句升华主题,所以末尾两句的权重提高;二是引入TF-IDF,一首诗如果多次出现同一个意象词,这个词的情感权重会更大。
改进后,我在300首诗的手工标注集上测试,准确率提升到78%左右。对毕业设计来说,这个数字放在论文里已经可以解释,我还在混淆矩阵里列出了“悲愁”和“愤懑”容易混淆的问题,让分析过程更有说服力。
4. 智能问答与AI自动写诗
4.1 问答意图识别与查询模板设计
智能问答我采用“规则模板优先,词典扩展兜底”的方案。因为问答场景限制在古诗词领域,没必要训练一个完整的NLU模型,一套关键词规则就能覆盖90%的用户输入。
我把问题分成四类:
- 诗人相关:“某诗人的代表作有哪些”“某诗人的朝代是什么”
- 诗词相关:“包含某意象的诗有哪些”“某某诗是谁写的”
- 情感相关:“表达思乡情感的诗有哪些”
- 统计排行:“哪个朝代的诗最多”“使用某个意象最多的是谁”
每条模板绑定一条对应的Cypher查询。比如用户输入“李清照的代表作有哪些”,抽取到诗人“李清照”和意图“代表作”,生成查询:
MATCH (a:Author {name: "李清照"})-[:创作]->(p:Poem) RETURN p.title LIMIT 10;匹配不到模板时,系统会把问题交给图谱搜索:把问题分词后,匹配到哪个实体词,就返回该实体的一级关系和属性,作为兜底答案。这样用户输入“说说关于月亮的诗”,也能得到结果。
4.2 查询结果组装与自然语言生成
Neo4j返回的是结构化记录,直接显示数组会很难看,所以需要把查询结果组装成完整句。我在后端写了一个答案渲染函数,把诗人、朝代、诗名、情感标签拼接进去,比如返回:“李清照,宋代女词人,代表作有《如梦令》《声声慢》等,情感倾向为悲愁。”
组装时要注意空结果。用户问题里包含实体,但图谱里确实没有时,必须返回“暂时没有找到相关诗词,换个关键词试试”,而不是抛出一个异常页。问答系统的稳定性很重要,答辩演示时最怕现场输入一个冷门关键词导致后端报错。
4.3 轻量级AI自动写诗模型
自动写诗是整个项目最吸睛的功能。我训练的模型大小控制在50MB以内,基于LSTM加注意力机制,训练数据是清洗后的绝句。训练过程并不需要昂贵显卡,CPU也能完成,只是时间稍长。
模型结构是这样的:输入是诗句前半句的字符序列,经过Embedding层映射成向量,再经过两层LSTM编码,然后用Attention对编码层做加权,最后接全连接层输出下一个字的概率分布。损失函数用交叉熵。训练时我把每首五言绝句处理成这样的样本:给定“窗前明”,预测“月”;给定“窗前明月”,预测“光”。
写诗环节用的是“束搜索加温度采样”。温度参数控制随机性,温度越高诗句越大胆,但容易不通顺;温度越低越保守,但容易重复。实际调参后,我把温度设为0.8,再配合一个“韵脚约束”规则:五言绝句第二句末字和第四句末字尽量压相同的韵,这样生成的句子虽然有不完美的地方,但是一眼看过去很像诗。
我直接给出训练时的核心代码框架:
class PoetLSTM(nn.Module): def __init__(self, vocab_size, embed_size=256, hidden_size=256): super().__init__() self.embed = nn.Embedding(vocab_size, embed_size) self.lstm = nn.LSTM(embed_size, hidden_size, batch_first=True) self.attn = nn.Linear(hidden_size, hidden_size) self.fc = nn.Linear(hidden_size * 2, vocab_size) def forward(self, x): emb = self.embed(x) out, _ = self.lstm(emb) attn_weights = F.softmax(self.attn(out), dim=1) context = (out * attn_weights).sum(dim=1) output = self.fc(torch.cat([context, out[:, -1, :]], dim=1)) return output训练时选择50个epoch,学习率从0.001开始,每10个epoch衰减一次。训练结束后,模型能生成“山间一片叶,月下两行风”这类句子,虽然谈不上文学价值,但作为毕业设计的技术验证已经足够。如果想展示得更炫,可以在后端预留一个“大模型写诗”接口,把用户输入发给本地部署的开源中文模型,再配合规则校验,生成质量会高很多。
5. 常见问题与排查技巧
5.1 Neo4j连接与数据导入堵点
第一个高频问题是Neo4j驱动版本不匹配。使用neo4j官方Python驱动时,服务端版本和驱动版本不一致会直接连接失败,我当时折腾了很久,最后才发现是版本问题。用pip install neo4j==5.x配合Neo4j 5.x社区版可以避开这个坑。
第二个问题是CSV导入乱码。CSV文件如果直接用Excel另存为,默认编码可能是GBK,Neo4j的LOAD CSV默认读UTF-8,中文全乱。解决办法是导入前用Python统一转存为UTF-8编码,并且加上with open('poems.csv', encoding='utf-8-sig')处理BOM头。
第三个问题是关系重复创建。CREATE语句在重复执行时会产生重复关系,正确做法是使用MERGE。如果是批量导入后发现了重复关系,可以用Cypher去重:
MATCH (a)-[r:创作]->(b) WITH a,b,collect(r) AS rels WHERE size(rels) > 1 FOREACH (r IN tail(rels) | DELETE r)5.2 实体抽取与中文分词的坑
jieba分词对古诗词的切分效果并不好,主要原因是古诗词里大量单字词和双字词混用。比如“春眠不觉晓”可能被切成“春眠/不觉/晓”,但“春眠”在诗词里其实是一个整体意象。我在自定义词典里加入了大量“春眠”“孤舟”“寒江”“落日”等组合词,同时关闭了HMM新词发现,避免把古词切碎。
另一个坑是实体链接。图谱中的“作者”和“朝代”实体,在正文中不一定出现,需要在结构化字段中单独抽取。问答模块里,用户输入“李白”与图谱中存储的“李白”一致时能匹配;如果用户输入“诗仙”,就必须建立“诗仙”到“李白”的别名映射表。我当时做了一个简单实体词典,把常见别称、字号、雅号都映射到标准作者名上。
5.3 情感分析效果不理想怎么调
情感分析准确率不高,80%的原因是情感词典覆盖不足。古诗词里很多情感隐藏在景语中,“明月”可以是思乡,“西风”可以是悲凉,“骏马”可以是豪迈。如果词典没有这些组合词的标注,模型就会返回“闲适”。解决方法是针对训练集里预测错误样本做错误分析,看哪些词影响了判断,然后补进词典。
还有一个容易被忽略的地方:程度词和否定词。现代文本里“不”通常是明确否定,古诗词里“不见”却可能表示“看不见”,并不一定反转情感。我把“不见”“无端”“未必”这类词单独建了“古诗词否定词表”,和现代否定词分开处理,效果提升明显。
6. 个人实操经验
做这个项目最大的体会是,别着急写代码,先把数据模型画出来。所有功能都围绕着一张图谱展开,如果实体和关系设计混乱,后面的问答和可视化全都会返工。我刚开始把“朝代”设计成作者的属性而不是节点,后来想把同一朝代诗人连起来,发现写查询很别扭,只能推翻重建,多花了一周时间。
答辩时最有说服力的不是页面有多炫,而是每条数据之间的关联是否讲得通。我会现场演示一个完整链路:输入“思乡”关键词 -> 可视化出现一批带思乡标签的诗 -> 点击某一首 -> 情感分析显示悲愁 -> 继续问“作者还有哪些作品” -> 问答系统返回同一作者的其他诗。整个过程一气呵成,评委能直观看到知识图谱的价值。
最后分享一个偷懒技巧:可视化页面不要自己造轮子。ECharts的关系图和词云组件已经足够成熟,把精力花在Neo4j查询和后端接口上,能节省三分之一的开发时间。代码目录也要从一开始就分好,我建议分成data/、graph/、nlp/、api/、frontend/五个目录,后面写论文、画架构图、做PPT时非常省事。如果你正在做类似选题,照着这条路走下来,大概率能交出一份既有技术深度,又有展示亮点的毕业设计。