一、本阶段的组队学习网站地址:datawhale
二、本期主要学习内容:
如何构造一个数据全部存于内存的数据集类(见第6-1节)
学习基于节点表征学习的图节点预测任务(见第6-2节)
学习基于节点表征学习的边预测任务的实践(见第6-2节)
三、构造一个数据全部存于内存的数据集类
对于占用内存有限的数据集,我们可以将整个数据集的数据都存储到内存里。PyG为我们提供了方便的方式来构造数据完全存于内存的数据集类(简称为InMemory数据集类)。
在PyG中,我们通过继承InMemoryDataset类来自定义一个数据可全部存储到内存的数据集类。
class InMemoryDataset(root: Optional[str] = None, transform: Optional[Callable] = None, pre_transform: Optional[Callable] = None, pre_filter: Optional[Callable] = None)
参数说明可以参考网站说明。
下面代码给出了PlanetoidPubMed数据集类的构造:
import os.path as osp
import torch
from torch_geometric.data import (InMemoryDataset, download_url)
from torch_geometric.io import read_planetoid_data
class PlanetoidPubMed(InMemoryDataset):
r""" 节点代表文章,边代表引用关系。
训练、验证和测试的划分通过二进制掩码给出。
参数:
root (string): 存储数据集的文件夹的路径
transform (callable, optional): 数据转换函数,每一次获取数据时被调用。
pre_transform (callable, optional): 数据转换函数,数据保存到文件前被调用。
"""
url = 'https://github.com/kimiyoung/planetoid/raw/master/data'
# url = 'https://gitee.com/rongqinchen/planetoid/raw/master/data'
# 如果github的链接不可用,请使用gitee的链接
def __init__(self, root, transform=None, pre_transform=None):
super(PlanetoidPubMed, self).__init__(root, transform, pre_transform)
self.data, self.slices = torch.load(self.processed_paths[0])
@property
def raw_dir(self):
return osp.join(self.root, 'raw')
@property
def processed_dir(self):
return osp.join(self.root, 'processed')
@property
def raw_file_names(self):
names = ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']
return ['ind.pubmed.{}'.format(name) for name in names]
@property
def processed_file_names(self):
return 'data.pt'
def download(self):
for name in self.raw_file_names:
download_url('{}/{}'.format(self.url, name), self.raw_dir)
def process(self):
data = read_planetoid_data(self.raw_dir, 'pubmed')
data = data if self.pre_transform is None else self.pre_transform(data)
torch.save(self.collate([data]), self.processed_paths[0])
def __repr__(self):
return '{}()'.format(self.name)
这个数据集包含三个分类任务,共19,717个结点,88,648条边,节点特征维度为500。
四、学习基于节点表征学习的图节点预测任务
基于节点表征学习的图节点预测任务在任务3中已经通过三种图神经网络进行验证(MLP、GCN和GAT)。唯一不同的地方是重定义一个GAT图神经网络,使其能够通过参数来定义GATConv的层数,以及每一层GATConv的out_channels。并且使用使用了torch_geometric.nn.Sequential容器进行序列化操作.主要代码如下:
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'))
conv_list.append(ReLU(inplace=True),)
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
五 学习基于节点表征学习的边预测任务的实践
边预测任务,目标是预测两个节点之间是否存在边。
图神经网络的构造和上一节的不同,如下所示:
import torch
from torch_geometric.nn import GCNConv
class Net(torch.nn.Module):
def __init__(self, in_channels, out_channels):
super(Net, self).__init__()
self.conv1 = GCNConv(in_channels, 128)
self.conv2 = GCNConv(128, out_channels)
def encode(self, x, edge_index):
x = self.conv1(x, edge_index)
x = x.relu()
return self.conv2(x, edge_index)
def decode(self, z, pos_edge_index, neg_edge_index):
edge_index = torch.cat([pos_edge_index, neg_edge_index], dim=-1)
return (z[edge_index[0]] * z[edge_index[1]]).sum(dim=-1)
def decode_all(self, z):
prob_adj = z @ z.t()
return (prob_adj > 0).nonzero(as_tuple=False).t()
增加了两个方法:
解码(decode),它根据边两端节点的表征生成边为真的几率(odds)。
decode_all(self, z)用于推理(inference)阶段,我们要对所有的节点对预测存在边的几率。
同时定义节点之间连接的标签:
def get_link_labels(pos_edge_index, neg_edge_index):
num_links = pos_edge_index.size(1) + neg_edge_index.size(1)
link_labels = torch.zeros(num_links, dtype=torch.float)
link_labels[:pos_edge_index.size(1)] = 1.
return link_labels
训练代码和测试代码如下:
def train(data, model, optimizer):
model.train()
neg_edge_index = negative_sampling(
edge_index=data.train_pos_edge_index,
num_nodes=data.num_nodes,
num_neg_samples=data.train_pos_edge_index.size(1))
optimizer.zero_grad()
z = model.encode(data.x, data.train_pos_edge_index)
link_logits = model.decode(z, data.train_pos_edge_index, neg_edge_index)
link_labels = get_link_labels(data.train_pos_edge_index, neg_edge_index).to(data.x.device)
loss = F.binary_cross_entropy_with_logits(link_logits, link_labels)
loss.backward()
optimizer.step()
return loss
def test(data, model):
model.eval()
z = model.encode(data.x, data.train_pos_edge_index)
results = []
for prefix in ['val', 'test']:
pos_edge_index = data[f'{prefix}_pos_edge_index']
neg_edge_index = data[f'{prefix}_neg_edge_index']
link_logits = model.decode(z, pos_edge_index, neg_edge_index)
link_probs = link_logits.sigmoid()
link_labels = get_link_labels(pos_edge_index, neg_edge_index)
results.append(roc_auc_score(link_labels.cpu(), link_probs.cpu()))
return results
五 作业
实践问题一:尝试使用PyG中的不同的网络层去代替GCNConv,以及不同的层数和不同的out_channels,来实现节点分类任务。
采用论坛提供的代码中的网络结构:
model = GAT(num_features=dataset.num_features, hidden_channels_list=[200, 100], num_classes=dataset.num_classes).to(device)
识别率为78%
经过多次尝试, 多层网络.例如:
hidden_channels_list=[200, 100, 200]
hidden_channels_list=[200, 50, 50, 100]
hidden_channels_list=[100,50, 50,100]
…
识别率都没有超过[200,100]的.
实践问题二:在边预测任务中,尝试用torch_geometric.nn.Sequential容器构造图神经网络。
主要代码不同之处在于网络的构造,如下所示:
def __init__(self, in_channels, hidden_channels_list, out_channels):
super(Net, self).__init__()
torch.manual_seed(12345)
hns = [in_channels] + hidden_channels_list
conv_list = []
for idx in range(len(hidden_channels_list)):
conv_list.append((GCNConv(hns[idx], hns[idx + 1]), 'x, edge_index -> x'))
conv_list.append(ReLU(inplace=True),)
conv_list.append((GCNConv(hns[-1], out_channels), 'x, edge_index -> x'))
self.convseq = Sequential('x, edge_index', conv_list)
def encode(self, x, edge_index):
# x = self.convseq(x, edge_index)
# x = F.dropout(x, p=0.5, training=self.training)
# x = x.relu()
return self.convseq(x, edge_index)
调用的时候,创建网络实例如下:
model = Net(dataset.num_features, [200, 100], 64).to(device)