☰
知识图谱与图神经网络驱动的电影推荐系统:从图谱构建到GCN实战
2026/9/28 3:55:12 网站建设 项目流程

简介:基于Python的知识图谱与图神经网络电影推荐系统源码,专为毕业设计、期末大作业及课程设计打造,适合具备基础Python知识并希望动手实践图神经网络项目的学习者。系统利用知识图谱构建电影实体与关系网络,再通过图神经网络提取用户和电影的深层特征,从而大幅提升电影推荐的准确度。代码注释详尽、模块划分清晰,涵盖主程序入口、知识图谱加载、图卷积模型、训练评估、数据转换等环节,并配有单元测试与Windows平台运行脚本,方便阅读和二次开发。资源共37个文件,包含21个Python脚本、5个dat数据文件、5个zbak备份文件、2个txt及2个readme/md说明文档,压缩包仅14.84MB,轻量易部署。系统功能完善、界面美观,经过严格调试后可直接使用,个人手打项目已获导师认可。目前已有65人学习下载,配套说明文档与备份文件,是快速掌握知识图谱与图神经网络落地应用的实用参考。

1. 毕设用知识图谱+图神经网络做电影推荐:为什么它比纯协同过滤更值得做

很多做毕业设计的同学一开始都会选电影推荐系统,因为 MovieLens 数据集现成、协同过滤代码满网都是。但答辩时一问“你的系统相比 SVD 改进在哪”,大多数人就卡住了。这个基于 Python 的标题给了一条差异化路线:把电影、导演、演员、类型、用户行为一起建模成知识图谱,再用图神经网络(GNN)在图上做表示学习,最后输出推荐结果。它既有图谱构建的工程含量,又有模型设计的算法含量,正好覆盖毕设里“工作量”和“创新点”两个得分点。适合有 Python 基础、愿意碰 Neo4j 和 PyTorch 的读者。下面按实际方案拆解。

2. 电影知识图谱怎么建:从原始数据到 Neo4j 实体关系入库

2.1 实体与关系设计:先想清楚图谱里有哪些节点和边

很多同学上来就写代码,结果图谱建得一团乱。知识图谱的根基是本体设计,也就是先定义清楚“有哪些实体、哪些关系、属性约束是什么”,而不是拿到数据就往 Neo4j 里灌。做电影推荐,我一般把实体分成两类:一类是电影的属性实体,一类是用户行为实体。属性实体包括电影、人物、类型三类;电影节点存 title、release_year、rating 这些基础属性,人物节点用一个 Person 标签统一表示,用 role 字段区分演员和导演,类型节点单独抽出来,因为同一部电影属于多个类型,抽成节点比存成逗号分隔的字符串更适合后面的图查询和 GNN 聚合。

用户行为实体就是 User,它通过 RATED 边连接到电影,边上带 score 和时间戳,这是后面图神经网络训练的核心交互信号。有人图省事,把导演、演员全塞进 Movie 节点的属性里,结果 GNN 聚合时完全学不到“演员关系”这种语义,这是模型效果上不去的最常见原因。知识图谱的价值就在于把关系变成显式的边,这一步省掉,后面调参再猛也救不回来。先定义本体再建图的思路,在工业场景下的知识图谱设计里同样是第一步;企业里建图谱前会有数据治理团队先产出本体文档,毕设不需要那么重的流程,但至少要手工画一张实体关系草图。我自己踩过的坑是:跳过设计环节直接灌数据,最后图谱里同一个意思的关系出现三种写法,比如 acted_in、出演、ACTOR_OF,GNN 建模时光是清洗关系类型就花了两天。

实体/关系标签/类型主要属性
电影Moviemovie_id, title, release_year
人物Personperson_id, name, role
类型Genregenre_id, name
用户Useruser_id
出演关系ACTED_IN无
导演关系DIRECTED无
属于关系BELONGS_TO无
评分关系RATEDscore, timestamp

2.2 用 Pandas 把原始数据拆成节点表和边表

数据从哪来?常见做法是拿 MovieLens 100k 或 1M 数据集做行为数据,再拿 TMDB 或电影网站爬虫数据补电影属性。爬虫不是必选项,能稳定拿到数据就行,建议优先用官方 API 或现成数据集。关键步骤是先把原始表拆成“节点文件”和“边文件”两种格式,因为 Neo4j 批量导入工具 LOAD CSV 对字段命名有约定,写错了后缀整批导入就报错。

