☰
知识图谱与图神经网络:电影推荐系统实战指南
2026/10/9 13:14:06 网站建设 项目流程

简介:这是一份基于Python的知识图谱与图神经网络(KGCN)的电影推荐系统完整毕设项目,主要面向计算机相关专业的本科毕业设计、课程大作业以及有一定Python基础的推荐系统学习者,项目获评审分98分,源码经本地编译与严格调试,下载后按readme配置即可运行。代码模块覆盖数据预处理(data_process.py)、知识图谱构建(create_kg.py)、KGCN模型定义与训练(model.py、train.py)、工具函数(utility.py)及Web可视化页面(web/app.py),可以帮助读者理解知识图谱如何为电影推荐提供辅助语义信息,并掌握GNN在推荐场景中的落地思路。资源包共31个文件,包括21个Python脚本、5个dat评分数据文件、2个txt说明、2个readme及1个md文档,其中users.dat、ratings.dat、movies.dat分别保存用户、评分与电影条目,整体压缩包仅14.84MB,数据齐全、目录清晰。自发布以来已有86人学习下载,整体代码风格清晰,注释与readme较齐全,既可直接运行学习,也适合在毕业设计中引入深度学习和知识图谱方法,或作为端到端完整代码参考快速复现与二次开发。

1. 从标题说起:为什么电影推荐要用知识图谱和图神经网络

基于Python的知识图谱和图神经网络电影推荐系统,拆开看是三条技术线:知识图谱负责把电影、演员、导演、类型组织成一张有语义的网,图神经网络负责沿着这张网做信息聚合,推荐模块负责把用户和电影映射进同一个向量空间打分。三者串起来,解决的是传统协同过滤的三个老毛病——冷启动、数据稀疏、推荐结果说不清为什么。

你拿到一套带源码和完整数据的毕设项目时,最该关心的不是模型有多新,而是它能不能一次跑通、指标能不能复现、论文里有没有故事可讲。知识图谱加图神经网络这个组合恰好三点全占:图谱构建是数据处理的工作量,GNN模型是核心创新点,推荐评估是实验章节,每一步都有明确交付物。本文从数据整理开始,把图谱构建、模型实现、训练评估、避坑排查和答辩加分五段路完整走一遍,适合正在做电影推荐方向毕设的本科生,也适合想把这套方案迁到自己推荐系统里的后端工程师。

2. 图谱先行:把电影数据整理成三元组

2.1 先想清楚实体和关系的边界

知识图谱在推荐项目里的作用,是给电影补充“上下文”。协同过滤只知道“谁给什么电影打了分”,知识图谱则告诉你这部电影和哪些演员、导演、类型标签有关联。这些关联就是推荐解释的来源。

拿到源码后第一步,先看数据文件里有什么。常见的公开电影数据集会提供四张表:movies.csv(电影ID、标题、类型列表)、users.csv(用户ID、性别、年龄、职业)、ratings.csv(用户ID、电影ID、评分、时间戳),以及一个可选的credits.csv(电影ID、导演列表、主演列表)。这套项目的数据目录大概率长这样:

data/ ├── movies.csv ├── users.csv ├── ratings.csv └── credits.csv

实体类型需要你手动定义。电影是核心实体,演员、导演、类型标签是三类辅助实体,用户在图谱之外单独建模。关系我建议精简单一,不要设计得太复杂——毕设项目里方向不一致的关系反而会让GNN学不到有效信息。我一般只保留四条关系:belongs_to(电影→类型)、directed_by(电影→导演)、acted_by(电影→演员),反向关系在构建邻接矩阵时由模型自动处理。

2.2 三元组生成:实体统一编号是关键一步

这条路上最容易翻车的,是不同表里同一个实体用了不同的ID。credits.csv里的演员名是字符串,movies.csv里的电影ID是整数,类型标签又是字符串。直接把它们拼成三元组然后扔进模型,模型会认为“Tom Hanks”和“tomal_hanks”是两个实体。所以第一步永远是做统一实体映射。

