☰
基于知识图谱的Python电影推荐系统毕业设计:从图谱构建到推荐融合
2026/10/12 0:16:44 网站建设 项目流程

简介:这是一套面向计算机相关专业毕业设计场景的Python电影推荐系统源码,采用知识图谱架构,融合协同过滤算法,可有效缓解传统推荐系统的冷启动问题。项目难度中等,适合作为课程作业、学期末综合实践或自学训练素材,也适合需要实际项目经验的学习者参考。压缩包共67个文件,约892KB,以43个py脚本为核心,辅以zbak备份、txt数据说明、cfg配置文件、md技术文档及sql建库脚本,涵盖知识图谱构建、用户行为分析与推荐算法等模块。代码遵循PEP8规范并配有详细注释,数据集经多维度清洗,构建了导演、演员、类型、题材等实体关系的电影知识网络,可实现基于语义相似度的深度推荐。目前已有61人学习,读者可从中获得完整项目结构、环境配置指南与部署教程,便于快速理解知识图谱推荐系统的实现思路与工程组织方式。

1. 从一份「基于知识图谱的Python电影推荐系统毕业设计项目源码」说起

很多同学拿到这个题目时,第一反应是去搜「电影推荐系统源码」,然后找到一堆协同过滤的代码,改改数据集就交差了。但答辩老师只要问一句「你的知识图谱在哪、实体关系怎么定义的、图查询怎么落到推荐上」,基本就露馅了。这个标题真正的难点不在推荐算法,而在于知识图谱构建和推荐逻辑的融合——前者决定你的系统有没有「知识」,后者决定这些知识能不能变成推荐理由。

这套方案适合三类人:一是计算机毕业设计选题卡在推荐系统方向、想做出差异化的同学;二是已经会 Python 基础、想找一个能跑通「数据采集→图谱构建→图查询→推荐排序」完整链路的练手项目;三是需要一套可讲解、可演示、可写论文的系统,而不是一个黑盒模型。下面我按自己带过几届毕设的经验,把这条链路拆开讲清楚,包括每一步用什么库、参数怎么设、哪里最容易翻车。

2. 知识图谱电影推荐系统的技术选型与数据建模

2.1 为什么不用纯协同过滤,而要引入知识图谱

协同过滤的核心问题是冷启动和可解释性。一个用户只看过三部电影,ItemCF 算出来的相似度基本是噪声;而知识图谱可以把「导演」「演员」「类型」「年份」「地区」这些结构化关系补进来,即使交互数据稀疏,也能通过实体路径找到候选电影。常见做法是:用图谱做召回和可解释路径,用协同过滤或矩阵分解做精排,两者加权融合。

从工程角度看,知识图谱推荐系统的数据流是这样的:原始电影元数据 → 实体识别与关系抽取 → 三元组存储 → 图查询召回 → 特征拼接 → 排序输出。每一步都有对应的 Python 库,不需要从零造轮子。

2.2 数据来源与字段设计

电影数据一般来自公开数据集或自己爬取。我一般会保留以下字段,它们直接决定图谱的节点和边:

字段用途是否作为图谱节点/边
电影ID唯一标识节点属性
电影名称展示与检索节点属性
导演关系抽取导演节点 + 执导边
演员列表关系抽取演员节点 + 出演边
类型多值分类类型节点 + 属于边
上映年份时间特征节点属性
地区分类特征地区节点 + 产地边
评分排序依据节点属性
用户评分记录交互数据用户节点 + 评分边

字段设计的原则是:能作为推荐理由的,才建边。比如「因为你看过这个导演的片子」比「因为评分高」更有说服力,所以导演边必须建。

2.3 图数据库选型:Neo4j 还是 NetworkX

毕业设计阶段,我建议用 Neo4j 做存储和查询,用 NetworkX 做本地分析和可视化。Neo4j 的 Cypher 查询写起来直观,答辩演示也好看;NetworkX 适合在 Python 里快速算路径和中心度。

安装 Neo4j 桌面版后,用 Python 驱动连接:

