1. PyG中图的表示及使用——Data类

1.1 Data对象的创建

1.1.1 通过构造函数

Data类的构造函数如下,

  1. class Data(object):
  2. def __init__(self, x=None, edge_index=None, edge_attr=None, y=None, **kwargs):
  3. r"""
  4. Args:
  5. x (Tensor, optional): 节点属性矩阵,大小为`[num_nodes, num_node_features]`
  6. edge_index (LongTensor, optional): 边索引矩阵,大小为`[2, num_edges]`,第0行为尾节点,第1行为头节点,头指向尾
  7. edge_attr (Tensor, optional): 边属性矩阵,大小为`[num_edges, num_edge_features]`
  8. y (Tensor, optional): 节点、图或者是边的标签,任意大小
  9. """
  10. self.x = x
  11. self.edge_index = edge_index
  12. self.edge_attr = edge_attr
  13. self.y = y
  14. for key, item in kwargs.items():
  15. if key == 'num_nodes':
  16. self.__num_nodes__ = item
  17. else:
  18. self[key] = item
  • edge_index每一列定义一条边,其中第一行为边起始节点的索引,第二行为边结束节点的索引。这种表示方法被称为COO格式(coordinate format),通常用于表示稀疏矩阵。
  • PyG不是用稠密矩阵Task01 Data类与Dataset类 - 图1来持有邻接矩阵的信息,而是用仅存储邻接矩阵Task01 Data类与Dataset类 - 图2中非Task01 Data类与Dataset类 - 图3元素的稀疏矩阵来表示图
  • 通常,一个图至少包含x, edge_index, edge_attr, y, num_nodes5个属性,当图包含其他属性时,我们可以通过指定额外的参数使**Data**对象包含其他的属性
    1. graph = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y, num_nodes=num_nodes, other_attr=other_attr)

下面来看一个示例

  1. import torch
  2. from torch_geometric.data import Data
  3. edge_index = torch.tensor([[0, 1, 1, 2],
  4. [1, 0, 2, 1]], dtype=torch.long)
  5. x = torch.tensor([[-1],
  6. [0],
  7. [1]], dtype=torch.float)
  8. data = Data(x=x, edge_index=edge_index)
  9. # Data(edge_index=[2, 4], x=[3, 1])

上面这段代码产生的图的结构如下图所示

image-20210614101321478.png

如果要用edge_index定义节点索引元组列表的话,应该使用contigious在将他们传递给构造函数前先进行转置操作,再调用该方法,代码如下:

  1. from torch_geometric.data import Data
  2. edge_index = torch.tensor([[0, 1],
  3. [1, 0],
  4. [1, 2],
  5. [2, 1]], dtype=torch.long)
  6. x = torch.tensor([[-1], [0], [1]], dtype=torch.float)
  7. data = Data(x=x, edge_index=edge_index.t().contiguous())
  8. # Data(edge_index=[2, 4], x=[3, 1])

这里每一条边用两个元组定义,说明每条边的两个方向。

1.1.2 将dict对象转为Data对象

也可以将一个dict对象转边为一个Data对象,代码如下:

  1. graph_dict = {
  2. 'x': x,
  3. 'edge_index': edge_index,
  4. 'edge_attr': edge_attr,
  5. 'y': y,
  6. 'num_nodes': num_nodes,
  7. 'other_attr': other_attr
  8. }
  9. graph_data = Data.from_dict(graph_dict)

这里from_dict是一个类方法:

  1. @classmethod
  2. def from_dict(cls, dictionary):
  3. r"""Creates a data object from a python dictionary."""
  4. data = cls()
  5. for key, item in dictionary.items():
  6. data[key] = item
  7. return data

【注意】:graph_dict中属性值的类型与大小的要求与Data类的构造函数的要求相同。

1.2 Data对象的常见操作

Data类由许多实用函数对实例对象操作,下面来简单看一下这些操作

  1. # 获取Data对象包含的属性的关键字
  2. print(data.keys)
  3. # ['x', 'edge_index']
  4. # 获取属性
  5. print(data['x'])
  6. # tensor([[-1.0], [0.0], [1.0]])
  7. # 设置属性
  8. data['x'] = x
  9. for key, item in data:
  10. print("{} found in data".format(key))
  11. # x found in data
  12. # edge_index found in data
  13. 'edge_attr' in data
  14. # False
  15. # 对边排序并移除重复的边
  16. graph_data.coalesce()
  17. # 节点数量
  18. data.num_nodes
  19. # 3
  20. # 边数量
  21. data.num_edges
  22. # 4
  23. # 节点属性的维度,边属性的维度查找同理
  24. # data.node_features
  25. data.num_node_features
  26. # 1
  27. # 是否含有孤立节点
  28. data.contains_isolated_nodes()
  29. # False
  30. # 是否含有自环边
  31. data.contains_self_loops()
  32. # False
  33. # 是否为有向图
  34. data.is_directed()
  35. # False
  36. # 用作训练集的节点
  37. data.train_mask.sum()
  38. # 用作训练集的节点的数量
  39. int(data.train_mask.sum()) / data.num_nodes
  40. # Transfer data object to GPU.
  41. device = torch.device('cuda')
  42. data = data.to(device)

1.3 Data对象转变为其他的数据

  • Data对象转换为dict对象:

    1. def to_dict(self):
    2. return {key: item for key, item in self}
  • Data对象转换为namedtuple

    1. def to_namedtuple(self):
    2. keys = self.keys
    3. DataTuple = collections.namedtuple('DataTuple', keys)
    4. return DataTuple(*[self[key] for key in keys])

2. PyG中图数据集的表示及使用——Dataset类

PyG内置了大量常用的基准数据集,接下来以PyG内置的Planetoid数据集为例,来学习PyG中图数据集的表示及使用

2.1 生成数据集对象并分析数据集

在第一次生成PyG内置的数据集时,程序首先下载原始文件,然后将原始文件处理成包含**Data**对象的**Dataset**对象并保存到文件。代码如下:

  1. from torch_geometric.datasets import Planetoid
  2. dataset = Planetoid(root='/dataset/Cora', name='Cora')
  3. # Cora()
  4. len(dataset)
  5. # 1
  6. dataset.num_classes
  7. # 7
  8. dataset.num_node_features
  9. # 1433

2.2 分析数据集中样本

从上一节中可以看出,该数据集只有一个图,包含7个分类节点的属性为1433维

  1. data = dataset[0]
  2. # Data(edge_index=[2, 10556], test_mask=[2708],
  3. # train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])
  4. data.is_undirected()
  5. # True
  6. data.train_mask.sum().item()
  7. # 140
  8. data.val_mask.sum().item()
  9. # 500
  10. data.test_mask.sum().item()
  11. # 1000

该数据集包含唯一一个图,有2708个节点,节点特征为1433维,有10556条边,有140个用作训练集的节点,有500个用作验证集的节点,有1000个用作测试集的节点。

2.3 自定义切分数据集

以 ENZYMES 数据集(含有6个分类,600张图)为例

  1. from torch_geometric.datasets import TUDataset
  2. dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')
  3. # 访问第一个图
  4. data = dataset[0]
  5. print(data)
  6. # Data(edge_index=[2, 168], x=[37, 3], y=[1])
  7. # 使用切片分割数据集,要求训练集和测试集的比例是7:3
  8. train_data = dataset[:420]
  9. test_data = dataset[420:]
  10. print(train_data, ',', test_data)
  11. # ENZYMES(420), ENZYMES(180)
  12. # 如果不确定在拆分之前数据集是否已经打乱,可以通过运行如下函数来随机排列数据集
  13. dataset = dataset.shuffle()

2.4 数据集的使用

假设已经定义好了一个图神经网络模型,其名为Net。在下方的代码中,展示了节点分类图数据集在训练过程中的使用。

  1. devic = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  2. model = Net().to(device)
  3. data = dataset[0].to(device)
  4. optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
  5. model.train()
  6. for epoch in range(200):
  7. optimizer.zero_grad()
  8. out = model(data)
  9. loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
  10. loss.backward()
  11. optimizer.step()

3. 小批量处理

神经网络通常以批处理方式进行训练。PyG通过创建稀疏块对角邻接矩阵(由edge_index定义)并在节点维度中连接特征和目标矩阵来实现小批量的并行化。这种组合允许在一批中的示例上有不同数量的节点和边,如下图所示

image-20210615192447316.png

PyG中包含其自身的torch_geometric.data.DataLoader,其可以处理这一连接过程,下面来看一个例子

  1. from torch_geometric.datasets import TUDataset
  2. from torch_geometric.data import DataLoader
  3. dataset = TUDataset(root='/Dataset/ENZYMES', name='ENZYMES', use_node_attr=True)
  4. loader = DataLoader(dataset, batch_size=32, shuffle=True)
  5. for batch in loader:
  6. print(batch)
  7. print(batch.num_graphs)

结果如下图所示

image-20210616091723540.png

torch_geometric.data.Batch继承torch_geometric.data.Data,并包含一个batch附加属性

batch是一个列向量,它在批处理中将每个节点映射到其各自的图:

image-20210616093257365.png

