ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

知识图谱+图神经网络:构建可解释电影推荐系统实战

知识图谱+图神经网络:构建可解释电影推荐系统实战

简介:一套基于Python的知识图谱与图神经网络电影推荐系统毕业设计源码,主要面向计算机、软件工程等专业的学生,适用于毕业设计、期末大作业或课程设计场景。项目以知识图谱为背景,借助图神经网络建模用户与电影之间的高阶关系,完整覆盖数据预处理、知识图谱构建、模型训练与评估、Web交互展示等环节。资源包为zip格式,共31个文件,以21个Python脚本为主,辅以电影评分等dat数据文件、txt与readme说明、md文档,整体大小约14.84MB;目录按功能拆分为数据处理、图谱构建、模型层、应用层等模块,结构清晰,便于按需阅读和替换。目前已有172人浏览学习。代码注释较详细,新手也能较快上手;内置KGCN等网络模型实现,系统功能完整、界面简洁,且经过运行调试,可直接演示或二次开发,同时能帮助读者梳理推荐系统从原始数据清洗到图谱构建再到模型推理的完整实现思路。

1. 知识图谱+图神经网络:电影推荐系统不是炫技,是毕业设计最稳的“可解释”路线

打开各种招聘网站,算法岗 JD 里出现频率最高的三个词就是知识图谱、图神经网络和推荐系统。把它们拼成一套电影推荐系统,正好卡住多个热门技术点,这大概是为什么这个题目能成为高分毕设方向。这个项目的本质不是做一个能看懂的 Demo,而是把用户、电影、导演、演员、类型这些分散的数据建成一张图,让图神经网络在图上做预测——同一部电影,为什么推荐给 A 不推荐给 B,答案由关系和路径决定,天然可解释。

这套系统适合三类人:想用 Python 体系串起数据处理、模型训练和 Web 展示的毕业生;想在简历里放一个完整推荐项目、但不想只写“调了 sklearn”的初学者;以及想把知识图谱构建和图神经网络同时落地的同学。这个课题的难度不在模型,而在把知识图谱构建、图神经网络训练和 Django 工程拼起来缝好。下面就是我从选型、建模到代码实现的完整链路。

2. 推荐系统选型判断:为什么这个组合能拿高分,而不是堆砌技术

2.1 三种推荐路线对比:协同过滤、深度兴趣网络与 KG-GNN 的边界

最常见的推荐系统毕业设计是协同过滤,比如基于用户的 UserCF、基于物品的 ItemCF,或者矩阵分解 SVD。早期用 MovieLens 数据集跑一个 SVD,能出 ROC 曲线、能算召回率,一个不错的毕设就有了。但它的硬伤也很明显:推荐结果没有理由,用户看不到“为什么推荐这部电影”。面试官问一句“你的系统怎么解释推荐结果”,就只能答“因为相似用户看过”。

深度兴趣网络 DIN 这类点击率模型做了特征交叉,效果不错,但特征工程太重,观众年龄、职业、地域都得清洗、拼接。对于电影域来说,最有用的一类特征是内容之间的语义关系——比如诺兰导演、克里斯托弗·诺兰、星际穿越和盗梦空间——这类关系用稠密特征表达很差,但用图结构表达却非常自然。

知识图谱和图神经网络的组合解决了这两个问题:知识图谱构建提供关系语义,图神经网络做关系推理。同一个知识图谱里,“诺兰导演了星际穿越”“马修·麦康纳演过星际穿越”这两条路径,就能解释为什么系统给喜欢“星际穿越”的用户推荐“盗梦空间”。这是协同过滤给不了的东西。

2.2 知识图谱 + 图神经网络:一个“图特征 + 用户行为”的双塔架构

常见做法是把推荐系统拆成“用户侧”和“物品侧”两个塔。用户侧只处理用户的历史行为序列(看过什么、评分多少、点没点收藏);物品侧则把所有电影放在知识图谱里,用图神经网络把每部电影编码成一个向量。两个向量做内积,就是用户对这部电影的偏好得分。

知识图谱在里面的角色是物品特征生成的来源,图神经网络的角色是从图中邻居节点聚合信息。用户的物品是电影,但电影的特征不只是类型和年份,还包括演员、导演、编剧、获奖信息、系列归属等。这些结构信息用 TF-IDF 没法表达,用 embedding 又过于隐晦,只有图结构能同时保留实体和关系。

这里要提醒一句:不是非得上大模型才有用。对毕业设计而言,一个只有 3~4 层的图卷积网络就能跑出明显优于矩阵分解的效果。真正的难点在召回和排序的数据准备,以及把模型嵌入到一个能在浏览器里演示的系统。

2.3 为什么这个项目“对新手不友好但值得做”:关键技能清单

坦率说,这不是一个几天能写完的小项目,它同时涉及 Python 数据处理、图构建、PyTorch 的 GNN 模块和 Django 工程。但它的价值也正在于此。一个系统里有四种技能栈,面试或答辩时能讲的东西足够撑满 20 分钟。

技能清单大致是:爬虫与数据清洗(拿电影元数据)、关系三元组建模、图数据库或图存储设计、PyTorch Geometric 的 GNN 训练、Django REST Framework 接口开发和简单的 Vue 或 Bootstrap 前端。不会图神经网络,可以只做知识图谱构建和协同过滤的对比实验;不会 Django,可以用 Flask + ECharts 做一个交互式可视化。核心是知识图谱和周 neural 网络能同时在系统里跑起来。

3. 第一步建模:把残缺的原始数据做成可以训练的知识图谱

3.1 数据源选型和字段设计:不要一上来抓“全量数据”

电影推荐领域最常见的公开数据集是 MovieLens 1M——100 万条评分,6040 个用户、3900 部电影。这个集子适合做模型训练,但不适合做知识图谱展示,原因是它只有电影的 ID、标题和类型,没有导演、演员、剧情这样的实体关系。所以必须做实体对齐,把 MovieLens 的 movieId 和 IMDb/TMDb 的 ID 对齐,再去补导演、演员、编剧、类型、语言、发行年份等属性。

对齐的方法很直接,用电影的标题、年份和类型做一个相似度匹配。常见做法是人工抽 100 部电影建立映射表,再用模糊匹配补全。不要试图把 3900 部全部对齐,能对齐到 1500–2000 部就足以构建一个看起来丰满了。对齐后过滤掉没有导演或演员信息的条目,再把 Q 条评分过滤成至少看过两部有完整信息的电影的用户的记录,这样 GNN 训练时不会出现大量“只有一个交互”的冷启动节点。

数据类型分两类:实体表和关系表。实体表包括 user、movie、person(导演/演员)、genre、keyword;关系表则包括 rate、directed_by、acted_by、has_genre、belongs_to_series 等。一张电影表存静态属性(年份、时长、语言),另外的人、类型、关键词单独建表。

3.2 构建三元组与邻居采样:从关系表到图结构

知识图谱在代码里的最小单元是三元组 (h, r, t),即头实体、关系、尾实体。例如“盗梦空间 - 导演 - 克里斯托弗·诺兰”就是一个三元组。这个三元组再换成内部的数字 ID 存储,就变成一个边表:head_id, relation_id, tail_id。所谓“构建知识图谱”,本质上是把多张 MySQL/Pandas 表压成三列 edge list,再加上每个节点的类型标注。

这里不建议新手直接上 Neo4j,因为大部分实验室机器上装 Neo4j 需要 JVM 环境,学生机容易卡,而且训练 GNN 时最终要的仍然是内存里的 PyTorch 张量。Neo4j 适合做可视化和交互查询,不适合直接训练。常见做法是:用 pandas 清洗三元组并用 PyTorch Geometric 构建图数据,另用 Neo4j 做前后端联调时的知识图谱可视化查询服务。两条线可以并行,但核心训练逻辑不依赖图数据库。