pip install neo4j networkx pandas
from neo4j import GraphDatabase # 连接本地 Neo4j,默认 bolt 端口 7687 driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) def create_movie_node(tx, movie): # 用 MERGE 避免重复插入,ON CREATE 只在新建时设置属性 tx.run(""" MERGE (m:Movie {movieId: $movieId}) ON CREATE SET m.title = $title, m.year = $year, m.rating = $rating """, **movie) with driver.session() as session: session.execute_write(create_movie_node, { "movieId": "1", "title": "肖申克的救赎", "year": 1994, "rating": 9.7 })

这段代码的关键点是MERGE而不是CREATE,否则重复导入会产生大量重复节点。参数movieId是业务主键,必须唯一;ON CREATE保证已有节点不会被覆盖属性。

2.4 三元组抽取与批量导入

有了字段设计,接下来把 CSV 转成三元组。常见做法是用 pandas 读数据,逐行构造(头实体, 关系, 尾实体),再批量写入 Neo4j。

import pandas as pd df = pd.read_csv("movies.csv") triples = [] for _, row in df.iterrows(): movie_id = f"m_{row['movieId']}" triples.append((movie_id, "TITLE", row["title"])) triples.append((movie_id, "YEAR", str(row["year"]))) for actor in str(row["actors"]).split("|"): if actor.strip(): triples.append((movie_id, "ACTED_BY", actor.strip())) for genre in str(row["genres"]).split("|"): if genre.strip(): triples.append((movie_id, "BELONGS_TO", genre.strip())) print(f"共生成 {len(triples)} 条三元组")

逻辑说明:电影节点用m_前缀避免和演员节点 ID 冲突;演员和类型用|分隔,这是常见的数据集格式。参数上,split后必须strip(),否则会带入空格导致实体对不上。批量写入时建议每 500 条一个事务,太大容易内存溢出,太小则速度慢。

3. 用 Python 构建电影知识图谱的完整步骤

3.1 环境准备与依赖安装

先确认 Python 版本在 3.8 以上,然后安装核心依赖。这里不推荐用最新版,因为部分图算法库对 3.12 支持还不稳定。

python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install neo4j==5.14.0 pandas==2.1.0 networkx==3.1 py2neo==2021.2.4

py2neo是可选的老牌驱动,有些教程用它,但官方推荐neo4j驱动。如果安装numpy或cv2报错,通常是 Python 版本和 wheel 不匹配,换 3.10 最稳。

3.2 从 CSV 到 Neo4j:批量建节点和边

下面这段代码把电影、演员、类型、导演四类节点和对应关系一次性写入。注意先建节点再建边,否则关系找不到端点。

from neo4j import GraphDatabase import pandas as pd driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) def batch_write(tx, query, rows): tx.run(query, rows=rows) with driver.session() as session: df = pd.read_csv("movies.csv") # 建电影节点 movie_rows = [{"id": f"m_{r['movieId']}", "title": r["title"], "year": int(r["year"]), "rating": float(r["rating"])} for _, r in df.iterrows()] session.execute_write(batch_write, """ UNWIND $rows AS row MERGE (m:Movie {movieId: row.id}) SET m.title = row.title, m.year = row.year, m.rating = row.rating """, movie_rows) # 建演员节点并连边 actor_rows = [] for _, r in df.iterrows(): for actor in str(r["actors"]).split("|"): if actor.strip(): actor_rows.append({"movieId": f"m_{r['movieId']}", "actor": actor.strip()}) session.execute_write(batch_write, """ UNWIND $rows AS row MATCH (m:Movie {movieId: row.movieId}) MERGE (a:Actor {name: row.actor}) MERGE (m)-[:ACTED_BY]->(a) """, actor_rows)

UNWIND是 Cypher 里处理批量数据的标准写法,比循环单条插入快一个数量级。参数$rows是一个字典列表,每个字典的键要和row.xxx对应。如果报Expected a map错误,检查传入的是不是列表。

3.3 用 Cypher 做图查询召回

图谱建好后,推荐召回就是写查询。比如「找出和用户看过的电影同导演、同类型、但用户没看过的电影」:

def recommend_by_graph(tx, watched_ids, limit=10): query = """ MATCH (m:Movie)-[:DIRECTED_BY]->(d:Director)<-[:DIRECTED_BY]-(rec:Movie) WHERE m.movieId IN $watched AND NOT rec.movieId IN $watched RETURN DISTINCT rec.title AS title, rec.rating AS rating ORDER BY rating DESC LIMIT $limit """ result = tx.run(query, watched=watched_ids, limit=limit) return [record.data() for record in result]