import pandas as pd movies = pd.read_csv("movies.csv") # movieId, title, genres ratings = pd.read_csv("ratings.csv") # userId, movieId, rating, timestamp # movie 节点表:从标题里拆出上映年份 movie_nodes = movies[["movieId", "title"]].rename( columns={"movieId": "movie_id:ID", "title": "title"} ) movie_nodes["release_year"] = movie_nodes["title"].str.extract(r"\((\d{4})\)") movie_nodes.to_csv("movie_nodes.csv", index=False) # user 节点表:userId 去重即可 user_nodes = ratings[["userId"]].drop_duplicates().rename( columns={"userId": "user_id:ID"} ) user_nodes.to_csv("user_nodes.csv", index=False) # RATED 边表:两端 ID 用 :START_ID 和 :END_ID 标记 rated_edges = ratings.rename( columns={"userId": "user_id:START_ID", "movieId": "movie_id:END_ID", "rating": "score"} ) rated_edges[["user_id:START_ID", "movie_id:END_ID", "score", "timestamp"]].to_csv( "rated_edges.csv", index=False )

这里的关键在字段名后缀::ID 表示主键,:START_ID 和 :END_ID 表示边的两端,这是 LOAD CSV 能识别的最小约定。很多同学不写后缀,导入时一直报 Couldn't load the external resource,其实是字段名的问题。另外,MovieLens 的 genres 字段是管道符分隔的,如果直接存成字符串,在 Neo4j 里做类型聚合很别扭;正确做法是在导出阶段就把它拆开,单独生成一个 BELONGS_TO 的边文件,每行是 movie_id 加一个 genre_id,这样图谱的语义才干净。

2.3 批量写入 Neo4j:py2neo 事务分批、LOAD CSV 与索引

毕设数据量一般在十万级以下,py2neo 直接写完全够用,而且可以在同一个 Python 工程里和后面的 GNN 流程串起来。写入顺序有讲究:先写节点,再写边,边两端的节点必须已存在,否则会创建出孤立节点或者直接标红报错。

from py2neo import Graph, Node, Relationship, Subgraph graph = Graph("http://localhost:7474", auth=("neo4j", "your_password")) def write_nodes(df, label, key_field): """分批写节点,每 500 条提交一次事务,避免事务过大拖垮内存""" batch = [] for row in df.itertuples(): props = {k: v for k, v in row._asdict().items() if k not in ("Index", key_field)} batch.append(Node(label, **{key_field: getattr(row, key_field)}, **props)) if len(batch) >= 500: graph.create(Subgraph(batch)) batch = [] if batch: graph.create(Subgraph(batch)) write_nodes(user_nodes, "User", "user_id") write_nodes(movie_nodes, "Movie", "movie_id")

参数说明:batch 控制在 500 条,是因为 Neo4j 单事务太大容易把堆内存打满,太小又频繁提交导致导入变慢。毕设机器 8G 内存时 500 是稳妥值,到百万级边数再换 neo4j-admin import 离线导入。写边之前务必建索引,否则每次 match 坐标都是全库扫描,十万条边能跑半小时。

CREATE INDEX FOR (m:Movie) ON (m.movie_id); CREATE INDEX FOR (u:User) ON (u.user_id);

索引建好后,边的写入我通常不用 py2neo 的逐条 match,而是把边表导出 CSV,再用 LOAD CSV 配合 USING PERIODIC COMMIT 导入,两边都不卡。Cypher 脚本大致是这样的:

USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///rated_edges.csv" AS row MATCH (u:User {user_id: toInteger(row.user_id)}) MATCH (m:Movie {movie_id: toInteger(row.movie_id)}) CREATE (u)-[:RATED {score: toFloat(row.score)}]->(m)

脚本里 toInteger 和 toFloat 是必修课。CSV 导入后所有字段默认是字符串,不显式转换,score 排序、GNN 取特征都会出问题。这里的 USING PERIODIC COMMIT 500 和 py2neo 的 batch=500 是一个目的:每处理 500 行提交一次事务,防止长事务把 NeO4j 内存撑爆。

提示:Neo4j Desktop 和 Community Server 对 CSV 路径的处理有差异。Desktop 把 csv 放在 import 目录下,路径写 file:/// 开头;Server 环境要确认 dbms.directories.import 配置指向哪个目录,路径写错了会直接报找不到文件。

3. 图神经网络模型怎么选:GCN、GAT 还是 LightGCN,三个必调参数

3.1 为什么推荐要用 GNN:把协同过滤放进图里看

传统协同过滤的核心假设是“相似用户喜欢相似物品”,在图上体现为 user-item 二部图的消息传播。矩阵分解做的事情,本质上是 user-item 邻接矩阵的低秩近似,它只建模了一阶交互——用户和电影的直接连接。GNN 不一样:每一层卷积让节点的表示向邻居聚合,两层之后,一个用户就能感知到“和我看过同一批电影的人还喜欢什么”,这是高阶协同信号。知识图谱在其中的作用,是让 movie-actor-director-genre 这些属性关系也变成图里的边,聚合时不仅看到行为相似性,还能把“同一导演”“同一类型”这些语义相似性带进表示,这正是这个方案相比纯协同过滤的核心卖点。

选型的答案取决于图谱里有什么。如果只有 user-item 行为边,LightGCN 是首选,它去掉了 GCN 里对推荐无益的非线性变换,训练更快;如果图谱里知识图谱是主体、属性边很丰富,就用 GCN 或 GAT,让不同类型的边贡献各自的语义。毕设标题既然同时点了知识图谱和图神经网络,我建议用带属性的 GCN 做底子,既覆盖了 GNN 的技术点,实现难度也适中,答辩时讲“属性图上的消息传递”比讲“纯行为图”更有故事性。

GAT 比 GCN 多了注意力机制,理论上能给不同邻居分配不同权重,但电影推荐场景的图很稀疏,GAT 的收益有限,训练却慢不少。我的建议是:先用 GCN 跑通全流程作为论文基线,学有余力再把第一层换成 GAT 做对比实验,这正好是毕业设计里“消融实验”的素材,比空谈模型创新实在得多。

3.2 最小可跑的 GCN 训练代码:基于 DGL 的实现

图神经网络的工程实现,我通常选 DGL,它对异构图和多关系边的支持比 PyG 更顺手,文档也跟得上。下面这段代码是把用户行为图读进来、用两层 GCN 学嵌入的最小实现:

import dgl import torch import torch.nn as nn import torch.nn.functional as F class GCNEncoder(nn.Module): def __init__(self, in_feats, hidden_dim, out_dim, dropout=0.3): super().__init__() self.conv1 = dgl.nn.GraphConv(in_feats, hidden_dim) self.conv2 = dgl.nn.GraphConv(hidden_dim, out_dim) self.dropout = nn.Dropout(dropout) def forward(self, g, features): h = F.relu(self.conv1(g, features)) h = self.dropout(h) h = self.conv2(g, h) return h # user_id 和 movie_id 拼进同一套节点编号再建图 # 注意 movie_id 要加 offset,避免和 user_id 撞号 user_id = torch.tensor(rated_edges["user_id"].values) movie_id = torch.tensor(rated_edges["movie_id"].values + offset) g = dgl.graph((user_id, movie_id)) g = dgl.add_self_loop(g) # 初始特征:随机初始化够用,想更强可以拼电影的类型向量 in_feats = 64 features = torch.randn(g.num_nodes(), in_feats) model = GCNEncoder(in_feats, hidden_dim=128, out_dim=64) emb = model(g, features) # 所有节点的嵌入

逻辑说明:dgl.graph 接收边起点数组和边终点数组,用户和电影落在同一套 id 空间里,所以这里用 offset 把电影 id 整体平移,避免和用户 id 重叠。add_self_loop 是 GCN 的标配,不加的话第一层卷积会忽略节点自身特征,表达力明显下降。初始特征用随机值在“邻居聚合”机制下仍然能学出节点在图上的位置信息,这个可以理解为空特征版本;想加强效果,可以把电影的类型 one-hot 向量或预训练词向量拼进来,毕设里属于加分项而不是必选项。

数据量到百万边时,全图前向会越来越慢,这时应该换成邻居采样的小批量训练,用 dgl.dataloading.NeighborSampler 每次只聚合两层邻居。这是从毕设走向真实项目必须知道的扩展点,答辩时主动提一句,老师印象分会不一样。

