数据完整存储与内存的数据集类+节点预测与边预测任务
数据完整存储与内存的数据集类
对于占用内存有限的数据集如Planetoid的Cora等,我们可以将整个数据集的数据都存储到内存里。PyG为我们提供了方便的方式来构造数据完全存于内存的数据集类——InMemoryDataset类。
PyG使用数据集的一般过程
主要包括以下4步:
- 从网络上下载数据原始文件。
- 每一个图样本生成一个Data 对象。
- 过滤Data 对象。
- 保存Data 对象到文件。
并不一定需要执行每一步。
InMemoryDataset基类
CLASS InMemoryDataset(root: Optional[str] = None, transform: Optional[Callable] = None, pre_transform: Optional[Callable] = None, pre_filter: Optional[Callable] = None)
root:字符串类型,存储数据集的文件夹的路径(此路径自定义)。该文件夹下有两个文件夹(raw_dir(数据集原始文件)和processed_dir(处理后的数据))。如下图。
transform :函数类型,一个数据转换函数,它接收一个Data对象并
返回一个转换后的Data对象。
pre_transform :函数类型,一个数据转换函数,它接收一个Data对
象并返回一个转换后的Data对象。
pre_filter :函数类型,一个检查数据是否要保留的函数,它接收一
个Data对象,返回此 Data对象是否应该被包含在最终的数据集中。
Planetoid例子
class Planetoid(InMemoryDataset):
url = 'https://github.com/kimiyoung/planetoid/raw/master/data'
def __init__(self, root: str, name: str, split: str = "public",
num_train_per_class: int = 20, num_val: int = 500,
num_test: int = 1000, transform: Optional[Callable] = None,
pre_transform: Optional[Callable] = None):
self.name = name
super().__init__(root, transform, pre_transform)
self.data, self.slices = torch.load(self.processed_paths[0])
self.split = split
assert self.split in ['public', 'full', 'random']
if split == 'full':
data = self.get(0)
data.train_mask.fill_(True)
data.train_mask[data.val_mask | data.test_mask] = False
self.data, self.slices = self.collate([data])
elif split == 'random':
data = self.get(0)
data.train_mask.fill_(False)
for c in range(self.num_classes):
idx = (data.y == c).nonzero(as_tuple=False).view(-1)
idx = idx[torch.randperm(idx.size(0))[:num_train_per_class]]
data.train_mask[idx] = True
remaining = (~data.train_mask).nonzero(as_tuple=False).view(-1)
remaining = remaining[torch.randperm(remaining.size(0))]
data.val_mask.fill_(False)
data.val_mask[remaining[:num_val]] = True
data.test_mask.fill_(False)
data.test_mask[remaining[num_val:num_val + num_test]] = True
self.data, self.slices = self.collate([data])
@property
def raw_dir(self) -> str:
return osp.join(self.root, self.name, 'raw')
@property
def processed_dir(self) -> str:
return osp.join(self.root, self.name, 'processed')
@property
def raw_file_names(self) -> List[str]:
names = ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']
return [f'ind.{self.name.lower()}.{name}' for name in names]
@property
def processed_file_names(self) -> str:
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, self.name)
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) -> str:
return f'{self.name}()'
通过Planetoid例子来进行分析,要继承InMemoryDataset类来构造一个我们自己的数据集类。我们需要重写以下四个函数:raw_file_names(),processed_file_names(),download()和process()。
在InMemoryDataset类中显示为:
@property
def raw_file_names(self) -> Union[str, List[str], Tuple]:
r"""The name of the files to find in the :obj:`self.raw_dir` folder in
order to skip the download."""
raise NotImplementedError
@property
def processed_file_names(self) -> Union[str, List[str], Tuple]:
r"""The name of the files to find in the :obj:`self.processed_dir`
folder in order to skip the processing."""
raise NotImplementedError
def download(self):
r"""Downloads the dataset to the :obj:`self.raw_dir` folder."""
raise NotImplementedError
def process(self):
r"""Processes the dataset to the :obj:`self.processed_dir` folder."""
raise NotImplementedError
节点预测与边预测任务
节点预测任务
节点预测任务以是节点的值为主。去继承一个以GAT为主图神经网络,需要重写__init__()和forward()函数。如下:
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
其中Sequential为PyG的。如下图:
边预测任务
边预测任务,目标是预测两个节点之间是否存在边。与节点预测任务不同的是需要重写的函数不同。需要重写以下四个函数__init__()、encode()、decode()和decode_all()函数。
encode()部分:
def encode(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
decode()部分:
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)
decode_all()部分:
def decode_all(self, z):
prob_adj = z @ z.t()
return (prob_adj > 0).nonzero(as_tuple=False).t()
在通过常规的神经网络训练的范式即可完成。
作业
问题一
尝试使用PyG中的不同的网络层去代替GCNConv,以及不同的层数和不同的out_channels ,来实现节点分类任务。
此处选择使用GCNConv来代替GATConv,隐藏层有200->100变成200->100->100->20。
原始结果:
作业结果:
问题二
在边预测任务中,尝试用torch_geometric.nn.Sequential容器构造图神经网络。
修改代码处
def __init__(self, num_features, hidden_channels_list, num_classes):
super(Net, 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(nn.ReLU(inplace=True))
self.convseq = Sequential('x, edge_index', conv_list)
self.linear = nn.Linear(hidden_channels_list[-1], num_classes)
隐藏层设置为[128,64,32,32,8]。
作业结果为:
问题三
以data.train_pos_edge_index为实际参数来进行训练集负样本采样,但这样采样得到的负样本可能包含一些验证集的正样本与测试集的正样本,即可能将真实的正样本标记为负样本,由此会产生冲突。
加循环判断:
flag = 1
while flag:
if (len(train_neg_edge_set & val_pos_edge_set) > 0) or (len(train_neg_edge_set & test_pos_edge_set) > 0):
# 训练集负样本与验证集负样本存在交集,或训练集负样本与测试集负样本存在交集
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))
else:
flag = 0
参考文献
[1] https://github.com/datawhalechina/team-learning-nlp
[2] https://pytorch-geometric.readthedocs.io/en/latest/index.html