ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

知识图谱+GNN:从0到1搭建电影推荐系统全攻略

知识图谱+GNN:从0到1搭建电影推荐系统全攻略 简介本资源是一套面向高校本科生毕业设计与课程综合实践的电影智能推荐系统实现方案聚焦知识图谱构建与图神经网络建模两大核心技术解决传统协同过滤推荐可解释性弱、冷启动效果差等痛点。压缩包共37个文件含21个Python核心模块如kg_loader.py、train.py、model.py、app.py等、5个数据文件users.dat/ratings.dat/movies.dat等、2个README说明文档及若干备份与工具脚本整体大小14.88MB结构清晰、注释详尽便于初学者理解知识图谱嵌入、KGCN模型训练与Web服务部署全流程。已有49人学习下载配套完整源码、数据预处理脚本、GPU内存管理工具及可直接运行的Flask前端界面覆盖从知识图谱构建、模型训练到在线推理的全链路工程实践兼具学术严谨性与工程落地性。 从 0 到 1 搭一套电影推荐系统我选了知识图谱和 GNN 这条路。项目本身不算大但里面能讲的东西非常多——数据怎么清洗、图谱怎么设计、模型怎么选、部署要避开哪些坑每一环都能单独写一篇。我把整个项目的核心实现思路、关键代码、参数设置和踩过的坑完整梳理一遍希望给想入坑图神经网络的同学一条相对顺的路径。先说结论这套基于 Python 实现的电影推荐系统核心是先把用户、电影、演员、导演、类型等实体和关系整理成一张异构图知识图谱再通过图神经网络NGCF / LightGCN 这类模型对节点做嵌入表示学习最后用内积或其他相似度方式生成 TopK 推荐列表。项目代码结构完整训练、评估、部署都有整体难度中偏上适合已经会用 PyTorch、懂一点推荐系统基础知识的开发者。1. 项目核心思路为什么非得用知识图谱和图神经网络1.1 传统协同过滤的局限如果你做过推荐系统一定知道传统协同过滤CF是最朴素的方案UserCF 找相似用户ItemCF 找相似物品本质都是基于用户-物品交互矩阵计算相似度。这套逻辑在数据量足够大、交互足够密集的场景下表现不错但遇到两个问题就很头疼。一是冷启动。新用户没有任何历史行为新电影没有任何交互记录CF 完全失效。二是特征稀疏。你只知道用户和电影之间有没有交互但不知道这个用户为什么喜欢这部电影更不知道电影之间的深层关联。比如一个用户看了《盗梦空间》CF 会推荐同样热门的《星际穿越》因为有很多人同时看了这两部。但如果我们把知识图谱加入系统能知道《盗梦空间》的导演是诺兰主演有莱昂纳多属于科幻类型另一个用户只看过《泰坦尼克号》从交互矩阵看两者八竿子打不着但从知识图谱看两部电影都有莱昂纳多共同导演也是詹姆斯·卡梅隆和诺兰的长期合作团队这个关联就能被捕获。1.2 知识图谱在图推荐里的角色知识图谱本质是语义网络的工业落地形态。它把真实世界的实体和关系用三元组表示头实体、关系、尾实体例如莱昂纳多出演盗梦空间、盗梦空间属于科幻。在电影推荐项目里知识图谱承担两个角色。第一扩展物品之间的语义关联弥补协同矩阵的信息缺失。第二为图神经网络提供邻居结构和传播路径。GNN 的核心操作是消息传递即每个节点通过学习聚合邻居节点信息来更新自身向量表示而邻居是谁、怎么聚合完全依赖图谱结构。用户、电影、演员、导演这些实体天然可以组织成一张异构图包含不同类型的节点和关系。图神经网络正好擅长在这种结构上学习节点嵌入。这就是为什么选择“知识图谱GNN”而不是传统的向量数据库做语义检索——推荐任务需要利用高阶关联做预测不是单纯找语义相似。1.3 方案选型对比NGCF 和 LightGCN模型层面我试过两个方案NGCFNeural Graph Collaborative Filtering和 LightGCNLight Graph Convolution Network。NGCF 是 2019 年 SIGIR 的论文核心思路是把协同过滤信号融入 GNN在用户-物品图上做嵌入传播。它比传统 CF 强在有显式的消息传递机制每一层都会聚合邻居信息并且带特征转换矩阵和激活函数建模能力更强但参数量也更大训练稍微慢一些。我在项目里以 NGCF 作为基线模型因为它的结构更适合理解 GNN 的完整链路。LightGCN 是 NGCF 的简化版2020 年 SIGIR 提出。它把 NGCF 里的特征转换矩阵、非线性激活函数全部去掉只保留最简单的加权聚合操作。作者通过实验证明这些组件在协同过滤场景下不仅没有提升效果反而增加了过拟合风险。LightGCN 实现简单、收敛快、效果不输 NGCF生产环境更实用。我最终主模型用的 NGCF 变体同时在代码里留了 LightGCN 的实现作为对比。两者切换只需要改模型配置后面会讲。2. 数据准备与知识图谱构建这步决定了系统上限2.1 数据来源与预处理数据方面我用的是 MovieLens 1M 数据集它包含 100 万条评分记录、约 6000 个用户和 4000 部电影。这个数据集很经典大多数学术论文都用它做 benchmark。不过它只包含用户 ID、电影 ID、评分和时间戳以及电影的基本类型信息信息量太少。所以我额外从 TMDBThe Movie Database拉取了电影的导演、演员、制片公司、预算、票房、标签等扩展信息。这样一来知识图谱的实体类型更丰富关系类型也更多图结构对电影语义的表达力会明显提升。预处理阶段有几个必须处理的坑中文用户评分数据比较少所以选了 MovieLens 1M。如果项目要求中文环境可以考虑用豆瓣数据但豆瓣的数据抓取和反爬限制比较麻烦不是首选。MovieLens 里的电影 ID 是内部编号TMDB 的 ID 是另一个体系两者需要做实体对齐。我是通过 IMDB ID 做桥接因为 MovieLens 数据集里本身带 IMDB IDTMDB API 也允许按 IMDB ID 查询电影。这一步如果对齐不好会导致后面图谱大量缺边。对齐完成后把实体和关系整理成三张表实体表entity、关系表relation、三元组表triplet。其中关系表我定义了 6 类关系关系头实体尾实体说明出演演员电影演员参演了该电影导演导演电影导演执导了该电影属于电影类型电影属于该类型用户看过用户电影用户对电影有评分行为用户收藏用户电影用户评分 4 视为收藏相似电影电影基于内容特征计算的相似关系2.2 用 Neo4j 还是直接用 PyTorch 的图结构构建知识图谱时有两条技术路线。第一条是用 Neo4j 这类图数据库建图谱优点是有标准查询语言 Cypher、可视化方便、方便实时查询路径缺点是要额外部署数据库服务数据同步麻烦模型训练时还得把图导成 PyTorch 能用的数据结构属于“存的时候爽用的时候痛”。第二条是直接用 Python 的 networkx 构建图结构再用 PyTorch 的 sparse tensor 表示邻接矩阵全程在内存里操作。好处是训练模型时完全不用切换数据格式部署时也不需要额外依赖坏处是没有可视化界面、不方便做复杂图查询。我的方案是两者结合项目前期用 Neo4j 做知识图谱的可视化验证和关系查询确认图谱结构没毛病之后导出三元组列表在训练管线里转换成 PyTorch 的稀疏邻接矩阵。这样既保留图数据库的调试便利性又避免了线上服务对 Neo4j 的强依赖。如果你的项目只需要模型训练不涉及图谱可视化完全可以跳过 Neo4j直接用 networkx 加 pandas 处理三元组逻辑更简单后面部署也少一个组件。数据最终处理成模型可用的格式包含三部分用户-物品交互矩阵稀疏矩阵1 表示有交互物品-实体邻接矩阵表示电影节点和知识图谱实体的连接实体-关系邻接矩阵表示知识图谱内部的结构联合起来就是一张完整的异构图用户节点、电影节点、属性节点演员、导演、类型全部在同一个大图里。2.3 实体链接与去重处理知识图谱构建里最容易出问题的是实体对齐。以演员为例中文名重名的特别多如果只用姓名做 key会把不同演员合并成同一个实体。我的做法是“姓名出生日期出生地”组合判断。TMDB 的数据里有生日和出生地通过这个组合可以把重名的演员区分开。如果数据字段缺失就用电影 ID 反查演员 ID以 TMDB 的人物 ID 为准不做额外的合并操作。还有一个容易忽略的问题是导演同时参演电影。比如诺兰偶尔在自己电影里客串这时同一个实体可能同时连接“导演”和“出演”两种关系。这种多重关系是允许的在图里表现为两个不同类型的关系边处理时只需要保证邻接矩阵的构建逻辑不冲突就行。3. 图神经网络模型设计与训练把“消息传递”讲透3.1 NGCF 的核心公式与原理解读NGCF 的输入是节点的初始嵌入向量每个用户和电影随机初始化一个向量也可以加入内容特征初始化比如 BERT 向量但 MovieLens 场景下没有评论文本所以直接用随机初始化。模型迭代 L 层每层做一次邻居信息聚合。第 l1 层用户节点的嵌入更新公式是e_u^(l1) LeakyReLU( W_1 · e_u^(l) W_2 · (e_u^(l) ⊙ e_N(u)^(l)) )其中 e_N(u)^(l) 表示用户 u 所有邻居节点在第 l 层的嵌入加权和具体公式是e_N(u)^(l) Σ (1 / sqrt(|N(u)| · |N(i)|)) · e_i^(l)这里的 1 / sqrt(|N(u)|·|N(i)|) 是拉普拉斯归一化的标准做法。为什么用这个系数因为单纯取平均值会让度数大的节点比如热门电影邻居聚合结果过于平滑丢失个性。拉普拉斯归一化通过度数加权让低度节点在聚合时获得相对更大的权重缓解热门偏差。符号 ⊙ 表示逐元素乘积。这个设计叫“特征交互”意图是让中心节点和邻居节点在每一维特征上产生乘法交互相当于建模“用户和电影在某个潜在因子上的匹配程度”。我对这个公式的理解是W_1 保留自身特征W_2 建模和邻居的联合特征LeakyReLU 做非线性激活。有了每层的输出最后一步是把所有层的嵌入拼接起来作为节点的最终表示。3.2 LightGCN 的简化与代码实现LightGCN 的改进非常直接去掉特征转换矩阵 W_1、W_2 和非线性激活函数层间传播只做邻居聚合e_u^(l1) Σ (1 / sqrt(|N(u)|·|N(i)|)) · e_i^(l)最终表示是各层嵌入的加权平均权重可以学习也可以设置为固定值比如每层 1/3。下面是 LightGCN 的核心代码片段我用 PyTorch 实现了一个简化版本import torch import torch.nn as nn class LightGCN(nn.Module): def __init__(self, n_users, n_items, embed_dim, n_layers): super().__init__() self.n_users n_users self.n_items n_items self.embed_dim embed_dim self.n_layers n_layers # 用户和物品共享嵌入维度初始化 self.user_embed nn.Embedding(n_users, embed_dim) self.item_embed nn.Embedding(n_items, embed_dim) nn.init.normal_(self.user_embed.weight, std0.1) nn.init.normal_(self.item_embed.weight, std0.1) def forward(self, adj_mat): # adj_mat: [n_usersn_items, n_usersn_items] 稀疏矩阵 ego_embeds torch.cat([self.user_embed.weight, self.item_embed.weight], dim0) all_embeds [ego_embeds] for _ in range(self.n_layers): ego_embeds torch.sparse.mm(adj_mat, ego_embeds) all_embeds.append(ego_embeds) # 各层平均得到最终嵌入 final_embeds torch.stack(all_embeds, dim0).mean(dim0) users, items torch.split(final_embeds, [self.n_users, self.n_items]) return users, items这个代码逻辑非常简洁核心就是循环做稀疏矩阵乘。它之所以有效是因为在用户-物品二部图上不断传播嵌入等价于在图上做随机游走的平滑最终每个节点的表示包含了 L 跳邻居的信息。3.3 BPR Loss 和负采样策略模型训练用的损失函数是 BPR LossBayesian Personalized Ranking。训练数据不是普通的 (user, item, label) 三元组而是 (user, positive_item, negative_item)。positive_item 是用户真实交互过的电影negative_item 是从用户没交互过的电影里随机采样得到。BPR Loss 的核心假设是用户对正样本的偏好程度应该高于负样本。公式是L -ln σ( y_ui - y_uj ) λ ||Θ||²其中 y_ui 是用户对正样本的预测分数y_uj 是负样本的预测分数。网络对预测分数越高的样本学习越充分所以理论上差的负样本对模型提升帮助更大。但实际实现中随机负采样已经够用没必要一上来就上难负样本策略。采样代码import random def sample_negatives(user_pos_items, n_users, n_items, num_neg1): neg_samples [] for u in range(n_users): pos user_pos_items[u] candidates [i for i in range(n_items) if i not in pos] neg random.sample(candidates, num_neg) neg_samples.append(neg) return neg_samples注意每次 epoch 重新采样负样本避免模型记住固定的负样本导致评估指标虚高。3.4 训练配置和评估指标我用的是 Adam 优化器学习率 0.001嵌入维度 64GNN 层数 3batch size 1024训练 200 epoch。评估指标选了 Recall20 和 NDCG20。训练配置的经验值是嵌入维度 64 在 MovieLens 1M 上足够如果再大收益很小显存和内存开销反而明显增加。层数 3 是很多论文的默认值超过 5 层会明显出现过平滑。评估时对每个用户把所有未交互电影都算一遍预测分数取 Top20再和测试集的正样本对比。如果全量计算1 万个用户 × 4 千部电影 4000 万次内积PyTorch 矩阵乘法大概几秒可以接受。需要特别注意的是数据切分方式。我用的是 leave-one-out 切分对每个用户从交互历史里随机选 1 个作为测试正样本1 个作为验证样本其余全部用于训练。这是 GNN 推荐论文的标准做法防止数据泄漏——如果把测试样本混进训练集模型可能直接记住这个用户看过哪部电影评估指标虚高。4. 推荐服务与部署从模型到可用的系统4.1 推荐流程设计模型训练完成后推荐系统进入在线服务阶段。在线服务最核心的问题是如何保证推荐效率。如果每来一个请求都重新跑一遍模型前向计算速度完全跟不上。我的方案是先离线计算好所有用户的 TopK 候选列表在线阶段直接查表返回。整个推荐流程分三层离线层每隔一定时间比如每天重新训练模型或者增量更新模型然后把所有用户的 Top100 候选列表预计算好写入 Redis。在线层用户请求进来直接从 Redis 读取该用户的 Top100 候选再经过过滤、排序、多样性打散返回 Top20。兜底层如果用户是新用户Redis 里没有候选列表返回热门电影 Top20 兜底。4.2 FastAPI 接口实现API 层我用 FastAPI 搭了一个轻量服务代码非常简单from fastapi import FastAPI from pydantic import BaseModel import redis app FastAPI() cache redis.Redis(hostlocalhost, port6379, db0) class RecommendRequest(BaseModel): user_id: int top_k: int 20 app.get(/health) def health(): return {status: ok} app.post(/recommend) def recommend(req: RecommendRequest): key frec:{req.user_id} cached cache.lrange(key, 0, req.top_k - 1) if cached: return {user_id: req.user_id, items: [int(c) for c in cached]} # 新用户兜底返回热门电影 ID hot_items cache.lrange(hot_items, 0, req.top_k - 1) return {user_id: req.user_id, items: [int(c) for c in hot_items]}这里有个细节我没有直接把 PyTorch 模型加载到在线服务里而是用 Redis 缓存 TopK 列表。原因很简单模型加载还需要批次计算单次推理延迟通常几十毫秒如果能承受直接加载模型也更灵活。但对中小型项目来说预计算候选列表的延迟可以压到 3 毫秒以内稳定性和可维护性都更好。如果你的场景需要实时计算比如用户刚看了某部电影需要立即刷新推荐可以先把用户和物品的最终嵌入向量算好存到 FAISS 向量索引里在线阶段先查 FAISS 得到候选再用模型分数重排。这样兼顾效率和实时性。4.3 服务部署与容器化部署环境我用的是 Docker Nginx Gunicorn。FastAPI 应用本身支持 uvicorn但生产环境我建议用 Gunicorn 来管理多个 worker 进程。一个简易的 Dockerfile 大概长这样FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [gunicorn, main:app, -w, 4, -k, uvicorn.workers.UvicornWorker, --bind, 0.0.0.0:8080]启动命令里 -w 4 表示启动 4 个 worker 进程。这里一个容易踩的坑是如果每个 worker 都加载一次 PyTorch 模型4 个进程可能会占用太多内存。所以在线阶段我的方案是预计算候选列表缓存到 Redis模型服务只在训练批次启动在线服务不需要加载模型内存占用很低。安全方面API 接口做了简单的 QPS 限制和请求鉴权防止随意调用导致 Redis 被刷。当然如果只是学习演示这步可以省略。5. 效果对比与关键参数分析5.1 不同模型的效果对比我做了对比实验分别跑完 UserCF、ItemCF、NGCF 和 LightGCN测试集上的 Recall20 指标如下模型Recall20NDCG20UserCF0.08610.0324ItemCF0.12150.0488NGCF0.16320.0765LightGCN0.16580.0791LightGCN 在这种场景下比 NGCF 略好一点主要原因是 NGCF 的参数量更大在 MovieLens 1M 这种中等规模数据上有些过拟合。如果数据量再大一个量级NGCF 的强特征表达能力可能会体现出来。所以做项目时不要迷信“越复杂越好”还是要看数据规模和场景匹配度。5.2 关键超参数对效果的影响嵌入维度和 GNN 层数是最影响效果的两个参数。我做了几组实验直接用 LightGCN固定其他配置嵌入维度从 16 提到 64Recall20 从 0.1452 升到 0.1658128 维时指标变化不大但训练时间涨了 60%。所以推荐赛道上 64 维是性价比最高的选择。层数方面2 层到 3 层效果提升明显但 4 层以上指标不升反降。这个现象叫过平滑因为层数太多所有节点的表示会趋于一致无法区分个体差异。如果你用 GNN 做推荐层数建议锁定在 2~3 层。学习率也是个关键参数。太大模型早期就发散太小100 个 epoch 内跑不完。我用的 Adam 默认 0.001在 0.0005~0.002 之间调整效果差别不大超过 0.005 基本报废。5.3 冷启动处理方案冷启动是推荐系统最经典的问题GNN 方案虽然没有完全解决但比传统 CF 好不少。我的处理策略分三层新用户没有交互数据图里的邻居为空模型无法生成有意义的嵌入。我的方案是用热门榜 最近发布的新电影做混合推荐同时收集用户反馈积累 3~5 条交互后再跑 GNN 模型。新电影利用知识图谱的关系信息。即使没有任何用户打过分电影节点也能通过类型、导演、演员等属性节点和其他电影产生关联GNN 仍然可以生成可用的嵌入。这个就是知识图谱带来的最大增量。新导演 / 新演员如果图谱里完全没有他们的信息只能走语义相似度匹配比如根据影片简介用 Sentence Transformer 算向量相似度在向量库里召回相似电影再融合进推荐列表。6. 常见问题与排查实录6.1 训练不收敛 / Loss 飙升最常见的原因是学习率设置太大。Adam 默认 0.001但如果你的数据是经过某种归一化处理的梯度量级可能不一样需要调低到 0.0001。另一个罪魁祸首是负采样逻辑写错了负样本和正样本在同一位置对调模型根本无法区分哪些是用户喜欢的、哪些是讨厌的。再次强调一定要在训练每个 epoch 重新采样负样本不要复用固定吃过的负样本集合。固定负样本会造成严重过拟合训练曲线看起来很好测试指标低得离谱。6.2 评估指标和论文结果对不上MovieLens 1M 的论文指标通常比较高一个很重要的原因是评估协议不同。如果论文用全量交互作为测试集你用 leave-one-out结果自然会差很多。另外有些论文的 recall 是“用户所有已交互物品里有多少出现在 TopK”你的实现可能是“每个用户只取一个正样本算命中率”这俩算法天差地别。所以跑实验前先确认评估协议是否一致否则对比没有意义。我代码里实现的是标准 leave-one-out Recall20/NDCG20和大多数 GNN 推荐论文一致。6.3 建图时内存溢出MovieLens 1M 的图还比较小邻接矩阵大约 2 万节点 × 2 万节点稀疏存储没问题。但如果你换到 100 万用户的数据集全图邻接矩阵可能撑爆内存。解决方法是使用邻居采样mini-batch每次只取当前批次节点及其 L 跳邻居用小图做计算。GraphSAGE 和 PinSAGE 都是这个思路它们不是过时技术对大规模图是必备方案。6.4 Neo4j 导出的三元组不完整Neo4j 导出 CSV 时默认会把节点里没有关系的孤立实体也导出来。这些孤立实体在图里没有边除了白占内存还可能干扰模型训练。我的做法是在导出时加过滤条件只保留至少有一条关系边的实体节点导完再检查一遍实体数和关系数是不是和数据源对得上。6.5 API 响应慢如果实时推理导致响应慢先看一下是否每次请求都执行了模型前向计算。如果是直接改成预计算缓存方案。如果是缓存已命中但还是慢检查服务和 Redis 是否在同一台机器上网络开销是内网返回毫秒级如果跨云环境访问瓶颈可能就出在网络上。7. 后续优化方向与扩展思路这套系统完全可以扩展。我目前已经做的或计划做的优化有这些一是把单目标 GNN 改成多目标框架。现在只针对评分预测做优化如果加一个“点击率预测”的辅助任务两个 loss 联合训练模型泛化能力会更强尤其在用户行为稀疏的场景下效果明显。二是引入多模态特征。目前的图谱只有结构化关系没有挖掘文本和视觉信息。电影的简介、海报都可以转化成向量作为嵌入的初始化或者辅助特征输入到 GNN对长尾电影和冷启动新片的推荐效果会有大幅提升。三是在线学习。现在模型是离线训练、离线预计算用户反馈无法实时影响推荐。可以加一个增量更新逻辑比如用户产生新交互行为后马上更新该用户的嵌入向量再重新算一次 TopK。在 FAISS 这种向量索引配合下这个过程可以做到秒级。四是尝试更前沿的模型比如 KGAT知识图谱注意力网络、CKAN 这些把知识图谱和注意力机制结合的方法。但回归到工程本身如果你刚上手我建议还是先把 LightGCN 用熟再上复杂模型否则出了问题很难定位是数据问题还是模型问题。最后说点个人体会。做这个项目最大的收获不是模型效果刷到多高而是把整条链路跑通了——从原始数据到知识图谱从图结构到 GNN 训练从模型到线上服务。每一环单独拉出来都有现成的开源方案但串起来的时候问题一个接一个比如实体对齐、负采样协议、评估指标口径这些坑不实操一遍根本不会意识到。如果你准备复现这个项目建议先把数据准备和评估协议做好模型反而是最容易的部分。本文还有配套的精品资源点击获取
返回列表