1.5 异构图

(中文版)

异构图可以包含不同类型的节点和边。不同类型的节点/边具有独立的ID空间和特征存储。例如在下图中,用户和游戏节点的ID都从零开始,并且它们具有不同的特征。

https://data.dgl.ai/asset/image/user_guide_graphch_2.png

一个包含两种类型节点(用户和游戏)和两种类型边(关注和玩)的异构图示例。

创建异构图

在DGL中,异构图(简称heterograph)通过一系列图来指定,每个图对应一个关系。每个关系是一个字符串三元组(source node type, edge type, destination node type)。由于关系消除了边类型的歧义,DGL称它们为规范边类型。

以下代码片段是DGL中创建异构图的一个示例。

>>> import dgl
>>> import torch as th

>>> # Create a heterograph with 3 node types and 3 edges types.
>>> graph_data = {
...    ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
...    ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
...    ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
... }
>>> g = dgl.heterograph(graph_data)
>>> g.ntypes
['disease', 'drug', 'gene']
>>> g.etypes
['interacts', 'interacts', 'treats']
>>> g.canonical_etypes
[('drug', 'interacts', 'drug'),
 ('drug', 'interacts', 'gene'),
 ('drug', 'treats', 'disease')]

请注意,同质图和二分图只是具有一种关系的特殊异质图。

>>> # A homogeneous graph
>>> dgl.heterograph({('node_type', 'edge_type', 'node_type'): (u, v)})
>>> # A bipartite graph
>>> dgl.heterograph({('source_type', 'edge_type', 'destination_type'): (u, v)})

与异构图相关联的元图是图的模式。它指定了节点集和节点之间边的类型约束。元图中的节点\(u\)对应于相关联的异构图中的节点类型。元图中的边\((u, v)\)表示在相关联的异构图中存在从类型为\(u\)的节点到类型为\(v\)的节点的边。

>>> g
Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},
      num_edges={('drug', 'interacts', 'drug'): 2,
                 ('drug', 'interacts', 'gene'): 2,
                 ('drug', 'treats', 'disease'): 1},
      metagraph=[('drug', 'drug', 'interacts'),
                 ('drug', 'gene', 'interacts'),
                 ('drug', 'disease', 'treats')])
>>> g.metagraph().edges()
OutMultiEdgeDataView([('drug', 'drug'), ('drug', 'gene'), ('drug', 'disease')])

查看API:dgl.heterograph(), ntypes, etypes, canonical_etypes, metagraph.

处理多种类型

当引入多个节点/边类型时,用户需要指定特定的节点/边类型以调用DGLGraph API获取类型特定的信息。此外,不同类型的节点/边具有独立的ID。

>>> # Get the number of all nodes in the graph
>>> g.num_nodes()
10
>>> # Get the number of drug nodes
>>> g.num_nodes('drug')
3
>>> # Nodes of different types have separate IDs,
>>> # hence not well-defined without a type specified
>>> g.nodes()
DGLError: Node type name must be specified if there are more than one node types.
>>> g.nodes('drug')
tensor([0, 1, 2])

要为特定节点/边类型设置/获取特征,DGL提供了两种新的语法—— g.nodes[‘node_type’].data[‘feat_name’]g.edges[‘edge_type’].data[‘feat_name’]

>>> # Set/get feature 'hv' for nodes of type 'drug'
>>> g.nodes['drug'].data['hv'] = th.ones(3, 1)
>>> g.nodes['drug'].data['hv']
tensor([[1.],
        [1.],
        [1.]])
>>> # Set/get feature 'he' for edge of type 'treats'
>>> g.edges['treats'].data['he'] = th.zeros(1, 1)
>>> g.edges['treats'].data['he']
tensor([[0.]])

如果图只有一个节点/边类型,则无需指定节点/边类型。

>>> g = dgl.heterograph({
...    ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
...    ('drug', 'is similar', 'drug'): (th.tensor([0, 1]), th.tensor([2, 3]))
... })
>>> g.nodes()
tensor([0, 1, 2, 3])
>>> # To set/get feature with a single type, no need to use the new syntax
>>> g.ndata['hv'] = th.ones(4, 1)

注意

当边的类型唯一决定了源节点和目标节点的类型时,可以使用一个字符串而不是字符串三元组来指定边的类型。例如,对于一个具有两种关系('user', 'plays', 'game')('user', 'likes', 'game')的异构图,可以安全地使用'plays''likes'来指代这两种关系。

从磁盘加载异构图

Comma Separated Values (CSV)

存储异构图的一种常见方法是将不同类型的节点和边存储在不同的CSV文件中。示例如下。