import pandas as pd def build_kg_triples(movies_df, credits_df): """从电影表和演职员表生成 (head, relation, tail) 三元组列表""" triples = [] # 电影 -> 类型 for _, row in movies_df.iterrows(): movie_id = row["movie_id"] for genre in str(row["genres"]).split("|"): triples.append((movie_id, "belongs_to", genre)) # 电影 -> 导演 for _, row in credits_df.iterrows(): movie_id = row["movie_id"] for director in str(row["directors"]).split(","): director = director.strip() if director: triples.append((movie_id, "directed_by", director)) # 电影 -> 主演 for _, row in credits_df.iterrows(): movie_id = row["movie_id"] for actor in str(row["actors"]).split(","): actor = actor.strip() if actor: triples.append((movie_id, "acted_by", actor)) return triples triples = build_kg_triples(movies_df, credits_df) print(f"生成三元组 {len(triples)} 条")

这段代码的逻辑很简单:遍历每一部电影,把类型字段按竖线拆分、演职员字段按逗号拆分,逐条生成三元组。注意导演和演员的处理逻辑有个细微差别——directors和actors字段里可能有空值,strip()之后还要再判断一次,否则空字符串会变成挂到电影上的幽灵实体。

拿到原始三元组后,做全局实体编号:

def map_entities(triples): """把三元组里的所有实体映射成连续整数ID""" unique_entities = set() for head, _, tail in triples: unique_entities.add(head) unique_entities.add(tail) entity2id = {ent: idx for idx, ent in enumerate(unique_entities)} return entity2id entity2id = map_entities(triples) num_entities = len(entity2id) print(f"实体总数 {num_entities}") # 三元组转索引 triples_idx = [ (entity2id[h], relation2id[r], entity2id[t]) for h, r, t in triples ]

relation2id需要你在前面手动定义,比如{"belongs_to": 0, "directed_by": 1, "acted_by": 2}。实体ID映射完成之后,源数据里的字符串ID就全部变成了模型能消费的整数,这一步做扎实了,后面所有代码都不会因为ID对不上而报错。

2.3 训练集和测试集的划分:先定位任务,再切数据

推荐系统的数据集划分有个经典陷阱:如果你把ratings.csv随机打乱后按比例切分,那么同一部电影的评分会同时出现在训练集和测试集里,模型在训练时已经见过这部电影的“未来”。这在高分毕设里是会被答辩老师直接点名的硬伤。正确做法是按时间戳排序后切分,模拟线上系统的真实使用场景。

ratings = pd.read_csv("data/ratings.csv") ratings = ratings.sort_values("timestamp").reset_index(drop=True) split_idx = int(len(ratings) * 0.8) train_ratings = ratings.iloc[:split_idx] test_ratings = ratings.iloc[split_idx:] # 清洗:去掉测试集中没在训练集出现过的用户 valid_users = set(train_ratings["user_id"].unique()) test_ratings = test_ratings[test_ratings["user_id"].isin(valid_users)]

这里有个参数值得注意:0.8是经验值,代表80%的交互记录用于训练、20%用于测试。如果数据量很大,可以考虑调到0.9,但测试集太小会导致评估指标的波动剧烈,AUC可能在0.75到0.82之间乱跳。清洗这个动作不能省——图神经网络无法为从未见过的用户生成嵌入向量,强行预测会报索引越界,或者更隐蔽地,你会看到一个虚高的评估结果,因为模型在这些冷用户上只预测了少数容易的样本。

3. 模型实现:用图注意力网络做推荐

3.1 为什么是GNN而不是矩阵分解

矩阵分解(MF)把用户和电影分别映射成向量,用内积预测评分。它的问题是每个电影的向量完全由它的评分记录决定,没有评分的冷门电影只能拿一个随机初始化的向量。知识图谱的存在改变了这个局面:图神经网络让电影的向量不仅来自评分,还来自它身上的演员、导演、类型标签,这些信息在评分稀疏时依然稳定。

图神经网络在推荐里的标准做法是“实体嵌入 + 邻域聚合”。电影的嵌入经过一层图卷积后,会带上它邻居实体的信息——比如和某位导演合作过的其他电影,或者同类型标签下的其他电影。两层图卷积之后,电影嵌入里就包含了它的“二阶邻居”信息:同一类型下、由同一导演执导过的其他电影导演。这就是GNN在推荐系统里比矩阵分解强的根本原因:矩阵分解只能捕获直接交互,GNN能捕获多跳关系。

常见的图卷积层有GCN、GraphSAGE、GAT三种。GCN实现最简单,聚合时邻居权重相等;GAT引入注意力机制,让模型自己学“哪类邻居对推荐更重要”;GraphSAGE支持邻居采样,适合超大规模图谱。毕设项目我推荐用GAT,理由有二:一是注意力权重可以直接拿来做可视化,论文里能放图;二是实现量只比GCN多十几行。