import pandas as pd import torch from torch_geometric.data import Data # 加载三元组:triplet.csv 的三列分别是 head_id, relation_id, tail_id triplet_df = pd.read_csv("data/triplet.csv") head = torch.tensor(triplet_df["head_id"].values, dtype=torch.long) tail = torch.tensor(triplet_df["tail_id"].values, dtype=torch.long) edge_type = torch.tensor(triplet_df["relation_id"].values, dtype=torch.long) # 自环处理:确保知识图谱中每个孤立节点能聚合到自身 self_loop_head = torch.arange(triplet_df[["head_id", "tail_id"]].max().max() + 1) self_loop_tail = self_loop_head.clone() self_loop_type = torch.zeros_like(self_loop_head) # 合并成最终的边表 edge_index = torch.stack([torch.cat([head, self_loop_head]), torch.cat([tail, self_loop_tail])], dim=0) edge_type = torch.cat([edge_type, self_loop_type]) data = Data(edge_index=edge_index, edge_type=edge_type)

这段代码的核心是构建 PyTorch Geometric 所需的边表结构。edge_index是 2×E 的张量,第一行表示边的起点,第二行表示边的终点。edge_type表示每条边的类型,也就是知识图谱里的关系类型。最后加了自环,让每个节点都能通过 relation_type=0 的自环边聚合到自己的邻居,否则孤立节点在 GNN 的邻居传递中会丢失自身信息。

训练前要重点检查三个参数:max()+1这里必须是整个数据集中实体的总数,而不是三元组表的行数;如果实体 ID 没有从 0 开始连续编号,要先做 LabelEncoder 转换;edge_type的类型得是 long 型,因为 R-GCN 卷积层按关系类型建立权重矩阵时要用它做索引。

3.3 邻居特征与初始向量:为什么不能把 ID 直接当成特征

图神经网络做的是“邻居聚合”,每个节点在每层都会把邻居的特征加权求和,然后更新自己的表示。如果在构建知识图谱时不给节点任何特征,模型只能学一个“ID 的嵌入”,冷启动用户和电影基本没有意义。所以构建节点初始特征这一步不能省。

常见做法是给每部电影拼接一个文本向量,比如用 TF-IDF 把“类型+剧情”压成 128 维的稀疏向量;用户节点则用其历史评分过的电影 embedding 均值初始化。这里最省力又有效的办法是给电影做一个 300 维的预训练词向量平均,沿着“title + genres + overview”生成的文本做 embedding pooling。用 gensim 的 KeyedVectors 加载中文或英文向量都行,但注意版本要匹配。

import gensim.downloader as api import numpy as np from sklearn.preprocessing import normalize # 加载预训练英文词向量,66MB 左右 wv = api.load("glove-wiki-gigaword-100") dim = 100 def movie_embedding(row, wv, dim): tokens = str(row["title"] + " " + row["genres"] + " " + row.get("overview", "")).lower().split() vecs = [wv[t] for t in tokens if t in wv] if len(vecs) == 0: return np.zeros(dim) return np.mean(vecs, axis=0) vecs = [] for _, row in movies_df.iterrows(): vecs.append(movie_embedding(row, wv, dim)) feat_matrix = normalize(np.vstack(vecs), norm="l2")

这段代码把文本特征转成节点特征矩阵。加载的glove-wiki-gigaword-100是 100 维向量,训练快、内存占用小,值得作为首选。movie_embedding先做小写和分词,再对所有 token 的词向量做均值池化。如果一部电影的标题+类型里一个词都不在词表里,就用零向量兜底,但这会带来一个所有冷启动电影都相同的坑。

这个环节最讽刺的问题是:知识图谱构建完成后,模型性能往往不看图谱有多“满”,而看有没有特征对齐。实体、关系的数量做到 300 万条反而会拖慢 GNN 训练速度,而 10 万条以内的三元组、1 万以内的节点数已经足够支撑一个能毕业演示的图了。知识图谱构建的目标是“刚好够用”,不是“完整复刻 IMDb”。

