ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

图神经网络PyTorch实战:GCN、GTN、SiGAT与SDGNN实现对比

2026/9/7 5:52:52 拓冰建站 浏览量
图神经网络PyTorch实战:GCN、GTN、SiGAT与SDGNN实现对比 图神经网络Graph Neural NetworkGNN最近几年已经从论文里的热门概念变成推荐系统、社交网络分析、知识图谱补全和材料科学等方向常用的建模工具。它和普通神经网络最大的区别在于输入数据不是规则的方格或序列而是带有连接关系的图结构。本文围绕一个适合初学者和毕业论文场景的 GNN 项目主线展开用 PyTorch 分别实现 GCN、GTN、SiGAT、SDGNN 四种具有代表性的图神经网络模型从环境安装、图数据表示、模型代码、训练评估到常见报错排查形成一个可以复现的最小闭环。读完本文你能自己跑通一个多模型对比实验并在此基础上扩展出自己的毕设方案。1. 先从四个模型讲起GNN 项目到底在解决什么问题1.1 什么是图神经网络从欧氏数据到图结构数据传统深度学习处理的数据通常是欧氏结构数据。图像是规则网格序列是线性排列卷积核和循环结构都依赖这种规则性。图数据不同社交网络中的用户、引文网络中的论文、分子结构中的原子它们之间的连接关系没有固定顺序节点数量也不固定。图神经网络的核心思想是让每个节点通过“消息传递”不断聚合邻居信息更新自己的表示。经过若干轮聚合节点向量里既包含自身特征也包含局部结构信息。用一个通俗的说法GNN 做的事情就是“看一个节点同时看它的邻居再通过邻居的特征重新描述它”。理解这一点后再看 GCN、GTN、SiGAT、SDGNN 四个名字就不会觉得抽象。它们都是 GNN 家族中的具体模型差异在于消息传递的方式不同GCNGraph Convolutional Network用归一化邻接矩阵做固定权重的邻居聚合是最经典的入门模型。GTNGraph Transformer Network面向异构图通过可学习元路径自动组合多种关系缓解人工设计元路径的麻烦。SiGAT 面向边类型和交互语义丰富的图对不同边类型分别计算注意力权重再聚合到目标节点。SDGNN 面向带符号有向图把正边、负边分开建模适用于信任关系、喜好关系等场景。1.2 四个模型的定位差异与应用场景毕设或工程里做多模型对比不能只是为了“数量多”要清楚它们各自解决什么问题。模型图类型核心手段典型场景GCN同构无向图对称归一化邻接矩阵上的图卷积引文分类、社交标签预测GTN异构图自动学习元路径并生成新邻接矩阵学术网络、商品推荐SiGAT多边类型、交互语义图按边类型拆分注意力谣言检测、交互行为建模SDGNN带符号有向图正负边分别聚合结合结构理论信任预测、立场分析这四种模型覆盖了图数据的常见复杂度变化从同构到异构从无向到有向从单一关系到多种关系从无符号到有符号。毕设里常见写法是“GCN 作为基线GTN/SiGAT/SDGNN 针对不同数据特性做改进”然后从准确率、F1、参数量、训练时间几个维度做对比实验。需要先说明本文实现的版本是面向教学的最小化版本目的是把模型的核心思想用 PyTorch 代码呈现出来。正式论文复现时还需要回到对应原始论文按论文的超参数和预处理细节做对齐。2. PyTorch 环境准备与图数据结构设计2.1 PyTorch 环境安装CPU 和 GPU 两条路线GNN 模型在节点分类这种小规模任务上CPU 也能跑起来。但如果数据是完整 Cora、Citeseer 或自定义大图建议使用 GPU 版本。推荐用 Anaconda 创建独立环境避免和系统 Python 或项目依赖冲突。安装命令以当前 PyTorch 官网给出的版本为准下面是一条常见的 GPU 环境安装示例conda create -n gnn python3.10 -y conda activate gnn conda install pytorch torchvision torchaudio pytorch-cuda12.1 -c pytorch -c nvidia如果机器没有独立显卡或只是想快速验证模型流程安装 CPU 版本conda install pytorch torchvision torchaudio cpuonly -c pytorch安装完成后一定要验证是否可用。这一步能提前暴露版本匹配问题避免后续训练时才发现 CUDA 不可用。import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU mode)CUDA 版本和 PyTorch 版本需要匹配。比如 PyTorch 对应的 CUDA 版本是 cu121 或 cu130而本机驱动只支持较低 CUDA就会导致torch.cuda.is_available()返回 False。遇到这种情况优先检查显卡驱动再确认安装的是否是 CPU 版本或匹配的 CUDA 版本。除了 PyTorch本项目的核心依赖还有这些依赖用途numpy矩阵计算与数据处理scipy稀疏矩阵操作scikit-learn准确率、F1、数据集划分networkx图结构可视化与结构分析matplotlib训练曲线和 t-SNE 可视化安装命令pip install numpy scipy scikit-learn networkx matplotlib如果后续改用 PyTorch GeometricPyG还需要单独安装对应 torch 版本的torch-geometric。对于教学项目和多数毕设实验先用 PyTorch 原生代码实现一遍更有利于理解底层传播逻辑。2.2 图的表示邻接矩阵、特征矩阵与边类型在 PyTorch 里图数据通常用三个核心部分表示节点特征矩阵x形状是[N, D]其中 N 是节点数D 是特征维度。边索引edge_index形状是[2, E]每一列表示一条有向边src - dst。标签y形状是[N, C]或[N]取决于分类任务还是回归任务。边索引的 COO 格式比完整邻接矩阵节省内存是 GNN 代码里最常见的形式。但为了直观理解 GCN 的矩阵乘法可以先把它转成稠密邻接矩阵。下面以 6 个节点组成的简单图为例import torch import torch.nn.functional as F # 节点特征6 个节点每个节点 5 维特征 x torch.randn(6, 5) # 有向边列表这里统一表示 src - dst edge_index torch.tensor([[0, 1, 2, 3, 4, 0, 1], [1, 2, 3, 4, 5, 3, 4]], dtypetorch.long) # 节点标签3 分类任务 y torch.tensor([0, 0, 1, 1, 2, 2])如果需要转成邻接矩阵可以这样写def to_adjacency(edge_index, n): adj torch.zeros(n, n) src, dst edge_index adj[src, dst] 1.0 # 如果无向图加上反向边 adj[dst, src] 1.0 return adj adj to_adjacency(edge_index, 6) print(adj)对于异构图或带边类型的图还需要一个edge_type数组长度和edge_index的列数一致表示每条边属于哪种关系。对于带符号图还需要edge_sign通常用 1 表示正边-1 表示负边。2.3 用 PyTorch 实现一个最简 Graph 数据类一个轻量的数据类可以避免在训练代码里反复管理零散变量。不需要依赖 PyG直接用torch.Tensor组织即可。class GraphData: def __init__(self, x, edge_indexNone, yNone, edge_typeNone, edge_signNone): self.x x self.edge_index edge_index self.y y self.edge_type edge_type self.edge_sign edge_sign def num_nodes(self): return self.x.size(0) def adjacency(self): n self.num_nodes() adj torch.zeros(n, n) if self.edge_index is not None: src, dst self.edge_index adj[src, dst] 1.0 return adj def to(self, device): self.x self.x.to(device) if self.edge_index is not None: self.edge_index self.edge_index.to(device) if self.y is not None: self.y self.y.to(device) if self.edge_type is not None: self.edge_type self.edge_type.to(device) if self.edge_sign is not None: self.edge_sign self.edge_sign.to(device) return self这个类的好处是数据可以整体搬到 GPU也不用在模型 forward 里重复判断边类型是否存在。实际项目中还可以继续加入节点掩码、训练集索引、验证集索引等字段。3. 动手实现四个模型PyTorch 版本核心代码3.1 GCN基于邻接矩阵归一化的卷积传播GCN 的传播公式可以写成H^{(l1)} σ(Â H^{(l)} W^{(l)})其中 Â 是加入自环并对角归一化后的邻接矩阵。加入自环是为了让节点聚合邻居时也保留自身信息对称归一化则是为了减弱度数高的节点对聚合结果的主导作用。class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim, biasTrue): super().__init__() self.linear nn.Linear(in_dim, out_dim, biasbias) def forward(self, x, adj_norm): # adj_norm x 完成邻居聚合再做线性变换 return self.linear(torch.mm(adj_norm, x)) class GCN(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout0.5): super().__init__() self.layer1 GCNLayer(in_dim, hidden_dim) self.layer2 GCNLayer(hidden_dim, out_dim) self.dropout nn.Dropout(dropout) def preprocess(self, adj): # 加入自环 adj adj torch.eye(adj.size(0), deviceadj.device) deg adj.sum(dim1).clamp(min1e-8) deg_inv_sqrt deg.pow(-0.5) return deg_inv_sqrt.view(-1, 1) * adj * deg_inv_sqrt.view(1, -1) def forward(self, x, adj): adj_norm self.preprocess(adj) h F.relu(self.layer1(self.dropout(x), adj_norm)) h self.layer2(self.dropout(h), adj_norm) return h两层 GCN 是节点分类任务里最常用的配置。第一层把原始特征映射到隐藏空间第二层输出类别 logits。训练时一般只在最后一层之前加 dropout防止过拟合。这里要特别注意preprocess每次 forward 都会计算归一化矩阵。对大规模图推荐在训练前预处理一次并缓存。上面的写法方便教学但不适合超大图。3.2 GTN用可学习元路径组合异构图关系异构图中节点可以通过不同类型的关系相连。例如学术网络中“作者-论文”和“论文-作者”属于不同关系元路径则描述了一条跨多种关系到达目标节点的路径。传统方法需要人工指定元路径GTN 的思路是让模型自己学习如何组合关系矩阵生成任务最需要的元路径邻接矩阵。下面是最小化版本。假设有 R 种关系每种关系对应一个邻接矩阵模型学习长度为 L 的元路径如何选择关系。class GTNLayer(nn.Module): def __init__(self, num_relations, meta_path_len): super().__init__() self.meta_path_len meta_path_len self.w nn.Parameter(torch.randn(meta_path_len, num_relations)) def forward(self, rel_adj): # rel_adj: [R, N, N]第 i 个矩阵表示第 i 种关系的邻接矩阵 # 逐步组合关系矩阵模拟一条长度为 meta_path_len 的元路径 result None for step in range(self.meta_path_len): weight torch.softmax(self.w[step], dim0) # 当前步按权重融合所有关系矩阵 current torch.einsum(r,rnm-nm, weight, rel_adj) if result is None: result current else: # 矩阵乘法模拟元路径的连续游走 result torch.mm(result, current) return result生成新的邻接矩阵后再交给一个 GCN 或者线性分类器即可。完整 GTN 还会对每个通道生成的元路径矩阵做 1x1 卷积并在通道间取最大值或平均值这里略去了这些细节。真正复现时需要按原论文增加通道数、中间层和图正则项。这个模型最容易出错的地方是关系矩阵的数量和edge_type的数量不一致。构造rel_adj时一定要确认每一类关系都至少有一条边否则对应矩阵全为 0会影响元路径学习。3.3 SiGAT按边类型拆分注意力做交互建模SiGAT 的核心是不同的边类型代表不同的交互语义因此计算注意力权重时不能共用一组参数而应该按边类型分别计算。这样模型可以学到“积极互动”和“消极互动”对节点表示的不同影响。教学实现可以按边类型循环对每一类边使用独立的注意力向量class SigatLayer(nn.Module): def __init__(self, in_dim, out_dim, num_edge_types, dropout0.5): super().__init__() self.w nn.Linear(in_dim, out_dim) self.a nn.Parameter(torch.randn(num_edge_types, 2 * out_dim)) self.dropout nn.Dropout(dropout) def softmax_by_target(self, score, target, n): # 对每个目标节点在它的入边上做 softmax alpha torch.zeros_like(score) for v in range(n): idx target v if idx.sum() 0: continue z score[idx] - score[idx].max() e z.exp() alpha[idx] e / e.sum() return alpha def forward(self, x, edge_index, edge_type): h self.w(x) src, dst edge_index n x.size(0) out torch.zeros_like(h) for t in range(self.a.size(0)): mask edge_type t if mask.sum() 0: continue s src[mask] d dst[mask] cat torch.cat([h[s], h[d]], dim1) score F.leaky_relu((cat * self.a[t]).sum(dim1), 0.2) alpha self.softmax_by_target(score, d, n) for idx, v in enumerate(d): out[v] out[v] alpha[idx].unsqueeze(-1) * h[s[idx]] return F.relu(out)循环版本便于理解但性能差。真实项目中应该用torch.scatter_add或者引入torch_scatter库做并行聚合。毕设实验如果数据量不大循环版能跑但如果要强调效率建议改成批量算子实现。一个容易踩的坑是edge_type里的值超出self.a.size(0)比如边类型从 1 开始计数。建议所有边类型从 0 开始连续编号并在建模前检查print(edge_type.unique()) print(edge_type.max())3.4 SDGNN把正边和负边分开聚合带符号图里正边表示信任、喜欢、同意负边表示不信任、不喜欢、反对。如果把正负边混在一起做邻居聚合语义会被抵消。SDGNN 的思路是给正边和负边分别使用不同的变换参数从而保留符号信息。class SDGNNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.w_pos nn.Linear(in_dim, out_dim) self.w_neg nn.Linear(in_dim, out_dim) def forward(self, x, edge_index, edge_sign, degreeNone): src, dst edge_index h_pos self.w_pos(x) h_neg self.w_neg(x) out torch.zeros_like(h_pos) for i in range(len(src)): feat h_pos[src[i]] if edge_sign[i] 1 else h_neg[src[i]] out[dst[i]] out[dst[i]] feat if degree is not None: out out / degree.clamp(min1e-8).unsqueeze(-1) return out训练时可以把多个 SDGNNLayer 堆叠起来最后一层输出节点嵌入再用内积或 MLP 做链接预测。链接预测的标签来自边是否存在以及边的符号因此模型不仅要预测两个节点之间有没有边还要预测边的正负。论文级别的 SDGNN 还会结合平衡理论和地位理论构造结构约束作为额外监督信号。教学版本不展开这些理论细节但理解“正负边分参数”这一思想足以支撑你在此基础上做算法改进。4. 训练、评估与项目结构设计4.1 通用训练流程封装四类模型虽然结构不同但训练框架完全一致定义模型、定义损失、选择优化器、循环迭代、定期评估验证集。可以把流程封装成一个函数方便复用到多个模型上。以节点分类为例def train_node_classification(model, data, train_mask, val_mask, epochs200, lr0.01, weight_decay5e-4): optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decayweight_decay) loss_fn nn.CrossEntropyLoss() for epoch in range(epochs): model.train() optimizer.zero_grad() logits model(data.x, data.adjacency()) loss loss_fn(logits[train_mask], data.y[train_mask]) loss.backward() optimizer.step() if (epoch 1) % 20 0: acc evaluate_acc(model, data, val_mask) print(fepoch {epoch 1}, loss {loss.item():.4f}, val acc {acc:.4f})对于 SiGAT 和 SDGNN调用模型时要额外传入edge_type或edge_sign所以可以针对模型写一个统一的 forward 分发函数或者在训练函数里判断模型类型。如果是链接预测任务需要改损失函数。比如用验证集上的正负边构造样本预测边存在的概率计算二元交叉熵。4.2 评估指标与可视化节点分类常用准确率和 Macro-F1。当类别不均衡时Macro-F1 比准确率更可靠。链接预测则要看 ROC-AUC 和 AP。from sklearn.metrics import accuracy_score, f1_score, roc_auc_score def evaluate_acc(model, data, mask): model.eval() with torch.no_grad(): logits model(data.x, data.adjacency()) pred logits.argmax(dim1) return accuracy_score(data.y[mask].cpu(), pred[mask].cpu()) def evaluate_f1(model, data, mask): model.eval() with torch.no_grad(): logits model(data.x, data.adjacency()) pred logits.argmax(dim1) return f1_score(data.y[mask].cpu(), pred[mask].cpu(), averagemacro)可视化方面可以用 sklearn 的 TSNE 对训练好的节点嵌入降维再用 matplotlib 按类别着色。这一步在毕设报告里很有价值能直观展示模型是否把同类节点聚在一起。4.3 本地测试用最小数据集不建议一上来就跑完整 Cora 数据集。先用一个几十个节点的自造小图验证代码链路确认模型能收敛、维度正确再切换到正式数据。下面是一个最小数据集示例可以直接用于测试 GCNdata GraphData( xtorch.randn(6, 5), edge_indextorch.tensor([[0, 1, 2, 3, 4, 0, 1], [1, 2, 3, 4, 5, 3, 4]], dtypetorch.long), ytorch.tensor([0, 0, 1, 1, 2, 2]) ) train_mask torch.tensor([True, True, True, False, True, False]) val_mask torch.tensor([False, False, False, True, False, True]) model GCN(in_dim5, hidden_dim16, out_dim3) train_node_classification(model, data, train_mask, val_mask, epochs200)小数据上损失应该稳定下降。如果不下降优先检查学习方法率、归一化、标签索引是否对齐。5. 常见问题排查从环境到模型输出5.1 环境安装与 CUDA 相关毕设答辩前最容易出问题的不是模型代码而是环境。常见现象是代码在别人机器上能跑换到自己的机器就报错。问题现象常见原因检查方式处理建议torch 导入失败Python 版本不兼容或安装命令不对检查 Python 版本和管理员权限用 Anaconda 重建环境按官网命令安装cuda.is_available() 为 False装的 CPU 版或驱动版本低打印 torch.version查看是否带 cu 后缀安装匹配 GPU 驱动的 CUDA 版本训练时显存不足邻接矩阵转稠密后过大查看 N 和 E 的大小改用稀疏矩阵算子或 mini-batch 采样线下安装了旧版 PyTorch依赖冲突pip list 或 conda list重装并固定版本号新环境的建议是先查官网复制当前稳定版的安装命令不要照抄旧教程里的 CUDA 版本号。5.2 数据维度与图结构错误图数据代码里最典型的错误是维度对不上。例如edge_index中的节点编号超出了x的行数。排查顺序打印x.size()得到节点数 N。打印edge_index.max()和edge_index.min()。确认所有索引都满足0 idx N。确认adjacency()函数生成的矩阵是[N, N]。确认标签数量等于 N。另一个常见问题是孤立节点。某些节点没有任何邻居经过消息传递后表示不更新。解决方法是在邻接矩阵中加入自环或者在聚合时保留节点自身特征。5.3 训练不收敛或模型效果差模型能跑但不收敛通常不是代码语法错误而是设计问题。问题现象可能原因解决方向loss 不下降学习率过大或过小尝试 0.1、0.01、0.001 等不同量级准确率接近随机未归一化邻接矩阵导致数值不稳定检查 GCN 预处理逻辑验证集先升后降过拟合增加 dropout、减小 hidden_dim、增加权重衰减深层 GCN 后效果变差过平滑所有节点表示趋于一致减少层数或加残差连接GTN 输出全为相同值关系矩阵组合后过度平滑检查 softmax 权重是否集中在单一关系上过平滑是 GNN 领域的经典问题。堆叠太多层后每个节点都聚合了很远范围的邻居局部差异消失。毕设实验里如果发现“层数越多效果越差”这是正常现象可以作为论文中的一点分析内容写进去。5.4 可复用的排错清单遇到任何 GNN 模型运行失败按照下面的顺序检查[ ] PyTorch 是否激活了正确环境版本是否和代码兼容。[ ] 输入特征维度in_dim是否和模型第一层定义一致。[ ] 输出维度是否和类别数一致。[ ] 邻接矩阵是否为浮点型设备是否和模型一致。[ ]edge_index最大值是否越界。[ ] 训练集、验证集、测试集是否重叠。[ ] 损失函数是否和任务类型匹配。[ ] 是否设置随机种子结果可复现。[ ] 模型在 CPU 上能否跑通再切到 GPU。6. 毕业论文扩展与生产环境落地建议6.1 毕设项目如何从“跑通”到“有创新点”很多毕设的问题不是“没有模型”而是“只有模型没有研究点”。基于本文四个模型的组合可以从以下几个方向扩展对比实验固定数据集和超参数跑 GCN、GTN、SiGAT、SDGNN比较准确率、F1、训练时间、显存占用。消融实验对 SiGAT去掉边类型注意力替换成统一注意力观察效果下降幅度。参数分析统计图结构对模型的影响例如稀疏度、异质性比例、正负边比例。数据集扩展在公开数据集 CitrHepTh、Amazon、Yelp 等上面验证而不是只用自造小图。模型改进在 SDGNN 中融合位置编码或结构约束在 GTN 中引入多头元路径形成自己的创新点。做实验时一定要固定随机种子。不同模型初始化不同效果差异可能来自随机性而不是模型本身。6.2 从教学实现到真实工程的关键边界教学代码和工程代码之间有明显差距。生产环境需要额外考虑四点第一图规模。完整邻接矩阵的空间复杂度是 O(N^2)。一万个节点就需要存储一亿个元素这在大图上不可接受。工程上应该使用稀疏矩阵乘法或者把图采样成 mini-batch。第二库的选型。PyG、DGL 提供了高效的消息传递原语、常用数据集和抽样器。教学代码理解了原理后工程实现建议迁移到这些库上。第三数据版本与复现。图数据的构建逻辑、边过滤规则、标签划分方式都要写清楚。否则论文数据结果无法复现。第四服务化。训练完成的 GNN 模型可以导出为 TorchScript 或 ONNX 部署到线上但需要处理动态图输入、批量化请求、特征拼接等工程问题。下面是工程化之前可以按顺序检查的清单[ ] 使用稀疏矩阵表示邻接关系而不是稠密矩阵。[ ] 数据集划分固定并记录 seed。[ ] 模型参数保存为 checkpoint包含 epoch、优化器状态和最佳指标。[ ] 日志完整记录每个 epoch 的 loss 和验证指标。[ ] 代码中预留边类型、边符号等扩展字段。[ ] 真实验证前先在小型数据集上测试 pipeline。6.3 对新手最有价值的练习路径如果这是你第一次接触 GNN建议按下面的路径推进先用 GCN 完成一个节点分类最小实验理解邻接矩阵归一化。然后把数据换成异构图加入edge_type对照 GTN 理解元路径自动学习。接着调整代码把消息聚合从“均值”改成“注意力”理解 SiGAT 的边类型注意力。最后把图改成带符号有向图理解 SDGNN 的正负边分开建模。每一步都要写一份实验记录记录模型结构、超参数、结果和踩坑过程。这些记录稍加整理就是毕设论文里的实验章节。GNN 入门的关键不是背公式而是反复跑通一条从数据到代码再到结果的完整链路。本文给出的四个模型和配套流程就是为了让你在最短时间内建立这条链路再沿着它往更深的理论和更复杂的工程方案扩展。