GNN-1-Data类与Dataset类

参考开源学习地址:datawhale

1. 为什么要在图上进行深度学习?

同一图的节点存在连接关系,这表明节点不是独立的。然而,传统的机器学习技术假设样本是独立且同分布的,因此传统机器学习方法不适用于图计算任务。图机器学习研究如何构建节点表征,节点表征要求同时包含节点自身的信息和节点邻接的信息,从而我们可以在节点表征上应用传统的分类技术实现节点分类。图机器学习成功的关键在于如何为节点构建表征。深度学习已经被证明在表征学习中具有强大的能力,它大大推动了计算机视觉、语音识别和自然语言处理等各个领域的发展。因此,将深度学习与图连接起来,利用神经网络来学习节点表征,将带来前所未有的机会。

2.图结构数据

一、图的表示:

  • 定义一(图)
  • 定义二(图的邻接矩阵)

二、图的属性:

  • 定义三(结点的度,degree)
  • 定义四(邻接结点,neighbors)
  • 定义五(行走,walk)
  • 定理六(行走的个数)
  • 定义七(路径,path)
  • 定义八(子图,subgraph)
  • 定义九(连通分量,connected component)
  • 定义十(连通图,connected graph)
  • 定义十一(最短路径,shortest path)
  • 定义十二(直径,diameter)
  • 定义十三(拉普拉斯矩阵,Laplacian Matrix)
  • 定义十四(对称归一化的拉普拉斯矩阵,Symmetric normalized Laplacian)
    在这里插入图片描述

3.环境配置

(1) 引言

PyTorch Geometric (PyG)是面向几何深度学习的PyTorch的扩展库,几何深度学习指的是应用于图和其他不规则、非结构化数据的深度学习。基于PyG库,我们可以轻松地根据数据生成一个图对象,然后很方便的使用它;我们也可以容易地为一个图数据集构造一个数据集类,然后很方便的将它用于神经网络。

(2) 安装正确版本的pytorch和cudatoolkit,此处安装1.8.1版本的pytorch和11.1版本的cudatoolk

参考:
Anaconda 安装pytorch

window10系统英伟达NVIDIA显卡驱动和CUDA软件的安装和升级

(3) 安装正确版本的PyG

pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html
pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html
pip install torch-cluster -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html
pip install torch-spline-conv -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html
pip install torch-geometric

其他版本的安装方法以及安装过程中出现的大部分问题的解决方案可以在Installation of of PyTorch Geometric 页面找到。

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

4.1 Data对象的创建

Data类的官方文档为torch_geometric.data.Data

4.1.1通过构造函数

Data类的构造函数

class Data(object):

    def __init__(self, x=None, edge_index=None, edge_attr=None, y=None, **kwargs):
    r"""
    Args:
        x (Tensor, optional): 节点属性矩阵,大小为`[num_nodes, num_node_features]`
        edge_index (LongTensor, optional): 边索引矩阵,大小为`[2, num_edges]`,第0行为尾节点,第1行为头节点,头指向尾
        edge_attr (Tensor, optional): 边属性矩阵,大小为`[num_edges, num_edge_features]`
        y (Tensor, optional): 节点或图的标签,任意大小(,其实也可以是边的标签)
	
    """
    self.x = x
    self.edge_index = edge_index
    self.edge_attr = edge_attr
    self.y = y

    for key, item in kwargs.items():
        if key == 'num_nodes':
            self.__num_nodes__ = item
        else:
            self[key] = item

PyG文档之二:快速入门

图一般被用来建模和描述目标(节点)间成对的关系(边)。在Pytorch Gemometric(以后均简称pyg)中,一个图是由torch_geometric.data.Data的一个实例来描述的,设此图有N个节点,每个节点有n个特征,M条边,每条边有m个特征,默认情况下拥有如下的属性:

  • data.x: 节点的特征矩阵,形状:[N,n]

  • data.edge_index: 用COO格式储存的图数据,形状:[2,M](如不理解没事,后面我会在栗子中详细介绍),数据类型是torch.long
    COO就是坐标,coordinate.思想也很简单,每列分别存储:行坐标,纵坐标,值。

  • data.edge_attr: 边特征矩阵,形状[M,m]

  • data.y: 要训练的目标(可以是任意形状),如节点级目标[节点数,* ],图级目标[1,* ],此处*代表样本数量。

  • data.pos: 节点位置矩阵,形状:[N,num_dimensions],在有些图中,节点是具有坐标属性,比如3D点云,每个节点都是3维空间中的一个坐标,类似的也可以是其它维度的。

说明:

  • edge_index的每一列定义一条边,其中第一行为边起始节点的索引,第二行为边结束节点的索引。这种表示方法被称为COO格式(coordinate format),通常用于表示稀疏矩阵。
  • PyG不是用稠密矩阵A ∈ { 0 , 1 } ∣ V ∣ × ∣ V ∣ \mathbf{A} \in \{ 0, 1 \}^{|\mathcal{V}| \times |\mathcal{V}|}A{0,1}V×V来持有邻接矩阵的信息,而是用仅存储邻接矩阵A \mathbf{A}A中非0 00元素的稀疏矩阵来表示图。
  • 通常,一个图至少包含x, edge_index, edge_attr, y, num_nodes5个属性,当图包含其他属性时,我们可以通过指定额外的参数使Data对象包含其他的属性
