pytorch如何搭建GCN有关的网络

前言:本人研究领域为交通方面,做科研需要搭建GCN有关的网络,比如GCN-GAN【1】,基于GCN的权值完成网络【2】,以及基于这些网络的新的GCN网络框架。但是搜索了一些网上使用pytorch搭建GCN网络的资料,只有github上面的无解释代码和最近几年发表的论文,有详细讲解的资料很少,这对于快速入门GCN实战,会有很大的门槛,鉴于此,经过几天的探索实战,我将自己的关于使用pytorch搭建GCN类型网络的经验分享在这里,对论文和代码如何对应做一个解释说明,对常见的关于GCN和pytorch.geometric的资料进行总结说明,并且拿出一个框架代码进行详细注释,为使用GCN实战入门提供一点帮助,也方便自己日后查阅。

一. 现有资料说明

1.github网站https://github.com/rusty1s/pytorch_geometric上面是最近几年发表的论文源码pytorch的实现:

目录examples下面是常见的论文里面提到的数据集的实验,包括根据酶结构进行图的分类,auto-encoder,诸如此类

后面我会以里面的enzymes_topk_pool.py(使用topk pool池化方式根据酶的结构进行分类,是论文Graph U-Nets提到的一种池化方式)代码为例进行详细说明

examples下面所有py代码的对应实验我也没有完全对应起来,但是都是github下面的论文里面提到的实验,在用到的时候可以自己找一找

  1. pytorch下面有一个geometric包专门用来搭建GCN网络,中文文档地址为:

https://pytorch-geometric.readthedocs.io/en/latest/index.html

有些上面提到的github中论文的源码会直接转跳到这个网站,但是这个文档的缺点是对于一些参数的说明比较难懂,很多一笔带过,不知道输出是什么,如何使用,这就需要我们将这个文档的源码和对应的论文结合起来看

这个网站中的一些方法下面都列出了对应的论文和源码

二. 代码详细说明

下面就以github里面examples下面的enzymes_topk_pool.py代码为例,结合论文和源码进行参数设置、维度变化等的说明

(1)首先是用到的包和数据加载部分,加载的数据是TUDataset里面的酶的结构数据,其中的batch_size=80表示每个批次有80张图,因为这是一个分类问题,所以对应的y值是每张图的类别值

import os.path as osp

import torch
import torch.nn.functional as F
from torch_geometric.datasets import TUDataset
from torch_geometric.data import DataLoader
from torch_geometric.nn import GraphConv, TopKPooling
from torch_geometric.nn import global_mean_pool as gap, global_max_pool as gmp

path = osp.join(osp.dirname(osp.realpath(‘file’)), ‘…’, ‘data’, ‘ENZYMES’)
dataset = TUDataset(path, name=‘ENZYMES’)
dataset = dataset.shuffle()
#print(“dataset.num_classes=”,dataset.num_classes)
n = len(dataset) // 10
test_dataset = dataset[:n]#这个表示拿出十分之一用来测试
train_dataset = dataset[n:]#后面的十分之九用来训练
test_loader = DataLoader(test_dataset, batch_size=80)#表示一个批次
train_loader = DataLoader(train_dataset, batch_size=80)
(2)DataLoader函数自动将数据分成了10份,返回的是一个迭代器,下面的代码将train_loader中的内容打印出来看看,并进行解释:

for data in train_loader:

print("data=",data)
print("data.batch=",data.batch)
print("data.batch.shape=",data.batch.shape)
print("data.x.shape=",data.x.shape)
print("data.num_features=",data.num_features)
print("\n")

这个循环的前两次循环的结果如下:

data= Batch(batch=[2468], edge_index=[2, 9540], x=[2468, 3], y=[80])
data.batch= tensor([ 0, 0, 0, …, 79, 79, 79])
data.batch.shape= torch.Size([2468])
data.x.shape= torch.Size([2468, 3])
data.num_features= 3

data= Batch(batch=[2588], edge_index=[2, 9854], x=[2588, 3], y=[80])
data.batch= tensor([ 0, 0, 0, …, 79, 79, 79])
data.batch.shape= torch.Size([2588])
data.x.shape= torch.Size([2588, 3])
data.num_features= 3
可以看出data数据里面包含至少四部分,分别为batch,维度为1*2468维,表示这个batch中的80张图一共有2468个节点,batch这个变量里面的值表示该节点对应的是80张图里面的哪张图,从后面对batch值的打印也可以看到它的值为0-79共80个值

edge_index里面存储的是邻接矩阵信息,具体如何构建的请参考链接【3】

x是输入的特征矩阵,维度为2468*3,表示一共2468个节点,每个节点共3个特征

y的维度为1*80,就表示这一batch中80张图每张图对应的分类类别