3.3 三个必调参数:层数、嵌入维度、dropout

层数是最容易翻车的。网上很多教程照抄 GCN 论文写三层四层,但推荐场景里两层是分水岭。三层以上,用户的表示会聚合到太大范围的邻居,所有用户输出趋同,推荐多样性直接崩掉,这个现象在 GNN 里叫过平滑。我的经验是图谱边数在十万级时,两层 GCN 的 Recall@20 普遍比三层高 3 到 5 个百分点。判断方法很简单:把模型输出的嵌入做一次 PCA 可视化,如果不同用户的点挤成一团,就是层数多了。

嵌入维度从 64 起步,128 是上限。毕设数据量小,256 维不仅训练慢,验证集 loss 还会在某个 epoch 后反弹。过拟合的判断标准是:训练 loss 贴到 0 而验证 loss 开始上升,这时候优先降维度而不是加正则。dropout 建议设在 0.2 到 0.4 之间,用户行为图通常很稀疏,dropout 太小时模型很容易把噪声当信号记住。

参数建议值取值范围调参信号
层数21~3嵌入 PCA 可视化挤成一团就减层
嵌入维度6432~128验证 loss 反弹就降维
dropout0.30.2~0.4训练/验证 loss 差距过大就加大
学习率1e-35e-4~1e-2loss 震荡就降学习率

学习率单独说一下。Adam 配 1e-3 是大多数 GNN 工程默认的起手式,如果 loss 曲线像锯齿一样上下跳,先降到 5e-4,不要去动网络结构。调参时每次只改一个变量,把结果记录成表格,这个习惯在写毕设的“实验对比”章节时非常值钱,比临时抱佛脚补数据有用得多。

4. 推荐链路整合:从 GNN 嵌入到 Top-N 推荐与离线评估

4.1 召回 vs 排序:GNN 的输出到底拿来做哪一步

推荐系统里,召回负责从百万物品里快速筛出几百个候选,排序负责给候选精确打分。毕设通常把两步合并:用 GNN 学出的用户嵌入和电影嵌入,直接对全部电影算内积相似度,取 Top-N 返回。这样做的优势是流程短、答辩时好讲;劣势是当电影数量超过五万时全量内积有毫秒级延迟,但毕设规模完全可接受。另一种更完整的做法是把 GNN 嵌入当成特征,喂给一层 LR 或 MLP 做排序,这更接近工业界的推荐架构,但需要额外构造排序样本,工作量翻倍。我建议毕设选第一种:直接内积排序,把“全量内积”和“Top-N 截断”讲清楚,已经能体现对推荐系统的理解深度。

整个项目的源码目录,我建议按 data、kg、model、server 四个目录组织:data 放原始数据和处理脚本,kg 放图谱构建和查询,model 放 GNN 模型和训练,server 放 Web 接口。评审老师打开目录能一眼看到四条线,这个结构本身就说明项目是完整的工程,而不是单文件脚本堆出来的。

4.2 BPR 损失训练与候选集打分

模型训练用的损失函数是贝叶斯个性化排序(BPR),它的核心思路是:用户交互过的电影得分应当高于随机抽出的负样本。正样本直接取训练集里的 RATED 边,负样本从用户没看过的电影里抽样,抽样策略直接决定训练质量,具体坑在第五章展开。

import random import torch def bpr_loss(user_emb, pos_emb, neg_emb): """BPR 损失:正样本得分与负样本得分的差值经 sigmoid 后取负对数""" pos_score = (user_emb * pos_emb).sum(dim=-1) neg_score = (user_emb * neg_emb).sum(dim=-1) return -torch.log(torch.sigmoid(pos_score - neg_score)).mean() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(50): random.shuffle(train_triples) # train_triples: (user_id, pos_id, neg_id) for u, pos, neg in train_triples: emb = model(g, features) # 每步全图前向,小图可接受 loss = bpr_loss(emb[u], emb[pos], emb[neg]) optimizer.zero_grad() loss.backward() optimizer.step()

逻辑说明:BPR 损失不直接预测评分,而是学习用户对不同电影的偏好排序。训练时每个三元组包含用户、正样本电影、负样本电影,模型要让正样本得分比负样本高。这里每步都对全图做一次前向,图小的时候没关系;图大到百万边,就要用 4.2 提到的 NeighborSampler 改成小批量训练。打分阶段就更直接了:用训练好的模型对全图算一次嵌入,用户向量和电影向量做内积,排序取前 N 个。

