Datawhale组队学习-图神经网络(一)
Taks 01
学习书籍、工具及软件
- 本次学习资料为Datawhale社区在Github上的开源项目和官方文档
- 电脑配置为Macbook m1,操作系统为MacOS 11.4 Big Sur。所用编程环境为Anaconda 下的 Jupyter Notebook,python 3.8.8。
学习图神经网络的原因
在过去的深度学习应用中,我们接触的数据形式主要是这四种:矩阵、张量、序列(sequence)和时间序列(time series)。然而来自现实世界应用的数据更多地是图的结构,如社交网络、交通网络、蛋白质与蛋白质相互作用网络、知识图谱和大脑网络等。图提供了一种通用的数据表示方法,众多其他类型的数据也可以转化为图的形式。此外,大量的现实世界的问题可以作为图上的一组小的计算任务来解决。推断节点属性、检测异常节点(如垃圾邮件发送者)、识别与疾病相关的基因、向病人推荐药物等,都可以概括为节点分类问题。推荐、药物副作用预测、药物与目标的相互作用识别和知识图谱的完成(knowledge graph completion)等,本质上都是边预测问题。
同一图的节点存在连接关系,这表明节点不是独立的。然而,传统的机器学习技术假设样本是独立且同分布的,因此传统机器学习方法不适用于图计算任务。图机器学习研究如何构建节点表征,节点表征要求同时包含节点自身的信息和节点邻接的信息,从而我们可以在节点表征上应用传统的分类技术实现节点分类。图机器学习成功的关键在于如何为节点构建表征。深度学习已经被证明在表征学习中具有强大的能力,它大大推动了计算机视觉、语音识别和自然语言处理等各个领域的发展。因此,将深度学习与图连接起来,利用神经网络来学习节点表征,将带来前所未有的机会。
然而,如何将神经网络应用于图,这一问题面临着巨大的挑战。首先,传统的深度学习是为规则且结构化的数据设计的,图像、文本、语音和时间序列等都是规则且结构化的数据。但图是不规则的,节点是无序的,节点可以有不同的邻居节点。其次,规则数据的结构信息是简单的,而图的结构信息是复杂的,特别是在考虑到各种类型的复杂图,它们的节点和边可以关联丰富的信息,这些丰富的信息无法被传统的深度学习方法捕获。
图深度学习是一个新兴的研究领域,它将深度学习技术与图数据连接起来,推动了现实中的图预测应用的发展。然而,此研究领域也面临着前所未有的挑战。
注:以上内容整理自“Deep Learning on Graphs: An Introduction”!!!
PyG 环境配置
1.安装Anaconda
因为本人已有Anaconda环境,这里不再赘述。
2.创建新环境
conda create -n pytorch python=3.8.8
#在shell中输入python查看到自己python版本
激活环境
conda activate pytorch
修改镜像源
因为Anaconda的默认源下载速度缓慢甚至导致报错,需要换成国内源,这里我换到了清华源,国内其它源也可:
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --set show_channel_urls yes
3.安装PyTorch和PyG
安装PyTorch:
首先进入官网找到自己对应的PyTorch安装命令:
conda install pytorch torchvision torchaudio -c pytorch
检查是否安装成功
安装PyG:
本次电脑系统和配置为MacOS m1芯片,直接安装CPU版本:
pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-1.8.0+cpu.html
pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-1.8.0+cpu.html
pip install torch-cluster -f https://pytorch-geometric.com/whl/torch-1.8.0+cpu.html
pip install torch-spline-conv -f https://pytorch-geometric.com/whl/torch-1.8.0+cpu.html
pip install torch-geometric
其他版本的安装方法以及安装过程中出现的大部分问题的解决方案可以在Installation of of PyTorch Geometric 页面找到。
至此,本次学习所需环境全部安装完毕。
一、简单图论
学习资料来源于DataWhale,这里列出我在学习中感到重要的,容易混淆和困惑的知识点:
-
在图的计算任务中,节点(node)一定含有信息(至少含有节点的度的信息),边(edge)可能含有信息。
-
因为定义,无向图的邻接矩阵是对称的。我们默认无权图的各条边权重为1,边不存在则为0,所以无权图的矩阵元素只有0和1.
-
对于有向有权图,结点 v i v_i vi的出度(out degree)等于从 v i v_i vi出发的边的权重之和,结点 v i v_i vi的入度(in degree)等于从连向 v i v_i vi的边的权重之和。结点 v i v_i vi的度记为 d ( v i ) d(v_i) d(vi),入度记为 d i n ( v i ) d_{in}(v_i) din(vi),出度记为 d o u t ( v i ) d_{out}(v_i) dout(vi)。
-
无向图,无权图和无向无权图都为有向有权图的特殊情况。无向图的结点的出度与入度相等。有权图各边的权重为 1 1 1,那么结点 v i v_i vi的出度(out degree)等于从 v i v_i vi出发的边的数量,结点 v i v_i vi的入度(in degree)等于从连向 v i v_i vi的边的数量。
-
领接节点:
- 结点 v i v_i vi的邻接结点为与结点 v i v_i vi直接相连的结点,其被记为** N ( v i ) \mathcal{N(v_i)} N(vi)**。
- **结点 v i v_i vi的 k k k跳远的邻接节点(neighbors with k k k-hop)**指的是到结点 v i v_i vi要走 k k k步的节点(一个节点的 2 2 2跳远的邻接节点包含了自身)。
-
有一图,其邻接矩阵为 L \mathbf{L} L, L n \mathbf{L}^{n} Ln为邻接矩阵的 n n n次方,那么 L n [ i , j ] \mathbf{L}^{n}[i,j] Ln[i,j]等于从结点 v i v_i vi到结点 v j v_j vj的长度为 n n n的行走的个数。
这个概念当时我难以理解,查阅资料后在此补充我的理解:
我们需要从数学的计算角度去考虑此定义:
我们先构造一个图:
该图的邻接矩阵 L = ( 0 1 0 1 1 0 1 1 0 1 0 1 1 1 1 0 ) \mathbf{L}=\left(\begin{array}{lllll} 0 & 1 & 0 & 1 \\ 1 & 0 & 1 & 1 \\ 0 & 1 & 0 & 1 \\ 1 & 1 & 1 & 0 \\ \end{array}\right) L=⎝⎜⎜⎛0101101101011110⎠⎟⎟⎞,则 L 2 = A = ( 2 1 2 1 1 3 1 2 2 1 2 1 1 2 1 3 ) \mathbf{L^2}=\bf A=\left(\begin{array}{lllll} 2 & 1 & 2 & 1 \\ 1 & 3 & 1 & 2 \\ 2 & 1 & 2 & 1 \\ 1 & 2 & 1 & 3 \\ \end{array}\right) L2=A=⎝⎜⎜⎛2121131221211213⎠⎟⎟⎞,我们对B的第一行进行展开:
a 11 = a 11 ∗ a 11 + a 12 ∗ a 21 + a 13 ∗ a 31 + a 14 ∗ a 41 = 2 a 12 = a 11 ∗ a 12 + a 12 ∗ a 22 + a 13 ∗ a 32 + a 12 ∗ a 42 = 1 a 13 = a 11 ∗ a 13 + a 12 ∗ a 23 + a 13 ∗ a 33 + a 14 ∗ a 43 = 2 a 14 = a 11 ∗ a 14 + a 12 ∗ a 24 + a 13 ∗ a 34 + a 14 ∗ a 44 = 1 a_{11} = a_{11}*a_{11} + a_{12}*a_{21} + a_{13}*a_{31} + a_{14}*a_{41} = 2\\ a_{12} = a_{11}*a_{12} + a_{12}*a_{22} + a_{13}*a_{32} + a_{12}*a_{42} = 1\\ a_{13} = a_{11}*a_{13} + a_{12}*a_{23} + a_{13}*a_{33} + a_{14}*a_{43} = 2\\ a_{14} = a_{11}*a_{14} + a_{12}*a_{24} + a_{13}*a_{34} + a_{14}*a_{44} = 1 a11=a11∗a11+a12∗a21+a13∗a31+a14∗a41=2a12=a11∗a12+a12∗a22+a13∗a32+a12∗a42=1a13=a11∗a13+a12∗a23+a13∗a33+a14∗a43=2a14=a11∗a14+a12∗a24+a13∗a34+a14∗a44=1
不难看出,因为矩阵乘法的缘故,两个相乘时,第一个的纵坐标等于第二个的横坐标。例如 a 12 ∗ a 21 a_{12}*a_{21} a12∗a21就相当于从1“走”到2,再从2“走”到1,并且只有当两者都为1,即存在这两条路径的时候这个乘积才会为1,那么就表示从1出发到达1路径长度为2的路径+1。其实矩阵 L L L的含义可以这样解释, l i j l_{ij} lij表示的是,从点i出发走一步到点j有多少条路径,这时 l i j l_{ij} lij要么为1,要么为0(意为没有边)。而乘上一个矩阵 L L L就相当于步数+1。现在我们来分析 A A A这个矩阵的含义, a i j a_{ij} aij表示的是,从点i出发走2步到达点j有多少条路径。
我们设 L 3 = B \bf L^3 = \bf B L3=B,则:
b 11 = a 11 ∗ l 11 + a 12 ∗ l 21 + a 13 ∗ l 31 + a 14 ∗ l 41 = 2 ∗ 0 + 1 ∗ 1 + 2 ∗ 0 + 1 ∗ 1 = 2 b 12 = a 11 ∗ l 12 + a 12 ∗ l 22 + a 13 ∗ l 32 + a 14 ∗ l 42 = 2 ∗ 1 + 1 ∗ 0 + 2 ∗ 1 + 1 ∗ 1 = 5 b_{11} = a_{11}*l_{11} + a_{12}*l_{21} + a_{13}*l_{31} + a_{14}*l_{41} = 2*0 + 1*1 + 2*0 +1*1 = 2\\ b_{12} = a_{11}*l_{12} + a_{12}*l_{22} + a_{13}*l_{32} + a_{14}*l_{42} = 2*1 + 1*0 + 2*1 + 1*1 = 5 b11=a11∗l11+a12∗l21+a13∗l31+a14∗l41=2∗0+1∗1+2∗0+1∗1=2b12=a11∗l12+a12∗l22+a13∗l32+a14∗l42=2∗1+1∗0+2∗1+1∗1=5
这个则为三步的数量。以此类推,若 G = A n \bf G=\bf A^n G=An, g i j g_{ij} gij表示的是从i出发走到点j走n步,有多少种走法。 -
直径(diameter):给定一个连通图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其直径为其所有节点对之间的最短路径的最大值,形式化定义为
diameter ( G ) = max v s , v t ∈ V min p ∈ P s t ∣ p ∣ \operatorname{diameter}(\mathcal{G})=\max _{v_{s}, v_{t} \in \mathcal{V}} \min _{p \in \mathcal{P}_{s t}}|p| diameter(G)=vs,vt∈Vmaxp∈Pstmin∣p∣
-
拉普拉斯矩阵(Laplacian Matrix):给定一个图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其邻接矩阵为 A A A,其拉普拉斯矩阵定义为 L = D − A \mathbf{L=D-A} L=D−A,其中 D = d i a g ( d ( v 1 ) , ⋯ , d ( v N ) ) \mathbf{D=diag(d(v_1), \cdots, d(v_N))} D=diag(d(v1),⋯,d(vN))。
-
对称归一化的拉普拉斯矩阵(Symmetric normalized Laplacian):给定一个图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其邻接矩阵为 A A A,其规范化的拉普拉斯矩阵定义为:
L = D − 1 2 ( D − A ) D − 1 2 = I − D − 1 2 A D − 1 2 \mathbf{L=D^{-\frac{1}{2}}(D-A)D^{-\frac{1}{2}}=I-D^{-\frac{1}{2}}AD^{-\frac{1}{2}}} L=D−21(D−A)D−21=I−D−21AD−21
这里的 − 1 2 -\frac{1}{2} −21是矩阵的二分之一次方再取逆。
D是图KaTeX parse error: Undefined control sequence: \cal at position 1: \̲c̲a̲l̲{G}的度矩阵,是一个对角阵,对角上的元素为各顶点的的度
I是指单位阵
具体关于拉普拉斯矩阵和对称归一化拉普拉斯矩阵查看此处
-
二部图(Bipartite Graphs):节点分为两类,只有不同类的节点之间存在边。
-
同质图(Homogeneous Graph):只有一种类型的节点和一种类型的边的图。
-
异质图(Heterogeneous Graph):存在多种类型的节点和多种类型的边的图。
二、Data
类——PyG中图的表示及其使用
Data
对象的创建
Data
类的官方文档为torch_geometric.data.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
这里方括号里的值的是指该属性的维度,其中edge_index
的对应顺序是不影响结果的,只是用来计算邻接矩阵。
[[0,1,2],[3,4,5]]
和[[1,0,2],[4,3,5]]
的定义是等价的。
其中简单复习COO的知识,另有CSR和CRC这俩互为转置的数据格式。
我们用一个简单例子简要看一下data数据的具体细节:
import torch
from torch_geometric.data import Data
x = torch.tensor([[2,1], [5,6], [3,7], [12,0]], dtype=torch.float)
y = torch.tensor([0, 1, 0, 1], dtype=torch.float)
edge_index = torch.tensor([[0, 2, 1, 0, 3],
[3, 1, 0, 1, 2]], dtype=torch.long)
data = Data(x=x, y=y, edge_index=edge_index)
>>> Data(edge_index=[2, 5], x=[4, 2], y=[4])
通常,一个图至少包含x, edge_index, edge_attr, y, num_nodes
5个属性,当图包含其他属性时,我们可以通过指定额外的参数使Data
对象包含其他的属性:
graph = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y, num_nodes=num_nodes, other_attr=other_attr)
data类的简单调用:
from torch_geometric.datasets import KarateClub
dataset = KarateClub()
data = dataset[0] # Get the first graph object.
print(data)
# Data(edge_index=[2, 156], train_mask=[34], x=[34, 34], y=[34])
# 获取图的一些信息
print(f'Number of nodes: {data.num_nodes}') # 节点数量
# Number of nodes: 34
print(f'Number of edges: {data.num_edges}') # 边数量
# Number of edges: 156
print(f'Number of node features: {data.num_node_features}') # 节点属性的维度
# Number of node features: 34
print(f'Number of node features: {data.num_features}') # 同样是节点属性的维度
# Number of node features: 34
print(f'Number of edge features: {data.num_edge_features}') # 边属性的维度
# Number of edge features: 0
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}') # 平均节点度
# Average node degree: 4.59
print(f'if edge indices are ordered and do not contain duplicate entries.: {data.is_coalesced()}') # 是否边是有序的同时不含有重复的边
# if edge indices are ordered and do not contain duplicate entries.: True
print(f'Number of training nodes: {data.train_mask.sum()}') # 用作训练集的节点
# Number of training nodes: 4
print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.2f}') # 用作训练集的节点的数量
# Training node label rate: 0.12
print(f'Contains isolated nodes: {data.contains_isolated_nodes()}') # 此图是否包含孤立的节点
# Contains isolated nodes: False
print(f'Contains self-loops: {data.contains_self_loops()}') # 此图是否包含自环的边
# Contains self-loops: False
print(f'Is undirected: {data.is_undirected()}') # 此图是否是无向图
# Is undirected: True #
这里需要注意的是print(data)
得出的结果是它的各属性的shape。如果我们想要看到data的具体细节的话我们需要用到接下来的知识。
转dict
对象为Data
对象
用Data
的内置from_dict
方法:
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
它其实构造了一个数据结构类似于dict的这么一个类方法,这里需要注意的是graph_dict
中属性值的类型与大小的要求与Data
类的构造函数的要求相同。
转Data
对象为其他对象
-
用
Data
内置的to_dict
方法将Data
对象转换为dict
对象:def to_dict(self): return {key: item for key, item in self}
-
用
Data
内置的to_nametuple
方法将Data
对象转换为nametuple
对象:def to_namedtuple(self): keys = self.keys DataTuple = collections.namedtuple('DataTuple', keys) return DataTuple(*[self[key] for key in keys])
这里需要补充一下关于
nametuple
的知识:import collections User = collections.namedtuple('User', 'name age id') user = User('tester', '22', '464643123') print(user) # User(name='tester', age='22', id='464643123')
获取Data
对象的具体内容
-
用上述两种方法,转
Data
对象为其他对象后,查看转换后的对象:print(data.to_namedtuple()) """ DataTuple(x=tensor([[1., 0., 0., ..., 0., 0., 0.], [0., 1., 0., ..., 0., 0., 0.], [0., 0., 1., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 1., 0., 0.], [0., 0., 0., ..., 0., 1., 0.], [0., 0., 0., ..., 0., 0., 1.]]), edge_index=tensor([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 10, 10, 10, 11, 12, 12, 13, 13, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33], [ 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 17, 19, 21, 31, 0, 2, 3, 7, 13, 17, 19, 21, 30, 0, 1, 3, 7, 8, 9, 13, 27, 28, 32, 0, 1, 2, 7, 12, 13, 0, 6, 10, 0, 6, 10, 16, 0, 4, 5, 16, 0, 1, 2, 3, 0, 2, 30, 32, 33, 2, 33, 0, 4, 5, 0, 0, 3, 0, 1, 2, 3, 33, 32, 33, 32, 33, 5, 6, 0, 1, 32, 33, 0, 1, 33, 32, 33, 0, 1, 32, 33, 25, 27, 29, 32, 33, 25, 27, 31, 23, 24, 31, 29, 33, 2, 23, 24, 33, 2, 31, 33, 23, 26, 32, 33, 1, 8, 32, 33, 0, 24, 25, 28, 32, 33, 2, 8, 14, 15, 18, 20, 22, 23, 29, 30, 31, 33, 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32]]), y=tensor([0, 0, 0, 0, 3, 3, 3, 0, 1, 0, 3, 0, 0, 0, 1, 1, 3, 0, 1, 0, 1, 0, 1, 1, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1]), train_mask=tensor([ True, False, False, False, True, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False])) """ print(data.to_dict()) """ {'edge_index': tensor([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 10, 10, 10, 11, 12, 12, 13, 13, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33], [ 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 17, 19, 21, 31, 0, 2, 3, 7, 13, 17, 19, 21, 30, 0, 1, 3, 7, 8, 9, 13, 27, 28, 32, 0, 1, 2, 7, 12, 13, 0, 6, 10, 0, 6, 10, 16, 0, 4, 5, 16, 0, 1, 2, 3, 0, 2, 30, 32, 33, 2, 33, 0, 4, 5, 0, 0, 3, 0, 1, 2, 3, 33, 32, 33, 32, 33, 5, 6, 0, 1, 32, 33, 0, 1, 33, 32, 33, 0, 1, 32, 33, 25, 27, 29, 32, 33, 25, 27, 31, 23, 24, 31, 29, 33, 2, 23, 24, 33, 2, 31, 33, 23, 26, 32, 33, 1, 8, 32, 33, 0, 24, 25, 28, 32, 33, 2, 8, 14, 15, 18, 20, 22, 23, 29, 30, 31, 33, 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32]]), 'train_mask': tensor([ True, False, False, False, True, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False]), 'x': tensor([[1., 0., 0., ..., 0., 0., 0.], [0., 1., 0., ..., 0., 0., 0.], [0., 0., 1., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 1., 0., 0.], [0., 0., 0., ..., 0., 1., 0.], [0., 0., 0., ..., 0., 0., 1.]]), 'y': tensor([0, 0, 0, 0, 3, 3, 3, 0, 1, 0, 3, 0, 0, 0, 1, 1, 3, 0, 1, 0, 1, 0, 1, 1, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1])} """
-
用
Data
内置的__iter__
方法查看:from torch_geometric.datasets import KarateClub dataset = KarateClub() data = dataset[0] __data__ = data.__iter__() print(__data__) # <generator object Data.__iter__ at 0x7fb924b21120>
这里发现它生成了一个生成器对象,通过一些方法进行查看:
print(list(__data__)) print(tuple(__data__)) for i in __data__: print(i) """ [('edge_index', tensor([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 10, 10, 10, 11, 12, 12, 13, 13, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33], [ 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 17, 19, 21, 31, 0, 2, 3, 7, 13, 17, 19, 21, 30, 0, 1, 3, 7, 8, 9, 13, 27, 28, 32, 0, 1, 2, 7, 12, 13, 0, 6, 10, 0, 6, 10, 16, 0, 4, 5, 16, 0, 1, 2, 3, 0, 2, 30, 32, 33, 2, 33, 0, 4, 5, 0, 0, 3, 0, 1, 2, 3, 33, 32, 33, 32, 33, 5, 6, 0, 1, 32, 33, 0, 1, 33, 32, 33, 0, 1, 32, 33, 25, 27, 29, 32, 33, 25, 27, 31, 23, 24, 31, 29, 33, 2, 23, 24, 33, 2, 31, 33, 23, 26, 32, 33, 1, 8, 32, 33, 0, 24, 25, 28, 32, 33, 2, 8, 14, 15, 18, 20, 22, 23, 29, 30, 31, 33, 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32]])), ('train_mask', tensor([ True, False, False, False, True, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False])), ('x', tensor([[1., 0., 0., ..., 0., 0., 0.], [0., 1., 0., ..., 0., 0., 0.], [0., 0., 1., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 1., 0., 0.], [0., 0., 0., ..., 0., 1., 0.], [0., 0., 0., ..., 0., 0., 1.]])), ('y', tensor([0, 0, 0, 0, 3, 3, 3, 0, 1, 0, 3, 0, 0, 0, 1, 1, 3, 0, 1, 0, 1, 0, 1, 1, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1]))] """ # 其他两种方式只有最外围括号变了,这里不贴结果了
查看输出可以看到,
to_namedtuple()
的顺序发生了改变。这里稍微深究一下原因:查看
to_namedtuple
的源码:def to_namedtuple(self): keys = self.keys DataTuple = collections.namedtuple('DataTuple', keys) return DataTuple(*[self[key] for key in keys])
可以看到顺序跟
key
的顺序有关。
查看keys
的源码:def keys(self): r"""Returns all names of graph attributes.""" keys = [key for key in self.__dict__.keys() if self[key] is not None] keys = [key for key in keys if key[:2] != '__' and key[-2:] != '__'] return keys
跳转到
__init__
的源码:def __init__(self, x=None, edge_index=None, edge_attr=None, y=None, pos=None, normal=None, face=None, **kwargs): self.x = x self.edge_index = edge_index self.edge_attr = edge_attr self.y = y self.pos = pos self.normal = normal self.face = face for key, item in kwargs.items(): if key == 'num_nodes': self.__num_nodes__ = item else: self[key] = item if edge_index is not None and edge_index.dtype != torch.long: raise ValueError( (f'Argument `edge_index` needs to be of type `torch.long` but ' f'found type `{edge_index.dtype}`.')) if face is not None and face.dtype != torch.long: raise ValueError( (f'Argument `face` needs to be of type `torch.long` but found ' f'type `{face.dtype}`.')) if torch_geometric.is_debug_enabled(): self.debug()
综上可以看到顺序与
__init__
有关
获取Data
对象属性
x = graph_data['x']
# 这里的x也可以是edge_index或者其他的之类的
设置Data
对象属性
graph_data['x'] = x
# 同上
获取Data
对象包含的属性的关键字
graph_data.keys()
对边排序并移除重复的边
graph_data.coalesce()
三、Dataset
类——PyG中图数据集的表示及其使用
PyG内置了大量常用的基准数据集,接下来我们以PyG内置的Planetoid
数据集为例,来学习PyG中图数据集的表示及使用。
生成数据集对象并分析数据集
Planetoid
数据集类的官方文档为torch_geometric.datasets.Planetoid。
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='./dataset', name='Cora')
# Cora()
len(dataset)
# 1
dataset.num_classes
# 7
dataset.num_node_features
# 1433
# 意味着这个数据集只有一个图,包含7个分类任务,节点的属性为1433维度。
注意这里需要使用相对路径
./
表示目前所在目录。(/
表示根目录,../
表示上一层目录)Cora数据集由机器学习论文组成。 这些论文分为以下七个类别之一:
- 基于案例
- 遗传算法
- 神经网络
- 概率方法
- 强化学习
- 规则学习
- 理论
这些论文的选择方式是,在最终语料库中,每篇论文引用或被至少一篇其他论文引用。整个语料库中有 2708篇论文。
在词干堵塞和去除词尾后,只剩下 1433个 唯一的单词。文档频率小于10的所有单词都被删除。
现在我们看到该数据集包含的唯一的图,有2708个节点(2708篇论文),节点特征为1433维(1433个唯一的单词),有10556条边,有140个用作训练集的节点,有500个用作验证集的节点,有1000个用作测试集的节点。
数据集的使用
假设我们定义好了一个图神经网络模型,其名为Net
。在下方的代码中,我们展示了节点分类图数据集在训练过程中的使用。
model = Net().to(device)
data = dataset[0].to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
model.train()
for epoch in range(200):
optimizer.zero_grad()
out = model(data)
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
四、作业实践——“机构-作者-论文”网络的构建
请通过继承Data类实现一个类,专门用于表示“机构-作者-论文”的网络。该网络包含“机构“、”作者“和”论文”三类节点,以及“作者-机构“和“作者-论文“两类边。对要实现的类的要求:1)用不同的属性存储不同节点的属性;2)用不同的属性存储不同的边(边没有属性);3)逐一实现获取不同节点数量的方法。
解答:
我们先画个图:
class MyData(Data):
def __init__(self, paper_x=None, author_x=None, institution_x=None, write_edge_index=None, contribute_edge_index=None, write_edge_attr=None, contribute_edge_attr=None, y=None, **kwargs):
self.paper_x = paper_x # 论文节点属性矩阵
self.author_x = author_x # 作者节点属性矩阵
self.institution_x = institution_x # 机构节点属性矩阵
self.write_edge_index = write_edge_index # “作者-论文”边索引矩阵
self.contribute_edge_index = contribute_edge_index # “作者-机构“边索引矩阵
self.write_edge_attr = write_edge_attr # “作者-论文”边属性矩阵
self.contribute_edge_attr = contribute_edge_attr # “作者-机构”边属性矩阵
self.y = y #
def num_paper_x()
self.paper_x.shape[0] # 0为paper数量,1为paper 特征数量
def num_author_x()
self.author_x.shape[0]
def num_institution_x()
self.institution_x.shape[0]