(3)最后就是整个网络的构建和训练的代码:

class Net(torch.nn.Module):
def init(self):
super(Net, self).init()

    self.conv1 = GraphConv(dataset.num_features, 128)#num_features表示节点的特征数,为3
    self.pool1 = TopKPooling(128, ratio=0.8)
    self.conv2 = GraphConv(128, 128)
    self.pool2 = TopKPooling(128, ratio=0.8)
    self.conv3 = GraphConv(128, 128)
    self.pool3 = TopKPooling(128, ratio=0.8)

    self.lin1 = torch.nn.Linear(256, 128)
    self.lin2 = torch.nn.Linear(128, 64)
    self.lin3 = torch.nn.Linear(64, dataset.num_classes)

def forward(self, data):
    x, edge_index, batch = data.x, data.edge_index, data.batch
    #print("data.batch=",data.batch)
    print("raw_x.shape=",x.shape)
    x = F.relu(self.conv1(x, edge_index))
    print("conv1_x.shape=",x.shape)
    x, edge_index, _, batch, _, _ = self.pool1(x, edge_index, None, batch)
    print("x.shape=",x.shape)
    
    x_gmp=gmp(x, batch)
    x_gap=gap(x, batch)
    print("x_gmp.shape=",x_gmp.shape)
    print("x_gap.shape=",x_gap.shape)
    x1 = torch.cat([x_gmp, x_gap], dim=1)#表示在列上合并,增加更多的列
    print("x1.shape=",x1.shape)

    x = F.relu(self.conv2(x, edge_index))
    x, edge_index, _, batch, _, _ = self.pool2(x, edge_index, None, batch)
    x2 = torch.cat([gmp(x, batch), gap(x, batch)], dim=1)
    print("x2.shape=",x2.shape)

    x = F.relu(self.conv3(x, edge_index))
    x, edge_index, _, batch, _, _ = self.pool3(x, edge_index, None, batch)
    x3 = torch.cat([gmp(x, batch), gap(x, batch)], dim=1)
    print("x3.shape=",x3.shape)

    x = x1 + x2 + x3
    print("x+.shape=",x.shape)

    x = F.relu(self.lin1(x))
    x = F.dropout(x, p=0.5, training=self.training)
    x = F.relu(self.lin2(x))
    x = F.log_softmax(self.lin3(x), dim=-1)
    
    print("final_x.shape",x.shape)
    return x

device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
model = Net().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.0005)

def train(epoch):
model.train()

loss_all = 0
for data in train_loader:
    print("raw_data.y.shape",data.y.shape)
    data = data.to(device)
    optimizer.zero_grad()
    output = model(data)
    print("data.y.shape=",data.y.shape)
    loss = F.nll_loss(output, data.y)
    loss.backward()
    loss_all += data.num_graphs * loss.item()
    optimizer.step()
    print("\n")
return loss_all / len(train_dataset)

def test(loader):
model.eval()

correct = 0
for data in loader:
    data = data.to(device)
    pred = model(data).max(dim=1)[1]
    correct += pred.eq(data.y).sum().item()
    print("\n")
return correct / len(loader.dataset)

for epoch in range(1, 2):
loss = train(epoch)
train_acc = test(train_loader)
test_acc = test(test_loader)
print(‘Epoch: {:03d}, Loss: {:.5f}, Train Acc: {:.5f}, Test Acc: {:.5f}’.
format(epoch, loss, train_acc, test_acc))
print("\n")
部分打印结果如下:

raw_data.y.shape torch.Size([80])
raw_x.shape= torch.Size([2468, 3])
conv1_x.shape= torch.Size([2468, 128])
x.shape= torch.Size([2004, 128])
x_gmp.shape= torch.Size([80, 128])
x_gap.shape= torch.Size([80, 128])
x1.shape= torch.Size([80, 256])
x2.shape= torch.Size([80, 256])
x3.shape= torch.Size([80, 256])
x+.shape= torch.Size([80, 256])
final_x.shape torch.Size([80, 6])
data.y.shape= torch.Size([80])
根据打印结果对网络结构进行分析:

先根据class Net画出整个变量的流向图:

图中数字都表示维度

可以看出主要用到GCN网络,Topk pool池化方式,global mean/max pool

GCN网络使用的是论文【4】中提到的方法,根据当前l层得到l+1层主要的公式如下:

前面的D和A是度矩阵和邻接矩阵跟单元矩阵I处理后的矩阵,整个H(l)的前半段可以看成是邻接矩阵A,这样的话需要学习的只有W这个参数矩阵。H(l)在输入层可以看成是特征矩阵X,这样上面的表达式就成了H(l+1)=AXW,假设输入的节点数为10,每个节点有3个特征,A的维度就为1010,X为103,假设GCN的参数为(3,128),那么W的维度为3128,最终经过一层GCN之后特征矩阵H的维度变成10128。因此,GCN的两个重要参数in_channel 和out_channel其实表示的是W的维度,或者说in_channel是特征数,out_channel是输出的每个节点的特征数。

