gcn层核心是a_hat @ x @ w,需构造带自环且对称归一化的邻接矩阵a_hat = d^{-1/2}(a+i)d^{-1/2},注意零度节点保护、稀疏运算、xavier初始化及a_hat预计算。

GCN层的核心实现是torch.nn.Linear加邻接矩阵归一化传播
PyTorch本身不提供开箱即用的GCNConv,但用原生API几行就能写出来。关键不是调包,而是理解GCN前向传播公式:A_hat @ X @ W,其中A_hat是带自环、对称归一化的邻接矩阵,X是节点特征,W是可学习权重。跳过torch_geometric直接手写,反而更清楚每一步在做什么。
常见错误是直接用原始邻接矩阵A做乘法——这会导致梯度爆炸或节点度偏差放大。必须先构造A_hat = D^{-1/2} (A + I) D^{-1/2},且D要基于A + I计算(补自环后再度归一)。
- 输入
X形状为[num_nodes, in_features],输出为[num_nodes, out_features] -
A必须是torch.sparse_coo_tensor或转为torch.float稠密矩阵(小图可用,大图务必稀疏) - 归一化前一定要加自环:
A = A + torch.eye(A.size(0)),否则孤立节点特征会完全丢失
手动构建A_hat时最容易漏掉torch.sqrt(D)的零值保护
计算D^{-1/2}时,如果某个节点度为0(即D[i] == 0),直接取倒数会得到inf或nan,后续整个矩阵乘法就崩了。这不是理论假设——真实数据中删除边、子图分割后常出现零度节点。
正确做法是显式屏蔽:用torch.where(D > 0, D**(-0.5), 0.0),而不是D**(-0.5)。稀疏场景下还要注意torch.sparse.sum返回的是稠密向量,需确保维度对齐。
- 别用
np.linalg.inv或torch.inverse算D^{-1/2}——这是对角阵,逐元素开方即可 - 若
A是稀疏的,A_hat也应保持稀疏;用torch.sparse.mm(A_hat, X)而非@,避免意外转稠密 - 调试时打印
A_hat.to_dense().sum(dim=1),每行和应≈1(验证归一化是否生效)
把GCN封装成nn.Module时,weight初始化不能用默认torch.nn.Linear策略
GCN的权重初始化直接影响训练稳定性。原始论文建议用torch.nn.init.xavier_uniform_,范围是±√(6 / (in_features + out_features))。而nn.Linear默认用kaiming_uniform_,适合ReLU激活,但GCN通常接ReLU或None,且输入已受邻接矩阵平滑,需要更小的初始方差。
另一个坑是忘记requires_grad=True——如果你手动创建self.weight = nn.Parameter(...)但没设requires_grad,模型根本不会更新参数,loss也不下降,还查不出原因。
- 推荐写法:
self.weight = nn.Parameter(torch.empty(in_feats, out_feats)),然后立刻nn.init.xavier_uniform_(self.weight) - 不要在
forward里重复计算A_hat——它不随batch变,应在__init__或预处理阶段算好并缓存 - 若支持不同图结构输入(如Batch GCN),
A_hat必须按每个图单独归一化,不能全局统一度矩阵
实际跑通第一层GCN前,先用单位矩阵A = torch.eye(n)做单元测试
这是最有效的排障手段。当A是单位阵,A_hat == A,GCN层退化为纯线性变换:output == X @ W。此时你可以用torch.allclose(output, torch.matmul(X, W))验证结果一致性。一旦这个通过,再换真实A,就能排除权重、输入、维度等干扰,专注查邻接矩阵处理逻辑。
很多“结果全零”“loss不降”的问题,其实卡在A_hat构造阶段——比如忘了加自环导致D对角线有0,或者稀疏矩阵索引错位让A_hat变成全零。
- 测试用例节点数别超过100,避免数值误差干扰判断
- 固定
torch.manual_seed(42),确保每次W初始化一致,方便比对 - 检查
A_hat.dtype == X.dtype,混合float32/float64会导致静默精度丢失
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











