简介:这份资源是面向计算机相关专业学生与项目实战学习者的Python毕业设计完整源码,主题为基于知识图谱的电影推荐系统,适合用作毕业设计、课程设计或期末大作业。项目经导师指导并通过评审,评审分98分,源码均经本地编译与严格调试,可正常运行,难度适中。压缩包共55个文件,约890KB,以43个py脚本为核心,另含4个txt数据说明、4个cfg配置、3个md文档与1个sql建表脚本,覆盖爬虫采集、知识图谱构建与推荐算法等模块。内容预览显示包含豆瓣、互动百科、百度百科等数据采集脚本及电影信息与评分处理逻辑,可帮助读者理解从数据抓取到图谱构建再到推荐输出的完整链路。目前已有155人学习,适合需要完整项目参考、排错思路与目录结构借鉴的学习者下载使用。
1. 从零搭一套知识图谱电影推荐:毕业设计选它到底图什么
做毕业设计最怕两件事:一是选题太水,答辩老师一句“这跟网上教程有啥区别”就给你问住;二是代码跑不起来,环境配三天,最后连数据都没入库。基于知识图谱的电影推荐系统源码之所以在计算机毕业设计里年年有人选,核心原因是它同时踩中了两个能拿得出手的技术点——图数据库建模和推荐算法,而且数据现成、可视化好看、答辩有话说。它解决的不是“猜你喜欢”那种纯协同过滤的老问题,而是把电影、导演、演员、类型、用户评分这些实体和关系织成一张网,用图结构去回答“喜欢《教父》的人还该看什么”。适合谁?适合会一点 Python、想做出有技术纵深感的毕业设计、又不愿意从零造轮子的同学。这一章先把这套系统到底由哪几块拼起来讲清楚,后面几章再一步步落地。
一套完整的知识图谱电影推荐系统,通常拆成四层:数据采集层负责把电影元数据、演职员表、用户评分抓下来或读进来;知识图谱层用 Neo4j 这类图数据库存实体和关系,把“电影—导演—演员—类型—用户”连成图;推荐算法层在图上做路径查询、相似度计算或图嵌入,产出推荐列表;应用层用 Web 框架把推荐结果和可视化界面暴露出来。很多人一上来就写推荐算法,结果发现图里根本没几条关系,算法再花哨也是空转。正确的顺序是先把图谱建扎实,再谈推荐。下面从环境搭建开始,把这条链路走通。
2. 环境与数据准备:把 Python、Neo4j 和电影数据集凑齐
这一章解决“跑起来之前要装什么、数据从哪来”的问题。环境没配好,后面全是玄学报错,所以先把地基打平。
2.1 Python 环境与依赖安装
Python 安装教程网上一抓一大把,但毕业设计场景下我建议直接用 3.9 或 3.10,太新的版本有些图算法库还没跟上。装完 Python 后,用虚拟环境隔离依赖,别把系统环境搞乱。VS Code 配 Python 环境时,记得选对解释器路径,否则 pip 装到 A 环境、代码跑在 B 环境,这种翻车我见过太多次。
# 创建虚拟环境,名字叫 kg_movie python -m venv kg_movie # 激活虚拟环境(Windows) kg_movie\Scripts\activate # 激活虚拟环境(macOS / Linux) source kg_movie/bin/activate # 安装核心依赖 pip install neo4j pandas numpy scikit-learn flask py2neo这里neo4j是官方驱动,py2neo是更上层的封装,写起来更顺手;pandas处理 CSV 数据;scikit-learn后面算相似度会用到;flask用来做推荐接口和简单页面。版本不用锁死,但建议neo4j驱动用 5.x,和 Neo4j 5.x 服务端匹配。
2.2 Neo4j 安装与知识图谱建模
Neo4j 构建知识图谱是这套系统的核心。去官网下 Desktop 版最省事,装完创建一个本地数据库,记下端口(默认 7687)和密码。启动后浏览器打开http://localhost:7474,能进 Neo4j Browser 就说明服务通了。
建模之前先想清楚节点和关系。电影推荐场景下,我一般设计这几类节点和关系:
| 节点标签 | 属性 | 关系类型 | 指向 |
|---|---|---|---|
| Movie | movieId, title, year | DIRECTED_BY | Director |
| Director | name | ACTED_IN | Actor |
| Actor | name | HAS_GENRE | Genre |
| Genre | name | RATED | Movie |
| User | userId | LIKED | Movie |
建约束是为了查询快,也防止重复插入。在 Neo4j Browser 里执行:
// 给 Movie 的 movieId 建唯一约束 CREATE CONSTRAINT movie_id IF NOT EXISTS FOR (m:Movie) REQUIRE m.movieId IS UNIQUE; // 给 User 的 userId 建唯一约束 CREATE CONSTRAINT user_id IF NOT EXISTS FOR (u:User) REQUIRE u.userId IS UNIQUE; // 给 Genre 的 name 建唯一约束 CREATE CONSTRAINT genre_name IF NOT EXISTS FOR (g:Genre) REQUIRE g.name IS UNIQUE;约束建好后,插入重复数据会直接报错,而不是默默产生脏节点。这一步很多教程跳过,等到推荐结果里出现重复电影才后悔。
2.3 电影数据集的选择与导入
数据集推荐用 MovieLens,它自带movies.csv、ratings.csv、links.csv,电影元数据够用,评分数据量大。下载后放到项目data/目录。导入用 Python 脚本批量写,比手写 Cypher 靠谱。
from py2neo import Graph, Node, Relationship import pandas as pd # 连接 Neo4j,改成你自己的密码 graph = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) # 读电影数据 movies = pd.read_csv("data/movies.csv") # 批量创建 Movie 节点和 Genre 关系 for _, row in movies.iterrows(): movie = Node("Movie", movieId=int(row["movieId"]), title=row["title"]) graph.merge(movie, "Movie", "movieId") # merge 避免重复 # genres 字段是 "Action|Comedy" 这种格式 for genre_name in row["genres"].split("|"): genre = Node("Genre", name=genre_name) graph.merge(genre, "Genre", "name") rel = Relationship(movie, "HAS_GENRE", genre) graph.merge(rel) print("电影与类型导入完成")merge是关键,它等价于“存在就匹配、不存在就创建”,比create安全。movieId用int转换,因为 CSV 读进来可能是字符串。导入几万条数据时,逐条 merge 会慢,生产做法是用UNWIND批量 Cypher,但毕业设计数据量下逐条也能接受,先跑通再优化。
3. 知识图谱构建:把电影、演员、用户连成一张网
数据进了库只是第一步,真正让图谱“活”起来的是关系。这一章讲怎么把评分、演职员这些关系补全,以及怎么验证图谱建对了。
3.1 用 Cypher 批量补全演员与导演关系
MovieLens 的movies.csv里没有演员和导演,需要额外数据源。常见做法是接 TMDB API 或用现成的补充数据集。假设你已经有一份credits.csv,包含movieId, director, actors(actors 用分号分隔),导入逻辑如下:
credits = pd.read_csv("data/credits.csv") for _, row in credits.iterrows(): movie = graph.nodes.match("Movie", movieId=int(row["movieId"])).first() if not movie: continue # 电影不存在就跳过,避免建孤儿节点 # 建导演关系 director = Node("Director", name=row["director"]) graph.merge(director, "Director", "name") graph.merge(Relationship(movie, "DIRECTED_BY", director)) # 建演员关系,actors 用分号分隔 for actor_name in str(row["actors"]).split(";"): actor_name = actor_name.strip() if not actor_name: continue actor = Node("Actor", name=actor_name) graph.merge(actor, "Actor", "name") graph.merge(Relationship(movie, "ACTED_IN", actor)) print("演职员关系导入完成")这里先match电影再建关系,是为了防止 credits 里有 movies.csv 中没有的电影,产生孤立节点。strip()去掉演员名前后空格,否则“Tom Hanks”和“ Tom Hanks”会变成两个节点,这种脏数据在推荐时会拉低准确率。
3.2 用户评分关系与图谱完整性校验
评分是推荐的燃料。ratings.csv里是userId, movieId, rating, timestamp,导入时把评分作为关系属性:
ratings = pd.read_csv("data/ratings.csv") # 只取评分 >= 4 的作为“喜欢”,减少噪声 liked = ratings[ratings["rating"] >= 4.0] for _, row in liked.iterrows(): user = Node("User", userId=int(row["userId"])) graph.merge(user, "User", "userId") movie = graph.nodes.match("Movie", movieId=int(row["movieId"])).first() if movie: rel = Relationship(user, "LIKED", movie, rating=float(row["rating"])) graph.merge(rel) print("用户评分关系导入完成")阈值 4.0 是经验值,MovieLens 是 5 分制,4 分以上算正面反馈。阈值调低会引入噪声,调高会导致很多用户没有足够历史,推荐冷启动严重。导入完在 Neo4j Browser 里跑几条校验查询:
// 看节点总数分布 MATCH (n) RETURN labels(n) AS label, count(*) AS cnt ORDER BY cnt DESC; // 看有没有孤立电影(没有任何关系) MATCH (m:Movie) WHERE NOT (m)--() RETURN count(m) AS isolated_movies; // 看用户喜欢关系的数量 MATCH ()-[r:LIKED]->() RETURN count(r) AS liked_count;如果isolated_movies大于 0,说明有电影没连上类型或演职员,推荐时这些电影永远不会被图路径命中,需要回头补数据。
3.3 图谱可视化与查询验证
答辩时老师最爱看图。Neo4j Browser 自带可视化,跑一条路径查询就能出图:
// 查《Toy Story》关联的演员和类型 MATCH (m:Movie {title: "Toy Story (1995)"})-[r]-(n) RETURN m, r, n LIMIT 25;如果图里节点稀稀拉拉,说明关系没建全。正常应该能看到电影连着多个演员、多个类型。可视化不只是好看,它能帮你肉眼发现建模错误,比如某个类型节点名字拼错、某个演员重复。这一步做完,图谱层就算稳了。
4. 推荐算法落地:在图上做路径查询与相似度计算
图谱建好,接下来是推荐。这一章讲两种可落地的推荐思路:基于图路径的规则推荐,和基于图嵌入的相似度推荐。前者简单直观,后者更有技术含量,毕业设计里两种都写上,答辩时能体现工作量。
4.1 基于图路径的推荐:共同邻居与类型匹配
最直接的推荐逻辑是:找到和目标用户喜欢相同电影的其他用户,把他们喜欢但目标用户没看过的电影推过来。用 Cypher 一条查询就能实现:
// 给 userId=1 的用户推荐电影 MATCH (u1:User {userId: 1})-[:LIKED]->(m:Movie)<-[:LIKED]-(u2:User) WHERE u1 <> u2 MATCH (u2)-[:LIKED]->(rec:Movie) WHERE NOT (u1)-[:LIKED]->(rec) RETURN rec.title AS recommendation, count(u2) AS score ORDER BY score DESC LIMIT 10;这条查询的逻辑是:先找和用户 1 喜欢同一部电影的用户 2,再看用户 2 还喜欢哪些用户 1 没看过的电影,按共同用户数排序。score就是推荐强度。这种基于共同邻居的推荐可解释性强,答辩时能说清楚“为什么推这部”。
还可以加类型约束,比如只推用户偏好的类型:
MATCH (u1:User {userId: 1})-[:LIKED]->(m:Movie)-[:HAS_GENRE]->(g:Genre) WITH u1, g, count(m) AS genre_count ORDER BY genre_count DESC LIMIT 3 MATCH (rec:Movie)-[:HAS_GENRE]->(g) WHERE NOT (u1)-[:LIKED]->(rec) RETURN DISTINCT rec.title AS recommendation, g.name AS genre LIMIT 10;先统计用户最喜欢的三个类型,再推这些类型里没看过的电影。逻辑简单,但效果不差,适合作为 baseline。
4.2 基于图嵌入的推荐:Node2Vec 与向量相似度
想让推荐更有“算法味”,可以用 Node2Vec 把图节点转成向量,再算余弦相似度。思路是:在图上随机游走生成序列,用 Word2Vec 训练出每个节点的向量,电影向量越接近,说明在图里越相似。
import numpy as np from node2vec import Node2Vec import networkx as nx from py2neo import Graph graph_db = Graph("bolt://localhost:7687", auth=("neo4j", "your_password")) # 把 Neo4j 图导出成 networkx 图 G = nx.Graph() query = "MATCH (a)-[r]->(b) RETURN a, type(r) AS rel, b LIMIT 50000" for record in graph_db.run(query): a = record["a"] b = record["b"] # 用节点 id 作为 networkx 节点标识 G.add_edge(str(a.identity), str(b.identity)) # 训练 Node2Vec node2vec = Node2Vec(G, dimensions=64, walk_length=30, num_walks=200, workers=4) model = node2vec.fit(window=10, min_count=1, batch_words=4) # 取某部电影的向量,找最相似的电影 def recommend_similar(movie_node_id, topn=10): vec = model.wv[str(movie_node_id)] similar = model.wv.most_similar([vec], topn=topn) return similar print(recommend_similar(1)) # 传入电影节点 iddimensions=64是向量维度,毕业设计够用,调大更精细但更慢;walk_length和num_walks控制游走规模,数据量大时适当调小避免跑太久。most_similar返回的是节点 id 和相似度,需要再回 Neo4j 查电影标题。这种方法的坑是:节点 id 是 Neo4j 内部 id,重建数据库后会变,生产环境要用业务 id 映射。
4.3 推荐结果融合与排序
两种方法各有短板:路径推荐依赖共同用户,冷启动用户没结果;嵌入推荐依赖图结构,新电影向量不准。常见做法是融合打分:
def hybrid_recommend(user_id, movie_node_id, alpha=0.6): # alpha 控制路径推荐权重 path_recs = get_path_recommendations(user_id) # 返回 {movie: score} embed_recs = get_embedding_recommendations(movie_node_id) # 返回 {movie: sim} # 归一化后加权 final = {} for m, s in path_recs.items(): final[m] = final.get(m, 0) + alpha * normalize(s) for m, s in embed_recs.items(): final[m] = final.get(m, 0) + (1 - alpha) * normalize(s) return sorted(final.items(), key=lambda x: x[1], reverse=True)[:10]alpha是融合权重,0.6 表示更信路径推荐。这个参数没有标准答案,要在自己的数据上试。归一化函数把两种分数缩到同一量级,否则量纲不同会互相压制。融合后排序取 Top10 返回给前端。
5. 避坑与排查:知识图谱电影推荐最容易翻车的 5 个地方
这一章全是血泪经验,每条按“现象 → 原因 → 解决”写,照着排查能省下大量时间。
现象一:Neo4j 连不上,报ServiceUnavailable。原因通常是服务没启动、端口不对或密码错。Desktop 版有时候后台进程挂了但界面还开着。解决:先去 Neo4j Browser 确认能登录,再看 Python 里auth密码是否和设置一致,端口默认 7687,改过要同步。
现象二:导入数据后查询很慢,几万节点就卡。原因是没有建索引或约束,Neo4j 全表扫描。解决:给常用查询属性建索引,比如CREATE INDEX movie_title FOR (m:Movie) ON (m.title);。另外批量导入用UNWIND代替逐条 merge,速度差几十倍。
现象三:推荐结果里出现重复电影。原因是 merge 时用的属性不唯一,或者同一部电影被不同 movieId 插入两次。解决:检查唯一约束是否生效,导入前对 CSV 去重,drop_duplicates(subset=["movieId"])。
现象四:Node2Vec 训练报内存不足。原因是图太大,随机游走序列撑爆内存。解决:限制导出边数(LIMIT),调小num_walks和walk_length,或者只对电影和类型子图做嵌入,不把用户节点放进去。
现象五:Flask 接口返回中文乱码。原因是响应头没指定编码。解决:return jsonify(data), 200, {"Content-Type": "application/json; charset=utf-8"},或者用json.dumps(..., ensure_ascii=False)。
6. 让推荐结果可解释:一个能写进论文的路径展示技巧
毕业设计答辩时,老师最常问的是“你为什么推这部电影”。如果你只能回答“算法算出来的”,分数不会高。这一章讲一个具体技巧:把推荐背后的图路径可视化出来,让每条推荐都有据可查。
思路是:对每条推荐结果,反向查它在图里是怎么被命中的。比如路径推荐命中的电影,可以查出是哪些共同用户、哪些共同电影牵的线。
// 查推荐电影《Toy Story》为什么被推给 userId=1 MATCH (u1:User {userId: 1})-[:LIKED]->(common:Movie)<-[:LIKED]-(u2:User) MATCH (u2)-[:LIKED]->(rec:Movie {title: "Toy Story (1995)"}) RETURN common.title AS because_you_liked, u2.userId AS similar_user LIMIT 5;查询返回“因为你喜欢《A》,和你相似的用户 2 也喜欢《Toy Story》”,这就是可解释推荐。把这段逻辑封装成接口,前端展示成“推荐理由”,论文里可以写成“基于图路径的可解释推荐模块”。
再进一步,可以把路径长度作为推荐置信度。路径越短,说明关系越直接,推荐越可信:
| 路径长度 | 含义 | 置信度 |
|---|---|---|
| 2 跳 | 共同用户直接喜欢 | 高 |
| 3 跳 | 通过类型或演员关联 | 中 |
| 4 跳以上 | 弱关联 | 低 |
实现时用shortestPath或变长路径查询:
MATCH (u:User {userId: 1}), (rec:Movie {title: "Toy Story (1995)"}) MATCH p = shortestPath((u)-[*..4]-(rec)) RETURN length(p) AS hops, [n IN nodes(p) | coalesce(n.title, n.name, n.userId)] AS path_nodes;[*..4]限制最多 4 跳,避免全图搜索。返回的path_nodes就是路径上的节点序列,前端可以画成链路图。这个技巧不复杂,但能让你的毕业设计从“调库跑通”变成“有分析深度”,答辩时主动展示,效果很好。
我自己做这类系统时,最大的教训是:别等算法写完才回头看图建得对不对。图谱是地基,地基歪了,上面算法再花哨也是空中楼阁。每次导入一批数据,先跑几条校验查询,确认节点和关系数量合理,再往下走。这个习惯帮我省了无数个通宵排查的夜晚。希望帮到你。
本文还有配套的精品资源,点击获取