项目文件夹

文件
Mufei Li be444e52d9 [Doc/Feature] Refactor, doc update and behavior fix for graphs (#1983)
* Update graph

* Fix for dgl.graph

* from_scipy

* Replace canonical_etypes with relations

* from_networkx

* Update for hetero_from_relations

* Roll back the change of canonical_etypes to relations

* heterograph

* bipartite

* Update doc

* Fix lint

* Fix lint

* Fix test cases

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Update

* Fix test

* Fix

* Update

* Use DGLError

* Update

* Update

* Update

* Update

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Update

* Fix

* Update

* Fix

* Fix

* Fix

* Update

* Fix

* Update

* Fix

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Fix

* Fix

* Update

* Update

* Update

* Update

* Update

* Update

* rewrite sanity checks

* delete unnecessary checks

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Fix

* Update

* Update

* Update

* Fix

* Fix

* Fix

* Update

* Fix

* Update

* Fix

* Fix

* Update

* Fix

* Update

* Fix

Co-authored-by: xiang song(charlie.song) <classicxsong@gmail.com>
Co-authored-by: Minjie Wang <wmjlyjemaine@gmail.com>
Co-authored-by: Quan Gan <coin2028@hotmail.com>
2020-08-18 04:26:29 +08:00

305 行
11 KiB
Python

"""Data utilities."""
import scipy as sp
import networkx as nx
from ..base import DGLError
from .. import backend as F
from . import checks
def elist2tensor(elist, idtype):
"""Function to convert an edge list to edge tensors.
Parameters
----------
elist : iterable of int pairs
List of (src, dst) node ID pairs.
idtype : int32, int64, optional
Integer ID type. Must be int32 or int64.
Returns
-------
(Tensor, Tensor)
Edge tensors.
"""
if len(elist) == 0:
u, v = [], []
else:
u, v = zip(*elist)
u = list(u)
v = list(v)
return F.tensor(u, idtype), F.tensor(v, idtype)
def scipy2tensor(spmat, idtype):
"""Function to convert a scipy matrix to edge tensors.
Parameters
----------
spmat : scipy.sparse.spmatrix
SciPy sparse matrix.
idtype : int32, int64, optional
Integer ID type. Must be int32 or int64.
Returns
-------
(Tensor, Tensor)
Edge tensors.
"""
spmat = spmat.tocoo()
row = F.tensor(spmat.row, idtype)
col = F.tensor(spmat.col, idtype)
return row, col
def networkx2tensor(nx_graph, idtype, edge_id_attr_name=None):
"""Function to convert a networkx graph to edge tensors.
Parameters
----------
nx_graph : nx.Graph
NetworkX graph.
idtype : int32, int64, optional
Integer ID type. Must be int32 or int64.
edge_id_attr_name : str, optional
Key name for edge ids in the NetworkX graph. If not found, we
will consider the graph not to have pre-specified edge ids. (Default: None)
Returns
-------
(Tensor, Tensor)
Edge tensors.
"""
if not nx_graph.is_directed():
nx_graph = nx_graph.to_directed()
# Relabel nodes using consecutive integers
nx_graph = nx.convert_node_labels_to_integers(nx_graph, ordering='sorted')
has_edge_id = edge_id_attr_name is not None
if has_edge_id:
num_edges = nx_graph.number_of_edges()
src = [0] * num_edges
dst = [0] * num_edges
for u, v, attr in nx_graph.edges(data=True):
eid = int(attr[edge_id_attr_name])
if eid < 0 or eid >= nx_graph.number_of_edges():
raise DGLError('Expect edge IDs to be a non-negative integer smaller than {:d}, '
'got {:d}'.format(num_edges, eid))
src[eid] = u
dst[eid] = v
else:
src = []
dst = []
for e in nx_graph.edges:
src.append(e[0])
dst.append(e[1])
src = F.tensor(src, idtype)
dst = F.tensor(dst, idtype)
return src, dst
def graphdata2tensors(data, idtype=None, bipartite=False, **kwargs):
"""Function to convert various types of data to edge tensors and infer
the number of nodes.
Parameters
----------
data : graph data
Various kinds of graph data.
idtype : int32, int64, optional
Integer ID type. If None, try infer from the data and if fail use
int64.
bipartite : bool, optional
Whether infer number of nodes of a bipartite graph --
num_src and num_dst can be different.
kwargs
- edge_id_attr_name : The name (str) of the edge attribute that stores the edge
IDs in the NetworkX graph.
- top_map : The dictionary mapping the original IDs of the source nodes to the
new ones.
- bottom_map : The dictionary mapping the original IDs of the destination nodes
to the new ones.
Returns
-------
src : Tensor
Src nodes.
dst : Tensor
Dst nodes.
num_src : int
Number of source nodes
num_dst : int
Number of destination nodes.
"""
if idtype is None and not (isinstance(data, tuple) and F.is_tensor(data[0])):
# preferred default idtype is int64
# if data is tensor and idtype is None, infer the idtype from tensor
idtype = F.int64
checks.check_valid_idtype(idtype)
if isinstance(data, tuple) and (not F.is_tensor(data[0]) or not F.is_tensor(data[1])):
# (Iterable, Iterable) type data, convert it to (Tensor, Tensor)
if len(data[0]) == 0:
# force idtype for empty list
data = F.tensor(data[0], idtype), F.tensor(data[1], idtype)
else:
# convert the iterable to tensor and keep its native data type so we can check
# its validity later
data = F.tensor(data[0]), F.tensor(data[1])
if isinstance(data, tuple):
# (Tensor, Tensor) type data
src, dst = data
# sanity checks
# TODO(minjie): move these checks to C for faster graph construction.
if F.dtype(src) != F.dtype(dst):
raise DGLError('Expect the source and destination node IDs to have the same type,'
' but got {} and {}.'.format(F.dtype(src), F.dtype(dst)))
if F.context(src) != F.context(dst):
raise DGLError('Expect the source and destination node IDs to be on the same device,'
' but got {} and {}.'.format(F.context(src), F.context(dst)))
if F.dtype(src) not in (F.int32, F.int64):
raise DGLError('Expect the source ID tensor to have data type int32 or int64,'
' but got {}.'.format(F.dtype(src)))
if F.dtype(dst) not in (F.int32, F.int64):
raise DGLError('Expect the destination ID tensor to have data type int32 or int64,'
' but got {}.'.format(F.dtype(dst)))
if idtype is not None:
src, dst = F.astype(src, idtype), F.astype(dst, idtype)
elif isinstance(data, list):
src, dst = elist2tensor(data, idtype)
elif isinstance(data, sp.sparse.spmatrix):
src, dst = scipy2tensor(data, idtype)
elif isinstance(data, nx.Graph):
edge_id_attr_name = kwargs.get('edge_id_attr_name', None)
if bipartite:
top_map = kwargs.get('top_map')
bottom_map = kwargs.get('bottom_map')
src, dst = networkxbipartite2tensors(
data, idtype, top_map=top_map,
bottom_map=bottom_map, edge_id_attr_name=edge_id_attr_name)
else:
src, dst = networkx2tensor(
data, idtype, edge_id_attr_name=edge_id_attr_name)
else:
raise DGLError('Unsupported graph data type:', type(data))
if len(src) != len(dst):
raise DGLError('Expect the source and destination ID tensors to have the same length,'
' but got {} and {}.'.format(len(src), len(dst)))
if len(src) > 0 and (F.as_scalar(F.min(src, 0)) < 0 or F.as_scalar(F.min(dst, 0)) < 0):
raise DGLError('All IDs must be non-negative integers.')
# infer number of nodes
infer_from_raw = infer_num_nodes(data, bipartite=bipartite)
if infer_from_raw is None:
num_src, num_dst = infer_num_nodes((src, dst), bipartite=bipartite)
else:
num_src, num_dst = infer_from_raw
return src, dst, num_src, num_dst
def networkxbipartite2tensors(nx_graph, idtype, top_map, bottom_map, edge_id_attr_name=None):
"""Function to convert a networkx bipartite to edge tensors.
Parameters
----------
nx_graph : nx.Graph
NetworkX graph. It must follow the bipartite graph convention of networkx.
Each node has an attribute ``bipartite`` with values 0 and 1 indicating
which set it belongs to.
top_map : dict
The dictionary mapping the original node labels to the node IDs for the source type.
bottom_map : dict
The dictionary mapping the original node labels to the node IDs for the destination type.
idtype : int32, int64, optional
Integer ID type. Must be int32 or int64.
edge_id_attr_name : str, optional
Key name for edge ids in the NetworkX graph. If not found, we
will consider the graph not to have pre-specified edge ids. (Default: None)
Returns
-------
(Tensor, Tensor)
Edge tensors.
"""
has_edge_id = edge_id_attr_name is not None
if has_edge_id:
num_edges = nx_graph.number_of_edges()
src = [0] * num_edges
dst = [0] * num_edges
for u, v, attr in nx_graph.edges(data=True):
if u not in top_map:
raise DGLError('Expect the node {} to have attribute bipartite=0 '
'with edge {}'.format(u, (u, v)))
if v not in bottom_map:
raise DGLError('Expect the node {} to have attribute bipartite=1 '
'with edge {}'.format(v, (u, v)))
eid = int(attr[edge_id_attr_name])
if eid < 0 or eid >= nx_graph.number_of_edges():
raise DGLError('Expect edge IDs to be a non-negative integer smaller than {:d}, '
'got {:d}'.format(num_edges, eid))
src[eid] = top_map[u]
dst[eid] = bottom_map[v]
else:
src = []
dst = []
for e in nx_graph.edges:
u, v = e[0], e[1]
if u not in top_map:
raise DGLError('Expect the node {} to have attribute bipartite=0 '
'with edge {}'.format(u, (u, v)))
if v not in bottom_map:
raise DGLError('Expect the node {} to have attribute bipartite=1 '
'with edge {}'.format(v, (u, v)))
src.append(top_map[u])
dst.append(bottom_map[v])
src = F.tensor(src, dtype=idtype)
dst = F.tensor(dst, dtype=idtype)
return src, dst
def infer_num_nodes(data, bipartite=False):
"""Function for inferring the number of nodes.
Parameters
----------
data : graph data
Supported types are:
* Tensor pair (u, v)
* SciPy matrix
* NetworkX graph
bipartite : bool, optional
Whether infer number of nodes of a bipartite graph --
num_src and num_dst can be different.
Returns
-------
num_src : int
Number of source nodes.
num_dst : int
Number of destination nodes.
or
None
If the inference failed.
"""
if isinstance(data, tuple) and len(data) == 2 and F.is_tensor(data[0]):
u, v = data
nsrc = F.as_scalar(F.max(u, dim=0)) + 1 if len(u) > 0 else 0
ndst = F.as_scalar(F.max(v, dim=0)) + 1 if len(v) > 0 else 0
elif isinstance(data, sp.sparse.spmatrix):
nsrc, ndst = data.shape[0], data.shape[1]
elif isinstance(data, nx.Graph):
if data.number_of_nodes() == 0:
nsrc = ndst = 0
elif not bipartite:
nsrc = ndst = data.number_of_nodes()
else:
nsrc = len({n for n, d in data.nodes(data=True) if d['bipartite'] == 0})
ndst = data.number_of_nodes() - nsrc
else:
return None
if not bipartite:
nsrc = ndst = max(nsrc, ndst)
return nsrc, ndst