图神经网络Graph Neural Networks这几年几乎成了深度学习圈子里最热的词之一。如果你平时刷技术文章、逛GitHub一定见过GNN频繁出现如果你做的方向涉及社交网络分析、分子性质预测、推荐系统、知识图谱推理那你大概率已经动过要不要上GNN的念头。我最早接触GNN是为了处理用户行为序列里的关系依赖折腾了一阵子GCN之后又把GAT、GraphSAGE、GIN挨个试了一圈现在回头看很多东西当时要是有人帮我讲透能少走太多弯路。这篇就把我理解里的图神经网络从头到尾捋一遍尽量不堆公式用最直白的方式把核心逻辑、常用变体、实际写代码时怎么落地以及那些网上不常写但很要命的细节坑讲清楚。适合谁来读一种是已经会用PyTorch但完全没接触过图数据的同学另一种是听过GNN但一直没搞明白消息传递到底在做什么、参数更新到底发生什么的人。这篇不需要你有图论基础有几个基本概念我会顺带解释。看完你应该能独立用PyTorch Geometric这类框架跑通一个GNN模型也知道拿到新数据集时该怎么选模型、怎么设超参、怎么调试。1. 图神经网络到底是什么1.1 一句话版本让神经网络学会看图说话传统深度学习处理的数据有个共同点结构极其规整。图像是规则的网格每个像素有固定的邻居位置文本是顺序的序列每个词有明确的前后顺序。卷积神经网络之所以有效是因为它用固定大小的卷积核在网格上滑动提取局部特征循环神经网络之所以有效是因为它沿着时间方向逐个读取输入保留记忆。这些模型都隐含了一个假设数据可以被放进一张规范的表格或者序列里。但现实世界里的数据远没有那么配合。社交网络上每个人连接着数量不等的好友分子结构里一个原子周围可能有三个邻居也可能有五个电商平台里用户和商品的交互关系更像一张没有规律可言的网。这类数据宽度不固定、顺序不固定、大小不固定你没法直接把它塞进一个定长的卷积核里去计算。图神经网络就是专门为这类不规则结构设计的神经网络它能够直接在图上做特征提取通过邻居信息更新每个节点的表示最后用来分类、预测、生成或者做检索。我习惯用一个生活化的类比去理解GNN一群人站在一个广场上每个人手里拿着一张写着自身属性的卡片。现在要对每个人做身份判断但身份不只看自己手里写了什么还取决于周围站着谁、周围的人又认识谁。GNN干的事情就是让每个人先看看自己手里的卡片再偷偷瞄一圈周围人的卡片把信息汇总之后更新自己手里的卡片。这个过程重复几轮之后每个人手里的卡片就不再只代表自己而是一整套关系背景下的综合画像。GNN在数学上做的事情本质上就是这个瞄一眼邻居、更新自己的过程只是它把这个过程设计成了可微分的神经网络层可以用反向传播去训练。1.2 为什么图比图像、文本更难处理要理解GNN为什么晚出现得先理解图数据到底难在哪里。图像虽然大但它有严格的局部性左下角的像素和右上角的像素在卷积核眼里毫无关系而你永远不必担心卷积核滑出去。文本虽然长但依然是一个带方向的链条每个位置有明确的前后文。图不一样它有三个不讲武德的特性第一个是不规则的邻居数量。一个社交网络里有人只有两个好友有人有两万个好友。所有基于固定大小邻域的卷积操作在这里直接失效因为你没法用一个3x3的卷积核去覆盖数量不等的邻居。第二个是没有天然节点顺序。一张图不靠像素位置或时间戳来排列节点。你把节点的编号在存储层面打乱图本身的语义不会变。这意味着模型不能依赖节点索引必须对节点顺序的置换保持天然不变性。这不是一个容易满足的条件普通的神经网络和卷积网络都做不到需要在网络结构设计上刻意保证置换等变性。第三个是规模差异巨大层内关联复杂。分子图很小可能只有几十个原子社交网络图很大有上亿个节点。不像图像可以靠裁剪来统一输入尺寸图的结构复杂度和规模几乎完全由数据决定无法人为控制。即使技术细节全部就会光是把采样、存储、GPU显存管理搞明白也要花不少功夫。也正是这三点逼着研究者设计了完全不同于CNN、RNN的信息聚合结构这就是GNN的核心消息传递框架。理解了消息传递就理解了百分之七八十的GNN。2. GNN是怎么工作的消息传递机制2.1 三个核心概念消息、聚合、更新几乎所有现代GNN架构都可以统一到同一个消息传递范式里。这个范式在每层网络里做三件事每个节点向邻居发送自己的当前特征向量这条特征向量就叫消息每个节点收集来自邻居的消息用某种方式把它们合并成一个单一向量这一步叫聚合节点把自己上一步的特征和聚合后的邻居消息合并经过一个非线性变换得到新特征这一步叫更新。重复这个过程L次就得到了一个L层的GNN。第1层结束后每个节点知道了它直接邻居的信息第2层结束后它知道了邻居的邻居的信息。所以GNN的感受野是层数决定的和CNN的感受野由卷积核大小和层数决定是同一个逻辑。这个过程可以用一段极简的伪代码表示for layer in range(num_layers): messages [] for node in graph.nodes: neighbor_features [features[n] for n in graph.neighbors(node)] msg aggregate(neighbor_features) # 聚合邻居消息 messages.append(combine(features[node], msg)) # 合并自身与邻居 features update(messages) # 经过非线性变换看起来非常简单对吧但注意上面这段伪代码只是逻辑示意。真正的实现需要考虑两件事第一所有节点要同步更新不能一个节点更新了就用新特征去影响同一层的另一个节点第二消息传递必须在矩阵层面并行计算否则图规模一大就慢到没法用。这两点决定了实际写代码时处理方式会和伪代码有较大差异后面实操部分再详细说。2.2 最经典的GCN层公式拆开看在众多GNN变体里图卷积网络是最广为人知的一个其思想来自谱域图信号处理的简化。GCN的层间更新公式看起来有点吓人我拆开讲[ H^{(l1)} \sigma\left( \hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2} H^{(l)} W^{(l)} \right) ]这里面几个符号的含义(H^{(l)}) 是第 (l) 层所有节点的特征矩阵形状是 (N \times d)N是节点数d是特征维度(\hat{A} A I) 是加了自环的邻接矩阵也就是给每个节点加一条指向自己的边。这一步很关键不加自环的话聚合过程只会考虑邻居而丢了自己上一层的特征(\hat{D}) 是 (\hat{A}) 的度矩阵对角线上的元素是每个节点的度。其中 (-1/2) 次方的作用是归一化让不同度数的节点更新时特征尺度差异不会太大(W^{(l)}) 是本层可学习的权重矩阵形状是 (d \times d)其中d是这一层的输出维度(\sigma) 是激活函数比如ReLU。整个计算过程可以理解为三步先给每个节点补上自连接再用度矩阵做一次对称归一化相当于给每个邻居的消息按邻居度数打个折扣最后做一次线性变换加非线性激活。权重矩阵W是唯一可学习的参数聚合规则本身没有参数。那为什么这个归一化要这么做直接取平均不行吗可以但会出现问题。假如一个节点有1万个邻居另一个节点只有2个邻居如果简单求平均高权势节点连接数极多的聚合结果会被海量消息稀释而低连接数节点可能非常敏感。度矩阵归一化本质上是希望聚合结果的尺度对节点度数不太敏感。GCN选择的是对称归一化也就是 (1/\sqrt{d_i d_j})它考虑了两个端点各自的度数比单纯平均更平衡。还有一种常见做法是通过度矩阵直接左乘来做对称归一化不同框架甚至不同论文里写法不太一样效果差异不小这也是实践中的一个小坑。2.3 多层网络的表达力层数意味着什么GNN堆到两层、三层、四五层到底在做什么用一句话概括层数决定了每个节点最终能感知到的社交圈半径。一层GNN过后节点只见过自己的一度好友两层过后见过的就是二度以内的人知道了我朋友的朋友大体是什么类型三层就是三度。听起来层数越多越好现实恰恰相反。图神经网络有个臭名昭著的问题叫过平滑层数越多所有节点的表示会逐渐趋于一致最后完全丧失区分度。原因也不难理解聚合过程其实是在做局部的信息平均每聚合一次节点间的差异性就被磨平一点。消息在多轮传递中不断混合整个图的表示趋向同一个点。这在分类任务上简直是灾难。所以实际的GNN模型一般都很浅。经典的GCN论文里实验配置通常是两到三层配合一个跳跃连接或者残差连接来缓解信息丢失。不要习惯性地把图像领域的越深越强思维带过来。做节点分类时三层GCN往往比六层更好这不是玄学是数学结构决定的。后面常见问题部分我再细讲分层数和性能的取舍以及怎么检测你的模型已经过平滑。3. 消息传递的几种主流变体3.1 GCN、GraphSAGE、GAT差在哪儿消息传递范式只是一个框架具体怎么聚合邻居消息可以有无数种设计。有几类设计思路被验证最有效我来分别讲讲差异和适用场景。GCN聚合方式就是归一化后求平均。简单、高效、参数少非常适合小规模图、同质性高的场景。缺点也很明显对所有邻居一视同仁没有注意力机制表达能力有限。另外GCN是直推式模型训练时要用整张图计算这让它在超大规模图上的应用受到限制。GraphSAGE名字是Graph SAmple and aggreGatE的缩写核心改动是两点一是引入了采样每个节点只随机选固定数量的邻居来聚合不再依赖整张图二是聚合方式更丰富支持均值、LSTM、池化等。这个采样设计让GraphSAGE可以处理大规模图也让它天然支持归纳式学习也就是训练时没见过的节点也能推理。邻居数超大时GraphSAGE是比GCN稳妥得多的选择。GAT引入注意力机制每个邻居对聚合结果的贡献权重不再是固定的而是根据两个节点当前特征学习得到的。它的优势是可以自动发现哪些邻居更重要。比如在论文引用网络里某篇论文的引用中领域方向相同的那几篇应该比方向差异很大的其他论文影响更大。GAT在不同节点动态分配权重表达能力最强但计算量也最大显存占用更敏感。用一个表来对比会更直观模型聚合方式是否采样归纳/直推计算复杂度典型适用场景GCN归一化平均否直推为主低小规模同质图基线模型GraphSAGE均值/LSTM/池化是归纳为主中大规模图、动态新增节点GAT注意力加权否均可较高异质性明显、边权重重要的图3.2 从聚合函数到池化读出输出层的设计GNN最后一层输出的节点特征怎么变成最终的预测结果这取决于任务类型。节点分类直接把节点特征接一个全连接层加Softmax边预测常见做法是把两个端点特征拼接或做点积输入到MLP里判断连接概率图分类要麻烦一点因为GNN的输出维度是 (N \times d)不同图的N不一样没有办法直接全连接。图分类的常见解法是引入读出操作也就是对全图节点做一次置换不变的池化常用的有全局平均池化所有节点特征取平均简单但丢信息很多全局最大池化逐维度取最大值能保留最具判别力的特征但会丢全局分布信息注意力池化让网络自己学一个重要性权重对节点特征做加权求和基于层次结构的池化比如DiffPool、TopK Pooling先聚类分团再逐层压缩图信息损失相对可控。实践中我发现训小数据集时平均池化和注意力池化的差距并不大当图规模差异很大有的图10个节点有的图上千个节点时平均池化会严重偏向小图。如果你发现验证集分数总是忽高忽低可以考虑换成最大池化或者加一层基于节点度的加权池化试试。3.3 设计自己的聚合器从零理解消息函数最灵活的理解方式是把GNN层拆成四个可自定义的部分消息函数、聚合函数、更新函数、附加结构。很多框架比如PyTorch Geometric给了底层API允许你自由组合。消息函数定义邻居怎么把自己的特征传递过来最常见的就是直接传递特征、拼接自身特征和邻居特征、或者加一个线性变换聚合函数定义多条消息怎么合并常见的有sum、mean、max它们的差异非常重要sum比mean更能保留节点度数信息max对异常点更鲁棒。学术上有个结论叫GIN定理大意是求和聚合在理论上比平均、最大更接近表达力上限。原因是sum能区分没有邻居和有一个全零特征邻居而mean和max做不到。但是实际项目里mean通常已经够用而且训练更稳定。这类取舍没有绝对答案项目里多做几个消融实验比在网上找哪个聚合器最好更靠谱。4. 动手搭一个GNN从数据到训练的完整流程这一部分我们直接进入实操。下面我用PyTorch Geometric写一个完整的节点分类demo跑通Cora数据集。4.1 环境准备PyTorch Geometric安装PyTorch Geometric通常简称PyG是目前最主流的GNN框架底层依赖PyTorch。安装PyG最痛苦的地方是版本匹配官方推荐用以下方式pip install torch pip install torch-scatter torch-sparse torch-cluster torch-spline-conv -f https://data.pyg.org/whl/torch-2.0.0cu118.html pip install torch-geometric注意需要根据你自己的PyTorch版本和CUDA版本修改上面的URL部分。我强烈建议先运行下面这段代码验证环境是否正常很多人的问题就出在编译依赖和CUDA不一致上import torch import torch_geometric print(torch.__version__) print(torch_geometric.__version__) print(torch.cuda.is_available())如果看不到版本号或者导入时直接报缺少动态库多半是torch-scatter和torch-sparse版本与PyTorch不匹配。老老实实去PyG官方whl列表页查你对应的版本比在GitHub上试各种workaround要快得多。4.2 加载数据并理解图数据的基础结构PyG内置了Cora数据集一个学术论文引用网络2708篇论文互有引用关系每篇论文用1433维的词袋向量描述类标签是7个研究方向中的一个。运行from torch_geometric.datasets import Planetoid dataset Planetoid(root./data, nameCora) data dataset[0] print(f节点数: {data.num_nodes}) print(f边数: {data.num_edges}) print(f特征维度: {data.num_node_features}) print(f类别数: {dataset.num_classes})PyG的Data对象里核心属性有四个x节点特征矩阵形状为节点数乘以特征维度edge_index边的起始点、终点索引形状是2乘以边数y节点标签train_mask、val_mask、test_mask指示哪些节点用于训练、验证、测试。edge_index是很多新手最容易理解错的。它不是一个邻接矩阵而是两个等长数组组成的列表第一个数组是边的起点、第二个数组是边的终点。比如edge_index [[0, 1, 2], [1, 2, 0]]表示有三条边0-1、1-2、2-0。这种方式存储稀疏图时内存开销远小于邻接矩阵在百万级节点图上这是生死攸关的区别。4.3 用PyG定义两层GCN模型PyG实现GCN层非常简洁我们可以写一个两层网络并配合Dropout。import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class GCN(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels): super().__init__() self.conv1 GCNConv(in_channels, hidden_channels) self.conv2 GCNConv(hidden_channels, out_channels) def forward(self, data): x, edge_index data.x, data.edge_index x self.conv1(x, edge_index) x F.relu(x) x F.dropout(x, trainingself.training) x self.conv2(x, edge_index) return F.log_softmax(x, dim1)注意forward里传进去的是data而不是单独的张量原因是PyG的Data对象包含了图和节点的所有信息。GCNConv内部会处理边索引到稀疏矩阵的转换、自环添加和归一化。这里隐藏维度选了16是Cora经典实验里的常用配置。隐藏维度太大会过拟合毕竟训练节点只有140个太小则表达力不足。希望你会有此刻理解超参数实验重要性的体感。训练循环和普通PyTorch基本相同但有几个特定于图数据的细节。完整训练代码可以这么写device torch.device(cuda if torch.cuda.is_available() else cpu) model GCN(dataset.num_features, 16, dataset.num_classes).to(device) data data.to(device) optimizer torch.optim.Adam(model.parameters(), lr0.01, weight_decay5e-4) def train(): model.train() optimizer.zero_grad() out model(data) loss F.nll_loss(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() return loss.item() def test(): model.eval() out model(data) pred out.argmax(dim1) accs [] for mask in [data.train_mask, data.val_mask, data.test_mask]: correct (pred[mask] data.y[mask]).sum().item() acc correct / mask.sum().item() accs.append(acc) return accs for epoch in range(200): loss train() if epoch % 20 0: train_acc, val_acc, test_acc test() print(fEpoch {epoch:03d}, Loss: {loss:.4f}, fTrain: {train_acc:.4f}, Val: {val_acc:.4f}, Test: {test_acc:.4f})跑完之后Cora上两层GCN的测试准确率正常应该在80%上下。如果低于75%先检查你是不是忘了加自环、忘加ReLU、或者学习率设置异常。这个数值本身就是模型的健康指标。4.4 扩展到GAT三行代码替换想从GCN换成GAT只需要改两处导入GATConv然后初始化时传入注意力头数。GATConv的隐藏层维度需要是hidden_channels除以heads的整数倍否则拼接时会形状报错。一般是各层heads设为8输出层heads设为1。from torch_geometric.nn import GATConv class GAT(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels): super().__init__() self.conv1 GATConv(in_channels, hidden_channels, heads8) self.conv2 GATConv(hidden_channels * 8, out_channels, heads1) def forward(self, data): x, edge_index data.x, data.edge_index x self.conv1(x, edge_index) x F.relu(x) x F.dropout(x, trainingself.training) x self.conv2(x, edge_index) return F.log_softmax(x, dim1)GAT在Cora上的结果通常会比GCN高两到三个百分点但相应训练时间也长一些显存占用量更大。小规模数据上GAT的优势不明显大图或异质图上优势会更突出。5. GNN到底能用来做什么四个典型任务5.1 节点分类、边预测、图分类、图生成GNN是一套通用骨架具体落到业务里有四种最常见范式。节点分类是最经典的任务上面的Cora就是。应用场景包括社交网络用户画像推断、学术网络中论文方向识别、电商风险节点识别。模型输入的是整张图和部分节点的标签输出是剩下节点的类别预测。这个任务对应直推式学习训练和测试都在同一张图上。边预测的目标是判断两个节点之间是否存在连接。常见应用包括推荐系统预测用户-商品交互、知识图谱补全预测实体间缺失的关系、药物副作用预测判断两种药物连用是否存在风险。注意边预测不能直接拿节点分类模型去套需要专门处理训练集/测试集的边划分测试阶段还要生成负样本。简单做法是随机剔除一部分真实边当正样本再随机采样不存在的边当负样本。图分类是针对整图输出一个标签。典型应用是分子属性预测判断一个分子是否有毒性、程序分析判断代码片段是否包含漏洞、材料科学预测晶体结构性质。图分类要把所有节点的Embedding池化成图级向量然后接分类器。这个任务更考验读出阶段的设计。图生成的应用包括新药物分子设计、蛋白质结构生成、场景图生成。图生成比前三个任务复杂得多因为需要同时生成节点和边每一步都面临结构上的组合爆炸问题。目前工业界落地相对较少研究属性更重但潜力很大。5.2 推荐系统里的GNN从被推过的东西到用户兴趣推荐系统是GNN商业化最成功的领域之一。传统协同过滤只使用用户和物品的直接交互记录做内积计算GNN则可以通过构建用户-物品-属性-上下文的异质图捕捉多跳关系。用户在某个品类下点击了一个商品这个商品又被类似人群购买过相关信息可以通过GNN的消息传递融入用户Embedding从而提升召回准确率。Pinterest早期就公开过PinSage一个基于GraphSAGE思想构建的工业级推荐系统在10亿级节点上做视觉推荐这套方案后来被写进论文成了图推荐系统的标杆。想了解工业落地细节的PinSage是很好的学习对象。5.3 什么时候不要用GNN说句得罪人的话GNN不是银弹很多场景用不上硬上反而麻烦。如果数据本身没有明显的图结构比如你有的是结构化表格数据每个样本是独立的特征那么普通MLP或树模型就够了强行构造图只会引入噪声。如果你只是要处理序列关系用LSTM、Transformer等序列模型可能更简单、更高效。图模型不是万能的序列关系严格来说是图的一个特例但特化模型在特化场景里往往更高效。如果你图规模巨大十亿级以上训练GNN的基础设施成本会很高。分布式采样、邻居缓存、增量更新全都得自己搭这不是一个团队轻轻松松能完成的事。如果图上节点特征极其稀疏或完全缺失GNN的聚合就没什么信息好传递效果会大打折扣。此时先做特征工程或冷启动填充比起把希望全寄托在结构上要靠谱得多。6. 常见问题与排查技巧实录6.1 过平滑你的模型层数是不是太多了现象验证集准确率随着网络加深不增反降所有节点的表示逐渐趋同可视化后是一坨糊状点云。排查步骤很简单画一条测试准确率-层数曲线通常2层最高3层开始下降5层以上可能接近随机猜测。如果层数超过3层反而分数上升不排除是数据特殊但这个概率不高。对策有几种最直接的减少层数回到2到3层加残差连接让节点保留部分上一层个性用JK-Net的跳跃连接机制把每层的表示都拼起来或者考虑减小聚合范围用GraphSAGE的采样策略来控制感受野。在开始堆网络结构之前先把层数和hidden size调对很多问题自然就消失了。6.2 东拼西凑的锅显存爆炸和训练速度慢得离谱整图训练的GCN在只有几万个节点的图上可能勉强能跑但到百万级节点时邻接矩阵和特征矩阵会把显存直接撑爆。解决办法是邻居采样每次迭代只随机抽一部分节点和它们的邻居子图来训练。PyG里可以用NeighborLoaderfrom torch_geometric.loader import NeighborLoader train_loader NeighborLoader( data, num_neighbors[10, 10], batch_size1024, shuffleTrue, )num_neighbors列表的长度决定了采样几跳邻居每个数字是每一跳采样的邻居数。这里[10, 10]就是采样两跳、每跳最多10个邻居。采样邻居数量越少内存越省但信息丢失越多。一般来说从10到20起调比较稳妥。显存炸了还有另一个常见原因边预测任务里把所有的负样本一次性生成并拼成大矩阵。正确做法是参考PyG的NegativeSampling和mini-batch训练一次只生成一个batch的负样本。6.3 数据准备阶段最容易犯的错数据准备简直是重灾区我踩过的坑包括把有向边当成无向边处理导致邻接矩阵不对称模型结构崩溃忽略自环问题导致消息传递时节点永远不保留自己特征结果模型预测大幅下降测试集里混入了训练时见过的边使得评估分数虚高工业场景里直接导致上线效果崩盘。一个对应的小建议正式训练前先打印图结构的基本信息统计一下自环数量、重边数量、孤立节点数用networkx把子图画出来看一眼。这一步花不了5分钟但能省下后面几个小时查bug的时间。图的很多结构性问题用眼睛看比用代码查更直观。6.4 可复现性为什么同一套代码结果每次不一样GNN训练结果不稳定是出了名的原因包括图数据加载时的随机shuffle、邻居采样随机性、Dropout导致的随机性以及某些自定义算子在不同平台上导致的小数差异。要在项目里保证可复现至少做这几件事设置所有随机种子PyTorch、NumPy、Python的random设置torch.manual_seed(0)和torch.cuda.manual_seed_all(0)加载数据时使用固定的划分不要每次重新随机分割如果用了NeighborLoader固定shuffle的随机种子。另外要注意PyG的部分算子为了性能使用原子操作在GPU上会引入很小但不可忽略的随机性这在深层次的可复现性要求下是无解的。一般项目里我们能保证相同代码在相同环境下两次运行结果一致就够了跨机器、跨CUDA版本的结果差异不必过度纠结。7. 关于变体与前沿方向的一点心得GNN这个领域变化非常快除了上面提到的GCN、GraphSAGE、GAT这些经典架构之外这两年有几个方向我认为值得关注。一个是异质图神经网络能处理多类型节点多类型边的复杂图典型框架有HAN、HGT、RGCN。现实业务几乎没有同质图用户和朋友关系、用户购买商品、商品属于品牌、品牌有类别标签天然就是异质的。想把GNN真正推向业务前沿异质图几乎是绕不开的。另一个是图Transformer把Transformer的注意力机制直接平移到图上。这类方法在部分基准上超越传统GNN尤其擅长捕捉长距离依赖代价是计算成本高目前主要还是在中小规模图上玩。还有图预训练与图自监督学习在大规模无标签图上预训练模型再在下游任务上微调。这个方向很像NLP领域BERT带起来的预训练范式对工业界特别有吸引力因为真实业务里标注数据永远稀缺无标签的图结构数据却取之不尽。做图数据和做图像、文本一个很大的区别是图数据的质量极难评估。图像不清晰、文本乱码第一时间就能看出来图的结构对不对、信息有没有泄露、标签划分合理性如何全都是藏在深处的问题。我在实际项目中最大的感受是从NLP或CV转过来的人需要先建立图结构敏感性遇到任何奇怪的结果先怀疑数据负样本生成的问题再怀疑图结构本身第三才怀疑模型。顺序反了调试效率会低很多。最后说个小技巧做边预测时有个非常经典的调度——训练/验证/测试的边划分需要保证不使节点完全孤立。有人图省事直接随机屏蔽掉20%的边做测试结果有一些节点在训练时变成了孤立点消息传不到它们的表示模型性能自然崩了。正确做法是尽量让节点持续保留度至少为1或者用专门的边划分工具。这个细节论文里是不会告诉你的但真的能决定你的实验成不成立。
阅读完成 · 觉得有帮助?