图卷积·1

图卷积 (Graph Convolution)

 
图卷积是图神经网络(GNN)中的核心操作,它能够在图结构数据上传播和聚合节点特征信息。
  

 
图卷积通常遵循"消息传递"框架,包含三个步骤:

  消息生成:每个节点基于自身和邻居的特征生成消息
  
  消息聚合:将来自邻居的消息聚合起来
  
  特征更新:结合自身特征和聚合消息更新节点表示
    

 
$H^{(l+1)} = σ(  D̃^{-1/2} Â D̃^{-1/2}  H^{(l)} W^{(l)}  )$
    

 
```
• Â = A + I_N  (加自环)
• D̃_{ii} = ∑j Â{ij}
• W^{(l)} ∈ℝ^{F×F'} 为可学习权重
• σ(·) 为非线性激活,如 ReLU。
```
    

à = A + I

 
à = A + I (邻接矩阵加上自连接),这里所说的自连接I,是节点到自己的边吗?
    

 
是的,您理解完全正确!在图卷积网络(GCN)的公式中:

**Ã = A + I**

这里的 **I** 确实表示**单位矩阵(Identity Matrix)**,也就是您所说的"节点到自己的边"。具体来说:

1. **邻接矩阵 A**:
    - 传统的邻接矩阵 A 表示节点之间的连接关系
    - A[i,j] = 1 表示节点i和节点j之间有边
    - A[i,i] = 0(默认节点没有自环)

2. **单位矩阵 I**:
    - 对角线元素为1,其他位置为0
    - I[i,i] = 1 表示给每个节点添加一个自连接(self-loop)
    - 相当于让每个节点在信息传递时也会考虑自己的特征

3. **Ã = A + I 的作用**:
    - 确保每个节点在聚合邻居信息时也会保留自己的原始特征
    - 解决了原始邻接矩阵没有自连接的问题
    - 从数学上,这相当于在图上给每个节点添加了一个自环边

### 直观示例

假设有一个简单的3节点图:
```
0 - 1
  \ /
  2
```

原始邻接矩阵 A:
```
[[0, 1, 1],
  [1, 0, 1],
  [1, 1, 0]]
```

单位矩阵 I:
```
[[1, 0, 0],
  [0, 1, 0],
  [0, 0, 1]]
```

添加自连接后的 Ã:
```
[[1, 1, 1],
  [1, 1, 1],
  [1, 1, 1]]
```

### 为什么需要自连接?

1. **防止特征信息丢失**:没有自连接时,节点特征会在多次传播后被"稀释"
2. **数值稳定性**:帮助度矩阵 D̂ 保持良好定义(避免度为0的节点)
3. **更合理的特征更新**:让节点在更新时同时考虑邻居和自身特征

### 在PyTorch Geometric中的实现

实际上,PyTorch Geometric的GCNConv层默认已经处理了自连接,您不需要手动添加。可以通过`add_self_loops`参数控制:

```python
# 默认情况下add_self_loops=True
conv = GCNConv(in_channels, out_channels, add_self_loops=True)

# 如果原始数据已经包含自环,可以禁用
conv = GCNConv(in_channels, out_channels, add_self_loops=False)
```

### 数学意义

在GCN的传播公式中:
H⁽ˡ⁺¹⁾ = σ(D̂⁻¹/² Ã D̂⁻¹/² H⁽ˡ⁾ W⁽ˡ⁾)

添加 I 后:
- 度矩阵 D̂ 的对角线元素至少为1(因为每个节点至少有一个自连接)
- 确保了 D̂⁻¹/² 的计算不会出现除零错误
- 使归一化传播更加合理

希望这个解释清楚了自连接的概念和作用!

    

 
from torch_geometric.nn import GCNConv
import torch

class GCN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, out_channels)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index).relu()
        x = self.conv2(x, edge_index)
        return x
        
    

 
import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, num_features, hidden_dim, num_classes):
        super().__init__()
        self.conv1 = GCNConv(num_features, hidden_dim)
        self.conv2 = GCNConv(hidden_dim, num_classes)
    
    def forward(self, data):
        x, edge_index = data.x, data.edge_index
        
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, training=self.training)
        x = self.conv2(x, edge_index)
        
        return F.log_softmax(x, dim=1)
        
    

 
-----------------------------------------------------------------------------------------  
    

 


 

  

 