4. GNN 模型选型与实现:把知识图谱喂给 R-GCN 并做推荐

4.1 为什么用 R-GCN 而不是 GCN:关系类型决定聚合权重

知识图谱里的关系不是无差别的“连了一条边”,导演关系和演员关系在语义上完全不同。普通 GCN 把所有边当成同一种关系做消息传递,会把“导演了”和“演过”混在一起,显然不科学。R-GCN(Relational Graph Convolutional Network)为每一种关系学习一个独立的权重矩阵,聚合时按关系类型区分处理,是最贴合知识图谱的标准选择。

R-GCN 的更新公式是一次 sum 聚合加上一个自连接。具体到实现,PyTorch Geometric 的RGCNConv层会自动完成按关系类型分开的nn.Linear计算和加和。需要手动指定的是关系类型数num_relations,也就是三元组里 relation_id 的取值个数。如果清洗后只有导演、演员、类型、编剧本、系列五类关系,这个参数就是 5 加上自环的 1。

这个参数直接影响性能表现。关系类别太少,模型退化成 GCN,不同边的语义会被平均掉;关系类别太多又容易过拟合,因为每一种关系相当于一个子图。经验值是控制在 5~10 个之间,能把评分预测 AUC 做到 0.85 以上。

4.2 编写 R-GCN 模型和一版训练代码

这里给出一个可以直接运行的 R-GCN 模型主体。用户节点和电影节点位于同一个图里,通过“user-rate-movie”这条边连接。输入特征是上一节构建的feat_matrix,输出维度设为 64。

import torch import torch.nn.functional as F from torch_geometric.nn import RGCNConv class RGCNRec(torch.nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, num_relations, num_bases=None): super().__init__() # 第一层 R-GCN,将输入特征映射到隐层 self.conv1 = RGCNConv(in_dim, hidden_dim, num_relations, num_bases=num_bases) # 第二层 R-GCN,把隐层压缩到输出向量 self.conv2 = RGCNConv(hidden_dim, out_dim, num_relations, num_bases=num_bases) def forward(self, x, edge_index, edge_type): x = self.conv1(x, edge_index, edge_type) x = F.relu(x) x = F.dropout(x, training=self.training) x = self.conv2(x, edge_index, edge_type) return x

训练时把评分大于等于 4 的边当作正样本,评分为 0 或小于 3 的当作负样本。为了加快迭代,训练逼近的目标是“用户对电影的偏好得分”,而偏好得分是用户节点的嵌入与电影节点的嵌入的内积。这样模型输出经过sigmoid之后可以直接当点击率使用。

from sklearn.metrics import roc_auc_score model = RGCNRec(in_dim=100, hidden_dim=64, out_dim=64, num_relations=6) optimizer = torch.optim.Adam(model.parameters(), lr=0.005) def train(model, data, epochs=50): model.train() for epoch in range(epochs): optimizer.zero_grad() out = model(data.x, data.edge_index, data.edge_type) # 正样本:用户节点 n_id 到电影节点 m_id 的偏好分 pos_score = (out[user_ids] * out[movie_ids]).sum(dim=1) # 负样本:随机采样同样数量的不存在的观看关系 neg_score = (out[neg_user_ids] * out[neg_movie_ids]).sum(dim=1) 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)]) ) loss.backward() optimizer.step() if epoch % 10 == 0: print(f"Epoch {epoch}, Loss {loss.item():.4f}")

训练周期一般 30~50 轮就够,因为知识图谱节点数量不到 1 万。lr=0.005是初始值,如果 loss 出现震荡,降回调到 0.001。num_bases参数这里有意没有设置,它会用完整的按关系独立矩阵;如果关系数量太多,可以通过num_bases=4让不同关系共享一组基矩阵,能显著减少参数。

这里是不是一定要用内积做最后评分?可以选择 MLP。但在小数据集上,内积泛化更稳定,因为知识图谱嵌入本身已经在一个高维空间里,用户和电影的内积等价于计算余弦相似度,不需要额外的非线性层。往后做要做冷启动测试,直接把新电影的文本特征经过 GNN 得到一个向量,和用户向量算内积就能推荐。

