PyG-Sequential容器-设计序列图神经网络

引言

之前我们介绍过由2层GATConv组成的神经网络,当我们需要定义一个复杂的序列图神经网络(上一层的输出将作为下一层的输入)时,可以使用torch.nn.Sequential容器将多个层连接起来。本文将先介绍torch.nn.Sequential容器,包括如何使用和相关的参数、标识;其次,将利用该容器设计一个基于GATConv的序列神经网络,并将模型用于节点分类这一下游任务。

torch.nn.Sequential容器

torch.nn.Sequential容器可用于定义顺序GNN 模型。 由于GNN运算符接受多个输入参数,torch_geometric.nn.Sequential 需要全局输入参数和单个运算符的函数头定义。 如果省略,中间模块将对其前一个模块的输出进行操作。

  1. 我们首先定义一个卷积层列表conv_list,用于存序列卷积层,其中上一卷积层的输出将作为下一卷积层的输入。加载各个卷积层。

    GATConv初始化传入的参数'x, edge_index -> x' 为函数头,比如GATConv的输入形参(x, edge_index)和返回值(x)。

  2. 然后利用Sequential('x, edge_index',conv_list)容器将卷积层列表conv_list包装起来,其中'x, edge_index'是模型传入的形参。

from torch_geometric.nn import GATConv, Sequential

feature_num=1433
num_classes=7
hidden_channels_list=[feature_num,128,64,num_classes]
for idx in range(len(hidden_channels_list)):
conv_list.append((GATConv(hns[idx],hns[idx+1],'x, edge_index -> x')))
#'x, edge_index -> x' 是函数头,比如GCNConv的输入形参和返回值
conv_list.append(ReLU(inplace=True),) #意思是是否将得到的值计算得到的值覆盖之前的值,相对于x=x.relu()

convseq=Sequential('x, edge_index',conv_list)
#'x, edge_index'是模型传入的形参

序列GAT完成节点分类任务

数据加载

数据加载有困难请参考之前的博客:图神经网络的下游任务1-利用节点特征进行节点分类 | 冬于的博客 (ifwind.github.io)

from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures

dataset = Planetoid(root='dataset/PlanetoidPubMed',transform=NormalizeFeatures())
print('dataset.num_features:', dataset.num_features)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
data = dataset[0].to(device)

设计Net

现在我们重定义一个GAT神经网络,使其能够通过参数定义GATConv的层数,以及每一层GATConvout_channels,该网络由多个GATConv顺序相连而构成。

我们的神经网络模型定义如下:

import os.path as osp

import torch
import torch.nn.functional as F
from torch_geometric.nn import GATConv, Sequential
from torch.nn import Linear, ReLU
#设计Net-GAT完成节点分类任务
class GAT(torch.nn.Module):
def __init__(self, num_features, hidden_channels_list, num_classes):
super(GAT, self).__init__()
torch.manual_seed(12345)
hns = [num_features] + hidden_channels_list
conv_list = []
for idx in range(len(hidden_channels_list)):
conv_list.append((GATConv(hns[idx], hns[idx+1]), 'x, edge_index -> x')) #'x, edge_index -> x' 是函数投,比如GCNConv的输入形参和返回值
conv_list.append(ReLU(inplace=True),)#意思是是否将得到的值计算得到的值覆盖之前的值,相对于x=x.relu()

#'x, edge_index'是模型传入的形参
self.convseq = Sequential('x, edge_index', conv_list)
self.linear = Linear(hidden_channels_list[-1], num_classes)

def forward(self, x, edge_index):
x = self.convseq(x, edge_index)
x = F.dropout(x, p=0.5, training=self.training)
x = self.linear(x)
return x

model = GAT(num_features=dataset.num_features, hidden_channels_list=[200, 100], num_classes=dataset.num_classes).to(device)

训练和测试

model=GAT(num_features=dataset.num_features,hidden_channels_list=[200,100],num_classes=dataset.num_classes).to(device)
optimizer=torch.optim.Adam(model.parameters(),lr=0.01,weight_decay=5e-4)
criterion=torch.nn.CrossEntropyLoss()

def train(model,data,optimizer,criterion):
model.train()
optimizer.zero_grad()
out=model(data.x,data.edge_index)
loss=criterion(out[data.train_mask],data.y[data.train_mask])
loss.backward()
optimizer.step()
return loss

def test(model,data):
model.eval()
out=model(data.x,data.edge_index)
pred=out.argmax(dim=1)
test_correct=pred[data.test_mask]==data.y[data.test_mask]
test_acc=int(test_correct.sum())/int(data.test_mask.sum())
return test_acc

for epoch in range(1,101):
loss=train(model,data,optimizer,criterion)
print(f'Epoch:{epoch:03d}, Loss:{loss:.4f}')

test_acc=test(model,data)
print(f'Test Accuracy:{test_acc:.4f}')

可视化

import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

def visualize(out,color):
z = TSNE(n_components=2).fit_transform(out.detach().cpu().numpy())
plt.figure(figsize=(10,10))
plt.xticks([])
plt.yticks([])

plt.scatter(z[:,0],z[:,1],s=70,c=color,cmap='Set2')
plt.show()
out=model(data.x,data.edge_index)
visualize(out,color=data.y)

实验结果

Test Accuracy: 0.7770

可视化结果:

image-20210628093259206
image-20210628093259206