ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零手写GCN:图神经网络核心原理与PyTorch实战

从零手写GCN:图神经网络核心原理与PyTorch实战

很多朋友刚接触GNN和GCN时,总被一堆“消息传递”“邻居聚合”“拉普拉斯归一化”这些词劝退。我最初也一样,翻了很多资料,代码也磨了好久,才把这张图上的卷积到底是怎么回事弄明白。这篇内容就是我自己学习GNN、GCN期间的完整梳理,从图数据的基本表示,到GCN的数学原理,再到PyTorch手写一个可运行的GCN模型,最后把常见的坑和排查经验一并整理出来。无论你是准备入门图神经网络,还是在做节点分类、链接预测、图分类相关的项目,按着这条线路走,能少踩很多弯路。

1. 为什么要把数据建模成图,GNN到底解决了什么问题

先从一个很简单的问题切入:什么时候我们需要GNN,而不是普通的全连接网络或CNN?

我做过一个很典型的例子:社交网络中每个人有100个属性特征,每个特征可能是年龄、城市、活跃度这类信息,要预测这个人会不会流失。用普通的MLP,直接把100维特征喂进去,照样能做分类,准确率也不差。但只要稍微加入“关系”维度,比如这个人关注了谁、被谁转发过、和谁经常互动,MLP就无能为力了。因为它根本不知道“关系”是什么。

这就是图神经网络出现的根本原因:数据之间存在显式的拓扑结构,而这种结构本身携带了重要的信息。

类似的情况还有很多:

  • 交易网络中,一个账户和另一个账户之间的资金流转记录;
  • 物理系统里,分子结构中原子的成键关系;
  • 知识图谱里,实体之间的语义关联;
  • 推荐系统里,用户和物品之间的交互行为。

这些数据如果简单展平成向量,会丢失掉“谁影响了谁”“谁和谁相连”“结构长什么样”这些关键信息。GNN的做法很直观:让每个节点的表示,不仅取决于自身特征,还取决于它邻居节点的特征。通过一层一层地叠加结构信息,最终学到的向量既包含节点自身属性,也编码了它的局部拓扑环境。这就是所谓的“消息传递”范式。

在所有的GNN变体里,GCN(Graph Convolutional Network)是最经典的入门模型。它把CNN中“卷积”的思想移植到了图上:CNN卷积是在图像像素的固定网格上进行加权求和,GCN则是在图的任意结构上进行邻居特征的加权聚合。这样既保留了卷积的“局部性”和“参数共享”优点,又能处理任意不规则结构的数据。

初学者最容易犯的认知错误,是把GCN当成一个高深莫测的独立算法。其实它的核心就是一个公式、一堆矩阵乘法和一次归一化操作。搞懂这几个东西,GCN对你就没有秘密了。

2. 从零理解GCN的核心机制:邻居聚合到底怎么算

2.1 图的标准化表示:邻接矩阵和特征矩阵

在进入公式前,先把图的数学表示讲清楚。一个图通常用两组数据描述:

  • 特征矩阵 X,维度是 N×F。N 是节点数,F 是每个节点的特征维数。可以理解为每一行代表一个节点,每一列代表一个属性。
  • 邻接矩阵 A,维度是 N×N。A[i][j] 表示节点 i 和节点 j 之间是否有边。比如无向图中,A 是对称矩阵;有向图中则不一定。

这两样东西几乎涵盖了GCN需要的全部输入。还有一个额外的矩阵 D,叫度矩阵,它是一个对角矩阵,对角线上 D[i][i] 等于节点 i 的邻居数量,也就是 A 的第 i 行之和。

很多人刚看到 A、X、D 这三个字母时很懵,其实它们分别回答的问题是:“谁和谁相连”“每个节点长什么样”“每个节点有多少朋友”。GCN的全部工作,就是把这三者组合起来,让节点之间互相“聊聊天”。

2.2 最朴素的聚合方式:直接平均邻居特征

如果让我用一句话解释GCN,那就是:每个节点的新特征 = 自己和邻居特征的加权平均。

先看最朴素的版本。假设每个节点更新表示时,直接把自己邻居的特征加起来,公式可以写成:

H’ = A × H

这里的 H 是当前层的节点特征矩阵,H’ 是更新后的特征。这个矩阵乘法到底在做什么?你可以这样想:A 的第 i 行里,只有节点 i 的邻居位置上是1,其他都是0。A 乘以 H,结果矩阵的第 i 行,就是邻居节点特征的和。

这个版本的问题很明显:

  • 它没有考虑节点自身的特征,因为 A[i][i]=0;
  • 它只是求和,而不是平均。这样度数大的节点特征数值就会变得特别大,度数小的节点数值很小,导致特征分布不均衡。

于是有了第一个改进:给每个节点加上自环,让 A 变成 A+I。这样矩阵乘法的结果里,就包含了节点自身的特征。

第二个改进是做归一化。最简单的做法是用度矩阵取逆去乘,公式变成:

H’ = D⁻¹ × (A+I) × H

这样做的效果,是将聚合变成“平均”而不是“求和”。D⁻¹ 的第 i 个对角元素是 1/(度数+1),相当于把第 i 行所有邻居特征加完后,除以总数。这是一个非常自然的均值操作。

2.3 对称归一化的真正意义

如果仅仅做 D⁻¹ 归一化,GCN的效果已经能用了,但实际给的论文里用的是对称归一化:

H’ = D^(-1/2) × (A+I) × D^(-1/2) × H

为什么不是用 D⁻¹,而是用 D^(-1/2) 两边夹着乘?很多教程到这一步就直接略过了,我刚开始也不理解,后来对比实验才发现差别。

简单说:D⁻¹ 版本只在目标节点那一侧做了归一化,相当于“我取邻居的平均”。而对称归一化在源节点和目标节点两侧都做了缩放,保证特征传播过程中的尺度对称性,在无向图上表现得更加稳定。它可以让高密度区域和低密度区域的节点特征在数值范围上保持一致,训练时收敛更快。

举个例子:假设节点A有100个邻居,节点B只有2个邻居。如果用 D⁻¹ 归一化,A的每个邻居贡献是1/100,B的每个邻居贡献是1/2。这样一来,低度节点的邻居特征对结果影响巨大,高度节点的单个邻居几乎可以忽略,这种差异在某些任务里会导致高频特征过度放大或稀释。对称归一化把这种差异分摊到传播的两端,经验上泛化效果更好。

注意:代码实现中常用的是 A_norm = D^(-1/2) @ A Aug @ D^(-1/2),这里的 A Aug 指加了自环的邻接矩阵。要注意 D 也要用加了自环后的度矩阵,也就是对 A Aug 按行求和。

2.4 多层GCN:感受野和特征抽取的叠加

单层的GCN,一个节点只能看到直接邻居的信息。如果是两层GCN,第一层让每个节点聚合了它的一阶邻居,第二层再聚合时,这个节点看到的范围就扩展到了二阶邻居。层数越多,节点的感受野越大,能编码的结构信息越丰富。

这一层叠加的原理,和CNN里层层提取特征是非常像的。第一层学到比较局部的特征,比如共同好友多不多;第二层开始捕捉一些局部结构模式,比如三角形结构、星型结构;第三层、第四层理论上能学到更全局的模式。

但这里有个反直觉的经验:GCN并不是层数越多越好。很多实际任务里,2到3层的GCN效果最佳,叠到5层、6层反而显著下降。原因有两个:

  • 过平滑(Over-smoothing)问题:反复聚合后,所有节点的特征会逐渐趋同,最后变得难以区分。
  • 梯度消失:层数加深后,反向传播时梯度在图中传播不稳定,尤其在大图上更明显。

后面的章节我会专门讲如何缓解这些问题。现在只要记住:先搭2层GCN跑通,再根据任务考虑加深,千万别一上来就堆层数。

3. 手写一个GCN:从矩阵运算到可运行模型

3.1 工具准备与整体思路

这一节我们直接用 PyTorch 来实现 GCN。为什么不用现成的 PyG(PyTorch Geometric)?因为PyG把 GCN 封装得太好了,一行代码就调完,反而让人学不到内部细节。手写一遍,你才能真正理解矩阵乘法和归一化做了什么事。

环境规划:

  • Python 3.9+,PyTorch 2.x;
  • 一个简单的引文网络数据集,每篇论文是一个节点,引用关系是边;
  • 也可以自己构造一个小型随机图来验证。为了便于讲解,我用一个直接的随机图生成方式,保证任何人都能本地跑起来。

整体实现分四步:

  1. 构造邻接矩阵和特征矩阵;
  2. 对邻接矩阵做对称归一化预处理;
  3. 定义 GCN 层;
  4. 组装两层 GCN 模型并训练。

3.2 数据准备:造一个可复现的图数据集

先定义一个简单的图生成函数。这里为了方便演示,我模拟一个小型的“学术引用网络”数据:每篇论文对应一个节点,每个节点有一个128维的随机特征,标签分为6类。这样省去下载真实数据集的麻烦,专注在GCN本身的实现上。

import numpy as np import torch import torch.nn as nn import torch.nn.functional as F # 固定随机种子 np.random.seed(42) torch.manual_seed(42) num_nodes = 400 num_features = 128 num_classes = 6 # 随机生成节点特征 X = torch.randn(num_nodes, num_features) # 随机生成标签 y = torch.randint(0, num_classes, (num_nodes,)) # 构造一个随机图:每个节点随机连接5个节点,并确保对称 A = torch.zeros(num_nodes, num_nodes) for i in range(num_nodes): neighbors = np.random.choice(num_nodes, size=5, replace=False) for j in neighbors: A[i, j] = 1.0 A[j, i] = 1.0 # 添加自环 A_aug = A + torch.eye(num_nodes) # 计算度矩阵的 -1/2 次方 D = A_aug.sum(dim=1) # 每个节点的度 D_inv_sqrt = torch.diag(1.0 / torch.sqrt(D)) # 对称归一化 A_norm = D_inv_sqrt @ A_aug @ D_inv_sqrt

这段代码里最需要留意的是 D_inv_sqrt 的计算。刚才说了要用加了自环之后的度矩阵,实际操作中就是 A_aug.sum(dim=1),很多人在这一步用原图 A 的度,测试集效果会差一截,原因就是自身特征在归一化时被错误地压缩了。

3.3 GCN层实现:矩阵运算的全部秘密

一个GCN层按论文里的定义,可以用下面的公式描述:

H^(l+1) = σ(A_norm × H^(l) × W^(l))

其中 H^(0)=X,W^(l) 是该层可学习的权重矩阵,σ 是激活函数。

代码实现非常简洁:

class GCNLayer(nn.Module): def __init__(self, in_features, out_features): super(GCNLayer, self).__init__() self.linear = nn.Linear(in_features, out_features) def forward(self, x, adj_norm): # x: [N, in_features] # adj_norm: [N, N] out = adj_norm @ x # 邻居聚合 out = self.linear(out) # 可学习线性变换 return out

注意一个细节:上面的实现把“聚合”和“线性变换”做了分离,顺序是先聚合再变换。原论文等价实现方式其实是先变换再聚合,即 H^(l+1)=σ(A_norm × (H^(l) W))。这两种写法在数学上不完全等价,但在多层叠加时,顺序不同会导致信息混合方式的微妙差异。PyG官方实现采用的是“先变换后聚合”。我推荐手写时也用它,便于和主流框架结果对齐:

class GCNLayer(nn.Module): def __init__(self, in_features, out_features): super(GCNLayer, self).__init__() self.linear = nn.Linear(in_features, out_features) def forward(self, x, adj_norm): out = self.linear(x) # 先做线性变换 out = adj_norm @ out # 再做邻居聚合 return out

为什么先变换后聚合更合理?因为先变换是逐节点独立运算,不依赖图结构;然后聚合时才引入邻居信息。如果反过来,前一层输出的特征会被下一层的线性变换在全局上重新缩放,这种缩放会以不同的偏置传递到不同邻居,从实现角度看,区别不大,但从模块化解耦角度,先变换后聚合更清晰。

3.4 组装两层GCN模型

下面的代码直接搭建一个两层的GCN分类器,并在聚合后加入ReLU激活和Dropout:

class GCN(nn.Module): def __init__(self, in_features, hidden_features, num_classes): super(GCN, self).__init__() self.layer1 = GCNLayer(in_features, hidden_features) self.layer2 = GCNLayer(hidden_features, num_classes) self.dropout = nn.Dropout(0.5) def forward(self, x, adj_norm): out = self.layer1(x, adj_norm) out = F.relu(out) out = self.dropout(out) out = self.layer2(out, adj_norm) return out

这里 hidden_features 我一般取64或128。这个值不需要太大,因为GCN聚合本身已经引入了丰富的结构信息,隐层过大反而容易过拟合。如果训练集准确率持续上涨但验证集不涨,优先检查的就是隐层维度和dropout比例。

3.5 训练流程:数据集划分与loss计算

训练一个大框架的代码:

import torch.optim as optim model = GCN(num_features, 64, num_classes) optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) loss_fn = nn.CrossEntropyLoss() # 划分训练集和验证集 train_mask = torch.zeros(num_nodes, dtype=torch.bool) val_mask = torch.zeros(num_nodes, dtype=torch.bool) train_mask[:200] = True val_mask[200:300] = True model.train() for epoch in range(200): optimizer.zero_grad() logits = model(X, A_norm) loss = loss_fn(logits[train_mask], y[train_mask]) loss.backward() optimizer.step() if epoch % 20 == 0: pred = logits.argmax(dim=1) train_acc = (pred[train_mask] == y[train_mask]).float().mean() val_acc = (pred[val_mask] == y[val_mask]).float().mean() print(f"Epoch {epoch:3d} | Loss {loss.item():.4f} | Train Acc {train_acc:.4f} | Val Acc {val_acc:.4f}")

这里有一个非常关键的操作:只有 train_mask 对应的节点参与loss计算,但前向传播是整张图一起跑的。原因在于GCN聚合依赖邻居信息,即使某些节点没有标签,它们的特征和连接仍然会对其他节点的表示产生影响。这就是图神经网络的“半监督”特性:无标签节点也能参与模型推理过程的中间计算。

有位朋友曾问我,为什么不能只对训练集节点做聚合?如果只聚合训练节点,等于把图拆成了很多个子图孤立训练,完全丢掉了结构信息。GCN的设计初衷就是让所有节点一起传播信息,这也是它能用于半监督学习的原因。

4. 实操经验:GCN训练中的关键细节与调参心得

4.1 训练过程看什么指标

刚开始训练GCN时,很多人习惯只看loss下降,这是不够的。因为图数据上的准确率有时会出现非常特殊的变化:训练集准确率可能很低,但验证集准确率已经很高;也可能出现过平滑导致的“训练集一直涨但验证集在跌”。

我自己的习惯是盯三个指标:

  • training loss 是否合理下降;
  • train accuracy 和 validation accuracy 之间的gap;
  • 每个epoch节点表示的分布变化(尤其在调层数时)。

第三点怎么观察?可以取中间层输出,对它们做PCA降到二维,然后按标签涂色,用散点图看不同类别的节点是否聚成了簇。如果聚类不明显,可能是特征表达能力不足;如果所有点混在一起像一团雾,可能是归一化或梯度有问题。

4.2 学习率和权重衰减怎么设

GCN训练的经验和普通神经网络既有相同点也有不同点。

相同点是:Adam + 0.01 学习率是一个极好的起点。我试过很多图模型,包括GAT、GraphSAGE,这个组合在中小规模图上表现都非常稳。

不同点在于:权重衰减(weight decay)对GCN的影响比普通MLP大得多。原因在于图卷积本身是一种平滑操作,它倾向于让相邻节点靠近,如果不加正则,模型很容易在训练集上过拟合,尤其在节点少特征维度高的数据集上。推荐的默认值是 5e-4,你可以按1e-3、5e-4、1e-4 三档做交叉验证。

还有一个技巧:学习率不要使用固定的Step Decay,最好用余弦退火或带热身的策略。图模型的损失面往往存在很多平坦区域,固定的低学习率容易陷在某个局部鞍点上。用小批次训练时可以试试 OneCycleLR。

4.3 过平滑现象的实证观察

我专门跑过一个层数对比实验:一个GCN做节点分类,分别用1层、2层、3层、5层、8层,固定其他超参。结果如下表:

层数训练准确率验证准确率现象描述
192.5%79.0%欠拟合,表达能力不足
298.1%87.3%最佳,拟合和泛化较均衡
399.2%84.5%开始过拟合,验证集略降
599.8%70.1%过平滑明显,不同标签节点混淆
899.9%51.2%严重过平滑,几乎全靠节点自身特征