4.3 评价与消融:用 AUC、Recall@K 验证不是“黑匣子”

模型必须跑出对比实验,否则答辩环节会被追问到怀疑人生。建议三组对比:UserCF 协同过滤作为基线、GCN 加上关系类型混淆作为次基线、R-GCN 作为最终方案。数据集切分按时间戳:80% 历史行为训练,20% 最近行为测试。评估方式采用 leave-one-out 协议:每个测试用户抽出最后一次评分作为正确项,再从全部电影中随机采样 99 部错误项,模型对这 100 部排序,算 Recall@10 和 AUC。

from sklearn.metrics import ndcg_score, roc_auc_score # pred_scores: (n_test_user, 100) 每行是用户对100部电影的排序得分 # true_flag: (n_test_user, 100) 每行第0列是1,后面99列为0 auc = roc_auc_score(true_flag.flatten(), pred_scores.flatten()) recall_at_10 = (true_flag[:, :10].sum(axis=1) > 0).mean() ndcg = ndcg_score(true_flag, pred_scores) print(f"AUC={auc:.4f}, Recall@10={recall_at_10:.4f}, NDCG={ndcg:.4f}")

这套评估代码在复现时要注意true_flag中正样本必须放在第 0 列,排序时按分数从高到低排。如果正样本混在 100 部随机电影里,每次运行重复抽样会让指标有波动,建议设置固定随机种子。AUC 能上 0.85 以上、Recall@10 在 0.3 左右,已经可以放进论文了。

5. 整个系统怎么串起来:从训练好的模型到能在浏览器里玩的推荐应用

5.1 Django REST Framework 搭建后端推理接口

模型训练只是第一步,能演示的系统才是真正的毕业设计。常见方案是 Django + Django REST Framework 做后端接口,前端用 Vue 或 Bootstrap 展示。模型训练在离线阶段完成,将用户嵌入和电影嵌入保存为 numpy 数组或 torch tensor;在线推理阶段只查库,不再跑模型。

在线推理只需要两个操作:根据当前用户 ID 取出用户嵌入向量;与所有电影嵌入向量做内积,排序取 top-N。这个计算量在几千部电影的规模上毫秒级完成,远不需要 GPU。如果用户是全新的,没有历史行为,就从图神经网络里找到与它最像的 K 个用户,再把这 K 个用户看过的电影融合排序,这是最朴素的冷启动策略。

# inference/views.py import numpy as np from django.http import JsonResponse user_emb = np.load("model/embedding/user_emb.npy") movie_emb = np.load("model/embedding/movie_emb.npy") movie_list = np.load("model/embedding/movie_id.npy", allow_pickle=True) def recommend(request, uid): u_vec = user_emb[uid].reshape(1, -1) scores = (movie_emb @ u_vec.T).flatten() top_idx = scores.argsort()[-10:][::-1] movies = movie_list[top_idx] return JsonResponse({"uid": uid, "movies": movies.tolist(), "scores": scores[top_idx].tolist()})

这里的@符号就是矩阵乘法,等价于批量计算用户向量和所有电影向量的点积。Django 框架里每次请求都加载三个 numpy 文件是可以接受的,但如果在高并发下就先把movie_emb放进内存缓存。调用接口后前端拿到movies数组展示卡片,推荐理由则从知识图谱里查询第一条邻接路径。

5.2 知识图谱可视化:让答辩评委一眼看懂项目在做什么

毕设最终需要“能看见的东西”。推荐结果可以只展示一张电影海报列表,但那样体现不了知识图谱。最省事的做法是用 Neo4j 的 Browser 或 D3.js 画一个子图,然后通过电影 ID 从图里取出“用户兴趣-导演-电影”的路径。具体路径查询用 Cypher:

