GNN基础学习:从核心思想到PYG实现简单GNN

图神经网络核心数学表达

一、邻接矩阵:图的数学骨架

定义:对含 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} AijR
  • 自环:对角线非零( 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)=jAij(行或列求和)

三、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 D1A^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=Dii1jN(i)iHj 其中 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)=σ(D1A H(k1)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(k1)Rn×dk1 k − 1 k-1 k1 层输入特征
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)Rdk1×dk可学习权重矩阵
σ ( ⋅ ) \sigma(\cdot) σ()非线性激活函数(如 ReLU)

k k k 的含义

  • 表示神经网络层数/信息传播步数
  • k k k 层特征 H ( k ) H^{(k)} H(k) 聚合直接邻居在 k − 1 k-1 k1 层的特征
  • 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) jN(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(l1)
  • 目标节点 i i i 的上层特征 h i ( l − 1 ) h_i^{(l-1)} hi(l1)
  • 可选的边特征 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(l1),hj(l1),eij)

消息聚合 (Aggregation)

将节点 i i i 收到的所有邻居消息集合 { m i j ∣ j ∈ N ( i ) } \{m_{ij} | j \in N(i)\} {mijjN(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)({mijjN(i)})

节点更新 (Update)

结合聚合消息 M i ( l ) M_i^{(l)} Mi(l) 和节点自身特征 h i ( l − 1 ) h_i^{(l-1)} hi(l1),通过更新函数 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(l1),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模型

  1. 导入库
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
  1. 加载图数据
# 数据可视化函数
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=Dii1jN(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
  1. 训练并可视化
# 训练过程可视化
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如何通过聚合邻居信息学习有意义的节点表示,并最终完成节点分类任务。

进一步学习路线推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值