可以利用它来对每个图在节点维度上平均节点特征

  1. from torch_scatter import scatter_mean
  2. from torch_geometric.datasets import TUDataset
  3. from torch_geometric.data import DataLoader
  4. dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES', use_node_attr=True)
  5. loader = DataLoader(dataset, batch_size=32, shuffle=True)
  6. for data in loader:
  7. x = scatter_mean(data.x, data.batch, dim=0)
  8. print(x.size())

结果如下图所示:

image-20210616100031122.png

4. 数据转换

转换是torchvision变换图像和执行增强的常用方法。PyG带有自己的转换,它以一个Data对象作为输入并返回一个新的变换后的Data对象。可以使用torch_geometric.transforms.Compose在将处理过的数据集保存到磁盘 (pre_transform) 或访问数据集中的图形(transform)之前将转换链接在一起。

下面来看一个例子,在ShapEnet数据集上使用转换(包含17000个3D形状点云和16个形状类别的标签)

  1. from torch_geometric.datasets import ShapeNet
  2. dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'])
  3. print(dataset[0])
  4. # Data(pos=[2518, 3], y=[2518])

我们可以通过转换从点云生成最近邻图来将点云数据集转换为图数据集(简单来说,就是通过KNN算法寻找节点的邻居,然后加边生成图)

  1. import torch_geometric.transforms as T
  2. from torch_geometric.datasets import ShapeNet
  3. dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'],
  4. pre_transform=T.KNNGraph(k=6))
  5. print(dataset[0])
  6. # Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])

【注意】:使用pre_transform将数据保存至磁盘后,下一次启动时它已经包含图形的边,即使未通过任何转换。

此外,我们可以使用transform参数随机增强对象,比如,将每个节点位置平移一个小数

  1. import torch_geometric.transforms as T
  2. from torch_geometric.datasets import ShapeNet
  3. dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'],
  4. pre_transform=T.KNNGraph(k=6),
  5. transform=T.RandomTranslate(0.01))
  6. print(dataset[0])
  7. # Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])

5. 作业

要求:请通过继承Data类实现一个类,专门用于表示“机构-作者-论文”的网络。该网络包含“机构“、”作者“和”论文”三类节点,以及“作者-机构“和“作者-论文“两类边。对要实现的类的要求:

1)用不同的属性存储不同节点的属性;

2)用不同的属性存储不同的边(边没有属性);

3)逐一实现获取不同节点数量的方法。

分析:

代码:

  1. def __init__(self, x_p, x_a, x_i, edge_index_a2p, edge_index_a2i, edge_attr_a2p=None, edge_attr_a2i=None):
  2. super(myData, self).__init__()
  3. self.edge_index_a2p = edge_index_a2p
  4. self.edge_attr_a2p = edge_attr_a2p
  5. self.edge_index_a2i = edge_index_a2i
  6. self.edge_attr_a2i = edge_attr_a2i
  7. self.x_p = x_p
  8. self.x_a = x_a
  9. self.x_i = x_i

在这种定义下,edge_index_a2pedge_index_a2i应该相对独立的增加边对应的源节点和目标节点,比如 edge_index_a2p应该增加[[self.x_a.size(0)], [self.x_i.size(0)]]

  1. def __inc__(self, key, value):
  2. if key == 'edge_index_a2p':
  3. return torch.tensor([[self.x_a.size(0)], [self.x_p.size(0)]])
  4. if key == 'edge_index_a2i':
  5. return torch.tensor([[self.x_a.size(0)], [self.x_i.size(0)]])
  6. else:
  7. return super().__inc__(key, value)

实现取不同属性的节点的数量的方法:

  1. @property
  2. def num_paper_Nodes(self):
  3. return self.x_p.shape[0]
  4. @property
  5. def num_author_Nodes(self):
  6. return self.x_a.shape[0]
  7. @property
  8. def num_institution_Nodes(self):
  9. return self.x_i.shape[0]

测试代码:

  1. # 测试部分 假设作者数:3 论文数:2 机构数:2
  2. x_a = torch.randn(3,4)
  3. x_p = torch.randn(2,6)
  4. x_i = torch.randn(2,3)
  5. edge_index_a2p = torch.tensor([[0, 1, 1, 2],
  6. [0, 0, 1, 1]], dtype=torch.long)
  7. edge_index_a2i = torch.tensor([[0,1,2],
  8. [0,0,1]], dtype=torch.long)
  9. data = myData(x_a=x_a, x_p=x_p, x_i=x_i, edge_index_a2p=edge_index_a2p, edge_index_a2i=edge_index_a2i)
  10. data
  11. '''
  12. myData(edge_index_a2i=[2, 3], edge_index_a2p=[2, 4], x_a=[3, 4], x_i=[2, 3], x_p=[2, 6])
  13. '''
  14. data.num_author_Nodes
  15. # 3

6. 参考资料