基于知识图谱的电影推荐系统:从Neo4j建模到图算法融合实践

发布时间:2026/10/11 13:09:36
基于知识图谱的电影推荐系统:从Neo4j建模到图算法融合实践 简介面向计算机相关专业学生的Python知识图谱电影推荐系统源码包完整覆盖数据清洗、知识图谱构建、KGCN推荐模型训练与Web可视化展示流程适合毕业设计、课程设计及期末大作业。资源共31个文件包含21个Python源码、5个dat数据文件和txt/readme/md说明文档Python源码中create_kg、data_process分别负责图谱构建与数据处理KGCN模块实现核心推荐算法web/app.py提供交互界面另有evaluation.py等评估辅助脚本dat文件提供用户、评分、电影基础数据压缩包约14.84MB结构清晰便于按模块学习。已有80人学习下载。该项目评审分99分代码完整可直接运行随包说明文档和README对设计思路、模块划分与使用方式均有说明既能作为高分毕设的参考范本也适合系统学习知识图谱与推荐系统的结合实践。1. 知识图谱电影推荐系统这个毕设题值不值得做先看它解决什么问题电影推荐系统做了很多但绝大多数死在同一句追问上推荐理由是什么协同过滤只能告诉你「和你口味相近的人也看了这部」却说不出底层联系。用 Python 构建知识图谱的电影推荐系统把电影、演员、导演、类型和用户评分全部建成图推荐结果能从「因为你喜欢诺兰所以推荐《星际穿越》」这种路径上讲明白。这个标题背后是一整套毕设形态源码跑通推荐链路说明文档证明你会建模、会选算法、会做实验。它适合有 Python 基础、不想停留在纯调库层面、想拿高分的同学。下面从图建模讲到算法融合再讲答辩前那些躲不过的坑。2. 数据建模与导入把 MovieLens 三张表建成 Neo4j 图附 Python 批量导入脚本2.1 图模型设计电影、影人、类型与用户评分如何落点知识图谱构建的第一步不是写代码而是先回答一个问题哪些数据是节点哪些数据是关系。最常见的做法是用公开的 MovieLens 1M 做主数据它稳定、标注全、答辩时能说清来源。它的原始形态是 users.dat、movies.dat、ratings.dat 三张表放在图数据库里要重新映射。我的建议是把节点控制在五类User、Movie、Person、Genre外加一个可选的 Studio。Person 把演员和导演合一用 role 属性区分否则图会平白多一套节点导入和查询都变重。关系四类User 到 Movie 的 RATED带 rating 属性、Person 到 Movie 的 ACTED_IN、Person 到 Movie 的 DIRECTED、Movie 到 Genre 的 BELONGS_TO。评分数值必须放在 RATED 关系上不能放 Movie 属性里——同一部电影被几百人评分属性存不下语义也错。关系型三张表图模型落点说明movies 表Movie 节点movie_id 做唯一键users 表User 节点user_id 做唯一键ratings 表RATED 关系rating 作为关系属性演员/导演字段Person 节点 ACTED_IN / DIRECTED用 role 区分genres 字段Genre 节点 BELONGS_TO避免逗号字符串这一步最容易被忽略的是关系方向。RATED 从 User 指向 MovieACTED_IN 和 DIRECTED 从 Person 指向 Movie后续 PageRank 的种子传播方向全依赖这里。我一般会先在纸上画一遍带箭头的图确认方向后再写导入脚本这个习惯帮我少踩了很多坑。2.2 MovieLens 数据预处理CSV 清洗与编码统一MovieLens 原始文件用::做分隔符标题里还带年份括号不能直接导入。常见做法是先清洗成标准 CSV顺便把 year 字段拆出来避免每一行查询时再去截字符串。import csv from pathlib import Path def clean_movies(src: Path, dst: Path) - None: with open(src, encodinglatin-1) as f, \ open(dst, w, encodingutf-8, newline) as out: writer csv.writer(out) writer.writerow([movie_id, title, year]) for line in f: parts line.strip().split(::) movie_id int(parts[0]) title parts[1] year None if ( in title: year int(title.rsplit((, 1)[1].rstrip())) title title.rsplit((, 1)[0].strip() writer.writerow([movie_id, title, year])逻辑说明原始文件用 latin-1 读入是为了避免早期 MovieLens 数据在 Windows 下导出时出现编码错乱输出统一转成 UTF-8让后续 Neo4j 导入不会遇到中文乱码。year 从标题里拆出来拆不到就置 None 而不是 0否则排序和展示时会出现 0 年这种明显错误。参数方面src 和 dst 建议传绝对路径避免脚本在答辩机器上换目录后找不到文件year 字段在 CSV 里保持字符串格式等导入 Neo4j 时再用toInteger转换不要在清洗阶段强转。洗 ratings.dat 时同理把 user_id、movie_id、rating 拆成三列时间戳这一列如果你不做时间衰减推荐可以直接丢掉留着只会让说明文档多写一段用不上的解释。2.3 Python 直连 Neo4jMERGE 幂等导入与关系批量写入清洗完 CSV接下来用 Python 把数据写进 Neo4j。常见做法是装 py2neo 驱动然后通过 Bolt 协议连库。官方 Neo4j Python Driver 也行但 py2neo 的 Node、Relationship 封装对毕设代码更友好读起来像在操作对象而不是拼字符串。from py2neo import Graph GRAPH_URI bolt://localhost:7687 AUTH (neo4j, your_password) graph Graph(GRAPH_URI, authAUTH) # 清库反复调试时保证幂等 graph.run(MATCH (n) DETACH DELETE n) # 批量导入 Movie 节点 with open(movies.csv, encodingutf-8) as f: rows list(csv.DictReader(f)) graph.run( UNWIND $rows AS row MERGE (m:Movie {movie_id: toInteger(row.movie_id)}) SET m.title row.title, m.year toInteger(row.year) , rowsrows)逻辑说明DETACH DELETE n清空全库保证脚本可以重复执行UNWIND $rows把 Python 的字典列表展开成 Cypher 里的行流MERGE按 movie_id 匹配存在就更新属性不存在就创建天然幂等。用参数传$rows而不是拼字符串避免电影标题里的引号、反斜杠破坏 Cypher 语法。参数说明一次提交 5000 到 10000 行是安全区间我常用的批量大小是 5000提交太大会让 Neo4j 事务日志膨胀少提交则导入速度感人。关系批量写入同样用 UNWIND先把 ratings 读成 list再一次性写入边graph.run( UNWIND $rows AS row MATCH (u:User {user_id: toInteger(row.user_id)}) MATCH (m:Movie {movie_id: toInteger(row.movie_id)}) MERGE (u)-[r:RATED]-(m) SET r.rating toFloat(row.rating) , rowsrating_rows)这里的两个MATCH要求用户和电影节点已经存在所以导入顺序必须是先节点后关系。如果发现 RATED 关系数量远小于 ratings 行数十有八九是先导了关系再导节点或者两边的 movie_id 类型不一致一个 int 一个 string。检查方法很简单toInteger统一转换即可解决。2.4 导入后的图谱体检节点统计、孤立节点与关系方向检查导入完成后别急着写推荐算法先做一轮图谱体检。我见过太多人跳过这步结果推荐结果全是空的排查半天发现是关系方向写反了。MATCH (n) RETURN labels(n)[0] AS node_label, count(*) AS cnt ORDER BY cnt DESC这条 Cypher 按节点标签统计数量和 MovieLens 原始表行数对得上才算导入成功。如果 User 节点数和 users 表行数差了十几万说明 CSV 有重复 user_id需要回清洗脚本加去重如果 Movie 节点数比 movies 表还多通常是被空格或全角字符拆成了两个节点。接着查孤立电影即没有任何关系的 Movie 节点MATCH (m:Movie) WHERE NOT (m)--() RETURN m.title, m.movie_id LIMIT 20孤立节点意味着这部电影既没有演员、类型也没有用户评分。少量孤立节点可以接受但如果数量超过总数的 5%说明关系导入漏了某类数据推荐结果会带偏。再顺手抽查一条关系方向MATCH (u:User)-[r:RATED]-(m:Movie) RETURN u.user_id, m.title, r.rating LIMIT 5如果返回的是(m)-[RATED]-(u)说明方向反了。方向错在元路径召回里会直接变成「找到你还没看过的电影」的反向逻辑算法再调参都救不回来。这轮体检跑完数据底座才算真正立住。3. 推荐引擎落地元路径召回、Personalized PageRank 与社区发现的组合3.1 元路径召回一条 Cypher 找出口味相近的电影图建模完成后的第一个推荐思路是沿着元路径找相似电影。元路径就是图里的固定跳跃模式比如「用户 → 电影 → 演员 → 电影」含义是我评过《盗梦空间》《盗梦空间》里有诺兰诺兰还导过《星际穿越》那《星际穿越》就该推荐给我。这条路径完全可解释答辩时能直接画出图来讲。MATCH (u:User {user_id: $uid})-[:RATED]-(m1:Movie)-[:ACTED_IN|DIRECTED]-(p:Person)-[:ACTED_IN|DIRECTED]-(m2:Movie) WHERE NOT (u)-[:RATED]-(m2) WITH m2, count(DISTINCT p) AS shared_persons RETURN m2.title AS title, shared_persons ORDER BY shared_persons DESC LIMIT 20逻辑说明$uid是传入的目标用户ACTED_IN|DIRECTED表示沿两类关系任一跳把演员和导演都算作共享维度WHERE NOT过滤掉用户已经看过的电影避免推荐历史内容count(DISTINCT p)统计两部电影共用的影人数共享影人越多口味越接近。参数说明LIMIT 20 是召回规模别设太大元路径召回的精度有限Top 20 已经够后续融合用。如果你的图里加了 Genre可以再加一条-[:BELONGS_TO]-(:Genre)的路径段让「类型相同」也参与召回。这个方法最大的优点是快几十万节点的图单条 Cypher 查询在百毫秒级返回适合做实时推荐接口。它的短板也明显只有路径完全命中才出结果冷门电影如果演员关系稀疏召回率很低。所以元路径只能作为第一路召回不能当主算法。3.2 Personalized PageRank给用户一个种子让图谱替他漫游Personalized PageRank 是毕设推荐系统里性价比最高的算法原理和 Google PageRank 一致只是把种子节点从「全网网页」换成「某个用户评过的电影」。图上的随机游走从用户节点出发沿着 RATED、ACTED_IN 这些关系扩散最终分数高的电影节点就是推荐结果。实现走 Neo4j GDS 库不需要自己写矩阵迭代。query CALL gds.pageRank.stream({ nodeProjection: [User, Movie, Person, Genre], relationshipProjection: { RATED: { type: RATED, orientation: REVERSE, properties: { rating: { property: rating } } }, ACTED_IN: { type: ACTED_IN, orientation: NATURAL }, DIRECTED: { type: DIRECTED, orientation: NATURAL }, BELONGS_TO: { type: BELONGS_TO, orientation: NATURAL } }, sourceNodes: [$uid], dampingFactor: 0.85, maxIterations: 50, tolerance: 1e-4 }) YIELD nodeId, score WITH gds.util.asNode(nodeId) AS n, score WHERE n:Movie AND NOT EXISTS { (u:User {user_id: $uid})-[:RATED]-(n) } RETURN n.title AS title, score ORDER BY score DESC LIMIT 30 result graph.run(query, uidtarget_user_id).data()逻辑说明orientation: REVERSE把 RATED 方向反过来让随机游走能从 Movie 节点往 User 节点传播否则从用户种子出发根本走不到电影上sourceNodes指定个性化种子WHERE NOT EXISTS用子查询过滤已评分电影。gds.util.asNode把内部节点 ID 还原成图节点。参数说明dampingFactor 取 0.85 是 GDS 默认值适合一般场景如果想让推荐更贴近种子用户偏好而不是全网热门可以降到 0.6 到 0.7游走更容易回到种子附近。maxIterations 40 到 50 足够收敛设 100 只是空耗性能。properties.rating把评分作为边的权重实现「高分电影贡献更大」的语义这是提升效果最简单的一招。3.3 Louvain 社区发现补上热门算法覆盖不到的冷门片PPR 的问题在于它天然偏向高度数节点也就是热门电影。社区发现算法可以补齐这一路召回。Louvain 算法把图划分成若干个社区社区里的节点连接紧密通常对应「喜欢同一类题材的人群」或「经常合作的影人圈子」。在社区内做推荐能挖出跨类型、但结构上有联系的冷门电影。CALL gds.louvain.stream({ nodeProjection: [Movie, Person, Genre], relationshipProjection: { ACTED_IN: { type: ACTED_IN, orientation: NATURAL }, DIRECTED: { type: DIRECTED, orientation: NATURAL }, BELONGS_TO: { type: BELONGS_TO, orientation: NATURAL } }, includeIntermediateCommunities: false }) YIELD nodeId, communityId逻辑说明这个调用不含 User 和 RATED只对电影侧子图做社区划分这样输出的是「电影社区」而不是「用户社区」推荐语义更干净。拿到 communityId 后先在 Python 里找到目标用户评分最高的几部电影所属社区再取同一社区里用户没看过的电影做召回。参数说明includeIntermediateCommunities保持 false 即可我们只需要最终社区编号。Louvain 的随机种子会影响划分结果如果你发现每次运行社区都变可以在 GDS 调用里固定随机种子保证说明文档里的实验数据可复现。社区粒度太粗时常见做法是给louvain加maxLevels或minCommunitySize参数限制我一般限制社区最小规模为 10低于这个值的社区直接丢弃因为太小了没有推荐参考价值。3.4 三种结果怎么融合加权打分的参数经验有了三路召回最后一步是融合排序。常见做法是分别归一化到 0 到 1然后加权求和。三路召回的结果集先并集去重再按加权分数排序取 Top Ndef fuse(ppr: dict, meta: dict, comm: dict, top_n30): scores {} for source, weight in [(ppr, 0.5), (meta, 0.3), (comm, 0.2)]: max_score max(source.values()) if source else 1 for movie_id, raw_score in source.items(): scores[movie_id] scores.get(movie_id, 0) weight * raw_score / max_score ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_n] return ranked逻辑说明每个来源先除以自身最大值做归一化让三路分数不在同一个量纲时也能相加。权重上 PPR 占比最高是因为它语义最贴近「个性化」元路径次之社区发现只做辅助覆盖。参数说明0.5 / 0.3 / 0.2 是稳妥起点不是调参终点。如果你发现推荐列表全是热门大片说明 PPR 权重过高或 PPR 内部热门效应太强可以把 PPR 权重降到 0.4社区权重提到 0.3。反过来如果冷门到用户完全不认识就是社区权重过大。调参以 10 到 20 部人工标注的测试集为准别凭感觉反复横跳。4. 知识图谱推荐毕设避坑指南六条血泪经验从编码乱码到答辩现场翻车4.1 CSV 中文乱码Excel 导出的 GBK 和 Python 读入的 UTF-8现象用 Excel 打开清洗后的 CSV 一切正常Python 一读就报UnicodeDecodeError或者导入 Neo4j 后电影标题变成乱码。原因Windows 版 Excel 另存为 CSV 时默认写成 GBK 编码而 Python 的open()默认按 UTF-8 读取两边对不上。这个问题在数据预处理阶段最隐蔽因为你在 Excel 里看不出异常。解决所有读写 CSV 的open()都显式声明编码不清洗阶段写成encodingutf-8读原始文件时用encodinglatin-1或 GBK具体取决于数据来源。我自己的踩坑教训是别依赖默认编码一行代码能解决的事不值得浪费半天排查。4.2 py2neo 版本漂移昨天能跑的导入脚本今天报错现象导入脚本在一台机器上跑通换到答辩笔记本上直接报AttributeError: Graph object has no attribute auth或TypeError: Graph() got an unexpected keyword argument password。原因py2neo 4.x 用Graph(password...)初始化7.x 改成auth(user, pwd)元组API 变化很大。毕业设计代码在 6 个月内重装环境是常事依赖版本一变脚本就废。解决把项目依赖锁死。在 requirements.txt 里写死大版本比如py2neo2021.2.4这一档的版本号不要写py2neo裸依赖。安顿好后跑一遍导入脚本确认无误再继续写算法。血泪经验毕设答辩前一天别手贱升级任何依赖包。4.3 关系方向定义反了PageRank 的种子根本传不出去现象PPR 跑完推荐出来的电影跟用户的历史口味没有任何相关性甚至送出恐怖片给只看爱情片的用户。原因关系方向建反了RATED 写成了 Movie 指向 User而 PPR 里没有加orientation: REVERSE随机游走从用户节点出发沿关系一跳就到不了任何电影只能在用户节点之间打转。解决建图时统一按「主体指向客体」定义方向用户评电影所以 User 指向 Movie。写 PPR 时预先明确传播路径种子从 User 出发第一步必须沿 RATED 走所以 RATED 要 REVERSE。每次跑算法前先执行一遍图谱体检那条方向检查查询确认无误再跑完整实验。4.4 热门节点淹没推荐人气电影把个性化压得抬不起头现象推荐的 Top 10 永远是大片不管目标用户是谁结果一模一样个性化成了空话。原因PageRank 天然偏爱高入度节点热门电影在图里拥有海量 RATED 关系游走时被反复踩中分数自然最高。这是图算法的结构性问题不是调个 maxIterations 能解决的。解决两招配合。第一给 RATED 关系设权重评分越高权重越大让高分偏好主导传播。第二在融合阶段把结果按用户历史评分的平均类型分布做一次轻量过滤用户评分里 70% 是科幻片就把推荐列表里的非科幻片权重降一档。千万别在 PPR 里直接删热门电影节点那会把图谱语义弄坏。4.5 导入大图谱内存不足Neo4j 的堆到底怎么调现象运行导入脚本时 Neo4j 服务直接崩掉日志报OutOfMemoryError: Java heap space或者所有查询变得极慢。原因Neo4j 默认堆内存配置偏保守MovieLens 1M 虽然只有百万级关系但节点多、属性多导入事务如果一次提交数据量太大堆就爆了。解决先确认 Neo4j 是 4.x 还是 5.x4.x 在conf/neo4j.conf里改dbms.memory.heap.max_size1G这类配置5.x 则改server.memory.heap.max_size。顺便把导入脚本的批处理大小降到 5000 行。注意 Neo4j 堆内存是 JVM 堆不是操作系统内存调太大反而和页缓存抢资源1G 到 2G 就够这个数据量。4.6 答辩演示临时出问题Java 版本与插件依赖先查一遍现象演示现场 Neo4j 启动失败或者gds.pageRank.stream报Unknown procedure。原因Neo4j 5.x 要求 Java 17Java 11 跑不起来GDS 库和 APOC 插件没装或版本和 Neo4j 主版本不匹配时Cypher 过程直接找不到。这是讲台上最容易翻车的点因为自己在开发机上从来没触发过。解决答辩前做一次「目录安装检查」第一java -version确认版本号和 Neo4j 要求一致第二打开 Neo4j 的plugins/目录确认 GDS 和 APOC 的 jar 包存在且版本号前缀和 Neo4j 一致第三连着跑一遍MATCH (n) RETURN count(n)和 PPR 查询。我还会额外准备一个备用脚本如果 Neo4j 起不来直接用 Python 读 CSV 在内存里跑一个简化版 PPR保住演示底线。5. 可解释性与高分文档把推荐理由变成人话再把设计过程写进说明文档5.1 推荐路径可视化让用户看到「因为你看过 XX所以推荐 YY」知识图谱推荐系统答辩时最值钱的一句话是「我的推荐结果可以解释」。协同过滤给不出理由知识图谱能给。做法是在推荐结果的接口里回传一条路径前端展示成「你看过《盗梦空间》→ 诺兰 → 诺兰导演的《星际穿越》推荐给你」。MATCH path (u:User {user_id: $uid})-[:RATED]-(:Movie)-[:ACTED_IN|DIRECTED]-(:Person)-[:ACTED_IN|DIRECTED]-(rec:Movie) WHERE NOT (u)-[:RATED]-(rec) RETURN path LIMIT 5逻辑说明这条 Cypher 和元路径召回是同一套逻辑但RETURN path返回的是整条路径对象包含路径上的每个节点和关系后端可以把它序列化成 JSON节点列表带 title 和标签关系列表带类型。前端拿到后渲染成一串节点线条用户一眼就能看懂推荐链路。这个设计放在说明文档里就是「可解释性模块」是整篇论文最大的加分项。参数说明LIMIT 5 是因为展示区空间有限取前五条路径就够。路径里有重复 Person 时可以按人合并避免展示出三行「诺兰」。我一般让后端先按 Person 去重再返回前端。5.2 说明文档的骨架与侧重点图模型先行算法对比收尾高分毕设说明文档拼的不是字数是逻辑闭环。我的习惯是固定六个章节需求分析、图模型设计、数据导入与预处理、推荐算法设计、实验对比、总结与展望。其中图模型设计一定要配一张自己画的实体关系图把五类节点、四类关系画清楚这张图能直接向评审证明你对知识图谱的理解不是停留在概念上。算法设计章节不要只贴代码要有一段「为什么不用协同过滤」的对比分析协同过滤是基于用户一电影二分图的共现计数知识图谱则引入影人、类型等中间实体推荐结果可从路径上解释。实验对比小节给出 PrecisionK 和 RecallK 两个指标K 取 5、10、20 三档用 20 个用户做人工标注就能出实验数据不需要大规模跑离线评测。最后分享一个我保持到现在的习惯任何带图的项目动手前先写一页「数据映射表」把源表字段对应到节点或关系这个习惯让我在知识图谱推荐这个选题上少走了很多弯路答辩时的底气和效果都好了不少。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询