知识图谱+图神经网络:电影推荐系统的原理与工程实践

发布时间:2026/11/2 5:53:47
知识图谱+图神经网络:电影推荐系统的原理与工程实践 简介这是一套面向毕业设计场景的电影推荐系统Python项目核心思路是将知识图谱与图神经网络结合用于个性化推荐适合计算机相关专业学生作课程设计或毕设参考也适合推荐系统初学者学习算法落地数据侧采用经典的MovieLens 1M电影评分数据集运行环境为Python3.8与TensorFlow2.3.8代码结构清晰训练得到模型后即可输出推荐结果。资源包内共包含三十二个文件主体是二十一个Python脚本分别负责数据集处理、知识图谱构建、图神经网络模型搭建、训练与评估等关键环节同时附带五个数据文件、三个文本说明、两个说明文档和一个Markdown笔记便于快速上手整个资源包大小仅为十二点六九MB下载部署都很方便。已有四百九十人学习或下载该资源对希望快速掌握知识图谱推荐方案的同学这套项目提供了从原始数据清洗、图谱关系构建、模型训练到结果预测的完整可运行代码可作为毕设代码基础或进一步扩展起点实用价值较高。1. 从协同过滤到图神经网网络这份项目到底解决了什么问题做过推荐系统的同学都知道传统的协同过滤在面对冷启动用户和稀疏评分矩阵时表现往往不够理想。而基于 Python 的知识图谱和图神经网络电影推荐系统核心思路是在用户和物品之间插入一张知识图谱——电影和电影之间通过导演、演员、类型、标签等实体建立关联再利用图神经网络把这些多跳关系注入到推荐模型里。这套项目选用 ml-1m 数据集技术栈锁定 python3.8 tensorflow2.3.8工程结构上区分了数据预处理、知识图谱构建、KGCN 模型训练与结果预测几个模块适合需要完成毕设或课程设计的同学直接拿来做复现学习和二次开发。打开项目后主流程非常短先运行 main.py 生成 KCGN.h5 权重文件再运行 test 脚本完成预测。看起来简单但实际跑通和调参的过程里藏着不少细节这篇笔记就按数据准备、模型结构、训练实操、常见坑和验证方法五个层面拆开讲。2. 数据准备与知识图谱构建ml-1m 怎么变成可计算的图2.1 三个原始数据文件和 data_process.py 的字段血缘项目的数据源头是 MovieLens 1M 数据集对应文件是 users.dat、ratings.dat、movies.dat。三个文件的原始格式都是::分隔的文本和常见的 CSV 不太一样。users.dat 每行是UserID::Gender::Age::Occupation::Zip-coderatings.dat 每行是UserID::MovieID::Rating::Timestampmovies.dat 每行是MovieID::Title::Genres其中 Genres 是|分隔的多标签。data_process.py 的职责就是把这三份原始文件解析成后续训练可用的结构通常的处理方式是先把::替换成标准分隔符再过滤掉无效行和缺失字段。import pandas as pd # 读取 ratings.dat原始分隔符是 :: ratings pd.read_csv(data/ratings.dat, sep::, names[UserID, MovieID, Rating, Timestamp], enginepython) # 读取 movies.dat注意 Genres 字段内部以 | 分隔 movies pd.read_csv(data/movies.dat, sep::, names[MovieID, Title, Genres], enginepython, encodinglatin-1) # ratings 只保留 UserID、MovieID、Rating 三列用于后续训练 ratings ratings[[UserID, MovieID, Rating]] print(ratings.head())这里的sep::是关键——如果用默认逗号去读会直接把整行读成一个字段。enginepython是因为::属于多字符分隔符C 引擎不支持。编码方面 movies.dat 因为包含特殊字符建议显式指定latin-1而不是 UTF-8否则部分片名里的非 ASCII 字符会直接抛 UnicodeDecodeError。处理完后通常会做一步索引对齐把 UserID 和 MovieID 映射到从 0 开始的连续整数编号因为后面的嵌入层需要输入连续索引如果原始 ID 有空洞训练过程中会出现索引越界。映射方式就是一个简单的字典或 pandas 的 factorize。# 将用户和电影 ID 映射到连续索引避免嵌入层索引越界 user_ids ratings[UserID].unique() movie_ids ratings[MovieID].unique() user2idx {u: i for i, u in enumerate(user_ids)} movie2idx {m: j for j, m in enumerate(movie_ids)} ratings[user_idx] ratings[UserID].map(user2idx) ratings[movie_idx] ratings[MovieID].map(movie2idx) print(用户数:, len(user2idx), 电影数:, len(movie2idx))2.2 create_kg.py 背后的实体-关系-三元组逻辑知识图谱不是数据集自带的而是要从已有数据里自动生成。create_kg.py 的核心逻辑是抽取三元组(头实体, 关系, 尾实体)这决定了图神经网络能捕获哪些语义关联。对于电影推荐场景常见的做法是把电影作为头实体把导演、演员、电影类型、上映年份等属性作为尾实体属性名作为关系类型。# 伪代码示意从 movies.dat 抽取知识图谱三元组 triples [] # 电影 - 类型 for movie_id, genres in movie_genres.items(): for genre in genres: triples.append((movie_id, genre, genre)) # 电影 - 导演/演员需要额外数据或解析片名 # 如果项目内部只基于 ml-1m最常见的是类型作为关系 with open(data/kg.txt, w, encodingutf-8) as f: for h, r, t in triples: f.write(f{h}\t{r}\t{t}\n) print(三元组数量:, len(triples))kg.txt 是知识图谱的文件落地形态每一行是一个三元组这三个字段分别是电影 ID、关系类型、属性实体 ID。kg_loader.py 负责把这个文件读入内存并建立邻居映射。这个步骤的输出直接决定模型能看到几跳的图信息——如果只抽了类型一种关系用户从《盗梦空间》能关联到同为科幻片的《星际穿越》如果补上演员关系还能通过诺兰这个中间节点关联到其他导演的作品。数据准备到这里有一个容易被忽略的关键点训练样本里的正反馈和知识图谱的实体数量需要嵌套映射不能出现 Training 里出现的电影在主图中不存在的孤立点。如果某个电影 ID 在 ratings.dat 里出现过但因为没有类型信息而没进三元组这个电影在后续邻居采样时拿到的是空列表训练过程会出现 shape 不匹配的报错。常见做法是构建知识图谱之后把所有主图中存在的电影 ID 集合与训练样本做一次交集过滤。3. KGCN 模型核心layer.py、model.py、tool.py 各层拆解3.1 layer.py 里做的不是图卷积是邻域聚合KGCN 全称 Knowledge Graph Convolutional Network它和普通的 GCN 最大的区别在于每一层不是对整个图做拉普拉斯变换而是对一个实体在知识图谱上的 N 跳邻居做采样聚合。layer.py 在这个项目里实现的正是单层聚合给定头实体电影的嵌入向量去它的邻居集合里采样固定数量比如 N4的尾实体然后对邻居嵌入做注意力加权和。# layer.py 中聚合层的关键计算逻辑简化示意 import tensorflow as tf class Aggregator(tf.keras.layers.Layer): def __init__(self, input_dim, output_dim, neighbor_num4): super().__init__() self.neighbor_num neighbor_num # 两个全连接变换分别处理自身信息和邻居信息 self.self_w tf.keras.layers.Dense(output_dim) self.neigh_w tf.keras.layers.Dense(output_dim) def call(self, entity_emb, neighbor_emb): # entity_emb: [batch, dim] 当前物品嵌入 # neighbor_emb: [batch, neighbor_num, dim] 采样到的邻居嵌入 neighbor_agg tf.reduce_mean(neighbor_emb, axis1) # 先求均值 # 自身嵌入和邻居聚合结果拼接后过全连接 concat tf.concat([entity_emb, neighbor_agg], axis-1) return tf.nn.relu(self.neigh_w(concat))逻辑上这个 Aggregator 做了三件事对邻居嵌入取均值也可以用 attention 替代、将自身嵌入与邻居聚合结果拼接、过一层全连接得到更新后的实体表示。neighbor_num是超参数设小了会丢失图结构信息设大了会让每个 batch 的张量非常重。原项目里默认是 4实际调整时可以根据图谱平均出度决定。更细一点说KGCN 和 GCN 的区别还体现在归一化策略上。GCN 用的是拉普拉斯归一化系数1/sqrt(deg(u)*deg(v))而 KGCN 采用的是在采样后的邻居集合内做 softmax 归一化。layer.py 里如果能看到tf.nn.softmax的调用就说明作者做了 attention 式聚合这比纯均值聚合在准确率上通常高出一截。3.2 model.py 里完整链路从用户嵌入到评分预测model.py 是模型装配的核心文件它把用户、物品、知识图谱三路信息组合成最终的预测评分。整体拓扑是用户 ID 查用户嵌入表物品 ID 查物品嵌入表再通过知识图谱结构反复做领域聚合最后把用户嵌入和物品嵌入做内积得到预测分。# model.py 模型组装伪代码体现用户/物品/图信息的组合方式 class KGCN(tf.keras.Model): def __init__(self, user_num, entity_num, relation_num, embed_dim, neighbor_num): super().__init__() self.user_emb tf.keras.layers.Embedding(user_num, embed_dim) self.entity_emb tf.keras.layers.Embedding(entity_num, embed_dim) # 关系嵌入同样建表用于注意力计算 self.relation_emb tf.keras.layers.Embedding(relation_num, embed_dim) self.aggregators [Aggregator(embed_dim, embed_dim) for _ in range(2)] def call(self, user_ids, item_ids, neighbor_entities, neighbor_relations): user_vec self.user_emb(user_ids) # [batch, dim] item_vec self.entity_emb(item_ids) # [batch, dim] # 第一跳邻居聚合 neigh_vec self.aggregators[0](item_vec, neighbor_entities) # 第二跳可以再次聚合加深图信息传播 final_vec self.aggregators[1](neigh_vec, ...) # 用户嵌入与融合后物品嵌入做内积 logits tf.reduce_sum(user_vec * final_vec, axis-1) return tf.nn.sigmoid(logits)这里的entity_num在代码里往往是kg_loader返回的最大实体索引加一而不是原始电影数量。因为知识图谱里每个类型、每个演员都是一个独立实体所以嵌入表的大小会比电影数量大很多。用户在跑项目时如果碰到IndexError或者嵌入表维度不匹配优先检查这里的实体数量是不是和 kg.txt 的最大 ID 对齐了。另外要注意call方法里输入参数的组装顺序。项目里 main.py 准备 batch 时通常返回五元组用户列表、正样本物品列表、负样本物品列表、邻居实体集、邻居关系集顺序必须和model.call的定义保持一致这一块是最容易漏改导致 shape 报错的地方。3.3 tool.py 和 gpu_memory_growth.py被忽略但决定能否跑动的工具tool.py 在项目里承担了很多杂活通常包括计算互信息或相似度矩阵、构建稀疏矩阵对输入数据做预处理、保存和加载嵌入向量等。其中一段常用的逻辑是把用户评分矩阵转换成稠密或稀疏的交互矩阵供后续评估时计算召回率。# tool.py 中常见的交互矩阵构建逻辑 import numpy as np from scipy.sparse import coo_matrix def build_interaction_matrix(user_list, item_list, rating_list, user_num, item_num): # 构建 user-item 稀疏矩阵 rows np.array(user_list) cols np.array(item_list) data np.array(rating_list, dtypenp.float32) mat coo_matrix((data, (rows, cols)), shape(user_num, item_num)) return mat.tocsr()gpu_memory_growth.py 则是 GPU 显存使用策略的配置文件。TensorFlow 2.x 默认会抢占所有可用显存如果你是在共享服务器上跑很容易把机器搞崩。这个文件的作用是通过set_memory_growth把显存分配改成按需增长。import tensorflow as tf gpus tf.config.experimental.list_physical_devices(GPU) if gpus: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True)如果项目根目录里存在这个文件建议在 main.py 的第一行就 import 它而不是等网络建立之后再设置。因为显存策略一旦在图创建之后再修改TensorFlow 会直接忽略。常见做法是import gpu_memory_growth # 放在所有 import 的最前面先声明显存策略这里还有一个版本相关的细节TF 2.3.8 里tf.config.experimental.set_memory_growth可用但如果你把环境升级到 TF 2.8这个写法依然兼容不过会有一条 deprecation 警告。用 2.3.8 的朋友可以无视。4. main.py 训练全流程从原始数据到 KCGN.h5 的实时链路4.1 main.py 的执行顺序拆解main.py 是整个项目的入口从零开始训练到产出权重文件。它的执行顺序通常是这样的加载 ratings 数据和 kg 三元组、建立索引映射和邻居字典、生成正负样本 batch、实例化 KGCN 模型、开始训练循环并周期性保存模型。# main.py 训练主流程结构示意 import tensorflow as tf from data_loader import load_ratings, load_kg from model import KGCN from tool import generate_sampled_batch # 1. 加载数据 ratings load_ratings(data/ratings.dat) kg load_kg(data/kg.txt) # 2. 构建模型 model KGCN(user_numlen(user2idx), entity_numkg.entity_num, relation_numkg.relation_num, embed_dim32, neighbor_num4) # 3. 自定义损失函数和优化器 optimizer tf.keras.optimizers.Adam(learning_rate0.01) loss_fn tf.keras.losses.BinaryCrossentropy() # 4. 训练循环 for epoch in range(10): for batch in generate_sampled_batch(ratings, kg, batch_size256): user_ids, pos_items, neg_items, neighbors, rels batch with tf.GradientTape() as tape: pos_score model(user_ids, pos_items, neighbors, rels) neg_score model(user_ids, neg_items, neighbors, rels) loss loss_fn(tf.concat([pos_score, neg_score], axis0), tf.concat([tf.ones_like(pos_score), tf.zeros_like(neg_score)], axis0)) grads tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables)) # 5. 保存权重文件 model.save_weights(KCGN.h5)这里采用的是负采样训练方式对每个用户的正样本电影随机采样同等数量的未交互电影作为负样本然后让模型学会打分正样本高于负样本。评分标签是二值化的1 表示用户看过0 表示没看过这样的设定完全贴合隐式反馈推荐场景。save_weights(KCGN.h5)保存的只是模型权重不是完整模型结构。这意味着 test 脚本里必须重新定义一模一样的模型结构然后再 load 权重否则会出现 mismatched shape 的报错。这一点很多第一次跑项目的同学会忽略直接 load 一个 .h5 文件却忘了重建模型类。4.2 核心超参数的作用与调整建议下表整理了该项目几个关键超参数以及它们对训练过程的实际影响。实际调参时不要一次性全改最好一次只动一个变量。超参数典型值作用调整倾向embed_dim32用户和实体嵌入维度维度越大表达力越强但小数据集上容易过拟合neighbor_num4邻居采样个数图谱稠密时可加大到 8稀疏时减小learning_rate0.01优化器学习率loss 震荡时按 0.5 倍衰减batch_size256每批样本数显存不足时减半l2_reg1e-5正则化系数出现过拟合时适当增加epoch10完整训练轮数看验证集 loss 决定是否提前停止邻域采样数量这个参数在知识图谱场景下特别值得关注。电影知识图谱的类型关系平均出度可能很高几百部电影同属科幻而演员关系出度相对适中。如果 neighbor_num 设成 4等于每层只随机看 4 个邻居信息利用率会比较低但设成 32 后每个 batch 需要一次性加载 32 个邻居嵌入显存占用线性增长。我一般习惯先把 neighbor_num 固定在 8跑通后再根据显存余量往上加。运行 main.py 时如果 TensorFlow 日志里反复出现loss数值波动较大优先检查负采样逻辑是否正确——负样本如果采样到了用户真正看过的电影但没有被过滤掉模型会收到冲突的学习信号。常见做法是在生成负样本时加一个剔除集合判断确保负样本不在用户的历史交互列表中。训练结束后项目根目录会多出一个 KCGN.h5 文件。这个文件的大小和嵌入维度相关如果 embed_dim32 且实体数量较多权重文件通常在几十到上百兆之间。需要注意这个文件是整个预测链路的基石test.py 依赖它做预测丢了就只能重新训练。5. 避坑与排查从环境配置到结果异常的五个实操记录5.1 TensorFlow 2.3.8 与 Python 3.8 的组合兼容性问题现象按照 README 用 python3.8 tensorflow2.3.8 安装后import tensorflow 报错Could not load dynamic library cudart64_110.dll。原因TensorFlow 2.3.8 默认是 CUDA 11.0 的编译版本但机器上可能装了 CUDA 11.2 或更高版本导致运行时找不到对应的动态链接库。解决安装 2.3.8 之前先确认 CUDA 版本。如果用的是 CUDA 11.2 及以上可以直接升级 tensorflow 到 2.4或者用 conda 建一个 CUDA 11.0 的环境。我自己的做法是直接用 conda 创建环境时指定cudatoolkit11.0和cudnn8.0再装 tensorflow 2.3.8基本零报错。5.2 main.py 二次执行时 KCGN.h5 被直接覆盖现象第一次训练正常第二次改了几个参数重新跑 main.py发现之前训练好的 KCGN.h5 被静默覆盖了没有提示也没有备份。原因main.py 里用的model.save_weights(KCGN.h5)对同名文件是直接覆盖写入的TensorFlow 不会自动生成备份文件。解决每次训练前把旧的权重文件改名或者用带时间戳的保存路径。另一个做法是在 main.py 开头加一个检测逻辑如果文件存在就自动重命名为KCGN_old.h5。这样万一新模型效果不如旧模型还有后悔药可以吃。5.3 实体索引对不齐导致嵌入层维度 mismatch现象运行 test.py 加载 KCGN.h5 时提示Shapes (None, 32) and (None, 128) are incompatible。原因test.py 里构建模型时传入的 entity_num 和训练时不一致。常见情况是训练时 kg.txt 构建出来的实体数量为 5000但 test 阶段重新加载数据时漏加载了部分三元组实体数量变成 4500嵌入表维度就直接对不上了。解决模型构建参数必须从同一个数据源计算。建议把 entity_num 的计算封装在 data_loader 的加载函数里训练和测试脚本统一 import 这个值而不是各自从数据文件里数。5.4 显存爆炸和进程被 kill现象训练刚开始升级到第二个 epoch程序直接报ResourceExhaustedError退出有时连报错都没有只是进程被 OOM kill。原因前面提到的 gpu_memory_growth.py 没有在 main.py 里被 import导致 TensorFlow 在初始化时一次性分配了全部显存。如果同时有别的进程占用显存就直接挤爆了。解决把import gpu_memory_growth放到 main.py 文件的第一行确保在 TensorFlow 初始化之前设置好按需增长。另外可以通过 batch_size 减半或 neighbor_num 减半来缩减每批数据的张量体积。5.5 预测结果全部趋近同一个分数没有区分度现象test.py 跑完打印出的预测分值基本都在 0.47 ~ 0.53 之间几乎齐平完全无法区分用户的真实偏好。原因大概率是训练 epoch 太少没有收敛或者正则化系数太大把模型系数压得过于平均。当然也可能是负采样没有剔除用户看过电影导致正负样本语义混淆。解决先看训练日志里 loss 是否还在持续下降。如果 loss 已收敛但预测值仍集中说明模型容量不足或特征表达过于平滑可以尝试增大 embed_dim、减少 l2 正则系数。这个现象是图神经网络冷启动项目里最常见的诊断点通常先从训练曲线找原因而不是急着改模型结构。6. test.py 预测链路与模型验证方法6.1 预测脚本背后从权重文件到 Top-K 推荐结果test.py 的经验做法是先重建模型结构再 load_weights 把 KCGN.h5 加载进去然后用 predict 生成用户对所有未看过的电影打分最后按分数排序输出 Top-N 推荐。这里需要注意的一点是加载权重前必须保证模型结构完全一致一个 embed_dim 不一致都会导致加载失败。# test.py 预测流程结构示意 import tensorflow as tf from model import KGCN # 重建模型参数必须与训练时完全一致 model KGCN(user_numuser_num, entity_numentity_num, relation_numrelation_num, embed_dim32, neighbor_num4) # 加载训练好的权重 model.load_weights(KCGN.h5) # 对某个用户所有未打分电影预测分数 user_idx 0 unseen_movies [m for m in all_movie_ids if m not in user_watched[user_idx]] candidates tf.constant([user_idx] * len(unseen_movies), dtypetf.int32) items tf.constant(unseen_movies, dtypetf.int32) # 这里用全零填充邻居信息预测阶段不需要采样真实邻居 scores model(candidates, items, zero_neighbors, zero_relations).numpy().flatten() # 按分数排序取 Top 10 top_indices scores.argsort()[-10:][::-1] rec_movies [unseen_movies[i] for i in top_indices]预测阶段有个细节邻居信息在验证时可以不再采样而是用零填充因为权重已经固化图卷积的传播结果基本稳定。这个做法在很多图模型推理阶段通用能大幅减少推理时的计算量。不过如果你追求精度还是建议按训练同样的流程采样真实邻居。6.2 让评估更有说服力的两种验证方式作为毕设项目建议不要只停留在预测分数能跑出来这一步至少要加上离线评估指标。常见做法是随机抽 20% 的 rating 作为测试集训练完在测试集上计算 recall10、precision10 或 NDCG。更细一点还可以按用户维度分组画出不同用户活跃度下的推荐效果差异这部分数据可以直接写进毕设论文的实验章节。还有一个小习惯值得养成每次训练完把 KCGN.h5 连同训练超参数、验证指标一起记录到一个 json 文件里。因为图模型调参成本很高如果没有记录改了几轮参数之后回头找最优权重会非常痛苦。从那以后我每次训练结束都强制走一遍这个记录流程再加载模型做对比时效率高很多。这是拆完这个项目之后我认为最值得保留的工作习惯。如果你也准备拿这套工程作为毕设基础建议先把数据准备和模型构建两份代码通读一遍再动手训练剩下的交给时间——调参的过程本身就是最好的论文素材。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询