在社交网络分析、推荐系统、分子结构预测和知识图谱等场景中,数据天然以图(Graph)的形式存在。传统的卷积神经网络(CNN)擅长处理规则网格结构的数据(如图像),但对非欧几里得结构的图数据却力不从心。图神经网络(Graph Neural Network, GNN)应运而生,通过消息传递机制在图结构上学习节点表示,已成为处理图数据的核心深度学习技术。本文将从图数据的基本概念出发,深入讲解GCN和GAT两种经典图神经网络的数学原理,并提供完整的PyTorch实现代码。

一、图数据的基础表示
在深入图神经网络之前,我们需要先理解图数据的数学表示。一个图 G = (V, E) 由节点集合 V 和边集合 E 组成。在机器学习任务中,我们通常需要以下三个核心矩阵来描述一个图:
- 邻接矩阵 A:大小为 N×N(N 为节点数),A[i][j] = 1 表示节点 i 与节点 j 之间存在边,否则为 0。对于无向图,A 是对称矩阵。
- 节点特征矩阵 X:大小为 N×F(F 为每个节点的特征维度),每行代表一个节点的特征向量。
- 度矩阵 D:对角矩阵,D[i][i] 表示节点 i 的度(即与该节点相连的边数)。
下面用Python和PyTorch来构建一个简单的图数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
# 构建一个简单的5节点无向图
# 边连接: (0,1), (0,2), (1,3), (2,3), (3,4)
num_nodes = 5
num_features = 8
num_classes = 3
# 邻接矩阵
adj = torch.zeros(num_nodes, num_nodes)
edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
for i, j in edges:
adj[i][j] = 1.0
adj[j][i] = 1.0 # 无向图对称
# 加自环(每个节点与自身相连)
adj_self = adj + torch.eye(num_nodes)
# 节点特征矩阵(随机初始化作为示例)
X = torch.randn(num_nodes, num_features)
# 度矩阵
degree = adj_self.sum(dim=1, keepdim=False)
print(f"邻接矩阵:\n{adj_self}")
print(f"节点度: {degree}")
print(f"特征矩阵形状: {X.shape}")
在实际应用中,图数据通常来自真实场景。例如在引文网络 Cora 数据集中,节点是论文,边是引用关系,节点特征是词袋向量。在社交网络中,节点是用户,边是好友关系,节点特征可以是用户的画像信息。理解这些基础表示是后续理解图卷积操作的关键。
二、图卷积网络(GCN)原理与实现
图卷积网络(Graph Convolutional Network, GCN)由 Kipf 和 Welling 在 2017 年提出,是图神经网络领域最具里程碑意义的工作之一。GCN 的核心思想是通过邻接矩阵传播节点特征,使每个节点聚合邻居的信息来更新自身表示。
2.1 GCN 的数学推导
单层GCN的前向传播公式为:
1 H^(l+1) = σ(D̂^(-1/2) · Â · D̂^(-1/2) · H^(l) · W^(l))
其中各符号的含义如下:
- Â = A + I:邻接矩阵加上自环,确保每个节点在聚合时包含自身特征。
- D̂:Â 的度矩阵,即 D̂[i][i] = Σ_j Â[i][j]。
- D̂^(-1/2) · Â · D̂^(-1/2):对称归一化操作,消除节点度数差异带来的尺度问题,使高度节点和低度节点的特征贡献保持均衡。
- H^(l):第 l 层的节点特征矩阵,H^(0) = X。
- W^(l):第 l 层的可训练权重矩阵。
- σ:非线性激活函数(通常用 ReLU)。
这个归一化操作的本质是将邻居特征按邻居度数进行加权平均,避免度数大的节点主导聚合结果。直觉上理解:一个社交达人(度数高)的每一条连接信息权重较低,而一个小圈子中的连接信息权重较高,这与人类社交中的信息信任传递逻辑一致。
2.2 GCN 的 PyTorch 实现
下面是完整的 GCN 层和 GCN 模型实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 class GCNLayer(nn.Module):
"""单层图卷积"""
def __init__(self, in_features, out_features, bias=True):
super(GCNLayer, self).__init__()
self.linear = nn.Linear(in_features, out_features, bias=bias)
def forward(self, X, A_norm):
"""
X: 节点特征矩阵 [N, F_in]
A_norm: 归一化邻接矩阵 [N, N] (D̂^(-1/2)·Â·D̂^(-1/2))
"""
# 先做线性变换,再做图卷积传播
support = self.linear(X) # [N, F_out]
output = torch.matmul(A_norm, support) # [N, F_out]
return output
class GCN(nn.Module):
"""两层GCN模型用于节点分类"""
def __init__(self, in_features, hidden_features, num_classes, dropout=0.5):
super(GCN, self).__init__()
self.gc1 = GCNLayer(in_features, hidden_features)
self.gc2 = GCNLayer(hidden_features, num_classes)
self.dropout = dropout
def forward(self, X, A_norm):
# 第一层: GCN + ReLU
H = F.relu(self.gc1(X, A_norm))
H = F.dropout(H, self.dropout, training=self.training)
# 第二层: GCN (输出层,不加激活)
output = self.gc2(H, A_norm)
return F.log_softmax(output, dim=1)
# 构建归一化邻接矩阵
def normalize_adj(adj):
"""计算 D̂^(-1/2) · Â · D̂^(-1/2)"""
A_hat = adj + torch.eye(adj.size(0))
D_hat = torch.diag(torch.pow(A_hat.sum(dim=1), -0.5))
A_norm = torch.matmul(torch.matmul(D_hat, A_hat), D_hat)
return A_norm
# 使用示例
A_norm = normalize_adj(adj)
model = GCN(num_features, 16, num_classes, dropout=0.5)
output = model(X, A_norm)
print(f"GCN输出形状: {output.shape}") # [5, 3]
可以看到,GCN 的实现非常简洁。核心代码仅涉及矩阵乘法和线性变换。但其中归一化邻接矩阵的构建是关键步骤——如果跳过归一化直接使用 Â · H,模型将难以收敛,因为不同节点的特征量级差异过大会导致梯度不稳定。
三、图注意力网络(GAT)原理与实现
图注意力网络(Graph Attention Network, GAT)由 Veličković 等人在 2018 年提出,通过引入注意力机制替代GCN中的固定归一化权重,使模型能够自动学习邻居节点之间的重要性权重。GAT 相比 GCN 更灵活,在异构图和噪声边较多的场景中表现更优。