这条查询走的是「导演」路径,返回结果可以直接作为推荐理由。参数watched_ids是用户已看电影的 ID 列表,limit控制召回数量。实际项目中我会把导演、演员、类型三条路径的召回结果合并去重,再按评分加权。

3.4 图谱可视化与结果验证

答辩时一张清晰的图谱截图比十页文字都管用。用 NetworkX 把子图导出:

import networkx as nx from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "你的密码")) G = nx.DiGraph() with driver.session() as session: result = session.run(""" MATCH (m:Movie)-[r]->(n) WHERE m.movieId IN $ids RETURN m.title AS src, type(r) AS rel, n.name AS dst """, ids=["m_1", "m_2", "m_3"]) for record in result: G.add_edge(record["src"], record["dst"], label=record["rel"]) print(f"子图节点数:{G.number_of_nodes()},边数:{G.number_of_edges()}")

验证图谱是否建对,就看节点数和边数是否符合预期。如果边数远小于预期,通常是MERGE时属性名写错导致节点没匹配上。

4. 推荐算法与图谱的融合:从召回 to 排序

4.1 基于图路径的召回策略

召回阶段的目标是「不漏」,所以我会同时跑三条路径:同导演、同演员、同类型。每条路径取 Top 20,合并后去重。这里的关键参数是路径深度,一般不超过 2 跳,3 跳以上噪声会急剧增加。

def multi_path_recall(tx, watched_ids): paths = { "director": """ MATCH (m:Movie)-[:DIRECTED_BY]->(d)<-[:DIRECTED_BY]-(rec:Movie) WHERE m.movieId IN $watched AND NOT rec.movieId IN $watched RETURN DISTINCT rec.movieId AS id, rec.title AS title, '同导演' AS reason LIMIT 20 """, "actor": """ MATCH (m:Movie)-[:ACTED_BY]->(a)<-[:ACTED_BY]-(rec:Movie) WHERE m.movieId IN $watched AND NOT rec.movieId IN $watched RETURN DISTINCT rec.movieId AS id, rec.title AS title, '同演员' AS reason LIMIT 20 """, "genre": """ MATCH (m:Movie)-[:BELONGS_TO]->(g)<-[:BELONGS_TO]-(rec:Movie) WHERE m.movieId IN $watched AND NOT rec.movieId IN $watched RETURN DISTINCT rec.movieId AS id, rec.title AS title, '同类型' AS reason LIMIT 20 """ } candidates = {} for name, q in paths.items(): for row in tx.run(q, watched=watched_ids): candidates.setdefault(row["id"], {"title": row["title"], "reasons": []}) candidates[row["id"]]["reasons"].append(row["reason"]) return candidates

返回的reasons字段就是可解释性来源,前端可以直接显示「因为你看过同导演的片子」。

4.2 特征拼接与排序模型

召回后的候选集需要排序。毕业设计阶段不需要上深度学习,用 LightGBM 或逻辑回归就够了。特征包括:图谱路径数量、电影评分、年份、类型匹配度、用户历史平均分。

import pandas as pd from sklearn.linear_model import LogisticRegression # 构造特征矩阵 rows = [] for mid, info in candidates.items(): rows.append({ "movieId": mid, "path_count": len(info["reasons"]), "rating": movie_rating[mid], "year_gap": abs(2024 - movie_year[mid]), "label": 1 if mid in ground_truth else 0 }) X = pd.DataFrame(rows) model = LogisticRegression() model.fit(X[["path_count", "rating", "year_gap"]], X["label"])

path_count是最重要的特征,它直接反映图谱召回的置信度。year_gap用当前年份减上映年份,越小说明越新。训练时注意正负样本比例,如果正样本太少,用class_weight='balanced'。

4.3 冷启动用户的处理

新用户没有观看记录,图谱路径召回会返回空。常见做法是:用注册时选择的偏好类型做种子,查同类型高分电影;或者直接返回全局热门榜。我一般会在代码里加一个兜底分支:

