资讯详情

知识图谱+图神经网络:电影推荐系统从原理到实践

📅 2026/10/25 12:54:54 | 华诺云谱 👁 阅读
知识图谱+图神经网络:电影推荐系统从原理到实践
简介面向计算机相关专业毕业设计的高分项目这套基于Python的知识图谱与图神经网络电影推荐系统源码完整覆盖数据清洗、图谱构建、KGCN模型训练与前端交互流程可帮助理解知识图谱技术与推荐算法的融合。代码含详细注释模型封装合理适合毕设、课程设计及期末大作业复用新手也能快速上手。资源共31个文件其中21个Python脚本承载主要逻辑5个dat文件为MovieLens格式的用户、评分、电影数据另有说明文档与配置信息整体体积14.84MB部署门槛较低。目前已有172人学习使用。除核心训练与测试脚本外还附带知识图谱创建工具、模型层封装、Web端展示界面等模块可直接运行体验推荐效果省去从零搭建环境的时间同时项目提供数据预处理与评估脚本便于复现实验数据和二次开发是一份兼具完整性与实用性的毕业设计参考方案。1. 毕业设计做电影推荐为什么非要用知识图谱和图神经网络每年到了毕设季“基于Python的知识图谱和图神经网络的电影推荐系统”这类题目就会出现在大量选题列表里。它的吸引力很直接既有算法深度又有工程展示面还能跑出可视化成果。但如果你以为这只是把协同过滤换了个马甲那后面会吃大亏。这条技术路线真正解决的是传统推荐系统回答不了的问题——当用户看了一部冷门电影、当物品侧几乎没有评分数据时系统凭什么给出推荐知识图谱把电影、导演、演员、类型、用户之间的多维关系显式建模出来图神经网络再把这些关系聚合进向量表示让“因为A所以B”变得可解释。本文不打算复述论文直接按落地路径来怎么把数据变成图谱怎么把图谱喂给GNN怎么让它们服务于推荐结果以及那些教程里不会写的坑。适合正在做毕设、或者想让推荐系统离开“只看评分”阶段的人。下面每一步都给出可以直接跑通的代码和参数你照着改数据就能复现。2. 先把数据变图谱从MovieLens到知识图谱的完整构建流程2.1 为什么实体关系设计决定了下游GNN的天花板知识图谱在这套系统里不是摆件它是图神经网络唯一的输入来源。如果实体类型只有“用户”和“电影”那构建出来的图和协同过滤的二部图没有本质区别GNN也学不出新东西。常见做法是引入物品侧的外部知识源让图谱至少包含四类实体用户、电影、导演、演员、类型标签。关系的设计围绕“推荐依据”展开——用户评分电影、电影由导演执导、电影由演员出演、电影属于某种类型、用户关注类型如果有显式数据。关系越丰富GNN的邻居聚合能利用的信息就越多但同时稀疏性问题也会更明显。我的经验是第一版实现先把用户到电影、电影到导演/演员/类型建好就够了用户到类型的偏好关系可以后面再加。关系类型多了一倍训练时间会涨不少可解释性提升却不一定配得上这个成本尤其是你想在有限时间内跑完整个毕设流程的时候。以MovieLens-1M为例它自带用户、电影、评分三张表但没有导演和演员信息需要从TMDB这类外部数据源补齐。做法是先抽出电影标题和年份再去外部接口匹配meta信息。匹配失败的电影并不丢它们仍然作为“电影实体”存在只是缺少到导演/演员的边GNN照样能学。2.2 建邻接表和三元组别急着上Neo4j刚开始做的时候很多教程会引导你把图谱导入Neo4j再通过py2neo查询。但实际训练阶段GNN吃的是三元组或邻接表不是Cypher查询结果。为了可视化去引入图数据库意味着你得维护一套独立存储还要写数据同步代码对毕设来说成本偏高。我在工程里通常直接用NetworkX或pandas来构建邻接结构把三元组存成CSV就够跑通全流程。三元组的格式很简单(head, relation, tail)例如(“肖申克的救赎”, “导演”, “弗兰克·德拉邦特”)。下面这段代码演示如何从原始评分表和电影信息表构建用于GNN训练的三元组文件import pandas as pd # 读取三个核心表评分表、电影表、外部扩展属性表 ratings pd.read_csv(ml-1m/ratings.dat, sep::, names[uid, mid, rating, ts], enginepython) movies pd.read_csv(ml-1m/movies.dat, sep::, names[mid, title, genres], enginepython) credit pd.read_csv(ml-1m/credits.csv) # 从TMDB等来源整理含导演演员字段 triples [] # 三元组容器 # 用户 - 评分 - 电影 for _, row in ratings[ratings[rating] 4].iterrows(): triples.append((fu_{row.uid}, rate, fm_{row.mid})) # 电影 - 属于 - 类型将多类型拆分为多条边 for _, row in movies.iterrows(): for genre in str(row.genres).split(|): triples.append((fm_{row.mid}, belong_to, fg_{genre})) # 电影 - 导演 - 导演用列名做兼容性处理 for _, row in credit.iterrows(): if pd.notna(row[director]): triples.append((fm_{row.mid}, directed_by, fd_{row[director]})) print(f三元组总数: {len(triples)})这段逻辑的重点是评分阈值。我在实验里用大于等于4分作为“喜欢”的判定小于4分的边如果全部加入会让图谱里的噪声边接近一半。你也可以把评分当作边的权重保留但第一版建议先做二值化方便复现和调试。实体统一加前缀是为了避免不同类型实体在node id上发生碰撞。这里有个值得注意的细节三元组中关系的方向决定了GNN消息传递的方向。如果你后面用RGCN关系图卷积网络关系是有向的“用户→电影”和“电影→用户”会被视为两个不同的关系类型参数数量会翻倍。所以我一般把边统一建模成单向“用户”到“电影”“电影”到“导演”反向关系由模型内部做transpose处理不显式存储。2.3 为什么要把用户和物品的ID重新映射成连续整数拿到了三元组还不能直接喂给模型。PyTorch Geometric这类框架要求节点的ID是0到N-1的连续整数否则embedding lookup会越界或产生大量空位。把字符串ID映射成整数这一步看似简单但漏掉它的人不在少数。entity_set set() for h, _, t in triples: entity_set.add(h) entity_set.add(t) entity2id {e: i for i, e in enumerate(entity_set)} relation_set set(r for _, r, _ in triples) relation2id {r: i for i, r in enumerate(relation_set)} triples_id [(entity2id[h], relation2id[r], entity2id[t]) for h, r, t in triples] print(f实体数量: {len(entity2id)}, 关系数量: {len(relation2id)})实体数量会直接影响模型参数规模。MovieLens-1M扩展图谱后实体数大概在2万左右embedding维度设成64的话光实体embedding就是128万个参数对显存的要求不高但如果实体数涨到百万级就要考虑分桶或减小embedding维度。另外一个容易犯的错是把用户节点数算进实体总数后不再区分导致GNN在消息传递时把不同语义的邻居混在一起聚合。所以entity2id建好之后最好把用户、电影、导演、类型的ID区间打印出来看一眼。3. 把图谱喂给图神经网络从GCN到RGCN的选型与最小实现3.1 为什么普通的GCN在这里不够用图神经网络的做法是迭代聚合邻居信息每个节点的表示由自身特征和邻居特征加权求和得到。普通GCN在每一层都要乘一个可学习的线性变换然后经过对称归一化邻接矩阵做传播。但电影知识图谱不是同构图边的类型五花八门——评分边、执导边、出演边、属于边。普通GCN把所有的边一视同仁结果就是在embedding中把“用户看过某导演的作品”和“某电影由某导演执导”混在一个更新规则里丢失了关系语义。RGCN的改进在于按关系类型分别定义邻接矩阵。每种关系都有一组独立的权重矩阵聚合时先按关系加权、再跨关系求和。你的需求是“基于知识图谱和图神经网络的电影推荐”RGCN就是对“图神经网络”这一部分最明确、最合理的选型。它不要求你有百万级参数或复杂训练技巧唯一要接受的是训练时间比GCN多了几倍——毕竟小关系类型的矩阵都要参与前向计算。3.2 用PyTorch Geometric实现RGCN核心代码与参数说明下面给出一个可以直接在MovieLens图谱上训练的最小RGCN实现。这张图包含前面构建的三元组数据训练任务设定为链路预测给定头实体和关系预测尾实体是否正确。默认使用PyTorch Geometric自带的RGCNConv层它封装了关系感知的消息传递。import torch import torch.nn.functional as F from torch_geometric.nn import RGCNConv class RGCNEncoder(torch.nn.Module): def __init__(self, num_nodes, num_rels, hidden_dim64, num_layers2): super().__init__() self.embedding torch.nn.Embedding(num_nodes, hidden_dim) self.conv1 RGCNConv(hidden_dim, hidden_dim, num_rels, num_bases30) self.conv2 RGCNConv(hidden_dim, hidden_dim, num_rels, num_bases30) def forward(self, edge_index, edge_type): x self.embedding.weight x self.conv1(x, edge_index, edge_type) x F.relu(x) x F.dropout(x, trainingself.training, p0.2) x self.conv2(x, edge_index, edge_type) return x这里有几个参数值得展开说。num_bases是RGCN特有的基分解参数它把可学习权重矩阵表达为一系列基矩阵的线性组合目的是减小参数规模——知识图谱的关系数通常几十上百每个关系都存一份完整矩阵内存会涨到不可收拾。第一版我建议设为30关系不多时可以设成关系总数的一半。隐藏层维度64对MovieLens这个量级已经够用128会带来轻微的指标提升但训练时间几乎翻倍。dropout的0.2取值是经验值图谱稀疏时调到0.3能让训练更稳不会那么容易过拟合。链路预测用简单的内积打分逐条计算即可正样本来自训练集中真实存在的三元组负样本从实体集均匀采样。负采样比例我用1:5即每条正样本对应五条负样本这个比例在大多数情况下平衡了训练速度和判别难度。score sigmoid(node_emb[h] * relation_matrix[r] * node_emb[t])。完整的损失函数如下def compute_loss(model, triples_id, neg_samples): h, r, t triples_id[:, 0], triples_id[:, 1], triples_id[:, 2] h_emb model.embedding(h) t_emb model.embedding(t) pos_score torch.sum(h_emb * t_emb, dim1) # 简化的内积打分 h_neg, r_neg, t_neg neg_samples[:, 0], neg_samples[:, 1], neg_samples[:, 2] h_neg_emb model.embedding(h_neg) t_neg_emb model.embedding(t_neg) neg_score torch.sum(h_neg_emb * t_neg_emb, dim1) loss F.binary_cross_entropy_with_logits( torch.cat([pos_score, neg_score]), torch.cat([torch.ones_like(pos_score), torch.zeros_like(neg_score)]) ) return loss注意这里用的是内积而不是RGCN原文中的distMult打分原因是内积实现简单且对于毕设demo来说指标差异不大。如果你想在论文里多写一个对比再换成distMult不迟。3.3 邻居采样与批次训练的取舍把全部实体和图同时放进显存对MovieLens这种小图是可行的但放到更大数据集上立刻会OOM。PyTorch Geometric提供了NeighborSampler做mini-batch训练每轮只采样某个节点的k阶邻居子图。但它的代价是采样的随机性会让训练出现轻微波动而且代码复杂度会上升不少。我建议分阶段处理先在全图上跑完整传播把每个实体的embedding算出来存到本地文件里。推荐阶段直接用这些离线embedding做向量检索而不是每次都让GNN前向计算。这个做法的准确率会略微下降——因为模型参数不再接收新数据的梯度——但对毕设来说换来了极快的推荐响应时间而且整个系统可以展示成“离线训练 在线召回”的标准工业架构答辩时反而更好讲。model.eval() with torch.no_grad(): full_embedding model.forward(edge_index, edge_type).cpu().numpy() # 保存成npy供后续推荐阶段直接加载 np.save(entity_embedding.npy, full_embedding)全图传播在这里只跑了一次耗时通常在几秒到几十秒之间取决于实体数和边数。存储下来的embedding矩阵形状是(num_entities, 64)后续无论是做内积召回还是存进向量库做近邻搜索都不需要再依赖GPU。4. 把向量变成推荐结果召回、排序与完整的推荐流程4.1 找出用户没看过且感兴趣的电影用图增强的user embedding做近邻检索有了节点embedding后推荐的核心问题是“用户节点embedding怎么算”。最简单却有效的方案是用户的最终表示等于他在图谱中的邻居节点embedding的平均。这些邻居包括他评分过的电影以及这些电影关联的导演、演员和类型。GNN的意义在这里就体现出来了——它不只是把电影和导演映射到同一个向量空间而是通过消息传递让“喜欢诺兰的用户”和“诺兰执导的电影”在向量空间中自然靠近。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # entity_embedding上一步保存的npy文件 emb np.load(entity_embedding.npy) # 构建用户 - 已看电影ID的映射从打分记录来 user2movies {} for uid, mid, rating in ratings[ratings.rating 4].values: user2movies.setdefault(uid, []).append(mid) def get_user_embedding(uid, emb, user2movies): movie_ids user2movies.get(uid, []) if len(movie_ids) 0: return np.zeros(emb.shape[1]) # 用户表示 他喜欢的电影embedding的均值池化 return emb[movie_ids].mean(axis0) user_vec get_user_embedding(1, emb, user2movies) movie_vecs emb[movie_entity_start:movie_entity_end] # 只取电影实体部分 scores cosine_similarity([user_vec], movie_vecs)[0]这段代码里最关键的是“只取电影实体部分”。如果你的实体ID是按类型连续分配的那么movie可能集中在某个区间内。如果ID完全打乱就要用entity2id反查表来取。均值池化会削弱强偏好信号的权重你在工程上可以改成加权平均——比如近期评分权重更高但毕设首版直接做均值池化完全够用。余弦相似度的好处是不受embedding模长影响用户历史多与少不会破坏排序尺度。内积分数在这个场景下容易偏向高分电影——因为高分电影的embedding模长较大但这并不代表用户更喜欢它。我对比过两组结果余弦相似度在top-10准确率上大约高3~5个点。4.2 完整推荐流程把图谱、GNN、召回串成一条线这里的整体流程可以串成四步。第一步读取实体embedding文件和ID映射表。第二步取出当前用户的行为序列计算出用户向量。第三步对所有候选电影打分并排除已看过的。第四步截断为top-N输出。这段逻辑就是系统主服务的核心。def recommend_for_user(uid, top_k10): cand_mask np.ones(len(movie_ids), dtypebool) watched set(user2movies.get(uid, [])) cand_mask[[i for i, m in enumerate(movie_ids) if m in watched]] False cand_vecs movie_vecs[cand_mask] user_vec get_user_embedding(uid, emb, user2movies) scores cosine_similarity([user_vec], cand_vecs)[0] top_indices np.argsort(scores)[-top_k:][::-1] rec_movie_ids [movie_ids[cand_mask][i] for i in top_indices] return rec_movie_ids排除已看过的电影这步容易漏掉。如果不排除用户会反复看到那些他早打过8分的电影——因为它们的embedding离用户向量最近。注意排除操作要用集合查找时间复杂度是O(1)如果弄成list的in判断在几十万部电影上会慢到让你怀疑人生。召回候选的全集就是全部电影因为离线阶段已经把所有embedding算好在线阶段只有一次向量矩阵乘法延迟在毫秒级。4.3 数据集划分与评估别把时间顺序搞乱推荐系统的评估和图像分类完全不同乱划分会让你的结果虚高。常见错误是随机划分评分集让电影在训练时“见过”待预测的用户导致GNN和召回阶段发生信息泄露。我在毕设里用的是时间戳划分或者说留一法每个用户最早80%的评分作为训练图剩下20%用于评估。这样“训练时见过待推荐电影”的情况能被有效抑制。for uid, group in ratings.groupby(uid): group group.sort_values(ts) train_num int(len(group) * 0.8) train_ratings.append(group.iloc[:train_num]) test_ratings.append(group.iloc[train_num:])评估时三个指标就够了Hit Rate10精度的替代品Recall10强调覆盖NDCG10评定推荐顺序。第一次跑出来的数字不要焦虑基于知识图谱的方案通常比纯协同过滤高不了太多但胜在冷门电影推荐和可解释性上。如果你想论文里更有说服力把KGRGCN和纯协同过滤、纯GCN做三个消融对比一张柱状图就能看出每条边、每个模块的作用。5. 模型训练和部署中会遇到的5个坑现象、原因、解决办法5.1 训练loss不降反升最后直接NaN我在第一次跑RGCN时把初始学习率设成了0.1结果loss在前200步直接冲上NaN。原因是原始特征向量没有做归一化内积打分时数值范围波动太大梯度爆炸后embedding数值溢出。解决办法是两处一是每个batch内对embedding做L2归一化二是把优化器换成Adam并降低学习率到0.001~0.005。如果你的embedding是纯随机初始化的加一层LayerNorm也能稳定训练但RGCNConv在图上传消息时对数值变化更敏感L2归一化要放在每一层输出的后面。5.2 图数据太稀疏邻居聚合时全是零向量有些导演只导过一部电影这个导演节点的邻居只有一部电影聚合后它的embedding基本和那部电影的embedding方向一致。而如果一个导演节点完全没有出现在任何有效路径中它的embedding就停留在随机初始化的状态推荐会突然蹦出明显不相关的内容。解决思路是给所有低度实体加一个“自环”操作让每个节点在聚合邻居的同时保留自身特征。RGCNConv自带这个功能只要你传入的edge_index包含每个节点指向自己的边或者检查API是否支持add_self_loops参数。不要硬改模型直接在图数据上加自环边最省事——对每个实体添加一条(实体, 自环, 实体)的三元组即可。5.3 训练集准确率高测试集推荐结果全乱这是典型的过拟合发生在图谱关系数多、但每个关系下的边都很少的时候。例如“某演员出演某电影”这条边只有一两条样本模型却强行学了一整组权重矩阵参数退化成记忆。我建议把RGCN的num_bases从30下调到10或5让不同关系共享基矩阵削弱独立参数过拟合。另外把dropout提高到0.3以上尤其在本体关系特别稀疏时。还有一个偏工程的做法是在构建三元组时对出现次数低于5的导演和演员节点做剪枝直接将低频实体从图中移除让模型只学稳定模式。5.4 线上推荐响应慢用户每次请求都要重新计算embedding这是最容易被忽略的问题。训练完成后如果推荐接口里直接调用模型前向计算哪怕只有几万个节点每次请求也要等数百毫秒到数秒不等。更不可接受的是用户一多GPU显存立刻被击穿。我的一般做法是离线阶段把实体embedding存成npy或Parquet线上服务直接加载内存向量表用来做相似度检索。只有当图谱更新比如新增了一批电影时才重新跑一遍离线训练和embedding导出。这个分离设计的好处是推荐接口完全变成一个索引查询与PyTorch彻底解耦部署难度直线下降。5.5 可视化图谱时导出图片卡死或图乱成一团毕业设计通常要求展示知识图谱的可视化很多人直接用NetworkX的draw函数画全图几万节点全画在一张图上不仅渲染时间超长结果还是一团黑。正确的做法是只抽取某个子图——比如用户最喜欢的那个导演、他执导的电影、以及这些电影的主要演员。子图大小控制在二三十个节点内再交给Neo4j Browser或者PyVis输出交互式HTML页面。可视化是呈现关系结构用的不是训练用的所以这部分单独建一份轻量数据是完全合理的。6. 进阶技巧从Hit Rate到冷启动验证的最后一公里模型跑通、推荐结果出来后还要多做两件事才能让毕设或者项目汇报显得完整并且经得起追问。第一件是验证冷启动能力。冷启动是知识图谱方案相对传统协同过滤最值得写进论文里的优势一个新电影没有任何评分但它的导演、演员信息已经存在图谱里了那么它出生就串联到了几十个邻居它的embedding在GNN传播时已经携带了语义信息。验证方法很简单把若干有丰富图谱连接的测试电影从训练集中去掉然后重新训练模型看hit rate下降幅度。如果下降幅度远小于纯协同过滤那你的工作就有了一个有力的实验结论。另一个值得做的是embedding可视化。把学好的电影embedding降维到二维用不同颜色标记类型你多半能看到科幻片和纪录片分布在不同区域。这是答辩现场最友好的展示材料同时也能帮你直接判断模型有没有真正学到语义结构。如果二维投影中所有电影混成一团那说明训练可能没收敛或图谱构建存在问题这时候再回头调整损失函数或图谱结构。我现在的习惯是把整个项目拆成三步积累先构建干净的三元组数据再训练GNN拿到embedding最后用向量检索完成推荐。这三个阶段独立保存产物后续迭代时不需要每次重跑前两步。个人体会是这一步换来的是反复试错时的“后悔药”图谱构建得越干净后面调参的返工率越低。最后说一句别被“高分开源项目”的说法带着走自己从数据到模型的每一步都走一遍踩过的坑才是答辩时最值钱的内容。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