资讯详情

资讯详情

AGNN图神经网络推荐算法:属性编码原理与毕设复现避坑指南

简介基于属性和图神经网络的推荐算法AGNN本科毕业设计项目包面向推荐系统方向的学生与研究者聚焦于融合用户和商品属性及图结构信息以提升推荐精准度与个性化体验。压缩包内共19个文件主要包含Python模型代码、数据处理脚本、CSV与NumPy数据、TXT说明及README文档整体大小约4.19MB轻量便于本地复现。源码中包含AGNN模型的核心实现、训练与测试脚本并配有多套数据集的预处理逻辑可帮助读者理解图卷积、图池化等操作在真实推荐数据上的应用。目前已有62人学习下载适合需要完成毕业设计、课程项目或入门GNN推荐算法的读者参考。项目资料同样包含作者对数据预处理、模型调参与结果评估的实践记录能够帮助初学者降低搭建门槛快速复现并改进自己的推荐模型。1. AGNN 推荐算法毕设先看清它在解决什么再决定要不要复现图神经网络和推荐算法的组合在近两年的本科毕设选题里几乎成了标配而 AGNNAttribute-aware Graph Neural Network是其中很典型的一类它不像 DeepFM 那样把属性当排序特征而是把用户属性、物品属性编码成节点特征再通过图卷积的消息传递去扩散和聚合最后用节点嵌入的内积给用户打推荐分。我第一次在本地跑通这份资源时第一感受是“竟然不难”第二感受是“坑全在数据准备阶段”——如果你直接把它默认的 MovieLens 路径换掉大概率会在负采样、归一化和邻接矩阵维度这三个点上轮流翻车。这篇笔记会从原理讲到复现路径再讲到五条血泪踩坑记录适合正在做毕设、以及想快速上手 GNN 推荐但你不想从头造轮子的从业者。2. 先搞清楚 AGNN 的原理属性编码和图构建的四步关键选择2.1 协同过滤的边界为什么要把属性搬进图里传统协同过滤只依赖用户和物品的交互矩阵两个用户如果没有共同交互过的物品模型就找不到任何可比对的信号新物品上线时更是没有历史记录只能靠内容特征兜底。AGNN 的思路很直接把“用户是谁、物品是什么”这两类信息从推荐排序的旁路特征变成图里的节点属性参与每一层的聚合和传播。这样做的直接收益是冷启动用户和稀疏交互用户也能通过自己的属性特征在图中找到和自己相似的一群邻居从而得到相对合理的初始嵌入而不至于全靠随机初始化。很多人会误以为属性只是把年龄、品类 One-Hot 之后拼进向量就行但 AGNN 里属性承担的职责更重。它决定了图里一个节点在聚合邻居信息时自身携带的信号是什么。如果属性编码做不好后面图卷积再深也救不回来。所以复现这份资源时我建议你最优先看两个文件一个是数据预处理脚本看属性编码怎么写另一个是模型 forward 里节点特征怎么进图卷积。这两处只要有一处思路不对实验对比里的“属性增强”优势就出不来。2.2 交互图构建从原始点击记录到稀疏矩阵AGNN 的第一步是把用户和物品的交互记录构建成一张二部图。常见做法是把所有用户和所有物品都作为图节点交互行为作为边。边的权重可以取 1也可以取点击次数、评分分值、评分归一化后的强度。我一般会用 scipy 的稀疏矩阵来承载这张图因为后面 GNN 层的矩阵乘法和归一化都建立在稀疏格式上能省下大量显存。import numpy as np import pandas as pd from scipy import sparse df pd.read_csv(ratings.csv) # 至少包含 user_id, item_id 两列 users df[user_id].astype(category).cat.codes.values items df[item_id].astype(category).cat.codes.values n_users users.max() 1 n_items items.max() 1 data np.ones(len(df), dtypenp.float32) # 隐式反馈全部记成 1 # coo 矩阵适合构建随后转成 csr 便于行切片和矩阵乘法 adj sparse.coo_matrix((data, (users, items)), shape(n_users, n_items)) adj adj.tocsr()这里有两个参数很关键。第一个是n_users和n_items的取值不能直接拿原始 ID 的最大值去算因为原始 ID 可能是不连续的字符串或者跳过编号必须先做astype(category)再取cat.codes否则邻接矩阵的行列数会大于实际节点数训练时索引到处越界。第二个是data的取值如果原始数据里有重复交互比如同一用户多次点击同一物品直接把data全设成 1 会丢失次数信息。我一般会把行、列、值做一次 groupby 聚合把重复交互的次数作为边权重。2.3 属性编码三种做法该选哪一种属性怎么塞进图是 AGNN 设计的核心分水岭。资源里的代码一般会提供一种实现但你自己改数据时很可能需要换方案。常见做法有三种适用场景差别很大。做法实现方式适用场景风险方案一One-Hot 拼接用户年龄、性别、职业等离散属性做 One-Hot和 ID 嵌入拼在一起属性数量少、取值空间小的小型数据集维度爆炸、特征稀疏方案二属性做边权重把连续属性归一化后乘到邻接矩阵的边上有明确连续度量如价格差、距离、评分差离散属性无法直接参与方案三属性向量 Gate属性特征过一层线性变换再对邻居聚合结果做门控加权属性混合离散和连续想给论文增加改进点实现复杂度高、调参难如果只是复现毕设、验证 AGNN 的有效性方案一最快落地代码改动量最小。但有一类坑很隐蔽多值属性比如电影类型同时是“动作”和“冒险”不能直接把两个值只留一个否则属性覆盖不全会引入噪声。通常在预处理里会把多值属性拆成多列稀疏矩阵或者用 MultiLabelBinarizer 处理再做矩阵拼接。方案三更适合想在答辩时讲出“我改进了属性融合方式”的读者。把属性向量拆成两部分一部分直接加在节点特征上另一部分作为 sigmoid 门控乘到邻居聚合结果上。这种做法相当于让模型自己决定“当前节点的属性信息该保留多少、邻居信息该吸收多少”在属性噪声大的数据集上往往比方案一稳。2.4 归一化为什么 GNN 里拉普拉斯归一化是硬要求图卷积本质是邻居特征加权求和如果不做归一化一个高热度节点的向量会把方向带歪。AGNN 沿用的正是 GCN 的对称归一化规则把邻接矩阵左乘右乘一个度矩阵的逆平方根。作用是把每个节点的聚合范围拉回同一量级热度高的节点不会主导最终嵌入方向。from scipy import sparse import numpy as np # D_ii 每个节点的度加 1e-6 防止度数为 0 的节点除零 degree np.array(adj.sum(axis1)).flatten() 1e-6 d_inv_sqrt sparse.diags(1.0 / np.sqrt(degree)) norm_adj d_inv_sqrt adj d_inv_sqrt注意这里的adj是用户-物品二部邻接矩阵不是方阵。如果直接把norm_adj拿去和图卷积相乘矩阵维度对不上。我一般会构建一个方阵把用户侧和物品侧放在同一个矩阵里左上块是零矩阵右下块也是零矩阵右上角放用户-物品交互左下角放它的转置。这样图卷积里每个用户节点能聚合物品邻居物品节点也能反向聚合用户邻居。这个方阵的构造不复杂但新手最容易在这里报维度错误后面避坑章会再提。3. 复现模型主干消息传递、BPR 损失与采样器三件套3.1 消息传递层一段手写 GCN 层加注释AGNN 的“神经网络”本体通常就是一到两层 GCN 卷积。你得先有个概念图卷积层做的事本质上是对节点特征做一次“邻居加权求和再变换”。如果只写一层节点只能看到直接邻居写两层节点就能看到二跳邻居。毕设场景里两层是最常见的三层以上容易出现过度平滑——所有节点的嵌入都趋于一致推荐结果反而退化成热门榜。import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.fc nn.Linear(in_dim, out_dim) self.bn nn.BatchNorm1d(out_dim) def forward(self, x, norm_adj): x self.fc(x) # x: [num_nodes, in_dim] - [num_nodes, out_dim] out torch.sparse.mm(norm_adj, x) # 邻居加权聚合 return F.relu(self.bn(out))这里的torch.sparse.mm接收的是稀疏邻接矩阵只对非零元素做计算。第 2 章构建的方阵norm_adj就是它的输入。我之所以在每层后面加BatchNorm1d是因为 GCN 层数叠加后特征分布漂移很快尤其当属性特征来自不同量纲的数据集时没有 BN 的模型经常出现 loss 降不下去的现象。如果你的显存比较紧张可以把BatchNorm1d去掉但学习率要相应调小。3.2 训练模式全图嵌入加批次负采样AGNN 的图规模在毕设数据集上通常不会太大全图嵌入一次算完、再按批次采样负样本是常见的做法。意思是不必每次梯度更新都重新构建全图而是把用户、物品的所有节点特征一次送入 GCN得到全量嵌入然后在每次迭代时只抽取一部分用户和物品对来计算 loss。这样既保留了图结构的全局信息又避免了全量交互对计算 loss 的巨大开销。optimizer torch.optim.Adam(model.parameters(), lr0.005, weight_decay1e-4) for epoch in range(200): model.train() total_loss 0.0 for batch_users, pos_items, neg_items in train_loader: user_emb_all, item_emb_all model(user_feat, item_feat, norm_adj) user_emb user_emb_all[batch_users] pos_emb item_emb_all[pos_items] neg_emb item_emb_all[neg_items] pos_scores (user_emb * pos_emb).sum(dim-1) neg_scores (user_emb * neg_emb).sum(dim-1) loss bpr_loss(pos_scores, neg_scores) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() if epoch % 20 0: print(fepoch {epoch:3d} | loss {total_loss / len(train_loader):.4f})几个参数值得盯一下。负采样比例我一般固定在 1:2 到 1:4 之间太多负样本会让模型偏向“什么都推不出去”太少则区分度不够。学习率 0.005 是一个比较稳妥的起点如果你的 loss 前几个 epoch 就在剧烈震荡先降到 0.001。嵌入维度 64 到 128 之间对大多数学术数据集都够用不需要盲目加大。3.3 BPR 损失用相对排序代替点击率拟合BPR 损失的思路是用户对正样本的偏好得分应该明显高于负样本损失函数不在乎得分绝对值。它的公式是负的 log sigmoid(正样本得分 - 负样本得分)如果正样本得分比负样本高sigmoid 输出接近 1损失接近 0反之损失被拉大。相比 BCE 直接拟合二分类标签BPR 对隐式反馈的用户交互数据更友好因为点击和未点击之间并不是真实的概率关系只是相对偏好。def bpr_loss(pos_scores, neg_scores): # 向量化计算输入形状都是 [batch_size] diff pos_scores - neg_scores loss -torch.log(torch.sigmoid(diff) 1e-12).mean() return loss这里1e-12是数值稳定项防止 sigmoid 输出恰好为 0 时 log 出现负无穷。实现很简单但有一个前提经常被忽略负样本绝对不能落在用户历史交互过的正样本集合里否则模型会被同一个物品的“既是正又是负”左右拉扯。数据预处理阶段就得把这个过滤做干净我在下一章会给具体代码。3.4 评估指标RecallK 与 NDCGK 的计算口径毕设答辩时RecallK 和 NDCGK 基本是必问指标。它们都依赖一个候选集对一个用户挑出 99 个随机负样本再混入该用户测试集中的正样本组成 100 个候选物品排序后取前 K 个和测试集正样本做交集。如果不设置候选集直接对所有物品全局排序一个是慢另一个是不同用户参与比较的物品数量不对等指标会被“简单用户”和“困难用户”的不均衡拉偏。from math import log2 def recall_at_k(ranking, test_items, k20): # ranking: 按分数从高到低排好的候选物品列表 return len(set(ranking[:k]) set(test_items)) / len(set(test_items)) def ndcg_at_k(ranking, test_items, k20): dcg 0.0 for i, item in enumerate(ranking[:k]): if item in set(test_items): dcg 1.0 / log2(i 2) # IDCG理想情况下正样本排在最前面时的 DCG idcg sum(1.0 / log2(i 2) for i in range(min(k, len(test_items)))) return dcg / idcg if idcg 0 else 0.0实现里最容易出错的地方是idcg的计算。它应该用“测试集正样本个数”来算理想排序下的最大 DCG而不是用候选集长度。如果test_items有 3 个K 取 20idcg只需要算前 3 个位置的折扣累加。新手上路时为了图省事直接除以 K算出来的 NDCG 会系统性偏低答辩时很容易被追问。4. 跑通一份数据要过五关从环境到出指标的完整路径4.1 环境清单常见版本搭配与安装顺序GNN 相关代码的环境问题比模型本身更容易劝退人。这份资源通常依赖 PyTorch 和 PyG 或者 DGL两者版本不匹配是重灾区。我习惯用 conda 建独立环境先装 CPU 版把代码跑通再装 CUDA 版提速度。一套比较稳的组合是Python 3.9、PyTorch 2.0.1、PyG 2.3.0、NumPy 1.24、SciPy 1.10CUDA 选 11.8。PyG 的安装不要直接用 pip 装最新版要按 PyTorch 版本和系统选择对应的 wheels 地址装完马上import torch_geometric验证一遍。如果你不想被 PyG 依赖绑架也可以只用 PyTorch 自带稀疏矩阵手写 GCN 层很多毕设资源本身就是用纯 PyTorch 实现的。这样环境复杂度会低不少排错也更直观。建议先看代码里有没有import torch_geometric或者import dgl再决定环境方案不要一上来就装大礼包。4.2 从原始数据到训练集隐式反馈阈值和负采样用 MovieLens 这类评分数据做隐式反馈推荐时第一步是把评分转成 0/1。常见阈值是 3.5 或 4.0低于阈值的交互被视作不感兴趣不进入图。阈值越高训练集越稀疏但交互质量越高阈值越低图越稠密噪声也越多。通常在 4.0 比较均衡。import pandas as pd df pd.read_csv(ml-1m/ratings.dat, sep::, headerNone, names[uid, mid, rating, ts], enginepython) df df[df[rating] 4] # 隐式反馈阈值 df df.groupby([uid, mid]).size().reset_index(namecount) # 生成训练三元组user, pos_item, neg_item def build_train_triplets(df, num_items, seed42): rng np.random.default_rng(seed) train_dict df.groupby(uid)[mid].apply(list).to_dict() rows, pos_cols, neg_cols [], [], [] for uid, pos_items in train_dict.items(): pos_set set(pos_items) for mid in pos_items: neg mid while neg in pos_set: neg int(rng.integers(0, num_items)) rows.append(uid) pos_cols.append(mid) neg_cols.append(neg) return rows, pos_cols, neg_cols这段代码里while neg in pos_set是防重复的关键但也要小心边界情况如果某个用户的交互物品数接近物品总数while 循环可能耗费大量时间。小型数据集基本无所谓但如果你自己准备的数据集非常稠密建议改用“能交互的物品集合的补集”直接采样避免死循环。参数上num_items必须和第 2 章映射后的 item code 总数一致否则采样的负样本索引会越界。4.3 训练流程的骨架与 checkpoint 保存训练循环在第 3 章已经给了主体这里补一个完整的骨架。核心点在于边训练边保存验证集上的最优指标而不是保存最后一个 epoch 的权重。GNN 推荐模型训练到后期loss 还在下降但 RecallK 经常已经过拟合到交互最频繁的用户群体上了。best_recall 0.0 for epoch in range(200): model.train() run_loss 0.0 for batch in train_loader: loss train_step(model, batch, optimizer) run_loss loss if epoch % 20 0: recall_val evaluate(model, val_data, k20) print(fepoch {epoch} | loss {run_loss:.4f} | val Recall20 {recall_val:.4f}) if recall_val best_recall: best_recall recall_val torch.save(model.state_dict(), best_agnn.pt)这里我用evaluate函数替代了内部逻辑实际写代码时把它替换成候选集评分和排序即可。一个值得记录的习惯每次评估完把loss和val_recall写入同一个 CSV 文件用pandas.read_csv瞟一眼就能知道模型是从哪个 epoch 开始过拟合的比靠记忆靠谱得多。4.4 评估候选集固定候选集才能让指标可比做评估时除了训练时的负样本还要单独生成一套测试候选集。每个用户取 99 个“没交互过”的负样本加上他的测试正样本组成 100 个候选物品。这样算出来的 Recall20 就表示模型在 100 个候选里能否把正样本排进前 20和别的方法对比才公平。for user in test_users: train_seen set(train_dict[user]) test_pos set(test_dict[user]) # 从没交互过的物品中随机抽 99 个 pool np.setdiff1d(all_items, list(train_seen | test_pos)) negs rng.choice(pool, sizemin(99, len(pool)), replaceFalse) # 正样本和负样本一起组成候选集 candidates np.concatenate([negs, list(test_pos)])这里有个隐蔽问题如果你把测试集正样本也从“可被抽作负样本的池子”里排除掉剩下的候选负样本和正样本就是在同一批“未被训练看过”的物品里比较这是常规做法。反过来如果负采样池没有排除测试正样本测试正样本就存在被抽成负样本的可能评估时模型对它的打分自然被压低指标会系统性偏低。这个排池逻辑不分数据集是所有 GNN 推荐模型评估统一口径答辩前一定要自查。4.5 判定收敛loss 下降不等于指标上升GNN 推荐训练中很容易出现 loss 一路下降、指标却原地踏步的情况。原因是 BPR 只约束正负样本的相对排序不约束打分绝对值模型完全可能把全部分数压到很小、但排序仍正确。所以判断模型是否训练到位必须同时看 val Recall20 和 val NDCG20。我一般观察两个信号一是 val Recall 不再上升并且保持稳定三五轮二是训练 loss 和验证指标之间出现反向走势前者继续下、后者开始掉说明过拟合已经开始了。如果指标从头到尾都不涨不要急着调模型结构先去检查两件事。第一候选集里是否混进了训练集正样本第二邻接矩阵和特征是否对得上节点顺序。这两处问题在代码里几乎不报错只会安安静静地让指标变差属于新手最难受的“黑匣子”问题。5. 复现 AGNN 的避坑记录五个让我白加班的问题5.1 显存爆炸小数据集也 OOM问题出在 to_dense现象数据集规模明明不大训练到第二个 epoch 就报 CUDA out of memory。原因代码里把稀疏邻接矩阵调用了.to_dense()。稀疏矩阵在用户一万、物品一万时非零元素只有几十万转成稠密矩阵后变成一亿个浮点数直接占满显存。这是 PyTorch 里最容易写出的隐性陷阱torch.sparse.mm本来可以不碰稠密格式但一行.to_dense()就让之前的稀疏优化全部白费。解决全程保持 sparse 格式矩阵乘法一律用torch.sparse.mm。如果 PyG 的稠密消息传递导致 OOM可以换成 mini-batch 邻居采样。我后来给自己定了一条规矩任何 GNN 代码里禁止出现.to_dense()除非显存冗余到百分之八十以上空闲。5.2 指标忽高忽低负采样没固定种子评估结果像抽奖现象同一个配置跑三遍val Recall20 的差距能到一个半百分点论文里的实验结果图根本没法画。原因训练负采样和评估候选集都用了默认随机源每次运行抽到的负样本不同排行难度每次都不一样。推荐算法对负样本极其敏感模型看到的负样本决定了它学会的边界评估候选集不同则让指标没有可比性。解决所有随机采样都要传入固定的seed最好在数据预处理阶段就把训练三元组、评估候选集一次性生成好以.npy文件保存后续训练和评估只从文件读取。这样每次实验的输入完全一致跑出来的指标差异才是模型权重和超参的真实反映。5.3 属性量纲未归一化loss 变成 NaN 或者剧烈抖动现象加上价格、收入、点击次数这类数值型属性后前几个 epoch 的 loss 直接变成 NaN。原因价格可能是几百上千的整数而评分归一化后在 0 到 1 之间两者经过线性层进 GCN梯度尺度相差几百倍稍不留神就梯度爆炸。属性特征不是越原始越好图神经网络的表达能力再强也扛不住量纲失衡。解决数值型属性做 MinMax 归一化到 [0,1]或者做 Z-Score 标准化到均值为 0、方差为 1。做完之后最好打印一列特征的描述性统计看一眼避免有极端离群点把归一化结果压成一堆接近 0 的值。属性是离散型的话则应当走 One-Hot 或者 Embedding而不是直接填数字当连续值用。5.4 加了属性反而更差多值 One-Hot 把图特征变成噪声现象AGNN 完整版比去掉属性的 GCN baseline 还低论文逻辑直接崩掉。原因多值属性被拆成了几百维 One-Hot在物品数量只有几百上千的数据集上每个维度平均非零比例极低属性向量大部分是 0。图卷积把这种稀疏向量在邻居间传播后不仅没有带来区分信息还稀释了 ID 嵌入本身的有效表达。解决先对多值属性做 Embedding 或者 PCA 降维把维度压到 16 到 32 维再进节点特征矩阵。属性之间如果有明显的共线关系比如类型里“动作”和“冒险”经常同时出现还可以先跑一次 PCA 看累计方差贡献率。这个经验放在毕设答辩里也很有说服力说明你不只是套模型而是对特征做过实验和分析。5.5 换数据集就报错模型把 n_users 写成了常量现象把 MovieLens 换成自己的业务数据运行到模型初始化时报 IndexError或者嵌入维度不匹配。原因资源里的代码为了单片数据集跑起来方便在 main 函数里直接写死了n_users 6040、n_items 3706这类常量模型初始化又用这些常量创建 Embedding 表。数据集一换用户数和物品数对不上必然越界。解决所有 size 相关参数从数据预处理结果中读取用变量传进模型构造函数并且加上断言检查。比如assert user_feat.shape[0] n_users尽早暴露错误而不是让它在训练中段以诡异的方式炸掉。从那以后我复现任何 GNN 项目第一件事就是检查有没有写死的 shape。6. 把 AGNN 的结论变成自己的东西消融实验和一招嵌入检查技巧6.1 消融实验关掉属性和关掉图结构各跑一遍复现只是一半工作答辩时老师最常问的是“你的提升到底来自属性还是来自图结构”。要回答这个问题就得做消融实验。做法是在主实验之外设置两个 variant第一个把属性特征全部置为全 0 向量模型退化成纯 GNN第二个把邻接矩阵换成单位矩阵让 GCN 退化成对自身特征的单层变换也就是不做任何邻居聚合。# 消融一去掉属性特征置零 zero_user_feat torch.zeros_like(user_feat) zero_item_feat torch.zeros_like(item_feat) recall_no_attr evaluate(model, zero_user_feat, zero_item_feat, k20) # 消融二去掉图结构邻接矩阵换成单位矩阵 import torch identity_adj torch.eye(n_users n_items, dtypetorch.float32) recall_no_graph evaluate(model, user_feat, item_feat, identity_adj, k20)两种消融的对比结果通常会呈现一个规律去掉图结构后指标掉得最狠说明消息传递是 AGNN 的主菜去掉属性后指标小幅下降说明属性主要负责在同一个交互圈子里区分出不同偏好。这个结论并非每个数据集都成立如果你的数据里属性本身覆盖度很低属性消融的落差会很小该如实写成“属性在当前数据上贡献有限”不要硬编一个漂亮的数据出来。我在做实验时会把三个配置的结果列在一个表里完整 AGNN、去掉属性、去掉图结构各一行题注写清楚比较条件和候选集完全一致。6.2 看嵌入PCA 降维一眼看出图传播是否生效训练结束后很多人只看指标就收工。但指标只能告诉你“好不好”不能告诉你“图到底有没有在起作用”。更直接的验证是可视化节点嵌入。把训练好的全量用户嵌入取出来用 PCA 降成二维散点图再按用户属性标签着色。如果同一属性的用户在图里聚成明显的团簇同时不同属性之间又有边界说明图传播把邻居信息洗匀了但保留了属性差异模型学得是对的。如果散点完全混合成一块均匀的云大概率第一层聚合就没生效邻接矩阵可能被重构成对角线了。from sklearn.decomposition import PCA import matplotlib.pyplot as plt user_emb model.get_user_embedding(user_feat, norm_adj).detach().numpy() pca PCA(n_components2).fit_transform(user_emb) plt.figure(figsize(8, 6)) scatter plt.scatter(pca[:, 0], pca[:, 1], cuser_age_label, s4, alpha0.6) plt.colorbar(scatter) plt.title(User Embedding PCA) plt.savefig(user_embedding_pca.png)这个技巧在答辩现场可以作为实验分析页的一张贴图效果比堆一堆参数表更好。你甚至可以把全 0 属性的消融模型也画一张散点图两张图并排比较能直观看到属性让嵌入产生了哪些分离。我第一次做这张图时才发现自己之前负采样过滤做错了嵌入全部挤在一团——幸好可视化在关键时刻救了命。从那以后我每次训练完都强制自己走一遍“消融对比加嵌入可视化”确认模型确实学的是图和属性而不是随机种子给的运气。希望这份拆解能让你少走几趟弯路把 AGNN 跑透。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →