一个图看起来是由一些小圆点(称为顶点或结点)和连接这些圆点的直线或曲线(称为边)组成的
图的定义:
-
图G是由两个集合V和E组成(记做 G = (V, E)):
V = {v1,v2,v3,...vn,} 是由G的结点(vertex)组成的集合
E = {e1,e2,e3,...en} 是由连接两个结点的边(edge)组成的集合
-
(无向图)若边e(唯一的边)连接结点v1和v2, 则表示为 e = (v1,v2)或 e = (v2,v1),表示连接结点v1和结点v2的边
-
(有向图)若边e(唯一的边)连接有序结点对v1和v2, 则表示为 e = (v1,v2),
表示一条从结点v1到结点v2的边
-
(有向图和无向图)G中的一条边连接结点v1和结点v2,则称结点v1和结点v2相关联,结点v1和结点v2是相邻结点
-
(有向图和无向图)一般情况下,E 和 V 都是有限的集合,且 V 为非空
在此无边图中
结点v1、结点v2、结点v3和结点v4都没有边与之相连,所以称这四个结点为孤立顶点(isolated vertex)
图的分类:
图的分类很多种,包括有/无向图,简单图/多重图等等
-
有向图(directed graph):图的每一条边带有一个箭头,表示一个方向
-
无向图(undirected graph):图的每一条边不带箭头,没有方向
-
简单图(simple graph):既没有圈也没有平行边的图称为简单图
-
多重图(multigraph):含有圈和平行边的图,支持两结点间的边数多于一条
一般情况下所称的图是无向图,圈和平行边的定义将在下文给出。
在此多重图中
- 边e2和边e3都连接了结点v2和结点v3,所以称边e2和边e3为平行边(parallel edge)。
- 边e5 = (v4,v4),所以称边e5为圈(loop)。
图的结构
将以此图举例解释以下内容
路径
- 路径(path):从一个结点v1到另一个结点vn所经过的路程,表示为(v1,e1,v2,e2,...en,vn)
例如:(v1,e10,v7,e7,v6,e5,v5,e4,v4,e6,v7)
- 简单路径(simple path):从结点vi到vj的不存在重复结点的路径
例如:(v1,e1,v2,e2,v3) -
回路
- 回路(cycle):从vi到vi的路径,长度非0,不存在重复边的路径
例如:(v1,e10,v7,e7,v6,e5,v5,e4,v4,e6,v7,e9,v8,e11,v1) -
- 简单回路(simple cycle):从vi到vi的回路,除了开始和结束的结点相同之外,不存在相同的结点
例如:(v1,e10,v7,e9,v8,e11,v1) -
连通图(connected graph)
- 存在从任意一个结点vi到另外一个任意结点vj的路径的图(所有结点都是连通的图),反之,就是非连通图,上述的简单图和多重图都是连通图
子图
- 在非联通图G中,通常有多个部分,每个部分都称为G的子图
- G1 = (V, E), V = {v1,,v2,v3}, E = {e1,e2,e3}
G2 = (V, E), V = {v4,v5}, E = {e4}
G3 = (V, E), V = {v6}, E为空集
PyTorch Geometric 基础知识
这一部分我们介绍一下 PyG 的基础知识,主要包括 torch_geometric.data 部分。另外,还会介绍怎么设计自己的 消息传递(Message Passing)范式 。
Data 类
Data
类的官方文档为torch_geometric.data.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
torch_geometric.data 包里有一个 Data 类,通过 Data 类我们可以很方便的创建图结构。
定义一个图结构,需要以下变量:
- 每个节点(node)的 features
- 边的连接关系或者边的 features
我们以下面的图结构为例,看看怎么用 Data 类创建图结构:
- 图的节点x可以根据其值进行向量表示,而节点与节点间使用邻接矩阵(这里用的边表edge_index)来表示。
- 邻接矩阵dege_index主要由源节点(第一列)和目标节点(第二列)组成。源节点和目标节点顺序对应。比如 ,在图中,节点0的目标节点(指向的点)有节点1和节点3,因此节点0可以用
[[0,0],[1,3]]
来表示。所以,邻接矩阵的关键是,源节点列和目标节点列的对应关系表示。
在上图中,一共有四个节点 v1,v2,v3,v4,其中每个节点都有一个二维的特征向量和一个标签 y。这个特征向量和标签可以用 FloatTensor
来表示:
x = torch.tensor([[2,1], [5,6], [3,7], [12,0]], dtype=torch.float)
y = torch.tensor([0, 1, 0, 1], dtype=torch.float)
图的连接关系(边)可以用 COO 格式表示。COO 格式的维度是 [2, num_edges]
,其中第一个列表是所有边上起始节点的 index,第二个列表是对应边上目标节点的 index:
edge_index = torch.tensor([[0, 1, 2, 0, 3],
[1, 0, 1, 3, 2]], dtype=torch.long)
注意上面的数据里定义边的顺序是无关紧要的,这个数据仅仅用来计算邻接矩阵用的,比如上面的定义和下面的定义是等价的:
edge_index = torch.tensor([[0, 2, 1, 0, 3],
[3, 1, 0, 1, 2]], dtype=torch.long)
综上所述,我们可以这样定义上面的图结构:
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])
作业
-
请通过继承
Data
类实现一个类,专门用于表示“机构-作者-论文”的网络。该网络包含“机构“、”作者“和”论文”三类节点,以及“作者-机构“和“作者-论文“两类边。 -
对要实现的类的要求:
-
1)用不同的属性存储不同节点的属性;
-
2)用不同的属性存储不同的边(边没有属性);
-
3)逐一实现获取不同节点数量的方法。
class Net(Data):
def __init__(self, institution_x, author_x, paper_x, work_edge_index, publish_edge_index, work_edge_attr, publish_edge_attr, y, **kwargs):
super().__init__(**kwargs)
self.institution_x = institution_x
self.author_x = author_x
self.paper_x = paper_x
self.work_edge_index = work_edge_index
self.publish_edge_index = publish_edge_index
self.work_edge_attr = work_edge_attr
self.publish_edge_attr = publish_edge_attr
self.y = y
@property
def institution_nums(self):
return self.institution_x.shape[0]
@property
def author_nums(self):
return self.author_x.shape[0]
@property
def paper_nums(self):
return self.paper_x.shape[0]