PyTorch Geometric (PyG) 是 PyTorch 的扩展库,提供用于处理图形数据的工具和构建块。它包括各种图神经网络层、数据处理实用程序和图相关操作,以简化基于 GNN 的模型的开发。
Recurrent GNNs 是一类图神经网络模型,用于处理图数据的时间动态性。它们允许在图上进行时间步长的递推操作,以便对图中的节点和边进行动态更新。
- 图神经网络(GNN):GNN 是一种设计用于处理图结构数据的神经网络。它处理以图形表示的数据,其中节点表示实体,边表示这些实体之间的关系。GNN 通过学习通过图传播信息来用于各种任务,例如节点分类、链接预测和图分类。
- GNN 模型的近似:GNN 可以具有各种架构,包括图卷积网络 (GCN)、GraphSAGE、门控图神经网络 (GGNN) 等。
- 门控图卷积(GGC):门控图卷积是 GNN 中使用的一种特定类型的图卷积层。它引入了与门控循环单元 (GRU) 类似的门控机制,以控制消息传递期间的信息流。GGC 可以帮助 GNN 建模远程依赖关系,并且对于捕获图中的复杂关系很有用。
Graph Neural Network Model公式
图神经网络(Graph Neural Network,GNN)模型的更新规则:
以下是图神经网络模型的迭代过程,其中每个节点的特征表示会根据其本地信息、邻居节点的表示以及邻居节点的本地信息进行更新。节点的最终输出可以通过将节点表示与本地信息输入到输出计算函数中来获得
x
t
t
+
1
=
f
w
(
l
v
,
l
c
o
(
v
)
,
x
n
e
(
v
)
t
,
l
n
e
(
v
)
)
O
v
t
=
g
w
(
x
v
t
,
l
v
)
x_t^{t+1}=f_w(l_v,l_{co(v)},x^t_{ne(v)},l_{ne(v)})\\ O_v^t = g_w(x_v^t,l_v)
xtt+1=fw(lv,lco(v),xne(v)t,lne(v))Ovt=gw(xvt,lv)
其中
-
x t t + 1 x_t^{t+1} xtt+1:这表示在时间步 t t t时,节点 v v v的特征表示(或状态)更新为时间步 t + 1 t+1 t+1时的新值。这是在模型中学习的主要目标,即如何更新节点的表示。
-
f w ( ⋅ ) f_w(\cdot) fw(⋅):这是一个表示节点特征更新函数的函数,它将多个输入组合在一起来计算新的节点表示。
- l v l_v lv:这是节点 v v v的本地信息,可能包括节点自身的特征或其他本地信息。
- l c o ( v ) l_{co(v)} lco(v):这是节点 v v v的协同节点(co-neighbors)的本地信息,表示与节点 v v v相连接的其他节点的信息。
- x n e ( v ) t x^t_{ne(v)} xne(v)t:这是节点 v v v的邻居节点在时间步 t t t时的表示,表示与节点 v v v直接相连的邻居节点的当前表示。
- l n e ( v ) l_{ne(v)} lne(v):这是节点 v v v的邻居节点的本地信息,表示与节点 v v v直接相连的邻居节点的信息。
函数 f w ( ⋅ ) f_w(\cdot) fw(⋅)使用这些输入来计算节点 v v v在时间步 t + 1 t+1 t+1时的新表示。
-
O v t O_v^t Ovt:这表示在时间步 t t t时,节点 v v v的输出。这通常用于表示图神经网络的最终输出,或者用于执行与图数据相关的任务,例如节点分类或图分类。
-
g w ( ⋅ ) g_w(\cdot) gw(⋅):这是一个表示输出计算函数的函数,它将多个输入组合在一起来计算节点的输出。具体来说,它接受节点 v v v在时间步 t t t时的表示 x v t x_v^t xvt以及节点 v v v的本地信息 l v l_v lv,然后计算节点 v v v在时间步 t t t时的输出 O v t O_v^t Ovt。
导入包
import os
:导入Python的os
模块,该模块用于与操作系统进行交互,包括管理环境变量和目录。import torch
:导入PyTorch库,PyTorch是一个流行的开源机器学习框架,通常用于深度学习任务,包括神经网络。os.environ['TORCH'] = torch.__version__
:将一个名为'TORCH'
的环境变量设置为当前PyTorch库的版本号(torch.__version__
)。环境变量是系统范围的变量,可以被系统中运行的进程和程序访问。
import os
import torch
os.environ['TORCH'] = torch.__version__
print(torch.__version__)
import os.path as osp # 处理文件路径
import torch
import torch.nn as nn # 神经网络模块,其中包含了构建神经网络模型的工具
import torch.nn.functional as F # 函数模块,其中包含了一些常用的神经网络函数,如激活函数和损失函数
import torch_geometric.transforms as T #变换模块,用于对图数据进行预处理和转换
import torch_geometric
from torch_geometric.datasets import Planetoid, TUDataset # 导入了Planetoid和TUDataset数据集类。这些类用于加载和处理图数据集。
from torch_geometric.data import DataLoader # 导入了DataLoader类,用于批量加载图数据
from torch_geometric.nn.inits import uniform # 初始化神经网络参数
from torch.nn import Parameter as Param # 将一个张量标记为模型的参数,以便在反向传播中进行更新
from torch import Tensor # 处理张量数据
torch.manual_seed(42) # 随机数生成器的种子,以确保代码的随机性是可重复的,即每次运行时产生相同的随机结果
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device = "cpu"
from torch_geometric.nn.conv import MessagePassing # 实现消息传递神经网络的基类
加载数据集
dataset = 'Cora'
:'Cora’是一个常用的图数据集,通常用于图神经网络的示例和实验。transform = T.Compose([T.TargetIndegree(),])
:数据预处理的转换器transform
,它是一个由torch_geometric.transforms.Compose
创建的组合转换器。- 在这个组合中,只包含一个转换器
T.TargetIndegree()
。转换器的作用是将每个节点的入度(即连接到节点的边的数量)作为目标属性,用于后续的训练任务。
- 在这个组合中,只包含一个转换器
path = osp.join('data', dataset)
:使用osp.join()
函数将字符串’data’和数据集的名称拼接在一起,以构建数据集的完整文件路径。这里假设数据集文件存储在名为’data’的文件夹中。dataset = Planetoid(path, dataset, transform=transform)
:Planetoid
是PyTorch Geometric库中的一个数据集类,专门用于加载常见的图数据集。它接受三个参数:path
:数据集文件的路径,这里传入了前面定义的path
变量。dataset
:数据集的名称,这里传入了前面定义的dataset
变量,即’Cora’。transform
:数据预处理的转换器,这里传入了前面定义的transform
变量,以便在加载数据时应用预处理。
data = dataset[0]
:从加载的数据集中选择了第一个图数据,然后将其赋值给变量data
。这个data
对象包含了图数据的各种属性,如节点特征、边信息、目标标签等,以及应用了预处理的数据。
dataset = 'Cora'
transform = T.Compose([T.TargetIndegree(),])
path = osp.join('data', dataset)
dataset = Planetoid(path, dataset, transform=transform)
data = dataset[0]
"""
Data(edge_attr=[10556, 1], edge_index=[2, 10556], test_mask=[2708], train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])
"""
其中输出:
x=[2708, 1433]
:【所有节点的特征】——图中有2708个节点。每个节点的特征维度为1433edge_index=[2, 10556]
:【边的索引,表示图中的边连接关系】- 每个边由两个元素组成,分别表示边的起始节点和目标节点
- 图中有10556个节点之间的连接关系
y=[2708]
:【目标标签】,表示每个节点的标签或类别信息- 2708个节点,每个节点都有一个与之对应的标签
train_mask=[2708]
:【训练集掩码】,用于指示哪些节点用于模型的训练- 有2708个节点,每个节点都有一个与之对应的二进制值,指示该节点是否包含在训练集中
val_mask=[2708]
:【验证集掩码】,用于指示哪些节点用于模型的验证test_mask=[2708]
:【测试集掩码】,用于指示哪些节点用于模型的测试edge_attr=[10556, 1]
:【边属性】,表示与每条边相关联的属性信息- 有10556条边,每条边都有一个与之对应的属性
- 每个边属性的维度为1,即每个边属性都是一个标量值
dataset = 'Cora'
path = osp.join('data', dataset) # 构建数据集的路径
dataset = Planetoid(path, dataset, transform=T.NormalizeFeatures()) # Planetoid是一个常用的图数据集,通常用于图分类任务。这里的transform参数指定了数据集的预处理步骤,使用了T.NormalizeFeatures()进行特征规范化。特征规范化通常是将节点特征缩放到均值为0,方差为1的标准正态分布。
data = dataset[0]
data = data.to(device) # 可以是CPU或GPU。这一步是为了确保数据在训练时与模型在同一设备上进行计算,以提高训练效率
"""
Data(edge_index=[2, 10556], test_mask=[2708], train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])
"""
MLP Model:常用的前馈神经网络架构
input_dim
:输入特征的维度,即MLP模型接受的输入数据的特征数量。hid_dims
:一个列表,包含了MLP的隐藏层的维度。例如,hid_dims=[64, 32]
表示MLP包括两个隐藏层,第一个隐藏层有64个神经元,第二个隐藏层有32个神经元。out_dim
:MLP的输出维度,通常对应于任务的输出类别数量。in_features
和out_features
分别是当前层的输入和输出维度。nn.Linear
表示每一层都是一个线性变换(仿射变换)。
class MLP(nn.Module):
def __init__(self, input_dim, hid_dims, out_dim):
super(MLP, self).__init__()
self.mlp = nn.Sequential() # Sequential容器,用于依次堆叠MLP的层次结构
dims = [input_dim] + hid_dims + [out_dim] # 创建了一个包含了MLP层维度的列表。它从输入层的维度开始,然后添加了所有隐藏层的维度,最后加上输出层的维度。这个列表用于动态地构建MLP的层。
for i in range(len(dims)-1): # 逐层构建MLP。因为最后一层是输出层,不需要添加激活函数。
self.mlp.add_module('lay_{}'.format(i),nn.Linear(in_features=dims[i], out_features=dims[i+1])) # 在Sequential容器中添加线性(全连接)层
if i+2 < len(dims): # 检查是否是最后一层。如果不是最后一层,将添加激活函数
self.mlp.add_module('act_{}'.format(i), nn.Tanh()) # 在Sequential容器中添加tanh激活函数层
def reset_parameters(self):
"""重置MLP中每一层的权重参数。它采用Xavier初始化方法,以确保权重的初始值合理"""
for i, l in enumerate(self.mlp):
if type(l) == nn.Linear:
nn.init.xavier_normal_(l.weight)
def forward(self, x):
"""MLP类的前向传播方法。它接受输入数据x,然后通过MLP模型的各个层次进行前向传播,并返回最终的输出"""
return self.mlp(x)
GNNM Model:将节点状态传播和节点状态输出结合
GNNM
类是一个自定义的图神经网络模型,它通过消息传递和状态迭代来学习节点的表示,并将最终的节点表示映射到输出层以进行分类任务。该模型可以在图数据上进行迭代传播,直到满足收敛条件,然后输出预测结果。
这是GNNM
类的构造函数,用于初始化模型的各个参数和层次结构。它接受许多参数,包括:
n_nodes
:图中节点的数量out_channels
:模型的输出维度features_dim
:节点特征的维度hid_dims
:一个列表,包含MLP模型隐藏层的维度num_layers
:最大迭代次数,用于控制模型的迭代次数。eps
:收敛的阈值aggr
:消息聚合的方式,这里默认为’add’,表示使用加法聚合bias
:是否使用偏置
将邻接节点的特征与边的权重相乘,以得到传递的消息。
x_j
表示邻接节点的特征edge_weight
表示边的权重
将邻接矩阵的转置与节点特征相乘并进行聚合
adj_t
表示邻接矩阵的转置x
表示节点特征
class GNNM(MessagePassing): # 继承基类,用于消息传递的自定义图神经网络模型
def __init__(self, n_nodes, out_channels, features_dim, hid_dims, num_layers = 50, eps=1e-3, aggr = 'add',
bias = True, **kwargs):
super(GNNM, self).__init__(aggr=aggr, **kwargs) # 调用父类的构造函数,并传递消息聚合方式和其他参数。
self.node_states = Param(torch.zeros((n_nodes, features_dim)), requires_grad=False) # 创建了一个可学习的参数node_states,表示节点的状态。这个状态是一个形状为(n_nodes, features_dim)的张量,它的梯度不会被计算。
self.out_channels = out_channels # 输出维度
self.eps = eps# 收敛阈值
self.num_layers = num_layers# 最大迭代次数
self.transition = MLP(features_dim, hid_dims, features_dim)# MLP模型用于状态迭代
self.readout = MLP(features_dim, hid_dims, out_channels)#MLP模型用于节点状态输出
self.reset_parameters()#重置MLP模型参数
print(self.transition)#
print(self.readout)
def reset_parameters(self):
"""重置MLP模型中每一层的权重采纳数,确保权重的初始值合理"""
self.transition.reset_parameters()
self.readout.reset_parameters()
def forward(self):
"""模型的前向传播方法:用于迭代传播消息,指导满足收敛条件"""
# 使用MLP模型将最终的节点状态映射到输出层
# 并进行log softmax操作,以获得最终的输出
edge_index = data.edge_index
edge_weight = data.edge_attr
node_states = self.node_states
for i in range(self.num_layers):
m = self.propagate(edge_index, x=node_states, edge_weight=edge_weight,
size=None)
new_states = self.transition(m)
with torch.no_grad():
distance = torch.norm(new_states - node_states, dim=1)
convergence = distance < self.eps
node_states = new_states
if convergence.all():
break
out = self.readout(node_states)
return F.log_softmax(out, dim=-1)
def message(self, x_j, edge_weight):
"""用于定义消息传递过程"""
return x_j if edge_weight is None else edge_weight.view(-1, 1) * x_j
def message_and_aggregate(self, adj_t, x) :
"""定义消息传递和聚合过程"""
return matmul(adj_t, x, reduce=self.aggr)
def __repr__(self):
"""返回模型的字符串表示,通常包括模型的名称和参数"""
return '{}({}, num_layers={})'.format(self.__class__.__name__,
self.out_channels,
self.num_layers)
self.transition:
MLP(
(mlp): Sequential(
(lay_0): Linear(in_features=32, out_features=64, bias=True)
(act_0): Tanh()
(lay_1): Linear(in_features=64, out_features=64, bias=True)
(act_1): Tanh()
(lay_2): Linear(in_features=64, out_features=64, bias=True)
(act_2): Tanh()
(lay_3): Linear(in_features=64, out_features=64, bias=True)
(act_3): Tanh()
(lay_4): Linear(in_features=64, out_features=64, bias=True)
(act_4): Tanh()
(lay_5): Linear(in_features=64, out_features=32, bias=True)
)
)
self.readout:
MLP(
(mlp): Sequential(
(lay_0): Linear(in_features=32, out_features=64, bias=True)
(act_0): Tanh()
(lay_1): Linear(in_features=64, out_features=64, bias=True)
(act_1): Tanh()
(lay_2): Linear(in_features=64, out_features=64, bias=True)
(act_2): Tanh()
(lay_3): Linear(in_features=64, out_features=64, bias=True)
(act_3): Tanh()
...
(lay_5): Linear(in_features=64, out_features=7, bias=True)
)
)
GNNM model:
GNNM(7, num_layers=50)
训练和验证
图神经网络训练过程,训练的目标是对节点进行分类任务
核心是一个训练循环,每个训练周期内都会执行一次前向传播、损失计算和反向传播,以更新模型参数。同时,模型在训练集、验证集和测试集上进行了准确率的评估。整个过程共进行50个训练周期。
data.num_nodes
:模型的输入节点数dataset.num_classes
:输出类别数- 隐藏层维度为32,有5个隐藏层,每层
eps
:控制收敛的参数。然后,将模型移动到设备上,可以是CPU或GPU。
model = GNNM(data.num_nodes, dataset.num_classes, 32, [64,64,64,64,64], eps=0.01).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Adam优化器,用于更新模型的参数。学习率为0.001。
loss_fn = nn.CrossEntropyLoss() # 定义了损失函数,这里使用交叉熵损失函数,用于衡量模型的预测和真实标签之间的差异。
"""切分数据集"""
test_dataset = dataset[:len(dataset) // 10]
train_dataset = dataset[len(dataset) // 10:]
"""数据集的加载器,用于批量加载训练数据"""
test_loader = DataLoader(test_dataset)
train_loader = DataLoader(train_dataset)
def train():
"""模型训练"""
model.train() # 训练模式
optimizer.zero_grad() # 梯度清零
loss_fn(model()[data.train_mask], data.y[data.train_mask]).backward() #计算损失,并反向传播
optimizer.step()# 更新模型参数
def test():
"""模型测试"""
model.eval()
logits, accs = model(), []
for _, mask in data('train_mask', 'val_mask', 'test_mask'):
pred = logits[mask].max(1)[1]
acc = pred.eq(data.y[mask]).sum().item() / mask.sum().item()
accs.append(acc)
return accs
for epoch in range(1, 51):
train()
accs = test()
train_acc = accs[0]
val_acc = accs[1]
test_acc = accs[2]
# 打印每个训练周期的训练准确率、验证准确率和测试准确率
print('Epoch: {:03d}, Train Acc: {:.5f}, '
'Val Acc: {:.5f}, Test Acc: {:.5f}'.format(epoch, train_acc,
val_acc, test_acc))
Epoch: 001, Train Acc: 0.17857, Val Acc: 0.20400, Test Acc: 0.18300
Epoch: 002, Train Acc: 0.14286, Val Acc: 0.16400, Test Acc: 0.14500
Epoch: 003, Train Acc: 0.19286, Val Acc: 0.13800, Test Acc: 0.11200
…
Epoch: 047, Train Acc: 0.22857, Val Acc: 0.18400, Test Acc: 0.13800
Epoch: 048, Train Acc: 0.24286, Val Acc: 0.16200, Test Acc: 0.14900
Epoch: 049, Train Acc: 0.25000, Val Acc: 0.16800, Test Acc: 0.15000
Epoch: 050, Train Acc: 0.23571, Val Acc: 0.18200, Test Acc: 0.14700
Gated Graph Neural Network
定义了一个包含 Gated Graph Convolutional Layer(GGCL)的 GGNN 模型,用于图分类任务。 GGCL 具有多层的消息传递和 GRU 单元,而 GGNN 模型使用 GGCL 从输入数据中提取特征,并将其映射到类别分数。
初始化函数,用于定义图卷积层的参数和属性:
out_channels
:输出特征的维度。num_layers
:图卷积层的层数。aggr
:聚合函数的类型,这里默认为’add’,表示采用加法聚合。bias
:是否使用偏置项,默认为 True。
GGNN的初始化函数,用于定义模型的结构。它包含以下两个子模块:
conv
:一个GatedGraphConv
图卷积层,输入特征维度为 1433,层数为 3。mlp
:一个多层感知机,用于将图卷积层的输出映射到类别分数。
class GatedGraphConv(MessagePassing):
def __init__(self, out_channels, num_layers, aggr = 'add',
bias = True, **kwargs):
super(GatedGraphConv, self).__init__(aggr=aggr, **kwargs)
self.out_channels = out_channels
self.num_layers = num_layers
self.weight = Param(Tensor(num_layers, out_channels, out_channels))
self.rnn = torch.nn.GRUCell(out_channels, out_channels, bias=bias)
self.reset_parameters()
def reset_parameters(self):
"""用于重置模型参数的函数,包括权重和 GRU 单元的参数"""
uniform(self.out_channels, self.weight)
self.rnn.reset_parameters()
def forward(self, data):
"""前向传播函数,用于执行图卷积操作"""
# 它接受一个名为 data 的输入参数,其中包含了节点特征 x、边的索引 edge_index 和边属性 edge_weight。在每个图卷积层中
# 计算节点特征的线性变换,并传播消息到相邻节点。
# 使用 GRU 单元来更新节点特征。
# 返回最终的节点特征。
x = data.x
edge_index = data.edge_index
edge_weight = data.edge_attr
if x.size(-1) > self.out_channels:
raise ValueError('The number of input channels is not allowed to '
'be larger than the number of output channels')
if x.size(-1) < self.out_channels:
zero = x.new_zeros(x.size(0), self.out_channels - x.size(-1))
x = torch.cat([x, zero], dim=1)
for i in range(self.num_layers):
m = torch.matmul(x, self.weight[i])
m = self.propagate(edge_index, x=m, edge_weight=edge_weight,
size=None)
x = self.rnn(m, x)
return x
def message(self, x_j, edge_weight):
"""用于定义消息传递过程。消息传递采用简单的线性变换和乘法聚合。"""
return x_j if edge_weight is None else edge_weight.view(-1, 1) * x_j
def message_and_aggregate(self, adj_t, x):
return matmul(adj_t, x, reduce=self.aggr)
def __repr__(self):
"""返回图卷积层的字符串表示形式,包括输出特征的维度和层数"""
return '{}({}, num_layers={})'.format(self.__class__.__name__,
self.out_channels,
self.num_layers)
class GGNN(torch.nn.Module):
def __init__(self):
super(GGNN, self).__init__()
self.conv = GatedGraphConv(1433, 3)
self.mlp = MLP(1433, [32,32,32], dataset.num_classes)
def forward(self):
"""前向传播函数,用于执行模型的前向计算"""
x = self.conv(data)# 通过 conv 图卷积层处理输入数据 data
x = self.mlp(x)# 将结果传递给 mlp 输出层
return F.log_softmax(x, dim=-1)# 模型使用 log-softmax 函数来计算类别分数
GGNN(
(conv): GatedGraphConv(1433, num_layers=3)
(mlp): MLP(
(mlp): Sequential(
(lay_0): Linear(in_features=1433, out_features=32, bias=True)
(act_0): Tanh()
(lay_1): Linear(in_features=32, out_features=32, bias=True)
(act_1): Tanh()
(lay_2): Linear(in_features=32, out_features=32, bias=True)
(act_2): Tanh()
(lay_3): Linear(in_features=32, out_features=7, bias=True)
)
)
)
model = GGNN().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
loss_fn = nn.CrossEntropyLoss()
test_dataset = dataset[:len(dataset) // 10]
train_dataset = dataset[len(dataset) // 10:]
test_loader = DataLoader(test_dataset)
train_loader = DataLoader(train_dataset)
def train():
model.train()
optimizer.zero_grad()
loss_fn(model()[data.train_mask], data.y[data.train_mask]).backward()
optimizer.step()
def test():
model.eval()
logits, accs = model(), []
for _, mask in data('train_mask', 'val_mask', 'test_mask'):
pred = logits[mask].max(1)[1]
acc = pred.eq(data.y[mask]).sum().item() / mask.sum().item()
accs.append(acc)
return accs
for epoch in range(1, 51):
train()
accs = test()
train_acc = accs[0]
val_acc = accs[1]
test_acc = accs[2]
print('Epoch: {:03d}, Train Acc: {:.5f}, '
'Val Acc: {:.5f}, Test Acc: {:.5f}'.format(epoch, train_acc,
val_acc, test_acc))
Epoch: 001, Train Acc: 0.15000, Val Acc: 0.13600, Test Acc: 0.13100
Epoch: 002, Train Acc: 0.17857, Val Acc: 0.25400, Test Acc: 0.23500
Epoch: 003, Train Acc: 0.18571, Val Acc: 0.13400, Test Acc: 0.13800
Epoch: 004, Train Acc: 0.29286, Val Acc: 0.22000, Test Acc: 0.22500
…
Epoch: 048, Train Acc: 0.91429, Val Acc: 0.62600, Test Acc: 0.63700
Epoch: 049, Train Acc: 0.93571, Val Acc: 0.62600, Test Acc: 0.62700
Epoch: 050, Train Acc: 0.92857, Val Acc: 0.62400, Test Acc: 0.62100