3.2 图注意力聚合层的PyTorch实现

用PyTorch实现一个单层图注意力网络,核心是计算注意力系数并加权聚合邻居信息。省略复杂的多头机制,先展示单头版本,代码逻辑更清晰:

import torch import torch.nn as nn import torch.nn.functional as F class GraphAttentionLayer(nn.Module): """单头图注意力层""" def __init__(self, in_dim, out_dim, dropout=0.3): super().__init__() self.W = nn.Linear(in_dim, out_dim, bias=False) self.a = nn.Parameter(torch.randn(2 * out_dim)) # 注意力向量 self.dropout = dropout def forward(self, x, adj): # x: [N, in_dim] 实体嵌入矩阵 # adj: [N, N] 稀疏邻接矩阵 h = self.W(x) # [N, out_dim] N = h.size(0) # 拼接所有实体对的表示,计算注意力分数 h_i = h.unsqueeze(1).expand(N, N, -1) # [N, N, out_dim] h_j = h.unsqueeze(0).expand(N, N, -1) # [N, N, out_dim] attn = F.leaky_relu(self.a @ torch.cat([h_i, h_j], dim=-1).transpose(1, 2)) attn = attn.squeeze(1) # [N, N] # 只保留有边相连的实体对,并做softmax归一化 attn = attn.masked_fill(adj == 0, float("-inf")) attn = F.softmax(attn, dim=-1) attn = F.dropout(attn, p=self.dropout, training=self.training) # 加权聚合邻居特征 out = torch.bmm(attn.unsqueeze(0), h.unsqueeze(0)).squeeze(0) return F.elu(out)

这个实现把图的邻接矩阵当成掩码矩阵用:attn矩阵里每个位置计算的是实体i和实体j的注意力分数,但只有adj中为1的位置才保留分数,其他位置被-inf遮蔽,经过softmax之后权重归零。逻辑上等价于“只对邻居做加权平均”。

需要注意adj必须是归一化的邻接矩阵,否则聚合后的向量尺度会随节点度变化而剧烈波动。常见的做法是用对称归一化D^-1/2 * A * D^-1/2,但这在代码里直接做会很占内存,毕设阶段我建议用稀疏矩阵存储。self.a的初始化用torch.randn就够了,注意别用torch.zeros,否则一开始所有实体对的注意力权重相同,梯度会因为对称性而无法有效传播。

3.3 完整推荐模型:图谱特征加用户偏好

图注意力层负责给知识图谱里的实体生成嵌入,但推荐系统还需要用户侧的表示。我把用户也做成一个Embedding层,最终评分是用户向量和电影向量的内积。这样整个模型是端到端训练的,不需要分阶段做图谱嵌入和推荐预测。

class KGRecModel(nn.Module): """知识图谱 + GAT 电影推荐模型""" def __init__(self, num_users, num_entities, num_relations, embed_dim=64, dropout=0.3): super().__init__() # 实体和用户共享同一个嵌入维度 self.user_emb = nn.Embedding(num_users, embed_dim) self.entity_emb = nn.Embedding(num_entities, embed_dim) self.gnn_layer = GraphAttentionLayer(embed_dim, embed_dim, dropout) self.dropout = nn.Dropout(dropout) def forward(self, user_ids, movie_ids, adj): # 所有实体的初始嵌入 x = self.entity_emb.weight # 经过图注意力层,实体表示带上邻居信息 x = self.gnn_layer(x, adj) x = self.dropout(x) # 用户向量和电影向量打分 user_vec = self.user_emb(user_ids) movie_vec = x[movie_ids] scores = (user_vec * movie_vec).sum(dim=-1) return scores

embed_dim这个参数是模型里最关键的超参数之一。设成64在电影推荐场景下是性价比很高的选择:太小(比如16)表达不了用户偏好的复杂性,太大(比如256)在几万实体规模下训练会明显变慢,收敛也更慢。num_relations这个参数在单层GAT实现里没直接用到,但保留它有两个用途——一是和论文里的模型定义对齐,二是如果你后面换成R-GCN这类关系感知模型时不需要改模型签名。

打分用内积是均值回归推荐的标准做法,和MF一致。如果要做评分预测而不是点击率预测,把打分头换成MLP层或者直接加一个sigmoid输出,都是常见改动。但我建议毕设阶段保持内积,因为它的可解释性最好,答辩时可以画出用户向量和电影向量的余弦相似度分布,展示模型学到了有意义的空间结构。

