通过本节,将学到:

  • 从头开始用DGL构建一个Graph
  • 给Graph添加节点和边的特征
  • 获取一些图的信息,如节点的度或节点的其他属性
  • 将DGL graph 转换到另一个graph
  • 加载、保存DGL graph

从头构建Graph

DGL通过DGLGraph对象来创建一个有向图,我们可以直接通过指定节点,以及src节点和target节点来创建一个graph。节点的id从0开始。

例如下面一段代码构建了一个有向星型图,共有6个节点,中心节点的id是0,边分别是从中心到叶子节点。

import dgl
import numpy as np
import torchg = dgl.graph(([0, 0, 0, 0, 0], [1, 2, 3, 4, 5]), num_nodes=6)
# Equivalently, PyTorch LongTensors also work.
g = dgl.graph((torch.LongTensor([0, 0, 0, 0, 0]), torch.LongTensor([1, 2, 3, 4, 5])), num_nodes=6)# You can omit the number of nodes argument if you can tell the number of nodes from the edge list alone.
g = dgl.graph(([0, 0, 0, 0, 0], [1, 2, 3, 4, 5]))
print(g.edges())

输出:

(tensor([0, 0, 0, 0, 0]), tensor([1, 2, 3, 4, 5]))

注意DGLGraph默认使用有向图以方便计算,如果要创建无向图,需要创建一个bidirectional graph。

指定节点和边的特征

很多graph的节点和边都包含属性,虽然在现实中,节点和边的属性都是任意的,但是在DGLGraph中,只接受tensor(numerical contents)类型。因此,节点或边的属性必须拥有相同的shape,我们将节点或边的属性称作features

我们可以通过ndataedata接口来分配或检索节点和边的features

# Assign a 3-dimensional node feature vector for each node.
g.ndata['x'] = torch.randn(6, 3)
# Assign a 4-dimensional edge feature vector for each edge.
g.edata['a'] = torch.randn(5, 4)# 可以给每个节点一个5*4维的特征矩阵
g.ndata['y'] = torch.randn(6, 5, 4)
print(g.edata['a'])
print(g.ndata)

输出结果如下:

tensor([[-0.0667,  0.4955, -0.7605,  1.0864],[ 1.3225, -0.0638,  0.4249, -0.9549],[ 1.5967,  0.6893,  0.0693, -0.2029],[ 0.9383, -0.9211, -0.3480, -1.2578],[-0.2237, -0.3213,  0.2844, -2.1222]])
{'x': tensor([[-1.5809,  0.5587, -0.2951],[-0.6097, -0.6469,  1.1301],[-0.2151, -0.4234,  0.1623],[ 0.0742,  1.0390,  0.4727],[-0.9907,  0.0712, -1.9446],[-0.8176, -1.3116, -0.0609]]), 'y': tensor([[[ 0.5657,  1.5601, -0.5912, -0.9166],[-2.1264, -1.0500, -1.5964,  0.8197],[-0.1032,  0.8735,  0.5557,  0.2568],[-0.2407,  0.5534, -0.4418, -0.8438],[-1.3463, -1.3163,  0.4165,  0.4069]],[[ 0.5147,  0.7456,  0.5775, -0.8002],[-0.7700, -0.9576, -0.4264,  0.5365],[-0.2953, -1.0986, -0.7701,  0.6752],[-0.8701,  0.0455, -0.0241,  1.4218],[ 0.8420,  1.5854,  0.2167, -0.3292]],[[-1.9216, -1.4101, -0.8027, -0.1626],[ 0.8344,  0.6824, -0.2703, -0.6369],[-0.8784, -1.3154,  2.5829, -0.6084],[ 1.0764,  0.6415, -0.0548,  2.0256],[-0.2596,  0.9234, -0.7495,  0.4572]],[[ 0.4695,  0.4599,  1.0253, -1.6217],[-0.9483, -1.1822,  0.6945,  0.2053],[-1.6246, -0.2697,  0.3077, -0.1492],[ 0.1044,  1.3403, -2.2207,  0.7767],[-1.0187,  0.9309,  1.3097, -0.8092]],[[ 0.3566, -0.9140, -0.0288,  0.8432],[ 1.4894, -1.0284,  1.1628,  1.6677],[ 0.9602,  0.8019,  0.2072,  0.9472],[-0.8926,  0.6656,  0.2531,  0.1492],[ 1.2601,  0.5075,  0.6341, -0.6500]],[[-0.3595,  0.8457,  0.0679, -0.2255],[ 0.0466, -0.5428, -1.8730,  1.7333],[-1.2157,  0.6068, -0.4385, -1.6794],[ 0.2981,  1.2320, -0.1630,  2.4952],[-1.3436, -2.1708, -0.3203, -0.9315]]])}

目前有很多成熟的方法可以将属性编码成numerical features:
对于分类属性(比如性别,职业),可以通过one-hot进行编码
对于字符串型的属性(比如文章引用),可以考虑nlp模型
对于图像的属性,可以考虑CNN等视觉模型

获取Graph的结构信息

DGLGraph对象提供了很多方法来获取Graph的结构信息:

print(g.num_nodes())  # 6
print(g.num_edges())  # 5
# Out degrees of the center node
print(g.out_degrees(0))  # 5 出度 图中每个节点出度的最大值
# In degrees of the center node - note that the graph is directed so the in degree should be 0.
print(g.in_degrees(0))  # 0 入度 图中每个节点入度的最大值

Graph 转换

DGL提供了很多转换的API,例如从一个图中抽取一张子图。

# 从原始图中获取节点0、节点1和节点3来成成一个子图
sg1 = g.subgraph([0, 1, 3])
# 从原始图中抽取边0、边1和边3来成成一个子图
sg2 = g.edge_subgraph([0, 1, 3])

可以在子图中通过dgl.NID或者dgl.EID来获取父图中节点和边的映射。

# The original IDs of each node in sg1
print(sg1.ndata[dgl.NID])
# The original IDs of each edge in sg1
print(sg1.edata[dgl.EID])
# The original IDs of each node in sg2
print(sg2.ndata[dgl.NID])
# The original IDs of each edge in sg2
print(sg2.edata[dgl.EID])

输出:

tensor([0, 1, 3])
tensor([0, 2])
tensor([0, 1, 2, 4])
tensor([0, 1, 3])

subgraphedge_subgraph同样会将父图的属性复制一份给子图:

# sg1的每个原始节点的属性
print(sg1.ndata['x'])
# sg1的每个原始边的属性
print(sg1.edata['a'])
# sg2中每个原始节点的属性
print(sg2.ndata['x'])
# sg2中每个原始边的属性
print(sg2.edata['a'])

还有一种常用的转换方式就是通过给原始graph添加反向边dgl.add_reverse_edges

newg = dgl.add_reverse_edges(g)
newg.edges()

如果你有一个无向图,可以通过这种方式来转成bidirectional graph。这种方式会将原始图中所有的边都添加一个反向边。

保存、加载Graph

我们可以通过dgl.save_graphs来保存一个Graph或者一个Graph列表,通过dgl.load_graphs来加载graph

# 保存graph
dgl.save_graphs('graph.dgl', g)
dgl.save_graphs('graphs.dgl', [g, sg1, sg2])
# 加载graph
(g,), _ = dgl.load_graphs('graph.dgl')
print(g)
(g, sg1, sg2), _ = dgl.load_graphs('graphs.dgl')
print(g)
print(sg1)
print(sg2)

输出:

Graph(num_nodes=6, num_edges=5,ndata_schemes={'y': Scheme(shape=(5, 4), dtype=torch.float32), 'x': Scheme(shape=(3,), dtype=torch.float32)}edata_schemes={'a': Scheme(shape=(4,), dtype=torch.float32)})
Graph(num_nodes=6, num_edges=5,ndata_schemes={'y': Scheme(shape=(5, 4), dtype=torch.float32), 'x': Scheme(shape=(3,), dtype=torch.float32)}edata_schemes={'a': Scheme(shape=(4,), dtype=torch.float32)})
Graph(num_nodes=3, num_edges=2,ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64), 'x': Scheme(shape=(3,), dtype=torch.float32), 'y': Scheme(shape=(5, 4), dtype=torch.float32)}edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64), 'a': Scheme(shape=(4,), dtype=torch.float32)})
Graph(num_nodes=4, num_edges=3,ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64), 'x': Scheme(shape=(3,), dtype=torch.float32), 'y': Scheme(shape=(5, 4), dtype=torch.float32)}edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64), 'a': Scheme(shape=(4,), dtype=torch.float32)})