3.1 GAT 的注意力计算
GAT 的核心是为每条边计算一个注意力系数,步骤如下:
第一步:计算未归一化的注意力分数。对每个节点对 (i, j),将它们的特征拼接后通过一个可训练的注意力向量 a 计算注意力分数:
1 e_ij = LeakyReLU(a^T · [W·h_i || W·h_j])
其中 W 是共享的线性变换矩阵,|| 表示向量拼接。只有当节点 i 和 j 之间有边时才计算 e_ij。
第二步:归一化注意力系数。使用 softmax 对节点 i 的所有邻居的注意力分数进行归一化:
1 α_ij = softmax_j(e_ij) = exp(e_ij) / Σ_{k∈N(i)} exp(e_ik)
第三步:加权聚合。使用归一化后的注意力系数对邻居特征加权求和:
1 h_i' = σ(Σ_{j∈N(i)} α_ij · W · h_j)
多头注意力(Multi-Head Attention)是 GAT 的另一个重要特性。通过运行 K 个独立的注意力头,将结果拼接或取平均,增强模型的表示能力:
1
2 h_i' = ||_{k=1}^{K} σ(Σ_{j∈N(i)} α_ij^k · W^k · h_j) # 拼接(中间层)
h_i' = σ(1/K Σ_{k=1}^{K} Σ_{j∈N(i)} α_ij^k · W^k · h_j) # 平均(输出层)
3.2 GAT 的 PyTorch 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 class GATLayer(nn.Module):
"""单层图注意力"""
def __init__(self, in_features, out_features, num_heads=4,
concat=True, dropout=0.6, leaky_relu_slope=0.2):
super(GATLayer, self).__init__()
self.num_heads = num_heads
self.out_features = out_features
self.concat = concat
self.dropout = dropout
# 每个注意力头独立的W和a
self.W = nn.Parameter(
torch.Tensor(num_heads, in_features, out_features)
)
self.a = nn.Parameter(
torch.Tensor(num_heads, 2 * out_features, 1)
)
self.leaky_relu = nn.LeakyReLU(leaky_relu_slope)
nn.init.xavier_uniform_(self.W)
nn.init.xavier_uniform_(self.a)
def forward(self, X, adj):
N = X.size(0)
# 线性变换: [N, F_in] -> [K, N, F_out]
h = torch.einsum('nf,kfo->kno', X, self.W)
# 计算注意力系数
# 为每对节点拼接特征: [K, N, N, 2*F_out]
h_i = h.unsqueeze(2).repeat(1, 1, N, 1) # [K, N, N, F_out]
h_j = h.unsqueeze(1).repeat(1, N, 1, 1) # [K, N, N, F_out]
concat_h = torch.cat([h_i, h_j], dim=-1) # [K, N, N, 2*F_out]
# e_ij = LeakyReLU(a^T · [Wh_i || Wh_j])
e = self.leaky_relu(
torch.einsum('knmo,mop->knop', concat_h, self.a).squeeze(-1)
) # [K, N, N]
# 使用邻接矩阵mask: 只保留有边的节点对
adj_mask = adj.unsqueeze(0).expand(self.num_heads, N, N)
e = e.masked_fill(adj_mask == 0, float('-inf'))
# Softmax归一化
alpha = F.softmax(e, dim=2) # [K, N, N]
alpha = F.dropout(alpha, self.dropout, training=self.training)
# 加权聚合: h_i' = Σ_j α_ij * h_j
out = torch.einsum('knm,kmo->kno', alpha, h) # [K, N, F_out]
if self.concat:
# 多头拼接: [N, K*F_out]
out = out.permute(1, 0, 2).reshape(N, -1)
else:
# 多头平均: [N, F_out]
out = out.mean(dim=0)
return out
class GAT(nn.Module):
"""两层GAT模型用于节点分类"""
def __init__(self, in_features, hidden_features, num_classes,
num_heads=4, dropout=0.6):
super(GAT, self).__init__()
self.gat1 = GATLayer(in_features, hidden_features,
num_heads=num_heads, concat=True,
dropout=dropout)
# 输出层用1个注意力头,concat=False
self.gat2 = GATLayer(hidden_features * num_heads,
num_classes, num_heads=1,
concat=False, dropout=dropout)
self.dropout = dropout
def forward(self, X, adj):
h = F.dropout(X, self.dropout, training=self.training)
h = F.elu(self.gat1(h, adj))
h = F.dropout(h, self.dropout, training=self.training)
h = self.gat2(h, adj)
return F.log_softmax(h, dim=1)
# 使用示例
gat_model = GAT(num_features, 8, num_classes, num_heads=4, dropout=0.6)
out = gat_model(X, adj)
print(f"GAT输出形状: {out.shape}") # [5, 3]
GAT的实现比GCN复杂不少,核心在于注意力系数的计算和邻接矩阵mask的使用。mask机制确保只有存在边的节点对才参与注意力计算,不存在边的节点对注意力分数被设为负无穷,经过softmax后权重为0。这一设计使GAT能自适应地学习不同邻居的重要性,而非像GCN那样使用固定的度数归一化。
四、实战:Cora引文网络节点分类
为了展示GCN和GAT在真实数据上的表现,我们使用经典的Cora数据集进行节点分类任务。Cora数据集包含2708篇论文,分为7个类别,5429条引用边,每个论文用1433维词袋特征表示。这是一个标准的图节点分类基准。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 import torch.optim as optim
# 模拟Cora格式的数据加载(实际使用可用PyG的Planetoid)
# 这里用我们之前构建的小图做完整训练流程演示
def train_node_classification(model, X, A_norm, adj, labels,
train_mask, val_mask, test_mask,
lr=0.01, weight_decay=5e-4,
epochs=200):
optimizer = optim.Adam(model.parameters(), lr=lr,
weight_decay=weight_decay)
best_val_acc = 0
best_test_acc = 0
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
output = model(X, A_norm)
loss = F.nll_loss(output[train_mask], labels[train_mask])
loss.backward()
optimizer.step()
# 验证
model.eval()
with torch.no_grad():
preds = model(X, A_norm).argmax(dim=1)
val_acc = (preds[val_mask] == labels[val_mask]).float().mean()
test_acc = (preds[test_mask] == labels[test_mask]).float().mean()
if val_acc > best_val_acc:
best_val_acc = val_acc
best_test_acc = test_acc
if (epoch + 1) % 20 == 0:
print(f'Epoch {epoch+1:3d} | Loss: {loss:.4f} | '
f'Val Acc: {val_acc:.4f} | Test Acc: {test_acc:.4f}')
print(f'\nBest Test Acc: {best_test_acc:.4f}')
return best_test_acc
# 生成模拟标签和mask
labels = torch.randint(0, num_classes, (num_nodes,))
train_mask = torch.tensor([True, True, False, True, False])
val_mask = torch.tensor([False, False, True, False, True])
test_mask = torch.tensor([False, False, True, False, True])
print("=== GCN 训练 ===")
gcn_model = GCN(num_features, 16, num_classes, dropout=0.5)
train_node_classification(gcn_model, X, A_norm, adj, labels,
train_mask, val_mask, test_mask)
print("\n=== GAT 训练 ===")
gat_model = GAT(num_features, 8, num_classes, num_heads=4, dropout=0.6)
# GAT使用原始adj(非归一化),注意力机制自行处理权重
train_node_classification(gat_model, X, adj, adj, labels,
train_mask, val_mask, test_mask)
在Cora数据集上,标准GCN通常能达到81-82%的测试准确率,而GAT可达83%左右。两者各有优势,并非绝对谁优谁劣。在图结构噪声较大或邻居重要性差异显著的场景中,GAT的注意力机制更具优势;而在图结构清晰、计算资源有限的场景中,GCN更高效。
五、GCN与GAT对比与选型建议
下表对GCN和GAT的核心特性进行了对比:
| 特性 | GCN | GAT |
|---|---|---|
| 邻居权重 | 固定(度数归一化) | 可学习(注意力机制) |
| 参数量 | 较少(仅线性层权重) | 较多(W + 注意力向量a) |
| 计算复杂度 | O(E·F),E为边数 | O(N²·F),需计算所有节点对 |
| 可解释性 | 较低(权重隐含在归一化中) | 较高(注意力系数可视化) |
| 对噪声边鲁棒性 | 一般(固定权重受噪声影响) | 较强(可降低噪声边权重) |
| 适用场景 | 同质图、大规模图 | 异质图、需要可解释性 |
选型建议如下:
- 图规模较大(百万级节点以上):优先选择GCN或其采样变体(如GraphSAGE),因为GAT的 O(N²) 复杂度在大图上计算成本过高。
- 需要分析邻居重要性:选择GAT,注意力系数可以直接可视化,帮助理解模型决策依据。
- 图结构存在噪声边:选择GAT,注意力机制能自适应降低噪声边的权重。
- 快速原型验证:选择GCN,实现简单、训练速度快、调参容易。
- 需要更强表达能力:可尝试GAT或两者的组合,先GCN提取低频特征,再GAT做精细化聚合。
六、进阶方向与实践建议
掌握GCN和GAT后,可以进一步探索以下进阶方向:
GraphSAGE:归纳式学习
GCN和GAT都是直推式(transductive)学习,即在训练时能看到整个图。GraphSAGE引入了归纳式(inductive)学习,通过对邻居进行采样并聚合,使模型能泛化到训练时未见过的节点和图。这在实际部署中非常重要,因为新节点会不断加入图。
消息传递神经网络(MPNN)框架
MPNN是一个统一框架,将GCN、GAT、GraphSAGE等模型抽象为消息传递过程:消息函数→聚合函数→更新函数。理解MPNN框架后,你可以设计自己的图神经网络变体。
异构图神经网络
真实图通常包含多种节点类型和边类型(如知识图谱中的实体-关系-实体)。R-GCN、HGT等异构图神经网络通过为不同关系类型设计独立的变换矩阵,能更好地建模异构图的复杂结构。
实践建议
- 推荐使用PyTorch Geometric(PyG)或DGL框架,它们内置了GCN、GAT等经典模型和高效的数据加载器,能处理大规模图。
- 在训练图神经网络时,注意过拟合问题。图数据的训练样本数等于节点数,往往远少于参数量,需要使用Dropout、权重衰减和Early Stopping等正则化手段。
- 特征预处理很重要。对节点特征做标准化或归一化能显著提升模型收敛速度和最终性能。
- 评估图模型时,关注不同类别的准确率分布。图数据中类别不平衡问题常见,仅看整体准确率可能掩盖模型在小类别上的低性能。
总结
本文从图数据的基础表示出发,详细讲解了GCN和GAT两种经典图神经网络的数学原理和PyTorch实现。GCN通过对称归一化邻接矩阵实现高效的邻居特征聚合,适合大规模同质图;GAT通过可学习的注意力机制自适应地分配邻居权重,在异质图和需要可解释性的场景中表现更优。理解这两种模型的核心机制——归一化传播和注意力聚合——是掌握图神经网络的关键。在实际项目中,建议根据图规模、噪声水平和可解释性需求灵活选择,并借助PyTorch Geometric等框架加速开发。图神经网络领域仍在快速发展,GraphSAGE、GraphSAGE、异构图网络等进阶方法值得持续关注和学习。
汤不热吧