4. 训练与评估:把模型跑起来并用指标说话

4.1 训练采样策略和损失函数的选择

推荐训练和图像分类训练有一个核心差异:图像分类每个样本的标签是客观存在的,推荐的“标签”却有选择性偏差——你只观察到了用户点过的电影,没点过的可能是不喜欢,也可能是没看到。所以训练阶段要为每个正样本生成对应的负样本。常见策略是随机采样若干未交互过的电影作为负样本,代码可以这样写:

import random def sample_negative(user_id, movie_id, num_items, interacted): """生成负样本:采一个用户没交互过的电影ID""" while True: neg = random.randint(0, num_items - 1) if neg != movie_id and neg not in interacted[user_id]: return neg

负样本的数量是个关键参数。每个正样本配1个负样本(1:1采样)在大多数据集上效果够用;配到1:4或1:8在有些场景能小幅提升AUC,但训练时间也线性增长。对于毕设项目,1:3算是不错的折中,既能稳定训练,又不至于让采样循环拖慢整个迭代过程。

损失函数我推荐BCEWithLogitsLoss而不是MSE。原因是评分预测适合MSE,但推荐任务大多数时候被建模为“用户是否会喜欢”的二分类问题,BCE的梯度在分数远离0的时候会饱和,这让训练过程更稳定。BPR损失(贝叶斯个性化排序)也可以,它直接优化“正样本得分大于负样本”的相对顺序,但BPR对负采样质量的敏感度更高,负样本太容易时loss下降虚假地快。新手用BCE最稳。

import torch import torch.nn.functional as F def train_one_epoch(model, optimizer, train_data, adj, num_items, interacted, batch_size=1024): model.train() total_loss = 0.0 n_batch = 0 for (users, pos_items) in train_data: # 为每个正样本采样3个负样本 neg_items = [ sample_negative(u, i, num_items, interacted) for u, i in zip(users, pos_items) ] users_t = torch.tensor(users, dtype=torch.long) pos_t = torch.tensor(pos_items, dtype=torch.long) neg_t = torch.tensor(neg_items, dtype=torch.long) optimizer.zero_grad() pos_scores = model(users_t, pos_t, adj) neg_scores = model(users_t, neg_t, adj) # 正样本标签为1,负样本标签为0 labels = torch.cat([torch.ones_like(pos_scores), torch.zeros_like(neg_scores)]) all_scores = torch.cat([pos_scores, neg_scores]) loss = F.binary_cross_entropy_with_logits(all_scores, labels) loss.backward() optimizer.step() total_loss += loss.item() n_batch += 1 return total_loss / n_batch

这里batch_size=1024是经验值。显存不紧张可以设成2048甚至4096,批大小增大时可以考虑同步调低学习率,因为大batch的梯度方向更稳定,用小学习率更新更平滑。我习惯的配比是:批大小1024配学习率0.001,批大小4096配0.0005。

4.2 评估指标:AUC、Recall@K和NDCG@K怎么选

电影推荐的评估指标不能只看一个。AUC关注的是排序质量——随机抽一个正样本和负样本,模型把正样本排在负样本前面的概率。它在全体样本上计算,直观反映模型区分正负样本的能力。Recall@K关注的是头部命中——用户真正喜欢的电影有没有出现在推荐列表前K个位置。AUC高不代表点头命中一定高,所以两个指标都要看。

from sklearn.metrics import roc_auc_score def evaluate(model, test_df, adj, K=10): """返回AUC和Recall@K""" model.eval() users = test_df["user_id"].values items = test_df["movie_id"].values labels = test_df["label"].values with torch.no_grad(): users_t = torch.tensor(users, dtype=torch.long) items_t = torch.tensor(items, dtype=torch.long) scores = model(users_t, items_t, adj).sigmoid().numpy() auc = roc_auc_score(labels, scores) # Recall@K:按用户分组,只看每用户前K个预测 test_df = test_df.copy() test_df["score"] = scores test_df = test_df.sort_values(["user_id", "score"], ascending=[True, False]) hits = 0 total = 0 for user_id, group in test_df.groupby("user_id"): group = group.head(K) hits += group["label"].sum() total += group["label"].sum() recallK = hits / max(total, 1) return auc, recallK