4.3 离线评估:Recall@K 与 NDCG 怎么算才严谨

答辩时最怕被问“你怎么证明你的系统比基线好”。口头解释没用,必须跑离线评估拿数据说话。评估协议要按时间切分:把每个用户的评分记录按时间排序,前 80% 做训练,后 20% 做测试,这样可以避免随机切分带来的数据泄漏。

import math def recall_at_k(scores, test_items, k=20): """单个用户的 Recall@K:测试集命中了多少被模型捞回的电影""" topk = scores.argsort(descending=True)[:k].tolist() hits = len(set(topk) & test_items) return hits / len(test_items) if test_items else 0 def ndcg_at_k(scores, test_items, k=20): """单个用户的 NDCG@K:位置越靠前权重越大""" topk = scores.argsort(descending=True)[:k].tolist() dcg = 0.0 for i, item in enumerate(topk): if item in test_items: dcg += 1.0 / math.log2(i + 2) idcg = sum(1.0 / math.log2(i + 2) for i in range(min(k, len(test_items)))) return dcg / idcg if idcg > 0 else 0

逻辑说明:Recall@K 看的是“测试集里用户真正看的电影被捞回来多少”,NDCG 额外惩罚排在后部的命中项。注意 DCG 的折扣因子要写 log2(i+2),网上很多简版代码用 i+1 代替,数量级差不大但答辩老师较真时会被挑刺。同一个评估脚本跑矩阵分解或 ItemCF 作为基线,把 Recall@20 随 K 的变化画成折线图,一张图比十页文字说明都管用。画图用 Matplotlib 就够了,这也是 Python 数据分析与可视化基本功里最实在的一块。

5. 毕设避坑实录:知识图谱与 GNN 推荐系统最常见的 5 个翻车点

这一章写的都是做这类项目时真实踩过的坑,按出现频率从高到低排。每条按现象、原因、解决三段写,方便对号入座。如果你时间只够看一章,看这章。

5.1 坑一:py2neo 写入慢到怀疑人生,Neo4j 内存还爆了

现象:用 py2neo 导入 MovieLens 100k 的评分数据,跑了十分钟还没写完,Neo4j 内存占用一路飙升,最后直接 OutOfMemory。

原因:graph.create 在循环里一条一条调用,每次调用就是独立事务,产生上万个事务开销;同时 Movie 和 User 节点没建索引,边导入时每次坐标匹配都是全库扫描。

解决:节点写入用 Subgraph 打包,每 300 到 500 条提交一次;边导入改用 LOAD CSV 加 USING PERIODIC COMMIT;导入前先执行 CREATE INDEX。这三步改完,十万条边的导入从十分钟级别降到几十秒。

5.2 坑二:冷启动用户嵌入全零,推荐列表变成随机数

现象:把测试集里一个训练时没见过的用户输入模型,输出的嵌入全是 0,推荐结果和乱猜没有区别。

原因:图神经网络是转导学习,只能给训练时见过的节点学出表示。新节点没有邻居,消息传递聚合不到任何信息,随机初始化的特征也学不到梯度。

解决:两个方案。一是把用户属性(年龄、性别、职业)也建成节点放进图谱,新用户至少能从属性边聚合到信息;二是做内容兜底——新用户没有行为边时,直接推荐知识图谱里与热门类型关联的电影。毕设里我推荐第二种,实现成本低,答辩时也好解释“冷启动本来就该走规则兜底”。

5.3 坑三:随机切分训练测试导致指标虚高,被评委当场拆穿

现象:随机按 80/20 切分后,Recall@20 跑出 0.6 的“好成绩”,答辩时老师追问“你确定没有数据泄漏”,现场答不上来。

原因:随机切分时,用户未来的行为记录可能混进了训练集。模型在训练阶段已经“见过”测试交互,相当于先看了答案再考试,指标自然虚高。

解决:改成时间切分。按 timestamp 排序后,每个用户取前 80% 做训练,后 20% 做测试。同时要过滤掉只在测试集里出现的电影,否则这些电影没有嵌入可查,评估时要么报 key error,要么静默漏算,指标反而被低估。

5.4 坑四:负采样太随意,模型退化成“热门电影排序器”

