图数据加载与处理

下载Notebook下载样例代码查看源文件

MindSpore提供的mindspore.dataset模块可以帮助用户构建数据集对象,分批次地读取文本数据。同时,在各个数据集类中还内置了数据处理和数据分词算子,使得数据在训练过程中能够像经过pipeline管道的水一样源源不断地流向训练系统,提升数据训练效果。

本章将简要演示如何使用MindSpore加载和处理图数据。

图的概念

在介绍图数据的读取及增强之前,先介绍图的基本概念进行,有助于后续内容更好地理解。通常一个图(graph) G是由一系列的节点(vertices) V以及边(edges)E组成的,每条边都连接着图中的两个节点,用公式可表述为:

\[G = F(V, E)\]

简单的图如下所示。

basicGraph.png

图中包含节点V = {a, b, c, d},和边E = {(a, b), (b, c), (c, d), (d, b)},针对图中的连接关系通常需借助数学的方式进行描述,如常用的基于邻接矩阵的方式,用于描述上述图连接关系的矩阵C如下,其中a、 b、c、d对应为第1、2、 3、4个节点。

\[\begin{split}C=\begin{bmatrix} 1&1&0&0\\ 1&1&1&1\\ 0&1&1&1\\ 0&1&1&1\\ \end{bmatrix}\end{split}\]

数据集准备环节

  1. 数据集介绍

常用的图数据集包含Cora、Citeseer、PubMed等,在本文中我们基于Cora数据集进行介绍。

原始数据集可以从ucsc网站进行下载,本文采用kimiyoung提供的预处理后的版本[1]

其中,Cora数据集主体部分(cora.content)包含2708条样本,每条样本描述1篇科学论文的信息,论文都属于7个类别中的一个。每条样本数据包含三部分,依次为论文编号、论文的词向量(一个1433位的二进制)、论文的类别;引用数据集部分(cora.cites)包含5429行,每行包含两个论文编号,表示第二篇论文对第一篇论文进行了引用。

  1. 数据集下载

以下示例代码将cora数据集下载并解压到指定位置:

[1]:
import os
import shutil

if not os.path.exists("./cora"):
    os.mkdir("./cora")
    if not os.path.exists("./planetoid"):
        os.system("git clone https://github.com/kimiyoung/planetoid")
    content = os.listdir("./planetoid/data")
    new_content = []
    for name in content:
        if "cora" in name:
            new_content.append(name)
    for name in new_content:
        path = "./planetoid/data/"+name
        shutil.copy(path, "./cora")

下载预处理后的cora数据集目录如下所示。

./cora
├── ind.cora.allx
├── ind.cora.ally
├── ind.cora.graph
├── ind.cora.test.index
├── ind.cora.tx
├── ind.cora.ty
├── ind.cora.x
├── ind.cora.y
├── trans.cora.graph
├── trans.cora.tx
├── trans.cora.ty
├── trans.cora.x
└── trans.cora.y
  1. 数据集格式转换

将数据集转换为MindSpore Record格式,可借助models仓库提供的转换脚本进行转换,生成的MindSpore Record文件在./cora_mindrecord路径下。

[2]:
if not os.path.exists("./cora_mindrecord"):
    os.mkdir("./cora_mindrecord")
    os.system('git clone https://gitee.com/mindspore/models.git')
    os.system('python models/utils/graph_to_mindrecord/writer.py --mindrecord_script cora --mindrecord_file "./cora_mindrecord/cora_mr" --mindrecord_partitions 1 --mindrecord_header_size_by_bit 18 --mindrecord_page_size_by_bit 20 --graph_api_args "./cora"')

加载数据集

MindSpore目前支持加载文本领域常用的经典数据集和多种数据存储格式下的数据集,用户也可以通过构建自定义数据集类实现自定义方式的数据加载。

下面演示使用MindSpore.dataset模块中的MindDataset类加载上述已转换成MindSpore Record格式的cora数据集。

  1. 配置数据集目录,创建数据集对象。

[3]:
import mindspore.dataset as ds
import numpy as np

data_file = "./cora_mindrecord/cora_mr"
dataset = ds.GraphData(data_file)
  1. 访问对应的接口,获取图信息及特性、标签内容。

[4]:
# 查看图中结构信息
graph = dataset.graph_info()
print("graph info:", graph)

# 获取所有的节点信息
nodes = dataset.get_all_nodes(0)
nodes_list = nodes.tolist()
print("node shape:", len(nodes_list))

# 获取特征和标签信息,总共2708条数据
# 每条数据中特征信息是用于描述论文i,长度为1433的二进制表示,标签信息指的是论文所属的种类
raw_tensor = dataset.get_node_feature(nodes_list, [1, 2])
features, labels = raw_tensor[0], raw_tensor[1]