MATCH (u:User {uid: 42})-[r:RATED]->(m1:Movie)-[d:DIRECTED_BY]->(p:Person)<-[d2:DIRECTED_BY]-(rec:Movie) WHERE rec.movie_id <> m1.movie_id RETURN u, m1, p, rec LIMIT 5

这条展示的核心是让评委看到“你为什么推荐这部电影”。路径意思是:用户 42 给了电影 A 高分,电影 A 的导演 P 同时导演了电影 B,因此系统把 B 推荐给了用户。这比任何指标都直观。Django 后端把查询结果拼成 JSON,前端 D3 渲染成节点关系图即可。

5.3 离线和在线架构的边界:模型可以通过 ONNX 导出或固化向量

这里讲一个常见误用:把 PyTorch 模型直接嵌进 Django 里做推理。Django 进程每次重启都要重新加载模型权重,不仅慢而且容易引入大量依赖冲突。常见做法是离线训练完,把每层权重转成 numpy 并使用矩阵乘法推理,或者用torch.jit.script导出为一个静态模型文件。我在这个项目里用前者,因为嵌入表的大小不会超过几 MB,numpy 足以支撑负载。

同时要区分“离线任务”和“在线请求”。每天凌晨跑一次全量重算:用户新行为、新入库的电影、新知识图谱实体都转入离线模块,重新生成嵌入表。在线请求只做读取和排序。这样既不会让推荐结果过时,也不会因为高频批训练拖垮演示服务器。

6. 避坑指南:知识图谱 + GNN 电影推荐常见的 5 个翻车点

6.1 知识图谱构建完毕后,边表里有重复边,模型不收敛

现象是训练 loss 一直在高位摆动,验证集 AUC 停滞在 0.5 左右。原因是 userId 和 movieId 在原始评分表里非常离散,许多用户对同一部电影的历史评分重复出现。在构建三元组和边表前没有做drop_duplicates()去重,导致同一条 user-rate-movie 三元组出现多次,RGCNConv 的消息传递权重被重复累加。

解决方法是三元组构建函数里加一行triplet_df = triplet_df.drop_duplicates(subset=["head_id", "relation_id", "tail_id"])。这一步虽然简单,但直接影响边表统计。做完后看边表的规模,MovieLens 1M 的数据集三元组应该在 120 万条以内,超过这个数就要怀疑是重复边问题了。

6.2 节点 ID 没有做连续化,使用 embedding 时报 IndexError

现象是训练时embedding层报“index out of range”。原因是电影表里的 movieId 是字符串或者从 1 开始的稀疏 ID,但 R-GCN 要求节点索引是从 0 开始的连续整数。比如只有 3900 部电影,但 movieId 可能达到 2 万多。把原始 ID 直接当索引放进张量,当然会越界。

解决方法是先做 LabelEncoder,把用户和电影统一映射到 0~N-1。要特别注意的是,三元组中的 head/tail 同时包含用户和电影,实体空间要跨两个表一起编码。做le.fit_transform(pd.concat([head_id, tail_id]))而不是分别对用户表、电影表编码。

6.3 MovieLens 里的用户和 IMDb 里的实体对不齐,推荐结果串戏

现象是知识图谱里 “诺兰导演的电影” 包含了 5 部 MovieLens 里根本不在用户评分范围内的作品,导致推荐结果里总混入“查无此片”的条目。原因是两种数据源 ID 体系不同,没有经过统一的实体对齐。

解决方法是构建一个 title 层面的映射表:先用年份和标准化后的 title 做交集匹配,匹配不上的用模糊匹配,最后人工抽检 100 条记录保证准确率在 90% 以上。只有对齐好的实体才能进入三元组构建环节,这个约束必须写进数据处理流程,不能事后补救。

6.4 空特征节点太多,graph 里出现大量零向量

现象是冷启动的电影没有任何评分,也没有任何文本特征,它的 embedding 从一开始就是零向量。经过两层 GNN 聚合后,它会把自己的零向量传给邻居,导致同时推荐出大量垃圾电影。原因可追溯到电影文本特征拼接时,glove词典里没有对应的词,返回了零向量。

