1.5 异构图

(中文版)

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

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

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

创建异构图

在 DGL 中,异构图(简称 heterograph)由一系列图指定,如下所示,每个关系一个图。每个关系是一个字符串三元组 (源节点类型, 边类型, 目标节点类型)。由于关系消除了边类型的歧义,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)})

与异构图关联的 metagraph 是图的模式。它指定了节点集以及节点之间边的类型约束。metagraph 中的节点 \(u\) 对应于相关异构图中的一个节点类型。metagraph 中的边 \((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' 来指代这两个关系。

从磁盘加载异构图

逗号分隔值 (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 二进制格式

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,
      ...)