print("features shape:", features.shape)
print("labels shape:", labels.shape)
print("labels:", labels)
graph info: {'node_type': [0], 'edge_type': [0], 'node_num': {0: 2708}, 'edge_num': {0: 10858}, 'node_feature_type': [1, 2], 'edge_feature_type': [], 'graph_feature_type': []}
node shape: 2708
features shape: (2708, 1433)
labels shape: (2708,)
labels: [3 4 4 ... 3 3 3]

数据处理

下面演示构建pipeline,对节点进行采样等操作。

  1. 获取节点的邻居节点,构造邻接矩阵。

[5]:
neighbor = dataset.get_all_neighbors(nodes_list, 0)

# neighbor的第一列是node_id,第二列到最后一列存储的是第一列的邻居节点,如果不存在这么多,则用-1补齐。
print("neighbor:\n", neighbor)
neighbor:
 [[   0  633 1862 ...   -1   -1   -1]
 [   1    2  652 ...   -1   -1   -1]
 [   2 1986  332 ...   -1   -1   -1]
 ...
 [2705  287   -1 ...   -1   -1   -1]
 [2706  165 2707 ...   -1   -1   -1]
 [2707  598  165 ...   -1   -1   -1]]
  1. 依据节点的邻居节点信息,构造邻接矩阵。

[6]:
nodes_num = labels.shape[0]
node_map = {node_id: index for index, node_id in enumerate(nodes_list)}
adj = np.zeros([nodes_num, nodes_num], dtype=np.float32)

for index, value in np.ndenumerate(neighbor):
    # neighbor的第一列是node_id,第二列到最后一列存储的是第一列的邻居节点,如果不存在这么多,则用-1补齐。
    if value >= 0 and index[1] > 0:
        adj[node_map[neighbor[index[0], 0]], node_map[value]] = 1

print("adj:\n", adj)
adj:
 [[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 1. ... 0. 0. 0.]
 [0. 1. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 1.]
 [0. 0. 0. ... 0. 1. 0.]]
  1. 节点采样,支持常见的多次跳跃采样与随机游走采样方法等。

  • 多跳邻接点采样如(a)图所示,当次采样的节点将作为下次采样的起始点;随机游走方式如(b)图所示,随机选择一条路径依次遍历相邻的节点,对应图中则选择了从Vi到Vj的游走路径。

graph

[7]:
# 基于多次跳跃进行节点采样
neighbor = dataset.get_sampled_neighbors(node_list=nodes_list[0:21], neighbor_nums=[2], neighbor_types=[0])
print("neighbor:\n", neighbor)

# 基于随机游走进行节点采样
meta_path = [0]
walks = dataset.random_walk(nodes_list[0:21], meta_path)
print("walks:\n", walks)
neighbor:
 [[   0 1862  633]
 [   1  654    2]
 [   2 1666    1]
 [   3 2544 2544]
 [   4 1256 1761]
 [   5 1659 1629]
 [   6 1416  373]
 [   7  208  208]
 [   8  281 1996]
 [   9  723 2614]
 [  10 2545  476]
 [  11 1655 1839]
 [  12 2662 1001]
 [  13 1810 1701]
 [  14 2668 2077]
 [  15 1093 1271]
 [  16 2444  970]
 [  17 2140 1315]
 [  18 2082 1560]
 [  19 1939 1939]
 [  20 2375 2269]]
walks:
 [[   0 1862]
 [   1  654]
 [   2 1666]
 [   3 2544]
 [   4 2176]
 [   5 1659]
 [   6 1042]
 [   7  208]
 [   8  281]
 [   9  723]
 [  10 2545]
 [  11 1839]
 [  12 2662]
 [  13 1701]
 [  14 2034]
 [  15 1271]
 [  16 2642]
 [  17 2140]
 [  18 2145]
 [  19 1939]
 [  20 2269]]

由于上面代码中游走采样存在随机性,因此在执行时可能会出现不同的打印结果。

  1. 通过节点获取边/通过边获取节点。

[8]:
# 通过边获取节点
part_edges = dataset.get_all_edges(0)[:10]
nodes = dataset.get_nodes_from_edges(part_edges)
print("part edges:", part_edges)
print("nodes:", nodes)

# 通过节点获取边
nodes_pair_list = [(0, 633), (1, 652), (2, 332), (3, 2544)]
edges = dataset.get_edges_from_nodes(nodes_pair_list)
print("edges:", edges)
part edges: [0 1 2 3 4 5 6 7 8 9]
nodes: [[   0  633]
 [   0 1862]
 [   0 2582]
 [   1    2]
 [   1  652]
 [   1  654]
 [   2 1986]
 [   2  332]
 [   2 1666]
 [   2    1]]
edges: [ 0  4  7 11]

参考文献

[1] Yang Z, Cohen W, Salakhudinov R. Revisiting semi-supervised learning with graph embeddings.