图池化

 
图池化用于降低图的分辨率,生成图的层次化表示,类似于CNN中的空间池化。

主要类型
节点池化:选择或聚合节点来形成较小的图
例如:TopK池化、SAG池化

全局池化:将整个图的信息聚合为单个向量
例如:全局平均池化、全局最大池化、全局注意力池化
    

 

    

 

    

TopK池化

 
from torch_geometric.nn import TopKPooling

class GraphPoolingNetwork(torch.nn.Module):
    def __init__(self, num_features, hidden_dim, ratio=0.8):
        super().__init__()
        self.conv1 = GCNConv(num_features, hidden_dim)
        self.pool1 = TopKPooling(hidden_dim, ratio=ratio)
        self.conv2 = GCNConv(hidden_dim, hidden_dim)
        self.pool2 = TopKPooling(hidden_dim, ratio=ratio)
        
    def forward(self, data):
        x, edge_index, batch = data.x, data.edge_index, data.batch
        
        x = F.relu(self.conv1(x, edge_index))
        x, edge_index, _, batch, _, _ = self.pool1(x, edge_index, None, batch)
        
        x = F.relu(self.conv2(x, edge_index))
        x, edge_index, _, batch, _, _ = self.pool2(x, edge_index, None, batch)
        
        return x, edge_index, batch
        
    

SAGPool全局平均池化

 
from torch_geometric.nn import SAGPooling, global_mean_pool
import torch.nn.functional as F

class SAGPoolNet(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, ratio=0.5):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.pool1 = SAGPooling(hidden_channels, ratio=ratio)
        self.conv2 = GCNConv(hidden_channels, hidden_channels)
        self.pool2 = SAGPooling(hidden_channels, ratio=ratio)
        self.lin = torch.nn.Linear(hidden_channels, 1)  # 二分类

    def forward(self, x, edge_index, batch):
        x = self.conv1(x, edge_index).relu()
        x, edge_index, _, batch, _, _ = self.pool1(x, edge_index, batch=batch)
        x = self.conv2(x, edge_index).relu()
        x, edge_index, _, batch, _, _ = self.pool2(x, edge_index, batch=batch)
        x = global_mean_pool(x, batch)      # 图级别表征
        x = self.lin(x)
        return x
        
    

 
from torch_geometric.nn import global_mean_pool

class GlobalPoolingNetwork(torch.nn.Module):
    def __init__(self, num_features, hidden_dim, num_classes):
        super().__init__()
        self.conv1 = GCNConv(num_features, hidden_dim)
        self.conv2 = GCNConv(hidden_dim, hidden_dim)
        self.lin = torch.nn.Linear(hidden_dim, num_classes)
        
    def forward(self, data):
        x, edge_index, batch = data.x, data.edge_index, data.batch
        
        x = F.relu(self.conv1(x, edge_index))
        x = F.relu(self.conv2(x, edge_index))
        x = global_mean_pool(x, batch)  # 全局平均池化
        
        return self.lin(x)
        

 


 
图池化:挑重要节点,top k 中ratio=0.8是从每个节点的邻域节点中选出80%重要的节点吗?

在图池化(特别是TopK池化)中,ratio=0.8的含义需要更精确地理解,它不是从每个节点的邻域中选80%的节点,而是对整个图的节点进行全局排序后保留80%的重要节点。以下是详细解释:

1. TopK池化的核心思想

TopK池化(如PyTorch Geometric中的TopKPooling)的工作原理是:

学习每个节点的重要性分数:通过一个可学习的投影向量对节点特征进行评分。

全局排序:根据分数对所有节点(整个图的节点,而非单个节点的邻居)进行排序。

保留TopK节点:按比例(如ratio=0.8)保留分数最高的节点,其余节点被丢弃。


2. ratio=0.8的具体含义

全局比例:保留图中总节点数的80%(例如,原图有100个节点,池化后剩80个)。

非局部比例:不是对每个节点的邻居单独操作。

动态调整:如果图的节点数不同(如批处理中的多个图),每个图会独立保留自身节点数的80%。


3. 与邻域节点的关系

TopK池化不考虑局部邻域结构,而是全局筛选。例如:

节点A有3个邻居,节点B有5个邻居 → 池化时不会分别保留它们邻居的80%,而是将所有节点(包括A、B及其邻居)一起排序,保留总节点数的80%。

