图学习基础(GCN)
# 1 图的基础与图表示
# 1.1 图的基本概念
我们先理解图的一些基本术语:
- 图(Graph):由顶点(节点)和边组成,记为
- :节点集合(如人、物体)
- :边集合(如朋友关系、连接)
- 邻接矩阵(Adjacency Matrix):图中连接关系的矩阵表示
- 有向图 vs 无向图:边是否有方向
- 带权图:边是否有权重
# 1.2 实践:创建一个简单图并可视化
我们使用 NetworkX 和 matplotlib 创建一个简单图:
import networkx as nx
import matplotlib.pyplot as plt
# 创建一个无向图
G = nx.Graph()
# 添加节点
G.add_nodes_from([0, 1, 2, 3])
# 添加边(无向)
G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
# 可视化图
nx.draw(G, with_labels=True, node_color='lightblue', edge_color='gray')
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2 图数据的编码与特征表示
图神经网络的输入通常包括:
- 节点特征矩阵 ,每个节点一个 F 维向量
- 邻接矩阵 ,表示边的连接情况
我们可以构造一个小图并定义这些结构:
import torch
# 节点特征:4个节点,每个2维特征
X = torch.tensor([
[1.0, 0.0], # 节点 0
[0.0, 1.0], # 节点 1
[1.0, 1.0], # 节点 2
[0.0, 0.0], # 节点 3
])
# 邻接矩阵(无向图)
A = torch.tensor([
[0, 1, 0, 1], # 节点 0
[1, 0, 1, 0], # 节点 1
[0, 1, 0, 1], # 节点 2
[1, 0, 1, 0], # 节点 3
], dtype=torch.float32)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3 图神经网络核心思想 —— 消息传递(Message Passing)
在图神经网络中,每个节点会从邻居节点收集信息,并更新自身的特征表示。
# 3.1 直觉理解:
- 类似社交网络:你认识的人会影响你的“状态”。
- 每一轮传播后,节点会融合自己和邻居的信息,变得更“智能”。
# 3.2 基本图卷积网络 GCN 层(Kipf & Welling, 2017)
数学公式如下:
符号 | 含义 |
---|---|
原始邻接矩阵(表示图的连接结构) | |
单位矩阵(用于添加自环) | |
添加自环后的邻接矩阵 | |
的度矩阵(对角矩阵) | |
第 层的节点特征矩阵,初始为输入特征 | |
第 层的可学习参数(权重矩阵) | |
非线性激活函数(如 ReLU) |
# 3.3 为什么要这么做?
图神经网络的目标是:让每个节点聚合邻居的信息,更新自己的表示。
但原始邻接矩阵 有两个问题:
- 没有自环(self-loop):节点无法“看到”自己
- 不同节点度数不同:信息量不均衡,容易失衡(例如有些节点有100个邻居,有些只有1个)
所以我们做了两件事:
# 3.3.1 添加自环
这表示“每个节点也连接自己”——它就能保留自己的特征了。
# 3.3.2 邻接矩阵归一化(防止特征爆炸)
- 是 的度矩阵。每个对角线是一个节点的度数。
- 归一化是为了避免度高的节点把信息“冲淡”或“放大”太多。
这一步就像在图中做了一个“特征平滑”,让高连接节点和低连接节点影响差不多。
# 3.4 最后计算:
翻译成人话:
👉 “对每个节点,我们把它邻居的特征聚合(平均),再乘一个可学习权重,再过激活函数,得到更新后的特征。”
# 3.5 举个具体例子
假设我们有一个 4 个节点的小图,初始特征是:
H0 = X = [
[1.0, 0.0], # 节点 0
[0.0, 1.0], # 节点 1
[1.0, 1.0], # 节点 2
[0.0, 0.0], # 节点 3
]
1
2
3
4
5
6
2
3
4
5
6
邻接矩阵(+ 自环):
A_hat = [
[1, 1, 0, 1], # 节点 0 连接自己、1、3
[1, 1, 1, 0], # 节点 1 连接自己、0、2
[0, 1, 1, 1], # 节点 2 连接自己、1、3
[1, 0, 1, 1], # 节点 3 连接自己、0、2
]
1
2
3
4
5
6
2
3
4
5
6
归一化后:
这个矩阵就是你可以用来聚合邻居特征的“图卷积核”。
# 3.6 总结一句话
GCN 就是:
对每个节点,把邻居的特征平均后,加权映射(W),再过激活函数。
这种“卷积”不是图像上滑动的 kernel,而是结构上的“邻居平均 + 特征变换”。
如果你想,我可以用具体数字例子走一遍这个公式的完整计算过程(比如 2 个节点、每个特征是 2 维)。你想让我这样做吗?
# 4 手动实现一个图卷积网络 (GCN) 层(不使用框架)
我们用 PyTorch 实现这一层:
import torch
import torch.nn as nn
import torch.nn.functional as F
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, A):
# A: 邻接矩阵,X: 节点特征
# 加自环:A + I
I = torch.eye(A.size(0)).to(A.device)
A_hat = A + I
# 计算度矩阵 D
D_hat = torch.diag(torch.sum(A_hat, dim=1))
# 计算归一化邻接矩阵 D^{-1/2} A_hat D^{-1/2}
D_inv_sqrt = torch.inverse(torch.sqrt(D_hat))
A_norm = D_inv_sqrt @ A_hat @ D_inv_sqrt
# 图卷积
out = A_norm @ X
out = self.linear(out)
return F.relu(out)
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
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
# 4.1 多层 GCN:搭建一个两层的 GCN 网络
class GCN(nn.Module):
def __init__(self, in_features, hidden_dim, out_features):
super(GCN, self).__init__()
self.gcn1 = GCNLayer(in_features, hidden_dim)
self.gcn2 = GCNLayer(hidden_dim, out_features)
def forward(self, X, A):
x = self.gcn1(X, A)
x = self.gcn2(x, A)
return x
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 4.2 测试这个网络
我们继续使用前面的小图:
# 节点特征(4个节点,2维)
X = torch.tensor([
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[0.0, 0.0],
])
# 邻接矩阵
A = torch.tensor([
[0, 1, 0, 1],
[1, 0, 1, 0],
[0, 1, 0, 1],
[1, 0, 1, 0],
], dtype=torch.float32)
# 初始化模型
model = GCN(in_features=2, hidden_dim=4, out_features=2)
output = model(X, A)
print("输出节点表示:")
print(output)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输出是每个节点新的 2 维向量表示(可以用于分类、聚类等任务)。
上次更新: 2025/07/14, 21:11:30