【GNN】第三章:图数据
深度学习的三驾马车是数据、算法、算力。数据指的就是训练一个模型的数据,比如训练集验证集测试集;算法就是你的模型、架构、网络、优化算法等的统称;算力就是计算资源GPU等硬件资源。三者缺一不可。当我们拿到一个项目,最先遇到的就是如何获取数据、如何清洗数据、如何读取数据、如何预处理数据、如何进行数据增强、如何分小批次加载训练。全面了解和收集你的数据是你做项目无法绕开的一环,而且是最费时费力最最细碎麻烦的一个环节,甚至很多情况下是数据大于模型,因为garbage in garbage out嘛。
整理数据是一项非常非常重要的工作。任何一个优秀的模型或者效果好的模型,一定是在数据上做了非常非常多的处理和特征工程,甚至一些trick,样本的均衡性、样本的特点、有没有异常的样本、有没有特殊的样本、样本的分布、样本的标签、样本的向量表示、大小、多少、尺寸、形变、角度、遮挡、语义等等,各个细微处都要了然于胸,才能对症去调整模型。
更不幸的是,图数据和我们平时的看到的数据差别很大!而且更加的杂乱无章!如何把自己的数据处理成算法能够使用的标准格式,很多时候比图神经网络的训练更难一些。PyTorch Geometric是基于pytorch库的包,所以继承了pytorch的数据处理方法,所以本部分主要讲的是讲代码、数据处理实现的模块和类,如果看不懂的同学,建议从深度学习基础开始学起:
【深度学习】第一章:深度学习的入坑线路_深度学习的一个重大误区,深度学习入门路线推荐-优快云博客
说明:图(graph)是图(graph),数据集(dataset)是数据集(dataset), 图是包含在数据集中的。一个数据集中可以只有一个图也可以有多个图。所以我们先讲PyG中图的表示,然后再讲torch_geometric包中自带的数据集,最后讲如何将我们自己的数据转化为图数据。
一、PyG中和数据相关的模块
我们使用的图神经网络框架是PyTorch Geometric,所以我们先登录PyTorch Geometric官网,看看和数据相关的模块的介绍。
官网地址: PyG Documentation — pytorch_geometric documentation
从官网上看,PyG中和数据结构、数据处理、数据集相关的代码都放在:
1、 torch_geometric.data模块:其中,
(1)torch_geometric.data.Data类
这个类是创建图对象的。也就是每个图都是由torch_geometric.data.Data对象表示的,而且torch_geometric.data.Data对象包含了一张图的所有节点、边、标签、属性等信息。这个类的使用详解见下面的大标题二。
用户可以在程序运行中动态创建数据对象,而且也不需要将数据以pt文件形式保存到磁盘。用户自己创建的单个图数据可以采用这种方式创建。动态创建完毕后,Data.x表示节点信息,Data.edge_index表示边信息。
说明:pt文件是PyTorch中保存模型和数据的文件格式,你可以使用PyTorch库中的torch.load()函数来加载.pt文件。你必须有基本的pytorch代码编写基础才能看懂本篇章,如果没有从CNN的数据看起也行:【深度视觉】第二章:卷积网络的数据_卷积神经网络 数据分析-优快云博客 ,如果CNN这篇文章你也看不懂,那你真的得踏踏实实从0开始学了。
(2)torch_geometry.data.Dataset类
torch_geometry.data.Dataset类是继承PyTorch的torch.utils.data.Dataset类,所以torch_geometry.data.Dataset类在GNN领域就相当于是数据集处理的基类。一般大型数据集使用这个类。就是数据量大到无法一次将所有数据加载到内存时,用这个类来处理数据集。这个类的使用详解下面大标题四。
(3)torch_geometric.data.InMemoryDataset类
这个类又是继承torch_geometry.data.Dataset类,这个类适用于小规模数据集。就是你的数据集比较小,可以一下把整个数据集加载到内存,你就用这个类。后面你会看到PyG的很多内置数据集都是继承这个类。
2、torch_geometric.loader模块
用于数据训练加载的,比如切割小批次:torch_geometric.loader.DataLoader类用于将图数据集分割成多个小批量(mini-batches)
3、torch_geometric.transforms模块
里面都是一些数据增强的操作,使用这些方法可以对Data对象进行各种变换。
4、torch_geometric.utils模块 里面有一些数据处理的小工具。比如:to_networkx函数,可以把Data对象转化为networkx对象,然后就可以进行可视化了。
5、torch_geometric.datasets模块:这个模块中内置很多通用数据集,常见的基准数据集有:
Cora:一个根据科学论文之间相互引用关系而构建的图数据集合,论文分7类:Genetic_Algorithms, Neural_Networks, Probabilistic_Methods, Reinforcement_Learning, Rule_Learning, Theory, 共2708篇。
Citeseer: 一个论文之间引用信息数据集,论文分6类:Agents、AI、DB、IR、ML和HCL, 共包含3312篇论文。
Pubmed:生物医学方面的论文搜寻以及摘要数据集。
ENZYMES:包含600个图和6个类别。
此外,还包括了一系列3D点云数据集,比如FAUST, ShapeNet等。
每个数据集都可以通过它对应的类来下载和使用。PyG官网上对每个数据集都要详细的介绍和对应类的参数说明,只要你正确传入参数就可以自动下载,并生成一个这个数据集对象,你通过这个对象,一是你可以查看一些整个数据集层面的属性,二是可以查看一些图级别的属性和值。后面大标题三会展示一个例子。
这是对PyG中和数据相关的模块的简单介绍,下面我们讲的内容都是这些模块中的类或者函数的具体使用过程。
二、图的表示及其使用
图论及其工具networkx是传统机器学习的方法和工具。networkx是一个python包,所以通过networkx创建的图对象、属性等数据结构、数据类型都不能直接用于Pytorch深度学习框架中的。将图数据应用到深度学习的工具是PyTorch Geometric工具包,所以我们一定要通过PyG中的创建数据对象的类来创建数据。
在PyTorch Geometric中,创建图对象是通过torch_geometric.data.Data类来实现的。通过这个类创建的对象都可以直接送入图卷积层进行正向传播。所以本部分也就是在讲如何使用torch_geometric.data.Data类。
1、在PyG中创建一个图对象
(1)如果你的数据非常简单,而且所有节点都有边,就是没有孤立点,此时你只要制作一个边索引矩阵即可,Data会自动根据边索引给你生成节点的。下面展示创建过程:
(2)如果你的数据比较复杂,你就得把所有信息都先整理好:
注意点:
一是,在pyg中,一张图中的所有节点默认都是从0123...进行编码的。也就是用节点的索引号来表示节点。而且图中的边的表示也是根据这些节点索引号来制作的。当我们定义x(Data对象的节点的特征向量时)矩阵时,就意味着我们已经按照行顺序给每个节点赋予了它对应的唯一的一个索引号。也就是矩阵x的第一个特征向量对应的节点就是0号节点,第二个特征向量对应的节点就是1号节点,,,依次类推。
pyg是计算图数据的,所以pyg不能可视化。如果你想可视化你定义的图,那你得转化为networkx对象,而且networkx可视化图的时候,节点的标签显示的是节点的索引号。如果你想显示节点的名称,那你得自己制作节点的标签,用字典的形式来制作,把节点的索引号和名称对应起来,然后用nx.draw_networkx_labels(G, pos, labels=labels) 来显示节点的名字。下面小标题3有示例。
二是,edge_index定义的格式是:每一列定义一条边,其中第一行为边的起始节点索引source,第二行为边的结束节点索引target。默认边都是有向的,就是从起始节点source到结束节点terget。就是从edge_index矩阵的第一行对应到第二行,每个对应位置表示一条有向边。edge_index矩阵的列与列之间没有顺序要求,你可以交换任意两列的位置,对计算没影响!如果你图中的所有边都定义的是:source A->target B和target B->source A,那么Data就可以帮你转化为无向边。说得比较绕,意思就是:如果你想定义无向图,那你每条无向边都得定义成source A->target B,target B->source A,这种两对儿的形式。如果不是这样定义的,Data就默认是有向边了,边与边的位置没有前后之分。
三是,通常一个图至少包含x, edge_index, edge_attr, y, num_nodes,5个属性,当图包含其他属性时,可以通过指定额外的参数使Data对象包含其他的属性:
2、查看图对象的属性
在PyTorch Geometric中,每个图都由一个Data对象表示,这个Data对象的属性包含了描述图结构的所有信息:
3、可视化图对象
pyg是无法可视化的,得转化为netwokx对象再可视化:
三、PyG中基准数据集的使用流程
1、PyG的torch_geometric.datasets模块中内置了大量的、常见的、通用的基准数据集(Common Benchmark Datasets),可以从链接 torch_geometric.datasets — pytorch_geometric documentation 查看这些数据集的说明文档:
这些数据集都是一些论文贡献的、开源的数据集。很多论文都是在自己特定的数据集上做出一个什么baseline方法,于是这篇论文就贡献了一个数据集:
你想用哪个数据集,你点击进去看看这个数据集的API下载接口文档,把参数设置好,就可以顺利下载数据了。
2、展示一个例子:下载ENZYMES数据集:
根据参数说明,我们下载ENZYMES子数据集:
3、简单探索数据
先看看raw文件夹里面的txt文件里面都是什么:
说明:你实例化TUDataset类时,不管你是用dataset=TUDataset(root='D:/pytorch-data/GNN-datasets', name='ENZYMES', use_node_attr=True, use_edge_attr=True) 还是用dataset=TUDataset(root='D:/pytorch-data/GNN-datasets', name='ENZYMES')
实例化完毕后,你的raw文件夹里的东西和process文件夹里的东西都是一样的。就是永久化保存到本地硬盘里的东西都是一样,只是生成的dataset对象会有差异。至于为什么是这样的?这里面底层牵扯到pytorch的Dataset类、pyg的Dataset类、InMemoryDataset类和数据集TUDataset类,这几个类都是依次继承的,底层逻辑比较复杂,这个小标题是写如何使用内置数据集的,不适合在这里展开。所以后面我单独起一个小标题来探讨。
如果你没有选择后两个参数,你生成的dataset对象的属性如下:
如果你选择了后两个参数,你生成的dataset对象属性如下:
为什么会有这个差别呢?我们再看看node_features和node_attributes:
可见,两次生成的dataset对象中的节点的属性特征向量不一样!!!第一次把节点的标签当成节点的特征向量了。第二次把节点的属性向量+标签当成了节点的特征向量了。所以一定一定要仔细探索你的数据,一定要清楚你的数据!不然后面你训练时,如果你的任务是分类节点,你把节点的标签也放到里面学习,岂不是告诉答案去学习嘛?!滑了大稽了。
4、切分训练集和测试集
5、分小批次
这基本上就是使用内置数据集训练模型前的所有处理流程了。后面就是按照batch加载数据,训练模型了。
如果你用这个数据集做图级别的分类任务,你就用图级别的标签,进行监督训练。
如果你想对点进行分类,那你在训练时,一定记得把节点属性向量的最后三列给去掉,把最后三列当作节点的标签,进行监督训练。
如果你想对节点进行聚类,那就不要节点标签了,直接可以聚类了。
四、如何导入自己的数据集
1、继续探讨torch_geometry.data.Dataset类和torch_geometric.data.InMemoryDataset类
话接(三)3处。
数据集TUDataset类是继承了torch_geometric.data.InMemoryDataset类,而torch_geometric.data.InMemoryDataset类又是继承了torch_geometry.data.Dataset类,torch_geometry.data.Dataset类则是继承pytorch的torch.utils.data.Dataset类。而要想训练GNN网络,喂入网络的数据得是torch_geometric.data.Data类的实例对象。
(1)torch.utils.data.Dataset类是pytorch中处理数据的基类,只有我们写自己的数据集,都要无脑继承这个类。这个类可以帮我们实现海量数据的训练。就是我们在训练模型时,执行for epoch in epochs:for data in traindata时,不是把所有数据集都加载到内存,然后按照batch正向传播,而是当这个循环中需要这个batch中的小批量数据时,根据索引从磁盘读取-加载-数据增强-才进入训练的。这也就是为什么我们训练一个大模型,使用海量数据,都要在pytorch框架下,我们就可以省略很多底层的东西。这个类的使用方式我在将CNN时,有过不止一次的案例展示。熟悉深度学习的同学应该对这个类不陌生。
(2)torch_geometry.data.Dataset类继承torch.utils.data.Dataset类,所以torch_geometry.data.Dataset类保留了(1)的这种巧妙设计。但是毕竟图数据和CNN中的数据还是天差地别的。所以人们又开发了torch_geometry.data.Dataset类,就是在torch.utils.data.Dataset的基础上,又增加了如何把数据转化为图数据,也就是如何转化为torch_geometric.data.Data的数据类型。因为只有Data类型才能送入GNN网络嘛。所以当你的数据集是大型数据集,也就是整个数据集不能一下子全部加载到内存时,你就继承torch_geometry.data.Dataset这个类,你只要把数据处理的process方法重写(override)一下,你就可以无感训练GNN模型了。
(3)但是,此时又出现一个问题,就是把比如txt数据、csv数据、mat数据或者任何其他形式的原始数据,转化为Data对象是一个非常耗时的过程,所以人们又开发了torch_geometric.data.InMemoryDataset类,这个类就支持一下子把所有数据都加载到内存,然后在内存中-转Data-数据增强-分batch进入模型训练。这样就省时了很多。所以当我们是小数据集时,就是整个数据集可以加载到内存时,我们就继承这个类,同样也是重写一下proccess方法,就可以训练模型了。
这些只是理解这三个类的整体情况。下面继续深挖细节:
2、torch_geometric.data.InMemoryDataset类
(1)InMemoryDataset类初始化方法参数说明:
root参数:字符串类型,存储数据集的文件夹的路径。该文件夹下有两个文件夹:
一个文件夹名为raw,路径是raw_dir,用于存储未处理的文件。比如从网络上下载的数据集原始文件会被存放到这里。前面TUDataset数据集就是这种情况。
另一个文件夹名为processed,路径是processed_dir,处理后的数据被保存到这里。处理后的数据就是:
- 一是把所有图的节点和边都合并在一起的data.pt文件。后续全部加载到内存中等待训练的就是这个文件。从这个文件中就可以轻松获取整个数据集中的所有图的Data对象。
- 二是需要对原始数据进行检测和过滤的pre_filter.pt文件。如果你不需要对原始数据进行检查了,这个文件就是一个空文件。
- 三是需要对原始数据进行预处理的pre_transform.pt文件。预处理和数据增强是两个环节。预处理是基本的检查,比如缺失值异常值等这些操作,如果不需要这些操作这个文件也是一个空文件。
说明:意思就是如果我们继承了InMemoryDataset类,底层就自动把数据检查、数据预处理、合并后的Data对象,持久化到本地磁盘上了。当训练时,加载的就是整个文件,只需要再进行数据增强一下就可以传入模型了。
说明:raw_dir和processed_dir是属性方法,我们可以自定义要使用的文件夹。
transform参数:函数类型,一个数据转换函数,它接收一个Data对象并返回一个转换后的Data对象。此函数在每一次数据获取过程中都会被执行。获取数据的函数首先使用此函数对Data对象做转换,然后才返回数据。此函数应该用于数据增强(Data Augmentation)。该参数默认值为None,表示不对数据做转换。
pre_transform参数:函数类型,一个数据转换函数,它接收一个Data对象并返回一个转换后的Data对象。此函数在Data对象被保存到文件前调用。也就是上面说的pre_transform.pt文件。因此它用于只执行一次的数据预处理。该参数默认值为None,表示不做数据预处理。
pre_filter参数:函数类型,一个检查数据是否要保留的函数,它接收一个Data对象,返回此Data对象是否应该被包含在最终的数据集中。此函数也在Data对象被保存到文件pre_filter.pt前调用。该参数默认值为None,表示不做数据检查,保留所有的数据。
(2)通过继承InMemoryDataset类来构造一个我们自己的数据集类,我们至少要实现下面两个方法:
processed_file_names()。这是一个属性方法,返回一个存储合并后的Data对象的文件名。
process(): 处理原始数据,保存处理好的数据到processed_dir文件夹下的文件。重写这个方法才是核心中的核心。
raw_file_names():这是一个属性方法,返回一个数据集原始文件的文件名列表,数据集原始文件应该能在raw_dir文件夹中找到,否则调用download()函数下载文件到raw_dir文件夹。
download(): 下载数据集原始文件到raw_dir文件夹。
网上很多资料都说要写这个两个方法,其实这两个方法可以省略的,后面我举个例子,就把这个两个方法省略了。
(3)在process方法里,我们实现数据处理的逻辑:
首先,我们从数据集原始文件中读取样本并生成一个个Data对象,所有样本的Data对象保存在列表data_list中。
其次,如果要对数据做过滤的话,我们执行数据过滤的过程。就是如果pre_filter参数不为None,则进行样本过滤。
接着,如果要对数据做预处理的话,我们执行数据预处理的过程。就是如果pre_transform参数不为None,则调用pre_transform()函数进行数据处理。
最后,由于python保存一个巨大的列表是相当慢的,所以process方法是先将所有Data对象合并成一个巨大的Data对象再保存。其中,将一个个Data对象合并成一个大Data,是通过collate()函数实现的。就是collate()函数接收一个列表的Data对象,返回合并后的Data对象、以及用于从合并后的Data对象重构各个原始Data对象的切片字典slices。然后将这个巨大的Data对象和切片字典slices保存到文件data.pt、将当前使用的样本过滤方法和数据处理方法保存到pre_transform.pt文件和pre_filter.pt文件。
说明:函数collate()是继承InMemoryDataset类的类对象才有的方法。就是说collate()函数是绑定继承了InMemoryDataset类的类对象的。其他类对象是没有这个方法的。
说明:这些文件的存储路径是processed_paths() 属性方法返回的路径。如果将数据保存到多个文件中,则返回的路径有多个。
pocessed_paths()属性方法是在基类中定义的,它对self.processed_dir文件夹与processed_file_names()属性方法的返回每一个文件名做拼接,然后返回。
3、举一个例子展示一下继承torch_geometric.data.InMemoryDataset类的写法
对于小型数据集,我们可以通过继承torch_geometric.data.InMemoryDataset类来自定义一个加载全部样本到内存的数据集类。
前面讲ENZYMES数据集时,我们是通过ENZYMES数据集的API下载的,现在我们通过这个ENZYMES数据的url直接下载,然后自己手动导入这个数据集:
现在我自己写一个读取数据集的类,把桌面上文件夹中的ENZYMES数据集读出来,并把读出的数据集对象文件也放到桌面的my_processed_data文件夹里面:
实例化化这个类,就可以在桌面的my_processed_data文件夹里生成:
说明:我先后一直强调桌面,意思就是你的路径你可以根据你自己的情况自己设置的。
import pandas as pd
import torch.nn.functional
import torch
from torch_geometric.data import InMemoryDataset, download_url, Data
class MyOwnDataset(InMemoryDataset): #小型数据集,继承的类就用InMemoryDataset
def __init__(self, root, transform=None, pre_transform=None): #参数基本就是这个写法,最好别变
super().__init__(root, transform, pre_transform) #super函数中的参数不能省略!!super函数是调用父类的构造函数,所以代码会跳到下面两个重写方法!
self.data, self.slices = torch.load(self.processed_paths[0]) #load也会调用processed_file_names()方法,获得data.pt文件名,然后加载data.pt文件
@property
def processed_file_names(self):
return ['data.pt'] #这个文件名你可以写你自己想写的名字,但一般我们都写data.pt,执行super是时,就开始执行这个函数和下面的process函数
def process(self): #----------这个方法是你生成自己数据集的必写函数
#1、导入可能需要加工的文件--这一步主要看你拿到的是什么格式的原始数据,你根据你自己的数据具体处理
raw_A = r"C:\Users\25584\Desktop\myrawdata\ENZYMES_A.txt"
raw_node_attributes = r"C:\Users\25584\Desktop\myrawdata\ENZYMES_node_attributes.txt"
raw_node_labels = r"C:\Users\25584\Desktop\myrawdata\ENZYMES_node_labels.txt"
raw_graph_labels = r"C:\Users\25584\Desktop\myrawdata\ENZYMES_graph_labels.txt"
raw_graph_indicator = r"C:\Users\25584\Desktop\myrawdata\ENZYMES_graph_indicator.txt"
df0 = pd.read_csv(raw_A, header=None)
df1 = pd.read_csv(raw_node_attributes, header=None)
df2 = pd.read_csv(raw_node_labels, header=None)
df3 = pd.read_csv(raw_graph_labels, header=None)
df4 = pd.read_csv(raw_graph_indicator, header=None)
#2、导入整个数据集的所有的边
Edge_index = torch.tensor([df0[1], df0[0]], dtype=torch.long)-1 #-1是为了让节点从0开始,如果你原始数据就是从0开始编码的,就不用-1了。
#3、导入整个数据集的所有节点的特征------这一步我把节点的特征和节点的标签(one-hot形式)给合并了--这个处理看你自己了
x_attr = torch.tensor(np.array(df1), dtype=torch.float)
x_label = nn.functional.one_hot(torch.tensor(df2[0])-1).type(torch.float) #-1也是把标签调整到从0开始
x = torch.cat([x_attr, x_label], dim=1)
#4、导入所有图的标签
y = torch.tensor(df3[0]-1) #-1也是把图的标签从0开始
#5、生成datalist
data_list = [] #-------------这个变量名字不能变!!!!!
graph_indicator = torch.tensor(df4[0]-1) #根据indicator文件中的内容,来生成datalist, -1也表示从0开始
graph_index = graph_indicator.unique()
node_start = 0
for i in graph_index: #用循环,一张图生成一个Data对象,然后放到data_list中
graph_node_end = node_start + len(graph_indicator[graph_indicator==i])
temp = Edge_index[:, Edge_index[0] < graph_node_end]
graph_edge = temp[:, temp[0] >= node_start] - node_start
graph_x = x[node_start:graph_node_end]
graph_y = y[i].reshape(1).type(torch.float)
graph_data = Data(edge_index=graph_edge, x=graph_x, y=graph_y)
data_list.append(graph_data)
node_start = graph_node_end
#至此自己的数据集最核心的操作就处理完毕了。
#下面的代码都是父类的代码
if self.pre_filter is not None: #你也可以单独再写pre_filter函数,就可以调用pre_filter函数进行样本过滤了
data_list = [data for data in data_list if self.pre_filter(data)]
if self.pre_transform is not None: #同理,你也可以提前写了transform数据增强的操作
data_list = [self.pre_transform(data) for data in data_list]
data, slices = self.collate(data_list) #一是collate方法会调用processed_file_names,二是会把data_list转换成字典的形式,封装成data对象和slices对象
torch.save((data, slices), self.processed_paths[0]) #把data对象和slices对象保存成data.pt文件
#实例化,获取数据集对象
mydata = MyOwnDataset(root = r"C:\Users\25584\Desktop\my_processed_data")
mydata
mydata.data
mydata.slices
#切分训练集和测试集
from torch.utils.data import random_split
train_data, test_data = random_split(mydata, [0.7, 0.3], torch.manual_seed(0))
train_data[0]
train_data[1]
#分小批次
from torch_geometric.loader import DataLoader
batchdata = DataLoader(train_data, batch_size=3, shuffle=True)
batchdata
#循环一个batch看看数据对不对
for i in batchdata:
print(i)
for graph in i:
print(graph)
break
4、torch_geometric.data.Dataset基类
对于大型数据集,我们无法将所有数据同时全部加载到内存,就得通过继承torch_geometric.data.Dataset基类来自定义一个按需加载样本到内存的数据集类。
(1)对于大型数据集,我们最好不要让很重的数据清洗、预处理等操作放到加载数据的类中。一般的做法是先把raw data清洗、预处理完毕后,生成的新数据永久固化到硬盘中,然后再继承torch_geometric.data.Dataset这个类,读取数据。
还以ENZYMES数据集为例,前期我这样处理:
此时我继承torch_geometric.data.Dataset基类,写一个读取数据的类:
此时,你就方向按照batch喂入GNN训练模型了,你就别管内存够不够,底层都自动帮你边训练边加载了。
这个写法和pytorch中的torch.utils.data.Dataset类的用法一模一样。
(2)如果你的数据集很大,而且还没有把所有样本都整理成一个个.pt文件并持久化到硬盘上,我就想先跑跑模型怎么办?也可以就是有点麻烦:
import os
import torch
from torch_geometric.data import Dataset, Data
from torch_geometric.loader import DataLoader
class MyOwnDataset2(Dataset):
def __init__(self, root, transform=None, pre_transform=None, pre_filter=None):
super().__init__(root, transform, pre_transform, pre_filter) #执行B C
@property
def processed_file_names(self): #B
return ['data.pt']
def process(self): #C
raw_A = r"C:\Users\25584\Desktop\gnn_data\raw\ENZYMES_A.txt"
raw_node_attributes = r"C:\Users\25584\Desktop\gnn_data\raw\ENZYMES_node_attributes.txt"
raw_node_labels = r"C:\Users\25584\Desktop\gnn_data\raw\ENZYMES_node_labels.txt"
raw_graph_labels = r"C:\Users\25584\Desktop\gnn_data\raw\ENZYMES_graph_labels.txt"
raw_graph_indicator = r"C:\Users\25584\Desktop\gnn_data\raw\ENZYMES_graph_indicator.txt"
df0 = pd.read_csv(raw_A, header=None)
df1 = pd.read_csv(raw_node_attributes, header=None)
df2 = pd.read_csv(raw_node_labels, header=None)
df3 = pd.read_csv(raw_graph_labels, header=None)
df4 = pd.read_csv(raw_graph_indicator, header=None)
#2、导入所有的边
Edge_index = torch.tensor([df0[1], df0[0]], dtype=torch.long)-1
#3、导入所有节点的特征
x_attr = torch.tensor(np.array(df1), dtype=torch.float)
x_label = nn.functional.one_hot(torch.tensor(df2[0])-1).type(torch.float) #-1也是把标签调整到从0开始
x = torch.cat([x_attr, x_label], dim=1)
#4、导入所有图的标签
y = torch.tensor(df3[0]-1)
#5、生成datalist---这是核心
data_list = []
graph_indicator = torch.tensor(df4[0]-1)
graph_index = graph_indicator.unique()
node_start = 0
for i in graph_index: #用循环,一张图生成一个Data对象,然后放到data_list中
graph_node_end = node_start + len(graph_indicator[graph_indicator==i])
temp = Edge_index[:, Edge_index[0] < graph_node_end]
graph_edge = temp[:, temp[0] >= node_start] - node_start
graph_x = x[node_start:graph_node_end]
graph_y = y[i].reshape(1).type(torch.float)
graph_data = Data(edge_index=graph_edge, x=graph_x, y=graph_y)
data_list.append(graph_data)
node_start = graph_node_end
if self.pre_filter is not None:
data_list = [data for data in data_list if self.pre_filter(data)]
if self.pre_transform is not None:
data_list = [self.pre_transform(data) for data in data_list]
idx = 0 ####还是要用循环把一个个样本都持久化成.pt文件
for i in data_list:
data = i
torch.save(data, os.path.join(self.processed_dir, f'data_{idx}.pt'))
idx += 1
def get(self, idx):###这个函数不能省
data = torch.load(os.path.join(self.processed_dir, f'data_{idx}.pt'))
return data
至此,数据集怎么处理就讲解完毕。
五、图结构数据上的学习任务
由于学习任务不仅涉及到数据的处理还涉及到网络架构的搭建,所以不管是讲数据还是讲架构,最后我都关联一下学习任务:
从上图可见,是你的任务决定数据的,比如:
如果你是分类节点的任务,那你的数据中一定要有节点的标签,可以只有少部分,但不可以完全没有,因为GNN可以进行半监督学习,后面讲架构了,会给大家展示一个例子来说明。
如果你要对整张图进行分类,那你的数据中得有所有图的标签,就是图级标签。就是你得给所有的图都打上标签,否则你也是无法训练分类的。
总之,一个完整的深度学习项目要求:任务-数据-架构-训练-硬件相互匹配、环环相扣的。比如你的数据处理一定要符合你的任务目标;你的架构设计一方面要匹配你的任务,另一方面还要匹配你的训练;模型的复杂度一方面要匹配数据,另一方面要匹配你的电脑硬件;模型的训练一方面要追求效果,另一方还追求效率,就是降低训练的时间成本和硬件成本,此时恰当的数据增强手段、或者更加高效的训练技术都可以降低你的训练成本。也所以,要完成一个深度学习项目是要各个环节通盘考虑和取舍的。