解决方法是把movie_embedding的兜底策略从零向量改为随机小向量或类中心向量。具体做法:先用所有有特征的电影向量计算一个全局均值向量,遇到没有词命中的电影就把这个均值向量加上微小高斯噪声赋给它。这样既不引入语义偏置,也避免零向量污染消息传递。

6.5 推荐结果看起来有道理,但演示时用户切到新账号会产生空白页

现象是系统上线演示时,新注册用户没有历史行为,进入首页推荐列表报错。原因是代码只考虑了有嵌入的用户,没处理user_emb[uid]的越界问题。解决方法是建一个“游客用户”的兜底逻辑:把未登录或新注册用户映射到一个随机用户 ID,或者直接用所有用户向量的均值作为其表示。

方法是在推荐接口开头判断uid是否超出user_emb.shape[0],如果超出或该用户没有正向行为记录,就取全局用户向量的均值。如果想让效果更好,让前端传几个感兴趣的标签,后端从知识图谱里按标签召回电影,这也是一种很精致的冷启动演示方案。

7. 最后一步:让系统在答辩中显得“很值钱”的三个加分细节

7.1 推荐理由可解释性:把路径变成一句话

GNN 给了一个分数,但用户和评委不会只看分数,他们想看到的是“为什么”。实现方式是在推荐接口返回的 JSON 里加一个字段reason_path,比如 “你给《星际穿越》打了高分,该片导演诺兰也执导了《盗梦空间》”。这个理由不是模型生成的,是从知识图谱路径里查出来的。

具体做法是从推荐列表里挑出 top-1 的一部电影,在知识图谱中执行一跳查询,从用户已高分的电影出发找同导演或同演员的路径。Cypher 不通时可以直接用 pandas 扫一遍三元组:

def explain(user_id, rec_movie_id): user_watched = triple_df[(triple_df["head_id"] == user_id) & (triple_df["relation_id"] == RATE)] for movie, title in user_watched: path = triple_df[(triple_df["head_id"] == movie) & (triple_df["tail_id"] == rec_movie_id)] if not path.empty: return f"因为你看过《{movie_name[movie]}》" return "因为这部电影与你的兴趣图谱距离较近"

这个解释虽然朴素,但效果立竿见影——评委看到的不只是一个 JSON 打分,而是一条有逻辑链的推荐依据。答辩时演示一遍路径可视化,项目的高度立刻就不一样了。

7.2 消融实验:证明“知识图谱和 GNN 都比没有更强”

如果有时间,再做一组消融实验:把 R-GCN 的关系分支减掉变成 GCN,把知识图谱边表随机打乱 20% 再加进模型,观察指标下降。比如 GCN 的 AUC 掉到 0.81,打乱边的 R-GCN 掉到 0.78,完整版 R-GCN 是 0.86,这个数据放进论文就是很有说服力的一张表。

7.3 可视化:把图嵌入和知识图谱展示成“可讲故事”的页面

最后在效果页面上,用 D3.js 画一个力导向图展示用户节点和电影节点,把 GNN 的注意力权重或一跳邻居共现画成边的粗细。我习惯把查询出的推荐理由路径存成 JSON 片段,用 ECharts 的关系图类型直接渲染,不需要为由前端框架做复杂定制。这个可视化的核心目的是让非技术背景的评委也能 20 秒看懂项目在做什么。

这套系统能做到的边界在哪里?老实说,它不是业界领先的推荐引擎,离 YouTube 的排序模型差着十万八千里。但对一个毕业设计而言,它同时覆盖了知识图谱、图神经网络、工程化部署和可视化,而且每一步都有东西能讲、能演示、能应对追问。我做这类项目时最大的教训就是别贪多,把 10 万级三元组和 1 万级节点做好、把可解释路径做完整,已经足够拿高分了。希望这些细节和踩坑记录能帮你的毕业设计少走弯路。

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

返回列表