AUC的计算用sklearn现成方法即可。Recall@K的坑在于“分母”的定义:很多毕设误用全局正样本总数做分母,导致结果虚低。正确做法是分子分母都在测试集正样本范围内统计——分母是每用户前K个预测里的正样本+排在后边的正样本总和,可以理解为该用户全部正样本数。每组用户的total是相同的正样本数,只有把group["label"].sum()放在排序前统计才准确,这版代码里的total在循环内对每个用户取了它自己的正样本数,计算的是测试集整体命中率。

4.3 做个基线对比表,让实验结果有说服力

毕设答辩时,老师最常问的一个问题是:“你怎么证明你的模型比基线好?”没有对比实验的结果基本等于自说自话。项目里至少要跑两个基线:一个是去掉知识图谱的经典MF模型,一个是去掉图神经网络的嵌入拼接模型。对比表格的格式要固定,指标统一、随机种子统一。

模型AUCRecall@10训练时间/epoch
矩阵分解(MF)0.7210.1528秒
嵌入拼接(无GNN)0.7640.18312秒
知识图谱 + GAT(本模型)0.7980.21735秒

这一组的差距大概就是毕设模型该有的合理水平。AUC高出3到5个百分点是正常的,因为知识图谱确实补足了稀疏交互下的信息缺失。如果GAT模型只比MF高不到1个百分点,先不要怀疑GAT不行,大概率是图谱构建阶段出了问题,重点检查第二章的实体映射和三元组去重。

注意训练时间这个信号也很重要:GAT多出来的时间开销换来的是2到5个点的AUC提升,值不值在论文里写清楚,这本身就是亮点。答辩时你可以说:“在离线评估中,知识图谱GAT模型在AUC上提升3.4个百分点,虽然训练时间增长约3倍,但推荐精度收益在可控范围内。”这段话比任何形容词都有说服力。

5. 五个血泪教训:图谱推荐项目的避坑指南

5.1 三元组质量比模型结构更影响效果

现象:GAT模型训练后AUC只有0.61,比MF基线还低一截。

原因排查后发现,credits.csv里同一批人存在大量重复记录,比如同一导演在不同电影里被写成“James Cameron”和“James Cameron”(多了一个空格),模型把它们当成两个完全不相关的实体。这类实体对齐问题在原始数据里几乎是必然存在的:拼写变体、空格、大小写、缩写形式都会让三元组数量虚增30%以上。

解决:在构建三元组的代码里统一做字符串规范化处理。所有实体名在映射ID之前,统一执行strip()、压缩连续空格、统一小写。更稳妥的做法是写一段清洗检查脚本,扫描图谱中完全相同的实体名但不同ID的实体对,看看是不是误建了重复实体。

def normalize_entity(name): """清洗实体名称:去空格、统一小写""" return " ".join(str(name).strip().lower().split()) # 在build_kg_triples里对所有实体字段调用normalize_entity

5.2 全图邻接矩阵的内存爆炸

现象:实体数量超过5万时,构建稠密邻接矩阵[50000, 50000]直接内存溢出,或者训练时显卡OOM。

原因:图注意力层代码里为了计算方便用了全连接形式的注意力矩阵,这在图规模小时没问题,但实体数量一旦过万就会爆炸。[N, N]的浮点矩阵在N=5万时占用约20GB内存(4字节浮点位)。

解决:改用稀疏邻接矩阵存储,并在注意力计算时做掩码掩码即可——PyTorch的sparse_coo_tensor配PyG的GCNConv是标准做法。手写GAT层时,可以用adj.to_sparse()将稠密矩阵转稀疏,或者更彻底:用PyG的边列表格式(edge_index)+GATConv,让库内部处理稀疏聚合。毕设项目里我强烈建议换成GATConv,可以省掉一整类内存错误。

5.3 数据泄漏:先聚合图谱再划分数据会高估效果

现象:测试集AUC高达0.93,远超正常水平。答辩老师一看评估流程就指出问题——知识图谱的聚合使用了整个数据集里的credits.csv信息,而这些信息包含了测试集电影的导演、演员。训练阶段模型做图卷积时,测试电影的邻居信息已经通过共享的演员、类型节点传播到了目标电影身上,等于作弊式地给测试样本泄露了特征。

原因:知识图谱构建阶段和实验划分阶段缺少隔离。正确流程是:先按时间戳划分训练集和测试集(基于ratings.csv),再只根据训练集里电影的演员、导演信息构建图谱。测试集电影在图谱聚合时仍然存在,但它的邻居信息只来自训练阶段累积的交互记录和属性信息,这样评估才有意义。