现象:训练 loss 降得飞快,但 Recall@20 纹丝不动,推荐结果清一色是热门大片的 id。

原因:均匀随机负采样时,大部分负样本是用户没听过的冷门电影,模型轻松区分之后不学细腻偏好,转而把所有热门电影排到前面,这是最常见的模型退化。

解决:改成按流行度加权的负采样,以电影被评分次数的 0.75 次方作为抽样权重,这样负样本里既有难分的热门电影,也有冷门电影。PyTorch 里用 torch.utils.data.WeightedRandomSampler 一行实现。这个改动通常能把 Recall@20 拉高 2 到 4 个点,性价比极高。

5.5 坑五:DGL 和 PyTorch 版本不匹配,import 直接崩

现象:pip install dgl 装完,import dgl 报找不到 _C 扩展;或者代码在 GPU 机器上跑,模型前向时报 CUDA error。

原因:DGL 的预编译包和 PyTorch、CUDA 版本严格绑定,默认 pip 源装的通常是 CPU 版,和代码里 .to("cuda") 的调用不匹配。

解决:先确认 PyTorch 版本,再按对应版本安装 DGL。CPU 机器就全程不调 cuda,DGL 的 CPU 版和 GPU 版 API 完全一致,只是速度差异。答辩演示前一定要在演示机上完整跑一遍推理脚本,版本问题在会场现场装包是最狼狈的。

6. 最后的加分项:用 Flask 把推荐结果和图谱理由一起展示

模型训练完,推荐列表还停在 Jupyter 里,答辩时给评委看黑底白字的命令行输出,效果会打折扣。花半天做一个简单的 Flask 页面,输入用户 id,返回推荐电影,并附一句推荐理由——这个理由就是知识图谱最值钱的部分:“因为你喜欢《盗梦空间》,而《星际穿越》与它同导演、同类型”。

from flask import Flask, request, jsonify import torch app = Flask(__name__) @app.route("/recommend", methods=["POST"]) def recommend(): body = request.get_json() user_id = int(body["user_id"]) if user_id not in known_users: return jsonify({"items": hot_movie_fallback()}) # 冷启动兜底规则 emb = model(g, features) # 训练好的模型,推理时只前向一次 user_emb = emb[user_id] scores = torch.matmul(user_emb, emb[movie_ids_with_offset].T) topk = scores.argsort(descending=True)[:20].tolist() return jsonify({ "user_id": user_id, "items": [{"movie_id": mid, "reason": explain(mid, user_id)} for mid in topk] }) def explain(movie_id, user_id): """用 Cypher 查共同导演或共同类型,拼成一句话推荐理由""" query = """ MATCH (u:User {user_id: $uid})-[:RATED]->(m:Movie) WHERE (m)<-[:DIRECTED]-(:Person)-[:DIRECTED]->(:Movie {movie_id: $mid}) OR (m)-[:BELONGS_TO]->(:Genre)<-[:BELONGS_TO]-(:Movie {movie_id: $mid}) RETURN m.title AS liked_title LIMIT 1 """ result = graph.run(query, uid=user_id, mid=movie_id).data() if result: return f"因为你喜欢《{result[0]['liked_title']}》,它与推荐电影有共同导演或同属一个类型" return "因为和你看过的电影属于同一类型"

这个页面的核心不在 UI,而在 explain() 里的 Cypher 查询——它是把“知识图谱”这个概念可视化给评委看的唯一方式。推荐理由哪怕只有一行字,也比冷冰冰的评分列表有说服力。接口写好之后,用 Postman 或 requests 发一个 POST 请求就能验证,不需要把前端做得多花哨。

另一个值得做的验证是参数敏感性实验:固定其他变量,只改层数或嵌入维度,把 Recall@K 的变化画成折线。这是毕设论文“实验对比”章节最扎实的素材,也能反过来验证我在第三章说的“两层 GCN 是分水岭”。我做这个项目时最深的教训是:先花两天把图谱本体设计清楚再写任何代码。当初我贪快,导演和演员全塞在 Movie 节点属性里,结果图查询倒是顺手,GNN 却完全学不到语义关系,最后推倒重建,比一开始好好设计多花了一倍时间。图谱结构决定了模型能学到什么,这个先后顺序不能省。希望帮到你。

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

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

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

立即咨询