如果需要对局部邻域操作,需使用其他池化方法(如图聚类池化或基于邻域的池化)。


 
4. 代码示例验证
以PyTorch Geometric的TopKPooling为例:

 
import torch
from torch_geometric.nn import TopKPooling

# 假设一个图有100个节点,特征维度为32
num_nodes = 100
x = torch.randn(num_nodes, 32)          # 节点特征
edge_index = torch.randint(0, num_nodes, (2, 200))  # 随机边
batch = torch.zeros(num_nodes, dtype=torch.long)    # 单图的batch

pool = TopKPooling(in_channels=32, ratio=0.8)
x_pooled, edge_index_pooled, _, batch_pooled, _, _ = pool(x, edge_index, batch=batch)

print("原始节点数:", x.shape[0])       # 输出: 100
print("池化后节点数:", x_pooled.shape[0])  # 输出: 80


原始节点数: 100
池化后节点数: 80

 
5. 如何实现"邻域内选重要节点"?
如果需要在局部邻域内筛选节点(如对每个节点的邻居操作),可以:

先分组邻居:通过edge_index找到每个节点的邻域。

局部评分:对每个邻域的节点单独计算重要性(如注意力分数)。

筛选:保留每个邻域中TopK的节点。

但这种方法需要自定义实现,PyTorch Geometric未提供直接支持。


 
ratio=0.8 在TopK池化中是全局比例,保留全图80%的节点。

不是对邻域操作,若需局部筛选需自定义逻辑。

设计图池化时需明确目标是全局压缩还是局部结构保持。

 


✅适合的场景

 
图分类任务(Graph Classification)

  目标是预测整个图的标签(如分子性质预测)。
  
  池化后保留的节点会通过全局池化(如global_mean_pool)生成图级表示,丢弃节点的信息已被聚合。
  
层次化图表示学习
  
  通过多层TopKPooling逐步压缩图结构,生成多尺度特征。
  

❌ 不适合的场景

 
节点分类任务(Node Classification)

  需要对每个节点单独预测(如社交网络用户分类)。
  
  若直接丢弃部分节点,会导致这些节点的预测结果丢失。
  

要求保留全节点信息的任务
  
  如节点回归、链接预测等。
  

 

  

 
如果任务是节点分类,以下方法比TopKPooling更合适:

直接使用图卷积-无池化

 
class GCN_NodeClassifier(torch.nn.Module):
  def __init__(self, num_features, hidden_dim, num_classes):
      super().__init__()
      self.conv1 = GCNConv(num_features, hidden_dim)
      self.conv2 = GCNConv(hidden_dim, num_classes)
  
  def forward(self, data):
      x, edge_index = data.x, data.edge_index
      x = F.relu(self.conv1(x, edge_index))
      x = self.conv2(x, edge_index)
      return F.log_softmax(x, dim=1)  # 输出每个节点的类别概率


使用跳跃连接(Skip Connection)

 
x = x + self.conv1(x, edge_index)  # 残差连接

注意力机制(如GAT)

 
通过注意力权重自动学习节点重要性,无需显式丢弃节点:
    

from torch_geometric.nn import GATConv

class GAT_NodeClassifier(torch.nn.Module):
    def __init__(self, num_features, hidden_dim, num_classes):
        super().__init__()
        self.conv1 = GATConv(num_features, hidden_dim, heads=3)
        self.conv2 = GATConv(hidden_dim*3, num_classes, heads=1) 

    

DiffPool(可选)

 
若必须压缩图结构,可用微分池化(DiffPool)生成节点簇,但需复杂实现:
from torch_geometric.nn import DiffPool

# 需要预定义聚类分配矩阵
pool = DiffPool(in_channels=hidden_dim, out_channels=cluster_dim)

    

为什么节点分类避免TopKPooling?

 
信息丢失问题
被丢弃的节点无法参与后续传播和预测。
即使保留的节点也可能因邻居被丢弃而获得不完整信息。

评估困难
测试时若某些节点被丢弃,无法计算它们的分类准确率。

任务目标冲突
节点分类需要细粒度特征,而池化旨在抽象化特征。

    

 
结论:在节点分类任务中,通常应避免直接使用TopKPooling,优先选择无池化或信息保留更强的架构。
    
聚类式池化(DiffPool、MinCutPool 等),它们会把节点聚成若干簇,再用簇中心作为超节点,从而实现层级降采样。

 

    
参考
    神经网络】神经元ReLU、Leaky ReLU、PReLU和RReLU的比较