因此,上图中输入的特征矩阵X(24683)经过一层GCN(3128),输出的维度为2468*128,后面经过GCN的输出维度也是按照这样的方式计算。

接着是Topk pool,这是【5】中实现的池化方式。

思想主要是下面这张图:

采用投影的方式,利用了一个可学习的投影向量p,输入的特征矩阵Xl的维度为num_nodenum_feature,p的维度为num_feature1,先是蓝色的x和黄色的p相乘,然后除以||p||,得到维度为num_node*1的y,在y中取前k个在p的投影方向上值最大的node对应的索引,得到对应的特征,然后和得到的投影值点乘,得到处理后的紫色的特征矩阵。下面就是根据索引得到新的邻接矩阵。

Topk pool(128,0.8)中的参数128的意思是输入的每个节点的特征数,0.8表示保留80%的节点

所以2468128经过Topk pool(128,0.8)之后保留本batch中80%的2004个节点,每个节点的特征数仍然为128个,输出的维度为2004128,可能有人会问24680.8为什么不是严格等于2004,这是因为这个pool是在本batch共80张图上做的,每个图的节点数乘以0.8都会取结果的上限整数,所以比24680.8=1974多几个节点。

最后是gmp/gap:global max pool/global mean pool,这两个操作是在batch层次上进行的操作,输出的维度为80*128,80表示这个批次中有80张图,128表示每个节点的特征数

以global mean pool为例,在某一张输入特征为Xn上面的操作是这样的:

就是说在每个特征维度上,取所有节点在这个特征维度上面的最大值,如果输入的维度为num_node*num_feature

那么这张图操作之后为1*num_feature

那么为什么除了进行Topk pool之外,这里还要进行global pool呢,我觉得还是跟CNN中池化的作用一样,增加稀疏连接,加入正则化,防止模型过拟合。如果没有这个global pool的操作的话功能也是可以实现的,但是模型可能容易过拟合导致性能不好,就是说这里的global pool并不是必须的。其他的功能我还没有看出来,欢迎有更进一步理解的大佬指教。

码字不易,如果有帮助到您,麻烦点个赞或者打赏一点,多少不限,谢谢!

三. 参考资料:

【1】GCN-GAN: A Non-linear Temporal Link Prediction Model for Weighted Dynamic Networks

【2】Stochastic Weight Completion for Road Networks using Graph Convolutional Networks

【3】pytorch.geometric中数据格式的构建:(后面补充)

【4】SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS

【5】Graph U-Nets

  • 3
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
GCN(Graph Convolutional Network)是一种用于图数据的卷积神经网络。在PyTorch中,你可以使用DGL(Deep Graph Library)来实现GCN。 首先,确保已经安装了DGL库。你可以使用以下命令来安装: ``` pip install dgl ``` 下面是一个简单的示例代码,展示了如何使用DGL来实现GCN: ```python import dgl import torch import torch.nn as nn import torch.nn.functional as F from dgl.nn.pytorch import GraphConv class GCN(nn.Module): def __init__(self, in_features, hidden_features, out_features): super(GCN, self).__init__() self.conv1 = GraphConv(in_features, hidden_features) self.conv2 = GraphConv(hidden_features, out_features) def forward(self, g, features): x = F.relu(self.conv1(g, features)) x = self.conv2(g, x) return x # 使用示例 num_nodes = 5 # 图中节点的数量 in_features = 10 # 输入特征的维度 hidden_features = 16 # 隐藏层特征的维度 out_features = 2 # 输出特征的维度 # 创建一个图 g = dgl.graph(([0, 1, 1, 2, 3], [1, 2, 3, 0, 4])) # 定义边的连接方式 g = dgl.add_self_loop(g) # 添加自环 # 创建输入特征张量 features = torch.randn(num_nodes, in_features) # 创建GCN模型 model = GCN(in_features, hidden_features, out_features) # 前向传播 output = model(g, features) print(output) ``` 在这个示例中,我们首先使用DGL创建一个图`g`,然后创建一个输入特征张量`features`。接下来,我们定义并创建了一个简单的GCN模型`GCN`,其中使用了`GraphConv`层来实现图卷积操作。最后,我们通过调用模型的`forward`方法来进行前向传播,得到输出结果。 需要注意的是,这只是一个简单的示例,GCN的具体结构和参数设置可以根据具体任务进行调整和改进。另外,DGL还提供了更多的图神经网络模型和操作,你可以根据需要进行进一步的学习和探索。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值