1 图数据结构
图说起来也很简单,就是两个核心点,一个是图节点(nodes/vertics),一个是边(edges/links)表示节点之间的连接关系
总体而言,图可以是不规整的(irregular),对比而言,平时我们看到的图片都是规整的(regular),可以表示成矩阵或者向量。问题在于设计数据结构如何储存图,一般有两种方案
矩阵表示
又细分成两种
用邻接矩阵(adjacency matrix),度矩阵(degree matrix), 拉普拉斯矩阵(Laplacian matrix)去表示, 衍生出各种对拉普拉斯矩阵的操作,比如图傅里叶变换(graph fourier transform), 也有稀疏邻接矩阵
用关联矩阵(incidence matrix)表示,行表示节点,列表示边, 和这个相关的例如:超图(hypergraph)
这(两)种方式的缺点在于使用内存大, 矩阵维度和节点数目N挂钩。但是图的连接常常是稀疏的(sparse),也就是邻接矩阵中很多元素都是0(两个node没有连接关系),这些0元素会占据大量存储空间,使效率很低下。尤其是大型网络图,都不会把图完整的表示成一个矩阵。
邻接表
邻接表(Adjacency list),也是数据领域常用的存储图方式,比如将边表示成节点对,成为一个2*N_edges的matrix,第一行表示source node, 第二行表示target node。这样的好处在于可以只储存有边存在的,对稀疏结构友好。总体而言,如果图是dense的,可以考虑矩阵表示,如果是稀疏的,最好使用稀疏邻接矩阵或邻接表
2 数据Data
Data的内容
pytorch Geometric Data使用邻接表去表示图,同时也表示了node特征x, 边属性edge_attr等, 需要注意的是, Data只表示一张图(single graph)
1
| Data(x=None, edge_index=None, edge_attr=None, y=None)
|
一个Graph本质是torch_geometric.data.Data的实例,它包括以下几个常见对象(属性,attributes):
- data.x:节点的特征矩阵,形状为[num_nodes,num_node_features]
- data.edge_index:图的边索引,用COO稀疏矩阵格式保存,形状为[2,num_edgs],数据类型为torch.long;
- data.edge_attr:边的特征矩阵,形状为[num_edges,num_edge_features];
- data.y:计算损失所需的目标数据,target,针对训练的目标可能有不同的形状,比如节点级别的形状为[num_nodes,],或者图级别的形状为[1,];
1 2 3 4 5 6 7 8 9 10 11 12 13
| import torch from torch_geometric.data import Data
edge_index = torch.tensor([[0, 1, 1, 2], [1, 0, 2, 1]], dtype=torch.long) x = torch.tensor([[-1], [0], [1]], dtype=torch.float)
data = Data(x=x, edge_index=edge_index) print(data)
device=torch.device("cuda") data=data.to(device)
|
1 2 3 4 5 6
| data.num_nodes data.num_edges data.num_node_features data.has_isolated_nodes() data.has_self_loops() data.is_directed()
|
3 数据集Dataset
Dataset实例
我们可以看到数据集中的第一个图包含 37 个节点,每个节点有 3 个特征。 有 168/2 = 84 条无向边,并且该图恰好分配给一个类。 此外,数据对象正好持有一个图级别目标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from torch_geometric.datasets import TUDataset
dataset = TUDataset(root='/home/ykl/ENZYMES', name='ENZYMES')
print(len(dataset)) print(dataset.num_classes)
print(dataset.num_node_labels) print(dataset.num_node_features) print(dataset.num_node_attributes)
print(dataset.num_edge_labels) print(dataset.num_edge_features) print(dataset.num_edge_attributes)
print(dataset.num_features)
|
1 2 3 4 5 6 7 8
| 划分测试集和训练集
torch.manual_seed(12345) dataset = dataset.shuffle()
train_dataset = dataset[:150] test_dataset = dataset[150:]
|
Dataset中的Data对象
Data对象为每个节点保存了一个标签,以及附加的节点级属性:train_mask,val_mask,test_mask,其中:
- train_mask:表示针对哪些节点进行训练(140个节点);
- val_mask:表示针对哪些节点进行验证(500个节点);
- test_mask:表示针对哪些节点进行测试(1000个节点)
1 2 3 4 5 6 7 8
| data=dataset[0]
print(data.is_undirected()) # True print(data.num_nodes) # 2708 print(data.train_mask.sum().item()) # 140 print(data.val_mask.sum().item()) # 500 print(data.test_mask.sum().item()) # 1000
|
4 数据加载Dataloader
神经网络通常以批量方式进行训练。PyG 通过创建稀疏块对角邻接矩阵(由 定义edge_index)并在节点维度中连接特征和目标矩阵来实现小批量的并行化。这种组合允许在一批示例中使用不同数量的节点和边
$$
\begin{split}\mathbf{A} = \begin{bmatrix} \mathbf{A}_1 & & \ & \ddots & \ & & \mathbf{A}_n \end{bmatrix}, \qquad \mathbf {X} = \begin{bmatrix} \mathbf{X}_1 \ \vdots \ \mathbf{X}_n \end{bmatrix}, \qquad \mathbf{Y} = \begin{bmatrix} \mathbf{Y }_1 \ \vdots \ \mathbf{Y}_n \end{bmatrix}\end{split}
$$
1 2 3 4 5 6 7 8 9
| from torch_geometric.datasets import TUDataset from torch_geometric.data import DataLoader
dataset=TUDataset(root='/home/ykl/ENZYMES',name='ENZYMES') loader=DataLoader(dataset,batch_size=32,shuffle=True)
for batch in loader: print(batch.num_graphs)
|
5 数据转换
自定义Dataset
InMemoryDataset 中有下列四个函数需要我们实现:
raw_file_names()
返回一个包含所有未处理过的数据文件的文件名的列表。起始也可以返回一个空列表,然后在后面要说的 process() 函数里再定义。
processed_file_names()
返回一个包含所有处理过的数据文件的文件名的列表。
download()
如果在数据加载前需要先下载,则在这里定义下载过程,下载到 self.raw_dir 中定义的文件夹位置。如果不需要下载,返回 pass 即可。
process()
这是最重要的一个函数,我们需要在这个函数里把数据处理成一个 Data 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import torch from torch_geometric.data import InMemoryDataset from tqdm import tqdm class YooChooseBinaryDataset(InMemoryDataset): def __init__(self, root, transform=None, pre_transform=None): super(YooChooseBinaryDataset, self).__init__(root, transform, pre_transform) self.data, self.slices = torch.load(self.processed_paths[0]) @property def raw_file_names(self): return [] @property def processed_file_names(self): return ['../input/yoochoose_click_binary_1M_sess.dataset'] def download(self): pass def process(self): data_list = [] grouped = df.groupby('session_id') for session_id, group in tqdm(grouped): sess_item_id = LabelEncoder().fit_transform(group.item_id) group = group.reset_index(drop=True) group['sess_item_id'] = sess_item_id node_features = group.loc[group.session_id==session_id,['sess_item_id','item_id']].sort_values('sess_item_id').item_id.drop_duplicates().values node_features = torch.LongTensor(node_features).unsqueeze(1) target_nodes = group.sess_item_id.values[1:] source_nodes = group.sess_item_id.values[:-1] edge_index = torch.tensor([source_nodes, target_nodes], dtype=torch.long) x = node_features y = torch.FloatTensor([group.label.values[0]]) data = Data(x=x, edge_index=edge_index, y=y) data_list.append(data) data, slices = self.collate(data_list) torch.save((data, slices), self.processed_paths[0])
|