# data folder
data/
|-- drug.csv        # drug nodes
|-- gene.csv        # gene nodes
|-- disease.csv     # disease nodes
|-- drug-interact-drug.csv  # drug-drug interaction edges
|-- drug-interact-gene.csv  # drug-gene interaction edges
|-- drug-treat-disease.csv  # drug-treat-disease edges

与同构图的情况类似,可以使用像Pandas这样的包将CSV文件解析为numpy数组或框架张量,构建关系字典并从中构造异构图。该方法也适用于其他流行格式,如GML/JSON。

DGL Binary Format

DGL 提供了 dgl.save_graphs()dgl.load_graphs() 分别用于将异构图保存为二进制格式并从二进制格式加载它们。

边类型子图

可以通过指定要保留的关系来创建异质图的子图,如果有的话,特征也会被复制。

>>> g = dgl.heterograph({
...    ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
...    ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
...    ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
... })
>>> g.nodes['drug'].data['hv'] = th.ones(3, 1)

>>> # Retain relations ('drug', 'interacts', 'drug') and ('drug', 'treats', 'disease')
>>> # All nodes for 'drug' and 'disease' will be retained
>>> eg = dgl.edge_type_subgraph(g, [('drug', 'interacts', 'drug'),
...                                 ('drug', 'treats', 'disease')])
>>> eg
Graph(num_nodes={'disease': 3, 'drug': 3},
      num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'treats', 'disease'): 1},
      metagraph=[('drug', 'drug', 'interacts'), ('drug', 'disease', 'treats')])
>>> # The associated features will be copied as well
>>> eg.nodes['drug'].data['hv']
tensor([[1.],
        [1.],
        [1.]])

将异构图转换为同构图

异构图提供了一个清晰的接口,用于管理不同类型的节点/边及其相关特征。这在以下情况下特别有用:

  1. 不同类型节点/边的特征具有不同的数据类型或大小。

  2. 我们希望对不同类型的节点/边应用不同的操作。

如果上述条件不成立,并且在建模中不希望区分节点/边类型,那么DGL允许使用dgl.DGLGraph.to_homogeneous() API将异构图转换为同构图。其过程如下:

  1. 使用从0开始的连续整数重新标记所有类型的节点/边

  2. 合并用户指定的节点/边类型的特征。

>>> g = dgl.heterograph({
...    ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
...    ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))})
>>> g.nodes['drug'].data['hv'] = th.zeros(3, 1)
>>> g.nodes['disease'].data['hv'] = th.ones(3, 1)
>>> g.edges['interacts'].data['he'] = th.zeros(2, 1)
>>> g.edges['treats'].data['he'] = th.zeros(1, 2)

>>> # By default, it does not merge any features
>>> hg = dgl.to_homogeneous(g)
>>> 'hv' in hg.ndata
False

>>> # Copy edge features
>>> # For feature copy, it expects features to have
>>> # the same size and dtype across node/edge types
>>> hg = dgl.to_homogeneous(g, edata=['he'])
DGLError: Cannot concatenate column ‘he’ with shape Scheme(shape=(2,), dtype=torch.float32) and shape Scheme(shape=(1,), dtype=torch.float32)

>>> # Copy node features
>>> hg = dgl.to_homogeneous(g, ndata=['hv'])
>>> hg.ndata['hv']
tensor([[1.],
        [1.],
        [1.],
        [0.],
        [0.],
        [0.]])

原始的节点/边类型和类型特定的ID存储在ndataedata中。

>>> # Order of node types in the heterograph
>>> g.ntypes
['disease', 'drug']
>>> # Original node types
>>> hg.ndata[dgl.NTYPE]
tensor([0, 0, 0, 1, 1, 1])
>>> # Original type-specific node IDs
>>> hg.ndata[dgl.NID]
tensor([0, 1, 2, 0, 1, 2])

>>> # Order of edge types in the heterograph
>>> g.etypes
['interacts', 'treats']
>>> # Original edge types
>>> hg.edata[dgl.ETYPE]
tensor([0, 0, 1])
>>> # Original type-specific edge IDs
>>> hg.edata[dgl.EID]
tensor([0, 1, 0])

出于建模目的,可能希望将一些关系分组并对它们应用相同的操作。为了满足这一需求,可以首先获取异构图的一个边类型子图,然后将该子图转换为同构图。

>>> g = dgl.heterograph({
...    ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
...    ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
...    ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
... })
>>> sub_g = dgl.edge_type_subgraph(g, [('drug', 'interacts', 'drug'),
...                                    ('drug', 'interacts', 'gene')])
>>> h_sub_g = dgl.to_homogeneous(sub_g)
>>> h_sub_g
Graph(num_nodes=7, num_edges=4,
      ...)