图神经网络核心数学表达
一、邻接矩阵:图的数学骨架
定义:对含
n
n
n 个节点的图,邻接矩阵
A
A
A 是
n
×
n
n \times n
n×n 矩阵:
A
i
j
=
{
1
节点
i
与
j
存在边连接
0
无连接
A_{ij} = \begin{cases} 1 & \text{节点 } i \text{ 与 } j \text{ 存在边连接} \\ 0 & \text{无连接} \end{cases}
Aij={10节点 i 与 j 存在边连接无连接
特性:
- 无向图:对称矩阵( A = A T A = A^T A=AT)
- 加权图:元素值表示边权重( A i j ∈ R A_{ij} \in \mathbb{R} Aij∈R)
- 自环:对角线非零( A i i ≠ 0 A_{ii} \neq 0 Aii=0)
- 有向图:非对称矩阵( A ≠ A T A \neq A^T A=AT)
- 多重图:元素值表示平行边数量
数学性质:
- 特征值谱反映图连通性(0特征值数 = 连通分量数)
- 矩阵幂 A k A^k Ak 的元素 A i j k A^k_{ij} Aijk 表示从 i i i 到 j j j 的长度为 k k k 的路径数
二、节点度:连接性的度量
基本定义:
- 度(Degree):
deg
(
v
)
=
\text{deg}(v) =
deg(v)= 与节点
v
v
v 相连的边数
邻接矩阵视角:
deg ( i ) = ∑ j A i j ( 行或列求和 ) \text{deg}(i) = \sum_j A_{ij} \quad (\text{行或列求和}) deg(i)=j∑Aij(行或列求和)
三、GNN基础公式
1. 核心思想与邻接信息处理
核心思想:GNN 的核心思想借鉴了 CNN:一个像素的特征受其周围像素影响。类似地,GNN 认为一个图节点的特征会受到其邻居节点特征的影响。邻接矩阵 A 天然地编码了节点间的连接关系。
初始聚合 (A H): 直接使用邻接矩阵 A 与节点特征矩阵 H (大小为 (节点数, 特征维度)) 相乘 (A H),可以实现对每个节点的邻居特征求和。然而,这存在两个关键问题:
- 忽略自身信息: A 的主对角线为 0(无自环),导致聚合结果完全忽略了节点自身的特征。
- 度数偏差: 度数高的节点(邻居多)聚合后特征值会显著大于度数低的节点。
解决方案:
-
引入自环
邻接矩阵 A A A 加上单位矩阵 I I I,得到 A ^ = A + I \hat{A} = A + I A^=A+I。矩阵乘法 A ^ H \hat{A}H A^H 在聚合邻居特征时同时包含节点自身特征。 -
归一化处理
为解决度数偏差问题,采用平均聚合方式:- 计算 A ^ \hat{A} A^ 的度矩阵 D D D,其中 D D D 为对角矩阵, D i i = ∑ j A ^ i j D_{ii} = \sum_j \hat{A}_{ij} Dii=∑jA^ij(即节点 i i i 的度数,含自环)。
- 归一化公式为
D
−
1
A
^
H
D^{-1}\hat{A}H
D−1A^H,对每个节点
i
i
i,其聚合结果(
A
^
H
\hat{A}H
A^H 的第
i
i
i 行)除以度数
D
i
i
D_{ii}
Dii,实现邻居(含自身)特征的均值聚合。
数学表示 - 节点
i
i
i 的归一化输出特征为:
H i ′ = 1 D i i ∑ j ∈ N ( i ) ∪ i H j H_i' = \frac{1}{D_{ii}} \sum_{j \in \mathcal{N}(i) \cup {i}} H_j Hi′=Dii1j∈N(i)∪i∑Hj 其中 N ( i ) \mathcal{N}(i) N(i) 为节点 i i i 的邻居集合。
2. 构建可训练GNN层
在归一化聚合的基础上,引入可学习的参数和非线性激活,就构成了一个基础GNN层公式:
H
(
k
)
=
σ
(
D
−
1
A
^
H
(
k
−
1
)
W
(
k
)
)
H^{(k)} = \sigma \left( D^{-1} \widehat{A} H^{(k-1)} W^{(k)} \right)
H(k)=σ(D−1A
H(k−1)W(k))
符号说明:
符号 | 含义 |
---|---|
H ( k ) ∈ R n × d k H^{(k)} \in \mathbb{R}^{n \times d_k} H(k)∈Rn×dk | 第 k k k 层节点嵌入矩阵 ( n n n: 节点数, d k d_k dk: 特征维度) |
H ( k − 1 ) ∈ R n × d k − 1 H^{(k-1)} \in \mathbb{R}^{n \times d_{k-1}} H(k−1)∈Rn×dk−1 | 第 k − 1 k-1 k−1 层输入特征 |
A ^ = A + I \widehat{A} = A + I A =A+I | 带自环的邻接矩阵 |
D D D | A ^ \widehat{A} A 的度矩阵(对角阵) |
W ( k ) ∈ R d k − 1 × d k W^{(k)} \in \mathbb{R}^{d_{k-1} \times d_k} W(k)∈Rdk−1×dk | 可学习权重矩阵 |
σ ( ⋅ ) \sigma(\cdot) σ(⋅) | 非线性激活函数(如 ReLU) |
k k k 的含义:
- 表示神经网络层数/信息传播步数
- 第 k k k 层特征 H ( k ) H^{(k)} H(k) 聚合直接邻居在 k − 1 k-1 k−1 层的特征
- K K K 层 GNN 使节点表示 H ( K ) H^{(K)} H(K) 融合其 K K K-跳( K K K-hop)邻居信息
这个公式和GCN的公式有些细微差别,但是看似优美的递推公式,确是有着复杂严谨的数学推理作为依据。我目前看了还很懵,看不懂,先搁置着吧。GCN:早期GNN谱方法和空域方法的完美结合
消息传递框架与工具实践
一、GNN的统一视角:消息传递框架
在模型实现层面,绝大多数图神经网络(GNN)遵循通用的消息传递框架。每一层 l l l 对节点 i i i 执行以下操作:
消息生成 (Message)
对每个邻居节点 j ∈ N ( i ) j \in N(i) j∈N(i),生成从 j j j 到 i i i 的消息。消息函数 M S G ( l ) MSG^{(l)} MSG(l) 的输入通常包括:
- 邻居节点 j j j 的上层特征 h j ( l − 1 ) h_j^{(l-1)} hj(l−1)
- 目标节点 i i i 的上层特征 h i ( l − 1 ) h_i^{(l-1)} hi(l−1)
- 可选的边特征 e i j e_{ij} eij
消息生成的数学表达式为:
m
i
j
=
M
S
G
(
l
)
(
h
i
(
l
−
1
)
,
h
j
(
l
−
1
)
,
e
i
j
)
m_{ij} = MSG^{(l)}(h_i^{(l-1)}, h_j^{(l-1)}, e_{ij})
mij=MSG(l)(hi(l−1),hj(l−1),eij)
消息聚合 (Aggregation)
将节点 i i i 收到的所有邻居消息集合 { m i j ∣ j ∈ N ( i ) } \{m_{ij} | j \in N(i)\} {mij∣j∈N(i)} 聚合成单一向量 M i ( l ) M_i^{(l)} Mi(l)。聚合函数 A G G ( l ) AGG^{(l)} AGG(l) 需满足排列不变性(结果与邻居顺序无关),常用操作包括:
- 求和(sum)
- 取平均(mean)
- 取最大值(max)
聚合过程的数学表达式为:
M
i
(
l
)
=
A
G
G
(
l
)
(
{
m
i
j
∣
j
∈
N
(
i
)
}
)
M_i^{(l)} = AGG^{(l)}(\{m_{ij} | j \in N(i)\})
Mi(l)=AGG(l)({mij∣j∈N(i)})
节点更新 (Update)
结合聚合消息
M
i
(
l
)
M_i^{(l)}
Mi(l) 和节点自身特征
h
i
(
l
−
1
)
h_i^{(l-1)}
hi(l−1),通过更新函数
U
P
D
A
T
E
(
l
)
UPDATE^{(l)}
UPDATE(l) 计算当前层的新特征
h
i
(
l
)
h_i^{(l)}
hi(l):
h
i
(
l
)
=
U
P
D
A
T
E
(
l
)
(
h
i
(
l
−
1
)
,
M
i
(
l
)
)
h_i^{(l)} = UPDATE^{(l)}(h_i^{(l-1)}, M_i^{(l)})
hi(l)=UPDATE(l)(hi(l−1),Mi(l))
执行流程
该过程在每一层 l = 1 , … , L l = 1, \dots, L l=1,…,L 上对所有节点并行执行一次,其中 h i ( 0 ) h_i^{(0)} hi(0) 为节点的初始输入特征。不同GNN模型(如GCN、GAT、GraphSAGE)通过设计不同的 M S G MSG MSG、 A G G AGG AGG 和 U P D A T E UPDATE UPDATE 函数实现差异化。
二、NetworkX实战:图构建与可视化
import networkx as nx
import matplotlib.pyplot as plt
# 创建空图
G = nx.Graph()
# 添加节点和边
G.add_nodes_from([1,2,3])
G.add_edges_from([(1,2), (2,3), (3,1)])
# 计算节点度
print("节点度:", dict(G.degree())) # 输出:{1:2, 2:2, 3:2}
# 可视化
nx.draw(G, with_labels=True, node_color='lightblue')
plt.savefig('graph.png')
加载数据集并可视化
# 数据可视化函数
def visualize_graph(data, title="Karate Club Network"):
G = to_networkx(data, to_undirected=True)
plt.figure(figsize=(10, 8))
pos = nx.spring_layout(G, seed=42)
nx.draw_networkx(G, pos,
node_color=data.y,
cmap=plt.cm.tab10,
node_size=300,
with_labels=True,
font_size=8)
plt.title(title)
plt.axis('off')
plt.show()
三、PYG实践简单GNN模型
- 导入库
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.utils import add_self_loops, degree
from torch_geometric.data import Data
- 加载图数据
# 数据可视化函数
def visualize_graph(data, title="Karate Club Network"):
G = to_networkx(data, to_undirected=True)
plt.figure(figsize=(10, 8))
pos = nx.spring_layout(G, seed=42)
nx.draw_networkx(G, pos,
node_color=data.y,
cmap=plt.cm.tab10,
node_size=300,
with_labels=True,
font_size=8)
plt.title(title)
plt.axis('off')
plt.show()
# 加载Karate Club数据集
dataset = KarateClub()
data = dataset[0]
# 打印数据集信息
print("="*50)
print(f"Dataset: {dataset}")
print("="*50)
print(f"Number of nodes: {data.num_nodes}")
print(f"Number of edges: {data.num_edges}")
print(f"Number of features: {data.num_features}")
print(f"Number of classes: {data.y.max().item() + 1}")
print(f"Has isolated nodes: {data.has_isolated_nodes()}")
print(f"Has self loops: {data.has_self_loops()}")
# 可视化原始图结构
visualize_graph(data, "Original Karate Club Network")
3. 定义简单GNN模型
class SimpleGNNLayer(nn.Module):
"""实现基础GNN层公式:H^{(k)} = σ(D^{-1}ÂH^{(k-1)}W^{(k)})"""
def __init__(self, in_channels, out_channels):
super().__init__()
self.linear = nn.Linear(in_channels, out_channels) # 可训练权重W
self.reset_parameters()
def reset_parameters(self):
self.linear.reset_parameters()
def forward(self, x, edge_index):
"""
x: 节点特征矩阵 [num_nodes, in_channels]
edge_index: 边索引 [2, num_edges]
"""
# 1. 添加自环 Â = A + I
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
# 2. 计算度矩阵D(含自环)
row, col = edge_index
deg = degree(col, x.size(0), dtype=x.dtype) # 节点度数向量
# 3. 归一化处理:D^{-1}Â
deg_inv = deg.pow(-1) # D^{-1}
deg_inv[deg_inv == float('inf')] = 0 # 处理度数为0的节点
# 4. 消息传递:D^{-1}ÂH
# 创建归一化因子矩阵 [num_edges, 1]
norm = deg_inv[row]
# 5. 聚合邻居信息(含自环)
out = torch.zeros_like(x)
for i in range(edge_index.size(1)):
src, dst = row[i], col[i]
out[dst] += x[src] * norm[i]
# 6. 线性变换 + 激活函数
out = self.linear(out)
return F.relu(out)
class SimpleGNN(nn.Module):
"""两层SimpleGNN模型"""
def __init__(self, num_features, num_classes):
super().__init__()
self.gnn1 = SimpleGNNLayer(num_features, 16) # 第一层:16维隐藏层
self.gnn2 = SimpleGNNLayer(16, num_classes) # 第二层:输出分类层
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.gnn1(x, edge_index)
x = F.dropout(x, p=0.5, training=self.training)
x = self.gnn2(x, edge_index)
return F.log_softmax(x, dim=1)
关键实现解析:
- 自环处理:使用 add_self_loops 函数为邻接矩阵添加自环,实现 A ^ = A + I \hat{A} = A + I A^=A+I:
edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
- 归一化处理:计算度矩阵并生成归一化系数
deg = degree(col, x.size(0), dtype=x.dtype) # 计算度矩阵 D
deg_inv = deg.pow(-1) # D^{-1}
norm = deg_inv[row] # 归一化因子
- 消息传递:
显式实现聚合公式: H i ′ = 1 D i i ∑ j ∈ N ( i ) ∪ i H j H_i' = \frac{1}{D_{ii}} \sum_{j \in \mathcal{N}(i) \cup {i}} H_j Hi′=Dii1∑j∈N(i)∪iHj
通过遍历边索引完成消息传递:
for i in range(edge_index.size(1)):
src, dst = row[i], col[i]
out[dst] += x[src] * norm[i]
- 参数学习:通过线性层实现可训练权重
W
(
k
)
W^{(k)}
W(k)```python
self.linear = nn.Linear(in_channels, out_channels) # 可训练权重 W
self.linear = nn.Linear(in_channels, out_channels) # 可训练权重 W
- 训练并可视化
# 训练过程可视化
def visualize_training(loss_history, acc_history):
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(loss_history, label='Training Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.grid(True)
plt.subplot(1, 2, 2)
plt.plot(acc_history, 'g-', label='Test Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('Test Accuracy')
plt.grid(True)
plt.tight_layout()
plt.show()
# 节点嵌入可视化
def visualize_embeddings(model, data, epoch=None):
model.eval()
with torch.no_grad():
# 获取第二层的输入(即第一层的输出)
_, embeddings = model.gnn1(data.x, data.edge_index), None
embeddings = model.gnn1(data.x, data.edge_index)
# 使用t-SNE降维
tsne = TSNE(n_components=2, random_state=42)
embeddings_2d = tsne.fit_transform(embeddings.numpy())
plt.figure(figsize=(8, 6))
scatter = plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1],
c=data.y, cmap=plt.cm.tab10, s=100)
# 添加节点标签
for i, (x, y) in enumerate(embeddings_2d):
plt.text(x, y, str(i), fontsize=8, ha='center', va='center')
plt.legend(handles=scatter.legend_elements()[0],
labels=[f"Class {i}" for i in range(4)])
plt.title(f'Node Embeddings Visualization{" (Epoch "+str(epoch)+")" if epoch else ""}')
plt.xlabel('t-SNE Dimension 1')
plt.ylabel('t-SNE Dimension 2')
plt.grid(True)
plt.show()
# 创建模型和优化器
model = SimpleGNN(num_features=dataset.num_features,
num_classes=dataset.num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 训练模型
loss_history = []
acc_history = []
for epoch in range(100):
model.train()
optimizer.zero_grad()
out = model(data)
loss = F.nll_loss(out, data.y)
loss.backward()
optimizer.step()
# 计算准确率
_, pred = out.max(dim=1)
correct = pred.eq(data.y).sum().item()
acc = correct / data.num_nodes
loss_history.append(loss.item())
acc_history.append(acc)
# 每20个epoch可视化一次嵌入
if epoch % 20 == 0:
print(f'Epoch: {epoch:03d}, Loss: {loss.item():.4f}, Acc: {acc:.4f}')
visualize_embeddings(model, data, epoch)
# 最终可视化
visualize_training(loss_history, acc_history)
visualize_embeddings(model, data, "Final")
# 打印最终预测结果
model.eval()
_, pred = model(data).max(dim=1)
print("\nFinal Predictions:")
print(pred.numpy())
Epoch 0 :
Epoch 100:
训练前后,可以明显看出相同类别的节点形成明显聚类。这个可视化演示展示了GNN如何通过聚合邻居信息学习有意义的节点表示,并最终完成节点分类任务。