解决:在构建三元组时将测试集电影ID过滤掉,或确保图谱聚合前剔除了测试集独有的属性边。严格的做法是把实体属性和交互序列都按时间切分,但毕设阶段你只需要保证“创建图谱时看不到测试电影本身的属性边”即可。

5.4 损失不收敛:学习率与初始化的玄学

现象:训练到了10个epoch,损失仍然在0.72上下震荡不下降,或者第一个epoch结束损失就变成NaN。

原因:图注意力层的self.a参数如果初始化数值过大,在softmax之后会形成近似one-hot的分布,梯度通过它传播时数值幅度会爆炸。另一个常见原因是学习率设置太高,lr=0.01对图神经网络来说偏激进,权重更新一步就跳出正常参数区间。

解决:把self.a的初始化改成nn.init.xavier_uniform_,这个初始化方法让注意力向量的尺度与输入维度匹配,避免初始分数极端化。学习率从0.001起步,如果5个epoch后loss仍然不降,再递减到0.0001,而不是一开始就用大学习率加速。“先小步稳走,再分析损失曲线调整”是我处理GNN训练的基础习惯。

5.5 结果不可复现:随机种子和评估顺序

现象:同一套源码,昨天跑AUC是0.79,今天跑变成0.77,每次结果都不一样。

原因:模型初始化、负采样、数据加载这三个环节都用了随机过程,但没有固定随机种子。负采样阶段每一次运行采到的负样本都不同,模型学习和评估反复在小波动里游走,导致你没法公平比较不同模型结构的效果。

解决:在训练脚本开头固定所有随机源,包括Python内置的random、NumPy、PyTorch以及CUDA。同时把负采样函数作为生成器传入数据加载器,以保证每个epoch的同一位置负采样结果在代码层面上确定。

import random import numpy as np import torch def set_seed(seed=2024): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True set_seed(2024)

torch.backends.cudnn.deterministic = True这句很重要,但有些人会忽略它。它让CUDA卷积操作使用确定性算法,代价是运行略微变慢。在毕设阶段,这个代价完全值得——你的实验表里每一项都需要可复现,否则无法证明你的结论不是随机波动。

6. 答辩加分项:可解释推荐与消融实验

模型跑通、指标达标只是及格线。要让答辩有亮点,可以把“推荐结果可解释”这件事做出来。原理很简单:GAT的注意力权重本身就告诉了你模型在做决策时更关注什么。把权重可视化,或者从知识图谱里找出“为什么推荐这部”的路径,就是一个非常直观的加分点。

def explain_for_user(model, user_id, movie_id, adj, entity2name): """输出模型推荐一部电影时可解释的推理路径""" model.eval() with torch.no_grad(): x = model.entity_emb.weight x = model.gnn_layer(x, adj) user_vec = model.user_emb.weight[user_id] movie_vec = x[movie_id] score = (user_vec * movie_vec).sum().item() # 找目标电影在知识图谱里的邻居,按注意力权重排序 neighbors = adj[movie_id].nonzero().squeeze(1) w = torch.zeros(len(neighbors)) for i, nid in enumerate(neighbors): w[i] = (user_vec * x[nid]).sum().item() top_idx = w.argsort(descending=True)[:5] reasons = [ f"通过实体 {entity2name[neighbors[i].item()]} 关联,相似度 {w[i]:.4f}" for i in top_idx ] return score, reasons

这段代码背后的思想是“注意力即解释”:用户向量和目标电影的邻居实体向量做点积,得分高的邻居实体就是模型眼中促成推荐的那部分“原因”。实际操作时,你可以选一部冷门电影做展示,说“这部电影评分数据很少,但模型通过它和导演X、类型Y的关联做了推荐”,这一句话就把知识图谱的价值讲透了。

紧接着做消融实验。把GAT层换成一个简单的线性变换(去掉邻居聚合),对比两个版本的效果差异。如果差异明显,说明知识图谱的贡献是实质性的;如果差异不大,说明你的特征工程可能更关键。不管结果如何,消融实验本身就是论文里的一个完整章节,不要跳过。

最后分享一个我的习惯:项目收尾时,我会把数据集里最典型的10条推荐案例截图保存,包括用户ID、推荐电影、知识图谱路径、模型打分。答辩材料里放一张这样的图,面试官和评审老师都会觉得这个项目是完整的。这个习惯帮我应付过不少次临时提问,希望也能帮到你。

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

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

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

立即咨询