1 图数据结构

图说起来也很简单,就是两个核心点,一个是图节点(nodes/vertics),一个是边(edges/links)表示节点之间的连接关系

总体而言,图可以是不规整的(irregular),对比而言,平时我们看到的图片都是规整的(regular),可以表示成矩阵或者向量。问题在于设计数据结构如何储存图,一般有两种方案

矩阵表示

又细分成两种

  1. 用邻接矩阵(adjacency matrix),度矩阵(degree matrix), 拉普拉斯矩阵(Laplacian matrix)去表示, 衍生出各种对拉普拉斯矩阵的操作,比如图傅里叶变换(graph fourier transform), 也有稀疏邻接矩阵

  2. 用关联矩阵(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)

# 将数据集迁移到GPU上
device=torch.device("cuda")
data=data.to(device)
  • Data的其他属性
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) # 32

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 = []

# process by session_id
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])