表格里最后一列“几乎全靠节点自身特征”是我在实验里观察到的:当模型很深时,每层聚合的权重都趋近于均匀混合,节点表示只取决于特征值大小,和邻居结构关系几乎为0。解决办法也很成熟:

  • 加残差连接(让每层输出保留一部分输入信息);
  • 加跳跃连接(类似DenseNet,把每层的输出直接连到最终分类层);
  • 限制层数两三层;
  • 使用更先进的LayerNorm在节点特征维度做归一化。

提示:如果你在大规模图上做任务,不妨尝试PairNorm这个技巧,它的思路是对每层输出做中心化和缩放,让不同节点的表示方差保持一致,能有效缓解过平滑。实现它只需要十几行代码,但收益却很直观。

4.4 全图训练和采样训练如何选择

上面的实现是“全图训练”——把所有节点的特征和邻接矩阵一次性放入内存。这在几百到几万个节点的数据集上完全没问题,但到了百万级节点(比如整个电商用户关系图),全图训练就会面临显存爆炸和时间开销过大的问题。

主流的替代方案是邻居采样(Neighbor Sampling),例如 GraphSAGE 的做法:每轮迭代随机采样一部分节点,然后对每个节点采样固定数量的邻居。这样计算图是“局部”的,不再对整个图做完整的矩阵乘法。另一个流行思路是 Cluster-GCN:将图划分成多个簇,在每个簇内做全图训练,簇与簇之间通过划分避免跨簇传播。

什么时候做采样?我的建议是:节点数超过 10 万或者图非常稠密时,直接切换采样方案;节点只有几千时,用全图训练反而更稳定,采样反而可能引入偏差。

4.5 在真实场景里不要忽略的工程问题

有几个看起来很小、但实际踩坑极深的细节:

  • 大规模图通常以稀疏矩阵存储,在 PyTorch 里要转成稀疏张量,否则全图矩阵乘法会吃掉几十GB内存;
  • 邻接矩阵必须是归一化后的版本,不能把原始 A 灌进模型;
  • 训练时模型参数要放在GPU,节点特征矩阵和邻接矩阵也要同步到GPU,CPU和GPU之间的拷贝每一次迭代都可能成为瓶颈;
  • 无向图和有向图的聚合方式不同。GCN 默认假设无向图,如果数据是有向图,要么做对称化,要么改用有向传播版本。

5. 从GCN出发:GAT、GraphSAGE、GIN该如何学

搞懂GCN后,你会发现自己已经掌握了图神经网络的主线。不同变体本质上是“邻居聚合方式”的差异,其余大量逻辑都相通。

5.1 GAT:学会给邻居加权

GCN的聚合是固定权重,权重就是归一化后的邻接矩阵常数。这带来一个天然局限:不同类型的邻居对目标节点的影响被一视同仁。GAT(Graph Attention Network)的做法是用Attention机制动态计算每个邻居的权重,权重取决于节点特征,而不是固定的图结构。

如果你要处理的是异构图或关系复杂、邻居质量差异明显的场景,GAT通常比GCN效果更好。代价是参数多、训练更慢。

学习GAT时,抓住两个点就够了:

  1. 权重计算方式:通过一个小的可学习网络,把两个节点特征拼接后映射成一个标量,再做softmax归一化;
  2. 多头注意力:多个独立的注意力头并行计算,最后拼接或取平均。

5.2 GraphSAGE:从全图聚合到采样聚合

GraphSAGE 的核心变化有三个:

  • 不再做全图聚合,而是采样邻居;
  • 聚合器可替换:Mean、LSTM、Pooling都行;
  • 使用拼接(Concat)而不是相加来融合目标节点和邻居信息,保留更完整的自身信息。

如果你的图很大,GraphSAGE 几乎是最容易实现和部署的方案。它的 Mean 聚合版本,其实就是一个去掉了对称归一化的 GCN 加上邻居采样。

5.3 GIN:更强的表达能力

GCN 的一个理论弱点是它无法区分某些同构子图,换句话说,不同结构的局部图可能得到相同表示。GIN(Graph Isomorphism Network)通过独特的“更新函数”来逼近 Weisfeiler-Lehman 图同构测试,在理论上具有更强的表达能力。

它和GCN最明显的实现差异,是给自身特征乘以一个可调整的权重 (1+epsilon):

h_v = (1+epsilon) × h_v + sum(邻居特征)