graph = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y, num_nodes=num_nodes, other_attr=other_attr)

下面来看一个示例:

用一个具有三个节点和四个边的无加权和无向图的简单示例。每个节点只包含一个特征:
在这里插入图片描述

import torch
from torch_geometric.data import Data
#以列为单位,比如第一列:0,1 表示节点0到节点1的边:0->1,同理1,2表示边1->2,
#这样储存可以很方便储存稀疏的图。虽然这个图只有2条边,但是为了统一无向图和有向图的存储,
#因此需要每条边存2个方向。
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)

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

import torch
from torch_geometric.data import Data
#以行为单位更符合人类的逻辑,但是在构建数据时,记得先转置,再contiguous
edge_index = torch.tensor([[0, 1],
                           [1, 0],
                           [1, 2],
                           [2, 1]], dtype=torch.long)
x = torch.tensor([[-1], [0], [1]], dtype=torch.float)

data = Data(x=x, edge_index=edge_index.t().contiguous())

### `Data`对象转换成其他类型数据

我们可以将`Data`对象转换为`dict`对象:

```bash
def to_dict(self):
    return {key: item for key, item in self}

或转换为namedtuple

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

4.1.2 转dict对象为Data对象

我们也可以将一个dict对象转换为一个Data对象

graph_dict = {
    'x': x,
    'edge_index': edge_index,
    'edge_attr': edge_attr,
    'y': y,
    'num_nodes': num_nodes,
    'other_attr': other_attr
}
graph_data = Data.from_dict(graph_dict)

from_dict是一个类方法:

@classmethod
def from_dict(cls, dictionary):
    r"""Creates a data object from a python dictionary."""
    data = cls()
    for key, item in dictionary.items():
        data[key] = item

    return data

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

4.1.3 Data对象转换成其他类型数据

我们可以将Data对象转换为dict对象:

def to_dict(self):
    return {key: item for key, item in self}

或转换为namedtuple

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

获取Data对象属性

x = graph_data['x']

设置Data对象属性

graph_data['x'] = x

获取Data对象包含的属性的关键字

graph_data.keys()

对边排序并移除重复的边

graph_data.coalesce()

Data对象的其他性质
我们通过观察PyG中内置的一个图来查看Data对象的性质:

from torch_geometric.datasets import KarateClub

dataset = KarateClub()
data = dataset[0]  # Get the first graph object.
print(data)
print('==============================================================')

# 获取图的一些信息
print(f'Number of nodes: {data.num_nodes}') # 节点数量
print(f'Number of edges: {data.num_edges}') # 边数量
print(f'Number of node features: {data.num_node_features}') # 节点属性的维度
print(f'Number of node features: {data.num_features}') # 同样是节点属性的维度
print(f'Number of edge features: {data.num_edge_features}') # 边属性的维度
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}') # 平均节点度
print(f'if edge indices are ordered and do not contain duplicate entries.: {data.is_coalesced()}') # 是否边是有序的同时不含有重复的边
print(f'Number of training nodes: {data.train_mask.sum()}') # 用作训练集的节点
print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.2f}') # 用作训练集的节点数占比
print(f'Contains isolated nodes: {data.contains_isolated_nodes()}') # 此图是否包含孤立的节点
print(f'Contains self-loops: {data.contains_self_loops()}')  # 此图是否包含自环的边
print(f'Is undirected: {data.is_undirected()}')  # 此图是否是无向图

Data的一些内置函数

#上述代码中已经将Data对象赋给data
print(data.keys)
>>> ['x', 'edge_index']
#两种方式都可以,
print(data['x'])
print(data.x)
>>> tensor([[-1.0],
            [0.0],
            [1.0]])
#类似于字典的遍历方式
for key, item in data:
    print("{} found in data".format(key))
>>> x found in data
>>> edge_index found in data
#测试属性
'edge_attr' in data
>>> False
#以下几个属性是初始化图之后就自动生成的,不需要人为赋值
data.num_nodes
>>> 3
data.num_edges
>>> 4
data.num_node_features
>>> 1
#是否包含孤立节点
data.contains_isolated_nodes()
>>> False
#是否包含自连接节点,即自己到自己的边
data.contains_self_loops()
>>> False
#是否有向图
data.is_directed()
>>> False

# 和torch的张量数据切换完全一样,不再赘述
device = torch.device('cuda')
data = data.to(device)

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

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

Planetoid数据集类的官方文档为torch_geometric.datasets.Planetoid

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

如下方代码所示,在PyG中生成一个数据集是简单直接的。在第一次生成PyG内置的数据集时,程序首先下载原始文件,然后将原始文件处理成包含Data对象的Dataset对象并保存到文件。

【提醒】Planetoid无法直接下载Cora等数据集,采用从GitHub或gitee拉数据
GitHub项目是https://github.com/kimiyoung/planetoid,gitee项目是https://gitee.com/jiajiewu/planetoid。gitee的话在国内会更快点,所以推荐用gitee的。

把planetoid.py里面第48行的 url = ‘https://github.com/kimiyoung/planetoid/raw/master/data’ 改成 url=‘https://gitee.com/jiajiewu/planetoid/raw/master/data’
逻辑上就跟第二种解决方式里面从gitee拉数据一样。

Planetoid无法直接下载Cora等数据集的3个解决方式

from torch_geometric.datasets import Planetoid

dataset = Planetoid(root='/dataset/Cora', name='Cora')
# Cora()

len(dataset)
# 1

dataset.num_classes
# 7

dataset.num_node_features
# 1433

5.2 分析数据集中样本

可以看到该数据集只有一个图,包含7个分类任务,节点的属性为1433维度。

data = dataset[0]
# Data(edge_index=[2, 10556], test_mask=[2708],
#         train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])

data.is_undirected()
# True

data.train_mask.sum().item()
# 140

data.val_mask.sum().item()
# 500

data.test_mask.sum().item()
# 1000

现在我们看到该数据集包含的唯一的图,有2708个节点,节点特征为1433维,有10556条边,有140个用作训练集的节点,有500个用作验证集的节点,有1000个用作测试集的节点。PyG内置的其他数据集,请小伙伴一一试验,以观察不同数据集的不同。

5.3 自定义切分数据集

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

from torch_geometric.datasets import TUDataset
dataset = TUDataset(root='data/ENZYMES', name='ENZYMES')

# 访问第一个图
data = dataset[0]
print(data)
# Data(edge_index=[2, 168], x=[37, 3], y=[1])

# 使用切片分割数据集,要求训练集和测试集的比例是7:3
train_data = dataset[:420]
test_data = dataset[420:]
print(train_data, ',', test_data)
# ENZYMES(420), ENZYMES(180)

# 如果不确定在拆分之前数据集是否已经打乱,可以通过运行如下函数来随机排列数据集
dataset = dataset.shuffle()

6. 作业

题目: 请通过继承Data 类实现一个类,专门用于表示“机构-作者-论文”的网络。该网络包含“机构“、”作者“和”论文”三类节点,以及“作者-机构“和“作者-论文“两类边。对要实现的类的要求:1)用不同的属性存储不同节点的属性;2)用不同的属性存储不同的边(边没有属性);3)逐一实现获取不同节点数量的方法。

分析:

  • 节点有三类,分别记为 P:论文;A:作者;I:机构
  • 边有两类,分别记为 AtoI:作者-机构;AtoP:作者-论文
  • 这里可以视为两个二分图(作者-机构图和作者-论文图)存放在一个Data对象中
from torch_geometric.data import Data
import numpy as np
import torch

class myData(Data):
    '''
    节点有三类,分别记为 P:论文;A:作者;I:机构
    边有两类,分别记为 AtoI:作者-机构;AtoP:作者-论文
    '''
    def __init__(self, x_p, x_a, x_i, edge_index_a2p, edge_index_a2i, edge_attr_a2p=None, edge_attr_a2i=None):

        super(myData, self).__init__()
        self.edge_index_a2p = edge_index_a2p  #作者-论文
        self.edge_attr_a2p = edge_attr_a2p  
        self.edge_index_a2i = edge_index_a2i  #作者-机构
        self.edge_attr_a2i = edge_attr_a2i
        self.x_p = x_p  #论文
        self.x_a = x_a  #作者
        self.x_i = x_i  #机构
        
    def __inc__(self, key, value):
        if key == 'edge_index_a2p':
            return torch.tensor([[self.x_a.size(0)], [self.x_p.size(0)]])
        if key == 'edge_index_a2i':
            return torch.tensor([[self.x_a.size(0)], [self.x_i.size(0)]])
        else:
            return super().__inc__(key, value)
        
    @property
    def num_paper_Nodes(self):
        return self.x_p.shape[0]
    
    @property
    def num_author_Nodes(self):
        return self.x_a.shape[0]
    
    @property
    def num_institution_Nodes(self):
        return self.x_i.shape[0]
    
    
# 测试部分 假设作者数:3 论文数:2 机构数:2

x_a = torch.randn(3,4)
x_p = torch.randn(2,6)
x_i = torch.randn(2,3)

edge_index_a2p = torch.tensor([[0, 1, 1, 2],
                               [0, 0, 1, 1]], dtype=torch.long)
edge_index_a2i = torch.tensor([[0,1,2],
                               [0,0,1]], dtype=torch.long)

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)

print('data', data)
'''
	myData(edge_index_a2i=[2, 3], edge_index_a2p=[2, 4], x_a=[3, 4], x_i=[2, 3], x_p=[2, 6])
'''

print('data.num_author_Nodes', data.num_author_Nodes)
# 3