
简介这份资源是面向计算机相关专业学生与项目实战学习者的毕业设计完整方案主题为Python基于知识图谱的电影推荐问答系统难度适中适合用作大作业、毕设或知识图谱与推荐系统方向的练手项目。压缩包共44个文件约1.15MB以py源码、csv数据、xml配置、txt词表与png示意图为主另含少量css、js、html前端文件及md说明文档覆盖数据预处理、问题分类、模板匹配、图谱构建与服务端接口等模块。项目源码均经本地编译调试可正常运行并附有说明文档辅助理解整体流程。目前已有85人学习下载。读者可据此掌握从电影、类型、人物等数据到Neo4j图谱的构建思路理解问句分类与模板化问答的实现方式并参考目录结构快速定位核心脚本为二次开发或答辩准备提供完整参考。1. 电影知识图谱问答系统从「看不懂推荐」到「问得出理由」你打开一个推荐系统它给你推了《星际穿越》。你心里冒出的第一个问题往往不是「还有没有类似的」而是「为什么给我推这部」。传统协同过滤答不上来它只知道「跟你口味相似的人还看了什么」至于电影之间是导演相同、题材相近还是共享同一套世界观它一概不知。知识图谱问答系统要解决的正是这个黑匣子问题把电影、导演、演员、类型、评分这些实体和关系显式地存进图数据库用户用自然语言提问系统沿着图谱里的边去检索、推理最后给出一个带解释的答案。这套方案适合谁正在做计算机毕业设计、想找一个既有工程量又不至于失控的选题的同学也适合已经会写 Python、想补上「知识图谱 问答」这条链路的后端或数据方向从业者。它不需要训练大模型核心工作量在数据清洗、图谱建模和查询意图解析上一台普通笔记本就能跑通全流程。下面我按自己搭过一遍的顺序把选型、构建、问答实现和踩过的坑讲清楚。2. 技术选型与图谱 Schema为什么是 Neo4j 加规则解析2.1 存储层选 Neo4j 而不是关系库的理由电影推荐问答的本质是「多跳关系查询」。比如「找一部由诺兰执导、汉斯·季默配乐、评分高于 8.5 的科幻片」这条查询要跨越导演、配乐、类型、评分四类关系。用 MySQL 写就是四五个 JOIN 叠在一起SQL 又长又难维护改一个条件就要动整条语句。Neo4j 用 Cypher 表达同样的逻辑是沿着图上的边一步步走语义直观而且多跳查询在索引命中的情况下响应能压到毫秒级。常见做法是用 Neo4j 社区版本地起一个实例即可不需要集群。Python 侧通过官方驱动neo4j连接版本上注意驱动 5.x 和 Neo4j 5.x 配套混用 4.x 驱动连 5.x 服务端会在认证握手阶段报错这是新手最容易翻车的地方之一。2.2 电影知识图谱的节点与关系设计Schema 设计决定了后面问答能回答哪些问题。我一般会先列出「用户可能问什么」再倒推需要哪些实体和关系。电影领域高频问题集中在某导演的作品、某演员参演的电影、某类型的推荐、某部电影的相似推荐、评分区间筛选。据此设计如下结构。节点标签关键属性说明MoviemovieId, title, year, rating, overview电影主体rating 为均分PersonpersonId, name导演与演员共用用关系区分角色Genrename类型如科幻、剧情Countryname制片国家关系类型方向说明DIRECTEDPerson → Movie导演关系ACTED_INPerson → Movie参演关系可带 role 属性BELONGS_TOMovie → Genre类型归属PRODUCED_INMovie → Country制片国家SIMILAR_TOMovie → Movie相似度边权重用共现或向量算把导演和演员都建成 Person 节点、用关系类型区分比拆成两个标签更省空间查询时也少一层判断。SIMILAR_TO 这条边是推荐能力的来源可以先用「共同类型 共同演员」的 Jaccard 相似度算出来权重存在关系属性上问答时按权重排序取 TopN。2.3 环境搭建与依赖安装的最小命令先把运行环境固定下来避免后面因为版本漂移排查半天。Python 建议 3.9 到 3.11太新的版本部分图算法库还没跟上。# 创建独立虚拟环境避免污染全局包 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装核心依赖 pip install neo4j5.14.0 pandas2.1.1 jieba0.42.1 py2neo2021.2.4neo4j是官方驱动负责连接和事务pandas用来清洗 CSV 数据jieba做中文分词问答意图识别要用py2neo提供更顺手的图操作封装二选一即可我习惯两者都装批量导入用官方驱动日常查询用 py2neo。装完先验证连接from neo4j import GraphDatabase # 连接本地 Neo4j默认 bolt 协议端口 7687 driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 你的密码)) def test_conn(tx): result tx.run(RETURN connected AS msg) return result.single()[msg] with driver.session() as session: print(session.execute_read(test_conn)) driver.close()这段代码做的是建立驱动、开一个只读会话、跑一条最简单的 Cypher 验证链路。auth里的密码是 Neo4j 首次启动时设置的忘了就去neo4j-admin重置。如果这里报ServiceUnavailable九成是服务没起来或端口被占先确认 Neo4j Desktop 或命令行实例处于运行状态。3. 数据清洗与图谱批量导入把 CSV 变成可查询的边3.1 电影数据的字段清洗与去重公开电影数据集常见问题是字段缺失和重复。title 里混着上映年份比如「Toy Story (1995)」得先用正则把年份抽出来单独存否则按标题精确匹配会失败。评分字段有空值直接参与均值计算会拉低结果我一般用该电影已有评分的均值填充或者干脆过滤掉评分记录少于 5 条的电影。import pandas as pd import re # 读取原始数据注意编码中文数据常见 gbk movies pd.read_csv(movies.csv, encodingutf-8) ratings pd.read_csv(ratings.csv, encodingutf-8) # 从标题中抽取年份生成独立列 def extract_year(title): match re.search(r\((\d{4})\), title) return int(match.group(1)) if match else None movies[year] movies[title].apply(extract_year) # 去掉标题里的年份括号保留干净片名 movies[title] movies[title].str.replace(r\s*\(\d{4}\), , regexTrue) # 计算每部电影的均分和评分人数 agg ratings.groupby(movieId)[rating].agg([mean, count]).reset_index() agg.columns [movieId, rating, rating_count] # 过滤评分人数过少的电影避免噪声 agg agg[agg[rating_count] 5] movies movies.merge(agg, onmovieId, howinner) movies.to_csv(movies_clean.csv, indexFalse) print(f清洗后电影数{len(movies)})extract_year用正则捕获四位年份str.replace把括号年份从标题里抹掉保证后续按片名查询能命中。rating_count 5这个阈值是经验值太低会让冷门烂片混进推荐太高会砍掉大量长尾电影5 到 10 之间按数据量调。清洗完一定要打印行数和原始数据对比掉太多说明正则或合并逻辑有问题。3.2 用 Cypher 的 LOAD CSV 批量建节点数据干净后导入 Neo4j。节点量在几万级别时LOAD CSV是最省事的方式不用写 Python 循环直接在 Neo4j Browser 里跑。先把清洗后的 CSV 放到 Neo4j 的 import 目录下这是硬性要求路径不对会报找不到文件。// 建唯一约束既加速查询又防止重复节点 CREATE CONSTRAINT movie_id IF NOT EXISTS FOR (m:Movie) REQUIRE m.movieId IS UNIQUE; CREATE CONSTRAINT person_name IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE; // 导入电影节点 LOAD CSV WITH HEADERS FROM file:///movies_clean.csv AS row MERGE (m:Movie {movieId: toInteger(row.movieId)}) SET m.title row.title, m.year toInteger(row.year), m.rating toFloat(row.rating) RETURN count(m);MERGE而不是CREATE是关键重复执行脚本不会产生重复节点这在调试阶段能省很多事。toInteger和toFloat做类型转换CSV 读进来默认都是字符串不转的话后面按评分范围筛选会得到错误结果。约束要在导入前建好否则大数据量下 MERGE 会退化成全表扫描慢到怀疑人生。3.3 关系边的导入与相似度计算节点建完再连边。导演、演员、类型这些关系直接从数据集的字段拆出来。类型字段常是「Action|Adventure」这种竖线分隔要先 split。# 生成类型关系 CSV genre_rows [] for _, row in movies.iterrows(): for g in str(row[genres]).split(|): genre_rows.append({movieId: row[movieId], genre: g}) pd.DataFrame(genre_rows).to_csv(movie_genre.csv, indexFalse)// 导入类型节点与归属关系 LOAD CSV WITH HEADERS FROM file:///movie_genre.csv AS row MERGE (g:Genre {name: row.genre}) WITH g, row MATCH (m:Movie {movieId: toInteger(row.movieId)}) MERGE (m)-[:BELONGS_TO]-(g);相似度边是推荐的核心用共同类型数除以类型并集数算 Jaccard 系数超过阈值就连一条 SIMILAR_TO。这一步在 Python 里算好写成 CSV 再导入比在 Cypher 里做笛卡尔积高效得多。阈值我一般设 0.3太低会连出一堆弱相关边让推荐结果发散太高则很多电影连不上边推荐召回不足。导入完成后用一条查询验证图谱连通性MATCH (m:Movie)-[:BELONGS_TO]-(g:Genre) RETURN g.name AS genre, count(m) AS cnt ORDER BY cnt DESC LIMIT 10;如果某个类型数量为 0说明那批电影的导入漏了回去检查 CSV 里 movieId 类型是否和节点一致。4. 问答意图解析与 Cypher 模板让自然语言落到图上4.1 基于规则与分词的问题分类不训练模型的前提下意图识别用「关键词 模板匹配」最稳。先把用户问题用 jieba 分词再按关键词命中判断意图类别。电影问答的高频意图就那么几类查导演作品、查演员作品、按类型推荐、查相似电影、按评分筛选。每类对应一个 Cypher 模板。import jieba # 意图关键词表命中即归类 INTENT_KEYWORDS { director: [导演, 执导], actor: [主演, 演员, 出演], genre: [类型, 题材, 科幻, 喜剧, 动作], similar: [类似, 相似, 像], rating: [评分, 高分, 好看], } def detect_intent(question): words set(jieba.cut(question)) for intent, kws in INTENT_KEYWORDS.items(): if words set(kws): return intent return unknownjieba.cut把句子切成词集合用集合交集判断命中比字符串in更准能避免「导演」被「演」这种子串误伤。关键词表要按实际语料补比如用户爱说「片子」「电影」就加进去。unknown兜底后可以返回一句引导语而不是硬答。4.2 实体抽取与 Cypher 模板填充意图定了还要抽出问题里的电影名或人名才能填进模板。实体抽取同样用规则拿图谱里已有的片名和人名做匹配谁出现在问题里就取谁。这比上 NER 模型轻量得多对毕业设计规模的数据足够。def extract_entity(question, known_names): # 长名优先避免「星际」误匹配「星际穿越」 for name in sorted(known_names, keylen, reverseTrue): if name in question: return name return None # 各意图对应的 Cypher 模板 CYPHER_TEMPLATES { director: MATCH (p:Person)-[:DIRECTED]-(m:Movie) WHERE p.name $name RETURN m.title AS title, m.year AS year, m.rating AS rating ORDER BY m.rating DESC LIMIT 10 , similar: MATCH (m:Movie {title: $name})-[s:SIMILAR_TO]-(other:Movie) RETURN other.title AS title, s.weight AS weight ORDER BY s.weight DESC LIMIT 5 , }sorted(..., keylen, reverseTrue)保证先匹配长名字否则「星际穿越」会被「星际」截胡。模板里用$name参数占位执行时传参不要用字符串拼接否则片名里带引号就会引发 Cypher 语法错误这也是个隐蔽的坑。4.3 执行查询并组装自然语言答案拿到模板和实体后执行查询把结果行拼成一句人话返回。这一步决定了系统「像不像在对话」。def answer(question, session, known_names): intent detect_intent(question) entity extract_entity(question, known_names) if intent not in CYPHER_TEMPLATES or not entity: return 没太理解你的问题可以换个说法比如「诺兰导演过哪些电影」 cypher CYPHER_TEMPLATES[intent] records session.run(cypher, nameentity) rows [r.data() for r in records] if not rows: return f没有找到和「{entity}」相关的{intent}信息 if intent director: titles 、.join(r[title] for r in rows[:5]) return f{entity}执导的作品包括{titles} if intent similar: titles 、.join(r[title] for r in rows) return f和《{entity}》相似的有{titles} return str(rows)session.run传参执行r.data()把记录转成字典列表。返回前按意图做不同的措辞组装导演类问题列作品相似类问题列片名。空结果要给出友好提示而不是抛异常用户体验差别很大。这套流程跑通后一个最小可用的问答闭环就成立了。5. 避坑与排查那些让我重跑一整天的细节5.1 导入报「Couldnt load the external resource」现象是LOAD CSV直接失败提示找不到文件。原因通常是 CSV 没放进 Neo4j 的 import 目录或者用了绝对路径。Neo4j 出于安全默认只允许从 import 目录读文件。解决办法是把 CSV 拷进安装目录下的import文件夹Cypher 里用file:///文件名的相对写法。如果确实要用别的路径得改neo4j.conf里的dbms.security.allow_csv_import_from_file_urls和dbms.directories.import但毕业设计没必要折腾放 import 目录最省心。5.2 中文片名匹配不上现象是问「星际穿越」返回空结果但图谱里明明有。原因是导入时标题带了首尾空格或全角括号和用户输入不一致。解决是在清洗阶段统一str.strip()并把全角括号转半角。查询侧也可以在匹配前对实体做一次归一化。血泪经验是中文数据的空格和标点问题比英文多得多导入前不处理后面每个查询都要还债。5.3 多跳查询越来越慢现象是图谱涨到几万节点后相似推荐查询从毫秒变成几秒。原因是 SIMILAR_TO 边没有索引或者查询没走约束。解决是给高频查询的入口属性建索引比如CREATE INDEX movie_title IF NOT EXISTS FOR (m:Movie) ON (m.title)。另外检查 Cypher 有没有写成MATCH (m:Movie) WHERE m.title ...这种全表扫描改成MATCH (m:Movie {title: ...})让优化器用上索引。索引不是越多越好写多的场景下每个索引都会拖慢导入。5.4 意图识别把「导演」误判成「演员」现象是问「某某导演的电影」被当成演员查询。原因是关键词表里「导演」和「演员」都含「演」字分词后集合交集可能同时命中。解决是调整判断顺序把更具体的关键词放前面或者用「先长词后短词」的匹配策略命中「导演」就不再判「演员」。规则系统的通病就是关键词打架靠优先级和词表维护来压。5.5 相似度边把不相关电影连在一起现象是推荐结果里出现八竿子打不着的片子。原因是 Jaccard 阈值设太低或者只用了类型一个维度。解决是把阈值提到 0.3 以上并叠加演员、导演维度的共现权重综合打分后再连边。推荐质量对阈值非常敏感建议先用小样本调看 TopN 结果合不合理再全量跑。6. 把问答做成可演示的界面与效果验证毕业设计最终要能演示命令行问答不够看。最省事的做法是用 Streamlit 套一层 Web 界面几十行代码就能出一个输入框加结果区比 Flask 写前端快得多。import streamlit as st from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 你的密码)) st.title(电影知识图谱问答) question st.text_input(输入你的问题例如诺兰导演过哪些电影) if question: with driver.session() as session: # 这里调用第 4 章的 answer 函数 result answer(question, session, known_names) st.write(result)st.text_input拿到问题st.write渲染答案known_names从图谱里查一次缓存起来即可不用每次请求都拉。跑streamlit run app.py就能在浏览器打开。演示时准备几个典型问题导演作品、相似推荐、类型筛选覆盖不同意图效果最直观。验证系统好不好用我一般看三个指标。一是意图识别准确率手工标 50 条问题跑一遍看分类对不对低于 80% 就回去补关键词。二是查询响应时间单跳查询应在 100 毫秒内多跳相似推荐控制在 500 毫秒内超了查索引。三是推荐结果的人工合理性随机抽 20 部电影看相似推荐明显不相关的超过三成就调相似度阈值和维度权重。这三个指标不用写进论文当创新点但自己心里得有数答辩被问到才不慌。最后说个我自己的习惯图谱建好后先别急着写问答用 Neo4j Browser 把每类查询手写一遍确认 Cypher 逻辑和返回结果都对再封装进 Python。我早期图省事直接在代码里调结果 Cypher 写错了排查半天分不清是查询问题还是代码问题。先把图查明白再让代码去调这个顺序能省下大量返工。希望帮到你。本文还有配套的精品资源点击获取