这个看似微小的改动,让模型更显式地区分自身和邻居的贡献。做图分类任务,GIN 往往比 GCN 更稳。

5.4 选型参考

模型聚合方式优点适合场景
GCN固定权重,对称归一化简单、快、适合半监督节点分类、中等规模图
GAT动态注意力权重表达能力强,适应异质性高噪声图、关系复杂场景
GraphSAGE采样+可替换聚合器可扩展到大图大规模工业图
GIN可学习权重融合理论表达能力最强图分类、分子性质预测

另外要说一句,GNN领域发展非常快,除了上述几个经典模型,还有一批专门处理长程依赖、异构图、动态图的方案。但无论模型怎么进化,“消息传递”这个核心范式基本没变。学透了GCN,再去看其他模型就是换汤不换药的体验。

6. 常见问题与排查技巧实录

这个章节是实打实的经验沉淀。我曾花了很多时间在调GCN上,整理一份问题速查表,希望能帮你省下这些时间。

6.1 模型训练不收敛

现象:loss一直在震荡或者不下降。

排查顺序:

  1. 先把归一化做对。回到 A_norm 的计算步骤,确认是用 A_aug 的度矩阵,并且是对称归一化;
  2. 减小学习率,从0.01降到0.001或0.0001,图模型对学习率敏感度很高;
  3. 检查有没有把邻接矩阵和特征矩阵放到了不同设备上,设备不一致时forward静默报错很少见,但数值会异常;
  4. 把标签改成极简二分类,用一个小子图先跑通,再回归原数据。

6.2 所有节点的预测结果几乎一样

这就是过平滑,也是最容易遇到的现象之一。先把手头模型的层数降回两层,看看是否恢复正常。如果还不行,检查是否在多个层里反复使用了同一份 A_norm,而不是基于每层节点数量重新归一化。在标准GCN中,每一层共用同一个归一化邻接矩阵是可以的,前提是聚合方式保持一致。

6.3 训练集准确率高但验证集很低

设别分两步走:先加dropout(0.5很常见),再加weight_decay。如果还不行,检查你的数据划分是不是靠得太近——很多节点之间本身连通,训练集和验证集如果共享了大量邻居,验证集准确率就会被高估,这不是模型错误,而是数据分割策略问题。

6.4 GPU显存不足

最常见做法:

  • 切换成稀疏矩阵存储邻接矩阵;
  • 缩减隐层维度和层数;
  • 改用GraphSAGE式采样训练;
  • 梯度累积,小批量训练。

6.5 感受野和准确率的关系

在GCN中,感受野和层数直接相关。如果你的图直径很大,两层GCN可能根本看不到远距离节点信息,这会制约模型的全局理解能力。分析图数据时,先算一下图的平均最短路径长度,如果它远大于模型层数,那你需要考虑加深网络或换用能建模长程依赖的GNN方案。

6.6 稀疏特征下的常见坑

当特征矩阵非常稀疏时(如one-hot编码的用户属性矩阵),GCN里的矩阵乘法会产生大量无意义的0参与计算,白白浪费算力。可以先对特征做主成分降维,降到64维或128维,再喂给模型,效果更稳。

7. 写在最后的个人体会与建议

GCN和整个GNN家族的实践门槛其实不高,数学公式就那么多,调参经验也可以通过几次实验快速积累。真正的难点在于“定性思维”的转换:不要把每个样本当成独立个体,要把它们放在图结构里看环节之间的相互作用。

我自己的学习路径和经验总结成几句送给正在入门的朋友:

  • 先手写一层GCN,用随机图跑一遍,把矩阵乘法吃透;
  • 再用真实数据集跑节点分类,感受归一化、dropout、weight decay的实际影响;
  • 熟读GAT和GraphSAGE两个变体,基本覆盖大部分工程需求;
  • 遇到陌生任务,先从最简单模型出发跑一个baseline,再逐步加复杂度。

最后再说一个经常被我推荐的小技巧:训练GCN时,把第一层输出的节点表示做T-SNE或PCA可视化,定期保存图片。你会在上面亲眼看到,随着训练进行,类簇从一团乱麻逐渐分离。这个过程能帮你建立很强的直觉,比读十篇论文都管用。图和神经网络结合的领域还在快速发展,但只要你把GCN的地基夯扎实,后续的路会顺畅很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进