if not watched_ids: # 冷启动:按类型偏好召回 result = tx.run(""" MATCH (m:Movie)-[:BELONGS_TO]->(g:Genre) WHERE g.name IN $preferred RETURN m.title AS title, m.rating AS rating ORDER BY rating DESC LIMIT 10 """, preferred=user_preferred_genres)

这个分支保证任何用户进来都有推荐结果,不会出现空白页。

5. 避坑与常见问题排查

5.1 现象:Neo4j 导入几万条后越来越慢

原因:每条数据都开一个新事务,事务开销累积。解决:用UNWIND批量提交,每批 500 到 1000 条,并且给movieId、name建唯一约束。

CREATE CONSTRAINT movie_id IF NOT EXISTS FOR (m:Movie) REQUIRE m.movieId IS UNIQUE; CREATE CONSTRAINT actor_name IF NOT EXISTS FOR (a:Actor) REQUIRE a.name IS UNIQUE;

5.2 现象:推荐结果全是同一部电影

原因:某条路径的权重过高,或者LIMIT写在了去重之前。解决:先合并所有路径结果再去重,排序时对同一部电影的多条理由做加权,而不是简单取第一条。

5.3 现象:中文实体写入后查询不到

原因:Neo4j 默认编码和 Python 字符串编码不一致,或者 CSV 读取时没指定encoding='utf-8'。解决:读 CSV 时显式加编码参数,写入前用str.strip()清理空格。

5.4 现象:答辩时被问「图谱和推荐有什么关系」答不上来

原因:代码里图谱和推荐是两张皮,图谱只是存了数据,推荐还是纯协同过滤。解决:在推荐函数里显式打印每条推荐的理由,比如「同导演:诺兰」,让图谱路径成为推荐逻辑的一部分。

5.5 现象:pip install某个库报编译错误

原因:Python 版本太新,预编译 wheel 还没发布。解决:换 Python 3.10 或 3.11,或者用 conda 安装。毕业设计环境稳定比追新重要。

6. 让图谱推荐可验证:离线评估与一个实用技巧

6.1 离线评估指标怎么选

推荐系统常用 Recall@K、Precision@K、NDCG@K。毕业设计阶段,我建议至少跑通 Recall@10 和 NDCG@10,因为这两个指标能同时反映召回覆盖和排序质量。

def recall_at_k(recommended, ground_truth, k=10): rec_set = set(recommended[:k]) gt_set = set(ground_truth) return len(rec_set & gt_set) / len(gt_set) if gt_set else 0.0 def ndcg_at_k(recommended, ground_truth, k=10): dcg = sum(1.0 / np.log2(i + 2) for i, item in enumerate(recommended[:k]) if item in ground_truth) idcg = sum(1.0 / np.log2(i + 2) for i in range(min(len(ground_truth), k))) return dcg / idcg if idcg > 0 else 0.0

评估时把用户行为按时间切分,前 80% 做训练,后 20% 做测试。注意不要随机切分,否则会数据泄露,指标虚高。

6.2 一个提升可解释性的技巧:路径模板化

答辩老师最喜欢问「为什么推荐这个」。我一般会把图谱路径转成自然语言模板:

路径类型模板示例输出
同导演因为你喜欢导演{name}因为你喜欢导演克里斯托弗·诺兰
同演员因为你关注演员{name}因为你关注演员莱昂纳多
同类型因为你常看{genre}类型因为你常看科幻类型
组合路径同时满足{reason1}和{reason2}同时满足同导演和同类型

这个表可以直接写进论文的「可解释性设计」章节,也是系统演示时的加分项。

6.3 我踩过的一个坑

最早做这套系统时,我把所有关系都建成双向边,结果图查询出现大量环路,召回结果里全是已经看过的电影。后来改成单向边,并且在查询里强制加NOT rec.movieId IN $watched,问题才解决。图谱建模时,边的方向不是随便定的,它决定了查询的语义。另一个血泪经验是:不要等到最后才做评估,每加一条路径就测一次 Recall,否则最后指标掉了都不知道是哪一步引入的。

如果你正在做这个方向的毕业设计,我的建议是先把图谱建对,再谈算法融合。图谱是地基,推荐是上层建筑,地基歪了,后面调参都是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询