|
图卷积 (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的比较