chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:51 +08:00
commit c36a561cd8
2172 changed files with 455595 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
"""
The ``dgl`` package contains data structure for storing structural and feature data
(i.e., the :class:`DGLGraph` class) and also utilities for generating, manipulating
and transforming graphs.
"""
# Windows compatibility
# This initializes Winsock and performs cleanup at termination as required
import socket
# Backend and logging should be imported before other modules.
from .logging import enable_verbose_logging # usort: skip
from .backend import backend_name, load_backend # usort: skip
from . import (
container,
cuda,
dataloading,
function,
ops,
random,
sampling,
storages,
)
from ._ffi.base import __version__, DGLError
from ._ffi.function import (
extract_ext_funcs,
get_global_func,
list_global_func_names,
register_func,
)
from ._ffi.runtime_ctypes import TypeCode
from .base import ALL, EID, ETYPE, NID, NTYPE
from .readout import *
from .batch import *
from .convert import *
from .generators import *
from .dataloading import (
set_dst_lazy_features,
set_edge_lazy_features,
set_node_lazy_features,
set_src_lazy_features,
)
from .heterograph import ( # pylint: disable=reimported
DGLGraph,
DGLGraph as DGLHeteroGraph,
)
from .merge import *
from .subgraph import *
from .traversal import *
from .transforms import *
from .propagate import *
from .random import *
from . import optim
from .data.utils import load_graphs, save_graphs
from .frame import LazyFeature
from .global_config import is_libxsmm_enabled, use_libxsmm
from .utils import apply_each
from .mpops import *
from .homophily import *
from .label_informativeness import *
+1
View File
@@ -0,0 +1 @@
"""Namespace for internal apis."""
+3
View File
@@ -0,0 +1,3 @@
# C API and runtime
Borrowed and adapted from TVM project. (commit: 2ce5277)
+10
View File
@@ -0,0 +1,10 @@
"""C interfacing code.
This namespace contains everything that interacts with C code.
Most C related object are ctypes compatible, which means
they contains a handle field that is ctypes.c_void_p and can
be used via ctypes function calls.
Some performance critical functions are implemented by cython
and have a ctypes fallback implementation.
"""
+1
View File
@@ -0,0 +1 @@
"""ctypes specific implementation of FFI"""
+298
View File
@@ -0,0 +1,298 @@
# coding: utf-8
# pylint: disable=invalid-name, protected-access, too-many-branches, global-statement
"""Function configuration API."""
from __future__ import absolute_import
import ctypes
import traceback
from numbers import Integral, Number
from ..base import _LIB, c_str, check_call, string_types
from ..object_generic import convert_to_object, ObjectGeneric
from ..runtime_ctypes import DGLByteArray, DGLContext, DGLDataType
from . import ndarray as _nd, object as _object
from .ndarray import _make_array, NDArrayBase
from .object import ObjectBase
from .types import (
_wrap_arg_func,
C_TO_PY_ARG_SWITCH,
DGLCFuncFinalizer,
DGLPackedCFunc,
DGLValue,
RETURN_SWITCH,
TypeCode,
)
FunctionHandle = ctypes.c_void_p
ModuleHandle = ctypes.c_void_p
DGLRetValueHandle = ctypes.c_void_p
def _ctypes_free_resource(rhandle):
"""callback to free resources when it it not needed."""
pyobj = ctypes.cast(rhandle, ctypes.py_object)
ctypes.pythonapi.Py_DecRef(pyobj)
# Global callback that is always alive
DGL_FREE_PYOBJ = DGLCFuncFinalizer(_ctypes_free_resource)
ctypes.pythonapi.Py_IncRef(ctypes.py_object(DGL_FREE_PYOBJ))
def convert_to_dgl_func(pyfunc):
"""Convert a python function to DGL function
Parameters
----------
pyfunc : python function
The python function to be converted.
Returns
-------
dglfunc: dgl.nd.Function
The converted dgl function.
"""
local_pyfunc = pyfunc
def cfun(args, type_codes, num_args, ret, _):
"""ctypes function"""
num_args = (
num_args.value if isinstance(num_args, ctypes.c_int) else num_args
)
pyargs = (
C_TO_PY_ARG_SWITCH[type_codes[i]](args[i]) for i in range(num_args)
)
# pylint: disable=broad-except
try:
rv = local_pyfunc(*pyargs)
except Exception:
msg = traceback.format_exc()
_LIB.DGLAPISetLastError(c_str(msg))
return -1
if rv is not None:
if isinstance(rv, tuple):
raise ValueError(
"PackedFunction can only support one return value"
)
temp_args = []
values, tcodes, _ = _make_dgl_args((rv,), temp_args)
if not isinstance(ret, DGLRetValueHandle):
ret = DGLRetValueHandle(ret)
check_call(
_LIB.DGLCFuncSetReturn(ret, values, tcodes, ctypes.c_int(1))
)
_ = temp_args
_ = rv
return 0
handle = FunctionHandle()
f = DGLPackedCFunc(cfun)
# NOTE: We will need to use python-api to increase ref count of the f
# DGL_FREE_PYOBJ will be called after it is no longer needed.
pyobj = ctypes.py_object(f)
ctypes.pythonapi.Py_IncRef(pyobj)
check_call(
_LIB.DGLFuncCreateFromCFunc(
f, pyobj, DGL_FREE_PYOBJ, ctypes.byref(handle)
)
)
return _CLASS_FUNCTION(handle, False)
def _make_dgl_args(args, temp_args):
"""Pack arguments into c args dgl call accept.
temp_args is used to temporarily save the arguments so they will not be
freed during C API function call.
"""
num_args = len(args)
values = (DGLValue * num_args)()
type_codes = (ctypes.c_int * num_args)()
for i, arg in enumerate(args):
if arg is None:
values[i].v_handle = None
type_codes[i] = TypeCode.NULL
elif isinstance(arg, ObjectBase):
values[i].v_handle = arg.handle
type_codes[i] = TypeCode.OBJECT_HANDLE
elif isinstance(arg, (list, tuple, dict, ObjectGeneric)):
arg = convert_to_object(arg)
values[i].v_handle = arg.handle
type_codes[i] = TypeCode.OBJECT_HANDLE
temp_args.append(arg)
elif isinstance(arg, NDArrayBase):
values[i].v_handle = ctypes.cast(arg.handle, ctypes.c_void_p)
type_codes[i] = (
TypeCode.NDARRAY_CONTAINER
if not arg.is_view
else TypeCode.ARRAY_HANDLE
)
elif isinstance(arg, _nd._DGL_COMPATS):
values[i].v_handle = ctypes.c_void_p(arg._dgl_handle)
type_codes[i] = arg.__class__._dgl_tcode
elif isinstance(arg, Integral):
values[i].v_int64 = arg
type_codes[i] = TypeCode.INT
elif isinstance(arg, Number):
values[i].v_float64 = arg
type_codes[i] = TypeCode.FLOAT
elif isinstance(arg, DGLDataType):
values[i].v_str = c_str(str(arg))
type_codes[i] = TypeCode.STR
elif isinstance(arg, DGLContext):
values[i].v_ctx = arg
type_codes[i] = TypeCode.DGL_CONTEXT
elif isinstance(arg, bytearray):
arr = DGLByteArray()
arr.data = ctypes.cast(
(ctypes.c_byte * len(arg)).from_buffer(arg),
ctypes.POINTER(ctypes.c_byte),
)
arr.size = len(arg)
values[i].v_handle = ctypes.c_void_p(ctypes.addressof(arr))
temp_args.append(arr)
type_codes[i] = TypeCode.BYTES
elif isinstance(arg, string_types):
values[i].v_str = c_str(arg)
type_codes[i] = TypeCode.STR
# NOTE(minjie): module is not used in DGL
# elif isinstance(arg, _CLASS_MODULE):
# values[i].v_handle = arg.handle
# type_codes[i] = TypeCode.MODULE_HANDLE
elif isinstance(arg, FunctionBase):
values[i].v_handle = arg.handle
type_codes[i] = TypeCode.FUNC_HANDLE
elif isinstance(arg, ctypes.c_void_p):
values[i].v_handle = arg
type_codes[i] = TypeCode.HANDLE
elif callable(arg):
arg = convert_to_dgl_func(arg)
values[i].v_handle = arg.handle
type_codes[i] = TypeCode.FUNC_HANDLE
temp_args.append(arg)
else:
raise TypeError("Don't know how to handle type %s" % type(arg))
return values, type_codes, num_args
class FunctionBase(object):
"""Function base."""
__slots__ = ["handle", "is_global"]
# pylint: disable=no-member
def __init__(self, handle, is_global):
"""Initialize the function with handle
Parameters
----------
handle : FunctionHandle
the handle to the underlying function.
is_global : bool
Whether this is a global function in python
"""
self.handle = handle
self.is_global = is_global
def __del__(self):
if not self.is_global and _LIB is not None:
check_call(_LIB.DGLFuncFree(self.handle))
def __call__(self, *args):
"""Call the function with positional arguments
args : list
The positional arguments to the function call.
"""
temp_args = []
values, tcodes, num_args = _make_dgl_args(args, temp_args)
ret_val = DGLValue()
ret_tcode = ctypes.c_int()
check_call(
_LIB.DGLFuncCall(
self.handle,
values,
tcodes,
ctypes.c_int(num_args),
ctypes.byref(ret_val),
ctypes.byref(ret_tcode),
)
)
_ = temp_args
_ = args
return RETURN_SWITCH[ret_tcode.value](ret_val)
def __init_handle_by_constructor__(fconstructor, args):
"""Initialize handle by constructor"""
temp_args = []
values, tcodes, num_args = _make_dgl_args(args, temp_args)
ret_val = DGLValue()
ret_tcode = ctypes.c_int()
check_call(
_LIB.DGLFuncCall(
fconstructor.handle,
values,
tcodes,
ctypes.c_int(num_args),
ctypes.byref(ret_val),
ctypes.byref(ret_tcode),
)
)
_ = temp_args
_ = args
assert ret_tcode.value == TypeCode.OBJECT_HANDLE
handle = ret_val.v_handle
return handle
def _return_module(x):
"""Return function"""
handle = x.v_handle
if not isinstance(handle, ModuleHandle):
handle = ModuleHandle(handle)
return _CLASS_MODULE(handle)
def _handle_return_func(x):
"""Return function"""
handle = x.v_handle
if not isinstance(handle, FunctionHandle):
handle = FunctionHandle(handle)
return _CLASS_FUNCTION(handle, False)
# setup return handle for function type
_object.__init_by_constructor__ = __init_handle_by_constructor__
RETURN_SWITCH[TypeCode.FUNC_HANDLE] = _handle_return_func
RETURN_SWITCH[TypeCode.MODULE_HANDLE] = _return_module
RETURN_SWITCH[TypeCode.NDARRAY_CONTAINER] = lambda x: _make_array(
x.v_handle, False
)
C_TO_PY_ARG_SWITCH[TypeCode.FUNC_HANDLE] = _wrap_arg_func(
_handle_return_func, TypeCode.FUNC_HANDLE
)
C_TO_PY_ARG_SWITCH[TypeCode.MODULE_HANDLE] = _wrap_arg_func(
_return_module, TypeCode.MODULE_HANDLE
)
C_TO_PY_ARG_SWITCH[TypeCode.ARRAY_HANDLE] = lambda x: _make_array(
x.v_handle, True
)
C_TO_PY_ARG_SWITCH[TypeCode.NDARRAY_CONTAINER] = lambda x: _make_array(
x.v_handle, False
)
_CLASS_MODULE = None
_CLASS_FUNCTION = None
def _set_class_module(module_class):
"""Initialize the module."""
global _CLASS_MODULE
_CLASS_MODULE = module_class
def _set_class_function(func_class):
global _CLASS_FUNCTION
_CLASS_FUNCTION = func_class
+137
View File
@@ -0,0 +1,137 @@
# pylint: disable=invalid-name
"""Runtime NDArray api"""
from __future__ import absolute_import
import ctypes
from ..base import _LIB, c_str, check_call
from ..runtime_ctypes import DGLArrayHandle
from .types import (
_return_handle,
_wrap_arg_func,
C_TO_PY_ARG_SWITCH,
RETURN_SWITCH,
)
DGLPyCapsuleDestructor = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
_c_str_dltensor = c_str("dltensor")
_c_str_used_dltensor = c_str("used_dltensor")
# used for PyCapsule manipulation
if hasattr(ctypes, "pythonapi"):
ctypes.pythonapi.PyCapsule_GetName.restype = ctypes.c_char_p
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_New.restype = ctypes.py_object
def _from_dlpack(dltensor):
dltensor = ctypes.py_object(dltensor)
if ctypes.pythonapi.PyCapsule_IsValid(dltensor, _c_str_dltensor):
ptr = ctypes.pythonapi.PyCapsule_GetPointer(dltensor, _c_str_dltensor)
# XXX(minjie): The below cast should be unnecessary given the code to
# set restype of PyCapsule calls. But weirdly, this does not
# work out always.
ptr = ctypes.cast(ptr, ctypes.c_void_p)
handle = DGLArrayHandle()
check_call(_LIB.DGLArrayFromDLPack(ptr, ctypes.byref(handle)))
ctypes.pythonapi.PyCapsule_SetName(dltensor, _c_str_used_dltensor)
ctypes.pythonapi.PyCapsule_SetDestructor(
dltensor, DGLPyCapsuleDestructor(0)
)
return _make_array(handle, False)
raise ValueError(
"Expect a dltensor field, PyCapsule can only be consumed once"
)
def _dlpack_deleter(pycapsule):
pycapsule = ctypes.cast(pycapsule, ctypes.py_object)
if ctypes.pythonapi.PyCapsule_IsValid(pycapsule, _c_str_dltensor):
ptr = ctypes.pythonapi.PyCapsule_GetPointer(pycapsule, _c_str_dltensor)
# XXX(minjie): The below cast should be unnecessary given the code to
# set restype of PyCapsule calls. But weirdly, this does not
# work out always.
ptr = ctypes.cast(ptr, ctypes.c_void_p)
_LIB.DGLDLManagedTensorCallDeleter(ptr)
ctypes.pythonapi.PyCapsule_SetDestructor(
pycapsule, DGLPyCapsuleDestructor(0)
)
_c_dlpack_deleter = DGLPyCapsuleDestructor(_dlpack_deleter)
class NDArrayBase(object):
"""A simple Device/CPU Array object in runtime."""
__slots__ = ["handle", "is_view"]
# pylint: disable=no-member
def __init__(self, handle, is_view=False):
"""Initialize the function with handle
Parameters
----------
handle : DGLArrayHandle
the handle to the underlying C++ DGLArray
"""
self.handle = handle
self.is_view = is_view
def __del__(self):
if not self.is_view and _LIB:
check_call(_LIB.DGLArrayFree(self.handle))
@property
def _dgl_handle(self):
return ctypes.cast(self.handle, ctypes.c_void_p).value
def to_dlpack(self, alignment=0):
"""Produce an array from a DLPack Tensor without copying memory
Args
-------
alignment: int, default to be 0
Indicates the alignment requirement when converting to dlpack. Will copy to a
new tensor if the alignment requirement is not satisfied.
0 means no alignment requirement.
Returns
-------
dlpack : DLPack tensor view of the array data
"""
ptr = ctypes.c_void_p()
check_call(
_LIB.DGLArrayToDLPack(self.handle, ctypes.byref(ptr), alignment)
)
return ctypes.pythonapi.PyCapsule_New(
ptr, _c_str_dltensor, _c_dlpack_deleter
)
def _make_array(handle, is_view):
handle = ctypes.cast(handle, DGLArrayHandle)
return _CLASS_NDARRAY(handle, is_view)
_DGL_COMPATS = ()
def _reg_extension(cls, fcreate):
global _DGL_COMPATS
_DGL_COMPATS += (cls,)
if fcreate:
fret = lambda x: fcreate(_return_handle(x))
RETURN_SWITCH[cls._dgl_tcode] = fret
C_TO_PY_ARG_SWITCH[cls._dgl_tcode] = _wrap_arg_func(
fret, cls._dgl_tcode
)
_CLASS_NDARRAY = None
def _set_class_ndarray(cls):
global _CLASS_NDARRAY
_CLASS_NDARRAY = cls
+109
View File
@@ -0,0 +1,109 @@
"""ctypes object API."""
from __future__ import absolute_import
import ctypes
from ..base import _LIB, c_str, check_call
from ..object_generic import _set_class_object_base
from .types import (
_wrap_arg_func,
C_TO_PY_ARG_SWITCH,
DGLValue,
RETURN_SWITCH,
TypeCode,
)
ObjectHandle = ctypes.c_void_p
__init_by_constructor__ = None
"""Maps object type to its constructor"""
OBJECT_TYPE = {}
def _register_object(index, cls):
"""register object class in python"""
OBJECT_TYPE[index] = cls
def _return_object(x):
"""Construct a object object from the given DGLValue object"""
handle = x.v_handle
if not isinstance(handle, ObjectHandle):
handle = ObjectHandle(handle)
tindex = ctypes.c_int()
check_call(_LIB.DGLObjectGetTypeIndex(handle, ctypes.byref(tindex)))
cls = OBJECT_TYPE.get(tindex.value, ObjectBase)
# Avoid calling __init__ of cls, instead directly call __new__
# This allows child class to implement their own __init__
obj = cls.__new__(cls)
obj.handle = handle
return obj
RETURN_SWITCH[TypeCode.OBJECT_HANDLE] = _return_object
C_TO_PY_ARG_SWITCH[TypeCode.OBJECT_HANDLE] = _wrap_arg_func(
_return_object, TypeCode.OBJECT_HANDLE
)
class ObjectBase(object):
"""Object base class"""
__slots__ = ["handle"]
# pylint: disable=no-member
def __del__(self):
if _LIB is not None and hasattr(self, "handle"):
check_call(_LIB.DGLObjectFree(self.handle))
def __getattr__(self, name):
if name == "handle":
raise AttributeError(
"'handle' is a reserved attribute name that should not be used"
)
ret_val = DGLValue()
ret_type_code = ctypes.c_int()
ret_success = ctypes.c_int()
check_call(
_LIB.DGLObjectGetAttr(
self.handle,
c_str(name),
ctypes.byref(ret_val),
ctypes.byref(ret_type_code),
ctypes.byref(ret_success),
)
)
if not ret_success.value:
raise AttributeError(
"'%s' object has no attribute '%s'" % (str(type(self)), name)
)
return RETURN_SWITCH[ret_type_code.value](ret_val)
def __init_handle_by_constructor__(self, fconstructor, *args):
"""Initialize the handle by calling constructor function.
Parameters
----------
fconstructor : Function
Constructor function.
args: list of objects
The arguments to the constructor
Note
----
We have a special calling convention to call constructor functions.
So the return handle is directly set into the Object object
instead of creating a new Object.
"""
# assign handle first to avoid error raising
self.handle = None
handle = __init_by_constructor__(
fconstructor, args
) # pylint: disable=not-callable
if not isinstance(handle, ObjectHandle):
handle = ObjectHandle(handle)
self.handle = handle
_set_class_object_base(ObjectBase)
+91
View File
@@ -0,0 +1,91 @@
"""The C Types used in API."""
# pylint: disable=invalid-name
from __future__ import absolute_import as _abs
import ctypes
from ..base import _LIB, check_call, py_str
from ..runtime_ctypes import DGLByteArray, DGLContext, DGLDataType, TypeCode
class DGLValue(ctypes.Union):
"""DGLValue in C API"""
_fields_ = [
("v_int64", ctypes.c_int64),
("v_float64", ctypes.c_double),
("v_handle", ctypes.c_void_p),
("v_str", ctypes.c_char_p),
("v_type", DGLDataType),
("v_ctx", DGLContext),
]
DGLPackedCFunc = ctypes.CFUNCTYPE(
ctypes.c_int,
ctypes.POINTER(DGLValue),
ctypes.POINTER(ctypes.c_int),
ctypes.c_int,
ctypes.c_void_p,
ctypes.c_void_p,
)
DGLCFuncFinalizer = ctypes.CFUNCTYPE(None, ctypes.c_void_p)
def _return_handle(x):
"""return handle"""
handle = x.v_handle
if not isinstance(handle, ctypes.c_void_p):
handle = ctypes.c_void_p(handle)
return handle
def _return_bytes(x):
"""return handle"""
handle = x.v_handle
if not isinstance(handle, ctypes.c_void_p):
handle = ctypes.c_void_p(handle)
arr = ctypes.cast(handle, ctypes.POINTER(DGLByteArray))[0]
size = arr.size
res = bytearray(size)
rptr = (ctypes.c_byte * size).from_buffer(res)
if not ctypes.memmove(rptr, arr.data, size):
raise RuntimeError("memmove failed")
return res
def _wrap_arg_func(return_f, type_code):
tcode = ctypes.c_int(type_code)
def _wrap_func(x):
check_call(_LIB.DGLCbArgToReturn(ctypes.byref(x), tcode))
return return_f(x)
return _wrap_func
RETURN_SWITCH = {
TypeCode.INT: lambda x: x.v_int64,
TypeCode.FLOAT: lambda x: x.v_float64,
TypeCode.HANDLE: _return_handle,
TypeCode.NULL: lambda x: None,
TypeCode.STR: lambda x: py_str(x.v_str),
TypeCode.BYTES: _return_bytes,
TypeCode.DGL_CONTEXT: lambda x: DGLContext(
x.v_ctx.device_type, x.v_ctx.device_id
),
}
C_TO_PY_ARG_SWITCH = {
TypeCode.INT: lambda x: x.v_int64,
TypeCode.FLOAT: lambda x: x.v_float64,
TypeCode.HANDLE: _return_handle,
TypeCode.NULL: lambda x: None,
TypeCode.STR: lambda x: py_str(x.v_str),
TypeCode.BYTES: _return_bytes,
TypeCode.DGL_CONTEXT: lambda x: DGLContext(
x.v_ctx.device_type, x.v_ctx.device_id
),
}
+1
View File
@@ -0,0 +1 @@
"""cython2 namespace"""
+1
View File
@@ -0,0 +1 @@
"""cython3 namespace"""
+1
View File
@@ -0,0 +1 @@
*.cpp
+169
View File
@@ -0,0 +1,169 @@
from ..base import DGLError
from libcpp.vector cimport vector
from libcpp cimport bool
from cpython.version cimport PY_MAJOR_VERSION
from cpython cimport pycapsule
from libc.stdint cimport int32_t, int64_t, uint64_t, uint8_t, uint16_t
import ctypes
cdef enum DGLObjectTypeCode:
kObjectInt = 0
kObjectUInt = 1
kObjectFloat = 2
kHandle = 3
kNull = 4
kDGLDataType = 5
kDGLContext = 6
kArrayHandle = 7
kObjectHandle = 8
kModuleHandle = 9
kFuncHandle = 10
kStr = 11
kBytes = 12
kNDArrayContainer = 13
kExtBegin = 15
cdef extern from "dgl/runtime/c_runtime_api.h":
ctypedef struct DGLDataType:
uint8_t code
uint8_t bits
uint16_t lanes
ctypedef struct DGLContext:
int32_t device_type
int32_t device_id
ctypedef struct DGLArray:
void* data
DGLContext ctx
int32_t ndim
DGLDataType dtype
int64_t* shape
int64_t* strides
uint64_t byte_offset
ctypedef struct DLManagedTensor:
DGLArray dl_tensor
void* manager_ctx
void (*deleter)(DLManagedTensor* self)
ctypedef struct DGLValue:
int64_t v_int64
double v_float64
void* v_handle
const char* v_str
DGLDataType v_type
DGLContext v_ctx
ctypedef int64_t dgl_index_t
ctypedef DGLArray* DGLArrayHandle
ctypedef void* DGLStreamHandle
ctypedef void* DGLRetValueHandle
ctypedef void* DGLFunctionHandle
ctypedef void* ObjectHandle
ctypedef int (*DGLPackedCFunc)(
DGLValue* args,
int* type_codes,
int num_args,
DGLRetValueHandle ret,
void* resource_handle)
ctypedef void (*DGLPackedCFuncFinalizer)(void* resource_handle)
cdef extern from "dgl/runtime/c_runtime_api.h":
void DGLAPISetLastError(const char* msg)
const char *DGLGetLastError()
int DGLFuncCall(DGLFunctionHandle func,
DGLValue* arg_values,
int* type_codes,
int num_args,
DGLValue* ret_val,
int* ret_type_code) nogil
int DGLFuncFree(DGLFunctionHandle func)
int DGLCFuncSetReturn(DGLRetValueHandle ret,
DGLValue* value,
int* type_code,
int num_ret)
int DGLFuncCreateFromCFunc(DGLPackedCFunc func,
void* resource_handle,
DGLPackedCFuncFinalizer fin,
DGLFunctionHandle *out)
int DGLCbArgToReturn(DGLValue* value, int code)
int DGLArrayAlloc(dgl_index_t* shape,
dgl_index_t ndim,
DGLDataType dtype,
DGLContext ctx,
DGLArrayHandle* out)
int DGLArrayAllocSharedMem(const char *mem_name,
const dgl_index_t *shape,
int ndim,
int dtype_code,
int dtype_bits,
int dtype_lanes,
bool is_create,
DGLArrayHandle* out)
int DGLArrayFree(DGLArrayHandle handle)
int DGLArrayCopyFromTo(DGLArrayHandle src,
DGLArrayHandle to)
cdef extern from "dgl/runtime/c_object_api.h":
int DGLObjectFree(ObjectHandle handle)
int DGLObjectTypeKey2Index(const char* type_key,
int* out_index)
int DGLObjectGetTypeIndex(ObjectHandle handle,
int* out_index)
int DGLObjectGetAttr(ObjectHandle handle,
const char* key,
DGLValue* out_value,
int* out_type_code,
int* out_success)
cdef extern from "dgl/runtime/dlpack_convert.h":
int DGLArrayFromDLPack(DLManagedTensor* arr_from,
DGLArrayHandle* out)
int DGLArrayToDLPack(DGLArrayHandle arr_from,
DLManagedTensor** out,
int alignment)
void DGLDLManagedTensorCallDeleter(DLManagedTensor* dltensor)
cdef inline py_str(const char* x):
if PY_MAJOR_VERSION < 3:
return x
else:
return x.decode("utf-8")
cdef inline c_str(pystr):
"""Create ctypes char * from a python string
Parameters
----------
string : string type
python string
Returns
-------
str : c_char_p
A char pointer that can be passed to C API
"""
return pystr.encode("utf-8")
cdef inline CALL(int ret):
if ret != 0:
raise DGLError(py_str(DGLGetLastError()))
cdef inline object ctypes_handle(void* chandle):
"""Cast C handle to ctypes handle."""
return ctypes.cast(<unsigned long long>chandle, ctypes.c_void_p)
cdef inline void* c_handle(object handle):
"""Cast C types handle to c handle."""
cdef unsigned long long v_ptr
if handle.value is None:
return NULL
else:
v_ptr = handle.value
return <void*>(v_ptr)
+4
View File
@@ -0,0 +1,4 @@
include "./base.pxi"
include "./object.pxi"
include "./function.pxi"
include "./ndarray.pxi"
+308
View File
@@ -0,0 +1,308 @@
import ctypes
import traceback
from cpython cimport Py_INCREF, Py_DECREF
from numbers import Number, Integral
from ..base import string_types
from ..object_generic import convert_to_object, ObjectGeneric
from ..runtime_ctypes import DGLDataType as CTypesDGLDataType, \
DGLContext as CTypesDGLContext, \
DGLByteArray
cdef void dgl_callback_finalize(void* fhandle):
local_pyfunc = <object>(fhandle)
Py_DECREF(local_pyfunc)
cdef int dgl_callback(DGLValue* args,
int* type_codes,
int num_args,
DGLRetValueHandle ret,
void* fhandle) with gil:
cdef list pyargs
cdef DGLValue value
cdef int tcode
local_pyfunc = <object>(fhandle)
pyargs = []
for i in range(num_args):
value = args[i]
tcode = type_codes[i]
if (tcode == kObjectHandle or
tcode == kFuncHandle or
tcode == kModuleHandle or
tcode > kExtBegin):
CALL(DGLCbArgToReturn(&value, tcode))
if tcode != kArrayHandle:
pyargs.append(make_ret(value, tcode))
else:
pyargs.append(c_make_array(value.v_handle, True))
try:
rv = local_pyfunc(*pyargs)
except Exception:
msg = traceback.format_exc()
DGLAPISetLastError(c_str(msg))
return -1
if rv is not None:
if isinstance(rv, tuple):
raise ValueError("PackedFunction can only support one return value")
temp_args = []
make_arg(rv, &value, &tcode, temp_args)
CALL(DGLCFuncSetReturn(ret, &value, &tcode, 1))
return 0
def convert_to_dgl_func(object pyfunc):
"""Convert a python function to DGL function
Parameters
----------
pyfunc : python function
The python function to be converted.
Returns
-------
dglfunc: dgl.Function
The converted dgl function.
"""
cdef DGLFunctionHandle chandle
Py_INCREF(pyfunc)
CALL(DGLFuncCreateFromCFunc(dgl_callback,
<void*>(pyfunc),
dgl_callback_finalize,
&chandle))
ret = _CLASS_FUNCTION(None, False)
(<FunctionBase>ret).chandle = chandle
return ret
cdef inline int make_arg(object arg,
DGLValue* value,
int* tcode,
list temp_args) except -1:
"""Pack arguments into c args dgl call accept"""
cdef unsigned long long ptr
if isinstance(arg, ObjectBase):
value[0].v_handle = (<ObjectBase>arg).chandle
tcode[0] = kObjectHandle
elif isinstance(arg, NDArrayBase):
value[0].v_handle = (<NDArrayBase>arg).chandle
tcode[0] = (kNDArrayContainer if
not (<NDArrayBase>arg).c_is_view else kArrayHandle)
elif isinstance(arg, _DGL_COMPATS):
ptr = arg._dgl_handle
value[0].v_handle = (<void*>ptr)
tcode[0] = arg.__class__._dgl_tcode
elif isinstance(arg, (int, long)):
value[0].v_int64 = arg
tcode[0] = kObjectInt
elif isinstance(arg, float):
value[0].v_float64 = arg
tcode[0] = kObjectFloat
elif isinstance(arg, str):
tstr = c_str(arg)
value[0].v_str = tstr
tcode[0] = kStr
temp_args.append(tstr)
elif arg is None:
value[0].v_handle = NULL
tcode[0] = kNull
elif isinstance(arg, Number):
value[0].v_float64 = arg
tcode[0] = kObjectFloat
elif isinstance(arg, CTypesDGLDataType):
tstr = c_str(str(arg))
value[0].v_str = tstr
tcode[0] = kStr
temp_args.append(tstr)
elif isinstance(arg, CTypesDGLContext):
value[0].v_ctx = (<DGLContext*>(
<unsigned long long>ctypes.addressof(arg)))[0]
tcode[0] = kDGLContext
elif isinstance(arg, bytearray):
arr = DGLByteArray()
arr.data = ctypes.cast(
(ctypes.c_byte * len(arg)).from_buffer(arg),
ctypes.POINTER(ctypes.c_byte))
arr.size = len(arg)
value[0].v_handle = <void*>(
<unsigned long long>ctypes.addressof(arr))
tcode[0] = kBytes
temp_args.append(arr)
elif isinstance(arg, string_types):
tstr = c_str(arg)
value[0].v_str = tstr
tcode[0] = kStr
temp_args.append(tstr)
elif isinstance(arg, (list, tuple, dict, ObjectGeneric)):
arg = convert_to_object(arg)
value[0].v_handle = (<ObjectBase>arg).chandle
tcode[0] = kObjectHandle
temp_args.append(arg)
#elif isinstance(arg, _CLASS_MODULE):
# value[0].v_handle = c_handle(arg.handle)
# tcode[0] = kModuleHandle
elif isinstance(arg, FunctionBase):
value[0].v_handle = (<FunctionBase>arg).chandle
tcode[0] = kFuncHandle
elif isinstance(arg, ctypes.c_void_p):
value[0].v_handle = c_handle(arg)
tcode[0] = kHandle
elif callable(arg):
arg = convert_to_dgl_func(arg)
value[0].v_handle = (<FunctionBase>arg).chandle
tcode[0] = kFuncHandle
temp_args.append(arg)
else:
raise TypeError("Don't know how to handle type %s" % type(arg))
return 0
cdef inline bytearray make_ret_bytes(void* chandle):
handle = ctypes_handle(chandle)
arr = ctypes.cast(handle, ctypes.POINTER(DGLByteArray))[0]
size = arr.size
res = bytearray(size)
rptr = (ctypes.c_byte * size).from_buffer(res)
if not ctypes.memmove(rptr, arr.data, size):
raise RuntimeError('memmove failed')
return res
cdef inline object make_ret(DGLValue value, int tcode):
"""convert result to return value."""
if tcode == kObjectHandle:
return make_ret_object(value.v_handle)
elif tcode == kNull:
return None
elif tcode == kObjectInt:
return value.v_int64
elif tcode == kObjectFloat:
return value.v_float64
elif tcode == kNDArrayContainer:
return c_make_array(value.v_handle, False)
elif tcode == kStr:
return py_str(value.v_str)
elif tcode == kBytes:
return make_ret_bytes(value.v_handle)
elif tcode == kHandle:
return ctypes_handle(value.v_handle)
elif tcode == kDGLContext:
return CTypesDGLContext(value.v_ctx.device_type, value.v_ctx.device_id)
# (minjie): class module are not used in DGL.
#elif tcode == kModuleHandle:
# return _CLASS_MODULE(ctypes_handle(value.v_handle))
elif tcode == kFuncHandle:
fobj = _CLASS_FUNCTION(None, False)
(<FunctionBase>fobj).chandle = value.v_handle
return fobj
elif tcode in _DGL_EXT_RET:
return _DGL_EXT_RET[tcode](ctypes_handle(value.v_handle))
raise ValueError("Unhandled type code %d" % tcode)
cdef inline int FuncCall3(void* chandle,
tuple args,
int nargs,
DGLValue* ret_val,
int* ret_tcode) except -1:
cdef DGLValue[3] values
cdef int[3] tcodes
nargs = len(args)
temp_args = []
for i in range(nargs):
make_arg(args[i], &values[i], &tcodes[i], temp_args)
with nogil:
ret = DGLFuncCall(chandle, &values[0], &tcodes[0],
nargs, ret_val, ret_tcode)
if ret != 0:
raise DGLError(py_str(DGLGetLastError()))
return 0
cdef inline int FuncCall(void* chandle,
tuple args,
DGLValue* ret_val,
int* ret_tcode) except -1:
cdef int nargs
nargs = len(args)
if nargs <= 3:
FuncCall3(chandle, args, nargs, ret_val, ret_tcode)
return 0
cdef vector[DGLValue] values
cdef vector[int] tcodes
values.resize(max(nargs, 1))
tcodes.resize(max(nargs, 1))
temp_args = []
for i in range(nargs):
make_arg(args[i], &values[i], &tcodes[i], temp_args)
with nogil:
ret = DGLFuncCall(chandle, &values[0], &tcodes[0],
nargs, ret_val, ret_tcode)
if ret != 0:
raise DGLError(py_str(DGLGetLastError()))
return 0
cdef inline int ConstructorCall(void* constructor_handle,
int type_code,
tuple args,
void** handle) except -1:
"""Call contructor of a handle function"""
cdef DGLValue ret_val
cdef int ret_tcode
FuncCall(constructor_handle, args, &ret_val, &ret_tcode)
assert ret_tcode == type_code
handle[0] = ret_val.v_handle
return 0
cdef class FunctionBase:
cdef DGLFunctionHandle chandle
cdef int is_global
cdef inline _set_handle(self, handle):
if handle is None:
self.chandle = NULL
else:
self.chandle = c_handle(handle)
property is_global:
def __get__(self):
return self.c_is_global != 0
def __set__(self, value):
self.c_is_global = value
property handle:
def __get__(self):
if self.chandle == NULL:
return None
else:
return ctypes.cast(<unsigned long long>self.chandle, ctypes.c_void_p)
def __set__(self, value):
self._set_handle(value)
def __init__(self, handle, is_global):
self._set_handle(handle)
self.c_is_global = is_global
def __dealloc__(self):
if self.is_global == 0:
CALL(DGLFuncFree(self.chandle))
def __call__(self, *args):
cdef DGLValue ret_val
cdef int ret_tcode
FuncCall(self.chandle, args, &ret_val, &ret_tcode)
return make_ret(ret_val, ret_tcode)
_CLASS_FUNCTION = None
_CLASS_MODULE = None
def _set_class_module(module_class):
"""Initialize the module."""
global _CLASS_MODULE
_CLASS_MODULE = module_class
def _set_class_function(func_class):
global _CLASS_FUNCTION
_CLASS_FUNCTION = func_class
+110
View File
@@ -0,0 +1,110 @@
from ..runtime_ctypes import DGLArrayHandle as PyDGLArrayHandle
from cpython cimport PyCapsule_Destructor
cdef const char* _c_str_dltensor = "dltensor"
cdef const char* _c_str_used_dltensor = "used_dltensor"
cdef _c_dlpack_deleter(object pycaps):
cdef DLManagedTensor* dltensor
if pycapsule.PyCapsule_IsValid(pycaps, _c_str_dltensor):
dltensor = <DLManagedTensor*>pycapsule.PyCapsule_GetPointer(pycaps, _c_str_dltensor)
DGLDLManagedTensorCallDeleter(dltensor)
def _from_dlpack(object dltensor):
cdef DLManagedTensor* ptr
cdef DGLArrayHandle chandle
if pycapsule.PyCapsule_IsValid(dltensor, _c_str_dltensor):
ptr = <DLManagedTensor*>pycapsule.PyCapsule_GetPointer(dltensor, _c_str_dltensor)
CALL(DGLArrayFromDLPack(ptr, &chandle))
# set name and destructor to be empty
pycapsule.PyCapsule_SetDestructor(dltensor, NULL)
pycapsule.PyCapsule_SetName(dltensor, _c_str_used_dltensor)
return c_make_array(chandle, 0)
raise ValueError("Expect a dltensor field, pycapsule.PyCapsule can only be consumed once")
cdef class NDArrayBase:
cdef DGLArray* chandle
cdef int c_is_view
cdef inline _set_handle(self, handle):
cdef unsigned long long ptr
if handle is None:
self.chandle = NULL
else:
ptr = ctypes.cast(handle, ctypes.c_void_p).value
self.chandle = <DGLArray*>(ptr)
property _dgl_handle:
def __get__(self):
return <unsigned long long>self.chandle
property handle:
def __get__(self):
if self.chandle == NULL:
return None
else:
return ctypes.cast(
<unsigned long long>self.chandle, PyDGLArrayHandle)
def __set__(self, value):
self._set_handle(value)
def __init__(self, handle, is_view):
self._set_handle(handle)
self.c_is_view = is_view
def __dealloc__(self):
if self.c_is_view == 0:
CALL(DGLArrayFree(self.chandle))
def to_dlpack(self, alignment=0):
"""Produce an array from a DLPack Tensor without copying memory
Args
-------
alignment: int, default to be 0
Indicates the alignment requirement when converting to dlpack. Will copy to a
new tensor if the alignment requirement is not satisfied.
0 means no alignment requirement.
Returns
-------
dlpack : DLPack tensor view of the array data
"""
cdef DLManagedTensor* dltensor
if self.c_is_view != 0:
raise ValueError("to_dlpack do not work with memory views")
CALL(DGLArrayToDLPack(self.chandle, &dltensor, alignment))
return pycapsule.PyCapsule_New(dltensor, _c_str_dltensor, <PyCapsule_Destructor>_c_dlpack_deleter)
cdef c_make_array(void* chandle, is_view):
ret = _CLASS_NDARRAY(None, is_view)
(<NDArrayBase>ret).chandle = <DGLArray*>chandle
return ret
cdef _DGL_COMPATS = ()
cdef _DGL_EXT_RET = {}
def _reg_extension(cls, fcreate):
global _DGL_COMPATS
_DGL_COMPATS += (cls,)
if fcreate:
_DGL_EXT_RET[cls._dgl_tcode] = fcreate
def _make_array(handle, is_view):
cdef unsigned long long ptr
ptr = ctypes.cast(handle, ctypes.c_void_p).value
return c_make_array(<void*>ptr, is_view)
cdef object _CLASS_NDARRAY = None
def _set_class_ndarray(cls):
global _CLASS_NDARRAY
_CLASS_NDARRAY = cls
+91
View File
@@ -0,0 +1,91 @@
from ... import _api_internal
from ..base import string_types
from ..object_generic import _set_class_object_base
"""Maps object type to its constructor"""
OBJECT_TYPE = []
def _register_object(int index, object cls):
"""register object class"""
while len(OBJECT_TYPE) <= index:
OBJECT_TYPE.append(None)
OBJECT_TYPE[index] = cls
cdef inline object make_ret_object(void* chandle):
global OBJECT_TYPE
cdef int tindex
cdef list object_type
cdef object cls
object_type = OBJECT_TYPE
CALL(DGLObjectGetTypeIndex(chandle, &tindex))
if tindex < len(object_type):
cls = object_type[tindex]
if cls is not None:
obj = cls.__new__(cls)
else:
obj = ObjectBase.__new__(ObjectBase)
else:
obj = ObjectBase.__new__(ObjectBase)
(<ObjectBase>obj).chandle = chandle
return obj
cdef class ObjectBase:
cdef void* chandle
cdef _set_handle(self, handle):
cdef unsigned long long ptr
if handle is None:
self.chandle = NULL
else:
ptr = handle.value
self.chandle = <void*>(ptr)
property handle:
def __get__(self):
if self.chandle == NULL:
return None
else:
return ctypes_handle(self.chandle)
def __set__(self, value):
self._set_handle(value)
def __dealloc__(self):
CALL(DGLObjectFree(self.chandle))
def __getattr__(self, name):
cdef DGLValue ret_val
cdef int ret_type_code, ret_succ
CALL(DGLObjectGetAttr(self.chandle, c_str(name),
&ret_val, &ret_type_code, &ret_succ))
if ret_succ == 0:
raise AttributeError(
"'%s' object has no attribute '%s'" % (type(self), name))
return make_ret(ret_val, ret_type_code)
def __init_handle_by_constructor__(self, fconstructor, *args):
"""Initialize the handle by calling constructor function.
Parameters
----------
fconstructor : Function
Constructor function.
args: list of objects
The arguments to the constructor
Note
----
We have a special calling convention to call constructor functions.
So the return handle is directly set into the Object object
instead of creating a new Object.
"""
cdef void* chandle
ConstructorCall(
(<FunctionBase>fconstructor).chandle,
kObjectHandle, args, &chandle)
self.chandle = chandle
_set_class_object_base(ObjectBase)
+155
View File
@@ -0,0 +1,155 @@
# coding: utf-8
# pylint: disable=invalid-name
"""ctypes library and helper functions """
from __future__ import absolute_import
import ctypes
import logging
import os
import sys
import numpy as np
from . import libinfo
# ----------------------------
# library loading
# ----------------------------
if sys.version_info[0] == 3:
string_types = (str,)
numeric_types = (float, int, np.float32, np.int32)
# this function is needed for python3
# to convert ctypes.char_p .value back to python str
py_str = lambda x: x.decode("utf-8")
else:
string_types = (basestring,)
numeric_types = (float, int, long, np.float32, np.int32)
py_str = lambda x: x
class DGLError(Exception):
"""Error thrown by DGL function"""
pass # pylint: disable=unnecessary-pass
def _load_lib():
"""Load libary by searching possible path."""
lib_path = libinfo.find_lib_path()
lib = ctypes.CDLL(lib_path[0])
dirname = os.path.dirname(lib_path[0])
basename = os.path.basename(lib_path[0])
# DMatrix functions
lib.DGLGetLastError.restype = ctypes.c_char_p
return lib, basename, dirname
# version number
__version__ = libinfo.__version__
# library instance of nnvm
_LIB, _LIB_NAME, _DIR_NAME = _load_lib()
# The FFI mode of DGL
_FFI_MODE = os.environ.get("DGL_FFI", "auto")
# ----------------------------
# helper function in ctypes.
# ----------------------------
def check_call(ret):
"""Check the return value of C API call
This function will raise exception when error occurs.
Wrap every API call with this function
Parameters
----------
ret : int
return value from API calls
"""
if ret != 0:
raise DGLError(py_str(_LIB.DGLGetLastError()))
def c_str(string):
"""Create ctypes char * from a python string
Parameters
----------
string : string type
python string
Returns
-------
str : c_char_p
A char pointer that can be passed to C API
"""
return ctypes.c_char_p(string.encode("utf-8"))
def c_array(ctype, values):
"""Create ctypes array from a python array
Parameters
----------
ctype : ctypes data type
data type of the array we want to convert to
values : tuple or list
data content
Returns
-------
out : ctypes array
Created ctypes array
"""
return (ctype * len(values))(*values)
def decorate(func, fwrapped):
"""A wrapper call of decorator package, differs to call time
Parameters
----------
func : function
The original function
fwrapped : function
The wrapped function
"""
import decorator
return decorator.decorate(func, fwrapped)
tensor_adapter_loaded = False
def load_tensor_adapter(backend, version):
"""Tell DGL to load a tensoradapter library for given backend and version.
Parameters
----------
backend : str
The backend (currently ``pytorch``, ``mxnet`` or ``tensorflow``).
version : str
The version number of the backend.
"""
global tensor_adapter_loaded
version = version.split("+")[0]
if sys.platform.startswith("linux"):
basename = "libtensoradapter_%s_%s.so" % (backend, version)
elif sys.platform.startswith("darwin"):
basename = "libtensoradapter_%s_%s.dylib" % (backend, version)
elif sys.platform.startswith("win"):
basename = "tensoradapter_%s_%s.dll" % (backend, version)
else:
raise NotImplementedError("Unsupported system: %s" % sys.platform)
path = os.path.join(_DIR_NAME, "tensoradapter", backend, basename)
tensor_adapter_loaded = _LIB.DGLLoadTensorAdapter(path.encode("utf-8")) == 0
if not tensor_adapter_loaded:
logger = logging.getLogger("dgl-core")
logger.debug("Memory optimization with PyTorch is not enabled.")
def is_tensor_adaptor_enabled() -> bool:
"""Check whether TensorAdaptor is enabled."""
return tensor_adapter_loaded
+4
View File
@@ -0,0 +1,4 @@
"""Init all C APIs in the default namespace."""
from .function import _init_api
__all__ = _init_api("dgl.capi", __name__)
+350
View File
@@ -0,0 +1,350 @@
# pylint: disable=invalid-name, unused-import
"""Function namespace."""
from __future__ import absolute_import
import ctypes
import sys
from .base import _FFI_MODE, _LIB, c_str, check_call, py_str, string_types
IMPORT_EXCEPT = RuntimeError if _FFI_MODE == "cython" else ImportError
try:
# pylint: disable=wrong-import-position
if _FFI_MODE == "ctypes":
raise ImportError()
if sys.version_info >= (3, 0):
from ._cy3.core import (
_set_class_function,
_set_class_module,
convert_to_dgl_func,
FunctionBase as _FunctionBase,
)
else:
from ._cy2.core import (
_set_class_function,
_set_class_module,
convert_to_dgl_func,
FunctionBase as _FunctionBase,
)
except IMPORT_EXCEPT:
# pylint: disable=wrong-import-position
from ._ctypes.function import (
_set_class_function,
_set_class_module,
convert_to_dgl_func,
FunctionBase as _FunctionBase,
)
FunctionHandle = ctypes.c_void_p
class Function(_FunctionBase):
"""The PackedFunc object.
Function plays an key role to bridge front and backend in DGL.
Function provide a type-erased interface, you can call function with positional arguments.
The compiled module returns Function.
DGL backend also registers and exposes its API as Functions.
For example, the developer function exposed in dgl.ir_pass are actually
C++ functions that are registered as PackedFunc
The following are list of common usage scenario of dgl.Function.
- Automatic exposure of C++ API into python
- To call PackedFunc from python side
- To call python callbacks to inspect results in generated code
- Bring python hook into C++ backend
See Also
--------
dgl.register_func: How to register global function.
dgl.get_global_func: How to get global function.
"""
pass # pylint: disable=unnecessary-pass
class ModuleBase(object):
"""Base class for module"""
__slots__ = ["handle", "_entry", "entry_name"]
def __init__(self, handle):
self.handle = handle
self._entry = None
self.entry_name = "__dgl_main__"
def __del__(self):
check_call(_LIB.DGLModFree(self.handle))
@property
def entry_func(self):
"""Get the entry function
Returns
-------
f : Function
The entry function if exist
"""
if self._entry:
return self._entry
self._entry = self.get_function(self.entry_name)
return self._entry
def get_function(self, name, query_imports=False):
"""Get function from the module.
Parameters
----------
name : str
The name of the function
query_imports : bool
Whether also query modules imported by this module.
Returns
-------
f : Function
The result function.
"""
ret_handle = FunctionHandle()
check_call(
_LIB.DGLModGetFunction(
self.handle,
c_str(name),
ctypes.c_int(query_imports),
ctypes.byref(ret_handle),
)
)
if not ret_handle.value:
raise AttributeError("Module has no function '%s'" % name)
return Function(ret_handle, False)
def import_module(self, module):
"""Add module to the import list of current one.
Parameters
----------
module : Module
The other module.
"""
check_call(_LIB.DGLModImport(self.handle, module.handle))
def __getitem__(self, name):
if not isinstance(name, string_types):
raise ValueError("Can only take string as function name")
return self.get_function(name)
def __call__(self, *args):
if self._entry:
return self._entry(*args)
f = self.entry_func
return f(*args)
def register_func(func_name, f=None, override=False):
"""Register global function
Parameters
----------
func_name : str or function
The function name
f : function, optional
The function to be registered.
override: boolean optional
Whether override existing entry.
Returns
-------
fregister : function
Register function if f is not specified.
Examples
--------
The following code registers my_packed_func as global function.
Note that we simply get it back from global function table to invoke
it from python side. However, we can also invoke the same function
from C++ backend, or in the compiled DGL code.
.. code-block:: python
targs = (10, 10.0, "hello")
@dgl.register_func
def my_packed_func(*args):
assert(tuple(args) == targs)
return 10
# Get it out from global function table
f = dgl.get_global_func("my_packed_func")
assert isinstance(f, dgl.nd.Function)
y = f(*targs)
assert y == 10
"""
if callable(func_name):
f = func_name
func_name = f.__name__
if not isinstance(func_name, str):
raise ValueError("expect string function name")
ioverride = ctypes.c_int(override)
def register(myf):
"""internal register function"""
if not isinstance(myf, Function):
myf = convert_to_dgl_func(myf)
check_call(
_LIB.DGLFuncRegisterGlobal(c_str(func_name), myf.handle, ioverride)
)
return myf
if f:
return register(f)
return register
def get_global_func(name, allow_missing=False):
"""Get a global function by name
Parameters
----------
name : str
The name of the global function
allow_missing : bool
Whether allow missing function or raise an error.
Returns
-------
func : dgl.Function
The function to be returned, None if function is missing.
"""
handle = FunctionHandle()
check_call(_LIB.DGLFuncGetGlobal(c_str(name), ctypes.byref(handle)))
if handle.value:
return Function(handle, False)
else:
if allow_missing:
return None
else:
raise ValueError("Cannot find global function %s" % name)
def list_global_func_names():
"""Get list of global functions registered.
Returns
-------
names : list
List of global functions names.
"""
plist = ctypes.POINTER(ctypes.c_char_p)()
size = ctypes.c_uint()
check_call(
_LIB.DGLFuncListGlobalNames(ctypes.byref(size), ctypes.byref(plist))
)
fnames = []
for i in range(size.value):
fnames.append(py_str(plist[i]))
return fnames
def extract_ext_funcs(finit):
"""
Extract the extension PackedFuncs from a C module.
Parameters
----------
finit : ctypes function
a ctypes that takes signature of DGLExtensionDeclarer
Returns
-------
fdict : dict of str to Function
The extracted functions
"""
fdict = {}
def _list(name, func):
fdict[name] = func
myf = convert_to_dgl_func(_list)
ret = finit(myf.handle)
_ = myf
if ret != 0:
raise RuntimeError("cannot initialize with %s" % finit)
return fdict
def _get_api(f):
flocal = f
flocal.is_global = True
return flocal
def _init_api(namespace, target_module_name=None):
"""Initialize api for a given module name
namespace : str
The namespace of the source registry
target_module_name : str
The target module name if different from namespace
"""
target_module_name = target_module_name if target_module_name else namespace
if namespace.startswith("dgl."):
return _init_api_prefix(target_module_name, namespace[4:])
else:
return _init_api_prefix(target_module_name, namespace)
def _init_api_prefix(module_name, prefix):
module = sys.modules[module_name]
name_list = []
for name in list_global_func_names():
if name.startswith("_") and not name.startswith("_deprecate"):
# internal APIs are ignored
continue
name_split = name.rsplit(".", 1)
if name_split[0] != prefix:
continue
if len(name_split) == 1:
print('Warning: invalid API name "%s".' % name)
continue
fname = name_split[1]
target_module = module
f = get_global_func(name)
ff = _get_api(f)
ff.__name__ = fname
ff.__doc__ = "DGL PackedFunc %s. " % fname
setattr(target_module, ff.__name__, ff)
name_list.append(fname)
return name_list
def _init_internal_api():
for name in list_global_func_names():
if not name.startswith("_") or name.startswith("_deprecate"):
# normal APIs are ignored
continue
target_module = sys.modules["dgl._api_internal"]
fname = name
if fname.find(".") != -1:
print('Warning: invalid API name "%s".' % fname)
continue
f = get_global_func(name)
ff = _get_api(f)
ff.__name__ = fname
ff.__doc__ = "DGL PackedFunc %s. " % fname
setattr(target_module, ff.__name__, ff)
_set_class_function(Function)
+108
View File
@@ -0,0 +1,108 @@
"""Library information."""
from __future__ import absolute_import
import os
import pathlib
import sys
def find_lib_path(name=None, search_path=None, optional=False):
"""Find dynamic library files.
Parameters
----------
name : list of str
List of names to be found.
Returns
-------
lib_path : list(string)
List of all found path to the libraries
"""
# See https://github.com/dmlc/tvm/issues/281 for some background.
# NB: This will either be the source directory (if DGL is run
# inplace) or the install directory (if DGL is installed).
# An installed DGL's curr_path will look something like:
# $PREFIX/lib/python3.6/site-packages/dgl/_ffi
ffi_dir = os.path.dirname(os.path.abspath(os.path.expanduser(__file__)))
source_dir = os.path.join(ffi_dir, "..", "..", "..")
install_lib_dir = os.path.join(ffi_dir, "..", "..", "..", "..")
dll_path = []
if os.environ.get("DGL_LIBRARY_PATH", None):
dll_path.append(os.environ["DGL_LIBRARY_PATH"])
if sys.platform.startswith("linux") and os.environ.get(
"LD_LIBRARY_PATH", None
):
dll_path.extend(
[p.strip() for p in os.environ["LD_LIBRARY_PATH"].split(":")]
)
elif sys.platform.startswith("darwin") and os.environ.get(
"DYLD_LIBRARY_PATH", None
):
dll_path.extend(
[p.strip() for p in os.environ["DYLD_LIBRARY_PATH"].split(":")]
)
# Pip lib directory
dll_path.append(os.path.join(ffi_dir, ".."))
# Default cmake build directory
dll_path.append(os.path.join(source_dir, "build"))
dll_path.append(os.path.join(source_dir, "build", "Release"))
# Default make build directory
dll_path.append(os.path.join(source_dir, "lib"))
dll_path.append(install_lib_dir)
if search_path is not None:
if isinstance(search_path, (list, tuple, set)):
dll_path = dll_path + list(search_path)
elif isinstance(search_path, str):
dll_path.append(search_path)
else:
raise ValueError(
"type(search_path)={} is invalid".format(type(search_path))
)
dll_path = [
str(x.absolute()) if isinstance(x, pathlib.Path) else os.path.abspath(x)
for x in dll_path
]
if name is None:
if sys.platform.startswith("win32"):
name = ["libdgl.dll", "dgl.dll"]
elif sys.platform.startswith("darwin"):
name = "libdgl.dylib"
else:
name = "libdgl.so"
if isinstance(name, str):
name = [name]
lib_dll_path = []
for n in name:
lib_dll_path += [os.path.join(p, n) for p in dll_path]
lib_found = [p for p in lib_dll_path if os.path.isfile(p)]
if not lib_found:
message = (
"Cannot find the files.\n"
+ "List of candidates:\n"
+ str("\n".join(lib_dll_path))
)
if not optional:
raise RuntimeError(message)
return None
return lib_found
# current version
# We use the version of the incoming release for code
# that is under development.
# The following line is set by dgl/python/update_version.py
__version__ = "2.5"
+448
View File
@@ -0,0 +1,448 @@
# pylint: disable=invalid-name, unused-import
"""Runtime NDArray api"""
from __future__ import absolute_import
import ctypes
import sys
import numpy as np
from .base import _FFI_MODE, _LIB, c_array, c_str, check_call, string_types
from .runtime_ctypes import (
dgl_shape_index_t,
DGLArray,
DGLArrayHandle,
DGLContext,
DGLDataType,
TypeCode,
)
IMPORT_EXCEPT = RuntimeError if _FFI_MODE == "cython" else ImportError
try:
# pylint: disable=wrong-import-position
if _FFI_MODE == "ctypes":
raise ImportError()
if sys.version_info >= (3, 0):
from ._cy3.core import (
_from_dlpack,
_make_array,
_reg_extension,
_set_class_ndarray,
NDArrayBase as _NDArrayBase,
)
else:
from ._cy2.core import (
_from_dlpack,
_make_array,
_reg_extension,
_set_class_ndarray,
NDArrayBase as _NDArrayBase,
)
except IMPORT_EXCEPT:
# pylint: disable=wrong-import-position
from ._ctypes.ndarray import (
_from_dlpack,
_make_array,
_reg_extension,
_set_class_ndarray,
NDArrayBase as _NDArrayBase,
)
def context(dev_type, dev_id=0):
"""Construct a DGL context with given device type and id.
Parameters
----------
dev_type: int or str
The device type mask or name of the device.
dev_id : int, optional
The integer device id
Returns
-------
ctx: DGLContext
The corresponding context.
Examples
--------
Context can be used to create reflection of context by
string representation of the device type.
.. code-block:: python
assert dgl.context("cpu", 1) == dgl.cpu(1)
assert dgl.context("gpu", 0) == dgl.gpu(0)
assert dgl.context("cuda", 0) == dgl.gpu(0)
"""
if isinstance(dev_type, string_types):
dev_type = dev_type.split()[0]
if dev_type not in DGLContext.STR2MASK:
raise ValueError("Unknown device type %s" % dev_type)
dev_type = DGLContext.STR2MASK[dev_type]
return DGLContext(dev_type, dev_id)
def numpyasarray(np_data):
"""Return a DGLArray representation of a numpy array."""
data = np_data
assert data.flags["C_CONTIGUOUS"]
arr = DGLArray()
shape = c_array(dgl_shape_index_t, data.shape)
arr.data = data.ctypes.data_as(ctypes.c_void_p)
arr.shape = shape
arr.strides = None
arr.dtype = DGLDataType(np.dtype(data.dtype).name)
arr.ndim = data.ndim
# CPU device
arr.ctx = context(1, 0)
return arr, shape
def empty(shape, dtype="float32", ctx=context(1, 0)):
"""Create an empty array given shape and device
Parameters
----------
shape : tuple of int
The shape of the array
dtype : type or str
The data type of the array.
ctx : DGLContext
The context of the array
Returns
-------
arr : dgl.nd.NDArray
The array dgl supported.
"""
shape = c_array(dgl_shape_index_t, shape)
ndim = ctypes.c_int(len(shape))
handle = DGLArrayHandle()
dtype = DGLDataType(dtype)
check_call(
_LIB.DGLArrayAlloc(
shape,
ndim,
ctypes.c_int(dtype.type_code),
ctypes.c_int(dtype.bits),
ctypes.c_int(dtype.lanes),
ctx.device_type,
ctx.device_id,
ctypes.byref(handle),
)
)
return _make_array(handle, False)
def empty_shared_mem(name, is_create, shape, dtype="float32"):
"""Create an empty array with shared memory given shape and dtype
Parameters
----------
name : string
The name of the shared memory. It's a file name in Unix.
is_create : bool
Whether to create the shared memory or use the one created by somewhere else.
shape : tuple of int
The shape of the array
dtype : type or str
The data type of the array.
Returns
-------
arr : dgl.nd.NDArray
The array dgl supported.
"""
name = ctypes.c_char_p(name.encode("utf-8"))
shape = c_array(dgl_shape_index_t, shape)
ndim = ctypes.c_int(len(shape))
handle = DGLArrayHandle()
dtype = DGLDataType(dtype)
check_call(
_LIB.DGLArrayAllocSharedMem(
name,
shape,
ndim,
ctypes.c_int(dtype.type_code),
ctypes.c_int(dtype.bits),
ctypes.c_int(dtype.lanes),
is_create,
ctypes.byref(handle),
)
)
return _make_array(handle, False)
def from_dlpack(dltensor):
"""Produce an array from a DLPack tensor without memory copy.
Retrieves the underlying DLPack tensor's pointer to create an array from the
data. Removes the original DLPack tensor's destructor as now the array is
responsible for destruction.
Parameters
----------
dltensor : DLPack tensor
Input DLManagedTensor, can only be consumed once.
Returns
-------
arr: dgl.nd.NDArray
The array view of the tensor data.
"""
return _from_dlpack(dltensor)
class NDArrayBase(_NDArrayBase):
"""A simple Device/CPU Array object in runtime."""
@property
def shape(self):
"""Shape of this array"""
return tuple(
self.handle.contents.shape[i]
for i in range(self.handle.contents.ndim)
)
@property
def dtype(self):
"""Type of this array"""
return str(self.handle.contents.dtype)
@property
def ctx(self):
"""context of this array"""
return self.handle.contents.ctx
@property
def context(self):
"""context of this array"""
return self.ctx
def __hash__(self):
return ctypes.cast(self.handle, ctypes.c_void_p).value
def __eq__(self, other):
return self.same_as(other)
def __ne__(self, other):
return not self.__eq__(other)
def same_as(self, other):
"""Check object identity equality
Parameters
----------
other : object
The other object to compare to
Returns
-------
same : bool
Whether other is same as self.
"""
if not isinstance(other, NDArrayBase):
return False
return self.__hash__() == other.__hash__()
def __setitem__(self, in_slice, value):
"""Set ndarray value"""
if (
not isinstance(in_slice, slice)
or in_slice.start is not None
or in_slice.stop is not None
):
raise ValueError("Array only support set from numpy array")
if isinstance(value, NDArrayBase):
if value.handle is not self.handle:
value.copyto(self)
elif isinstance(value, (np.ndarray, np.generic)):
self.copyfrom(value)
else:
raise TypeError("type %s not supported" % str(type(value)))
def copyfrom(self, source_array):
"""Perform a synchronized copy from the array.
Parameters
----------
source_array : array_like
The data source we should like to copy from.
Returns
-------
arr : NDArray
Reference to self.
"""
if isinstance(source_array, NDArrayBase):
source_array.copyto(self)
return self
if not isinstance(source_array, np.ndarray):
try:
source_array = np.asarray(source_array, dtype=self.dtype)
except:
raise TypeError(
"array must be an array_like data,"
+ "type %s is not supported" % str(type(source_array))
)
t = DGLDataType(self.dtype)
shape, dtype = self.shape, self.dtype
if t.lanes > 1:
shape = shape + (t.lanes,)
t.lanes = 1
dtype = str(t)
if source_array.shape != shape:
raise ValueError(
"array shape do not match the shape of NDArray {0} vs {1}".format(
source_array.shape, shape
)
)
source_array = np.ascontiguousarray(source_array, dtype=dtype)
assert source_array.flags["C_CONTIGUOUS"]
data = source_array.ctypes.data_as(ctypes.c_void_p)
nbytes = ctypes.c_size_t(
source_array.size * source_array.dtype.itemsize
)
check_call(_LIB.DGLArrayCopyFromBytes(self.handle, data, nbytes))
return self
def __repr__(self):
res = "dgl.{0}@{1}".format(self.asnumpy().__repr__(), self.context)
return res
def __str__(self):
return str(self.asnumpy())
def asnumpy(self):
"""Convert this array to numpy array
Returns
-------
np_arr : numpy.ndarray
The corresponding numpy array.
"""
t = DGLDataType(self.dtype)
shape, dtype = self.shape, self.dtype
if t.lanes > 1:
shape = shape + (t.lanes,)
t.lanes = 1
dtype = str(t)
np_arr = np.empty(shape, dtype=dtype)
assert np_arr.flags["C_CONTIGUOUS"]
data = np_arr.ctypes.data_as(ctypes.c_void_p)
nbytes = ctypes.c_size_t(np_arr.size * np_arr.dtype.itemsize)
check_call(_LIB.DGLArrayCopyToBytes(self.handle, data, nbytes))
return np_arr
def copyto(self, target):
"""Copy array to target
Parameters
----------
target : NDArray
The target array to be copied, must have same shape as this array.
"""
if isinstance(target, DGLContext):
target = empty(self.shape, self.dtype, target)
if isinstance(target, NDArrayBase):
check_call(_LIB.DGLArrayCopyFromTo(self.handle, target.handle))
else:
raise ValueError("Unsupported target type %s" % str(type(target)))
return target
def pin_memory_(self):
"""Pin host memory and map into GPU address space (in-place)"""
check_call(_LIB.DGLArrayPinData(self.handle))
def unpin_memory_(self):
"""Unpin host memory pinned by pin_memory_()"""
check_call(_LIB.DGLArrayUnpinData(self.handle))
def record_stream(self, stream):
"""Record the stream that is using this tensor.
Note
----
This API is more for testing. Users should call ``record_stream``
on torch.Tensor or dgl.graph directly.
Parameters
----------
stream : DGLStreamHandle
"""
check_call(_LIB.DGLArrayRecordStream(self.handle, stream))
def free_extension_handle(handle, type_code):
"""Free c++ extension type handle
Parameters
----------
handle : ctypes.c_void_p
The handle to the extension type.
type_code : int
The tyoe code
"""
check_call(_LIB.DGLExtTypeFree(handle, ctypes.c_int(type_code)))
def register_extension(cls, fcreate=None):
"""Register a extension class to DGL.
After the class is registered, the class will be able
to directly pass as Function argument generated by DGL.
Parameters
----------
cls : class
The class object to be registered as extension.
Note
----
The registered class is requires one property: _dgl_handle and a class attribute _dgl_tcode.
- ```_dgl_handle``` returns integer represents the address of the handle.
- ```_dgl_tcode``` gives integer represents type code of the class.
Returns
-------
cls : class
The class being registered.
fcreate : function, optional
The creation function to create a class object given handle value.
Example
-------
The following code registers user defined class
MyTensor to be DLTensor compatible.
.. code-block:: python
@dgl.register_extension
class MyTensor(object):
_dgl_tcode = dgl.TypeCode.ARRAY_HANDLE
def __init__(self):
self.handle = _LIB.NewDLTensor()
@property
def _dgl_handle(self):
return self.handle.value
"""
if fcreate and cls._dgl_tcode < TypeCode.EXT_BEGIN:
raise ValueError(
"Cannot register create when extension tcode is same as buildin"
)
_reg_extension(cls, fcreate)
return cls
+117
View File
@@ -0,0 +1,117 @@
"""Object namespace"""
# pylint: disable=unused-import
from __future__ import absolute_import
import ctypes
import sys
from .. import _api_internal
from .base import _FFI_MODE, _LIB, c_str, check_call, py_str
from .object_generic import convert_to_object, ObjectGeneric
# pylint: disable=invalid-name
IMPORT_EXCEPT = RuntimeError if _FFI_MODE == "cython" else ImportError
try:
# pylint: disable=wrong-import-position
if _FFI_MODE == "ctypes":
raise ImportError()
if sys.version_info >= (3, 0):
from ._cy3.core import _register_object, ObjectBase as _ObjectBase
else:
from ._cy2.core import _register_object, ObjectBase as _ObjectBase
except IMPORT_EXCEPT:
# pylint: disable=wrong-import-position
from ._ctypes.object import _register_object, ObjectBase as _ObjectBase
def _new_object(cls):
"""Helper function for pickle"""
return cls.__new__(cls)
class ObjectBase(_ObjectBase):
"""ObjectBase is the base class of all DGL CAPI object.
The core attribute is ``handle``, which is a C raw pointer. It must be initialized
via ``__init_handle_by_constructor__``.
Note that the same handle **CANNOT** be shared across multiple ObjectBase instances.
"""
def __dir__(self):
plist = ctypes.POINTER(ctypes.c_char_p)()
size = ctypes.c_uint()
check_call(
_LIB.DGLObjectListAttrNames(
self.handle, ctypes.byref(size), ctypes.byref(plist)
)
)
names = []
for i in range(size.value):
names.append(py_str(plist[i]))
return names
def __hash__(self):
return _api_internal._raw_ptr(self)
def __eq__(self, other):
return self.same_as(other)
def __ne__(self, other):
return not self.__eq__(other)
def __reduce__(self):
cls = type(self)
return (_new_object, (cls,), self.__getstate__())
def __getstate__(self):
# TODO(minjie): TVM assumes that a Node (Object in DGL) can be serialized
# to json. However, this is not true in DGL because DGL Object is meant
# for runtime API, so it could contain binary data such as NDArray.
# If this feature is required, please raise a RFC to DGL issue.
raise RuntimeError("__getstate__ is not supported for object type")
def __setstate__(self, state):
# pylint: disable=assigning-non-slot
# TODO(minjie): TVM assumes that a Node (Object in DGL) can be serialized
# to json. However, this is not true in DGL because DGL Object is meant
# for runtime API, so it could contain binary data such as NDArray.
# If this feature is required, please raise a RFC to DGL issue.
raise RuntimeError("__setstate__ is not supported for object type")
def same_as(self, other):
"""check object identity equality"""
if not isinstance(other, ObjectBase):
return False
return self.__hash__() == other.__hash__()
def register_object(type_key=None):
"""Decorator used to register object type
Examples
--------
>>> @register_object
>>> class MyObject:
>>> ... pass
Parameters
----------
type_key : str or cls
The type key of the object
"""
object_name = type_key if isinstance(type_key, str) else type_key.__name__
def register(cls):
"""internal register function"""
tindex = ctypes.c_int()
ret = _LIB.DGLObjectTypeKey2Index(
c_str(object_name), ctypes.byref(tindex)
)
if ret == 0:
_register_object(tindex.value, cls)
return cls
if isinstance(type_key, str):
return register
return register(type_key)
+59
View File
@@ -0,0 +1,59 @@
"""Common implementation of Object generic related logic"""
# pylint: disable=unused-import
from __future__ import absolute_import
from numbers import Integral, Number
from .. import _api_internal
from .base import string_types
# Object base class
_CLASS_OBJECT_BASE = None
def _set_class_object_base(cls):
global _CLASS_OBJECT_BASE
_CLASS_OBJECT_BASE = cls
class ObjectGeneric(object):
"""Base class for all classes that can be converted to object."""
def asobject(self):
"""Convert value to object"""
raise NotImplementedError()
def convert_to_object(value):
"""Convert a python value to corresponding object type.
Parameters
----------
value : str
The value to be inspected.
Returns
-------
object : Object
The corresponding object value.
"""
if isinstance(value, _CLASS_OBJECT_BASE):
return value
if isinstance(value, (list, tuple)):
value = [convert_to_object(x) for x in value]
return _api_internal._List(*value)
if isinstance(value, dict):
vlist = []
for item in value.items():
if not isinstance(item[0], _CLASS_OBJECT_BASE) and not isinstance(
item[0], string_types
):
raise ValueError(
"key of map must already been a container type"
)
vlist.append(item[0])
vlist.append(convert_to_object(item[1]))
return _api_internal._Map(*vlist)
if isinstance(value, ObjectGeneric):
return value.asobject()
return _api_internal._Value(value)
+278
View File
@@ -0,0 +1,278 @@
"""Common runtime ctypes."""
# pylint: disable=invalid-name, super-init-not-called
from __future__ import absolute_import
import ctypes
import json
import numpy as np
from .. import _api_internal
from .base import _LIB, check_call
dgl_shape_index_t = ctypes.c_int64
class TypeCode(object):
"""Type code used in API calls"""
INT = 0
UINT = 1
FLOAT = 2
HANDLE = 3
NULL = 4
DGL_DATA_TYPE = 5
DGL_CONTEXT = 6
ARRAY_HANDLE = 7
OBJECT_HANDLE = 8
MODULE_HANDLE = 9
FUNC_HANDLE = 10
STR = 11
BYTES = 12
NDARRAY_CONTAINER = 13
EXT_BEGIN = 15
class DGLByteArray(ctypes.Structure):
"""Temp data structure for byte array."""
_fields_ = [
("data", ctypes.POINTER(ctypes.c_byte)),
("size", ctypes.c_size_t),
]
class DGLDataType(ctypes.Structure):
"""DGL datatype structure"""
_fields_ = [
("type_code", ctypes.c_uint8),
("bits", ctypes.c_uint8),
("lanes", ctypes.c_uint16),
]
CODE2STR = {0: "int", 1: "uint", 2: "float", 4: "handle"}
_cache = {}
def __new__(cls, type_str):
if type_str in cls._cache:
return cls._cache[type_str]
inst = super(DGLDataType, cls).__new__(DGLDataType)
if isinstance(type_str, np.dtype):
type_str = str(type_str)
arr = type_str.split("x")
head = arr[0]
inst.lanes = int(arr[1]) if len(arr) > 1 else 1
bits = 32
if head.startswith("int"):
inst.type_code = 0
head = head[3:]
elif head.startswith("uint"):
inst.type_code = 1
head = head[4:]
elif head.startswith("float"):
inst.type_code = 2
head = head[5:]
elif head.startswith("handle"):
inst.type_code = 4
bits = 64
head = ""
else:
raise ValueError("Do not know how to handle type %s" % type_str)
bits = int(head) if head else bits
inst.bits = bits
cls._cache[type_str] = inst
return inst
def __init__(self, type_str):
pass
def __repr__(self):
x = "%s%d" % (DGLDataType.CODE2STR[self.type_code], self.bits)
if self.lanes != 1:
x += "x%d" % self.lanes
return x
def __eq__(self, other):
return (
self.bits == other.bits
and self.type_code == other.type_code
and self.lanes == other.lanes
)
def __ne__(self, other):
return not self.__eq__(other)
RPC_SESS_MASK = 128
class DGLContext(ctypes.Structure):
"""DGL context strucure."""
_fields_ = [("device_type", ctypes.c_int), ("device_id", ctypes.c_int)]
MASK2STR = {
1: "cpu",
2: "gpu",
4: "opencl",
5: "aocl",
6: "sdaccel",
7: "vulkan",
8: "metal",
9: "vpi",
10: "rocm",
11: "opengl",
12: "ext_dev",
}
STR2MASK = {
"llvm": 1,
"stackvm": 1,
"cpu": 1,
"gpu": 2,
"cuda": 2,
"nvptx": 2,
"cl": 4,
"opencl": 4,
"aocl": 5,
"aocl_sw_emu": 5,
"sdaccel": 6,
"vulkan": 7,
"metal": 8,
"vpi": 9,
"rocm": 10,
"opengl": 11,
"ext_dev": 12,
}
_cache = {}
def __new__(cls, device_type, device_id):
if (device_type, device_id) in cls._cache:
return cls._cache[(device_type, device_id)]
inst = super(DGLContext, cls).__new__(DGLContext)
inst.device_type = device_type
inst.device_id = device_id
cls._cache[(device_type, device_id)] = inst
return inst
def __init__(self, device_type, device_id):
pass
@property
def exist(self):
"""Whether this device exist."""
return (
_api_internal._GetDeviceAttr(self.device_type, self.device_id, 0)
!= 0
)
@property
def max_threads_per_block(self):
"""Maximum number of threads on each block."""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 1)
@property
def warp_size(self):
"""Number of threads that executes in concurrent."""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 2)
@property
def max_shared_memory_per_block(self):
"""Total amount of shared memory per block in bytes."""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 3)
@property
def compute_version(self):
"""Get compute verison number in string.
Currently used to get compute capability of CUDA device.
Returns
-------
version : str
The version string in `major.minor` format.
"""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 4)
@property
def device_name(self):
"""Return the string name of device."""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 5)
@property
def max_clock_rate(self):
"""Return the max clock frequency of device."""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 6)
@property
def multi_processor_count(self):
"""Return the number of compute units of device."""
return _api_internal._GetDeviceAttr(self.device_type, self.device_id, 7)
@property
def max_thread_dimensions(self):
"""Return the maximum size of each thread axis
Returns
-------
dims: List of int
The maximum length of threadIdx.x, threadIdx.y, threadIdx.z
"""
return json.loads(
_api_internal._GetDeviceAttr(self.device_type, self.device_id, 8)
)
def sync(self):
"""Synchronize until jobs finished at the context."""
check_call(_LIB.DGLSynchronize(self.device_type, self.device_id, None))
def __eq__(self, other):
return (
isinstance(other, DGLContext)
and self.device_id == other.device_id
and self.device_type == other.device_type
)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
if self.device_type >= RPC_SESS_MASK:
tbl_id = self.device_type / RPC_SESS_MASK - 1
dev_type = self.device_type % RPC_SESS_MASK
return "remote[%d]:%s(%d)" % (
tbl_id,
DGLContext.MASK2STR[dev_type],
self.device_id,
)
return "%s(%d)" % (
DGLContext.MASK2STR[self.device_type],
self.device_id,
)
def __hash__(self):
return hash((self.device_type, self.device_id))
class DGLArray(ctypes.Structure):
"""DGLValue in C API"""
_fields_ = [
("data", ctypes.c_void_p),
("ctx", DGLContext),
("ndim", ctypes.c_int),
("dtype", DGLDataType),
("shape", ctypes.POINTER(dgl_shape_index_t)),
("strides", ctypes.POINTER(dgl_shape_index_t)),
("byte_offset", ctypes.c_uint64),
]
DGLArrayHandle = ctypes.POINTER(DGLArray)
DGLStreamHandle = ctypes.c_void_p
+46
View File
@@ -0,0 +1,46 @@
# pylint: disable=invalid-name, unused-import
"""Runtime stream APIs which are mainly for internal test use only.
For applications, please use PyTorch's stream management, of which DGL is aware.
"""
from __future__ import absolute_import
import ctypes
from .base import _FFI_MODE, _LIB, check_call
from .runtime_ctypes import DGLStreamHandle
def to_dgl_stream_handle(cuda_stream):
"""Convert torch.cuda.Stream to DGL stream handle
Parameters
----------
cuda_stream : torch.cuda.Stream.
Returns
-------
DGLStreamHandle
DGLStreamHandle of the input ``cuda_stream``.
"""
return ctypes.c_void_p(cuda_stream.cuda_stream)
def _dgl_get_stream(ctx):
"""Get the current CUDA stream of the given DGL context.
Parameters
----------
ctx : DGL context.
Returns
-------
DGLStreamHandle
DGLStreamHandle of the current CUDA stream.
"""
current_cuda_stream = DGLStreamHandle()
check_call(
_LIB.DGLGetStream(
ctx.device_type, ctx.device_id, ctypes.byref(current_cuda_stream)
)
)
return current_cuda_stream
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
from __future__ import absolute_import
import importlib
import json
import logging
import os
import sys
from . import backend
from .set_default_backend import set_default_backend
_enabled_apis = set()
logger = logging.getLogger("dgl-core")
def _gen_missing_api(api, mod_name):
def _missing_api(*args, **kwargs):
raise ImportError(
'API "%s" is not supported by backend "%s".'
" You can switch to other backends by setting"
" the DGLBACKEND environment." % (api, mod_name)
)
return _missing_api
def load_backend(mod_name):
# Load backend does four things:
# (1) Import backend framework (PyTorch, MXNet, Tensorflow, etc.)
# (2) Import DGL C library. DGL imports it *after* PyTorch/MXNet/Tensorflow. Otherwise
# DGL will crash with errors like `munmap_chunk(): invalid pointer`.
# (3) Sets up the tensoradapter library path.
# (4) Import the Python wrappers of the backend framework. DGL does this last because
# it already depends on both the backend framework and the DGL C library.
if mod_name == "pytorch":
import torch
mod = torch
elif mod_name == "mxnet":
import mxnet
mod = mxnet
elif mod_name == "tensorflow":
import tensorflow
mod = tensorflow
else:
raise NotImplementedError("Unsupported backend: %s" % mod_name)
from .._ffi.base import load_tensor_adapter # imports DGL C library
version = mod.__version__
load_tensor_adapter(mod_name, version)
logger.debug("Using backend: %s" % mod_name)
mod = importlib.import_module(".%s" % mod_name, __name__)
thismod = sys.modules[__name__]
for api in backend.__dict__.keys():
if api.startswith("__"):
# ignore python builtin attributes
continue
if api == "data_type_dict":
# load data type
if api not in mod.__dict__:
raise ImportError(
'API "data_type_dict" is required but missing for'
' backend "%s".' % (mod_name)
)
data_type_dict = mod.__dict__[api]()
for name, dtype in data_type_dict.items():
setattr(thismod, name, dtype)
# override data type dict function
setattr(thismod, "data_type_dict", data_type_dict)
# for data types with aliases, treat the first listed type as
# the true one
rev_data_type_dict = {}
for k, v in data_type_dict.items():
if not v in rev_data_type_dict.keys():
rev_data_type_dict[v] = k
setattr(thismod, "reverse_data_type_dict", rev_data_type_dict)
# log backend name
setattr(thismod, "backend_name", mod_name)
else:
# load functions
if api in mod.__dict__:
_enabled_apis.add(api)
setattr(thismod, api, mod.__dict__[api])
else:
setattr(thismod, api, _gen_missing_api(api, mod_name))
def get_preferred_backend():
default_dir = None
if "DGLDEFAULTDIR" in os.environ:
default_dir = os.getenv("DGLDEFAULTDIR")
else:
default_dir = os.path.join(os.path.expanduser("~"), ".dgl")
config_path = os.path.join(default_dir, "config.json")
backend_name = None
if "DGLBACKEND" in os.environ:
backend_name = os.getenv("DGLBACKEND")
elif os.path.exists(config_path):
with open(config_path, "r") as config_file:
config_dict = json.load(config_file)
backend_name = config_dict.get("backend", "").lower()
if backend_name in ["tensorflow", "mxnet", "pytorch"]:
return backend_name
else:
print(
"DGL backend not selected or invalid. "
"Assuming PyTorch for now.",
file=sys.stderr,
)
set_default_backend(default_dir, "pytorch")
return "pytorch"
load_backend(get_preferred_backend())
def is_enabled(api):
"""Return true if the api is enabled by the current backend.
Parameters
----------
api : str
The api name.
Returns
-------
bool
True if the API is enabled by the current backend.
"""
return api in _enabled_apis
def to_dgl_nd(data):
return zerocopy_to_dgl_ndarray(data)
def from_dgl_nd(data):
return zerocopy_from_dgl_ndarray(data)
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
from .sparse import *
from .tensor import *
+558
View File
@@ -0,0 +1,558 @@
import mxnet as mx
import numpy as np
from mxnet import nd
from ..._sparse_ops import (
_bwd_segment_cmp,
_csrmask,
_csrmm,
_csrsum,
_gsddmm,
_gspmm,
_scatter_add,
_segment_reduce,
)
from ...base import ALL, dgl_warning, is_all
from ...heterograph_index import create_unitgraph_from_csr
from .tensor import (
asnumpy,
context,
copy_to,
to_backend_ctx,
zerocopy_from_numpy,
)
__all__ = [
"gspmm",
"gsddmm",
"edge_softmax",
"segment_reduce",
"scatter_add",
"csrmm",
"csrsum",
"csrmask",
]
def _scatter_nd(index, src, n_rows):
"""Similar to PyTorch's scatter nd on first dimension."""
assert index.shape == src.shape
dgl_warning("MXNet do not support scatter_add, fallback to numpy.")
ctx = context(src)
index = asnumpy(index)
src = asnumpy(src)
shp = index.shape
ndim = src.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = np.arange(di, dtype=index.dtype)
offsets.append(
(stride * offset_i).reshape(
(1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + sum(offsets)
else:
new_idx = index
src = src.reshape(-1)
new_idx = new_idx.reshape(-1)
rst = np.zeros((stride * n_rows,), dtype=src.dtype)
np.add.at(rst, new_idx, src)
rst = rst.reshape(n_rows, *shp[1:])
rst = copy_to(zerocopy_from_numpy(rst), ctx)
return rst
def _gather_nd(index, src):
"""Similar to PyTorch's gather nd on first dimension."""
ctx = context(src)
shp = index.shape
ndim = src.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = nd.arange(di, dtype=index.dtype)
offsets.append(
(stride * offset_i).reshape(
(1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + copy_to(sum(offsets), ctx)
else:
new_idx = index
src = src.reshape(-1)
new_idx = new_idx.reshape(-1)
rst = nd.take(src, new_idx).reshape(shp)
return rst
def _reduce_grad(grad, shape):
"""Reduce gradient on the broadcast dimension
If there is broadcast in forward pass, gradients need to be reduced on
broadcast dimension. This function checks the input tensor shape and
gradient shape and perform the reduction.
Parameters
----------
grad: Tensor
Gradient tensor
shape: tuple
Shape of input tensor
Returns
-------
Tensor
"""
grad_shape = grad.shape[1:]
in_shape = shape[1:]
if in_shape == grad_shape:
# no need to reduce
return grad
num_to_squeeze = len(grad_shape) - len(in_shape)
# pad inshape
in_shape = (1,) * num_to_squeeze + in_shape
# pad in_shape
in_shape = (1,) * num_to_squeeze + in_shape
reduce_idx = np.nonzero(np.asarray(grad_shape) - np.asarray(in_shape))[0]
reduce_idx += 1 # skip batch dim
grad = grad.sum(axis=tuple(reduce_idx), keepdims=True)
return grad.reshape(shape)
def _need_reduce_last_dim(ufeat, efeat):
"""Indicates whether to reduce the last dimension on edges
in the backward pass of spmm,
if so, use dot instead of mul."""
ushp = ufeat.shape
eshp = efeat.shape
return ushp[1:-1] == eshp[1:-1] and eshp[-1] == 1 and ushp[-1] > 1
def _muldiv(op, x):
return 1.0 / x if op == "div" else x
def _addsub(op, x):
return -x if op == "sub" else x
def _expand(x, shape):
return x.broadcast_to((x.shape[0], *shape))
class GSpMM(mx.autograd.Function):
def __init__(self, gidx, op, reduce_op):
super(GSpMM, self).__init__()
self.gidx = gidx
self.op = op
self.reduce_op = reduce_op
def forward(self, X, Y):
out, (argX, argY) = _gspmm(self.gidx, self.op, self.reduce_op, X, Y)
self.save_for_backward(X, Y, argX, argY)
return out
def backward(self, dZ):
ctx = context(dZ)
X, Y, argX, argY = self.saved_tensors
gidx, op, reduce_op = self.gidx, self.op, self.reduce_op
if op != "copy_rhs":
g_rev = gidx.reverse()
if reduce_op == "sum":
if op in ["mul", "div"]:
dX = _gspmm(g_rev, "mul", "sum", dZ, _muldiv(op, Y))[0]
elif op in ["add", "sub"]:
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, Y)[0]
elif op == "copy_lhs":
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, None)[0]
else:
if op in ["mul", "div"]:
dX = _scatter_nd(
argX,
_muldiv(op, _gather_nd(argY, _expand(Y, dZ.shape[1:])))
* dZ,
X.shape[0],
)
elif op in ["add", "sub", "copy_lhs"]:
dX = _scatter_nd(argX, dZ, X.shape[0])
dX = _reduce_grad(dX, X.shape)
else:
dX = nd.zeros_like(X)
if op != "copy_lhs":
if reduce_op == "sum":
if op == "mul" and _need_reduce_last_dim(X, Y):
dY = _gsddmm(gidx, "dot", X, dZ)
elif op in ["mul", "div"]:
dY = _gsddmm(gidx, "mul", X, dZ)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _gsddmm(gidx, "copy_rhs", X, _addsub(op, dZ))
else:
if op in ["mul", "div"]:
dY = _scatter_nd(
argY,
_gather_nd(argX, _expand(X, dZ.shape[1:])) * dZ,
Y.shape[0],
)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _scatter_nd(argY, _addsub(op, dZ), Y.shape[0])
dY = _reduce_grad(dY, Y.shape)
else:
dY = nd.zeros_like(Y)
self.saved_tensors = None
return dX, dY
def gspmm(gidx, op, reduce_op, lhs_data, rhs_data):
func = GSpMM(gidx, op, reduce_op)
ctx = to_backend_ctx(gidx.ctx)
# XXX(minjie): There is a bug in MXNet's autograd system when one of the inputs
# does not require gradient. Although it still invokes the backward function,
# it does not set the gradient value to the correct buffer, resulting all the
# input gradients to be zero. Fix this by enforcing all the inputs to require
# gradients.
if lhs_data is None:
lhs_data = nd.zeros((1,), ctx=ctx)
lhs_data.attach_grad()
if rhs_data is None:
rhs_data = nd.zeros((1,), ctx=ctx)
rhs_data.attach_grad()
return func(lhs_data, rhs_data)
class GSDDMM(mx.autograd.Function):
def __init__(self, gidx, op, lhs_target, rhs_target):
super(GSDDMM, self).__init__()
self.gidx = gidx
self.op = op
self.lhs_target = lhs_target
self.rhs_target = rhs_target
def forward(self, X, Y):
out = _gsddmm(
self.gidx, self.op, X, Y, self.lhs_target, self.rhs_target
)
self.save_for_backward(X, Y)
return out
def backward(self, dZ):
ctx = context(dZ)
X, Y = self.saved_tensors
gidx, op = self.gidx, self.op
lhs_target, rhs_target = self.lhs_target, self.rhs_target
if op != "copy_rhs":
if lhs_target in ["u", "v"]:
_gidx = gidx if self.lhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_lhs"]:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0]
else: # mul, div, dot
if rhs_target == lhs_target:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[
0
] * _muldiv(op, Y)
elif self.rhs_target == "e":
dX = _gspmm(
_gidx, "copy_rhs", "sum", None, dZ * _muldiv(op, Y)
)[0]
else: # rhs_target = !lhs_target
dX = _gspmm(_gidx, "mul", "sum", _muldiv(op, Y), dZ)[0]
else: # lhs_target == 'e'
if op in ["add", "sub", "copy_lhs"]:
dX = dZ
else: # mul, div, dot
dX = _gsddmm(
gidx, "mul", dZ, _muldiv(op, Y), "e", rhs_target
)
dX = _reduce_grad(dX, X.shape)
else:
dX = nd.zeros_like(X)
if op != "copy_lhs":
if self.rhs_target in ["u", "v"]:
_gidx = gidx if rhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_rhs"]:
dY = _gspmm(
_gidx, "copy_rhs", "sum", None, _addsub(op, dZ)
)[0]
else: # mul, div, dot
if lhs_target == rhs_target:
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0] * X
elif self.lhs_target == "e":
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ * X)[0]
else: # rhs_target = !lhs_target
dY = _gspmm(_gidx, "mul", "sum", X, dZ)[0]
if op == "div":
dY = -dY / (Y**2)
else:
if op in ["add", "sub", "copy_rhs"]:
dY = _addsub(op, dZ)
else: # mul, div, dot
dY = _gsddmm(gidx, "mul", dZ, X, "e", lhs_target)
if op == "div":
dY = -dY / (Y**2)
dY = _reduce_grad(dY, Y.shape)
else:
dY = nd.zeros_like(Y)
self.saved_tensors = None
return dX, dY
def gsddmm(gidx, op, lhs_data, rhs_data, lhs_target="u", rhs_target="v"):
func = GSDDMM(gidx, op, lhs_target, rhs_target)
ctx = to_backend_ctx(gidx.ctx)
if lhs_data is None:
lhs_data = nd.zeros((1,), ctx=ctx)
if rhs_data is None:
rhs_data = nd.zeros((1,), ctx=ctx)
return func(lhs_data, rhs_data)
class EdgeSoftmax(mx.autograd.Function):
def __init__(self, gidx, eids, norm_by):
super(EdgeSoftmax, self).__init__()
if not is_all(eids):
gidx = gidx.edge_subgraph([eids], True).graph
if norm_by == "src":
gidx = gidx.reverse()
self.gidx = gidx
def forward(self, score):
"""Forward function.
Pseudo-code:
.. code:: python
score = dgl.EData(g, score)
score_max = score.dst_max() # of type dgl.NData
score = score - score_max # edge_sub_dst, ret dgl.EData
score_sum = score.dst_sum() # of type dgl.NData
out = score / score_sum # edge_div_dst, ret dgl.EData
return out.data
"""
gidx = self.gidx
score_max = _gspmm(gidx, "copy_rhs", "max", None, score)[0]
score = mx.nd.exp(_gsddmm(gidx, "sub", score, score_max, "e", "v"))
score_sum = _gspmm(gidx, "copy_rhs", "sum", None, score)[0]
out = _gsddmm(gidx, "div", score, score_sum, "e", "v")
self.save_for_backward(out)
return out
def backward(self, grad_out):
"""Backward function.
Pseudo-code:
.. code:: python
g, out = ctx.backward_cache
grad_out = dgl.EData(g, grad_out)
out = dgl.EData(g, out)
sds = out * grad_out # type dgl.EData
sds_sum = sds.dst_sum() # type dgl.NData
grad_score = sds - sds * sds_sum # multiple expressions
"""
(out,) = self.saved_tensors
gidx = self.gidx
sds = out * grad_out
accum = gspmm(gidx, "copy_rhs", "sum", None, sds)
grad_score = sds - gsddmm(gidx, "mul", out, accum, "e", "v")
self.save_tensors = None
return grad_score
def edge_softmax(gidx, logits, eids=ALL, norm_by="dst"):
softmax_op = EdgeSoftmax(gidx, eids, norm_by)
return softmax_op(logits)
class SegmentReduce(mx.autograd.Function):
def __init__(self, op, offsets):
super(SegmentReduce, self).__init__()
self.op = op
self.offsets = offsets
def forward(self, x):
y, arg = _segment_reduce(self.op, x, self.offsets)
self.save_for_backward(arg)
return y
def backward(self, dy):
(arg,) = self.saved_tensors
offsets = self.offsets
m = offsets[-1].asscalar()
if self.op == "sum":
offsets_np = asnumpy(offsets[1:])
indices_np = np.zeros((m + 1,), dtype=offsets_np.dtype)
np.add.at(indices_np, offsets_np, np.ones_like(offsets_np))
indices_np = np.cumsum(indices_np, -1)[:-1]
indices = zerocopy_from_numpy(indices_np)
dx = dy[indices]
else:
dx = _bwd_segment_cmp(dy, arg, m)
return dx
def segment_reduce(op, x, offsets):
segment_reduce_op = SegmentReduce(op, offsets)
return segment_reduce_op(x)
class ScatterAdd(mx.autograd.Function):
def __init__(self, idx, m):
super(ScatterAdd, self).__init__()
self.idx = idx
self.m = m
def forward(self, x):
y = _scatter_add(x, self.idx, self.m)
return y
def backward(self, dy):
return dy[self.idx]
def scatter_add(x, idx, m):
scatter_add_op = ScatterAdd(idx, m)
return scatter_add_op(x)
class CSRMM(mx.autograd.Function):
def __init__(self, gidxA, gidxB, num_vtypes):
super().__init__()
self.gidxA = gidxA
self.gidxB = gidxB
self.num_vtypes = num_vtypes
def forward(self, A_weights, B_weights):
gidxC, C_weights = _csrmm(
self.gidxA, A_weights, self.gidxB, B_weights, self.num_vtypes
)
(
nrows,
ncols,
C_indptr,
C_indices,
C_eids,
) = gidxC.adjacency_matrix_tensors(0, False, "csr")
# Note: the returned C_indptr, C_indices and C_eids tensors MUST be the same
# as the underlying tensors of the created graph gidxC.
self.backward_cache = gidxC
self.save_for_backward(A_weights, B_weights)
nrows = nd.array([nrows], dtype="int64")
ncols = nd.array([ncols], dtype="int64")
return nrows, ncols, C_indptr, C_indices, C_eids, C_weights
def backward(
self, dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights
):
# Only the last argument is meaningful.
gidxC = self.backward_cache
A_weights, B_weights = self.saved_tensors
dgidxA, dA_weights = _csrmm(
gidxC,
dC_weights,
self.gidxB.reverse(),
B_weights,
self.gidxA.number_of_ntypes(),
)
dgidxB, dB_weights = _csrmm(
self.gidxA.reverse(),
A_weights,
gidxC,
dC_weights,
self.gidxB.number_of_ntypes(),
)
dA_weights = _csrmask(dgidxA, dA_weights, self.gidxA)
dB_weights = _csrmask(dgidxB, dB_weights, self.gidxB)
return dA_weights, dB_weights
def csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes):
op = CSRMM(gidxA, gidxB, num_vtypes)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = op(
A_weights, B_weights
)
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.asscalar(),
ncols.asscalar(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
class CSRSum(mx.autograd.Function):
def __init__(self, gidxs):
super().__init__()
self.gidxs = gidxs
def forward(self, *weights):
gidxC, C_weights = _csrsum(self.gidxs, weights)
(
nrows,
ncols,
C_indptr,
C_indices,
C_eids,
) = gidxC.adjacency_matrix_tensors(0, False, "csr")
# Note: the returned C_indptr, C_indices and C_eids tensors MUST be the same
# as the underlying tensors of the created graph gidxC.
self.backward_cache = gidxC
nrows = nd.array([nrows], dtype="int64")
ncols = nd.array([ncols], dtype="int64")
return nrows, ncols, C_indptr, C_indices, C_eids, C_weights
def backward(
self, dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights
):
# Only the last argument is meaningful.
gidxC = self.backward_cache
return tuple(csrmask(gidxC, dC_weights, gidx) for gidx in self.gidxs)
def csrsum(gidxs, weights):
op = CSRSum(gidxs)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = op(*weights)
num_vtypes = gidxs[0].number_of_ntypes()
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.asscalar(),
ncols.asscalar(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
class CSRMask(mx.autograd.Function):
def __init__(self, gidxA, gidxB):
super().__init__()
self.gidxA = gidxA
self.gidxB = gidxB
def forward(self, A_weights):
return _csrmask(self.gidxA, A_weights, self.gidxB)
def backward(self, dB_weights):
return _csrmask(self.gidxB, dB_weights, self.gidxA)
def csrmask(gidxA, A_weights, gidxB):
op = CSRMask(gidxA, gidxB)
return op(A_weights)
+1
View File
@@ -0,0 +1 @@
"""Sparse optimizer is not supported for mxnet"""
+573
View File
@@ -0,0 +1,573 @@
from __future__ import absolute_import
import builtins
import numbers
import os
import mxnet as mx
import mxnet.ndarray as nd
import numpy as np
from ... import ndarray as dglnd
from ...function.base import TargetCode
from ...utils import version
if version.parse(mx.__version__) < version.parse("1.6.0"):
raise RuntimeError("DGL requires MXNet >= 1.6")
# After MXNet 1.5, empty tensors aren't supprted by default.
# After we turn on the numpy compatible flag, MXNet supports empty NDArray.
mx.set_np_shape(bool(os.environ.get("DGL_MXNET_SET_NP_SHAPE", True)))
def data_type_dict():
return {
"float16": np.float16,
"float32": np.float32,
"float64": np.float64,
"uint8": np.uint8,
"int8": np.int8,
"int16": np.int16,
"int32": np.int32,
"int64": np.int64,
"bool": np.bool_,
} # mxnet does not support bool
def cpu():
return mx.cpu()
def tensor(data, dtype=None):
if dtype == np.bool_:
# mxnet doesn't support bool
dtype = np.int32
if isinstance(data, nd.NDArray):
if dtype is None or data.dtype == dtype:
return data
else:
return data.astype(dtype)
else:
if isinstance(data, numbers.Number):
data = [data]
if dtype is None:
if isinstance(data, np.ndarray):
dtype = np.int32 if data.dtype == np.bool_ else data.dtype
elif len(data) == 0:
dtype = np.int64
else:
dtype = (
np.int64
if isinstance(data[0], numbers.Integral)
else np.float32
)
return nd.array(data, dtype=dtype)
def as_scalar(data):
if data.size != 1:
raise ValueError("The current array is not a scalar")
if data.shape != (1,):
data = data.expand_dims(axis=0)
return data.asscalar()
def get_preferred_sparse_format():
"""Get the preferred sparse matrix format supported by the backend.
Different backends have their preferred backend. This info is useful when
constructing a sparse matrix.
"""
return "csr"
def sparse_matrix(data, index, shape, force_format=False):
fmt = index[0]
if fmt == "coo":
if force_format:
raise TypeError(
"MXNet backend only supports CSR format,"
" but COO format is forced."
)
coord = index[1]
# generate convert idx
# FIXME: cannot use int64
tmp_data = nd.arange(
len(coord[0]), dtype=data.dtype, ctx=coord[0].context
)
tmp_spmat = nd.sparse.csr_matrix(
(tmp_data, (coord[0], coord[1])), tuple(shape), ctx=data.context
)
convert_idx = nd.cast(tmp_spmat.data, dtype="int64")
# shuffle the data
data = data[convert_idx]
spmat = nd.sparse.csr_matrix(
(data, tmp_spmat.indices, tmp_spmat.indptr),
tuple(shape),
ctx=data.context,
)
return spmat, convert_idx
elif fmt == "csr":
indices = index[1]
indptr = index[2]
spmat = nd.sparse.csr_matrix(
(data, indices, indptr), tuple(shape), ctx=data.context
)
# No conversion is required.
return spmat, None
else:
raise TypeError("Invalid format: %s." % fmt)
def sparse_matrix_indices(spmat):
return ("csr", spmat.indices, spmat.indptr)
def is_tensor(obj):
return isinstance(obj, nd.NDArray)
def shape(input):
# NOTE: the input cannot be a symbol
return input.shape
def dtype(input):
# NOTE: the input cannot be a symbol
return input.dtype
def ndim(input):
return input.ndim
def context(input):
return input.context
def device_type(ctx):
return ctx.device_type
def device_id(ctx):
return ctx.device_id
def to_backend_ctx(dglctx):
dev_type = dglctx.device_type
if dev_type == 1:
return mx.cpu()
elif dev_type == 2:
return mx.gpu(dglctx.device_id)
else:
raise ValueError("Unsupported DGL device context:", dglctx)
def astype(input, ty):
if ty == np.bool_:
ty = np.int32
return input.astype(ty)
def asnumpy(input):
return input.asnumpy()
def copy_to(input, ctx, **kwargs):
return input.as_in_context(ctx)
def is_pinned(input):
return input.context == mx.cpu_pinned()
def sum(input, dim, keepdims=False):
if len(input) == 0:
return nd.array([0.0], dtype=input.dtype, ctx=input.context)
return nd.sum(input, axis=dim, keepdims=keepdims)
def floor_div(in1, in2):
return in1 / in2
def reduce_sum(input):
return input.sum()
def cumsum(input, dim):
return nd.cumsum(input, axis=dim)
def mean(input, dim):
return nd.mean(input, axis=dim)
def reduce_mean(input):
return input.mean()
def max(input, dim):
return nd.max(input, axis=dim)
def reduce_max(input):
return input.max()
def min(input, dim):
return nd.min(input, axis=dim)
def reduce_min(input):
return input.min()
def topk(input, k, dim, descending=True):
return nd.topk(
input, axis=dim, k=k, ret_typ="value", is_ascend=not descending
)
def argtopk(input, k, dim, descending=True):
idx = nd.argsort(input, dim, is_ascend=not descending)
return nd.slice_axis(input, dim, 0, k)
def argsort(input, dim, descending):
idx = nd.argsort(input, dim, is_ascend=not descending)
idx = nd.cast(idx, dtype="int64")
return idx
def exp(input):
return nd.exp(input)
def inverse(input):
return nd.linalg_inverse(input)
def sqrt(input):
return nd.sqrt(input)
def softmax(input, dim=-1):
return nd.softmax(input, axis=dim)
def cat(seq, dim):
return nd.concat(*seq, dim=dim)
def stack(seq, dim):
return nd.stack(*seq, axis=dim)
def split(x, sizes_or_sections, dim):
if isinstance(sizes_or_sections, list) and len(sizes_or_sections) == 1:
assert len(x) == sizes_or_sections[0]
return [x]
if isinstance(sizes_or_sections, (np.ndarray, list)):
sizes_or_sections1 = tuple(np.cumsum(sizes_or_sections)[:-1])
return nd.split_v2(x, sizes_or_sections1, axis=dim)
def repeat(input, repeats, dim):
if isinstance(repeats, nd.NDArray):
return nd.array(
np.repeat(input.asnumpy(), repeats.asnumpy(), axis=dim),
ctx=input.context,
dtype=input.dtype,
)
else:
return nd.repeat(input, repeats, axis=dim)
def gather_row(data, row_index):
# MXNet workaround for empty row index
if len(row_index) == 0:
if data.shape[0] == 0:
return data
else:
return data[0:0]
if isinstance(row_index, nd.NDArray):
return nd.take(data, row_index)
else:
return data[
row_index,
]
def slice_axis(data, axis, begin, end):
dim = data.shape[axis]
if begin < 0:
begin += dim
if end <= 0:
end += dim
return nd.slice_axis(data, axis, begin, end)
def take(data, indices, dim):
return nd.take(data, indices, dim)
def narrow_row(data, start, stop):
return data[start:stop]
def index_add_inplace(data, row_idx, value):
raise NotImplementedError("MXNet doesn't support inplace index_add")
def scatter_row(data, row_index, value):
return mx.nd.contrib.index_copy(data, row_index, value)
def scatter_row_inplace(data, row_index, value):
data[row_index] = value
def squeeze(input, dim):
return nd.squeeze(input, axis=dim)
def unsqueeze(input, dim):
return nd.expand_dims(input, axis=dim)
def reshape(input, shape):
# NOTE: the input cannot be a symbol
return nd.reshape(input, shape)
def swapaxes(input, axis1, axis2):
return nd.swapaxes(input, axis1, axis2)
def empty(shape, dtype, ctx):
return nd.empty(shape, dtype=dtype, ctx=ctx)
def zeros(shape, dtype, ctx):
return nd.zeros(shape, dtype=dtype, ctx=ctx)
def zeros_like(input):
return nd.zeros_like(input)
def ones(shape, dtype, ctx):
return nd.ones(shape, dtype=dtype, ctx=ctx)
def uniform(shape, dtype, ctx, low, high):
return nd.random.uniform(low, high, ctx=ctx, dtype=dtype, shape=shape)
def randint(shape, dtype, ctx, low, high):
return nd.random.randint(low, high, ctx=ctx, dtype=dtype, shape=shape)
def pad_packed_tensor(input, lengths, value, l_min=None):
old_shape = input.shape
if isinstance(lengths, nd.NDArray):
lengths = list(lengths.asnumpy())
max_len = builtins.max(lengths)
if l_min is not None:
max_len = builtins.max(max_len, l_min)
batch_size = len(lengths)
ctx = input.context
dtype = input.dtype
x = nd.full(
(batch_size * max_len, *old_shape[1:]), value, ctx=ctx, dtype=dtype
)
index = []
for i, l in enumerate(lengths):
index.extend(range(i * max_len, i * max_len + l))
index = nd.array(index, ctx=ctx)
return scatter_row(x, index, input).reshape(
batch_size, max_len, *old_shape[1:]
)
def pack_padded_tensor(input, lengths):
batch_size, max_len = input.shape[:2]
ctx = input.context
index = []
for i, l in enumerate(lengths):
index.extend(range(i * max_len, i * max_len + l))
index = nd.array(index, ctx=ctx)
return gather_row(input.reshape(batch_size * max_len, -1), index)
def boolean_mask(input, mask):
return mx.contrib.nd.boolean_mask(input, mask)
def equal(x, y):
return x == y
def allclose(x, y, rtol=1e-4, atol=1e-4):
return np.allclose(x.asnumpy(), y.asnumpy(), rtol=rtol, atol=atol)
def logical_not(input):
return nd.logical_not(input)
def logical_and(input1, input2):
return nd.logical_and(input1, input2)
def clone(input):
return input.copy()
def clamp(data, min_val, max_val):
return nd.clip(data, min_val, max_val)
def replace_inf_with_zero(x):
return nd.where(nd.abs(x) == np.inf, nd.zeros_like(x), x)
def count_nonzero(input):
# TODO: fallback to numpy is unfortunate
tmp = input.asnumpy()
return np.count_nonzero(tmp)
def unique(input, return_inverse=False, return_counts=False):
# TODO: fallback to numpy is unfortunate
tmp = input.asnumpy()
if return_inverse and return_counts:
tmp, inv, count = np.unique(
tmp, return_inverse=True, return_counts=True
)
tmp = nd.array(tmp, ctx=input.context, dtype=input.dtype)
inv = nd.array(inv, ctx=input.context)
count = nd.array(count, ctx=input.context)
return tmp, inv, count
elif return_inverse or return_counts:
tmp, tmp2 = np.unique(
tmp, return_inverse=return_inverse, return_counts=return_counts
)
tmp = nd.array(tmp, ctx=input.context, dtype=input.dtype)
tmp2 = nd.array(tmp2, ctx=input.context)
return tmp, tmp2
else:
tmp = np.unique(tmp)
return nd.array(tmp, ctx=input.context, dtype=input.dtype)
def full_1d(length, fill_value, dtype, ctx):
return nd.full((length,), fill_value, dtype=dtype, ctx=ctx)
def nonzero_1d(input):
# TODO: fallback to numpy is unfortunate
tmp = input.asnumpy()
tmp = np.nonzero(tmp)[0]
r = nd.array(tmp, ctx=input.context, dtype=tmp.dtype)
return r
def sort_1d(input):
# TODO: this isn't an ideal implementation.
val = nd.sort(input, axis=None, is_ascend=True)
idx = nd.argsort(input, is_ascend=True)
idx = nd.cast(idx, dtype="int64")
return val, idx
def arange(start, stop, dtype=np.int64, ctx=None):
if start >= stop:
return nd.array([], dtype=dtype, ctx=ctx)
else:
return nd.arange(start, stop, dtype=dtype, ctx=ctx)
def rand_shuffle(arr):
return mx.nd.random.shuffle(arr)
def zerocopy_to_dlpack(arr):
return arr.to_dlpack_for_read()
def zerocopy_from_dlpack(dlpack_arr):
return nd.from_dlpack(dlpack_arr)
def zerocopy_to_numpy(arr):
# NOTE: not zerocopy
return arr.asnumpy()
def zerocopy_from_numpy(np_data):
np_data = np.asarray(np_data, order="C")
return mx.nd.from_numpy(np_data, zero_copy=True)
def zerocopy_to_dgl_ndarray(arr):
arr.to_dlpack_for_read()
return dglnd.from_dlpack(arr.to_dlpack_for_read())
def zerocopy_to_dgl_ndarray_for_write(arr):
return dglnd.from_dlpack(arr.to_dlpack_for_write())
def zerocopy_from_dgl_ndarray(arr):
return nd.from_dlpack(arr.to_dlpack())
def sync():
"""Synchronize computation.
In DL frameworks such as MXNet and TensorFlow, the computation in operators
are done asynchronously. This is to synchronize computation and makes sure
that all computation is complete after this function call.
"""
mx.nd.waitall()
def attach_grad(tensor):
tensor.attach_grad()
return tensor
def backward(x, head_gradient=None):
x.backward(head_gradient)
def grad(x):
return x.grad
def is_no_grad(x):
return (x != 0).sum() == 0
def is_recording():
return mx.autograd.is_recording()
record_grad = mx.autograd.record
class no_grad(object):
def __init__(self):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, exc_traceback):
pass
+2
View File
@@ -0,0 +1,2 @@
from .sparse import *
from .tensor import *
File diff suppressed because it is too large Load Diff
+535
View File
@@ -0,0 +1,535 @@
from __future__ import absolute_import
import builtins
import numbers
import numpy as np
import scipy # Weird bug in new pytorch when import scipy after import torch
import torch as th
from torch.utils import dlpack
from ... import ndarray as nd
from ...function.base import TargetCode
from ...utils import version
if version.parse(th.__version__) < version.parse("2.1.0"):
raise RuntimeError("DGL requires PyTorch >= 2.1.0")
def data_type_dict():
return {
"bfloat16": th.bfloat16,
"float16": th.float16,
"float32": th.float32,
"float64": th.float64,
"uint8": th.uint8,
"int8": th.int8,
"int16": th.int16,
"int32": th.int32,
"int64": th.int64,
"bool": th.bool,
}
def cpu():
return th.device("cpu")
def tensor(data, dtype=None):
if isinstance(data, numbers.Number):
data = [data]
if (
isinstance(data, list)
and len(data) > 0
and isinstance(data[0], th.Tensor)
):
# prevent GPU->CPU->GPU copies
if data[0].ndim == 0:
# zero dimenion scalar tensors
return th.stack(data)
if isinstance(data, th.Tensor):
return th.as_tensor(data, dtype=dtype, device=data.device)
else:
return th.as_tensor(data, dtype=dtype)
def as_scalar(data):
return data.item()
def get_preferred_sparse_format():
"""Get the preferred sparse matrix format supported by the backend.
Different backends have their preferred backend. This info is useful when
constructing a sparse matrix.
"""
return "coo"
def sparse_matrix(data, index, shape, force_format=False):
fmt = index[0]
if fmt != "coo":
raise TypeError(
"Pytorch backend only supports COO format. But got %s." % fmt
)
spmat = th.sparse_coo_tensor(index[1], data, shape)
return spmat, None
def sparse_matrix_indices(spmat):
return ("coo", spmat._indices())
def is_tensor(obj):
return isinstance(obj, th.Tensor)
def shape(input):
return input.shape
def dtype(input):
return input.dtype
def ndim(input):
return input.dim()
def context(input):
return input.device
def device_type(ctx):
return th.device(ctx).type
def device_id(ctx):
ctx = th.device(ctx)
if ctx.index is None:
return 0 if ctx.type == "cpu" else th.cuda.current_device()
else:
return ctx.index
def to_backend_ctx(dglctx):
dev_type = dglctx.device_type
if dev_type == 1:
return th.device("cpu")
elif dev_type == 2:
return th.device("cuda", dglctx.device_id)
else:
raise ValueError("Unsupported DGL device context:", dglctx)
def astype(input, ty):
return input.type(ty)
def asnumpy(input):
if isinstance(input, th.sparse.FloatTensor):
return input.to_dense().cpu().detach().numpy()
else:
return input.cpu().detach().numpy()
def copy_to(input, ctx, **kwargs):
ctx = th.device(ctx)
if ctx.type == "cpu":
return input.cpu()
elif ctx.type == "cuda":
if ctx.index is not None:
th.cuda.set_device(ctx.index)
return input.cuda(**kwargs)
else:
raise RuntimeError("Invalid context", ctx)
def is_pinned(input):
return input.is_pinned()
def sum(input, dim, keepdims=False):
return th.sum(input, dim=dim, keepdim=keepdims)
def floor_div(in1, in2):
return in1 // in2
def reduce_sum(input):
return input.sum()
def cumsum(input, dim):
return th.cumsum(input, dim=dim)
def mean(input, dim):
return th.mean(input, dim=dim)
def reduce_mean(input):
return input.mean()
def max(input, dim):
# NOTE: the second argmax array is not returned
return th.max(input, dim=dim)[0]
def reduce_max(input):
return input.max()
def min(input, dim):
# NOTE: the second argmin array is not returned
return th.min(input, dim=dim)[0]
def reduce_min(input):
return input.min()
def argsort(input, dim, descending):
return th.argsort(input, dim=dim, descending=descending)
def topk(input, k, dim, descending=True):
return th.topk(input, k, dim, largest=descending)[0]
def argtopk(input, k, dim, descending=True):
return th.topk(input, k, dim, largest=descending)[1]
def exp(input):
return th.exp(input)
def inverse(input):
return th.inverse(input)
def sqrt(input):
return th.sqrt(input)
def softmax(input, dim=-1):
return th.softmax(input, dim=dim)
def cat(seq, dim):
return th.cat(seq, dim=dim)
def stack(seq, dim):
return th.stack(seq, dim=dim)
def split(input, sizes_or_sections, dim):
return th.split(input, sizes_or_sections, dim)
def repeat(input, repeats, dim):
return th.repeat_interleave(input, repeats, dim) # PyTorch 1.1
def gather_row(data, row_index):
return th.index_select(data, 0, row_index.long())
def slice_axis(data, axis, begin, end):
return th.narrow(data, axis, begin, end - begin)
def take(data, indices, dim):
new_shape = data.shape[:dim] + indices.shape + data.shape[dim + 1 :]
return th.index_select(data, dim, indices.view(-1)).view(new_shape)
def narrow_row(x, start, stop):
return x[start:stop]
def index_add_inplace(data, row_idx, value):
data.index_add_(0, row_idx, value)
def scatter_row(data, row_index, value):
return data.index_copy(0, row_index.long(), value)
def scatter_row_inplace(data, row_index, value):
data[row_index.long()] = value
def squeeze(input, dim):
return th.squeeze(input, dim)
def unsqueeze(input, dim):
return th.unsqueeze(input, dim)
def reshape(input, shape):
return th.reshape(input, shape)
def swapaxes(input, axis1, axis2):
return th.transpose(input, axis1, axis2)
def empty(shape, dtype, ctx):
return th.empty(shape, dtype=dtype, device=ctx)
def zeros(shape, dtype, ctx):
return th.zeros(shape, dtype=dtype, device=ctx)
def zeros_like(input):
return th.zeros_like(input)
def ones(shape, dtype, ctx):
return th.ones(shape, dtype=dtype, device=ctx)
def uniform(shape, dtype, ctx, low, high):
return th.empty(shape, dtype=dtype, device=ctx).uniform_(low, high)
def randint(shape, dtype, ctx, low, high):
return th.randint(low, high, shape, dtype=dtype, device=ctx)
def pad_packed_tensor(input, lengths, value, l_min=None):
old_shape = input.shape
device = input.device
if not is_tensor(lengths):
lengths = th.tensor(lengths, dtype=th.int64, device=device)
else:
lengths = lengths.to(device)
max_len = as_scalar(lengths.max())
if l_min is not None:
max_len = builtins.max(max_len, l_min)
batch_size = len(lengths)
x = input.new(batch_size * max_len, *old_shape[1:])
x.fill_(value)
index = th.ones(len(input), dtype=th.int64, device=device)
cum_lengths = th.cumsum(lengths, 0)
index[cum_lengths[:-1]] += max_len - lengths[:-1]
index = th.cumsum(index, 0) - 1
x[index] = input
return x.view(batch_size, max_len, *old_shape[1:])
def pack_padded_tensor(input, lengths):
max_len = input.shape[1]
device = input.device
if not is_tensor(lengths):
lengths = th.tensor(lengths, dtype=th.int64, device=device)
else:
lengths = lengths.to(device)
input = input.view(-1, *input.shape[2:])
out_len = lengths.sum().item()
index = th.ones(out_len, dtype=th.int64, device=device)
cum_lengths = th.cumsum(lengths, 0)
index[cum_lengths[:-1]] += max_len - lengths[:-1]
index = th.cumsum(index, 0) - 1
return input[index]
def boolean_mask(input, mask):
if "bool" not in str(mask.dtype):
mask = th.as_tensor(mask, dtype=th.bool)
return input[mask]
def equal(x, y):
return x == y
def allclose(x, y, rtol=1e-4, atol=1e-4):
return th.allclose(x, y, rtol=rtol, atol=atol)
def logical_not(input):
return ~input
def logical_and(input1, input2):
return input1 & input2
def clone(input):
return input.clone()
def clamp(data, min_val, max_val):
return th.clamp(data, min_val, max_val)
def replace_inf_with_zero(x):
return th.masked_fill(x, th.isinf(x), 0)
def count_nonzero(input):
# TODO: fallback to numpy for backward compatibility
return np.count_nonzero(input)
def unique(input, return_inverse=False, return_counts=False):
if input.dtype == th.bool:
input = input.type(th.int8)
return th.unique(
input, return_inverse=return_inverse, return_counts=return_counts
)
def full_1d(length, fill_value, dtype, ctx):
return th.full((length,), fill_value, dtype=dtype, device=ctx)
def nonzero_1d(input):
x = th.nonzero(input, as_tuple=False).squeeze()
return x if x.dim() == 1 else x.view(-1)
def sort_1d(input):
return th.sort(input)
def arange(start, stop, dtype=th.int64, ctx=None):
return th.arange(start, stop, dtype=dtype, device=ctx)
def rand_shuffle(arr):
idx = th.randperm(len(arr))
return arr[idx]
def zerocopy_to_dlpack(input):
return dlpack.to_dlpack(input.contiguous())
def zerocopy_from_dlpack(dlpack_tensor):
return dlpack.from_dlpack(dlpack_tensor)
def zerocopy_to_numpy(input):
# NOTE: not zerocopy
return asnumpy(input)
def zerocopy_from_numpy(np_array):
return th.as_tensor(np_array)
def zerocopy_to_dgl_ndarray(data):
if data.dtype == th.bool:
data = data.byte()
return nd.from_dlpack(dlpack.to_dlpack(data.contiguous()))
# NGC PyTorch containers are shipping alpha version PyTorch.
if version.parse(th.__version__) >= version.parse("2.0.0a0"):
def check_is_view(input):
assert (
input.data_ptr() == input.untyped_storage().data_ptr()
), "Cannot convert view tensors to dgl ndarray for write."
else:
def check_is_view(input):
assert (
input.data_ptr() == input._storage().data_ptr()
), "Cannot convert view tensors to dgl ndarray for write."
def zerocopy_to_dgl_ndarray_for_write(input):
if input.numel() > 0:
# only check non-empty tensors
assert input.is_contiguous(), (
"Cannot convert non-contiguous tensors "
"to dgl ndarray for write. Call .to_contiguous() first."
)
check_is_view(input)
return zerocopy_to_dgl_ndarray(input)
def zerocopy_from_dgl_ndarray(data):
if data.shape == (0,):
# NOTE: PyTorch v1.5 does not accept DLPack object representing empty CUDA tensor.
# Related issue: https://github.com/pytorch/pytorch/issues/41182
# The issue will be fixed in v1.6 and later.
return th.tensor(
[], dtype=getattr(th, data.dtype), device=to_backend_ctx(data.ctx)
)
elif len(data.shape) == 0 or builtins.min(data.shape) == 0:
# Workaround the same issue as above, but preserve the shape of the
# empty tensor. This is needed by the sparse optimizer when one of
# processors may receive no gradients to update, but we want to keep
# the dimension of the embedding.
return th.empty(
data.shape,
dtype=getattr(th, data.dtype),
device=to_backend_ctx(data.ctx),
)
else:
return dlpack.from_dlpack(data.to_dlpack())
def sync():
# Pytorch performs computation synchronously, so no need for synchronization.
pass
def attach_grad(x):
if x.grad is not None:
x.grad.zero_()
return x
else:
return x.requires_grad_()
def backward(x, head_gradient=None):
if (
head_gradient is not None
and head_gradient.shape[0] == 1
and len(head_gradient.shape) == 1
):
# Fix for torch 1.3.1
head_gradient = th.tensor(head_gradient.item()).to(head_gradient.device)
x.backward(head_gradient)
def grad(x):
x.retain_grad()
return x.grad
def is_no_grad(x):
return x.grad is None or (x.grad == 0).all()
def is_recording():
return th.is_grad_enabled()
class record_grad(object):
def __init__(self):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, exc_traceback):
pass
no_grad = th.no_grad
+35
View File
@@ -0,0 +1,35 @@
import argparse
import json
import os
def set_default_backend(default_dir, backend_name):
os.makedirs(default_dir, exist_ok=True)
config_path = os.path.join(default_dir, "config.json")
with open(config_path, "w") as config_file:
json.dump({"backend": backend_name.lower()}, config_file)
print(
'Setting the default backend to "{}". You can change it in the '
"~/.dgl/config.json file or export the DGLBACKEND environment variable. "
"Valid options are: pytorch, mxnet, tensorflow (all lowercase)".format(
backend_name
)
)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"default_dir",
type=str,
default=os.path.join(os.path.expanduser("~"), ".dgl"),
)
parser.add_argument(
"backend",
nargs=1,
type=str,
choices=["pytorch", "tensorflow", "mxnet"],
help="Set default backend",
)
args = parser.parse_args()
set_default_backend(args.default_dir, args.backend[0])
@@ -0,0 +1,6 @@
import os
os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true"
from .sparse import *
from .tensor import *
+461
View File
@@ -0,0 +1,461 @@
import numpy as np
import tensorflow as tf
from ..._sparse_ops import (
_bwd_segment_cmp,
_csrmask,
_csrmm,
_csrsum,
_gsddmm,
_gspmm,
_scatter_add,
_segment_reduce,
)
from ...base import ALL, is_all
from ...heterograph_index import create_unitgraph_from_csr
from .tensor import asnumpy, context, copy_to, tensor, zerocopy_from_numpy
__all__ = [
"gspmm",
"gsddmm",
"edge_softmax",
"segment_reduce",
"scatter_add",
"csrmm",
"csrsum",
"csrmask",
]
def _scatter_nd(index, src, n_rows):
assert index.shape == src.shape
shp = index.shape
ctx = context(src)
ndim = index.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = tf.range(di, dtype=index.dtype)
offsets.append(
tf.reshape(
(stride * offset_i), (1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + copy_to(sum(offsets), ctx)
else:
new_idx = index
src = tf.reshape(src, (-1,))
new_idx = tf.reshape(new_idx, (-1, 1))
rst = tf.reshape(
tf.scatter_nd(new_idx, src, (stride * n_rows,)), (n_rows, *shp[1:])
)
return rst
def _gather_nd(index, src):
shp = index.shape
ctx = context(src)
ndim = index.ndim
offsets = []
stride = 1
for i in reversed(range(1, ndim)):
di = shp[i]
offset_i = tf.range(di, dtype=index.dtype)
offsets.append(
tf.reshape(
(stride * offset_i), (1,) * i + (di,) + (1,) * (ndim - 1 - i)
)
)
stride *= di
if ndim > 1:
new_idx = index * stride + copy_to(sum(offsets), ctx)
else:
new_idx = index
src = tf.reshape(src, (-1,))
new_idx = tf.reshape(new_idx, (-1))
rst = tf.reshape(tf.gather(src, new_idx), shp)
return rst
def _reduce_grad(grad, shape):
"""Reduce gradient on the broadcast dimension
If there is broadcast in forward pass, gradients need to be reduced on
broadcast dimension. This function checks the input tensor shape and
gradient shape and perform the reduction.
Parameters
----------
grad: Tensor
Gradient tensor
shape: tuple
Shape of input tensor
Returns
-------
Tensor
"""
grad_shape = grad.shape[1:]
in_shape = shape[1:]
if in_shape == grad_shape:
# no need to reduce
return grad
num_to_squeeze = len(grad_shape) - len(in_shape)
# pad inshape
in_shape = (1,) * num_to_squeeze + in_shape
reduce_idx = np.asarray(
np.nonzero(np.asarray(grad_shape) - np.asarray(in_shape))
)
reduce_idx += 1 # skip batch dim
reduce_idx_tensor = tf.constant(
tuple(reduce_idx.flatten().tolist()), dtype=tf.int32
)
grad = tf.reduce_sum(grad, axis=reduce_idx_tensor, keepdims=True)
return tf.reshape(grad, shape)
def _need_reduce_last_dim(ufeat, efeat):
"""Indicates whether to reduce the last dimension on edges
in the backward pass of spmm,
if so, use dot instead of mul."""
ushp = ufeat.shape
eshp = efeat.shape
return ushp[1:-1] == eshp[1:-1] and eshp[-1] == 1 and ushp[-1] > 1
def _muldiv(op, x):
return 1.0 / x if op == "div" else x
def _addsub(op, x):
return -x if op == "sub" else x
def _expand(x, shape):
return tf.broadcast_to(x, (x.shape[0], *shape))
def gspmm_real(gidx, op, reduce_op, X, Y):
out, (argX, argY) = _gspmm(gidx, op, reduce_op, X, Y)
def grad(dZ):
dZ = tensor(dZ)
if op != "copy_rhs":
g_rev = gidx.reverse()
if reduce_op == "sum":
if op in ["mul", "div"]:
dX = _gspmm(g_rev, "mul", "sum", dZ, _muldiv(op, Y))[0]
elif op in ["add", "sub"]:
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, Y)[0]
elif op == "copy_lhs":
dX = _gspmm(g_rev, "copy_lhs", "sum", dZ, None)[0]
else:
if op in ["mul", "div"]:
dX = _scatter_nd(
argX,
_muldiv(op, _gather_nd(argY, _expand(Y, dZ.shape[1:])))
* dZ,
X.shape[0],
)
elif op in ["add", "sub", "copy_lhs"]:
dX = _scatter_nd(argX, dZ, X.shape[0])
dX = _reduce_grad(dX, X.shape)
else:
dX = tf.zeros_like(X)
if op != "copy_lhs":
if reduce_op == "sum":
if op == "mul" and _need_reduce_last_dim(X, Y):
dY = _gsddmm(gidx, "dot", X, dZ)
elif op in ["mul", "div"]:
dY = _gsddmm(gidx, "mul", X, dZ)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _gsddmm(gidx, "copy_rhs", X, _addsub(op, dZ))
else:
out_shp = (Y.shape[0],) + dZ.shape[1:]
if op in ["mul", "div"]:
dY = _scatter_nd(
argY,
_gather_nd(argX, _expand(X, dZ.shape[1:])) * dZ,
Y.shape[0],
)
if op == "div":
dY = -dY / (Y**2)
elif op in ["add", "sub", "copy_rhs"]:
dY = _scatter_nd(argY, _addsub(op, dZ), Y.shape[0])
dY = _reduce_grad(dY, Y.shape)
else:
dY = tf.zeros_like(Y)
return dX, dY
return out, grad
def gspmm(gidx, op, reduce_op, X, Y):
@tf.custom_gradient
def _lambda(X, Y):
return gspmm_real(gidx, op, reduce_op, X, Y)
if X is None:
X = tf.zeros(())
if Y is None:
Y = tf.zeros(())
return _lambda(X, Y)
def gsddmm_real(gidx, op, X, Y, lhs_target, rhs_target):
out = _gsddmm(gidx, op, X, Y, lhs_target, rhs_target)
def grad(dZ):
if op != "copy_rhs":
if lhs_target in ["u", "v"]:
_gidx = gidx if lhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_lhs"]:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0]
else: # mul, div, dot
if rhs_target == lhs_target:
dX = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[
0
] * _muldiv(op, Y)
elif rhs_target == "e":
dX = _gspmm(
_gidx, "copy_rhs", "sum", None, dZ * _muldiv(op, Y)
)[0]
else: # rhs_target = !lhs_target
dX = _gspmm(_gidx, "mul", "sum", _muldiv(op, Y), dZ)[0]
else: # lhs_target == 'e'
if op in ["add", "sub", "copy_lhs"]:
dX = dZ
else: # mul, div, dot
dX = _gsddmm(
gidx, "mul", dZ, _muldiv(op, Y), "e", rhs_target
)
dX = _reduce_grad(dX, X.shape)
else:
dX = tf.zeros_like(X)
if op != "copy_lhs":
if rhs_target in ["u", "v"]:
_gidx = gidx if rhs_target == "v" else gidx.reverse()
if op in ["add", "sub", "copy_rhs"]:
dY = _gspmm(
_gidx, "copy_rhs", "sum", None, _addsub(op, dZ)
)[0]
else: # mul, div, dot
if lhs_target == rhs_target:
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ)[0] * X
elif lhs_target == "e":
dY = _gspmm(_gidx, "copy_rhs", "sum", None, dZ * X)[0]
else: # rhs_target = !lhs_target
dY = _gspmm(_gidx, "mul", "sum", X, dZ)[0]
if op == "div":
dY = -dY / (Y**2)
else:
if op in ["add", "sub", "copy_rhs"]:
dY = _addsub(op, dZ)
else: # mul, div, dot
dY = _gsddmm(gidx, "mul", dZ, X, "e", lhs_target)
if op == "div":
dY = -dY / (Y**2)
dY = _reduce_grad(dY, Y.shape)
else:
dY = tf.zeros_like(Y)
return dX, dY
return out, grad
def gsddmm(gidx, op, X, Y, lhs_target="u", rhs_target="v"):
@tf.custom_gradient
def _lambda(X, Y):
return gsddmm_real(gidx, op, X, Y, lhs_target, rhs_target)
if X is None:
X = tf.zeros(())
if Y is None:
Y = tf.zeros(())
return _lambda(X, Y)
def edge_softmax_real(gidx, score, eids=ALL, norm_by="dst"):
if not is_all(eids):
gidx = gidx.edge_subgraph([eids], True).graph
if norm_by == "src":
gidx = gidx.reverse()
score_max = _gspmm(gidx, "copy_rhs", "max", None, score)[0]
score = tf.math.exp(_gsddmm(gidx, "sub", score, score_max, "e", "v"))
score_sum = _gspmm(gidx, "copy_rhs", "sum", None, score)[0]
out = _gsddmm(gidx, "div", score, score_sum, "e", "v")
def edge_softmax_backward(grad_out):
sds = out * grad_out
accum = gspmm(gidx, "copy_rhs", "sum", None, sds)
grad_score = sds - gsddmm(gidx, "mul", out, accum, "e", "v")
return grad_score
return out, edge_softmax_backward
def edge_softmax(gidx, logits, eids=ALL, norm_by="dst"):
@tf.custom_gradient
def _lambda(logits):
return edge_softmax_real(gidx, logits, eids, norm_by)
return _lambda(logits)
def segment_reduce_real(op, x, offsets):
y, arg = _segment_reduce(op, x, offsets)
def segment_reduce_backward(dy):
m = x.shape[0]
if op == "sum":
offsets_np = asnumpy(offsets[1:])
indices_np = np.zeros((m + 1,), dtype=offsets_np.dtype)
np.add.at(indices_np, offsets_np, np.ones_like(offsets_np))
indices_np = np.cumsum(indices_np, -1)[:-1]
indices = zerocopy_from_numpy(indices_np)
dx = tf.gather(dy, indices)
else:
dx = _bwd_segment_cmp(dy, arg, m)
return dx
return y, segment_reduce_backward
def segment_reduce(op, x, offsets):
@tf.custom_gradient
def _lambda(x):
return segment_reduce_real(op, x, offsets)
return _lambda(x)
def scatter_add_real(x, idx, m):
y = _scatter_add(x, idx, m)
def scatter_add_backward(dy):
return tf.gather(dy, idx)
return y, scatter_add_backward
def scatter_add(x, idx, m):
@tf.custom_gradient
def _lambda(x):
return scatter_add_real(x, idx, m)
return _lambda(x)
def csrmm_real(gidxA, A_weights, gidxB, B_weights, num_vtypes):
gidxC, C_weights = _csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes)
nrows, ncols, C_indptr, C_indices, C_eids = gidxC.adjacency_matrix_tensors(
0, False, "csr"
)
def grad(dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights):
# Only the last argument is meaningful.
dgidxA, dA_weights = _csrmm(
gidxC,
dC_weights,
gidxB.reverse(),
B_weights,
gidxA.number_of_ntypes(),
)
dgidxB, dB_weights = _csrmm(
gidxA.reverse(),
A_weights,
gidxC,
dC_weights,
gidxB.number_of_ntypes(),
)
dA_weights = _csrmask(dgidxA, dA_weights, gidxA)
dB_weights = _csrmask(dgidxB, dB_weights, gidxB)
return dA_weights, dB_weights
return (
tf.constant(nrows),
tf.constant(ncols),
C_indptr,
C_indices,
C_eids,
C_weights,
), grad
def csrmm(gidxA, A_weights, gidxB, B_weights, num_vtypes):
@tf.custom_gradient
def _lambda(A_weights, B_weights):
return csrmm_real(gidxA, A_weights, gidxB, B_weights, num_vtypes)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = _lambda(
A_weights, B_weights
)
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.numpy(),
ncols.numpy(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
def csrsum_real(gidxs, weights):
gidxC, C_weights = _csrsum(gidxs, weights)
nrows, ncols, C_indptr, C_indices, C_eids = gidxC.adjacency_matrix_tensors(
0, False, "csr"
)
def grad(dnrows, dncols, dC_indptr, dC_indices, dC_eids, dC_weights):
# Only the last argument is meaningful.
return tuple(_csrmask(gidxC, dC_weights, gidx) for gidx in gidxs)
return (
tf.constant(nrows),
tf.constant(ncols),
C_indptr,
C_indices,
C_eids,
C_weights,
), grad
def csrsum(gidxs, weights):
@tf.custom_gradient
def _lambda(*weights):
return csrsum_real(gidxs, weights)
nrows, ncols, C_indptr, C_indices, C_eids, C_weights = _lambda(*weights)
num_vtypes = gidxs[0].number_of_ntypes()
gidxC = create_unitgraph_from_csr(
num_vtypes,
nrows.numpy(),
ncols.numpy(),
C_indptr,
C_indices,
C_eids,
["coo", "csr", "csc"],
)
return gidxC, C_weights
def csrmask_real(gidxA, A_weights, gidxB):
B_weights = _csrmask(gidxA, A_weights, gidxB)
def grad(dB_weights):
return _csrmask(gidxB, dB_weights, gidxA)
return B_weights, grad
def csrmask(gidxA, A_weights, gidxB):
@tf.custom_gradient
def _lambda(A_weights):
return csrmask_real(gidxA, A_weights, gidxB)
return _lambda(A_weights)
@@ -0,0 +1 @@
"""Sparse optimizer is not supported for tensorflow"""
+619
View File
@@ -0,0 +1,619 @@
"""Tensorflow backend implementation"""
from __future__ import absolute_import
import builtins
import numbers
import numpy as np
import tensorflow as tf
from ... import ndarray as nd
from ...function.base import TargetCode
from ...utils import version
if version.parse(tf.__version__) < version.parse("2.3.0"):
raise RuntimeError(
"DGL requires TensorFlow>=2.3.0 for the official DLPack support."
)
def zerocopy_to_dlpack(data):
return tf.experimental.dlpack.to_dlpack(data)
def zerocopy_from_dlpack(dlpack_tensor):
# TODO(Jinjing): Tensorflow requires memory to be 64-bytes aligned. We check the
# alignment and make a copy if needed. The functionality is better in TF's main repo.
aligned = nd.from_dlpack(dlpack_tensor).to_dlpack(64)
return tf.experimental.dlpack.from_dlpack(aligned)
def data_type_dict():
return {
"bfloat16": tf.bfloat16,
"float16": tf.float16,
"float32": tf.float32,
"float64": tf.float64,
"uint8": tf.uint8,
"int8": tf.int8,
"int16": tf.int16,
"int32": tf.int32,
"int64": tf.int64,
"bool": tf.bool,
}
def cpu():
return "/cpu:0"
def tensor(data, dtype=None):
if isinstance(data, tf.Tensor):
if dtype is None or data.dtype == dtype:
return data
else:
return tf.cast(data, dtype=dtype)
else:
if isinstance(data, numbers.Number):
data = [data]
return tf.convert_to_tensor(data, dtype=dtype)
def initialize_context():
tf.zeros(1)
def as_scalar(data):
data = data.numpy()
return data if np.isscalar(data) else data.item()
def get_preferred_sparse_format():
"""Get the preferred sparse matrix format supported by the backend.
Different backends have their preferred backend. This info is useful when
constructing a sparse matrix.
"""
return "coo"
def sparse_matrix(data, index, shape, force_format=False):
fmt = index[0]
if fmt != "coo":
raise TypeError(
"Tensorflow backend only supports COO format. But got %s." % fmt
)
# tf.SparseTensor only supports int64 indexing,
# therefore manually casting to int64 when input in int32
spmat = tf.SparseTensor(
indices=tf.cast(tf.transpose(index[1], (1, 0)), tf.int64),
values=data,
dense_shape=shape,
)
return spmat, None
def sparse_matrix_indices(spmat):
return ("coo", spmat.indices)
def is_tensor(obj):
return isinstance(obj, tf.Tensor)
def shape(input):
return input.shape
def dtype(input):
return input.dtype
def ndim(input):
return input.ndim
def context(input):
spec = tf.DeviceSpec.from_string(input.device)
return "/{}:{}".format(spec.device_type.lower(), spec.device_index)
def device_type(ctx):
return tf.DeviceSpec.from_string(ctx).device_type.lower()
def device_id(ctx):
return tf.DeviceSpec.from_string(ctx).device_index
def to_backend_ctx(dglctx):
dev_type = dglctx.device_type
if dev_type == 1:
return "/cpu:0"
elif dev_type == 2:
return "/gpu:%d" % (dglctx.device_id)
else:
raise ValueError("Unsupported DGL device context:", dglctx)
def astype(input, ty):
with tf.device(input.device):
return tf.cast(input, dtype=ty)
def asnumpy(input):
if isinstance(input, tf.SparseTensor):
# tf.sparse.to_dense assume sorted indices, need to turn off validate_indices in our cases
return tf.sparse.to_dense(input, validate_indices=False).numpy()
else:
return input.numpy()
def copy_to(input, ctx, **kwargs):
with tf.device(ctx):
new_tensor = tf.identity(input)
return new_tensor
def is_pinned(input):
return False # not sure how to do this
def sum(input, dim, keepdims=False):
if input.dtype == tf.bool:
input = tf.cast(input, tf.int32)
return tf.reduce_sum(input, axis=dim, keepdims=keepdims)
def floor_div(in1, in2):
return astype(in1 / in2, dtype(in1))
def reduce_sum(input):
if input.dtype == tf.bool:
input = tf.cast(input, tf.int32)
return tf.reduce_sum(input)
def cumsum(input, dim):
if input.dtype == tf.bool:
input = tf.cast(input, tf.int32)
return tf.cumsum(input, axis=dim)
def mean(input, dim):
return tf.reduce_mean(input, axis=dim)
def reduce_mean(input):
return tf.reduce_mean(input)
def max(input, dim):
return tf.reduce_max(input, axis=dim)
def reduce_max(input):
return tf.reduce_max(input)
def min(input, dim):
return tf.reduce_min(input, axis=dim)
def reduce_min(input):
return tf.reduce_min(input)
def argsort(input, dim, descending):
if descending:
return tf.cast(
tf.argsort(input, axis=dim, direction="DESCENDING"), dtype=tf.int64
)
else:
return tf.cast(
tf.argsort(input, axis=dim, direction="ASCENDING"), dtype=tf.int64
)
def topk(input, k, dim, descending=True):
if not descending:
input = -input
shape = np.arange(input.ndim)
shape[dim], shape[-1] = shape[-1], shape[dim]
out1 = tf.transpose(input, perm=shape)
out2 = tf.math.top_k(out1, k=k, sorted=True)
out = tf.transpose(out2[0], shape)
if not descending:
out = -out
return out
def argtopk(input, k, dim, descending=True):
if not descending:
input = -input
shape = np.arange(input.ndim)
shape[dim], shape[-1] = shape[-1], shape[dim]
out1 = tf.transpose(input, perm=shape)
out2 = tf.math.top_k(out1, k=k, sorted=True)
out = tf.transpose(out2[1], shape)
if not descending:
out = -out
return out
def exp(input):
return tf.exp(input)
def inverse(input):
return tf.linalg.inv(input)
def sqrt(input):
return tf.sqrt(input)
def softmax(input, dim=-1):
return tf.math.softmax(input, axis=dim)
def cat(seq, dim):
return tf.concat(seq, axis=dim)
def stack(seq, dim):
return tf.stack(seq, axis=dim)
def split(input, sizes_or_sections, dim):
return [
copy_to(_, input.device)
for _ in tf.split(input, sizes_or_sections, axis=dim)
]
def repeat(input, repeats, dim):
return tf.repeat(input, repeats, dim)
def gather_row(data, row_index):
return tf.gather(data, row_index)
def slice_axis(data, axis, begin, end):
# assert axis == 0
# tf doesn't behave well with negative
s = [slice(None) for i in range(data.ndim)]
if end == 0:
end = data.shape[axis]
s[axis] = slice(begin, end, None)
return data[tuple(s)]
def take(data, indices, dim):
return tf.gather_nd(data, indices, dim)
def narrow_row(x, start, stop):
return x[start:stop]
def scatter_row(data, row_index, value):
row_index = tf.expand_dims(row_index, 1)
# XXX(minjie): Normally, the copy_to here is unnecessary. However, TF has this
# notorious legacy issue that int32 type data is always on CPU, which will
# crash the program since DGL requires feature data to be on the same device
# as graph structure.
return copy_to(
tf.tensor_scatter_nd_update(data, row_index, value), data.device
)
def index_add_inplace(data, row_idx, value):
raise NotImplementedError("Tensorflow doesn't support inplace index_add")
def scatter_row_inplace(data, row_index, value):
raise NotImplementedError("Tensorflow doesn't support inplace update")
def squeeze(input, dim):
return tf.squeeze(input, axis=dim)
def unsqueeze(input, dim):
return tf.expand_dims(input, axis=dim)
def reshape(input, shape):
return tf.reshape(input, shape)
def swapaxes(input, axis1, axis2):
ndim = input.ndim
t = list(range(ndim))
t[axis1], t[axis2] = axis2 % ndim, axis1 % ndim
return tf.transpose(input, perm=t)
def empty(shape, dtype, ctx):
# tf doesn't have tf.empty(), use zeros() as a workaround
return zeros(shape, dtype, ctx)
def zeros(shape, dtype, ctx):
with tf.device(ctx):
t = tf.zeros(shape, dtype=dtype)
return t
def zeros_like(input):
return tf.zeros_like(input)
def ones(shape, dtype, ctx):
with tf.device(ctx):
t = tf.ones(shape, dtype=dtype)
return t
def uniform(shape, dtype, ctx, low, high):
with tf.device(ctx):
t = tf.random.uniform(shape, dtype=dtype, minval=low, maxval=high)
return t
def randint(shape, dtype, ctx, low, high):
with tf.device(ctx):
t = tf.random.uniform(shape, dtype=dtype, minval=low, maxval=high)
return t
def pad_packed_tensor(input, lengths, value, l_min=None):
old_shape = input.shape
if isinstance(lengths, tf.Tensor):
max_len = as_scalar(tf.reduce_max(lengths))
else:
max_len = builtins.max(lengths)
if l_min is not None:
max_len = builtins.max(max_len, l_min)
batch_size = len(lengths)
ndim = input.ndim
tensor_list = []
cum_row = 0
pad_nparray = np.zeros((ndim, 2), dtype=np.int32)
for l in lengths:
t = input[cum_row : cum_row + l]
pad_nparray[0, 1] = max_len - l
t = tf.pad(
t, tf.constant(pad_nparray), mode="CONSTANT", constant_values=value
)
tensor_list.append(t)
cum_row += l
return tf.stack(tensor_list, axis=0)
def pack_padded_tensor(input, lengths):
out_list = []
for i, l in enumerate(lengths):
t = input[i]
out = t[:l]
out_list.append(out)
return tf.concat(out_list, axis=0)
def boolean_mask(input, mask):
return tf.boolean_mask(input, mask)
def equal(x, y):
return x == y
def allclose(x, y, rtol=1e-4, atol=1e-4):
return np.allclose(
tf.convert_to_tensor(x).numpy(),
tf.convert_to_tensor(y).numpy(),
rtol=rtol,
atol=atol,
)
def logical_not(input):
return ~input
def logical_and(input1, input2):
return tf.math.logical_and(input1, input2)
def clone(input):
# TF tensor is always immutable so returning the input is safe.
return input
def clamp(data, min_val, max_val):
return tf.clip_by_value(data, min_val, max_val)
def replace_inf_with_zero(x):
return tf.where(tf.abs(x) == np.inf, 0, x)
def count_nonzero(input):
return int(tf.math.count_nonzero(input))
def unique(input, return_inverse=False, return_counts=False):
if return_inverse and return_counts:
return tf.unique_with_counts(input)
elif return_counts:
result = tf.unique_with_counts(input)
return result.y, result.count
elif return_inverse:
return tf.unique(input)
else:
return tf.unique(input).y
def full_1d(length, fill_value, dtype, ctx):
with tf.device(ctx):
t = tf.fill([length], value=fill_value)
t = tf.cast(t, dtype=dtype)
return t
def nonzero_1d(input):
nonzero_bool = tf.cast(input, tf.bool)
return tf.reshape(tf.where(nonzero_bool), (-1,))
def sort_1d(input):
return tf.sort(input), tf.cast(tf.argsort(input), dtype=tf.int64)
def arange(start, stop, dtype=tf.int64, ctx=None):
if not ctx:
ctx = "/cpu:0"
with tf.device(ctx):
t = tf.range(start, stop, dtype=dtype)
return t
def rand_shuffle(arr):
return tf.random.shuffle(arr)
def zerocopy_to_numpy(input):
return np.asarray(memoryview(input))
def zerocopy_from_numpy(np_array):
# NOTE: not zerocopy
# This assumes tensor should be on cpu
with tf.device("/cpu:0"):
t = tf.convert_to_tensor(np_array)
return t
def zerocopy_to_dgl_ndarray(data):
if device_type(data.device) == "gpu" and data.dtype in (tf.int32, tf.int64):
# NOTE: TF doesn't keep signed tensors on GPU due to legacy issues with
# shape inference. Convert it to unsigned and cast it back afterwards.
if data.dtype == tf.int32:
data = tf.cast(data, tf.uint32)
elif data.dtype == tf.int64:
data = tf.cast(data, tf.uint64)
return nd.cast_to_signed(nd.from_dlpack(zerocopy_to_dlpack(data)))
else:
return nd.from_dlpack(zerocopy_to_dlpack(data))
def zerocopy_to_dgl_ndarray_for_write(input):
return zerocopy_to_dgl_ndarray(input)
def zerocopy_from_dgl_ndarray(input):
return zerocopy_from_dlpack(input.to_dlpack())
def sync():
context = context().context()
context.async_wait()
class GradContext:
def __init__(self):
self.tensor_for_grad = []
self.grad_list = []
self.tape = None
def set_tape(self, tape):
self.tape = tape
def add_tensor(self, x):
idx_pop = []
for idx, ele in enumerate(self.tensor_for_grad):
if ele._id == x._id:
idx_pop.append(idx)
if len(idx_pop) > 0:
self.tensor_for_grad.pop(idx_pop[0])
if self.tape is not None:
self.tape.watch(x)
self.tensor_for_grad.append(x)
def backward(self, x, head_gradient=None):
if head_gradient is not None:
x = x * head_gradient
self.grad_list = self.tape.gradient(x, self.tensor_for_grad)
def is_no_grad(self, x):
idx_pop = []
for idx, ele in enumerate(self.tensor_for_grad):
if ele._id == x._id:
idx_pop.append(idx)
if len(idx_pop) == 0:
return True
else:
return self.grad_list[idx_pop[0]] is None
def grad(self, x):
idx_pop = []
for idx, ele in enumerate(self.tensor_for_grad):
if ele._id == x._id:
idx_pop.append(idx)
assert len(idx_pop) == 1
t = self.grad_list[idx_pop[0]]
return tf.convert_to_tensor(t)
cgrad = GradContext()
def get_cgrad():
return cgrad
class record_grad:
def __init__(self):
self.tape = tf.GradientTape()
def __enter__(self):
cgrad.set_tape(self.tape)
self.tape.__enter__()
for x in cgrad.tensor_for_grad:
self.tape.watch(x)
def __exit__(self, exc_type, exc_value, exc_traceback):
# pass
self.tape.__exit__(exc_type, exc_value, exc_traceback)
cgrad.tape = None
def attach_grad(x):
cgrad.add_tensor(x)
return x
def backward(x, head_gradient=None):
cgrad.backward(x, head_gradient)
def grad(x):
return cgrad.grad(x)
def is_no_grad(x):
return cgrad.is_no_grad(x)
def is_recording():
raise NotImplementedError("Tensorflow doesn't support is_recording")
no_grad = None
initialize_context()
+58
View File
@@ -0,0 +1,58 @@
"""Module for base types and utilities."""
from __future__ import absolute_import
import warnings
from ._ffi.base import DGLError # pylint: disable=unused-import
from ._ffi.function import _init_internal_api
# A special symbol for selecting all nodes or edges.
ALL = "__ALL__"
# An alias for [:]
SLICE_FULL = slice(None, None, None)
# Reserved column names for storing parent node/edge types and IDs in flattened heterographs
NTYPE = "_TYPE"
NID = "_ID"
ETYPE = "_TYPE"
EID = "_ID"
_INTERNAL_COLUMNS = {NTYPE, NID, ETYPE, EID}
def is_internal_column(name):
"""Return true if the column name is reversed by DGL."""
return name in _INTERNAL_COLUMNS
def is_all(arg):
"""Return true if the argument is a special symbol for all nodes or edges."""
return isinstance(arg, str) and arg == ALL
# pylint: disable=invalid-name
_default_formatwarning = warnings.formatwarning
class DGLWarning(UserWarning):
"""DGL Warning class."""
# pylint: disable=unused-argument
def dgl_warning_format(message, category, filename, lineno, line=None):
"""Format DGL warnings."""
if isinstance(category, DGLWarning):
return "DGL Warning: {}\n".format(message)
else:
return _default_formatwarning(
message, category, filename, lineno, line=None
)
def dgl_warning(message, category=DGLWarning, stacklevel=2):
"""DGL warning wrapper that defaults to ``DGLWarning`` instead of ``UserWarning`` category."""
return warnings.warn(message, category=category, stacklevel=stacklevel)
warnings.formatwarning = dgl_warning_format
_init_internal_api()
+544
View File
@@ -0,0 +1,544 @@
"""Utilities for batching/unbatching graphs."""
from collections.abc import Mapping
from . import backend as F, convert, utils
from .base import ALL, DGLError, EID, is_all, NID
from .heterograph import DGLGraph
from .heterograph_index import disjoint_union, slice_gidx
__all__ = ["batch", "unbatch", "slice_batch"]
def batch(graphs, ndata=ALL, edata=ALL):
r"""Batch a collection of :class:`DGLGraph` s into one graph for more efficient
graph computation.
Each input graph becomes one disjoint component of the batched graph. The nodes
and edges are relabeled to be disjoint segments:
================= ========= ================= === =========
graphs[0] graphs[1] ... graphs[k]
================= ========= ================= === =========
Original node ID 0 ~ N_0 0 ~ N_1 ... 0 ~ N_k
New node ID 0 ~ N_0 N_0 ~ N_0+N_1 ... \sum_{i=0}^{k-1} N_i ~
\sum_{i=0}^k N_i
================= ========= ================= === =========
Because of this, many of the computations on a batched graph are the same as if
performed on each graph individually, but become much more efficient
since they can be parallelized easily. This makes ``dgl.batch`` very useful
for tasks dealing with many graph samples such as graph classification tasks.
For heterograph inputs, they must share the same set of relations (i.e., node types
and edge types) and the function will perform batching on each relation one by one.
Thus, the result is also a heterograph and has the same set of relations as the inputs.
The numbers of nodes and edges of the input graphs are accessible via the
:func:`DGLGraph.batch_num_nodes` and :func:`DGLGraph.batch_num_edges` attributes
of the resulting graph. For homogeneous graphs, they are 1D integer tensors,
with each element being the number of nodes/edges of the corresponding input graph. For
heterographs, they are dictionaries of 1D integer tensors, with node
type or edge type as the keys.
The function supports batching batched graphs. The batch size of the result
graph is the sum of the batch sizes of all the input graphs.
By default, node/edge features are batched by concatenating the feature tensors
of all input graphs. This thus requires features of the same name to have
the same data type and feature size. One can pass ``None`` to the ``ndata``
or ``edata`` argument to prevent feature batching, or pass a list of strings
to specify which features to batch.
To unbatch the graph back to a list, use the :func:`dgl.unbatch` function.
Parameters
----------
graphs : list[DGLGraph]
Input graphs.
ndata : list[str], None, optional
Node features to batch.
edata : list[str], None, optional
Edge features to batch.
Returns
-------
DGLGraph
Batched graph.
Examples
--------
Batch homogeneous graphs
>>> import dgl
>>> import torch as th
>>> # 4 nodes, 3 edges
>>> g1 = dgl.graph((th.tensor([0, 1, 2]), th.tensor([1, 2, 3])))
>>> # 3 nodes, 4 edges
>>> g2 = dgl.graph((th.tensor([0, 0, 0, 1]), th.tensor([0, 1, 2, 0])))
>>> bg = dgl.batch([g1, g2])
>>> bg
Graph(num_nodes=7, num_edges=7,
ndata_schemes={}
edata_schemes={})
>>> bg.batch_size
2
>>> bg.batch_num_nodes()
tensor([4, 3])
>>> bg.batch_num_edges()
tensor([3, 4])
>>> bg.edges()
(tensor([0, 1, 2, 4, 4, 4, 5], tensor([1, 2, 3, 4, 5, 6, 4]))
Batch batched graphs
>>> bbg = dgl.batch([bg, bg])
>>> bbg.batch_size
4
>>> bbg.batch_num_nodes()
tensor([4, 3, 4, 3])
>>> bbg.batch_num_edges()
tensor([3, 4, 3, 4])
Batch graphs with feature data
>>> g1.ndata['x'] = th.zeros(g1.num_nodes(), 3)
>>> g1.edata['w'] = th.ones(g1.num_edges(), 2)
>>> g2.ndata['x'] = th.ones(g2.num_nodes(), 3)
>>> g2.edata['w'] = th.zeros(g2.num_edges(), 2)
>>> bg = dgl.batch([g1, g2])
>>> bg.ndata['x']
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]])
>>> bg.edata['w']
tensor([[1, 1],
[1, 1],
[1, 1],
[0, 0],
[0, 0],
[0, 0],
[0, 0]])
Batch heterographs
>>> hg1 = dgl.heterograph({
... ('user', 'plays', 'game') : (th.tensor([0, 1]), th.tensor([0, 0]))})
>>> hg2 = dgl.heterograph({
... ('user', 'plays', 'game') : (th.tensor([0, 0, 0]), th.tensor([1, 0, 2]))})
>>> bhg = dgl.batch([hg1, hg2])
>>> bhg
Graph(num_nodes={'user': 3, 'game': 4},
num_edges={('user', 'plays', 'game'): 5},
metagraph=[('drug', 'game')])
>>> bhg.batch_size
2
>>> bhg.batch_num_nodes()
{'user' : tensor([2, 1]), 'game' : tensor([1, 3])}
>>> bhg.batch_num_edges()
{('user', 'plays', 'game') : tensor([2, 3])}
See Also
--------
unbatch
"""
if len(graphs) == 0:
raise DGLError("The input list of graphs cannot be empty.")
if not (is_all(ndata) or isinstance(ndata, list) or ndata is None):
raise DGLError(
"Invalid argument ndata: must be a string list but got {}.".format(
type(ndata)
)
)
if not (is_all(edata) or isinstance(edata, list) or edata is None):
raise DGLError(
"Invalid argument edata: must be a string list but got {}.".format(
type(edata)
)
)
if any(g.is_block for g in graphs):
raise DGLError("Batching a MFG is not supported.")
relations = list(graphs[0].canonical_etypes)
relation_ids = [graphs[0].get_etype_id(r) for r in relations]
ntypes = list(graphs[0].ntypes)
ntype_ids = [graphs[0].get_ntype_id(n) for n in ntypes]
etypes = [etype for _, etype, _ in relations]
gidx = disjoint_union(
graphs[0]._graph.metagraph, [g._graph for g in graphs]
)
retg = DGLGraph(gidx, ntypes, etypes)
# Compute batch num nodes
bnn = {}
for ntype in ntypes:
bnn[ntype] = F.cat([g.batch_num_nodes(ntype) for g in graphs], 0)
retg.set_batch_num_nodes(bnn)
# Compute batch num edges
bne = {}
for etype in relations:
bne[etype] = F.cat([g.batch_num_edges(etype) for g in graphs], 0)
retg.set_batch_num_edges(bne)
# Batch node feature
if ndata is not None:
for ntype_id, ntype in zip(ntype_ids, ntypes):
all_empty = all(g._graph.num_nodes(ntype_id) == 0 for g in graphs)
frames = [
g._node_frames[ntype_id]
for g in graphs
if g._graph.num_nodes(ntype_id) > 0 or all_empty
]
# TODO: do we require graphs with no nodes/edges to have the same schema? Currently
# we allow empty graphs to have no features during batching.
ret_feat = _batch_feat_dicts(
frames, ndata, 'nodes["{}"].data'.format(ntype)
)
retg.nodes[ntype].data.update(ret_feat)
# Batch edge feature
if edata is not None:
for etype_id, etype in zip(relation_ids, relations):
all_empty = all(g._graph.num_edges(etype_id) == 0 for g in graphs)
frames = [
g._edge_frames[etype_id]
for g in graphs
if g._graph.num_edges(etype_id) > 0 or all_empty
]
# TODO: do we require graphs with no nodes/edges to have the same schema? Currently
# we allow empty graphs to have no features during batching.
ret_feat = _batch_feat_dicts(
frames, edata, "edges[{}].data".format(etype)
)
retg.edges[etype].data.update(ret_feat)
return retg
def _batch_feat_dicts(frames, keys, feat_dict_name):
"""Internal function to batch feature dictionaries.
Parameters
----------
frames : list[Frame]
List of frames
keys : list[str]
Feature keys. Can be '__ALL__', meaning batching all features.
feat_dict_name : str
Name of the feature dictionary for reporting errors.
Returns
-------
dict[str, Tensor]
New feature dict.
"""
if len(frames) == 0:
return {}
schemas = [frame.schemes for frame in frames]
# sanity checks
if is_all(keys):
utils.check_all_same_schema(schemas, feat_dict_name)
keys = schemas[0].keys()
else:
utils.check_all_same_schema_for_keys(schemas, keys, feat_dict_name)
# concat features
ret_feat = {k: F.cat([fd[k] for fd in frames], 0) for k in keys}
return ret_feat
def unbatch(g, node_split=None, edge_split=None):
"""Revert the batch operation by split the given graph into a list of small ones.
This is the reverse operation of :func:``dgl.batch``. If the ``node_split``
or the ``edge_split`` is not given, it calls :func:`DGLGraph.batch_num_nodes`
and :func:`DGLGraph.batch_num_edges` of the input graph to get the information.
If the ``node_split`` or the ``edge_split`` arguments are given,
it will partition the graph according to the given segments. One must assure
that the partition is valid -- edges of the i^th graph only connect nodes
belong to the i^th graph. Otherwise, DGL will throw an error.
The function supports heterograph input, in which case the two split
section arguments shall be of dictionary type -- similar to the
:func:`DGLGraph.batch_num_nodes`
and :func:`DGLGraph.batch_num_edges` attributes of a heterograph.
Parameters
----------
g : DGLGraph
Input graph to unbatch.
node_split : Tensor, dict[str, Tensor], optional
Number of nodes of each result graph.
edge_split : Tensor, dict[str, Tensor], optional
Number of edges of each result graph.
Returns
-------
list[DGLGraph]
Unbatched list of graphs.
Examples
--------
Unbatch a batched graph
>>> import dgl
>>> import torch as th
>>> # 4 nodes, 3 edges
>>> g1 = dgl.graph((th.tensor([0, 1, 2]), th.tensor([1, 2, 3])))
>>> # 3 nodes, 4 edges
>>> g2 = dgl.graph((th.tensor([0, 0, 0, 1]), th.tensor([0, 1, 2, 0])))
>>> # add features
>>> g1.ndata['x'] = th.zeros(g1.num_nodes(), 3)
>>> g1.edata['w'] = th.ones(g1.num_edges(), 2)
>>> g2.ndata['x'] = th.ones(g2.num_nodes(), 3)
>>> g2.edata['w'] = th.zeros(g2.num_edges(), 2)
>>> bg = dgl.batch([g1, g2])
>>> f1, f2 = dgl.unbatch(bg)
>>> f1
Graph(num_nodes=4, num_edges=3,
ndata_schemes={x : Scheme(shape=(3,), dtype=torch.float32)}
edata_schemes={w : Scheme(shape=(2,), dtype=torch.float32)})
>>> f2
Graph(num_nodes=3, num_edges=4,
ndata_schemes={x : Scheme(shape=(3,), dtype=torch.float32)}
edata_schemes={w : Scheme(shape=(2,), dtype=torch.float32)})
With provided split arguments:
>>> g1 = dgl.graph((th.tensor([0, 1, 2]), th.tensor([1, 2, 3])))
>>> g2 = dgl.graph((th.tensor([0, 0, 0, 1]), th.tensor([0, 1, 2, 0])))
>>> g3 = dgl.graph((th.tensor([0]), th.tensor([1])))
>>> bg = dgl.batch([g1, g2, g3])
>>> bg.batch_num_nodes()
tensor([4, 3, 2])
>>> bg.batch_num_edges()
tensor([3, 4, 1])
>>> # unbatch but merge g2 and g3
>>> f1, f2 = dgl.unbatch(bg, th.tensor([4, 5]), th.tensor([3, 5]))
>>> f1
Graph(num_nodes=4, num_edges=3,
ndata_schemes={}
edata_schemes={})
>>> f2
Graph(num_nodes=5, num_edges=5,
ndata_schemes={}
edata_schemes={})
Heterograph input
>>> hg1 = dgl.heterograph({
... ('user', 'plays', 'game') : (th.tensor([0, 1]), th.tensor([0, 0]))})
>>> hg2 = dgl.heterograph({
... ('user', 'plays', 'game') : (th.tensor([0, 0, 0]), th.tensor([1, 0, 2]))})
>>> bhg = dgl.batch([hg1, hg2])
>>> f1, f2 = dgl.unbatch(bhg)
>>> f1
Graph(num_nodes={'user': 2, 'game': 1},
num_edges={('user', 'plays', 'game'): 2},
metagraph=[('drug', 'game')])
>>> f2
Graph(num_nodes={'user': 1, 'game': 3},
num_edges={('user', 'plays', 'game'): 3},
metagraph=[('drug', 'game')])
See Also
--------
batch
"""
num_split = None
# Parse node_split
if node_split is None:
node_split = {ntype: g.batch_num_nodes(ntype) for ntype in g.ntypes}
elif not isinstance(node_split, Mapping):
if len(g.ntypes) != 1:
raise DGLError(
"Must provide a dictionary for argument node_split when"
" there are multiple node types."
)
node_split = {g.ntypes[0]: node_split}
if node_split.keys() != set(g.ntypes):
raise DGLError("Must specify node_split for each node type.")
for split in node_split.values():
if num_split is not None and num_split != len(split):
raise DGLError(
"All node_split and edge_split must specify the same number"
" of split sizes."
)
num_split = len(split)
# Parse edge_split
if edge_split is None:
edge_split = {
etype: g.batch_num_edges(etype) for etype in g.canonical_etypes
}
elif not isinstance(edge_split, Mapping):
if len(g.etypes) != 1:
raise DGLError(
"Must provide a dictionary for argument edge_split when"
" there are multiple edge types."
)
edge_split = {g.canonical_etypes[0]: edge_split}
if edge_split.keys() != set(g.canonical_etypes):
raise DGLError("Must specify edge_split for each canonical edge type.")
for split in edge_split.values():
if num_split is not None and num_split != len(split):
raise DGLError(
"All edge_split and edge_split must specify the same number"
" of split sizes."
)
num_split = len(split)
node_split = {
k: F.asnumpy(split).tolist() for k, split in node_split.items()
}
edge_split = {
k: F.asnumpy(split).tolist() for k, split in edge_split.items()
}
# Split edges for each relation
edge_dict_per = [{} for i in range(num_split)]
for rel in g.canonical_etypes:
srctype, etype, dsttype = rel
srcnid_off = dstnid_off = 0
u, v = g.edges(order="eid", etype=rel)
us = F.split(u, edge_split[rel], 0)
vs = F.split(v, edge_split[rel], 0)
for i, (subu, subv) in enumerate(zip(us, vs)):
edge_dict_per[i][rel] = (subu - srcnid_off, subv - dstnid_off)
srcnid_off += node_split[srctype][i]
dstnid_off += node_split[dsttype][i]
num_nodes_dict_per = [
{k: split[i] for k, split in node_split.items()}
for i in range(num_split)
]
# Create graphs
gs = [
convert.heterograph(edge_dict, num_nodes_dict, idtype=g.idtype)
for edge_dict, num_nodes_dict in zip(edge_dict_per, num_nodes_dict_per)
]
# Unbatch node features
for ntype in g.ntypes:
for key, feat in g.nodes[ntype].data.items():
subfeats = F.split(feat, node_split[ntype], 0)
for subg, subf in zip(gs, subfeats):
subg.nodes[ntype].data[key] = subf
# Unbatch edge features
for etype in g.canonical_etypes:
for key, feat in g.edges[etype].data.items():
subfeats = F.split(feat, edge_split[etype], 0)
for subg, subf in zip(gs, subfeats):
subg.edges[etype].data[key] = subf
return gs
def slice_batch(g, gid, store_ids=False):
"""Get a particular graph from a batch of graphs.
Parameters
----------
g : DGLGraph
Input batched graph.
gid : int
The ID of the graph to retrieve.
store_ids : bool
If True, it will store the raw IDs of the extracted nodes and edges in the ``ndata`` and
``edata`` of the resulting graph under name ``dgl.NID`` and ``dgl.EID``, respectively.
Returns
-------
DGLGraph
Retrieved graph.
Examples
--------
The following example uses PyTorch backend.
>>> import dgl
>>> import torch
Create a batched graph.
>>> g1 = dgl.graph(([0, 1], [2, 3]))
>>> g2 = dgl.graph(([1], [2]))
>>> bg = dgl.batch([g1, g2])
Get the second component graph.
>>> g = dgl.slice_batch(bg, 1)
>>> print(g)
Graph(num_nodes=3, num_edges=1,
ndata_schemes={}
edata_schemes={})
"""
start_nid = []
num_nodes = []
for ntype in g.ntypes:
batch_num_nodes = g.batch_num_nodes(ntype)
num_nodes.append(F.as_scalar(batch_num_nodes[gid]))
if gid == 0:
start_nid.append(0)
else:
start_nid.append(
F.as_scalar(F.sum(F.slice_axis(batch_num_nodes, 0, 0, gid), 0))
)
start_eid = []
num_edges = []
for etype in g.canonical_etypes:
batch_num_edges = g.batch_num_edges(etype)
num_edges.append(F.as_scalar(batch_num_edges[gid]))
if gid == 0:
start_eid.append(0)
else:
start_eid.append(
F.as_scalar(F.sum(F.slice_axis(batch_num_edges, 0, 0, gid), 0))
)
# Slice graph structure
gidx = slice_gidx(
g._graph,
utils.toindex(num_nodes),
utils.toindex(start_nid),
utils.toindex(num_edges),
utils.toindex(start_eid),
)
retg = DGLGraph(gidx, g.ntypes, g.etypes)
# Slice node features
for ntid, ntype in enumerate(g.ntypes):
stnid = start_nid[ntid]
for key, feat in g.nodes[ntype].data.items():
subfeats = F.slice_axis(feat, 0, stnid, stnid + num_nodes[ntid])
retg.nodes[ntype].data[key] = subfeats
if store_ids:
retg.nodes[ntype].data[NID] = F.arange(
stnid, stnid + num_nodes[ntid], retg.idtype, retg.device
)
# Slice edge features
for etid, etype in enumerate(g.canonical_etypes):
steid = start_eid[etid]
for key, feat in g.edges[etype].data.items():
subfeats = F.slice_axis(feat, 0, steid, steid + num_edges[etid])
retg.edges[etype].data[key] = subfeats
if store_ids:
retg.edges[etype].data[EID] = F.arange(
steid, steid + num_edges[etid], retg.idtype, retg.device
)
return retg
+102
View File
@@ -0,0 +1,102 @@
"""Container data structures used in DGL runtime.
reference: tvm/python/tvm/collections.py
"""
from __future__ import absolute_import as _abs
from . import _api_internal
from ._ffi.object import ObjectBase, register_object
from ._ffi.object_generic import convert_to_object
@register_object
class List(ObjectBase):
"""List container of DGL.
You do not need to create List explicitly.
Normally python list and tuple will be converted automatically
to List during dgl function call.
You may get List in return values of DGL function call.
"""
def __getitem__(self, i):
if isinstance(i, slice):
start = i.start if i.start is not None else 0
stop = i.stop if i.stop is not None else len(self)
step = i.step if i.step is not None else 1
if start < 0:
start += len(self)
if stop < 0:
stop += len(self)
return [self[idx] for idx in range(start, stop, step)]
if i < -len(self) or i >= len(self):
raise IndexError(
"List index out of range. List size: {}, got index {}".format(
len(self), i
)
)
if i < 0:
i += len(self)
ret = _api_internal._ListGetItem(self, i)
if isinstance(ret, Value):
ret = ret.data
return ret
def __len__(self):
return _api_internal._ListSize(self)
@register_object
class Map(ObjectBase):
"""Map container of DGL.
You do not need to create Map explicitly.
Normally python dict will be converted automaticall to Map during dgl function call.
You can use convert to create a dict[ObjectBase-> ObjectBase] into a Map
"""
def __getitem__(self, k):
return _api_internal._MapGetItem(self, k)
def __contains__(self, k):
return _api_internal._MapCount(self, k) != 0
def items(self):
"""Get the items from the map"""
akvs = _api_internal._MapItems(self)
return [(akvs[i], akvs[i + 1]) for i in range(0, len(akvs), 2)]
def __len__(self):
return _api_internal._MapSize(self)
@register_object
class StrMap(Map):
"""A special map container that has str as key.
You can use convert to create a dict[str->ObjectBase] into a Map.
"""
def items(self):
"""Get the items from the map"""
akvs = _api_internal._MapItems(self)
return [(akvs[i], akvs[i + 1]) for i in range(0, len(akvs), 2)]
@register_object
class Value(ObjectBase):
"""Object wrapper for various values."""
@property
def data(self):
"""Return the value data."""
return _api_internal._ValueGet(self)
def convert_to_strmap(value):
"""Convert a python dictionary to a dgl.contrainer.StrMap"""
assert isinstance(value, dict), "Only support dict"
if len(value) == 0:
return _api_internal._EmptyStrMap()
else:
return convert_to_object(value)
File diff suppressed because it is too large Load Diff
+425
View File
@@ -0,0 +1,425 @@
"""Implementation for core graph computation."""
# pylint: disable=not-callable
import numpy as np
from . import backend as F, function as fn, ops
from .base import ALL, dgl_warning, DGLError, EID, is_all, NID
from .frame import Frame
from .udf import EdgeBatch, NodeBatch
def is_builtin(func):
"""Return true if the function is a DGL builtin function."""
return isinstance(func, fn.BuiltinFunction)
def invoke_node_udf(graph, nid, ntype, func, *, ndata=None, orig_nid=None):
"""Invoke user-defined node function on the given nodes.
Parameters
----------
graph : DGLGraph
The input graph.
nid : Tensor
The IDs of the nodes to invoke UDF on.
ntype : str
Node type.
func : callable
The user-defined function.
ndata : dict[str, Tensor], optional
If provided, apply the UDF on this ndata instead of the ndata of the graph.
orig_nid : Tensor, optional
Original node IDs. Useful if the input graph is an extracted subgraph.
Returns
-------
dict[str, Tensor]
Results from running the UDF.
"""
ntid = graph.get_ntype_id(ntype)
if ndata is None:
if is_all(nid):
ndata = graph._node_frames[ntid]
nid = graph.nodes(ntype=ntype)
else:
ndata = graph._node_frames[ntid].subframe(nid)
nbatch = NodeBatch(
graph, nid if orig_nid is None else orig_nid, ntype, ndata
)
return func(nbatch)
def invoke_edge_udf(graph, eid, etype, func, *, orig_eid=None):
"""Invoke user-defined edge function on the given edges.
Parameters
----------
graph : DGLGraph
The input graph.
eid : Tensor
The IDs of the edges to invoke UDF on.
etype : (str, str, str)
Edge type.
func : callable
The user-defined function.
orig_eid : Tensor, optional
Original edge IDs. Useful if the input graph is an extracted subgraph.
Returns
-------
dict[str, Tensor]
Results from running the UDF.
"""
etid = graph.get_etype_id(etype)
stid, dtid = graph._graph.metagraph.find_edge(etid)
if is_all(eid):
u, v, eid = graph.edges(form="all")
edata = graph._edge_frames[etid]
else:
u, v = graph.find_edges(eid)
edata = graph._edge_frames[etid].subframe(eid)
if len(u) == 0:
dgl_warning(
"The input graph for the user-defined edge function "
"does not contain valid edges"
)
srcdata = graph._node_frames[stid].subframe(u)
dstdata = graph._node_frames[dtid].subframe(v)
ebatch = EdgeBatch(
graph,
eid if orig_eid is None else orig_eid,
etype,
srcdata,
edata,
dstdata,
)
return func(ebatch)
def invoke_udf_reduce(graph, func, msgdata, *, orig_nid=None):
"""Invoke user-defined reduce function on all the nodes in the graph.
It analyzes the graph, groups nodes by their degrees and applies the UDF on each
group -- a strategy called *degree-bucketing*.
Parameters
----------
graph : DGLGraph
The input graph.
func : callable
The user-defined function.
msgdata : dict[str, Tensor]
Message data.
orig_nid : Tensor, optional
Original node IDs. Useful if the input graph is an extracted subgraph.
Returns
-------
dict[str, Tensor]
Results from running the UDF.
"""
degs = graph.in_degrees()
nodes = graph.dstnodes()
if orig_nid is None:
orig_nid = nodes
ntype = graph.dsttypes[0]
ntid = graph.get_ntype_id_from_dst(ntype)
dstdata = graph._node_frames[ntid]
msgdata = Frame(msgdata)
# degree bucketing
unique_degs, bucketor = _bucketing(degs)
bkt_rsts = []
bkt_nodes = []
for deg, node_bkt, orig_nid_bkt in zip(
unique_degs, bucketor(nodes), bucketor(orig_nid)
):
if deg == 0:
# skip reduce function for zero-degree nodes
continue
bkt_nodes.append(node_bkt)
ndata_bkt = dstdata.subframe(node_bkt)
# order the incoming edges per node by edge ID
eid_bkt = F.zerocopy_to_numpy(graph.in_edges(node_bkt, form="eid"))
assert len(eid_bkt) == deg * len(node_bkt)
eid_bkt = np.sort(eid_bkt.reshape((len(node_bkt), deg)), 1)
eid_bkt = F.zerocopy_from_numpy(eid_bkt.flatten())
msgdata_bkt = msgdata.subframe(eid_bkt)
# reshape all msg tensors to (num_nodes_bkt, degree, feat_size)
maildata = {}
for k, msg in msgdata_bkt.items():
newshape = (len(node_bkt), deg) + F.shape(msg)[1:]
maildata[k] = F.reshape(msg, newshape)
# invoke udf
nbatch = NodeBatch(graph, orig_nid_bkt, ntype, ndata_bkt, msgs=maildata)
bkt_rsts.append(func(nbatch))
# prepare a result frame
retf = Frame(num_rows=len(nodes))
retf._initializers = dstdata._initializers
retf._default_initializer = dstdata._default_initializer
# merge bucket results and write to the result frame
if (
len(bkt_rsts) != 0
): # if all the nodes have zero degree, no need to merge results.
merged_rst = {}
for k in bkt_rsts[0].keys():
merged_rst[k] = F.cat([rst[k] for rst in bkt_rsts], dim=0)
merged_nodes = F.cat(bkt_nodes, dim=0)
retf.update_row(merged_nodes, merged_rst)
return retf
def _bucketing(val):
"""Internal function to create groups on the values.
Parameters
----------
val : Tensor
Value tensor.
Returns
-------
unique_val : Tensor
Unique values.
bucketor : callable[Tensor -> list[Tensor]]
A bucketing function that splits the given tensor data as the same
way of how the :attr:`val` tensor is grouped.
"""
sorted_val, idx = F.sort_1d(val)
unique_val = F.asnumpy(F.unique(sorted_val))
bkt_idx = []
for v in unique_val:
eqidx = F.nonzero_1d(F.equal(sorted_val, v))
bkt_idx.append(F.gather_row(idx, eqidx))
def bucketor(data):
bkts = [F.gather_row(data, idx) for idx in bkt_idx]
return bkts
return unique_val, bucketor
def data_dict_to_list(graph, data_dict, func, target):
"""Get node or edge feature data of the given name for all the types.
Parameters
-------------
graph : DGLGraph
The input graph.
data_dict : dict[str, Tensor] or dict[(str, str, str), Tensor]] or Tensor
Node or edge data stored in DGLGraph. The key of the dictionary
is the node type name or edge type name. If there is only single source
node type, data_dict is the value of feature(a Tensor) not a dict.
func : dgl.function.BaseMessageFunction
Built-in message function.
target : 'u', 'v' or 'e'
The target of the lhs or rhs data
Returns
--------
data_list : list(Tensor)
Feature data stored in a list of tensors. The i^th tensor stores the feature
data of type ``types[i]``.
"""
if isinstance(func, fn.BinaryMessageFunction):
if target in ["u", "v"]:
output_list = [None] * graph._graph.number_of_ntypes()
# If there is only single source node type, data_dict should be the value of
# feature, namely, a tensor.
if not isinstance(data_dict, dict):
src_id, dst_id = graph._graph.metagraph.find_edge(0)
if target == "u":
output_list[src_id] = data_dict
else:
output_list[dst_id] = data_dict
else:
for srctype, _, dsttype in graph.canonical_etypes:
if target == "u":
src_id = graph.get_ntype_id(srctype)
output_list[src_id] = data_dict[srctype]
else:
dst_id = graph.get_ntype_id(dsttype)
output_list[dst_id] = data_dict[dsttype]
else: # target == 'e'
output_list = [None] * graph._graph.number_of_etypes()
for rel in graph.canonical_etypes:
etid = graph.get_etype_id(rel)
output_list[etid] = data_dict[rel]
return output_list
else:
if target == "u":
lhs_list = [None] * graph._graph.number_of_ntypes()
if not isinstance(data_dict, dict):
src_id, _ = graph._graph.metagraph.find_edge(0)
lhs_list[src_id] = data_dict
else:
for srctype, _, _ in graph.canonical_etypes:
src_id = graph.get_ntype_id(srctype)
lhs_list[src_id] = data_dict[srctype]
return lhs_list
else: # target == 'e':
rhs_list = [None] * graph._graph.number_of_etypes()
for rel in graph.canonical_etypes:
etid = graph.get_etype_id(rel)
rhs_list[etid] = data_dict[rel]
return rhs_list
def invoke_gsddmm(graph, func):
"""Invoke g-SDDMM computation on the graph.
Parameters
----------
graph : DGLGraph
The input graph.
func : dgl.function.BaseMessageFunction
Built-in message function.
Returns
-------
dict[str, Tensor]
Results from the g-SDDMM computation.
"""
alldata = [graph.srcdata, graph.dstdata, graph.edata]
if isinstance(func, fn.BinaryMessageFunction):
x = alldata[func.lhs][func.lhs_field]
y = alldata[func.rhs][func.rhs_field]
op = getattr(ops, func.name)
if graph._graph.number_of_etypes() > 1:
lhs_target, _, rhs_target = func.name.split("_", 2)
x = data_dict_to_list(graph, x, func, lhs_target)
y = data_dict_to_list(graph, y, func, rhs_target)
z = op(graph, x, y)
else:
x = alldata[func.target][func.in_field]
op = getattr(ops, func.name)
if graph._graph.number_of_etypes() > 1:
# Convert to list as dict is unordered.
if func.name == "copy_u":
x = data_dict_to_list(graph, x, func, "u")
else: # "copy_e"
x = data_dict_to_list(graph, x, func, "e")
z = op(graph, x)
return {func.out_field: z}
def invoke_gspmm(
graph, mfunc, rfunc, *, srcdata=None, dstdata=None, edata=None
):
"""Invoke g-SPMM computation on the graph.
Parameters
----------
graph : DGLGraph
The input graph.
mfunc : dgl.function.BaseMessageFunction
Built-in message function.
rfunc : dgl.function.BaseReduceFunction
Built-in reduce function.
srcdata : dict[str, Tensor], optional
Source node feature data. If not provided, it use ``graph.srcdata``.
dstdata : dict[str, Tensor], optional
Destination node feature data. If not provided, it use ``graph.dstdata``.
edata : dict[str, Tensor], optional
Edge feature data. If not provided, it use ``graph.edata``.
Returns
-------
dict[str, Tensor]
Results from the g-SPMM computation.
"""
# sanity check
if mfunc.out_field != rfunc.msg_field:
raise DGLError(
"Invalid message ({}) and reduce ({}) function pairs."
" The output field of the message function must be equal to the"
" message field of the reduce function.".format(mfunc, rfunc)
)
if edata is None:
edata = graph.edata
if srcdata is None:
srcdata = graph.srcdata
if dstdata is None:
dstdata = graph.dstdata
alldata = [srcdata, dstdata, edata]
if isinstance(mfunc, fn.BinaryMessageFunction):
x = alldata[mfunc.lhs][mfunc.lhs_field]
y = alldata[mfunc.rhs][mfunc.rhs_field]
op = getattr(ops, "{}_{}".format(mfunc.name, rfunc.name))
if graph._graph.number_of_etypes() > 1:
lhs_target, _, rhs_target = mfunc.name.split("_", 2)
x = data_dict_to_list(graph, x, mfunc, lhs_target)
y = data_dict_to_list(graph, y, mfunc, rhs_target)
z = op(graph, x, y)
else:
x = alldata[mfunc.target][mfunc.in_field]
op = getattr(ops, "{}_{}".format(mfunc.name, rfunc.name))
if graph._graph.number_of_etypes() > 1 and not isinstance(x, tuple):
if mfunc.name == "copy_u":
x = data_dict_to_list(graph, x, mfunc, "u")
else: # "copy_e"
x = data_dict_to_list(graph, x, mfunc, "e")
z = op(graph, x)
return {rfunc.out_field: z}
def message_passing(g, mfunc, rfunc, afunc):
"""Invoke message passing computation on the whole graph.
Parameters
----------
g : DGLGraph
The input graph.
mfunc : callable or dgl.function.BuiltinFunction
Message function.
rfunc : callable or dgl.function.BuiltinFunction
Reduce function.
afunc : callable or dgl.function.BuiltinFunction
Apply function.
Returns
-------
dict[str, Tensor]
Results from the message passing computation.
"""
if (
is_builtin(mfunc)
and is_builtin(rfunc)
and getattr(ops, "{}_{}".format(mfunc.name, rfunc.name), None)
is not None
):
# invoke fused message passing
ndata = invoke_gspmm(g, mfunc, rfunc)
else:
# invoke message passing in two separate steps
# message phase
if is_builtin(mfunc):
msgdata = invoke_gsddmm(g, mfunc)
else:
orig_eid = g.edata.get(EID, None)
msgdata = invoke_edge_udf(
g, ALL, g.canonical_etypes[0], mfunc, orig_eid=orig_eid
)
# reduce phase
if is_builtin(rfunc):
msg = rfunc.msg_field
ndata = invoke_gspmm(g, fn.copy_e(msg, msg), rfunc, edata=msgdata)
else:
orig_nid = g.dstdata.get(NID, None)
ndata = invoke_udf_reduce(g, rfunc, msgdata, orig_nid=orig_nid)
# apply phase
if afunc is not None:
for k, v in g.dstdata.items(): # include original node features
if k not in ndata:
ndata[k] = v
orig_nid = g.dstdata.get(NID, None)
ndata = invoke_node_udf(
g, ALL, g.dsttypes[0], afunc, ndata=ndata, orig_nid=orig_nid
)
return ndata
+7
View File
@@ -0,0 +1,7 @@
""" CUDA wrappers """
from .. import backend as F
from .gpu_cache import GPUCache
if F.get_preferred_backend() == "pytorch":
from . import nccl
+86
View File
@@ -0,0 +1,86 @@
"""API wrapping HugeCTR gpu_cache."""
# Copyright (c) 2022, NVIDIA Corporation
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# @file gpu_cache.py
# @brief API for managing a GPU Cache
from .. import backend as F
from .._ffi.function import _init_api
class GPUCache(object):
"""High-level wrapper for GPU embedding cache"""
def __init__(self, num_items, num_feats, idtype=F.int64):
assert idtype in [F.int32, F.int64]
self._cache = _CAPI_DGLGpuCacheCreate(
num_items, num_feats, 32 if idtype == F.int32 else 64
)
self.idtype = idtype
self.total_miss = 0
self.total_queries = 0
def query(self, keys):
"""Queries the GPU cache.
Parameters
----------
keys : Tensor
The keys to query the GPU cache with.
Returns
-------
tuple(Tensor, Tensor, Tensor)
A tuple containing (values, missing_indices, missing_keys) where
values[missing_indices] corresponds to cache misses that should be
filled by quering another source with missing_keys.
"""
self.total_queries += keys.shape[0]
keys = F.astype(keys, self.idtype)
values, missing_index, missing_keys = _CAPI_DGLGpuCacheQuery(
self._cache, F.to_dgl_nd(keys)
)
self.total_miss += missing_keys.shape[0]
return (
F.from_dgl_nd(values),
F.from_dgl_nd(missing_index),
F.from_dgl_nd(missing_keys),
)
def replace(self, keys, values):
"""Inserts key-value pairs into the GPU cache using the Least-Recently
Used (LRU) algorithm to remove old key-value pairs if it is full.
Parameters
----------
keys: Tensor
The keys to insert to the GPU cache.
values: Tensor
The values to insert to the GPU cache.
"""
keys = F.astype(keys, self.idtype)
values = F.astype(values, F.float32)
_CAPI_DGLGpuCacheReplace(
self._cache, F.to_dgl_nd(keys), F.to_dgl_nd(values)
)
@property
def miss_rate(self):
"""Returns the cache miss rate since creation."""
return self.total_miss / self.total_queries
_init_api("dgl.cuda", __name__)
+189
View File
@@ -0,0 +1,189 @@
"""API wrapping NCCL primitives."""
import torch
import torch.distributed as dist
def sparse_all_to_all_push(idx, value, partition):
"""Perform an all-to-all-v operation, where by all processors send out
a set of indices and corresponding values. Indices and values,
corresponding to the current process, will copied into the output
arrays.
Note: This method requires 'torch.distributed.get_backend() == "nccl"'.
Parameters
----------
idx : torch.Tensor
The 1D set of indices to send to other processors.
value : torch.Tensor
The multi-dimension set of values to send to other processors.
The first dimension must match that of `idx`.
partition : NDArrayPartition
The object containing information for assigning indices to
processors.
Returns
-------
torch.Tensor
The 1D tensor of the recieved indices.
torch.Tensor
The set of recieved values.
Examples
--------
To perform a sparse_all_to_all_push(), a partition object must be
provided. A partition of a homgeonous graph, where the vertices are
striped across processes can be generated via:
>>> from dgl.partition import NDArrayPartition
>>> part = NDArrayPartition(g.num_nodes(), world_size, mode='remainder')
With this partition, each processor can send values to be associatd
with vertices in the graph. So if we have an array `global_idxs` of all of
the neighbors updated during mini-batch processing, and an array
`global_values` containing the new values associated with the neighbors,
we communicate them to the own processes via:
>>> my_idxs, my_values = nccl.sparse_all_to_all_push(global_idxs, global_values, part)
This communication pattern is common when communicating gradient
updates for node embeddings.
Indices the current process owns, do not need to treated specially,
as internally they will be copied to the output array. If we have a
set of indices in process 0 '[0, 3, 8, 9, 10]` and for process 1
'[0, 2, 4, 5, 8, 8, 9]'. Using a remainder partition will result
indices for processe 0 of '[0, 8, 10, 0, 2, 4, 8, 8]', and for
process 1 of '[3, 9, 5, 9]'.
"""
if not dist.is_initialized() or dist.get_world_size() == 1:
return idx, value
assert (
dist.get_backend() == "nccl"
), "requires NCCL backend to communicate CUDA tensors."
perm, send_splits = partition.generate_permutation(idx)
perm = perm.long()
# Get receive splits.
recv_splits = torch.empty_like(send_splits)
dist.all_to_all_single(recv_splits, send_splits)
# Use pinned memory to speedup D2H copy.
recv_splits = recv_splits.to("cpu", non_blocking=True)
send_splits = send_splits.to("cpu", non_blocking=True)
send_idx = idx[perm]
send_value = value[perm]
# Wait D2H copy finish.
torch.cuda.current_stream().synchronize()
recv_sum = recv_splits.sum()
recv_splits = recv_splits.tolist()
send_splits = send_splits.tolist()
# Send idx.
recv_idx = torch.empty((recv_sum,), dtype=idx.dtype, device=idx.device)
dist.all_to_all_single(recv_idx, send_idx, recv_splits, send_splits)
# Send value.
recv_value = torch.empty(
(recv_sum, *value.shape[1:]), dtype=value.dtype, device=value.device
)
dist.all_to_all_single(recv_value, send_value, recv_splits, send_splits)
return recv_idx, recv_value
def sparse_all_to_all_pull(req_idx, value, partition):
"""Perform an all-to-all-v operation, where by all processors request
the values corresponding to their set of indices.
Note: This method requires 'torch.distributed.get_backend() == "nccl"'.
Parameters
----------
req_idx : torch.Tensor
The set of indices this processor is requesting.
value : torch.Tensor
The multi-dimension set of values that can be requested from
this processor.
partition : NDArrayPartition
The object containing information for assigning indices to
processors.
Returns
-------
torch.Tensor
The set of recieved values, corresponding to `req_idx`.
Examples
--------
To perform a sparse_all_to_all_pull(), a partition object must be
provided. A partition of a homgeonous graph, where the vertices are
striped across processes can be generated via:
>>> from dgl.partition import NDArrayPartition
>>> part = NDArrayPartition(g.num_nodes(), world_size, mode='remainder')
With this partition, each processor can request values/features
associated with vertices in the graph. So in the case where we have
a set of neighbors 'nbr_idxs' we need features for, and each process
has a tensor 'node_feat' storing the features of nodes it owns in
the partition, the features can be requested via:
>>> nbr_values = nccl.sparse_all_to_all_pull(nbr_idxs, node_feat, part)
Then two the arrays 'nbr_idxs' and 'nbr_values' forms the sparse
set of features, where 'nbr_idxs[i]' is the global node id, and
'nbr_values[i]' is the feature vector for that node. This
communication pattern is useful for node features or node
embeddings.
"""
if not dist.is_initialized() or dist.get_world_size() == 1:
return value[req_idx.long()]
assert (
dist.get_backend() == "nccl"
), "requires NCCL backend to communicate CUDA tensors."
perm, req_splits = partition.generate_permutation(req_idx)
perm = perm.long()
# Get response splits.
resp_splits = torch.empty_like(req_splits)
dist.all_to_all_single(resp_splits, req_splits)
# Use pinned memory to speedup D2H copy.
resp_splits = resp_splits.to("cpu", non_blocking=True)
req_splits = req_splits.to("cpu", non_blocking=True)
req_idx = req_idx[perm]
# Wait D2H copy finish.
torch.cuda.current_stream().synchronize()
resp_sum = resp_splits.sum()
resp_splits = resp_splits.tolist()
req_splits = req_splits.tolist()
# Gather requested indices.
resp_idx = torch.empty(
(resp_sum,), dtype=req_idx.dtype, device=req_idx.device
)
dist.all_to_all_single(resp_idx, req_idx, resp_splits, req_splits)
# Convert requested indices to local indices depending on partition.
if resp_sum > 0:
resp_idx = partition.map_to_local(resp_idx)
# Collect the request value.
req_value = torch.empty(
(req_idx.size(0), *value.shape[1:]),
dtype=value.dtype,
device=value.device,
)
dist.all_to_all_single(req_value, value[resp_idx], req_splits, resp_splits)
# Permute the value back into the requested order.
return_value = torch.empty_like(req_value)
return_value[perm] = req_value
return return_value
+112
View File
@@ -0,0 +1,112 @@
"""The ``dgl.data`` package contains datasets hosted by DGL and also utilities
for downloading, processing, saving and loading data from external resources.
"""
from __future__ import absolute_import
from . import citation_graph as citegrh
from .actor import ActorDataset
from .movielens import MovieLensDataset
from .adapter import *
from .bitcoinotc import BitcoinOTC, BitcoinOTCDataset
from .citation_graph import (
CitationGraphDataset,
CiteseerGraphDataset,
CoraBinary,
CoraGraphDataset,
PubmedGraphDataset,
)
from .csv_dataset import CSVDataset
from .dgl_dataset import DGLBuiltinDataset, DGLDataset
from .fakenews import FakeNewsDataset
from .flickr import FlickrDataset
from .fraud import FraudAmazonDataset, FraudDataset, FraudYelpDataset
from .gdelt import GDELT, GDELTDataset
from .gindt import GINDataset
from .gnn_benchmark import (
AmazonCoBuy,
AmazonCoBuyComputerDataset,
AmazonCoBuyPhotoDataset,
Coauthor,
CoauthorCSDataset,
CoauthorPhysicsDataset,
CoraFull,
CoraFullDataset,
)
from .icews18 import ICEWS18, ICEWS18Dataset
from .karate import KarateClub, KarateClubDataset
from .knowledge_graph import FB15k237Dataset, FB15kDataset, WN18Dataset
from .minigc import *
from .ppi import LegacyPPIDataset, PPIDataset
from .qm7b import QM7b, QM7bDataset
from .qm9 import QM9, QM9Dataset
from .qm9_edge import QM9Edge, QM9EdgeDataset
from .rdf import AIFBDataset, AMDataset, BGSDataset, MUTAGDataset
from .reddit import RedditDataset
from .sbm import SBMMixture, SBMMixtureDataset
from .synthetic import (
BA2MotifDataset,
BACommunityDataset,
BAShapeDataset,
TreeCycleDataset,
TreeGridDataset,
)
from .tree import SST, SSTDataset
from .tu import LegacyTUDataset, TUDataset
from .utils import *
from .cluster import CLUSTERDataset
from .geom_gcn import (
ChameleonDataset,
CornellDataset,
SquirrelDataset,
TexasDataset,
WisconsinDataset,
)
from .heterophilous_graphs import (
AmazonRatingsDataset,
MinesweeperDataset,
QuestionsDataset,
RomanEmpireDataset,
TolokersDataset,
)
# RDKit is required for Peptides-Structural, Peptides-Functional dataset.
# Exception handling was added to prevent crashes for users who are using other
# datasets.
try:
from .lrgb import (
COCOSuperpixelsDataset,
PeptidesFunctionalDataset,
PeptidesStructuralDataset,
VOCSuperpixelsDataset,
)
except ImportError:
pass
from .pattern import PATTERNDataset
from .superpixel import CIFAR10SuperPixelDataset, MNISTSuperPixelDataset
from .wikics import WikiCSDataset
from .yelp import YelpDataset
from .zinc import ZINCDataset
def register_data_args(parser):
parser.add_argument(
"--dataset",
type=str,
required=False,
help="The input dataset. Can be cora, citeseer, pubmed, syn(synthetic dataset) or reddit",
)
def load_data(args):
if args.dataset == "cora":
return citegrh.load_cora()
elif args.dataset == "citeseer":
return citegrh.load_citeseer()
elif args.dataset == "pubmed":
return citegrh.load_pubmed()
elif args.dataset is not None and args.dataset.startswith("reddit"):
return RedditDataset(self_loop=("self-loop" in args.dataset))
else:
raise ValueError("Unknown dataset: {}".format(args.dataset))
+138
View File
@@ -0,0 +1,138 @@
"""
Actor-only induced subgraph of the film-directoractor-writer network.
"""
import os
import numpy as np
from ..convert import graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url
class ActorDataset(DGLBuiltinDataset):
r"""Actor-only induced subgraph of the film-directoractor-writer network
from `Social Influence Analysis in Large-scale Networks
<https://dl.acm.org/doi/10.1145/1557019.1557108>`, introduced by
`Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`
Nodes represent actors, and edges represent co-occurrence on the same
Wikipedia page. Node features correspond to some keywords in the Wikipedia
pages.
Statistics:
- Nodes: 7600
- Edges: 33391
- Number of Classes: 5
- 10 train/val/test splits
- Train: 3648
- Val: 2432
- Test: 1520
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Notes
-----
The graph does not come with edges for both directions.
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(ActorDataset, self).__init__(
name="actor",
url=_get_dgl_url("dataset/actor.zip"),
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""Load and process the data."""
try:
import torch
except ImportError:
raise ModuleNotFoundError(
"This dataset requires PyTorch to be the backend."
)
# Process node features and labels.
with open(f"{self.raw_path}/out1_node_feature_label.txt", "r") as f:
data = [x.split("\t") for x in f.read().split("\n")[1:-1]]
rows, cols = [], []
labels = torch.empty(len(data), dtype=torch.long)
for n_id, col, label in data:
col = [int(x) for x in col.split(",")]
rows += [int(n_id)] * len(col)
cols += col
labels[int(n_id)] = int(label)
row, col = torch.tensor(rows), torch.tensor(cols)
features = torch.zeros(len(data), int(col.max()) + 1)
features[row, col] = 1.0
self._num_classes = int(labels.max().item()) + 1
# Process graph structure.
with open(f"{self.raw_path}/out1_graph_edges.txt", "r") as f:
data = f.read().split("\n")[1:-1]
data = [[int(v) for v in r.split("\t")] for r in data]
dst, src = torch.tensor(data, dtype=torch.long).t().contiguous()
self._g = graph((src, dst), num_nodes=features.size(0))
self._g.ndata["feat"] = features
self._g.ndata["label"] = labels
# Process 10 train/val/test node splits.
train_masks, val_masks, test_masks = [], [], []
for i in range(10):
filepath = f"{self.raw_path}/{self.name}_split_0.6_0.2_{i}.npz"
f = np.load(filepath)
train_masks += [torch.from_numpy(f["train_mask"])]
val_masks += [torch.from_numpy(f["val_mask"])]
test_masks += [torch.from_numpy(f["test_mask"])]
self._g.ndata["train_mask"] = torch.stack(train_masks, dim=1).bool()
self._g.ndata["val_mask"] = torch.stack(val_masks, dim=1).bool()
self._g.ndata["test_mask"] = torch.stack(test_masks, dim=1).bool()
def has_cache(self):
return os.path.exists(self.raw_path)
def load(self):
self.process()
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._g
else:
return self._transform(self._g)
def __len__(self):
return 1
@property
def num_classes(self):
return self._num_classes
+633
View File
@@ -0,0 +1,633 @@
"""Dataset adapters for re-purposing a dataset for a different kind of training task."""
import json
import os
import numpy as np
from .. import backend as F
from ..base import DGLError
from ..convert import graph as create_dgl_graph
from ..sampling.negative import _calc_redundancy
from . import utils
from .dgl_dataset import DGLDataset
__all__ = ["AsNodePredDataset", "AsLinkPredDataset", "AsGraphPredDataset"]
class AsNodePredDataset(DGLDataset):
"""Repurpose a dataset for a standard semi-supervised transductive
node prediction task.
The class converts a given dataset into a new dataset object such that:
- Contains only one graph, accessible from ``dataset[0]``.
- The graph stores:
- Node labels in ``g.ndata['label']``.
- Train/val/test masks in ``g.ndata['train_mask']``, ``g.ndata['val_mask']``,
and ``g.ndata['test_mask']`` respectively.
- In addition, the dataset contains the following attributes:
- ``num_classes``, the number of classes to predict.
- ``train_idx``, ``val_idx``, ``test_idx``, train/val/test indexes.
If the input dataset contains heterogeneous graphs, users need to specify the
``target_ntype`` argument to indicate which node type to make predictions for.
In this case:
- Node labels are stored in ``g.nodes[target_ntype].data['label']``.
- Training masks are stored in ``g.nodes[target_ntype].data['train_mask']``.
So do validation and test masks.
The class will keep only the first graph in the provided dataset and
generate train/val/test masks according to the given split ratio. The generated
masks will be cached to disk for fast re-loading. If the provided split ratio
differs from the cached one, it will re-process the dataset properly.
Parameters
----------
dataset : DGLDataset
The dataset to be converted.
split_ratio : (float, float, float), optional
Split ratios for training, validation and test sets. They must sum to one.
target_ntype : str, optional
The node type to add split mask for.
Attributes
----------
num_classes : int
Number of classes to predict.
train_idx : Tensor
An 1-D integer tensor of training node IDs.
val_idx : Tensor
An 1-D integer tensor of validation node IDs.
test_idx : Tensor
An 1-D integer tensor of test node IDs.
Examples
--------
>>> ds = dgl.data.AmazonCoBuyComputerDataset()
>>> print(ds)
Dataset("amazon_co_buy_computer", num_graphs=1, save_path=...)
>>> new_ds = dgl.data.AsNodePredDataset(ds, [0.8, 0.1, 0.1])
>>> print(new_ds)
Dataset("amazon_co_buy_computer-as-nodepred", num_graphs=1, save_path=...)
>>> print('train_mask' in new_ds[0].ndata)
True
"""
def __init__(self, dataset, split_ratio=None, target_ntype=None, **kwargs):
self.dataset = dataset
self.split_ratio = split_ratio
self.target_ntype = target_ntype
super().__init__(
self.dataset.name + "-as-nodepred",
hash_key=(split_ratio, target_ntype, dataset.name, "nodepred"),
**kwargs
)
def process(self):
is_ogb = hasattr(self.dataset, "get_idx_split")
if is_ogb:
g, label = self.dataset[0]
self.g = g.clone()
self.g.ndata["label"] = F.reshape(label, (g.num_nodes(),))
else:
self.g = self.dataset[0].clone()
if "label" not in self.g.nodes[self.target_ntype].data:
raise ValueError(
"Missing node labels. Make sure labels are stored "
"under name 'label'."
)
if self.split_ratio is None:
if is_ogb:
split = self.dataset.get_idx_split()
train_idx, val_idx, test_idx = (
split["train"],
split["valid"],
split["test"],
)
n = self.g.num_nodes()
train_mask = utils.generate_mask_tensor(
utils.idx2mask(train_idx, n)
)
val_mask = utils.generate_mask_tensor(
utils.idx2mask(val_idx, n)
)
test_mask = utils.generate_mask_tensor(
utils.idx2mask(test_idx, n)
)
self.g.ndata["train_mask"] = train_mask
self.g.ndata["val_mask"] = val_mask
self.g.ndata["test_mask"] = test_mask
else:
assert (
"train_mask" in self.g.nodes[self.target_ntype].data
), "train_mask is not provided, please specify split_ratio to generate the masks"
assert (
"val_mask" in self.g.nodes[self.target_ntype].data
), "val_mask is not provided, please specify split_ratio to generate the masks"
assert (
"test_mask" in self.g.nodes[self.target_ntype].data
), "test_mask is not provided, please specify split_ratio to generate the masks"
else:
if self.verbose:
print("Generating train/val/test masks...")
utils.add_nodepred_split(self, self.split_ratio, self.target_ntype)
self._set_split_index()
self.num_classes = getattr(self.dataset, "num_classes", None)
if self.num_classes is None:
self.num_classes = len(
F.unique(self.g.nodes[self.target_ntype].data["label"])
)
def has_cache(self):
return os.path.isfile(
os.path.join(self.save_path, "graph_{}.bin".format(self.hash))
)
def load(self):
with open(
os.path.join(self.save_path, "info_{}.json".format(self.hash)), "r"
) as f:
info = json.load(f)
if (
info["split_ratio"] != self.split_ratio
or info["target_ntype"] != self.target_ntype
):
raise ValueError(
"Provided split ratio is different from the cached file. "
"Re-process the dataset."
)
self.split_ratio = info["split_ratio"]
self.target_ntype = info["target_ntype"]
self.num_classes = info["num_classes"]
gs, _ = utils.load_graphs(
os.path.join(self.save_path, "graph_{}.bin".format(self.hash))
)
self.g = gs[0]
self._set_split_index()
def save(self):
utils.save_graphs(
os.path.join(self.save_path, "graph_{}.bin".format(self.hash)),
[self.g],
)
with open(
os.path.join(self.save_path, "info_{}.json".format(self.hash)), "w"
) as f:
json.dump(
{
"split_ratio": self.split_ratio,
"target_ntype": self.target_ntype,
"num_classes": self.num_classes,
},
f,
)
def __getitem__(self, idx):
return self.g
def __len__(self):
return 1
def _set_split_index(self):
"""Add train_idx/val_idx/test_idx as dataset attributes according to corresponding mask."""
ndata = self.g.nodes[self.target_ntype].data
self.train_idx = F.nonzero_1d(ndata["train_mask"])
self.val_idx = F.nonzero_1d(ndata["val_mask"])
self.test_idx = F.nonzero_1d(ndata["test_mask"])
def negative_sample(g, num_samples):
"""Random sample negative edges from graph, excluding self-loops,
the result samples might be less than num_samples
"""
num_nodes = g.num_nodes()
redundancy = _calc_redundancy(num_samples, g.num_edges(), num_nodes**2)
sample_size = int(num_samples * (1 + redundancy))
edges = np.random.randint(0, num_nodes, size=(2, sample_size))
edges = np.unique(edges, axis=1)
# remove self loop
mask_self_loop = edges[0] == edges[1]
# remove existing edges
has_edges = F.asnumpy(g.has_edges_between(edges[0], edges[1]))
mask = ~(np.logical_or(mask_self_loop, has_edges))
edges = edges[:, mask]
if edges.shape[1] >= num_samples:
edges = edges[:, :num_samples]
return edges
class AsLinkPredDataset(DGLDataset):
"""Repurpose a dataset for link prediction task.
The created dataset will include data needed for link prediction.
Currently it only supports homogeneous graphs.
It will keep only the first graph in the provided dataset and
generate train/val/test edges according to the given split ratio,
and the correspondent negative edges based on the neg_ratio. The generated
edges will be cached to disk for fast re-loading. If the provided split ratio
differs from the cached one, it will re-process the dataset properly.
Parameters
----------
dataset : DGLDataset
The dataset to be converted.
split_ratio : (float, float, float), optional
Split ratios for training, validation and test sets. Must sum to one.
neg_ratio : int, optional
Indicate how much negative samples to be sampled
The number of the negative samples will be equal or less than neg_ratio * num_positive_edges.
Attributes
-------
feat_size: int
The size of the feature dimension in the graph
train_graph: DGLGraph
The DGLGraph for training
val_edges: Tuple[Tuple[Tensor, Tensor], Tuple[Tensor, Tensor]]
The validation set edges, encoded as
((positive_edge_src, positive_edge_dst), (negative_edge_src, negative_edge_dst))
test_edges: Tuple[Tuple[Tensor, Tensor], Tuple[Tensor, Tensor]]
The test set edges, encoded as
((positive_edge_src, positive_edge_dst), (negative_edge_src, negative_edge_dst))
Examples
--------
>>> ds = dgl.data.CoraGraphDataset()
>>> print(ds)
Dataset("cora_v2", num_graphs=1, save_path=...)
>>> new_ds = dgl.data.AsLinkPredDataset(ds, [0.8, 0.1, 0.1])
>>> print(new_ds)
Dataset("cora_v2-as-linkpred", num_graphs=1, save_path=/home/ubuntu/.dgl/cora_v2-as-linkpred)
>>> print(hasattr(new_ds, "test_edges"))
True
"""
def __init__(self, dataset, split_ratio=None, neg_ratio=3, **kwargs):
self.g = dataset[0]
self.num_nodes = self.g.num_nodes()
self.dataset = dataset
self.split_ratio = split_ratio
self.neg_ratio = neg_ratio
super().__init__(
dataset.name + "-as-linkpred",
hash_key=(neg_ratio, split_ratio, dataset.name, "linkpred"),
**kwargs
)
def process(self):
if self.split_ratio is None:
# Handle logics for OGB link prediction dataset
assert hasattr(
self.dataset, "get_edge_split"
), "dataset doesn't have get_edge_split method, please specify split_ratio and neg_ratio to generate the split"
# This is likely to be an ogb dataset
self.edge_split = self.dataset.get_edge_split()
self._train_graph = self.g
if "source_node" in self.edge_split["test"]:
# Probably ogbl-citation2
pos_e = (
self.edge_split["valid"]["source_node"],
self.edge_split["valid"]["target_node"],
)
neg_e_size = self.edge_split["valid"]["target_node_neg"].shape[
-1
]
neg_e_src = np.repeat(
self.edge_split["valid"]["source_node"], neg_e_size
)
neg_e_dst = np.reshape(
self.edge_split["valid"]["target_node_neg"], -1
)
self._val_edges = pos_e, (neg_e_src, neg_e_dst)
pos_e = (
self.edge_split["test"]["source_node"],
self.edge_split["test"]["target_node"],
)
neg_e_size = self.edge_split["test"]["target_node_neg"].shape[
-1
]
neg_e_src = np.repeat(
self.edge_split["test"]["source_node"], neg_e_size
)
neg_e_dst = np.reshape(
self.edge_split["test"]["target_node_neg"], -1
)
self._test_edges = pos_e, (neg_e_src, neg_e_dst)
elif "edge" in self.edge_split["test"]:
# Probably ogbl-collab
pos_e_tensor, neg_e_tensor = (
self.edge_split["valid"]["edge"],
self.edge_split["valid"]["edge_neg"],
)
pos_e = (pos_e_tensor[:, 0], pos_e_tensor[:, 1])
neg_e = (neg_e_tensor[:, 0], neg_e_tensor[:, 1])
self._val_edges = pos_e, neg_e
pos_e_tensor, neg_e_tensor = (
self.edge_split["test"]["edge"],
self.edge_split["test"]["edge_neg"],
)
pos_e = (pos_e_tensor[:, 0], pos_e_tensor[:, 1])
neg_e = (neg_e_tensor[:, 0], neg_e_tensor[:, 1])
self._test_edges = pos_e, neg_e
# delete edge split to save memory
self.edge_split = None
else:
assert self.split_ratio is not None, "Need to specify split_ratio"
assert self.neg_ratio is not None, "Need to specify neg_ratio"
ratio = self.split_ratio
graph = self.dataset[0]
n = graph.num_edges()
src, dst = graph.edges()
src, dst = F.asnumpy(src), F.asnumpy(dst)
n_train, n_val, n_test = (
int(n * ratio[0]),
int(n * ratio[1]),
int(n * ratio[2]),
)
idx = np.random.permutation(n)
train_pos_idx = idx[:n_train]
val_pos_idx = idx[n_train : n_train + n_val]
test_pos_idx = idx[n_train + n_val :]
neg_src, neg_dst = negative_sample(
graph, self.neg_ratio * (n_val + n_test)
)
neg_n_val, neg_n_test = (
self.neg_ratio * n_val,
self.neg_ratio * n_test,
)
neg_val_src, neg_val_dst = neg_src[:neg_n_val], neg_dst[:neg_n_val]
neg_test_src, neg_test_dst = (
neg_src[neg_n_val:],
neg_dst[neg_n_val:],
)
self._val_edges = (
F.tensor(src[val_pos_idx]),
F.tensor(dst[val_pos_idx]),
), (F.tensor(neg_val_src), F.tensor(neg_val_dst))
self._test_edges = (
F.tensor(src[test_pos_idx]),
F.tensor(dst[test_pos_idx]),
), (F.tensor(neg_test_src), F.tensor(neg_test_dst))
self._train_graph = create_dgl_graph(
(src[train_pos_idx], dst[train_pos_idx]),
num_nodes=self.num_nodes,
)
self._train_graph.ndata["feat"] = graph.ndata["feat"]
def has_cache(self):
return os.path.isfile(
os.path.join(self.save_path, "graph_{}.bin".format(self.hash))
)
def load(self):
gs, tensor_dict = utils.load_graphs(
os.path.join(self.save_path, "graph_{}.bin".format(self.hash))
)
self.g = gs[0]
self._train_graph = self.g
self._val_edges = (
tensor_dict["val_pos_src"],
tensor_dict["val_pos_dst"],
), (tensor_dict["val_neg_src"], tensor_dict["val_neg_dst"])
self._test_edges = (
tensor_dict["test_pos_src"],
tensor_dict["test_pos_dst"],
), (tensor_dict["test_neg_src"], tensor_dict["test_neg_dst"])
with open(
os.path.join(self.save_path, "info_{}.json".format(self.hash)), "r"
) as f:
info = json.load(f)
self.split_ratio = info["split_ratio"]
self.neg_ratio = info["neg_ratio"]
def save(self):
tensor_dict = {
"val_pos_src": self._val_edges[0][0],
"val_pos_dst": self._val_edges[0][1],
"val_neg_src": self._val_edges[1][0],
"val_neg_dst": self._val_edges[1][1],
"test_pos_src": self._test_edges[0][0],
"test_pos_dst": self._test_edges[0][1],
"test_neg_src": self._test_edges[1][0],
"test_neg_dst": self._test_edges[1][1],
}
utils.save_graphs(
os.path.join(self.save_path, "graph_{}.bin".format(self.hash)),
[self._train_graph],
tensor_dict,
)
with open(
os.path.join(self.save_path, "info_{}.json".format(self.hash)), "w"
) as f:
json.dump(
{"split_ratio": self.split_ratio, "neg_ratio": self.neg_ratio},
f,
)
@property
def feat_size(self):
return self._train_graph.ndata["feat"].shape[-1]
@property
def train_graph(self):
return self._train_graph
@property
def val_edges(self):
return self._val_edges
@property
def test_edges(self):
return self._test_edges
def __getitem__(self, idx):
return self.g
def __len__(self):
return 1
class AsGraphPredDataset(DGLDataset):
"""Repurpose a dataset for standard graph property prediction task.
The created dataset will include data needed for graph property prediction.
Currently it only supports homogeneous graphs.
The class converts a given dataset into a new dataset object such that:
- It stores ``len(dataset)`` graphs.
- The i-th graph and its label is accessible from ``dataset[i]``.
The class will generate a train/val/test split if :attr:`split_ratio` is provided.
The generated split will be cached to disk for fast re-loading. If the provided split
ratio differs from the cached one, it will re-process the dataset properly.
Parameters
----------
dataset : DGLDataset
The dataset to be converted.
split_ratio : (float, float, float), optional
Split ratios for training, validation and test sets. They must sum to one.
Attributes
----------
num_tasks : int
Number of tasks to predict.
num_classes : int
Number of classes to predict per task, None for regression datasets.
train_idx : Tensor
An 1-D integer tensor of training node IDs.
val_idx : Tensor
An 1-D integer tensor of validation node IDs.
test_idx : Tensor
An 1-D integer tensor of test node IDs.
node_feat_size : int
Input node feature size, None if not applicable.
edge_feat_size : int
Input edge feature size, None if not applicable.
Examples
--------
>>> from dgl.data import AsGraphPredDataset
>>> from ogb.graphproppred import DglGraphPropPredDataset
>>> dataset = DglGraphPropPredDataset(name='ogbg-molhiv')
>>> new_dataset = AsGraphPredDataset(dataset)
>>> print(new_dataset)
Dataset("ogbg-molhiv-as-graphpred", num_graphs=41127, save_path=...)
>>> print(len(new_dataset))
41127
>>> print(new_dataset[0])
(Graph(num_nodes=19, num_edges=40,
ndata_schemes={'feat': Scheme(shape=(9,), dtype=torch.int64)}
edata_schemes={'feat': Scheme(shape=(3,), dtype=torch.int64)}), tensor([0]))
"""
def __init__(self, dataset, split_ratio=None, **kwargs):
self.dataset = dataset
self.split_ratio = split_ratio
super().__init__(
dataset.name + "-as-graphpred",
hash_key=(split_ratio, dataset.name, "graphpred"),
**kwargs
)
def process(self):
is_ogb = hasattr(self.dataset, "get_idx_split")
if self.split_ratio is None:
if is_ogb:
split = self.dataset.get_idx_split()
self.train_idx = split["train"]
self.val_idx = split["valid"]
self.test_idx = split["test"]
else:
# Handle FakeNewsDataset
try:
self.train_idx = F.nonzero_1d(self.dataset.train_mask)
self.val_idx = F.nonzero_1d(self.dataset.val_mask)
self.test_idx = F.nonzero_1d(self.dataset.test_mask)
except:
raise DGLError(
"The input dataset does not have default train/val/test\
split. Please specify split_ratio to generate the split."
)
else:
if self.verbose:
print("Generating train/val/test split...")
train_ratio, val_ratio, _ = self.split_ratio
num_graphs = len(self.dataset)
num_train = int(num_graphs * train_ratio)
num_val = int(num_graphs * val_ratio)
idx = np.random.permutation(num_graphs)
self.train_idx = F.tensor(idx[:num_train])
self.val_idx = F.tensor(idx[num_train : num_train + num_val])
self.test_idx = F.tensor(idx[num_train + num_val :])
if hasattr(self.dataset, "num_classes"):
# GINDataset, MiniGCDataset, FakeNewsDataset, TUDataset,
# LegacyTUDataset, BA2MotifDataset
self.num_classes = self.dataset.num_classes
else:
# None for multi-label classification and regression
self.num_classes = None
if hasattr(self.dataset, "num_tasks"):
# OGB datasets
self.num_tasks = self.dataset.num_tasks
else:
self.num_tasks = 1
def has_cache(self):
return os.path.isfile(
os.path.join(self.save_path, "info_{}.json".format(self.hash))
)
def load(self):
with open(
os.path.join(self.save_path, "info_{}.json".format(self.hash)), "r"
) as f:
info = json.load(f)
if info["split_ratio"] != self.split_ratio:
raise ValueError(
"Provided split ratio is different from the cached file. "
"Re-process the dataset."
)
self.split_ratio = info["split_ratio"]
self.num_tasks = info["num_tasks"]
self.num_classes = info["num_classes"]
split = np.load(
os.path.join(self.save_path, "split_{}.npz".format(self.hash))
)
self.train_idx = F.zerocopy_from_numpy(split["train_idx"])
self.val_idx = F.zerocopy_from_numpy(split["val_idx"])
self.test_idx = F.zerocopy_from_numpy(split["test_idx"])
def save(self):
if not os.path.exists(self.save_path):
os.makedirs(self.save_path)
with open(
os.path.join(self.save_path, "info_{}.json".format(self.hash)), "w"
) as f:
json.dump(
{
"split_ratio": self.split_ratio,
"num_tasks": self.num_tasks,
"num_classes": self.num_classes,
},
f,
)
np.savez(
os.path.join(self.save_path, "split_{}.npz".format(self.hash)),
train_idx=F.zerocopy_to_numpy(self.train_idx),
val_idx=F.zerocopy_to_numpy(self.val_idx),
test_idx=F.zerocopy_to_numpy(self.test_idx),
)
def __getitem__(self, idx):
return self.dataset[idx]
def __len__(self):
return len(self.dataset)
@property
def node_feat_size(self):
g = self[0][0]
return g.ndata["feat"].shape[-1] if "feat" in g.ndata else None
@property
def edge_feat_size(self):
g = self[0][0]
return g.edata["feat"].shape[-1] if "feat" in g.edata else None
+191
View File
@@ -0,0 +1,191 @@
""" BitcoinOTC dataset for fraud detection """
import datetime
import gzip
import os
import shutil
import numpy as np
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import check_sha1, download, load_graphs, makedirs, save_graphs
class BitcoinOTCDataset(DGLBuiltinDataset):
r"""BitcoinOTC dataset for fraud detection
This is who-trusts-whom network of people who trade using Bitcoin on
a platform called Bitcoin OTC. Since Bitcoin users are anonymous,
there is a need to maintain a record of users' reputation to prevent
transactions with fraudulent and risky users.
Offical website: `<https://snap.stanford.edu/data/soc-sign-bitcoin-otc.html>`_
Bitcoin OTC dataset statistics:
- Nodes: 5,881
- Edges: 35,592
- Range of edge weight: -10 to +10
- Percentage of positive edges: 89%
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose: bool
Whether to print out progress information.
Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
graphs : list
A list of DGLGraph objects
is_temporal : bool
Indicate whether the graphs are temporal graphs
Raises
------
UserWarning
If the raw data is changed in the remote server by the author.
Examples
--------
>>> dataset = BitcoinOTCDataset()
>>> len(dataset)
136
>>> for g in dataset:
.... # get edge feature
.... edge_weights = g.edata['h']
.... # your code here
>>>
"""
_url = "https://snap.stanford.edu/data/soc-sign-bitcoinotc.csv.gz"
_sha1_str = "c14281f9e252de0bd0b5f1c6e2bae03123938641"
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(BitcoinOTCDataset, self).__init__(
name="bitcoinotc",
url=self._url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def download(self):
gz_file_path = os.path.join(self.raw_dir, self.name + ".csv.gz")
download(self.url, path=gz_file_path)
if not check_sha1(gz_file_path, self._sha1_str):
raise UserWarning(
"File {} is downloaded but the content hash does not match."
"The repo may be outdated or download may be incomplete. "
"Otherwise you can create an issue for it.".format(
self.name + ".csv.gz"
)
)
self._extract_gz(gz_file_path, self.raw_path)
def process(self):
filename = os.path.join(self.save_path, self.name + ".csv")
data = np.loadtxt(filename, delimiter=",").astype(np.int64)
data[:, 0:2] = data[:, 0:2] - data[:, 0:2].min()
delta = datetime.timedelta(days=14).total_seconds()
# The source code is not released, but the paper indicates there're
# totally 137 samples. The cutoff below has exactly 137 samples.
time_index = np.around((data[:, 3] - data[:, 3].min()) / delta).astype(
np.int64
)
self._graphs = []
for i in range(time_index.max()):
row_mask = time_index <= i
edges = data[row_mask][:, 0:2]
rate = data[row_mask][:, 2]
g = dgl_graph((edges[:, 0], edges[:, 1]))
g.edata["h"] = F.tensor(
rate.reshape(-1, 1), dtype=F.data_type_dict["int64"]
)
self._graphs.append(g)
@property
def graph_path(self):
return os.path.join(self.save_path, "dgl_graph.bin")
def has_cache(self):
return os.path.exists(self.graph_path)
def save(self):
save_graphs(self.graph_path, self.graphs)
def load(self):
self._graphs = load_graphs(self.graph_path)[0]
@property
def graphs(self):
return self._graphs
def __len__(self):
r"""Number of graphs in the dataset.
Return
-------
int
"""
return len(self.graphs)
def __getitem__(self, item):
r"""Get graph by index
Parameters
----------
item : int
Item index
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``edata['h']`` : edge weights
"""
if self._transform is None:
return self.graphs[item]
else:
return self._transform(self.graphs[item])
@property
def is_temporal(self):
r"""Are the graphs temporal graphs
Returns
-------
bool
"""
return True
def _extract_gz(self, file, target_dir, overwrite=False):
if os.path.exists(target_dir) and not overwrite:
return
print("Extracting file to {}".format(target_dir))
fname = os.path.basename(file)
makedirs(target_dir)
out_file_path = os.path.join(target_dir, fname[:-3])
with gzip.open(file, "rb") as f_in:
with open(out_file_path, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
BitcoinOTC = BitcoinOTCDataset
+953
View File
@@ -0,0 +1,953 @@
"""Cora, citeseer, pubmed dataset.
(lingfan): following dataset loading and preprocessing code from tkipf/gcn
https://github.com/tkipf/gcn/blob/master/gcn/utils.py
"""
from __future__ import absolute_import
import os, sys
import pickle as pkl
import warnings
import networkx as nx
import numpy as np
import scipy.sparse as sp
from .. import backend as F, convert
from ..batch import batch as batch_graphs
from ..convert import from_networkx, graph as dgl_graph, to_networkx
from ..transforms import reorder_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import (
_get_dgl_url,
deprecate_function,
deprecate_property,
generate_mask_tensor,
load_graphs,
load_info,
makedirs,
save_graphs,
save_info,
)
backend = os.environ.get("DGLBACKEND", "pytorch")
def _pickle_load(pkl_file):
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
if sys.version_info > (3, 0):
return pkl.load(pkl_file, encoding="latin1")
else:
return pkl.load(pkl_file)
class CitationGraphDataset(DGLBuiltinDataset):
r"""The citation graph dataset, including cora, citeseer and pubmeb.
Nodes mean authors and edges mean citation relationships.
Parameters
-----------
name: str
name can be 'cora', 'citeseer' or 'pubmed'.
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
reorder : bool
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
"""
_urls = {
"cora_v2": "dataset/cora_v2.zip",
"citeseer": "dataset/citeseer.zip",
"pubmed": "dataset/pubmed.zip",
}
def __init__(
self,
name,
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
reorder=False,
):
assert name.lower() in ["cora", "citeseer", "pubmed"]
# Previously we use the pre-processing in pygcn (https://github.com/tkipf/pygcn)
# for Cora, which is slightly different from the one used in the GCN paper
if name.lower() == "cora":
name = "cora_v2"
url = _get_dgl_url(self._urls[name])
self._reverse_edge = reverse_edge
self._reorder = reorder
super(CitationGraphDataset, self).__init__(
name,
url=url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""Loads input data from data directory and reorder graph for better locality
ind.name.x => the feature vectors of the training instances as scipy.sparse.csr.csr_matrix object;
ind.name.tx => the feature vectors of the test instances as scipy.sparse.csr.csr_matrix object;
ind.name.allx => the feature vectors of both labeled and unlabeled training instances
(a superset of ind.name.x) as scipy.sparse.csr.csr_matrix object;
ind.name.y => the one-hot labels of the labeled training instances as numpy.ndarray object;
ind.name.ty => the one-hot labels of the test instances as numpy.ndarray object;
ind.name.ally => the labels for instances in ind.name.allx as numpy.ndarray object;
ind.name.graph => a dict in the format {index: [index_of_neighbor_nodes]} as collections.defaultdict
object;
ind.name.test.index => the indices of test instances in graph, for the inductive setting as list object.
"""
root = self.raw_path
objnames = ["x", "y", "tx", "ty", "allx", "ally", "graph"]
objects = []
for i in range(len(objnames)):
with open(
"{}/ind.{}.{}".format(root, self.name, objnames[i]), "rb"
) as f:
objects.append(_pickle_load(f))
x, y, tx, ty, allx, ally, graph = tuple(objects)
test_idx_reorder = _parse_index_file(
"{}/ind.{}.test.index".format(root, self.name)
)
test_idx_range = np.sort(test_idx_reorder)
if self.name == "citeseer":
# Fix citeseer dataset (there are some isolated nodes in the graph)
# Find isolated nodes, add them as zero-vecs into the right position
test_idx_range_full = range(
min(test_idx_reorder), max(test_idx_reorder) + 1
)
tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1]))
tx_extended[test_idx_range - min(test_idx_range), :] = tx
tx = tx_extended
ty_extended = np.zeros((len(test_idx_range_full), y.shape[1]))
ty_extended[test_idx_range - min(test_idx_range), :] = ty
ty = ty_extended
features = sp.vstack((allx, tx)).tolil()
features[test_idx_reorder, :] = features[test_idx_range, :]
if self.reverse_edge:
graph = nx.DiGraph(nx.from_dict_of_lists(graph))
g = from_networkx(graph)
else:
graph = nx.Graph(nx.from_dict_of_lists(graph))
edges = list(graph.edges())
u, v = map(list, zip(*edges))
g = dgl_graph((u, v))
onehot_labels = np.vstack((ally, ty))
onehot_labels[test_idx_reorder, :] = onehot_labels[test_idx_range, :]
labels = np.argmax(onehot_labels, 1)
idx_test = test_idx_range.tolist()
idx_train = range(len(y))
idx_val = range(len(y), len(y) + 500)
train_mask = generate_mask_tensor(
_sample_mask(idx_train, labels.shape[0])
)
val_mask = generate_mask_tensor(_sample_mask(idx_val, labels.shape[0]))
test_mask = generate_mask_tensor(
_sample_mask(idx_test, labels.shape[0])
)
g.ndata["train_mask"] = train_mask
g.ndata["val_mask"] = val_mask
g.ndata["test_mask"] = test_mask
g.ndata["label"] = F.tensor(labels)
g.ndata["feat"] = F.tensor(
_preprocess_features(features), dtype=F.data_type_dict["float32"]
)
self._num_classes = onehot_labels.shape[1]
self._labels = labels
if self._reorder:
self._g = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
else:
self._g = g
if self.verbose:
print("Finished data loading and preprocessing.")
print(" NumNodes: {}".format(self._g.num_nodes()))
print(" NumEdges: {}".format(self._g.num_edges()))
print(" NumFeats: {}".format(self._g.ndata["feat"].shape[1]))
print(" NumClasses: {}".format(self.num_classes))
print(
" NumTrainingSamples: {}".format(
F.nonzero_1d(self._g.ndata["train_mask"]).shape[0]
)
)
print(
" NumValidationSamples: {}".format(
F.nonzero_1d(self._g.ndata["val_mask"]).shape[0]
)
)
print(
" NumTestSamples: {}".format(
F.nonzero_1d(self._g.ndata["test_mask"]).shape[0]
)
)
@property
def graph_path(self):
return os.path.join(self.save_path, self.save_name + ".bin")
@property
def info_path(self):
return os.path.join(self.save_path, self.save_name + ".pkl")
def has_cache(self):
if os.path.exists(self.graph_path) and os.path.exists(self.info_path):
return True
return False
def save(self):
"""save the graph list and the labels"""
save_graphs(str(self.graph_path), self._g)
save_info(str(self.info_path), {"num_classes": self.num_classes})
def load(self):
graphs, _ = load_graphs(str(self.graph_path))
info = load_info(str(self.info_path))
graph = graphs[0]
self._g = graph
# for compatability
graph = graph.clone()
graph.ndata.pop("train_mask")
graph.ndata.pop("val_mask")
graph.ndata.pop("test_mask")
graph.ndata.pop("feat")
graph.ndata.pop("label")
graph = to_networkx(graph)
self._num_classes = info["num_classes"]
self._g.ndata["train_mask"] = generate_mask_tensor(
F.asnumpy(self._g.ndata["train_mask"])
)
self._g.ndata["val_mask"] = generate_mask_tensor(
F.asnumpy(self._g.ndata["val_mask"])
)
self._g.ndata["test_mask"] = generate_mask_tensor(
F.asnumpy(self._g.ndata["test_mask"])
)
# hack for mxnet compatability
if self.verbose:
print(" NumNodes: {}".format(self._g.num_nodes()))
print(" NumEdges: {}".format(self._g.num_edges()))
print(" NumFeats: {}".format(self._g.ndata["feat"].shape[1]))
print(" NumClasses: {}".format(self.num_classes))
print(
" NumTrainingSamples: {}".format(
F.nonzero_1d(self._g.ndata["train_mask"]).shape[0]
)
)
print(
" NumValidationSamples: {}".format(
F.nonzero_1d(self._g.ndata["val_mask"]).shape[0]
)
)
print(
" NumTestSamples: {}".format(
F.nonzero_1d(self._g.ndata["test_mask"]).shape[0]
)
)
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._g
else:
return self._transform(self._g)
def __len__(self):
return 1
@property
def save_name(self):
return self.name + "_dgl_graph"
@property
def num_labels(self):
deprecate_property("dataset.num_labels", "dataset.num_classes")
return self.num_classes
@property
def num_classes(self):
return self._num_classes
""" Citation graph is used in many examples
We preserve these properties for compatability.
"""
@property
def reverse_edge(self):
return self._reverse_edge
def _preprocess_features(features):
"""Row-normalize feature matrix and convert to tuple representation"""
features = _normalize(features)
return np.asarray(features.todense())
def _parse_index_file(filename):
"""Parse index file."""
index = []
for line in open(filename):
index.append(int(line.strip()))
return index
def _sample_mask(idx, l):
"""Create mask."""
mask = np.zeros(l)
mask[idx] = 1
return mask
class CoraGraphDataset(CitationGraphDataset):
r"""Cora citation network dataset.
Nodes mean paper and edges mean citation
relationships. Each node has a predefined
feature with 1433 dimensions. The dataset is
designed for the node classification task.
The task is to predict the category of
certain paper.
Statistics:
- Nodes: 2708
- Edges: 10556
- Number of Classes: 7
- Label split:
- Train: 140
- Valid: 500
- Test: 1000
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
reorder : bool
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
Attributes
----------
num_classes: int
Number of label classes
Notes
-----
The node feature is row-normalized.
Examples
--------
>>> dataset = CoraGraphDataset()
>>> g = dataset[0]
>>> num_class = dataset.num_classes
>>>
>>> # get node feature
>>> feat = g.ndata['feat']
>>>
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> test_mask = g.ndata['test_mask']
>>>
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self,
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
reorder=False,
):
name = "cora"
super(CoraGraphDataset, self).__init__(
name,
raw_dir,
force_reload,
verbose,
reverse_edge,
transform,
reorder,
)
def __getitem__(self, idx):
r"""Gets the graph object
Parameters
-----------
idx: int
Item index, CoraGraphDataset has only one graph object
Return
------
:class:`dgl.DGLGraph`
graph structure, node features and labels.
- ``ndata['train_mask']``: mask for training node set
- ``ndata['val_mask']``: mask for validation node set
- ``ndata['test_mask']``: mask for test node set
- ``ndata['feat']``: node feature
- ``ndata['label']``: ground truth labels
"""
return super(CoraGraphDataset, self).__getitem__(idx)
def __len__(self):
r"""The number of graphs in the dataset."""
return super(CoraGraphDataset, self).__len__()
class CiteseerGraphDataset(CitationGraphDataset):
r"""Citeseer citation network dataset.
Nodes mean scientific publications and edges
mean citation relationships. Each node has a
predefined feature with 3703 dimensions. The
dataset is designed for the node classification
task. The task is to predict the category of
certain publication.
Statistics:
- Nodes: 3327
- Edges: 9228
- Number of Classes: 6
- Label Split:
- Train: 120
- Valid: 500
- Test: 1000
Parameters
-----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
reorder : bool
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
Attributes
----------
num_classes: int
Number of label classes
Notes
-----
The node feature is row-normalized.
In citeseer dataset, there are some isolated nodes in the graph.
These isolated nodes are added as zero-vecs into the right position.
Examples
--------
>>> dataset = CiteseerGraphDataset()
>>> g = dataset[0]
>>> num_class = dataset.num_classes
>>>
>>> # get node feature
>>> feat = g.ndata['feat']
>>>
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> test_mask = g.ndata['test_mask']
>>>
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self,
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
reorder=False,
):
name = "citeseer"
super(CiteseerGraphDataset, self).__init__(
name,
raw_dir,
force_reload,
verbose,
reverse_edge,
transform,
reorder,
)
def __getitem__(self, idx):
r"""Gets the graph object
Parameters
-----------
idx: int
Item index, CiteseerGraphDataset has only one graph object
Return
------
:class:`dgl.DGLGraph`
graph structure, node features and labels.
- ``ndata['train_mask']``: mask for training node set
- ``ndata['val_mask']``: mask for validation node set
- ``ndata['test_mask']``: mask for test node set
- ``ndata['feat']``: node feature
- ``ndata['label']``: ground truth labels
"""
return super(CiteseerGraphDataset, self).__getitem__(idx)
def __len__(self):
r"""The number of graphs in the dataset."""
return super(CiteseerGraphDataset, self).__len__()
class PubmedGraphDataset(CitationGraphDataset):
r"""Pubmed citation network dataset.
Nodes mean scientific publications and edges
mean citation relationships. Each node has a
predefined feature with 500 dimensions. The
dataset is designed for the node classification
task. The task is to predict the category of
certain publication.
Statistics:
- Nodes: 19717
- Edges: 88651
- Number of Classes: 3
- Label Split:
- Train: 60
- Valid: 500
- Test: 1000
Parameters
-----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
reorder : bool
Whether to reorder the graph using :func:`~dgl.reorder_graph`. Default: False.
Attributes
----------
num_classes: int
Number of label classes
Notes
-----
The node feature is row-normalized.
Examples
--------
>>> dataset = PubmedGraphDataset()
>>> g = dataset[0]
>>> num_class = dataset.num_of_class
>>>
>>> # get node feature
>>> feat = g.ndata['feat']
>>>
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> test_mask = g.ndata['test_mask']
>>>
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self,
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
reorder=False,
):
name = "pubmed"
super(PubmedGraphDataset, self).__init__(
name,
raw_dir,
force_reload,
verbose,
reverse_edge,
transform,
reorder,
)
def __getitem__(self, idx):
r"""Gets the graph object
Parameters
-----------
idx: int
Item index, PubmedGraphDataset has only one graph object
Return
------
:class:`dgl.DGLGraph`
graph structure, node features and labels.
- ``ndata['train_mask']``: mask for training node set
- ``ndata['val_mask']``: mask for validation node set
- ``ndata['test_mask']``: mask for test node set
- ``ndata['feat']``: node feature
- ``ndata['label']``: ground truth labels
"""
return super(PubmedGraphDataset, self).__getitem__(idx)
def __len__(self):
r"""The number of graphs in the dataset."""
return super(PubmedGraphDataset, self).__len__()
def load_cora(
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
):
"""Get CoraGraphDataset
Parameters
-----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Return
-------
CoraGraphDataset
"""
data = CoraGraphDataset(
raw_dir, force_reload, verbose, reverse_edge, transform
)
return data
def load_citeseer(
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
):
"""Get CiteseerGraphDataset
Parameters
-----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Return
-------
CiteseerGraphDataset
"""
data = CiteseerGraphDataset(
raw_dir, force_reload, verbose, reverse_edge, transform
)
return data
def load_pubmed(
raw_dir=None,
force_reload=False,
verbose=True,
reverse_edge=True,
transform=None,
):
"""Get PubmedGraphDataset
Parameters
-----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
reverse_edge : bool
Whether to add reverse edges in graph. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Return
-------
PubmedGraphDataset
"""
data = PubmedGraphDataset(
raw_dir, force_reload, verbose, reverse_edge, transform
)
return data
class CoraBinary(DGLBuiltinDataset):
"""A mini-dataset for binary classification task using Cora.
After loaded, it has following members:
graphs : list of :class:`~dgl.DGLGraph`
pmpds : list of :class:`scipy.sparse.coo_matrix`
labels : list of :class:`numpy.ndarray`
Parameters
-----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose: bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
name = "cora_binary"
url = _get_dgl_url("dataset/cora_binary.zip")
super(CoraBinary, self).__init__(
name,
url=url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
root = self.raw_path
# load graphs
self.graphs = []
with open("{}/graphs.txt".format(root), "r") as f:
elist = []
for line in f.readlines():
if line.startswith("graph"):
if len(elist) != 0:
self.graphs.append(dgl_graph(tuple(zip(*elist))))
elist = []
else:
u, v = line.strip().split(" ")
elist.append((int(u), int(v)))
if len(elist) != 0:
self.graphs.append(dgl_graph(tuple(zip(*elist))))
with open("{}/pmpds.pkl".format(root), "rb") as f:
self.pmpds = _pickle_load(f)
self.labels = []
with open("{}/labels.txt".format(root), "r") as f:
cur = []
for line in f.readlines():
if line.startswith("graph"):
if len(cur) != 0:
self.labels.append(np.asarray(cur))
cur = []
else:
cur.append(int(line.strip()))
if len(cur) != 0:
self.labels.append(np.asarray(cur))
# sanity check
assert len(self.graphs) == len(self.pmpds)
assert len(self.graphs) == len(self.labels)
@property
def graph_path(self):
return os.path.join(self.save_path, self.save_name + ".bin")
def has_cache(self):
if os.path.exists(self.graph_path):
return True
return False
def save(self):
"""save the graph list and the labels"""
labels = {}
for i, label in enumerate(self.labels):
labels["{}".format(i)] = F.tensor(label)
save_graphs(str(self.graph_path), self.graphs, labels)
if self.verbose:
print("Done saving data into cached files.")
def load(self):
self.graphs, labels = load_graphs(str(self.graph_path))
self.labels = []
for i in range(len(labels)):
self.labels.append(F.asnumpy(labels["{}".format(i)]))
# load pmpds under self.raw_path
with open("{}/pmpds.pkl".format(self.raw_path), "rb") as f:
self.pmpds = _pickle_load(f)
if self.verbose:
print("Done loading data into cached files.")
# sanity check
assert len(self.graphs) == len(self.pmpds)
assert len(self.graphs) == len(self.labels)
def __len__(self):
return len(self.graphs)
def __getitem__(self, i):
r"""Gets the idx-th sample.
Parameters
-----------
idx : int
The sample index.
Returns
-------
(dgl.DGLGraph, scipy.sparse.coo_matrix, int)
The graph, scipy sparse coo_matrix and its label.
"""
if self._transform is None:
g = self.graphs[i]
else:
g = self._transform(self.graphs[i])
return (g, self.pmpds[i], self.labels[i])
@property
def save_name(self):
return self.name + "_dgl_graph"
@staticmethod
def collate_fn(cur):
graphs, pmpds, labels = zip(*cur)
batched_graphs = batch_graphs(graphs)
batched_pmpds = sp.block_diag(pmpds)
batched_labels = np.concatenate(labels, axis=0)
return batched_graphs, batched_pmpds, batched_labels
def _normalize(mx):
"""Row-normalize sparse matrix"""
rowsum = np.asarray(mx.sum(1))
mask = np.equal(rowsum, 0.0).flatten()
rowsum[mask] = np.nan
r_inv = np.power(rowsum, -1).flatten()
r_inv[mask] = 0.0
r_mat_inv = sp.diags(r_inv)
return r_mat_inv.dot(mx)
def _encode_onehot(labels):
classes = list(sorted(set(labels)))
classes_dict = {
c: np.identity(len(classes))[i, :] for i, c in enumerate(classes)
}
labels_onehot = np.asarray(
list(map(classes_dict.get, labels)), dtype=np.int32
)
return labels_onehot
+132
View File
@@ -0,0 +1,132 @@
""" CLUSTERDataset for inductive learning. """
import os
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs
class CLUSTERDataset(DGLBuiltinDataset):
r"""CLUSTER dataset for semi-supervised clustering task.
Each graph contains 6 SBM clusters with sizes randomly selected between
[5, 35] and probabilities p = 0.55, q = 0.25. The graphs are of sizes 40
-190 nodes. Each node can take an input feature value in {0, 1, 2, ..., 6}
and values 1~6 correspond to classes 0~5 respectively, while value 0 means
that the class of the node is unknown. There is only one labeled node that
is randomly assigned to each community and most node features are set to 0.
Reference `<https://arxiv.org/pdf/2003.00982.pdf>`_
Statistics:
- Train examples: 10,000
- Valid examples: 1,000
- Test examples: 1,000
- Number of classes for each node: 6
Parameters
----------
mode : str
Must be one of ('train', 'valid', 'test').
Default: 'train'
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose : bool
Whether to print out progress information.
Default: False
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> from dgl.data import CLUSTERDataset
>>>
>>> trainset = CLUSTERDataset(mode='train')
>>>
>>> trainset.num_classes
6
>>> len(trainset)
10000
>>> trainset[0]
Graph(num_nodes=117, num_edges=4104,
ndata_schemes={'label': Scheme(shape=(), dtype=torch.int16),
'feat': Scheme(shape=(), dtype=torch.int64)}
edata_schemes={'feat': Scheme(shape=(1,), dtype=torch.float32)})
"""
def __init__(
self,
mode="train",
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
self._url = _get_dgl_url("dataset/SBM_CLUSTER.zip")
self.mode = mode
super(CLUSTERDataset, self).__init__(
name="cluster",
url=self._url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
self.load()
def has_cache(self):
graph_path = os.path.join(
self.save_path, "CLUSTER_{}.bin".format(self.mode)
)
return os.path.exists(graph_path)
def load(self):
graph_path = os.path.join(
self.save_path, "CLUSTER_{}.bin".format(self.mode)
)
self._graphs, _ = load_graphs(graph_path)
@property
def num_classes(self):
r"""Number of classes for each node."""
return 6
def __len__(self):
r"""The number of examples in the dataset."""
return len(self._graphs)
def __getitem__(self, idx):
r"""Get the idx^th sample.
Parameters
---------
idx : int
The sample index.
Returns
-------
:class:`dgl.DGLGraph`
graph structure, node features, node labels and edge features.
- ``ndata['feat']``: node features
- ``ndata['label']``: node labels
- ``edata['feat']``: edge features
"""
if self._transform is None:
return self._graphs[idx]
else:
return self._transform(self._graphs[idx])
+214
View File
@@ -0,0 +1,214 @@
import os
import numpy as np
from .. import backend as F
from ..base import DGLError
from .dgl_dataset import DGLDataset
from .utils import load_graphs, save_graphs, Subset
class CSVDataset(DGLDataset):
"""Dataset class that loads and parses graph data from CSV files.
This class requires the following additional packages:
- pyyaml >= 5.4.1
- pandas >= 1.1.5
- pydantic >= 1.9.0
The parsed graph and feature data will be cached for faster reloading. If
the source CSV files are modified, please specify ``force_reload=True``
to re-parse from them.
Parameters
----------
data_path : str
Directory which contains 'meta.yaml' and CSV files
force_reload : bool, optional
Whether to reload the dataset. Default: False
verbose: bool, optional
Whether to print out progress information. Default: True.
ndata_parser : dict[str, callable] or callable, optional
Callable object which takes in the ``pandas.DataFrame`` object created from
CSV file, parses node data and returns a dictionary of parsed data. If given a
dictionary, the key is node type and the value is a callable object which is
used to parse data of corresponding node type. If given a single callable
object, such object is used to parse data of all node type data. Default: None.
If None, a default data parser is applied which load data directly and tries to
convert list into array.
edata_parser : dict[(str, str, str), callable], or callable, optional
Callable object which takes in the ``pandas.DataFrame`` object created from
CSV file, parses edge data and returns a dictionary of parsed data. If given a
dictionary, the key is edge type and the value is a callable object which is
used to parse data of corresponding edge type. If given a single callable
object, such object is used to parse data of all edge type data. Default: None.
If None, a default data parser is applied which load data directly and tries to
convert list into array.
gdata_parser : callable, optional
Callable object which takes in the ``pandas.DataFrame`` object created from
CSV file, parses graph data and returns a dictionary of parsed data. Default:
None. If None, a default data parser is applied which load data directly and
tries to convert list into array.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
graphs : :class:`dgl.DGLGraph`
Graphs of the dataset
data : dict
any available graph-level data such as graph-level feature, labels.
Examples
--------
Please refer to :ref:`guide-data-pipeline-loadcsv`.
"""
META_YAML_NAME = "meta.yaml"
def __init__(
self,
data_path,
force_reload=False,
verbose=True,
ndata_parser=None,
edata_parser=None,
gdata_parser=None,
transform=None,
):
from .csv_dataset_base import (
DefaultDataParser,
load_yaml_with_sanity_check,
)
self.graphs = None
self.data = None
self.ndata_parser = {} if ndata_parser is None else ndata_parser
self.edata_parser = {} if edata_parser is None else edata_parser
self.gdata_parser = gdata_parser
self.default_data_parser = DefaultDataParser()
meta_yaml_path = os.path.join(data_path, CSVDataset.META_YAML_NAME)
if not os.path.exists(meta_yaml_path):
raise DGLError(
"'{}' cannot be found under {}.".format(
CSVDataset.META_YAML_NAME, data_path
)
)
self.meta_yaml = load_yaml_with_sanity_check(meta_yaml_path)
ds_name = self.meta_yaml.dataset_name
super().__init__(
ds_name,
raw_dir=os.path.dirname(meta_yaml_path),
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""Parse node/edge data from CSV files and construct DGL.Graphs"""
from .csv_dataset_base import (
DGLGraphConstructor,
EdgeData,
GraphData,
NodeData,
)
meta_yaml = self.meta_yaml
base_dir = self.raw_dir
node_data = []
for meta_node in meta_yaml.node_data:
if meta_node is None:
continue
ntype = meta_node.ntype
data_parser = (
self.ndata_parser
if callable(self.ndata_parser)
else self.ndata_parser.get(ntype, self.default_data_parser)
)
ndata = NodeData.load_from_csv(
meta_node,
base_dir=base_dir,
separator=meta_yaml.separator,
data_parser=data_parser,
)
node_data.append(ndata)
edge_data = []
for meta_edge in meta_yaml.edge_data:
if meta_edge is None:
continue
etype = tuple(meta_edge.etype)
data_parser = (
self.edata_parser
if callable(self.edata_parser)
else self.edata_parser.get(etype, self.default_data_parser)
)
edata = EdgeData.load_from_csv(
meta_edge,
base_dir=base_dir,
separator=meta_yaml.separator,
data_parser=data_parser,
)
edge_data.append(edata)
graph_data = None
if meta_yaml.graph_data is not None:
meta_graph = meta_yaml.graph_data
data_parser = (
self.default_data_parser
if self.gdata_parser is None
else self.gdata_parser
)
graph_data = GraphData.load_from_csv(
meta_graph,
base_dir=base_dir,
separator=meta_yaml.separator,
data_parser=data_parser,
)
# construct graphs
self.graphs, self.data = DGLGraphConstructor.construct_graphs(
node_data, edge_data, graph_data
)
if len(self.data) == 1:
self.labels = list(self.data.values())[0]
def has_cache(self):
graph_path = os.path.join(self.save_path, self.name + ".bin")
if os.path.exists(graph_path):
return True
return False
def save(self):
if self.graphs is None:
raise DGLError("No graphs available in dataset")
graph_path = os.path.join(self.save_path, self.name + ".bin")
save_graphs(graph_path, self.graphs, labels=self.data)
def load(self):
graph_path = os.path.join(self.save_path, self.name + ".bin")
self.graphs, self.data = load_graphs(graph_path)
if len(self.data) == 1:
self.labels = list(self.data.values())[0]
def __getitem__(self, i):
if F.is_tensor(i) and F.ndim(i) == 1:
return Subset(self, F.copy_to(i, F.cpu()))
if self._transform is None:
g = self.graphs[i]
else:
g = self._transform(self.graphs[i])
if len(self.data) == 1:
return g, self.labels[i]
elif len(self.data) > 0:
data = {k: v[i] for (k, v) in self.data.items()}
return g, data
else:
return g
def __len__(self):
return len(self.graphs)
+386
View File
@@ -0,0 +1,386 @@
import ast
import os
from typing import Callable, List, Optional
import numpy as np
import pandas as pd
import pydantic as dt
import yaml
from .. import backend as F
from ..base import dgl_warning, DGLError
from ..convert import heterograph as dgl_heterograph
class MetaNode(dt.BaseModel):
"""Class of node_data in YAML. Internal use only."""
file_name: str
ntype: Optional[str] = "_V"
graph_id_field: Optional[str] = "graph_id"
node_id_field: Optional[str] = "node_id"
class MetaEdge(dt.BaseModel):
"""Class of edge_data in YAML. Internal use only."""
file_name: str
etype: Optional[List[str]] = ["_V", "_E", "_V"]
graph_id_field: Optional[str] = "graph_id"
src_id_field: Optional[str] = "src_id"
dst_id_field: Optional[str] = "dst_id"
class MetaGraph(dt.BaseModel):
"""Class of graph_data in YAML. Internal use only."""
file_name: str
graph_id_field: Optional[str] = "graph_id"
class MetaYaml(dt.BaseModel):
"""Class of YAML. Internal use only."""
version: Optional[str] = "1.0.0"
dataset_name: str
separator: Optional[str] = ","
node_data: List[MetaNode]
edge_data: List[MetaEdge]
graph_data: Optional[MetaGraph] = None
def load_yaml_with_sanity_check(yaml_file):
"""Load yaml and do sanity check. Internal use only."""
with open(yaml_file) as f:
yaml_data = yaml.load(f, Loader=yaml.loader.SafeLoader)
try:
meta_yaml = MetaYaml(**yaml_data)
except dt.ValidationError as e:
print("Details of pydantic.ValidationError:\n{}".format(e.json()))
raise DGLError(
"Validation Error for YAML fields. Details are shown above."
)
if meta_yaml.version != "1.0.0":
raise DGLError(
"Invalid CSVDataset version {}. Supported versions: '1.0.0'".format(
meta_yaml.version
)
)
ntypes = [meta.ntype for meta in meta_yaml.node_data]
if len(ntypes) > len(set(ntypes)):
raise DGLError(
"Each node CSV file must have a unique node type name, but found duplicate node type: {}.".format(
ntypes
)
)
etypes = [tuple(meta.etype) for meta in meta_yaml.edge_data]
if len(etypes) > len(set(etypes)):
raise DGLError(
"Each edge CSV file must have a unique edge type name, but found duplicate edge type: {}.".format(
etypes
)
)
return meta_yaml
def _validate_data_length(data_dict):
len_dict = {k: len(v) for k, v in data_dict.items()}
lst = list(len_dict.values())
res = lst.count(lst[0]) == len(lst)
if not res:
raise DGLError(
"All data are required to have same length while some of them does not. Length of data={}".format(
str(len_dict)
)
)
def _tensor(data, dtype=None):
"""Float32 is the default dtype for float tensor in DGL
so let's cast float64 into float32 to avoid dtype mismatch.
"""
ret = F.tensor(data, dtype)
if F.dtype(ret) == F.float64:
ret = F.tensor(ret, dtype=F.float32)
return ret
class BaseData:
"""Class of base data which is inherited by Node/Edge/GraphData. Internal use only."""
@staticmethod
def read_csv(file_name, base_dir, separator):
csv_path = file_name
if base_dir is not None:
csv_path = os.path.join(base_dir, csv_path)
return pd.read_csv(csv_path, sep=separator)
@staticmethod
def pop_from_dataframe(df: pd.DataFrame, item: str):
ret = None
try:
ret = df.pop(item).to_numpy().squeeze()
except KeyError:
pass
return ret
class NodeData(BaseData):
"""Class of node data which is used for DGLGraph construction. Internal use only."""
def __init__(self, node_id, data, type=None, graph_id=None):
self.id = np.array(node_id)
self.data = data
self.type = type if type is not None else "_V"
self.graph_id = (
np.array(graph_id)
if graph_id is not None
else np.full(len(node_id), 0)
)
_validate_data_length(
{**{"id": self.id, "graph_id": self.graph_id}, **self.data}
)
@staticmethod
def load_from_csv(
meta: MetaNode, data_parser: Callable, base_dir=None, separator=","
):
df = BaseData.read_csv(meta.file_name, base_dir, separator)
node_ids = BaseData.pop_from_dataframe(df, meta.node_id_field)
graph_ids = BaseData.pop_from_dataframe(df, meta.graph_id_field)
if node_ids is None:
raise DGLError(
"Missing node id field [{}] in file [{}].".format(
meta.node_id_field, meta.file_name
)
)
ntype = meta.ntype
ndata = data_parser(df)
return NodeData(node_ids, ndata, type=ntype, graph_id=graph_ids)
@staticmethod
def to_dict(node_data: List["NodeData"]) -> dict:
# node_ids could be numeric or non-numeric values, but duplication is not allowed.
node_dict = {}
for n_data in node_data:
graph_ids = np.unique(n_data.graph_id)
for graph_id in graph_ids:
idx = n_data.graph_id == graph_id
ids = n_data.id[idx]
u_ids, u_indices, u_counts = np.unique(
ids, return_index=True, return_counts=True
)
if len(ids) > len(u_ids):
raise DGLError(
"Node IDs are required to be unique but the following ids are duplicate: {}".format(
u_ids[u_counts > 1]
)
)
if graph_id not in node_dict:
node_dict[graph_id] = {}
node_dict[graph_id][n_data.type] = {
"mapping": {
index: i for i, index in enumerate(ids[u_indices])
},
"data": {
k: _tensor(v[idx][u_indices])
for k, v in n_data.data.items()
},
"dtype": ids.dtype,
}
return node_dict
class EdgeData(BaseData):
"""Class of edge data which is used for DGLGraph construction. Internal use only."""
def __init__(self, src_id, dst_id, data, type=None, graph_id=None):
self.src = np.array(src_id)
self.dst = np.array(dst_id)
self.data = data
self.type = type if type is not None else ("_V", "_E", "_V")
self.graph_id = (
np.array(graph_id)
if graph_id is not None
else np.full(len(src_id), 0)
)
_validate_data_length(
{
**{"src": self.src, "dst": self.dst, "graph_id": self.graph_id},
**self.data,
}
)
@staticmethod
def load_from_csv(
meta: MetaEdge, data_parser: Callable, base_dir=None, separator=","
):
df = BaseData.read_csv(meta.file_name, base_dir, separator)
src_ids = BaseData.pop_from_dataframe(df, meta.src_id_field)
if src_ids is None:
raise DGLError(
"Missing src id field [{}] in file [{}].".format(
meta.src_id_field, meta.file_name
)
)
dst_ids = BaseData.pop_from_dataframe(df, meta.dst_id_field)
if dst_ids is None:
raise DGLError(
"Missing dst id field [{}] in file [{}].".format(
meta.dst_id_field, meta.file_name
)
)
graph_ids = BaseData.pop_from_dataframe(df, meta.graph_id_field)
etype = tuple(meta.etype)
edata = data_parser(df)
return EdgeData(src_ids, dst_ids, edata, type=etype, graph_id=graph_ids)
@staticmethod
def to_dict(edge_data: List["EdgeData"], node_dict: dict) -> dict:
edge_dict = {}
for e_data in edge_data:
(src_type, e_type, dst_type) = e_data.type
graph_ids = np.unique(e_data.graph_id)
for graph_id in graph_ids:
if graph_id in edge_dict and e_data.type in edge_dict[graph_id]:
raise DGLError(
f"Duplicate edge type[{e_data.type}] for same graph[{graph_id}], please place the same edge_type for same graph into single EdgeData."
)
idx = e_data.graph_id == graph_id
src_mapping = node_dict[graph_id][src_type]["mapping"]
dst_mapping = node_dict[graph_id][dst_type]["mapping"]
orig_src_ids = e_data.src[idx].astype(
node_dict[graph_id][src_type]["dtype"]
)
orig_dst_ids = e_data.dst[idx].astype(
node_dict[graph_id][dst_type]["dtype"]
)
src_ids = [src_mapping[index] for index in orig_src_ids]
dst_ids = [dst_mapping[index] for index in orig_dst_ids]
if graph_id not in edge_dict:
edge_dict[graph_id] = {}
edge_dict[graph_id][e_data.type] = {
"edges": (_tensor(src_ids), _tensor(dst_ids)),
"data": {
k: _tensor(v[idx]) for k, v in e_data.data.items()
},
}
return edge_dict
class GraphData(BaseData):
"""Class of graph data which is used for DGLGraph construction. Internal use only."""
def __init__(self, graph_id, data):
self.graph_id = np.array(graph_id)
self.data = data
_validate_data_length({**{"graph_id": self.graph_id}, **self.data})
@staticmethod
def load_from_csv(
meta: MetaGraph, data_parser: Callable, base_dir=None, separator=","
):
df = BaseData.read_csv(meta.file_name, base_dir, separator)
graph_ids = BaseData.pop_from_dataframe(df, meta.graph_id_field)
if graph_ids is None:
raise DGLError(
"Missing graph id field [{}] in file [{}].".format(
meta.graph_id_field, meta.file_name
)
)
gdata = data_parser(df)
return GraphData(graph_ids, gdata)
@staticmethod
def to_dict(graph_data: "GraphData", graphs_dict: dict) -> dict:
missing_ids = np.setdiff1d(
np.array(list(graphs_dict.keys())), graph_data.graph_id
)
if len(missing_ids) > 0:
raise DGLError(
"Found following graph ids in node/edge CSVs but not in graph CSV: {}.".format(
missing_ids
)
)
graph_ids = graph_data.graph_id
graphs = []
for graph_id in graph_ids:
if graph_id not in graphs_dict:
graphs_dict[graph_id] = dgl_heterograph(
{("_V", "_E", "_V"): ([], [])}
)
for graph_id in graph_ids:
graphs.append(graphs_dict[graph_id])
data = {
k: F.reshape(_tensor(v), (len(graphs), -1))
for k, v in graph_data.data.items()
}
return graphs, data
class DGLGraphConstructor:
"""Class for constructing DGLGraph from Node/Edge/Graph data. Internal use only."""
@staticmethod
def construct_graphs(node_data, edge_data, graph_data=None):
if not isinstance(node_data, list):
node_data = [node_data]
if not isinstance(edge_data, list):
edge_data = [edge_data]
node_dict = NodeData.to_dict(node_data)
edge_dict = EdgeData.to_dict(edge_data, node_dict)
graph_dict = DGLGraphConstructor._construct_graphs(node_dict, edge_dict)
if graph_data is None:
graph_data = GraphData(np.full(1, 0), {})
graphs, data = GraphData.to_dict(graph_data, graph_dict)
return graphs, data
@staticmethod
def _construct_graphs(node_dict, edge_dict):
graph_dict = {}
for graph_id in node_dict:
if graph_id not in edge_dict:
edge_dict[graph_id][("_V", "_E", "_V")] = {"edges": ([], [])}
graph = dgl_heterograph(
{
etype: edata["edges"]
for etype, edata in edge_dict[graph_id].items()
},
num_nodes_dict={
ntype: len(ndata["mapping"])
for ntype, ndata in node_dict[graph_id].items()
},
)
def assign_data(type, src_data, dst_data):
for key, value in src_data.items():
dst_data[type].data[key] = value
for type, data in node_dict[graph_id].items():
assign_data(type, data["data"], graph.nodes)
for (type), data in edge_dict[graph_id].items():
assign_data(type, data["data"], graph.edges)
graph_dict[graph_id] = graph
return graph_dict
class DefaultDataParser:
"""Default data parser for CSVDataset. It
1. ignores any columns which does not have a header.
2. tries to convert to list of numeric values(generated by
np.array().tolist()) if cell data is a str separated by ','.
3. read data and infer data type directly, otherwise.
"""
def __call__(self, df: pd.DataFrame):
data = {}
for header in df:
if "Unnamed" in header:
dgl_warning("Unnamed column is found. Ignored...")
continue
dt = df[header].to_numpy().squeeze()
if len(dt) > 0 and isinstance(dt[0], str):
# probably consists of list of numeric values
dt = np.array([ast.literal_eval(row) for row in dt])
data[header] = dt
return data
+349
View File
@@ -0,0 +1,349 @@
"""Basic DGL Dataset
"""
from __future__ import absolute_import
import abc
import hashlib
import os
import traceback
from ..utils import retry_method_with_fix
from .utils import download, extract_archive, get_download_dir, makedirs
class DGLDataset(object):
r"""The basic DGL dataset for creating graph datasets.
This class defines a basic template class for DGL Dataset.
The following steps will be executed automatically:
1. Check whether there is a dataset cache on disk
(already processed and stored on the disk) by
invoking ``has_cache()``. If true, goto 5.
2. Call ``download()`` to download the data if ``url`` is not None.
3. Call ``process()`` to process the data.
4. Call ``save()`` to save the processed dataset on disk and goto 6.
5. Call ``load()`` to load the processed dataset from disk.
6. Done.
Users can overwite these functions with their
own data processing logic.
Parameters
----------
name : str
Name of the dataset
url : str
Url to download the raw dataset. Default: None
raw_dir : str
Specifying the directory that will store the
downloaded data or the directory that
already stores the input data.
Default: ~/.dgl/
save_dir : str
Directory to save the processed dataset.
Default: same as raw_dir
hash_key : tuple
A tuple of values as the input for the hash function.
Users can distinguish instances (and their caches on the disk)
from the same dataset class by comparing the hash values.
Default: (), the corresponding hash value is ``'f9065fa7'``.
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
url : str
The URL to download the dataset
name : str
The dataset name
raw_dir : str
Directory to store all the downloaded raw datasets.
raw_path : str
Path to the downloaded raw dataset folder. An alias for
``os.path.join(self.raw_dir, self.name)``.
save_dir : str
Directory to save all the processed datasets.
save_path : str
Path to the processed dataset folder. An alias for
``os.path.join(self.save_dir, self.name)``.
verbose : bool
Whether to print more runtime information.
hash : str
Hash value for the dataset and the setting.
"""
def __init__(
self,
name,
url=None,
raw_dir=None,
save_dir=None,
hash_key=(),
force_reload=False,
verbose=False,
transform=None,
):
self._name = name
self._url = url
self._force_reload = force_reload
self._verbose = verbose
self._hash_key = hash_key
self._hash = self._get_hash()
self._transform = transform
# if no dir is provided, the default dgl download dir is used.
if raw_dir is None:
self._raw_dir = get_download_dir()
else:
self._raw_dir = raw_dir
if save_dir is None:
self._save_dir = self._raw_dir
else:
self._save_dir = save_dir
self._load()
def download(self):
r"""Overwite to realize your own logic of downloading data.
It is recommended to download the to the :obj:`self.raw_dir`
folder. Can be ignored if the dataset is
already in :obj:`self.raw_dir`.
"""
pass
def save(self):
r"""Overwite to realize your own logic of
saving the processed dataset into files.
It is recommended to use ``dgl.data.utils.save_graphs``
to save dgl graph into files and use
``dgl.data.utils.save_info`` to save extra
information into files.
"""
pass
def load(self):
r"""Overwite to realize your own logic of
loading the saved dataset from files.
It is recommended to use ``dgl.data.utils.load_graphs``
to load dgl graph from files and use
``dgl.data.utils.load_info`` to load extra information
into python dict object.
"""
pass
@abc.abstractmethod
def process(self):
r"""Overwrite to realize your own logic of processing the input data."""
pass
def has_cache(self):
r"""Overwrite to realize your own logic of
deciding whether there exists a cached dataset.
By default False.
"""
return False
@retry_method_with_fix(download)
def _download(self):
"""Download dataset by calling ``self.download()``
if the dataset does not exists under ``self.raw_path``.
By default ``self.raw_path = os.path.join(self.raw_dir, self.name)``
One can overwrite ``raw_path()`` function to change the path.
"""
if os.path.exists(self.raw_path): # pragma: no cover
return
makedirs(self.raw_dir)
self.download()
def _load(self):
"""Entry point from __init__ to load the dataset.
If cache exists:
- Load the dataset from saved dgl graph and information files.
- If loadin process fails, re-download and process the dataset.
else:
- Download the dataset if needed.
- Process the dataset and build the dgl graph.
- Save the processed dataset into files.
"""
load_flag = not self._force_reload and self.has_cache()
if load_flag:
try:
self.load()
if self.verbose:
print("Done loading data from cached files.")
except KeyboardInterrupt:
raise
except:
load_flag = False
if self.verbose:
print(traceback.format_exc())
print("Loading from cache failed, re-processing.")
if not load_flag:
self._download()
self.process()
self.save()
if self.verbose:
print("Done saving data into cached files.")
def _get_hash(self):
"""Compute the hash of the input tuple
Example
-------
Assume `self._hash_key = (10, False, True)`
>>> hash_value = self._get_hash()
>>> hash_value
'a770b222'
"""
hash_func = hashlib.sha1()
hash_func.update(str(self._hash_key).encode("utf-8"))
return hash_func.hexdigest()[:8]
def _get_hash_url_suffix(self):
"""Get the suffix based on the hash value of the url."""
if self._url is None:
return ""
else:
hash_func = hashlib.sha1()
hash_func.update(str(self._url).encode("utf-8"))
return "_" + hash_func.hexdigest()[:8]
@property
def url(self):
r"""Get url to download the raw dataset."""
return self._url
@property
def name(self):
r"""Name of the dataset."""
return self._name
@property
def raw_dir(self):
r"""Raw file directory contains the input data folder."""
return self._raw_dir
@property
def raw_path(self):
r"""Directory contains the input data files.
By default raw_path = os.path.join(self.raw_dir, self.name)
"""
return os.path.join(
self.raw_dir, self.name + self._get_hash_url_suffix()
)
@property
def save_dir(self):
r"""Directory to save the processed dataset."""
return self._save_dir
@property
def save_path(self):
r"""Path to save the processed dataset."""
return os.path.join(
self.save_dir, self.name + self._get_hash_url_suffix()
)
@property
def verbose(self):
r"""Whether to print information."""
return self._verbose
@property
def hash(self):
r"""Hash value for the dataset and the setting."""
return self._hash
@abc.abstractmethod
def __getitem__(self, idx):
r"""Gets the data object at index."""
pass
@abc.abstractmethod
def __len__(self):
r"""The number of examples in the dataset."""
pass
def __repr__(self):
return (
f'Dataset("{self.name}", num_graphs={len(self)},'
+ f" save_path={self.save_path})"
)
class DGLBuiltinDataset(DGLDataset):
r"""The Basic DGL Builtin Dataset.
Parameters
----------
name : str
Name of the dataset.
url : str
Url to download the raw dataset.
raw_dir : str
Specifying the directory that will store the
downloaded data or the directory that
already stores the input data.
Default: ~/.dgl/
hash_key : tuple
A tuple of values as the input for the hash function.
Users can distinguish instances (and their caches on the disk)
from the same dataset class by comparing the hash values.
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: False
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
"""
def __init__(
self,
name,
url,
raw_dir=None,
hash_key=(),
force_reload=False,
verbose=False,
transform=None,
):
super(DGLBuiltinDataset, self).__init__(
name,
url=url,
raw_dir=raw_dir,
save_dir=None,
hash_key=hash_key,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def download(self):
r"""Automatically download data and extract it."""
if self.url is not None:
zip_file_path = os.path.join(self.raw_dir, self.name + ".zip")
download(self.url, path=zip_file_path)
extract_archive(zip_file_path, self.raw_path)
+255
View File
@@ -0,0 +1,255 @@
import os
import numpy as np
import scipy.sparse as sp
from .. import backend as F
from ..convert import graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs, load_info, save_graphs, save_info
class FakeNewsDataset(DGLBuiltinDataset):
r"""Fake News Graph Classification dataset.
The dataset is composed of two sets of tree-structured fake/real
news propagation graphs extracted from Twitter. Different from
most of the benchmark datasets for the graph classification task,
the graphs in this dataset are directed tree-structured graphs where
the root node represents the news, the leaf nodes are Twitter users
who retweeted the root news. Besides, the node features are encoded
user historical tweets using different pretrained language models:
- bert: the 768-dimensional node feature composed of Twitter user historical tweets encoded by the bert-as-service
- content: the 310-dimensional node feature composed of a 300-dimensional “spacy” vector plus a 10-dimensional “profile” vector
- profile: the 10-dimensional node feature composed of ten Twitter user profile attributes.
- spacy: the 300-dimensional node feature composed of Twitter user historical tweets encoded by the spaCy word2vec encoder.
Reference: <https://github.com/safe-graph/GNN-FakeNews>
Note: this dataset is for academic use only, and commercial use is prohibited.
Statistics:
Politifact:
- Graphs: 314
- Nodes: 41,054
- Edges: 40,740
- Classes:
- Fake: 157
- Real: 157
- Node feature size:
- bert: 768
- content: 310
- profile: 10
- spacy: 300
Gossipcop:
- Graphs: 5,464
- Nodes: 314,262
- Edges: 308,798
- Classes:
- Fake: 2,732
- Real: 2,732
- Node feature size:
- bert: 768
- content: 310
- profile: 10
- spacy: 300
Parameters
----------
name : str
Name of the dataset (gossipcop, or politifact)
feature_name : str
Name of the feature (bert, content, profile, or spacy)
raw_dir : str
Specifying the directory that will store the
downloaded data or the directory that
already stores the input data.
Default: ~/.dgl/
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
name : str
Name of the dataset (gossipcop, or politifact)
num_classes : int
Number of label classes
num_graphs : int
Number of graphs
graphs : list
A list of DGLGraph objects
labels : Tensor
Graph labels
feature_name : str
Name of the feature (bert, content, profile, or spacy)
feature : Tensor
Node features
train_mask : Tensor
Mask of training set
val_mask : Tensor
Mask of validation set
test_mask : Tensor
Mask of testing set
Examples
--------
>>> dataset = FakeNewsDataset('gossipcop', 'bert')
>>> graph, label = dataset[0]
>>> num_classes = dataset.num_classes
>>> feat = dataset.feature
>>> labels = dataset.labels
"""
file_urls = {
"gossipcop": "dataset/FakeNewsGOS.zip",
"politifact": "dataset/FakeNewsPOL.zip",
}
def __init__(self, name, feature_name, raw_dir=None, transform=None):
assert name in [
"gossipcop",
"politifact",
], "Only supports 'gossipcop' or 'politifact'."
url = _get_dgl_url(self.file_urls[name])
assert feature_name in [
"bert",
"content",
"profile",
"spacy",
], "Only supports 'bert', 'content', 'profile', or 'spacy'"
self.feature_name = feature_name
super(FakeNewsDataset, self).__init__(
name=name, url=url, raw_dir=raw_dir, transform=transform
)
def process(self):
"""process raw data to graph, labels and masks"""
self.labels = F.tensor(
np.load(os.path.join(self.raw_path, "graph_labels.npy"))
)
num_graphs = self.labels.shape[0]
node_graph_id = np.load(
os.path.join(self.raw_path, "node_graph_id.npy")
)
edges = np.genfromtxt(
os.path.join(self.raw_path, "A.txt"), delimiter=",", dtype=int
)
src = edges[:, 0]
dst = edges[:, 1]
g = graph((src, dst))
node_idx_list = []
for idx in range(np.max(node_graph_id) + 1):
node_idx = np.where(node_graph_id == idx)
node_idx_list.append(node_idx[0])
self.graphs = [g.subgraph(node_idx) for node_idx in node_idx_list]
train_idx = np.load(os.path.join(self.raw_path, "train_idx.npy"))
val_idx = np.load(os.path.join(self.raw_path, "val_idx.npy"))
test_idx = np.load(os.path.join(self.raw_path, "test_idx.npy"))
train_mask = np.zeros(num_graphs, dtype=np.bool_)
val_mask = np.zeros(num_graphs, dtype=np.bool_)
test_mask = np.zeros(num_graphs, dtype=np.bool_)
train_mask[train_idx] = True
val_mask[val_idx] = True
test_mask[test_idx] = True
self.train_mask = F.tensor(train_mask)
self.val_mask = F.tensor(val_mask)
self.test_mask = F.tensor(test_mask)
feature_file = "new_" + self.feature_name + "_feature.npz"
self.feature = F.tensor(
sp.load_npz(os.path.join(self.raw_path, feature_file)).todense()
)
def save(self):
"""save the graph list and the labels"""
save_graphs(str(self.graph_path), self.graphs)
save_info(
self.info_path,
{
"label": self.labels,
"feature": self.feature,
"train_mask": self.train_mask,
"val_mask": self.val_mask,
"test_mask": self.test_mask,
},
)
@property
def graph_path(self):
return os.path.join(self.save_path, self.name + "_dgl_graph.bin")
@property
def info_path(self):
return os.path.join(self.save_path, self.name + "_dgl_graph.pkl")
def has_cache(self):
"""check whether there are processed data in `self.save_path`"""
return os.path.exists(self.graph_path) and os.path.exists(
self.info_path
)
def load(self):
"""load processed data from directory `self.save_path`"""
graphs, _ = load_graphs(str(self.graph_path))
info = load_info(str(self.info_path))
self.graphs = graphs
self.labels = info["label"]
self.feature = info["feature"]
self.train_mask = info["train_mask"]
self.val_mask = info["val_mask"]
self.test_mask = info["test_mask"]
@property
def num_classes(self):
"""Number of classes for each graph, i.e. number of prediction tasks."""
return 2
@property
def num_graphs(self):
"""Number of graphs."""
return self.labels.shape[0]
def __getitem__(self, i):
r"""Get graph and label by index
Parameters
----------
i : int
Item index
Returns
-------
(:class:`dgl.DGLGraph`, Tensor)
"""
if self._transform is None:
g = self.graphs[i]
else:
g = self._transform(self.graphs[i])
return g, self.labels[i]
def __len__(self):
r"""Number of graphs in the dataset.
Return
-------
int
"""
return len(self.graphs)
+178
View File
@@ -0,0 +1,178 @@
"""Flickr Dataset"""
import json
import os
import numpy as np
import scipy.sparse as sp
from .. import backend as F
from ..convert import from_scipy
from ..transforms import reorder_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, generate_mask_tensor, load_graphs, save_graphs
class FlickrDataset(DGLBuiltinDataset):
r"""Flickr dataset for node classification from `GraphSAINT: Graph Sampling Based Inductive
Learning Method <https://arxiv.org/abs/1907.04931>`_
The task of this dataset is categorizing types of images based on the descriptions and common
properties of online images.
Flickr dataset statistics:
- Nodes: 89,250
- Edges: 899,756
- Number of classes: 7
- Node feature size: 500
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose : bool
Whether to print out progress information.
Default: False
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
reorder : bool
Whether to reorder the graph using :func:`~dgl.reorder_graph`.
Default: False.
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import FlickrDataset
>>> dataset = FlickrDataset()
>>> dataset.num_classes
7
>>> g = dataset[0]
>>> # get node feature
>>> feat = g.ndata['feat']
>>> # get node labels
>>> labels = g.ndata['label']
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> test_mask = g.ndata['test_mask']
"""
def __init__(
self,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
reorder=False,
):
_url = _get_dgl_url("dataset/flickr.zip")
self._reorder = reorder
super(FlickrDataset, self).__init__(
name="flickr",
raw_dir=raw_dir,
url=_url,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""process raw data to graph, labels and masks"""
coo_adj = sp.load_npz(os.path.join(self.raw_path, "adj_full.npz"))
g = from_scipy(coo_adj)
features = np.load(os.path.join(self.raw_path, "feats.npy"))
features = F.tensor(features, dtype=F.float32)
y = [-1] * features.shape[0]
with open(os.path.join(self.raw_path, "class_map.json")) as f:
class_map = json.load(f)
for key, item in class_map.items():
y[int(key)] = item
labels = F.tensor(np.array(y), dtype=F.int64)
with open(os.path.join(self.raw_path, "role.json")) as f:
role = json.load(f)
train_mask = np.zeros(features.shape[0], dtype=bool)
train_mask[role["tr"]] = True
val_mask = np.zeros(features.shape[0], dtype=bool)
val_mask[role["va"]] = True
test_mask = np.zeros(features.shape[0], dtype=bool)
test_mask[role["te"]] = True
g.ndata["feat"] = features
g.ndata["label"] = labels
g.ndata["train_mask"] = generate_mask_tensor(train_mask)
g.ndata["val_mask"] = generate_mask_tensor(val_mask)
g.ndata["test_mask"] = generate_mask_tensor(test_mask)
if self._reorder:
self._graph = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
else:
self._graph = g
def has_cache(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
return os.path.exists(graph_path)
def save(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
save_graphs(graph_path, self._graph)
def load(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
g, _ = load_graphs(graph_path)
self._graph = g[0]
@property
def num_classes(self):
return 7
def __len__(self):
r"""The number of graphs in the dataset."""
return 1
def __getitem__(self, idx):
r"""Get graph object
Parameters
----------
idx : int
Item index, FlickrDataset has only one graph object
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``ndata['label']``: node label
- ``ndata['feat']``: node feature
- ``ndata['train_mask']``: mask for training node set
- ``ndata['val_mask']``: mask for validation node set
- ``ndata['test_mask']``: mask for test node set
"""
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
+415
View File
@@ -0,0 +1,415 @@
"""Fraud Dataset
"""
import os
import numpy as np
from scipy import io
from .. import backend as F
from ..convert import heterograph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs, save_graphs
class FraudDataset(DGLBuiltinDataset):
r"""Fraud node prediction dataset.
The dataset includes two multi-relational graphs extracted from Yelp and Amazon
where nodes represent fraudulent reviews or fraudulent reviewers.
It was first proposed in a CIKM'20 paper <https://arxiv.org/pdf/2008.08692.pdf> and
has been used by a recent WWW'21 paper <https://ponderly.github.io/pub/PCGNN_WWW2021.pdf>
as a benchmark. Another paper <https://arxiv.org/pdf/2104.01404.pdf> also takes
the dataset as an example to study the non-homophilous graphs. This dataset is built
upon industrial data and has rich relational information and unique properties like
class-imbalance and feature inconsistency, which makes the dataset be a good instance
to investigate how GNNs perform on real-world noisy graphs. These graphs are bidirected
and not self connected.
Reference: <https://github.com/YingtongDou/CARE-GNN>
Parameters
----------
name : str
Name of the dataset
raw_dir : str
Specifying the directory that will store the
downloaded data or the directory that
already stores the input data.
Default: ~/.dgl/
random_seed : int
Specifying the random seed in splitting the dataset.
Default: 717
train_size : float
training set size of the dataset.
Default: 0.7
val_size : float
validation set size of the dataset, and the
size of testing set is (1 - train_size - val_size)
Default: 0.1
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of label classes
graph : dgl.DGLGraph
Graph structure, etc.
seed : int
Random seed in splitting the dataset.
train_size : float
Training set size of the dataset.
val_size : float
Validation set size of the dataset
Examples
--------
>>> dataset = FraudDataset('yelp')
>>> graph = dataset[0]
>>> num_classes = dataset.num_classes
>>> feat = graph.ndata['feature']
>>> label = graph.ndata['label']
"""
file_urls = {
"yelp": "dataset/FraudYelp.zip",
"amazon": "dataset/FraudAmazon.zip",
}
relations = {
"yelp": ["net_rsr", "net_rtr", "net_rur"],
"amazon": ["net_upu", "net_usu", "net_uvu"],
}
file_names = {"yelp": "YelpChi.mat", "amazon": "Amazon.mat"}
node_name = {"yelp": "review", "amazon": "user"}
def __init__(
self,
name,
raw_dir=None,
random_seed=717,
train_size=0.7,
val_size=0.1,
force_reload=False,
verbose=True,
transform=None,
):
assert name in ["yelp", "amazon"], "only supports 'yelp', or 'amazon'"
url = _get_dgl_url(self.file_urls[name])
self.seed = random_seed
self.train_size = train_size
self.val_size = val_size
super(FraudDataset, self).__init__(
name=name,
url=url,
raw_dir=raw_dir,
hash_key=(random_seed, train_size, val_size),
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""process raw data to graph, labels, splitting masks"""
file_path = os.path.join(self.raw_path, self.file_names[self.name])
data = io.loadmat(file_path)
node_features = data["features"].todense()
# remove additional dimension of length 1 in raw .mat file
node_labels = data["label"].squeeze()
graph_data = {}
for relation in self.relations[self.name]:
adj = data[relation].tocoo()
row, col = adj.row, adj.col
graph_data[
(self.node_name[self.name], relation, self.node_name[self.name])
] = (row, col)
g = heterograph(graph_data)
g.ndata["feature"] = F.tensor(
node_features, dtype=F.data_type_dict["float32"]
)
g.ndata["label"] = F.tensor(
node_labels, dtype=F.data_type_dict["int64"]
)
self.graph = g
self._random_split(
g.ndata["feature"], self.seed, self.train_size, self.val_size
)
def __getitem__(self, idx):
r"""Get graph object
Parameters
----------
idx : int
Item index
Returns
-------
:class:`dgl.DGLGraph`
graph structure, node features, node labels and masks
- ``ndata['feature']``: node features
- ``ndata['label']``: node labels
- ``ndata['train_mask']``: mask of training set
- ``ndata['val_mask']``: mask of validation set
- ``ndata['test_mask']``: mask of testing set
"""
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self.graph
else:
return self._transform(self.graph)
def __len__(self):
"""number of data examples"""
return len(self.graph)
@property
def num_classes(self):
"""Number of classes.
Return
-------
int
"""
return 2
def save(self):
"""save processed data to directory `self.save_path`"""
graph_path = os.path.join(
self.save_path, self.name + "_dgl_graph_{}.bin".format(self.hash)
)
save_graphs(str(graph_path), self.graph)
def load(self):
"""load processed data from directory `self.save_path`"""
graph_path = os.path.join(
self.save_path, self.name + "_dgl_graph_{}.bin".format(self.hash)
)
graph_list, _ = load_graphs(str(graph_path))
g = graph_list[0]
self.graph = g
def has_cache(self):
"""check whether there are processed data in `self.save_path`"""
graph_path = os.path.join(
self.save_path, self.name + "_dgl_graph_{}.bin".format(self.hash)
)
return os.path.exists(graph_path)
def _random_split(self, x, seed=717, train_size=0.7, val_size=0.1):
"""split the dataset into training set, validation set and testing set"""
assert 0 <= train_size + val_size <= 1, (
"The sum of valid training set size and validation set size "
"must between 0 and 1 (inclusive)."
)
N = x.shape[0]
index = np.arange(N)
if self.name == "amazon":
# 0-3304 are unlabeled nodes
index = np.arange(3305, N)
index = np.random.RandomState(seed).permutation(index)
train_idx = index[: int(train_size * len(index))]
val_idx = index[len(index) - int(val_size * len(index)) :]
test_idx = index[
int(train_size * len(index)) : len(index)
- int(val_size * len(index))
]
train_mask = np.zeros(N, dtype=np.bool_)
val_mask = np.zeros(N, dtype=np.bool_)
test_mask = np.zeros(N, dtype=np.bool_)
train_mask[train_idx] = True
val_mask[val_idx] = True
test_mask[test_idx] = True
self.graph.ndata["train_mask"] = F.tensor(train_mask)
self.graph.ndata["val_mask"] = F.tensor(val_mask)
self.graph.ndata["test_mask"] = F.tensor(test_mask)
class FraudYelpDataset(FraudDataset):
r"""Fraud Yelp Dataset
The Yelp dataset includes hotel and restaurant reviews filtered (spam) and recommended
(legitimate) by Yelp. A spam review detection task can be conducted, which is a binary
classification task. 32 handcrafted features from <http://dx.doi.org/10.1145/2783258.2783370>
are taken as the raw node features. Reviews are nodes in the graph, and three relations are:
1. R-U-R: it connects reviews posted by the same user
2. R-S-R: it connects reviews under the same product with the same star rating (1-5 stars)
3. R-T-R: it connects two reviews under the same product posted in the same month.
Statistics:
- Nodes: 45,954
- Edges:
- R-U-R: 98,630
- R-T-R: 1,147,232
- R-S-R: 6,805,486
- Classes:
- Positive (spam): 6,677
- Negative (legitimate): 39,277
- Positive-Negative ratio: 1 : 5.9
- Node feature size: 32
Parameters
----------
raw_dir : str
Specifying the directory that will store the
downloaded data or the directory that
already stores the input data.
Default: ~/.dgl/
random_seed : int
Specifying the random seed in splitting the dataset.
Default: 717
train_size : float
training set size of the dataset.
Default: 0.7
val_size : float
validation set size of the dataset, and the
size of testing set is (1 - train_size - val_size)
Default: 0.1
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Examples
--------
>>> dataset = FraudYelpDataset()
>>> graph = dataset[0]
>>> num_classes = dataset.num_classes
>>> feat = graph.ndata['feature']
>>> label = graph.ndata['label']
"""
def __init__(
self,
raw_dir=None,
random_seed=717,
train_size=0.7,
val_size=0.1,
force_reload=False,
verbose=True,
transform=None,
):
super(FraudYelpDataset, self).__init__(
name="yelp",
raw_dir=raw_dir,
random_seed=random_seed,
train_size=train_size,
val_size=val_size,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class FraudAmazonDataset(FraudDataset):
r"""Fraud Amazon Dataset
The Amazon dataset includes product reviews under the Musical Instruments category.
Users with more than 80% helpful votes are labelled as benign entities and users with
less than 20% helpful votes are labelled as fraudulent entities. A fraudulent user
detection task can be conducted on the Amazon dataset, which is a binary classification
task. 25 handcrafted features from <https://arxiv.org/pdf/2005.10150.pdf> are taken as
the raw node features .
Users are nodes in the graph, and three relations are:
1. U-P-U : it connects users reviewing at least one same product
2. U-S-U : it connects users having at least one same star rating within one week
3. U-V-U : it connects users with top 5% mutual review text similarities (measured by
TF-IDF) among all users.
Statistics:
- Nodes: 11,944
- Edges:
- U-P-U: 351,216
- U-S-U: 7,132,958
- U-V-U: 2,073,474
- Classes:
- Positive (fraudulent): 821
- Negative (benign): 7,818
- Unlabeled: 3,305
- Positive-Negative ratio: 1 : 10.5
- Node feature size: 25
Parameters
----------
raw_dir : str
Specifying the directory that will store the
downloaded data or the directory that
already stores the input data.
Default: ~/.dgl/
random_seed : int
Specifying the random seed in splitting the dataset.
Default: 717
train_size : float
training set size of the dataset.
Default: 0.7
val_size : float
validation set size of the dataset, and the
size of testing set is (1 - train_size - val_size)
Default: 0.1
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Examples
--------
>>> dataset = FraudAmazonDataset()
>>> graph = dataset[0]
>>> num_classes = dataset.num_classes
>>> feat = graph.ndata['feature']
>>> label = graph.ndata['label']
"""
def __init__(
self,
raw_dir=None,
random_seed=717,
train_size=0.7,
val_size=0.1,
force_reload=False,
verbose=True,
transform=None,
):
super(FraudAmazonDataset, self).__init__(
name="amazon",
raw_dir=raw_dir,
random_seed=random_seed,
train_size=train_size,
val_size=val_size,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
+203
View File
@@ -0,0 +1,203 @@
""" GDELT dataset for temporal graph """
import os
import numpy as np
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_info, loadtxt, save_info
class GDELTDataset(DGLBuiltinDataset):
r"""GDELT dataset for event-based temporal graph
The Global Database of Events, Language, and Tone (GDELT) dataset.
This contains events happend all over the world (ie every protest held
anywhere in Russia on a given day is collapsed to a single entry).
This Dataset consists ofevents collected from 1/1/2018 to 1/31/2018
(15 minutes time granularity).
Reference:
- `Recurrent Event Network for Reasoning over Temporal Knowledge Graphs <https://arxiv.org/abs/1904.05530>`_
- `The Global Database of Events, Language, and Tone (GDELT) <https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/28075>`_
Statistics:
- Train examples: 2,304
- Valid examples: 288
- Test examples: 384
Parameters
----------
mode : str
Must be one of ('train', 'valid', 'test'). Default: 'train'
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
start_time : int
Start time of the temporal graph
end_time : int
End time of the temporal graph
is_temporal : bool
Does the dataset contain temporal graphs
Examples
----------
>>> # get train, valid, test dataset
>>> train_data = GDELTDataset()
>>> valid_data = GDELTDataset(mode='valid')
>>> test_data = GDELTDataset(mode='test')
>>>
>>> # length of train set
>>> train_size = len(train_data)
>>>
>>> for g in train_data:
.... e_feat = g.edata['rel_type']
.... # your code here
....
>>>
"""
def __init__(
self,
mode="train",
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
mode = mode.lower()
assert mode in ["train", "valid", "test"], "Mode not valid."
self.mode = mode
self.num_nodes = 23033
_url = _get_dgl_url("dataset/gdelt.zip")
super(GDELTDataset, self).__init__(
name="GDELT",
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
file_path = os.path.join(self.raw_path, self.mode + ".txt")
self.data = loadtxt(file_path, delimiter="\t").astype(np.int64)
# The source code is not released, but the paper indicates there're
# totally 137 samples. The cutoff below has exactly 137 samples.
self.time_index = np.floor(self.data[:, 3] / 15).astype(np.int64)
self._start_time = self.time_index.min()
self._end_time = self.time_index.max()
@property
def info_path(self):
return os.path.join(self.save_path, self.mode + "_info.pkl")
def has_cache(self):
return os.path.exists(self.info_path)
def save(self):
save_info(
self.info_path,
{
"data": self.data,
"time_index": self.time_index,
"start_time": self.start_time,
"end_time": self.end_time,
},
)
def load(self):
info = load_info(self.info_path)
self.data, self.time_index, self._start_time, self._end_time = (
info["data"],
info["time_index"],
info["start_time"],
info["end_time"],
)
@property
def start_time(self):
r"""Start time of events in the temporal graph
Returns
-------
int
"""
return self._start_time
@property
def end_time(self):
r"""End time of events in the temporal graph
Returns
-------
int
"""
return self._end_time
def __getitem__(self, t):
r"""Get graph by with events before time `t + self.start_time`
Parameters
----------
t : int
Time, its value must be in range [0, `self.end_time` - `self.start_time`]
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``edata['rel_type']``: edge type
"""
if t >= len(self) or t < 0:
raise IndexError("Index out of range")
i = t + self.start_time
row_mask = self.time_index <= i
edges = self.data[row_mask][:, [0, 2]]
rate = self.data[row_mask][:, 1]
g = dgl_graph((edges[:, 0], edges[:, 1]))
g.edata["rel_type"] = F.tensor(
rate.reshape(-1, 1), dtype=F.data_type_dict["int64"]
)
if self._transform is not None:
g = self._transform(g)
return g
def __len__(self):
r"""Number of graphs in the dataset.
Return
-------
int
"""
return self._end_time - self._start_time + 1
@property
def is_temporal(self):
r"""Does the dataset contain temporal graphs
Returns
-------
bool
"""
return True
GDELT = GDELTDataset
+483
View File
@@ -0,0 +1,483 @@
"""Datasets introduced in the Geom-GCN paper."""
import os
import numpy as np
from ..convert import graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url
class GeomGCNDataset(DGLBuiltinDataset):
r"""Datasets introduced in
`Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`__
Parameters
----------
name : str
Name of the dataset.
raw_dir : str
Raw file directory to store the processed data.
force_reload : bool
Whether to re-download the data source.
verbose : bool
Whether to print progress information.
transform : callable
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
"""
def __init__(self, name, raw_dir, force_reload, verbose, transform):
url = _get_dgl_url(f"dataset/{name}.zip")
super(GeomGCNDataset, self).__init__(
name=name,
url=url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""Load and process the data."""
try:
import torch
except ImportError:
raise ModuleNotFoundError(
"This dataset requires PyTorch to be the backend."
)
# Process node features and labels.
with open(f"{self.raw_path}/out1_node_feature_label.txt", "r") as f:
data = f.read().split("\n")[1:-1]
features = [
[float(v) for v in r.split("\t")[1].split(",")] for r in data
]
features = torch.tensor(features, dtype=torch.float)
labels = [int(r.split("\t")[2]) for r in data]
self._num_classes = max(labels) + 1
labels = torch.tensor(labels, dtype=torch.long)
# Process graph structure.
with open(f"{self.raw_path}/out1_graph_edges.txt", "r") as f:
data = f.read().split("\n")[1:-1]
data = [[int(v) for v in r.split("\t")] for r in data]
dst, src = torch.tensor(data, dtype=torch.long).t().contiguous()
self._g = graph((src, dst), num_nodes=features.size(0))
self._g.ndata["feat"] = features
self._g.ndata["label"] = labels
# Process 10 train/val/test node splits.
train_masks, val_masks, test_masks = [], [], []
for i in range(10):
filepath = f"{self.raw_path}/{self.name}_split_0.6_0.2_{i}.npz"
f = np.load(filepath)
train_masks += [torch.from_numpy(f["train_mask"])]
val_masks += [torch.from_numpy(f["val_mask"])]
test_masks += [torch.from_numpy(f["test_mask"])]
self._g.ndata["train_mask"] = torch.stack(train_masks, dim=1).bool()
self._g.ndata["val_mask"] = torch.stack(val_masks, dim=1).bool()
self._g.ndata["test_mask"] = torch.stack(test_masks, dim=1).bool()
def has_cache(self):
return os.path.exists(self.raw_path)
def load(self):
self.process()
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._g
else:
return self._transform(self._g)
def __len__(self):
return 1
@property
def num_classes(self):
return self._num_classes
class ChameleonDataset(GeomGCNDataset):
r"""Wikipedia page-page network on chameleons from `Multi-scale Attributed
Node Embedding <https://arxiv.org/abs/1909.13021>`__ and later modified by
`Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`__
Nodes represent articles from the English Wikipedia, edges reflect mutual
links between them. Node features indicate the presence of particular nouns
in the articles. The nodes were classified into 5 classes in terms of their
average monthly traffic.
Statistics:
- Nodes: 2277
- Edges: 36101
- Number of Classes: 5
- 10 train/val/test splits
- Train: 1092
- Val: 729
- Test: 456
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Notes
-----
The graph does not come with edges for both directions.
Examples
--------
>>> from dgl.data import ChameleonDataset
>>> dataset = ChameleonDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get data split
>>> train_mask = g.ndata["train_mask"]
>>> val_mask = g.ndata["val_mask"]
>>> test_mask = g.ndata["test_mask"]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(ChameleonDataset, self).__init__(
name="chameleon",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class SquirrelDataset(GeomGCNDataset):
r"""Wikipedia page-page network on squirrels from `Multi-scale Attributed
Node Embedding <https://arxiv.org/abs/1909.13021>`__ and later modified by
`Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`__
Nodes represent articles from the English Wikipedia, edges reflect mutual
links between them. Node features indicate the presence of particular nouns
in the articles. The nodes were classified into 5 classes in terms of their
average monthly traffic.
Statistics:
- Nodes: 5201
- Edges: 217073
- Number of Classes: 5
- 10 train/val/test splits
- Train: 2496
- Val: 1664
- Test: 1041
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Notes
-----
The graph does not come with edges for both directions.
Examples
--------
>>> from dgl.data import SquirrelDataset
>>> dataset = SquirrelDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get data split
>>> train_mask = g.ndata["train_mask"]
>>> val_mask = g.ndata["val_mask"]
>>> test_mask = g.ndata["test_mask"]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(SquirrelDataset, self).__init__(
name="squirrel",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class CornellDataset(GeomGCNDataset):
r"""Cornell subset of
`WebKB <http://www.cs.cmu.edu/afs/cs.cmu.edu/project/theo-11/www/wwkb/>`__,
later modified by `Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`__
Nodes represent web pages. Edges represent hyperlinks between them. Node
features are the bag-of-words representation of web pages. The web pages
are manually classified into the five categories, student, project, course,
staff, and faculty.
Statistics:
- Nodes: 183
- Edges: 298
- Number of Classes: 5
- 10 train/val/test splits
- Train: 87
- Val: 59
- Test: 37
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Notes
-----
The graph does not come with edges for both directions.
Examples
--------
>>> from dgl.data import CornellDataset
>>> dataset = CornellDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get data split
>>> train_mask = g.ndata["train_mask"]
>>> val_mask = g.ndata["val_mask"]
>>> test_mask = g.ndata["test_mask"]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(CornellDataset, self).__init__(
name="cornell",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class TexasDataset(GeomGCNDataset):
r"""Texas subset of
`WebKB <http://www.cs.cmu.edu/afs/cs.cmu.edu/project/theo-11/www/wwkb/>`__,
later modified by `Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`__
Nodes represent web pages. Edges represent hyperlinks between them. Node
features are the bag-of-words representation of web pages. The web pages
are manually classified into the five categories, student, project, course,
staff, and faculty.
Statistics:
- Nodes: 183
- Edges: 325
- Number of Classes: 5
- 10 train/val/test splits
- Train: 87
- Val: 59
- Test: 37
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Notes
-----
The graph does not come with edges for both directions.
Examples
--------
>>> from dgl.data import TexasDataset
>>> dataset = TexasDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get data split
>>> train_mask = g.ndata["train_mask"]
>>> val_mask = g.ndata["val_mask"]
>>> test_mask = g.ndata["test_mask"]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(TexasDataset, self).__init__(
name="texas",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class WisconsinDataset(GeomGCNDataset):
r"""Wisconsin subset of
`WebKB <http://www.cs.cmu.edu/afs/cs.cmu.edu/project/theo-11/www/wwkb/>`__,
later modified by `Geom-GCN: Geometric Graph Convolutional Networks
<https://arxiv.org/abs/2002.05287>`__
Nodes represent web pages. Edges represent hyperlinks between them. Node
features are the bag-of-words representation of web pages. The web pages
are manually classified into the five categories, student, project, course,
staff, and faculty.
Statistics:
- Nodes: 251
- Edges: 515
- Number of Classes: 5
- 10 train/val/test splits
- Train: 120
- Val: 80
- Test: 51
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Notes
-----
The graph does not come with edges for both directions.
Examples
--------
>>> from dgl.data import WisconsinDataset
>>> dataset = WisconsinDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get data split
>>> train_mask = g.ndata["train_mask"]
>>> val_mask = g.ndata["val_mask"]
>>> test_mask = g.ndata["test_mask"]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(WisconsinDataset, self).__init__(
name="wisconsin",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
+420
View File
@@ -0,0 +1,420 @@
"""Datasets used in How Powerful Are Graph Neural Networks?
(chen jun)
Datasets include:
MUTAG, COLLAB, IMDBBINARY, IMDBMULTI, NCI1, PROTEINS, PTC, REDDITBINARY, REDDITMULTI5K
https://github.com/weihua916/powerful-gnns/blob/master/dataset.zip
"""
import os
import numpy as np
from .. import backend as F
from ..convert import graph as dgl_graph
from ..utils import retry_method_with_fix
from .dgl_dataset import DGLBuiltinDataset
from .utils import (
download,
extract_archive,
load_graphs,
load_info,
loadtxt,
save_graphs,
save_info,
)
class GINDataset(DGLBuiltinDataset):
"""Dataset Class for `How Powerful Are Graph Neural Networks? <https://arxiv.org/abs/1810.00826>`_.
This is adapted from `<https://github.com/weihua916/powerful-gnns/blob/master/dataset.zip>`_.
The class provides an interface for nine datasets used in the paper along with the paper-specific
settings. The datasets are ``'MUTAG'``, ``'COLLAB'``, ``'IMDBBINARY'``, ``'IMDBMULTI'``,
``'NCI1'``, ``'PROTEINS'``, ``'PTC'``, ``'REDDITBINARY'``, ``'REDDITMULTI5K'``.
If ``degree_as_nlabel`` is set to ``False``, then ``ndata['label']`` stores the provided node label,
otherwise ``ndata['label']`` stores the node in-degrees.
For graphs that have node attributes, ``ndata['attr']`` stores the node attributes.
For graphs that have no attribute, ``ndata['attr']`` stores the corresponding one-hot encoding
of ``ndata['label']``.
Parameters
---------
name: str
dataset name, one of
(``'MUTAG'``, ``'COLLAB'``, \
``'IMDBBINARY'``, ``'IMDBMULTI'``, \
``'NCI1'``, ``'PROTEINS'``, ``'PTC'``, \
``'REDDITBINARY'``, ``'REDDITMULTI5K'``)
self_loop: bool
add self to self edge if true
degree_as_nlabel: bool
take node degree as label and feature if true
transform: callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for multiclass classification
Examples
--------
>>> data = GINDataset(name='MUTAG', self_loop=False)
The dataset instance is an iterable
>>> len(data)
188
>>> g, label = data[128]
>>> g
Graph(num_nodes=13, num_edges=26,
ndata_schemes={'label': Scheme(shape=(), dtype=torch.int64), 'attr': Scheme(shape=(7,), dtype=torch.float32)}
edata_schemes={})
>>> label
tensor(1)
Batch the graphs and labels for mini-batch training
>>> graphs, labels = zip(*[data[i] for i in range(16)])
>>> batched_graphs = dgl.batch(graphs)
>>> batched_labels = torch.tensor(labels)
>>> batched_graphs
Graph(num_nodes=330, num_edges=748,
ndata_schemes={'label': Scheme(shape=(), dtype=torch.int64), 'attr': Scheme(shape=(7,), dtype=torch.float32)}
edata_schemes={})
"""
def __init__(
self,
name,
self_loop,
degree_as_nlabel=False,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
self._name = name # MUTAG
gin_url = "https://raw.githubusercontent.com/weihua916/powerful-gnns/master/dataset.zip"
self.ds_name = "nig"
self.self_loop = self_loop
self.graphs = []
self.labels = []
# relabel
self.glabel_dict = {}
self.nlabel_dict = {}
self.elabel_dict = {}
self.ndegree_dict = {}
# global num
self.N = 0 # total graphs number
self.n = 0 # total nodes number
self.m = 0 # total edges number
# global num of classes
self.gclasses = 0
self.nclasses = 0
self.eclasses = 0
self.dim_nfeats = 0
# flags
self.degree_as_nlabel = degree_as_nlabel
self.nattrs_flag = False
self.nlabels_flag = False
super(GINDataset, self).__init__(
name=name,
url=gin_url,
hash_key=(name, self_loop, degree_as_nlabel),
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def raw_path(self):
return os.path.join(self.raw_dir, "GINDataset")
def download(self):
r"""Automatically download data and extract it."""
zip_file_path = os.path.join(self.raw_dir, "GINDataset.zip")
download(self.url, path=zip_file_path)
extract_archive(zip_file_path, self.raw_path)
def __len__(self):
"""Return the number of graphs in the dataset."""
return len(self.graphs)
def __getitem__(self, idx):
"""Get the idx-th sample.
Parameters
---------
idx : int
The sample index.
Returns
-------
(:class:`dgl.Graph`, Tensor)
The graph and its label.
"""
if self._transform is None:
g = self.graphs[idx]
else:
g = self._transform(self.graphs[idx])
return g, self.labels[idx]
def _file_path(self):
return os.path.join(
self.raw_dir,
"GINDataset",
"dataset",
self.name,
"{}.txt".format(self.name),
)
def process(self):
"""Loads input dataset from dataset/NAME/NAME.txt file"""
if self.verbose:
print("loading data...")
self.file = self._file_path()
with open(self.file, "r") as f:
# line_1 == N, total number of graphs
self.N = int(f.readline().strip())
for i in range(self.N):
if (i + 1) % 10 == 0 and self.verbose is True:
print("processing graph {}...".format(i + 1))
grow = f.readline().strip().split()
# line_2 == [n_nodes, l] is equal to
# [node number of a graph, class label of a graph]
n_nodes, glabel = [int(w) for w in grow]
# relabel graphs
if glabel not in self.glabel_dict:
mapped = len(self.glabel_dict)
self.glabel_dict[glabel] = mapped
self.labels.append(self.glabel_dict[glabel])
g = dgl_graph(([], []))
g.add_nodes(n_nodes)
nlabels = [] # node labels
nattrs = [] # node attributes if it has
m_edges = 0
for j in range(n_nodes):
nrow = f.readline().strip().split()
# handle edges and attributes(if has)
tmp = int(nrow[1]) + 2 # tmp == 2 + #edges
if tmp == len(nrow):
# no node attributes
nrow = [int(w) for w in nrow]
elif tmp > len(nrow):
nrow = [int(w) for w in nrow[:tmp]]
nattr = [float(w) for w in nrow[tmp:]]
nattrs.append(nattr)
else:
raise Exception("edge number is incorrect!")
# relabel nodes if it has labels
# if it doesn't have node labels, then every nrow[0]==0
if not nrow[0] in self.nlabel_dict:
mapped = len(self.nlabel_dict)
self.nlabel_dict[nrow[0]] = mapped
nlabels.append(self.nlabel_dict[nrow[0]])
m_edges += nrow[1]
g.add_edges(j, nrow[2:])
# add self loop
if self.self_loop:
m_edges += 1
g.add_edges(j, j)
if (j + 1) % 10 == 0 and self.verbose is True:
print(
"processing node {} of graph {}...".format(
j + 1, i + 1
)
)
print("this node has {} edgs.".format(nrow[1]))
if nattrs != []:
nattrs = np.stack(nattrs)
g.ndata["attr"] = F.tensor(nattrs, F.float32)
self.nattrs_flag = True
g.ndata["label"] = F.tensor(nlabels)
if len(self.nlabel_dict) > 1:
self.nlabels_flag = True
assert g.num_nodes() == n_nodes
# update statistics of graphs
self.n += n_nodes
self.m += m_edges
self.graphs.append(g)
self.labels = F.tensor(self.labels)
# if no attr
if not self.nattrs_flag:
if self.verbose:
print("there are no node features in this dataset!")
# generate node attr by node degree
if self.degree_as_nlabel:
if self.verbose:
print("generate node features by node degree...")
for g in self.graphs:
# actually this label shouldn't be updated
# in case users want to keep it
# but usually no features means no labels, fine.
g.ndata["label"] = g.in_degrees()
# extracting unique node labels
# in case the labels/degrees are not continuous number
nlabel_set = set([])
for g in self.graphs:
nlabel_set = nlabel_set.union(
set([F.as_scalar(nl) for nl in g.ndata["label"]])
)
nlabel_set = list(nlabel_set)
is_label_valid = all(
[label in self.nlabel_dict for label in nlabel_set]
)
if (
is_label_valid
and len(nlabel_set) == np.max(nlabel_set) + 1
and np.min(nlabel_set) == 0
):
# Note this is different from the author's implementation. In weihua916's implementation,
# the labels are relabeled anyway. But here we didn't relabel it if the labels are contiguous
# to make it consistent with the original dataset
label2idx = self.nlabel_dict
else:
label2idx = {nlabel_set[i]: i for i in range(len(nlabel_set))}
# generate node attr by node label
for g in self.graphs:
attr = np.zeros((g.num_nodes(), len(label2idx)))
attr[
range(g.num_nodes()),
[
label2idx[nl]
for nl in F.asnumpy(g.ndata["label"]).tolist()
],
] = 1
g.ndata["attr"] = F.tensor(attr, F.float32)
# after load, get the #classes and #dim
self.gclasses = len(self.glabel_dict)
self.nclasses = len(self.nlabel_dict)
self.eclasses = len(self.elabel_dict)
self.dim_nfeats = len(self.graphs[0].ndata["attr"][0])
if self.verbose:
print("Done.")
print(
"""
-------- Data Statistics --------'
#Graphs: %d
#Graph Classes: %d
#Nodes: %d
#Node Classes: %d
#Node Features Dim: %d
#Edges: %d
#Edge Classes: %d
Avg. of #Nodes: %.2f
Avg. of #Edges: %.2f
Graph Relabeled: %s
Node Relabeled: %s
Degree Relabeled(If degree_as_nlabel=True): %s \n """
% (
self.N,
self.gclasses,
self.n,
self.nclasses,
self.dim_nfeats,
self.m,
self.eclasses,
self.n / self.N,
self.m / self.N,
self.glabel_dict,
self.nlabel_dict,
self.ndegree_dict,
)
)
def save(self):
label_dict = {"labels": self.labels}
info_dict = {
"N": self.N,
"n": self.n,
"m": self.m,
"self_loop": self.self_loop,
"gclasses": self.gclasses,
"nclasses": self.nclasses,
"eclasses": self.eclasses,
"dim_nfeats": self.dim_nfeats,
"degree_as_nlabel": self.degree_as_nlabel,
"glabel_dict": self.glabel_dict,
"nlabel_dict": self.nlabel_dict,
"elabel_dict": self.elabel_dict,
"ndegree_dict": self.ndegree_dict,
}
save_graphs(str(self.graph_path), self.graphs, label_dict)
save_info(str(self.info_path), info_dict)
def load(self):
graphs, label_dict = load_graphs(str(self.graph_path))
info_dict = load_info(str(self.info_path))
self.graphs = graphs
self.labels = label_dict["labels"]
self.N = info_dict["N"]
self.n = info_dict["n"]
self.m = info_dict["m"]
self.self_loop = info_dict["self_loop"]
self.gclasses = info_dict["gclasses"]
self.nclasses = info_dict["nclasses"]
self.eclasses = info_dict["eclasses"]
self.dim_nfeats = info_dict["dim_nfeats"]
self.glabel_dict = info_dict["glabel_dict"]
self.nlabel_dict = info_dict["nlabel_dict"]
self.elabel_dict = info_dict["elabel_dict"]
self.ndegree_dict = info_dict["ndegree_dict"]
self.degree_as_nlabel = info_dict["degree_as_nlabel"]
@property
def graph_path(self):
return os.path.join(
self.save_path, "gin_{}_{}.bin".format(self.name, self.hash)
)
@property
def info_path(self):
return os.path.join(
self.save_path, "gin_{}_{}.pkl".format(self.name, self.hash)
)
def has_cache(self):
if os.path.exists(self.graph_path) and os.path.exists(self.info_path):
return True
return False
@property
def num_classes(self):
return self.gclasses
+544
View File
@@ -0,0 +1,544 @@
"""GNN Benchmark datasets for node classification."""
import os
import numpy as np
import scipy.sparse as sp
from .. import backend as F, transforms
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import (
_get_dgl_url,
deprecate_class,
deprecate_property,
load_graphs,
save_graphs,
)
__all__ = [
"AmazonCoBuyComputerDataset",
"AmazonCoBuyPhotoDataset",
"CoauthorPhysicsDataset",
"CoauthorCSDataset",
"CoraFullDataset",
"AmazonCoBuy",
"Coauthor",
"CoraFull",
]
def eliminate_self_loops(A):
"""Remove self-loops from the adjacency matrix."""
A = A.tolil()
A.setdiag(0)
A = A.tocsr()
A.eliminate_zeros()
return A
class GNNBenchmarkDataset(DGLBuiltinDataset):
r"""Base Class for GNN Benchmark dataset
Reference: https://github.com/shchur/gnn-benchmark#datasets
"""
def __init__(
self,
name,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
_url = _get_dgl_url("dataset/" + name + ".zip")
super(GNNBenchmarkDataset, self).__init__(
name=name,
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
npz_path = os.path.join(self.raw_path, self.name + ".npz")
g = self._load_npz(npz_path)
g = transforms.reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
self._graph = g
self._data = [g]
self._print_info()
def has_cache(self):
graph_path = os.path.join(self.save_path, "dgl_graph_v1.bin")
if os.path.exists(graph_path):
return True
return False
def save(self):
graph_path = os.path.join(self.save_path, "dgl_graph_v1.bin")
save_graphs(graph_path, self._graph)
def load(self):
graph_path = os.path.join(self.save_path, "dgl_graph_v1.bin")
graphs, _ = load_graphs(graph_path)
self._graph = graphs[0]
self._data = [graphs[0]]
self._print_info()
def _print_info(self):
if self.verbose:
print(" NumNodes: {}".format(self._graph.num_nodes()))
print(" NumEdges: {}".format(self._graph.num_edges()))
print(" NumFeats: {}".format(self._graph.ndata["feat"].shape[-1]))
print(" NumbClasses: {}".format(self.num_classes))
def _load_npz(self, file_name):
with np.load(file_name, allow_pickle=True) as loader:
loader = dict(loader)
num_nodes = loader["adj_shape"][0]
adj_matrix = sp.csr_matrix(
(
loader["adj_data"],
loader["adj_indices"],
loader["adj_indptr"],
),
shape=loader["adj_shape"],
).tocoo()
if "attr_data" in loader:
# Attributes are stored as a sparse CSR matrix
attr_matrix = sp.csr_matrix(
(
loader["attr_data"],
loader["attr_indices"],
loader["attr_indptr"],
),
shape=loader["attr_shape"],
).todense()
elif "attr_matrix" in loader:
# Attributes are stored as a (dense) np.ndarray
attr_matrix = loader["attr_matrix"]
else:
attr_matrix = None
if "labels_data" in loader:
# Labels are stored as a CSR matrix
labels = sp.csr_matrix(
(
loader["labels_data"],
loader["labels_indices"],
loader["labels_indptr"],
),
shape=loader["labels_shape"],
).todense()
elif "labels" in loader:
# Labels are stored as a numpy array
labels = loader["labels"]
else:
labels = None
g = dgl_graph((adj_matrix.row, adj_matrix.col))
g = transforms.to_bidirected(g)
g.ndata["feat"] = F.tensor(attr_matrix, F.data_type_dict["float32"])
g.ndata["label"] = F.tensor(labels, F.data_type_dict["int64"])
return g
@property
def num_classes(self):
"""Number of classes."""
raise NotImplementedError
def __getitem__(self, idx):
r"""Get graph by index
Parameters
----------
idx : int
Item index
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``ndata['feat']``: node features
- ``ndata['label']``: node labels
"""
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
r"""Number of graphs in the dataset"""
return 1
class CoraFullDataset(GNNBenchmarkDataset):
r"""CORA-Full dataset for node classification task.
Extended Cora dataset. Nodes represent paper and edges represent citations.
Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_
Statistics:
- Nodes: 19,793
- Edges: 126,842 (note that the original dataset has 65,311 edges but DGL adds
the reverse edges and remove the duplicates, hence with a different number)
- Number of Classes: 70
- Node feature size: 8,710
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> data = CoraFullDataset()
>>> g = data[0]
>>> num_class = data.num_classes
>>> feat = g.ndata['feat'] # get node feature
>>> label = g.ndata['label'] # get node labels
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(CoraFullDataset, self).__init__(
name="cora_full",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def num_classes(self):
"""Number of classes.
Return
-------
int
"""
return 70
class CoauthorCSDataset(GNNBenchmarkDataset):
r"""'Computer Science (CS)' part of the Coauthor dataset for node classification task.
Coauthor CS and Coauthor Physics are co-authorship graphs based on the Microsoft Academic Graph
from the KDD Cup 2016 challenge. Here, nodes are authors, that are connected by an edge if they
co-authored a paper; node features represent paper keywords for each authors papers, and class
labels indicate most active fields of study for each author.
Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_
Statistics:
- Nodes: 18,333
- Edges: 163,788 (note that the original dataset has 81,894 edges but DGL adds
the reverse edges and remove the duplicates, hence with a different number)
- Number of classes: 15
- Node feature size: 6,805
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> data = CoauthorCSDataset()
>>> g = data[0]
>>> num_class = data.num_classes
>>> feat = g.ndata['feat'] # get node feature
>>> label = g.ndata['label'] # get node labels
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(CoauthorCSDataset, self).__init__(
name="coauthor_cs",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def num_classes(self):
"""Number of classes.
Return
-------
int
"""
return 15
class CoauthorPhysicsDataset(GNNBenchmarkDataset):
r"""'Physics' part of the Coauthor dataset for node classification task.
Coauthor CS and Coauthor Physics are co-authorship graphs based on the Microsoft Academic Graph
from the KDD Cup 2016 challenge. Here, nodes are authors, that are connected by an edge if they
co-authored a paper; node features represent paper keywords for each authors papers, and class
labels indicate most active fields of study for each author.
Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_
Statistics
- Nodes: 34,493
- Edges: 495,924 (note that the original dataset has 247,962 edges but DGL adds
the reverse edges and remove the duplicates, hence with a different number)
- Number of classes: 5
- Node feature size: 8,415
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> data = CoauthorPhysicsDataset()
>>> g = data[0]
>>> num_class = data.num_classes
>>> feat = g.ndata['feat'] # get node feature
>>> label = g.ndata['label'] # get node labels
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(CoauthorPhysicsDataset, self).__init__(
name="coauthor_physics",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def num_classes(self):
"""Number of classes.
Return
-------
int
"""
return 5
class AmazonCoBuyComputerDataset(GNNBenchmarkDataset):
r"""'Computer' part of the AmazonCoBuy dataset for node classification task.
Amazon Computers and Amazon Photo are segments of the Amazon co-purchase graph [McAuley et al., 2015],
where nodes represent goods, edges indicate that two goods are frequently bought together, node
features are bag-of-words encoded product reviews, and class labels are given by the product category.
Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_
Statistics:
- Nodes: 13,752
- Edges: 491,722 (note that the original dataset has 245,778 edges but DGL adds
the reverse edges and remove the duplicates, hence with a different number)
- Number of classes: 10
- Node feature size: 767
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> data = AmazonCoBuyComputerDataset()
>>> g = data[0]
>>> num_class = data.num_classes
>>> feat = g.ndata['feat'] # get node feature
>>> label = g.ndata['label'] # get node labels
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(AmazonCoBuyComputerDataset, self).__init__(
name="amazon_co_buy_computer",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def num_classes(self):
"""Number of classes.
Return
-------
int
"""
return 10
class AmazonCoBuyPhotoDataset(GNNBenchmarkDataset):
r"""AmazonCoBuy dataset for node classification task.
Amazon Computers and Amazon Photo are segments of the Amazon co-purchase graph [McAuley et al., 2015],
where nodes represent goods, edges indicate that two goods are frequently bought together, node
features are bag-of-words encoded product reviews, and class labels are given by the product category.
Reference: `<https://github.com/shchur/gnn-benchmark#datasets>`_
Statistics
- Nodes: 7,650
- Edges: 238,163 (note that the original dataset has 119,043 edges but DGL adds
the reverse edges and remove the duplicates, hence with a different number)
- Number of classes: 8
- Node feature size: 745
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> data = AmazonCoBuyPhotoDataset()
>>> g = data[0]
>>> num_class = data.num_classes
>>> feat = g.ndata['feat'] # get node feature
>>> label = g.ndata['label'] # get node labels
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(AmazonCoBuyPhotoDataset, self).__init__(
name="amazon_co_buy_photo",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def num_classes(self):
"""Number of classes.
Return
-------
int
"""
return 8
class CoraFull(CoraFullDataset):
def __init__(self, **kwargs):
deprecate_class("CoraFull", "CoraFullDataset")
super(CoraFull, self).__init__(**kwargs)
def AmazonCoBuy(name):
if name == "computers":
deprecate_class("AmazonCoBuy", "AmazonCoBuyComputerDataset")
return AmazonCoBuyComputerDataset()
elif name == "photo":
deprecate_class("AmazonCoBuy", "AmazonCoBuyPhotoDataset")
return AmazonCoBuyPhotoDataset()
else:
raise ValueError('Dataset name should be "computers" or "photo".')
def Coauthor(name):
if name == "cs":
deprecate_class("Coauthor", "CoauthorCSDataset")
return CoauthorCSDataset()
elif name == "physics":
deprecate_class("Coauthor", "CoauthorPhysicsDataset")
return CoauthorPhysicsDataset()
else:
raise ValueError('Dataset name should be "cs" or "physics".')
+272
View File
@@ -0,0 +1,272 @@
"""For Graph Serialization"""
from __future__ import absolute_import
import os
from .. import backend as F
from .._ffi.function import _init_api
from .._ffi.object import ObjectBase, register_object
from ..base import dgl_warning, DGLError
from ..heterograph import DGLGraph
from .heterograph_serialize import save_heterographs
_init_api("dgl.data.graph_serialize")
__all__ = ["save_graphs", "load_graphs", "load_labels"]
@register_object("graph_serialize.StorageMetaData")
class StorageMetaData(ObjectBase):
"""StorageMetaData Object
attributes available:
num_graph [int]: return numbers of graphs
nodes_num_list Value of NDArray: return number of nodes for each graph
edges_num_list Value of NDArray: return number of edges for each graph
labels [dict of backend tensors]: return dict of labels
graph_data [list of GraphData]: return list of GraphData Object
"""
def is_local_path(filepath):
return not (
filepath.startswith("hdfs://")
or filepath.startswith("viewfs://")
or filepath.startswith("s3://")
)
def check_local_file_exists(filename):
if is_local_path(filename) and not os.path.exists(filename):
raise DGLError("File {} does not exist.".format(filename))
@register_object("graph_serialize.GraphData")
class GraphData(ObjectBase):
"""GraphData Object"""
@staticmethod
def create(g):
"""Create GraphData"""
# TODO(zihao): support serialize batched graph in the future.
assert (
g.batch_size == 1
), "Batched DGLGraph is not supported for serialization"
ghandle = g._graph
if len(g.ndata) != 0:
node_tensors = dict()
for key, value in g.ndata.items():
node_tensors[key] = F.zerocopy_to_dgl_ndarray(value)
else:
node_tensors = None
if len(g.edata) != 0:
edge_tensors = dict()
for key, value in g.edata.items():
edge_tensors[key] = F.zerocopy_to_dgl_ndarray(value)
else:
edge_tensors = None
return _CAPI_MakeGraphData(ghandle, node_tensors, edge_tensors)
def get_graph(self):
"""Get DGLGraph from GraphData"""
ghandle = _CAPI_GDataGraphHandle(self)
hgi = _CAPI_DGLAsHeteroGraph(ghandle)
g = DGLGraph(hgi, ["_U"], ["_E"])
node_tensors_items = _CAPI_GDataNodeTensors(self).items()
edge_tensors_items = _CAPI_GDataEdgeTensors(self).items()
for k, v in node_tensors_items:
g.ndata[k] = F.zerocopy_from_dgl_ndarray(v)
for k, v in edge_tensors_items:
g.edata[k] = F.zerocopy_from_dgl_ndarray(v)
return g
def save_graphs(filename, g_list, labels=None, formats=None):
r"""Save graphs and optionally their labels to file.
Besides saving to local files, DGL supports writing the graphs directly
to S3 (by providing a ``"s3://..."`` path) or to HDFS (by providing
``"hdfs://..."`` a path).
The function saves both the graph structure and node/edge features to file
in DGL's own binary format. For graph-level features, pass them via
the :attr:`labels` argument.
Parameters
----------
filename : str
The file name to store the graphs and labels.
g_list: list
The graphs to be saved.
labels: dict[str, Tensor]
labels should be dict of tensors, with str as keys
formats: str or list[str]
Save graph in specified formats. It could be any combination of
``coo``, ``csc`` and ``csr``. If not specified, save one format
only according to what format is available. If multiple formats
are available, selection priority from high to low is ``coo``,
``csc``, ``csr``.
Examples
----------
>>> import dgl
>>> import torch as th
Create :class:`DGLGraph` objects and initialize node
and edge features.
>>> g1 = dgl.graph(([0, 1, 2], [1, 2, 3]))
>>> g2 = dgl.graph(([0, 2], [2, 3]))
>>> g2.edata["e"] = th.ones(2, 4)
Save Graphs into file
>>> from dgl.data.utils import save_graphs
>>> graph_labels = {"glabel": th.tensor([0, 1])}
>>> save_graphs("./data.bin", [g1, g2], graph_labels)
See Also
--------
load_graphs
"""
# if it is local file, do some sanity check
if is_local_path(filename):
if os.path.isdir(filename):
raise DGLError(
"Filename {} is an existing directory.".format(filename)
)
f_path = os.path.dirname(filename)
if f_path and not os.path.exists(f_path):
os.makedirs(f_path)
g_sample = g_list[0] if isinstance(g_list, list) else g_list
if type(g_sample) == DGLGraph: # Doesn't support DGLGraph's derived class
save_heterographs(filename, g_list, labels, formats)
else:
raise DGLError(
"Invalid argument g_list. Must be a DGLGraph or a list of DGLGraphs."
)
def load_graphs(filename, idx_list=None):
"""Load graphs and optionally their labels from file saved by :func:`save_graphs`.
Besides loading from local files, DGL supports loading the graphs directly
from S3 (by providing a ``"s3://..."`` path) or from HDFS (by providing
``"hdfs://..."`` a path).
Parameters
----------
filename: str
The file name to load graphs from.
idx_list: list[int], optional
The indices of the graphs to be loaded if the file contains multiple graphs.
Default is loading all the graphs stored in the file.
Returns
--------
graph_list: list[DGLGraph]
The loaded graphs.
labels: dict[str, Tensor]
The graph labels stored in file. If no label is stored, the dictionary is empty.
Regardless of whether the ``idx_list`` argument is given or not,
the returned dictionary always contains the labels of all the graphs.
Examples
----------
Following the example in :func:`save_graphs`.
>>> from dgl.data.utils import load_graphs
>>> glist, label_dict = load_graphs("./data.bin") # glist will be [g1, g2]
>>> glist, label_dict = load_graphs("./data.bin", [0]) # glist will be [g1]
See Also
--------
save_graphs
"""
# if it is local file, do some sanity check
check_local_file_exists(filename)
version = _CAPI_GetFileVersion(filename)
if version == 1:
dgl_warning(
"You are loading a graph file saved by old version of dgl. \
Please consider saving it again with the current format."
)
return load_graph_v1(filename, idx_list)
elif version == 2:
return load_graph_v2(filename, idx_list)
else:
raise DGLError("Invalid DGL Version Number.")
def load_graph_v2(filename, idx_list=None):
"""Internal functions for loading DGLGraphs."""
if idx_list is None:
idx_list = []
assert isinstance(idx_list, list)
heterograph_list = _CAPI_LoadGraphFiles_V2(filename, idx_list)
label_dict = load_labels_v2(filename)
return [gdata.get_graph() for gdata in heterograph_list], label_dict
def load_graph_v1(filename, idx_list=None):
""" "Internal functions for loading DGLGraphs (V0)."""
if idx_list is None:
idx_list = []
assert isinstance(idx_list, list)
metadata = _CAPI_LoadGraphFiles_V1(filename, idx_list, False)
label_dict = {}
for k, v in metadata.labels.items():
label_dict[k] = F.zerocopy_from_dgl_ndarray(v)
return [gdata.get_graph() for gdata in metadata.graph_data], label_dict
def load_labels(filename):
"""
Load label dict from file
Parameters
----------
filename: str
filename to load DGLGraphs
Returns
----------
labels: dict
dict of labels stored in file (empty dict returned if no
label stored)
Examples
----------
Following the example in save_graphs.
>>> from dgl.data.utils import load_labels
>>> label_dict = load_graphs("./data.bin")
"""
# if it is local file, do some sanity check
check_local_file_exists(filename)
version = _CAPI_GetFileVersion(filename)
if version == 1:
return load_labels_v1(filename)
elif version == 2:
return load_labels_v2(filename)
else:
raise Exception("Invalid DGL Version Number")
def load_labels_v2(filename):
"""Internal functions for loading labels from V2 format"""
label_dict = {}
nd_dict = _CAPI_LoadLabels_V2(filename)
for k, v in nd_dict.items():
label_dict[k] = F.zerocopy_from_dgl_ndarray(v)
return label_dict
def load_labels_v1(filename):
"""Internal functions for loading labels from V1 format"""
metadata = _CAPI_LoadGraphFiles_V1(filename, [], True)
label_dict = {}
for k, v in metadata.labels.items():
label_dict[k] = F.zerocopy_from_dgl_ndarray(v)
return label_dict
+79
View File
@@ -0,0 +1,79 @@
"""For HeteroGraph Serialization"""
from __future__ import absolute_import
from .. import backend as F
from .._ffi.function import _init_api
from .._ffi.object import ObjectBase, register_object
from ..container import convert_to_strmap
from ..frame import Frame
from ..heterograph import DGLGraph
_init_api("dgl.data.heterograph_serialize")
def tensor_dict_to_ndarray_dict(tensor_dict):
"""Convert dict[str, tensor] to StrMap[NDArray]"""
ndarray_dict = {}
for key, value in tensor_dict.items():
ndarray_dict[key] = F.zerocopy_to_dgl_ndarray(value)
return convert_to_strmap(ndarray_dict)
def save_heterographs(filename, g_list, labels, formats):
"""Save heterographs into file"""
if labels is None:
labels = {}
if isinstance(g_list, DGLGraph):
g_list = [g_list]
assert all(
[type(g) == DGLGraph for g in g_list]
), "Invalid DGLGraph in g_list argument"
gdata_list = [HeteroGraphData.create(g) for g in g_list]
if formats is None:
formats = []
elif isinstance(formats, str):
formats = [formats]
_CAPI_SaveHeteroGraphData(
filename, gdata_list, tensor_dict_to_ndarray_dict(labels), formats
)
@register_object("heterograph_serialize.HeteroGraphData")
class HeteroGraphData(ObjectBase):
"""Object to hold the data to be stored for DGLGraph"""
@staticmethod
def create(g):
edata_list = []
ndata_list = []
for etype in g.canonical_etypes:
edata_list.append(tensor_dict_to_ndarray_dict(g.edges[etype].data))
for ntype in g.ntypes:
ndata_list.append(tensor_dict_to_ndarray_dict(g.nodes[ntype].data))
return _CAPI_MakeHeteroGraphData(
g._graph, ndata_list, edata_list, g.ntypes, g.etypes
)
def get_graph(self):
ntensor_list = list(_CAPI_GetNDataFromHeteroGraphData(self))
etensor_list = list(_CAPI_GetEDataFromHeteroGraphData(self))
ntype_names = list(_CAPI_GetNtypesFromHeteroGraphData(self))
etype_names = list(_CAPI_GetEtypesFromHeteroGraphData(self))
gidx = _CAPI_GetGindexFromHeteroGraphData(self)
nframes = []
eframes = []
for ntid, ntensor in enumerate(ntensor_list):
ndict = {
ntensor[i]: F.zerocopy_from_dgl_ndarray(ntensor[i + 1])
for i in range(0, len(ntensor), 2)
}
nframes.append(Frame(ndict, num_rows=gidx.num_nodes(ntid)))
for etid, etensor in enumerate(etensor_list):
edict = {
etensor[i]: F.zerocopy_from_dgl_ndarray(etensor[i + 1])
for i in range(0, len(etensor), 2)
}
eframes.append(Frame(edict, num_rows=gidx.num_edges(etid)))
return DGLGraph(gidx, ntype_names, etype_names, nframes, eframes)
+456
View File
@@ -0,0 +1,456 @@
"""
Datasets introduced in the 'A Critical Look at the Evaluation of GNNs under Heterophily: Are We
Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
"""
import os
import numpy as np
from ..convert import graph
from ..transforms.functional import to_bidirected
from .dgl_dataset import DGLBuiltinDataset
from .utils import download
class HeterophilousGraphDataset(DGLBuiltinDataset):
r"""Datasets introduced in the 'A Critical Look at the Evaluation of GNNs under Heterophily:
Are We Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
Parameters
----------
name : str
Name of the dataset. One of 'roman-empire', 'amazon-ratings', 'minesweeper', 'tolokers',
'questions'.
raw_dir : str
Raw file directory to store the processed data.
force_reload : bool
Whether to re-download the data source.
verbose : bool
Whether to print progress information.
transform : callable
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
"""
def __init__(
self,
name,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
name = name.lower().replace("-", "_")
url = f"https://github.com/yandex-research/heterophilous-graphs/raw/main/data/{name}.npz"
super(HeterophilousGraphDataset, self).__init__(
name=name,
url=url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def download(self):
download(
url=self.url, path=os.path.join(self.raw_path, f"{self.name}.npz")
)
def process(self):
"""Load and process the data."""
try:
import torch
except ImportError:
raise ModuleNotFoundError(
"This dataset requires PyTorch to be the backend."
)
data = np.load(os.path.join(self.raw_path, f"{self.name}.npz"))
src = torch.from_numpy(data["edges"][:, 0])
dst = torch.from_numpy(data["edges"][:, 1])
features = torch.from_numpy(data["node_features"])
labels = torch.from_numpy(data["node_labels"])
train_masks = torch.from_numpy(data["train_masks"].T)
val_masks = torch.from_numpy(data["val_masks"].T)
test_masks = torch.from_numpy(data["test_masks"].T)
num_nodes = len(labels)
num_classes = len(labels.unique())
self._num_classes = num_classes
self._g = to_bidirected(graph((src, dst), num_nodes=num_nodes))
self._g.ndata["feat"] = features
self._g.ndata["label"] = labels
self._g.ndata["train_mask"] = train_masks
self._g.ndata["val_mask"] = val_masks
self._g.ndata["test_mask"] = test_masks
def has_cache(self):
return os.path.exists(self.raw_path)
def load(self):
self.process()
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._g
else:
return self._transform(self._g)
def __len__(self):
return 1
@property
def num_classes(self):
return self._num_classes
class RomanEmpireDataset(HeterophilousGraphDataset):
r"""Roman-empire dataset from the 'A Critical Look at the Evaluation of GNNs under Heterophily:
Are We Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
This dataset is based on the Roman Empire article from English Wikipedia, which was selected
since it is one of the longest articles on Wikipedia. Each node in the graph corresponds to one
(non-unique) word in the text. Thus, the number of nodes in the graph is equal to the articles
length. Two words are connected with an edge if at least one of the following two conditions
holds: either these words follow each other in the text, or these words are connected in the
dependency tree of the sentence (one word depends on the other). Thus, the graph is a chain
graph with additional shortcut edges corresponding to syntactic dependencies between words. The
class of a node is its syntactic role (17 most frequent roles were selected as unique classes
and all the other roles were grouped into the 18th class). Node features are word embeddings.
Statistics:
- Nodes: 22662
- Edges: 65854
- Classes: 18
- Node features: 300
- 10 train/val/test splits
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import RomanEmpireDataset
>>> dataset = RomanEmpireDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get the first data split
>>> train_mask = g.ndata["train_mask"][:, 0]
>>> val_mask = g.ndata["val_mask"][:, 0]
>>> test_mask = g.ndata["test_mask"][:, 0]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(RomanEmpireDataset, self).__init__(
name="roman-empire",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class AmazonRatingsDataset(HeterophilousGraphDataset):
r"""Amazon-ratings dataset from the 'A Critical Look at the Evaluation of GNNs under
Heterophily: Are We Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
This dataset is based on the Amazon product co-purchasing data. Nodes are products (books, music
CDs, DVDs, VHS video tapes), and edges connect products that are frequently bought together. The
task is to predict the average rating given to a product by reviewers. All possible rating
values were grouped into five classes. Node features are the mean of word embeddings for words
in the product description.
Statistics:
- Nodes: 24492
- Edges: 186100
- Classes: 5
- Node features: 300
- 10 train/val/test splits
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import AmazonRatingsDataset
>>> dataset = AmazonRatingsDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get the first data split
>>> train_mask = g.ndata["train_mask"][:, 0]
>>> val_mask = g.ndata["val_mask"][:, 0]
>>> test_mask = g.ndata["test_mask"][:, 0]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(AmazonRatingsDataset, self).__init__(
name="amazon-ratings",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class MinesweeperDataset(HeterophilousGraphDataset):
r"""Minesweeper dataset from the 'A Critical Look at the Evaluation of GNNs under Heterophily:
Are We Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
This dataset is inspired by the Minesweeper game. The graph is a regular 100x100 grid where each
node (cell) is connected to eight neighboring nodes (with the exception of nodes at the edge of
the grid, which have fewer neighbors). 20% of the nodes are randomly selected as mines. The task
is to predict which nodes are mines. The node features are one-hot-encoded numbers of
neighboring mines. However, for randomly selected 50% of the nodes, the features are unknown,
which is indicated by a separate binary feature.
Statistics:
- Nodes: 10000
- Edges: 78804
- Classes: 2
- Node features: 7
- 10 train/val/test splits
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import MinesweeperDataset
>>> dataset = MinesweeperDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get the first data split
>>> train_mask = g.ndata["train_mask"][:, 0]
>>> val_mask = g.ndata["val_mask"][:, 0]
>>> test_mask = g.ndata["test_mask"][:, 0]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(MinesweeperDataset, self).__init__(
name="minesweeper",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class TolokersDataset(HeterophilousGraphDataset):
r"""Tolokers dataset from the 'A Critical Look at the Evaluation of GNNs under Heterophily:
Are We Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
This dataset is based on data from the Toloka crowdsourcing platform. The nodes represent
tolokers (workers). An edge connects two tolokers if they have worked on the same task. The goal
is to predict which tolokers have been banned in one of the projects. Node features are based on
the workers profile information and task performance statistics.
Statistics:
- Nodes: 11758
- Edges: 1038000
- Classes: 2
- Node features: 10
- 10 train/val/test splits
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import TolokersDataset
>>> dataset = TolokersDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get the first data split
>>> train_mask = g.ndata["train_mask"][:, 0]
>>> val_mask = g.ndata["val_mask"][:, 0]
>>> test_mask = g.ndata["test_mask"][:, 0]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(TolokersDataset, self).__init__(
name="tolokers",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class QuestionsDataset(HeterophilousGraphDataset):
r"""Questions dataset from the 'A Critical Look at the Evaluation of GNNs under Heterophily:
Are We Really Making Progress? <https://arxiv.org/abs/2302.11640>'__ paper.
This dataset is based on data from the question-answering website Yandex Q. Nodes are users, and
an edge connects two nodes if one user answered the other users question. The task is to
predict which users remained active on the website (were not deleted or blocked). Node features
are the mean of word embeddings for words in the user description. Users that do not have
description are indicated by a separate binary feature.
Statistics:
- Nodes: 48921
- Edges: 307080
- Classes: 2
- Node features: 301
- 10 train/val/test splits
Parameters
----------
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download the data source. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import QuestionsDataset
>>> dataset = QuestionsDataset()
>>> g = dataset[0]
>>> num_classes = dataset.num_classes
>>> # get node features
>>> feat = g.ndata["feat"]
>>> # get the first data split
>>> train_mask = g.ndata["train_mask"][:, 0]
>>> val_mask = g.ndata["val_mask"][:, 0]
>>> test_mask = g.ndata["test_mask"][:, 0]
>>> # get labels
>>> label = g.ndata['label']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(QuestionsDataset, self).__init__(
name="questions",
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
+172
View File
@@ -0,0 +1,172 @@
"""ICEWS18 dataset for temporal graph"""
import os
import numpy as np
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs, loadtxt, save_graphs
class ICEWS18Dataset(DGLBuiltinDataset):
r"""ICEWS18 dataset for temporal graph
Integrated Crisis Early Warning System (ICEWS18)
Event data consists of coded interactions between socio-political
actors (i.e., cooperative or hostile actions between individuals,
groups, sectors and nation states). This Dataset consists of events
from 1/1/2018 to 10/31/2018 (24 hours time granularity).
Reference:
- `Recurrent Event Network for Reasoning over Temporal Knowledge Graphs <https://arxiv.org/abs/1904.05530>`_
- `ICEWS Coded Event Data <https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/28075>`_
Statistics
- Train examples: 240
- Valid examples: 30
- Test examples: 34
- Nodes per graph: 23033
Parameters
----------
mode: str
Load train/valid/test data. Has to be one of ['train', 'valid', 'test']
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
-------
is_temporal : bool
Is the dataset contains temporal graphs
Examples
--------
>>> # get train, valid, test set
>>> train_data = ICEWS18Dataset()
>>> valid_data = ICEWS18Dataset(mode='valid')
>>> test_data = ICEWS18Dataset(mode='test')
>>>
>>> train_size = len(train_data)
>>> for g in train_data:
.... e_feat = g.edata['rel_type']
.... # your code here
....
>>>
"""
def __init__(
self,
mode="train",
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
mode = mode.lower()
assert mode in ["train", "valid", "test"], "Mode not valid"
self.mode = mode
_url = _get_dgl_url("dataset/icews18.zip")
super(ICEWS18Dataset, self).__init__(
name="ICEWS18",
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
data = loadtxt(
os.path.join(self.save_path, "{}.txt".format(self.mode)),
delimiter="\t",
).astype(np.int64)
num_nodes = 23033
# The source code is not released, but the paper indicates there're
# totally 137 samples. The cutoff below has exactly 137 samples.
time_index = np.floor(data[:, 3] / 24).astype(np.int64)
start_time = time_index[time_index != -1].min()
end_time = time_index.max()
self._graphs = []
for i in range(start_time, end_time + 1):
row_mask = time_index <= i
edges = data[row_mask][:, [0, 2]]
rate = data[row_mask][:, 1]
g = dgl_graph((edges[:, 0], edges[:, 1]))
g.edata["rel_type"] = F.tensor(
rate.reshape(-1, 1), dtype=F.data_type_dict["int64"]
)
self._graphs.append(g)
def has_cache(self):
graph_path = os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.mode)
)
return os.path.exists(graph_path)
def save(self):
graph_path = os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.mode)
)
save_graphs(graph_path, self._graphs)
def load(self):
graph_path = os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.mode)
)
self._graphs = load_graphs(graph_path)[0]
def __getitem__(self, idx):
r"""Get graph by index
Parameters
----------
idx : int
Item index
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``edata['rel_type']``: edge type
"""
if self._transform is None:
return self._graphs[idx]
else:
return self._transform(self._graphs[idx])
def __len__(self):
r"""Number of graphs in the dataset.
Return
-------
int
"""
return len(self._graphs)
@property
def is_temporal(self):
r"""Is the dataset contains temporal graphs
Returns
-------
bool
"""
return True
ICEWS18 = ICEWS18Dataset
+98
View File
@@ -0,0 +1,98 @@
"""KarateClub Dataset
"""
import networkx as nx
import numpy as np
from .. import backend as F
from ..convert import from_networkx
from .dgl_dataset import DGLDataset
from .utils import deprecate_property
__all__ = ["KarateClubDataset", "KarateClub"]
class KarateClubDataset(DGLDataset):
r"""Karate Club dataset for Node Classification
Zachary's karate club is a social network of a university
karate club, described in the paper "An Information Flow
Model for Conflict and Fission in Small Groups" by Wayne W. Zachary.
The network became a popular example of community structure in
networks after its use by Michelle Girvan and Mark Newman in 2002.
Official website: `<http://konect.cc/networks/ucidata-zachary/>`_
Karate Club dataset statistics:
- Nodes: 34
- Edges: 156
- Number of Classes: 2
Parameters
----------
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> dataset = KarateClubDataset()
>>> num_classes = dataset.num_classes
>>> g = dataset[0]
>>> labels = g.ndata['label']
"""
def __init__(self, transform=None):
super(KarateClubDataset, self).__init__(
name="karate_club", transform=transform
)
def process(self):
kc_graph = nx.karate_club_graph()
label = np.asarray(
[kc_graph.nodes[i]["club"] != "Mr. Hi" for i in kc_graph.nodes]
).astype(np.int64)
label = F.tensor(label)
g = from_networkx(kc_graph)
g.ndata["label"] = label
self._graph = g
self._data = [g]
@property
def num_classes(self):
"""Number of classes."""
return 2
def __getitem__(self, idx):
r"""Get graph object
Parameters
----------
idx : int
Item index, KarateClubDataset has only one graph object
Returns
-------
:class:`dgl.DGLGraph`
graph structure and labels.
- ``ndata['label']``: ground truth labels
"""
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
r"""The number of graphs in the dataset."""
return 1
KarateClub = KarateClubDataset
+779
View File
@@ -0,0 +1,779 @@
from __future__ import absolute_import
import os, sys
import pickle as pkl
import networkx as nx
import numpy as np
import scipy.sparse as sp
from .. import backend as F
from ..convert import graph as dgl_graph
from ..utils import retry_method_with_fix
from .dgl_dataset import DGLBuiltinDataset
from .utils import (
_get_dgl_url,
deprecate_function,
deprecate_property,
download,
extract_archive,
generate_mask_tensor,
get_download_dir,
load_graphs,
load_info,
makedirs,
save_graphs,
save_info,
)
class KnowledgeGraphDataset(DGLBuiltinDataset):
"""KnowledgeGraph link prediction dataset
The dataset contains a graph depicting the connectivity of a knowledge
base. Currently, the knowledge bases from the
`RGCN paper <https://arxiv.org/pdf/1703.06103.pdf>`_ supported are
FB15k-237, FB15k, wn18
Parameters
-----------
name : str
Name can be 'FB15k-237', 'FB15k' or 'wn18'.
reverse : bool
Whether add reverse edges. Default: True.
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
"""
def __init__(
self,
name,
reverse=True,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
self._name = name
self.reverse = reverse
url = _get_dgl_url("dataset/") + "{}.tgz".format(name)
super(KnowledgeGraphDataset, self).__init__(
name,
url=url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def download(self):
r"""Automatically download data and extract it."""
tgz_path = os.path.join(self.raw_dir, self.name + ".tgz")
download(self.url, path=tgz_path)
extract_archive(tgz_path, self.raw_path)
def process(self):
"""
The original knowledge base is stored in triplets.
This function will parse these triplets and build the DGLGraph.
"""
root_path = self.raw_path
entity_path = os.path.join(root_path, "entities.dict")
relation_path = os.path.join(root_path, "relations.dict")
train_path = os.path.join(root_path, "train.txt")
valid_path = os.path.join(root_path, "valid.txt")
test_path = os.path.join(root_path, "test.txt")
entity_dict = _read_dictionary(entity_path)
relation_dict = _read_dictionary(relation_path)
train = np.asarray(
_read_triplets_as_list(train_path, entity_dict, relation_dict)
)
valid = np.asarray(
_read_triplets_as_list(valid_path, entity_dict, relation_dict)
)
test = np.asarray(
_read_triplets_as_list(test_path, entity_dict, relation_dict)
)
num_nodes = len(entity_dict)
num_rels = len(relation_dict)
if self.verbose:
print("# entities: {}".format(num_nodes))
print("# relations: {}".format(num_rels))
print("# training edges: {}".format(train.shape[0]))
print("# validation edges: {}".format(valid.shape[0]))
print("# testing edges: {}".format(test.shape[0]))
# for compatability
self._train = train
self._valid = valid
self._test = test
self._num_nodes = num_nodes
self._num_rels = num_rels
# build graph
g, data = build_knowledge_graph(
num_nodes, num_rels, train, valid, test, reverse=self.reverse
)
(
etype,
ntype,
train_edge_mask,
valid_edge_mask,
test_edge_mask,
train_mask,
val_mask,
test_mask,
) = data
g.edata["train_edge_mask"] = train_edge_mask
g.edata["valid_edge_mask"] = valid_edge_mask
g.edata["test_edge_mask"] = test_edge_mask
g.edata["train_mask"] = train_mask
g.edata["val_mask"] = val_mask
g.edata["test_mask"] = test_mask
g.edata["etype"] = etype
g.ndata["ntype"] = ntype
self._g = g
@property
def graph_path(self):
return os.path.join(self.save_path, self.save_name + ".bin")
@property
def info_path(self):
return os.path.join(self.save_path, self.save_name + ".pkl")
def has_cache(self):
if os.path.exists(self.graph_path) and os.path.exists(self.info_path):
return True
return False
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._g
else:
return self._transform(self._g)
def __len__(self):
return 1
def save(self):
"""save the graph list and the labels"""
save_graphs(str(self.graph_path), self._g)
save_info(
str(self.info_path),
{"num_nodes": self.num_nodes, "num_rels": self.num_rels},
)
def load(self):
graphs, _ = load_graphs(str(self.graph_path))
info = load_info(str(self.info_path))
self._num_nodes = info["num_nodes"]
self._num_rels = info["num_rels"]
self._g = graphs[0]
train_mask = self._g.edata["train_edge_mask"].numpy()
val_mask = self._g.edata["valid_edge_mask"].numpy()
test_mask = self._g.edata["test_edge_mask"].numpy()
# convert mask tensor into bool tensor if possible
self._g.edata["train_edge_mask"] = generate_mask_tensor(
self._g.edata["train_edge_mask"].numpy()
)
self._g.edata["valid_edge_mask"] = generate_mask_tensor(
self._g.edata["valid_edge_mask"].numpy()
)
self._g.edata["test_edge_mask"] = generate_mask_tensor(
self._g.edata["test_edge_mask"].numpy()
)
self._g.edata["train_mask"] = generate_mask_tensor(
self._g.edata["train_mask"].numpy()
)
self._g.edata["val_mask"] = generate_mask_tensor(
self._g.edata["val_mask"].numpy()
)
self._g.edata["test_mask"] = generate_mask_tensor(
self._g.edata["test_mask"].numpy()
)
# for compatability (with 0.4.x) generate train_idx, valid_idx and test_idx
etype = self._g.edata["etype"].numpy()
self._etype = etype
u, v = self._g.all_edges(form="uv")
u = u.numpy()
v = v.numpy()
train_idx = np.nonzero(train_mask == 1)
self._train = np.column_stack(
(u[train_idx], etype[train_idx], v[train_idx])
)
valid_idx = np.nonzero(val_mask == 1)
self._valid = np.column_stack(
(u[valid_idx], etype[valid_idx], v[valid_idx])
)
test_idx = np.nonzero(test_mask == 1)
self._test = np.column_stack(
(u[test_idx], etype[test_idx], v[test_idx])
)
if self.verbose:
print("# entities: {}".format(self.num_nodes))
print("# relations: {}".format(self.num_rels))
print("# training edges: {}".format(self._train.shape[0]))
print("# validation edges: {}".format(self._valid.shape[0]))
print("# testing edges: {}".format(self._test.shape[0]))
@property
def num_nodes(self):
return self._num_nodes
@property
def num_rels(self):
return self._num_rels
@property
def save_name(self):
return self.name + "_dgl_graph"
def _read_dictionary(filename):
d = {}
with open(filename, "r+") as f:
for line in f:
line = line.strip().split("\t")
d[line[1]] = int(line[0])
return d
def _read_triplets(filename):
with open(filename, "r+") as f:
for line in f:
processed_line = line.strip().split("\t")
yield processed_line
def _read_triplets_as_list(filename, entity_dict, relation_dict):
l = []
for triplet in _read_triplets(filename):
s = entity_dict[triplet[0]]
r = relation_dict[triplet[1]]
o = entity_dict[triplet[2]]
l.append([s, r, o])
return l
def build_knowledge_graph(
num_nodes, num_rels, train, valid, test, reverse=True
):
"""Create a DGL Homogeneous graph with heterograph info stored as node or edge features."""
src = []
rel = []
dst = []
raw_subg = {}
raw_subg_eset = {}
raw_subg_etype = {}
raw_reverse_sugb = {}
raw_reverse_subg_eset = {}
raw_reverse_subg_etype = {}
# here there is noly one node type
s_type = "node"
d_type = "node"
def add_edge(s, r, d, reverse, edge_set):
r_type = str(r)
e_type = (s_type, r_type, d_type)
if raw_subg.get(e_type, None) is None:
raw_subg[e_type] = ([], [])
raw_subg_eset[e_type] = []
raw_subg_etype[e_type] = []
raw_subg[e_type][0].append(s)
raw_subg[e_type][1].append(d)
raw_subg_eset[e_type].append(edge_set)
raw_subg_etype[e_type].append(r)
if reverse is True:
r_type = str(r + num_rels)
re_type = (d_type, r_type, s_type)
if raw_reverse_sugb.get(re_type, None) is None:
raw_reverse_sugb[re_type] = ([], [])
raw_reverse_subg_etype[re_type] = []
raw_reverse_subg_eset[re_type] = []
raw_reverse_sugb[re_type][0].append(d)
raw_reverse_sugb[re_type][1].append(s)
raw_reverse_subg_eset[re_type].append(edge_set)
raw_reverse_subg_etype[re_type].append(r + num_rels)
for edge in train:
s, r, d = edge
assert r < num_rels
add_edge(s, r, d, reverse, 1) # train set
for edge in valid:
s, r, d = edge
assert r < num_rels
add_edge(s, r, d, reverse, 2) # valid set
for edge in test:
s, r, d = edge
assert r < num_rels
add_edge(s, r, d, reverse, 3) # test set
subg = []
fg_s = []
fg_d = []
fg_etype = []
fg_settype = []
for e_type, val in raw_subg.items():
s, d = val
s = np.asarray(s)
d = np.asarray(d)
etype = raw_subg_etype[e_type]
etype = np.asarray(etype)
settype = raw_subg_eset[e_type]
settype = np.asarray(settype)
fg_s.append(s)
fg_d.append(d)
fg_etype.append(etype)
fg_settype.append(settype)
settype = np.concatenate(fg_settype)
if reverse is True:
settype = np.concatenate([settype, np.full((settype.shape[0]), 0)])
train_edge_mask = generate_mask_tensor(settype == 1)
valid_edge_mask = generate_mask_tensor(settype == 2)
test_edge_mask = generate_mask_tensor(settype == 3)
for e_type, val in raw_reverse_sugb.items():
s, d = val
s = np.asarray(s)
d = np.asarray(d)
etype = raw_reverse_subg_etype[e_type]
etype = np.asarray(etype)
settype = raw_reverse_subg_eset[e_type]
settype = np.asarray(settype)
fg_s.append(s)
fg_d.append(d)
fg_etype.append(etype)
fg_settype.append(settype)
s = np.concatenate(fg_s)
d = np.concatenate(fg_d)
g = dgl_graph((s, d), num_nodes=num_nodes)
etype = np.concatenate(fg_etype)
settype = np.concatenate(fg_settype)
etype = F.tensor(etype, dtype=F.data_type_dict["int64"])
train_edge_mask = train_edge_mask
valid_edge_mask = valid_edge_mask
test_edge_mask = test_edge_mask
train_mask = (
generate_mask_tensor(settype == 1)
if reverse is True
else train_edge_mask
)
valid_mask = (
generate_mask_tensor(settype == 2)
if reverse is True
else valid_edge_mask
)
test_mask = (
generate_mask_tensor(settype == 3)
if reverse is True
else test_edge_mask
)
ntype = F.full_1d(
num_nodes, 0, dtype=F.data_type_dict["int64"], ctx=F.cpu()
)
return g, (
etype,
ntype,
train_edge_mask,
valid_edge_mask,
test_edge_mask,
train_mask,
valid_mask,
test_mask,
)
class FB15k237Dataset(KnowledgeGraphDataset):
r"""FB15k237 link prediction dataset.
FB15k-237 is a subset of FB15k where inverse
relations are removed. When creating the dataset,
a reverse edge with reversed relation types are
created for each edge by default.
FB15k237 dataset statistics:
- Nodes: 14541
- Number of relation types: 237
- Number of reversed relation types: 237
- Label Split:
- Train: 272115
- Valid: 17535
- Test: 20466
Parameters
----------
reverse : bool
Whether to add reverse edge. Default True.
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_nodes: int
Number of nodes
num_rels: int
Number of relation types
Examples
----------
>>> dataset = FB15k237Dataset()
>>> g = dataset.graph
>>> e_type = g.edata['e_type']
>>>
>>> # get data split
>>> train_mask = g.edata['train_mask']
>>> val_mask = g.edata['val_mask']
>>> test_mask = g.edata['test_mask']
>>>
>>> train_set = th.arange(g.num_edges())[train_mask]
>>> val_set = th.arange(g.num_edges())[val_mask]
>>>
>>> # build train_g
>>> train_edges = train_set
>>> train_g = g.edge_subgraph(train_edges,
relabel_nodes=False)
>>> train_g.edata['e_type'] = e_type[train_edges];
>>>
>>> # build val_g
>>> val_edges = th.cat([train_edges, val_edges])
>>> val_g = g.edge_subgraph(val_edges,
relabel_nodes=False)
>>> val_g.edata['e_type'] = e_type[val_edges];
>>>
>>> # Train, Validation and Test
"""
def __init__(
self,
reverse=True,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
name = "FB15k-237"
super(FB15k237Dataset, self).__init__(
name, reverse, raw_dir, force_reload, verbose, transform
)
def __getitem__(self, idx):
r"""Gets the graph object
Parameters
-----------
idx: int
Item index, FB15k237Dataset has only one graph object
Return
-------
:class:`dgl.DGLGraph`
The graph contains
- ``edata['e_type']``: edge relation type
- ``edata['train_edge_mask']``: positive training edge mask
- ``edata['val_edge_mask']``: positive validation edge mask
- ``edata['test_edge_mask']``: positive testing edge mask
- ``edata['train_mask']``: training edge set mask (include reversed training edges)
- ``edata['val_mask']``: validation edge set mask (include reversed validation edges)
- ``edata['test_mask']``: testing edge set mask (include reversed testing edges)
- ``ndata['ntype']``: node type. All 0 in this dataset
"""
return super(FB15k237Dataset, self).__getitem__(idx)
def __len__(self):
r"""The number of graphs in the dataset."""
return super(FB15k237Dataset, self).__len__()
class FB15kDataset(KnowledgeGraphDataset):
r"""FB15k link prediction dataset.
The FB15K dataset was introduced in `Translating Embeddings for Modeling
Multi-relational Data <http://papers.nips.cc/paper/5071-translating-embeddings-for-modeling-multi-relational-data.pdf>`_.
It is a subset of Freebase which contains about
14,951 entities with 1,345 different relations.
When creating the dataset, a reverse edge with
reversed relation types are created for each edge
by default.
FB15k dataset statistics:
- Nodes: 14,951
- Number of relation types: 1,345
- Number of reversed relation types: 1,345
- Label Split:
- Train: 483142
- Valid: 50000
- Test: 59071
Parameters
----------
reverse : bool
Whether to add reverse edge. Default True.
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_nodes: int
Number of nodes
num_rels: int
Number of relation types
Examples
----------
>>> dataset = FB15kDataset()
>>> g = dataset.graph
>>> e_type = g.edata['e_type']
>>>
>>> # get data split
>>> train_mask = g.edata['train_mask']
>>> val_mask = g.edata['val_mask']
>>>
>>> train_set = th.arange(g.num_edges())[train_mask]
>>> val_set = th.arange(g.num_edges())[val_mask]
>>>
>>> # build train_g
>>> train_edges = train_set
>>> train_g = g.edge_subgraph(train_edges,
relabel_nodes=False)
>>> train_g.edata['e_type'] = e_type[train_edges];
>>>
>>> # build val_g
>>> val_edges = th.cat([train_edges, val_edges])
>>> val_g = g.edge_subgraph(val_edges,
relabel_nodes=False)
>>> val_g.edata['e_type'] = e_type[val_edges];
>>>
>>> # Train, Validation and Test
>>>
"""
def __init__(
self,
reverse=True,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
name = "FB15k"
super(FB15kDataset, self).__init__(
name, reverse, raw_dir, force_reload, verbose, transform
)
def __getitem__(self, idx):
r"""Gets the graph object
Parameters
-----------
idx: int
Item index, FB15kDataset has only one graph object
Return
-------
:class:`dgl.DGLGraph`
The graph contains
- ``edata['e_type']``: edge relation type
- ``edata['train_edge_mask']``: positive training edge mask
- ``edata['val_edge_mask']``: positive validation edge mask
- ``edata['test_edge_mask']``: positive testing edge mask
- ``edata['train_mask']``: training edge set mask (include reversed training edges)
- ``edata['val_mask']``: validation edge set mask (include reversed validation edges)
- ``edata['test_mask']``: testing edge set mask (include reversed testing edges)
- ``ndata['ntype']``: node type. All 0 in this dataset
"""
return super(FB15kDataset, self).__getitem__(idx)
def __len__(self):
r"""The number of graphs in the dataset."""
return super(FB15kDataset, self).__len__()
class WN18Dataset(KnowledgeGraphDataset):
r"""WN18 link prediction dataset.
The WN18 dataset was introduced in `Translating Embeddings for Modeling
Multi-relational Data <http://papers.nips.cc/paper/5071-translating-embeddings-for-modeling-multi-relational-data.pdf>`_.
It included the full 18 relations scraped from
WordNet for roughly 41,000 synsets. When creating
the dataset, a reverse edge with reversed relation
types are created for each edge by default.
WN18 dataset statistics:
- Nodes: 40943
- Number of relation types: 18
- Number of reversed relation types: 18
- Label Split:
- Train: 141442
- Valid: 5000
- Test: 5000
Parameters
----------
reverse : bool
Whether to add reverse edge. Default True.
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_nodes: int
Number of nodes
num_rels: int
Number of relation types
Examples
----------
>>> dataset = WN18Dataset()
>>> g = dataset.graph
>>> e_type = g.edata['e_type']
>>>
>>> # get data split
>>> train_mask = g.edata['train_mask']
>>> val_mask = g.edata['val_mask']
>>>
>>> train_set = th.arange(g.num_edges())[train_mask]
>>> val_set = th.arange(g.num_edges())[val_mask]
>>>
>>> # build train_g
>>> train_edges = train_set
>>> train_g = g.edge_subgraph(train_edges,
relabel_nodes=False)
>>> train_g.edata['e_type'] = e_type[train_edges];
>>>
>>> # build val_g
>>> val_edges = th.cat([train_edges, val_edges])
>>> val_g = g.edge_subgraph(val_edges,
relabel_nodes=False)
>>> val_g.edata['e_type'] = e_type[val_edges];
>>>
>>> # Train, Validation and Test
>>>
"""
def __init__(
self,
reverse=True,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
name = "wn18"
super(WN18Dataset, self).__init__(
name, reverse, raw_dir, force_reload, verbose, transform
)
def __getitem__(self, idx):
r"""Gets the graph object
Parameters
-----------
idx: int
Item index, WN18Dataset has only one graph object
Return
-------
:class:`dgl.DGLGraph`
The graph contains
- ``edata['e_type']``: edge relation type
- ``edata['train_edge_mask']``: positive training edge mask
- ``edata['val_edge_mask']``: positive validation edge mask
- ``edata['test_edge_mask']``: positive testing edge mask
- ``edata['train_mask']``: training edge set mask (include reversed training edges)
- ``edata['val_mask']``: validation edge set mask (include reversed validation edges)
- ``edata['test_mask']``: testing edge set mask (include reversed testing edges)
- ``ndata['ntype']``: node type. All 0 in this dataset
"""
return super(WN18Dataset, self).__getitem__(idx)
def __len__(self):
r"""The number of graphs in the dataset."""
return super(WN18Dataset, self).__len__()
def load_data(dataset):
r"""Load knowledge graph dataset for RGCN link prediction tasks
It supports three datasets: wn18, FB15k and FB15k-237
Parameters
----------
dataset: str
The name of the dataset to load.
Return
------
The dataset object.
"""
if dataset == "wn18":
return WN18Dataset()
elif dataset == "FB15k":
return FB15kDataset()
elif dataset == "FB15k-237":
return FB15k237Dataset()
File diff suppressed because it is too large Load Diff
+248
View File
@@ -0,0 +1,248 @@
"""A mini synthetic dataset for graph classification benchmark."""
import math
import os
import networkx as nx
import numpy as np
from .. import backend as F
from ..convert import from_networkx
from ..transforms import add_self_loop
from .dgl_dataset import DGLDataset
from .utils import load_graphs, makedirs, save_graphs
__all__ = ["MiniGCDataset"]
class MiniGCDataset(DGLDataset):
"""The synthetic graph classification dataset class.
The datset contains 8 different types of graphs.
- class 0 : cycle graph
- class 1 : star graph
- class 2 : wheel graph
- class 3 : lollipop graph
- class 4 : hypercube graph
- class 5 : grid graph
- class 6 : clique graph
- class 7 : circular ladder graph
Parameters
----------
num_graphs: int
Number of graphs in this dataset.
min_num_v: int
Minimum number of nodes for graphs
max_num_v: int
Maximum number of nodes for graphs
seed: int, default is 0
Random seed for data generation
transform: callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_graphs : int
Number of graphs
min_num_v : int
The minimum number of nodes
max_num_v : int
The maximum number of nodes
num_classes : int
The number of classes
Examples
--------
>>> data = MiniGCDataset(100, 16, 32, seed=0)
The dataset instance is an iterable
>>> len(data)
100
>>> g, label = data[64]
>>> g
Graph(num_nodes=20, num_edges=82,
ndata_schemes={}
edata_schemes={})
>>> label
tensor(5)
Batch the graphs and labels for mini-batch training
>>> graphs, labels = zip(*[data[i] for i in range(16)])
>>> batched_graphs = dgl.batch(graphs)
>>> batched_labels = torch.tensor(labels)
>>> batched_graphs
Graph(num_nodes=356, num_edges=1060,
ndata_schemes={}
edata_schemes={})
"""
def __init__(
self,
num_graphs,
min_num_v,
max_num_v,
seed=0,
save_graph=True,
force_reload=False,
verbose=False,
transform=None,
):
self.num_graphs = num_graphs
self.min_num_v = min_num_v
self.max_num_v = max_num_v
self.seed = seed
self.save_graph = save_graph
super(MiniGCDataset, self).__init__(
name="minigc",
hash_key=(num_graphs, min_num_v, max_num_v, seed),
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
self.graphs = []
self.labels = []
self._generate(self.seed)
def __len__(self):
"""Return the number of graphs in the dataset."""
return len(self.graphs)
def __getitem__(self, idx):
"""Get the idx-th sample.
Parameters
---------
idx : int
The sample index.
Returns
-------
(:class:`dgl.Graph`, Tensor)
The graph and its label.
"""
if self._transform is None:
g = self.graphs[idx]
else:
g = self._transform(self.graphs[idx])
return g, self.labels[idx]
def has_cache(self):
graph_path = os.path.join(
self.save_path, "dgl_graph_{}.bin".format(self.hash)
)
if os.path.exists(graph_path):
return True
return False
def save(self):
"""save the graph list and the labels"""
if self.save_graph:
graph_path = os.path.join(
self.save_path, "dgl_graph_{}.bin".format(self.hash)
)
save_graphs(str(graph_path), self.graphs, {"labels": self.labels})
def load(self):
graphs, label_dict = load_graphs(
os.path.join(self.save_path, "dgl_graph_{}.bin".format(self.hash))
)
self.graphs = graphs
self.labels = label_dict["labels"]
@property
def num_classes(self):
"""Number of classes."""
return 8
def _generate(self, seed):
if seed is not None:
np.random.seed(seed)
self._gen_cycle(self.num_graphs // 8)
self._gen_star(self.num_graphs // 8)
self._gen_wheel(self.num_graphs // 8)
self._gen_lollipop(self.num_graphs // 8)
self._gen_hypercube(self.num_graphs // 8)
self._gen_grid(self.num_graphs // 8)
self._gen_clique(self.num_graphs // 8)
self._gen_circular_ladder(self.num_graphs - len(self.graphs))
# preprocess
for i in range(self.num_graphs):
# convert to DGLGraph, and add self loops
self.graphs[i] = add_self_loop(from_networkx(self.graphs[i]))
self.labels = F.tensor(np.array(self.labels).astype(np.int64))
def _gen_cycle(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
g = nx.cycle_graph(num_v)
self.graphs.append(g)
self.labels.append(0)
def _gen_star(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
# nx.star_graph(N) gives a star graph with N+1 nodes
g = nx.star_graph(num_v - 1)
self.graphs.append(g)
self.labels.append(1)
def _gen_wheel(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
g = nx.wheel_graph(num_v)
self.graphs.append(g)
self.labels.append(2)
def _gen_lollipop(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
path_len = np.random.randint(2, num_v // 2)
g = nx.lollipop_graph(m=num_v - path_len, n=path_len)
self.graphs.append(g)
self.labels.append(3)
def _gen_hypercube(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
g = nx.hypercube_graph(int(math.log(num_v, 2)))
g = nx.convert_node_labels_to_integers(g)
self.graphs.append(g)
self.labels.append(4)
def _gen_grid(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
assert num_v >= 4, (
"We require a grid graph to contain at least two "
"rows and two columns, thus 4 nodes, got {:d} "
"nodes".format(num_v)
)
n_rows = np.random.randint(2, num_v // 2)
n_cols = num_v // n_rows
g = nx.grid_graph([n_rows, n_cols])
g = nx.convert_node_labels_to_integers(g)
self.graphs.append(g)
self.labels.append(5)
def _gen_clique(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
g = nx.complete_graph(num_v)
self.graphs.append(g)
self.labels.append(6)
def _gen_circular_ladder(self, n):
for _ in range(n):
num_v = np.random.randint(self.min_num_v, self.max_num_v)
g = nx.circular_ladder_graph(num_v // 2)
self.graphs.append(g)
self.labels.append(7)
+646
View File
@@ -0,0 +1,646 @@
"""MovieLens dataset"""
import os
import numpy as np
import pandas as pd
from torch import LongTensor, Tensor
from ..base import dgl_warning
from ..convert import heterograph
from .dgl_dataset import DGLDataset
from .utils import (
_get_dgl_url,
download,
extract_archive,
load_graphs,
load_info,
save_graphs,
save_info,
split_dataset,
)
GENRES_ML_100K = [
"unknown",
"Action",
"Adventure",
"Animation",
"Children",
"Comedy",
"Crime",
"Documentary",
"Drama",
"Fantasy",
"Film-Noir",
"Horror",
"Musical",
"Mystery",
"Romance",
"Sci-Fi",
"Thriller",
"War",
"Western",
]
GENRES_ML_1M = GENRES_ML_100K[1:]
GENRES_ML_10M = GENRES_ML_100K + ["IMAX"]
try:
import torch
except ImportError:
HAS_TORCH = False
else:
HAS_TORCH = True
def check_pytorch():
"""Check if PyTorch is the backend."""
if not HAS_TORCH:
raise ModuleNotFoundError(
"MovieLensDataset requires PyTorch to be the backend."
)
class MovieLensDataset(DGLDataset):
r"""MovieLens dataset for edge prediction tasks. The raw datasets are extracted from
`MovieLens <https://grouplens.org/datasets/movielens/>`, introduced by
`Movielens unplugged: experiences with an occasionally connected recommender system <https://dl.acm.org/doi/10.1145/604045.604094>`.
The datasets consist of user ratings for movies and incorporate additional user/movie information in the form of features.
The nodes represent users and movies, and the edges store ratings that users assign to movies.
Statistics:
MovieLens-100K (ml-100k)
- Users: 943
- Movies: 1,682
- Ratings: 100,000 (1, 2, 3, 4, 5)
MovieLens-1M (ml-1m)
- Users: 6,040
- Movies: 3,706
- Ratings: 1,000,209 (1, 2, 3, 4, 5)
MovieLens-10M (ml-10m)
- Users: 69,878
- Movies: 10,677
- Ratings: 10,000,054 (0.5, 1, 1.5, ..., 4.5, 5.0)
Parameters
----------
name: str
Dataset name. (:obj:`"ml-100k"`, :obj:`"ml-1m"`, :obj:`"ml-10m"`).
valid_ratio: int
Ratio of validation samples out of the whole dataset. Should be in (0.0, 1.0).
test_ratio: int, optional
Ratio of testing samples out of the whole dataset. Should be in (0.0, 1.0). And its sum with
:obj:`valid_ratio` should be in (0.0, 1.0) as well. This parameter is invalid
when :obj:`name` is :obj:`"ml-100k"`, since its testing samples are pre-specified.
Default: None
raw_dir : str, optional
Raw file directory to download/store the data.
Default: ~/.dgl/
force_reload : bool, optional
Whether to re-download(if the dataset has not been downloaded) and re-process the dataset.
Default: False
verbose : bool, optional
Whether to print progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
random_state : int, optional
Random seed used for random dataset split. Default: 0
Notes
-----
- When :obj:`name` is :obj:`"ml-100k"`, the :obj:`test_ratio` is invalid, and the training ratio is equal to 1-:obj:`valid_ratio`.
When :obj:`name` is :obj:`"ml-1m"` or :obj:`"ml-10m"`, the :obj:`test_ratio` is valid,
and the training ratio is equal to 1-:obj:`valid_ratio`-:obj:`test_ratio`.
- The number of edges is doubled to form an undirected(bidirected) graph structure.
Examples
--------
>>> from dgl.data import MovieLensDataset
>>> dataset = MovieLensDataset(name='ml-100k', valid_ratio=0.2)
>>> g = dataset[0]
>>> g
Graph(num_nodes={'movie': 1682, 'user': 943},
num_edges={('movie', 'movie-user', 'user'): 100000, ('user', 'user-movie', 'movie'): 100000},
metagraph=[('movie', 'user', 'movie-user'), ('user', 'movie', 'user-movie')])
>>> # get ratings of edges in the training graph.
>>> rate = g.edges['user-movie'].data['rate'] # or rate = g.edges['movie-user'].data['rate']
>>> rate
tensor([5., 5., 3., ..., 3., 3., 5.])
>>> # get train, valid and test mask of edges
>>> train_mask = g.edges['user-movie'].data['train_mask']
>>> valid_mask = g.edges['user-movie'].data['valid_mask']
>>> test_mask = g.edges['user-movie'].data['test_mask']
>>> # get train, valid and test ratings
>>> train_ratings = rate[train_mask]
>>> valid_ratings = rate[valid_mask]
>>> test_ratings = rate[test_mask]
>>> # get input features of users
>>> g.nodes["user"].data["feat"] # or g.nodes["movie"].data["feat"] for movie nodes
tensor([[0.4800, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
[1.0600, 1.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
[0.4600, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
...,
[0.4000, 0.0000, 1.0000, ..., 0.0000, 0.0000, 0.0000],
[0.9600, 1.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
[0.4400, 0.0000, 1.0000, ..., 0.0000, 0.0000, 0.0000]])
"""
_url = {
"ml-100k": "dataset/ml-100k.zip",
"ml-1m": "dataset/ml-1m.zip",
"ml-10m": "dataset/ml-10m.zip",
}
def __init__(
self,
name,
valid_ratio,
test_ratio=None,
raw_dir=None,
force_reload=None,
verbose=None,
transform=None,
random_state=0,
):
check_pytorch()
assert name in [
"ml-100k",
"ml-1m",
"ml-10m",
], f"currently movielens does not support {name}"
# test regarding valid and test split ratio
assert (
valid_ratio > 0.0 and valid_ratio < 1.0
), f"valid_ratio {valid_ratio} must be in (0.0, 1.0)"
if name in ["ml-1m", "ml-10m"]:
assert (
test_ratio is not None and test_ratio > 0.0 and test_ratio < 1.0
), f"test_ratio({test_ratio}) must be set to a value in (0.0, 1.0) when using ml-1m and ml-10m"
assert (
test_ratio + valid_ratio > 0.0
and test_ratio + valid_ratio < 1.0
), f"test_ratio({test_ratio}) + valid_ratio({valid_ratio}) must be set to (0.0, 1.0) when using ml-1m and ml-10m"
if name == "ml-100k" and test_ratio is not None:
dgl_warning(
f"test_ratio ({test_ratio}) is not set to None for ml-100k. "
"Note that dataset split would not be affected by the test_ratio since "
"testing samples of ml-100k have been pre-specified."
)
self.valid_ratio = valid_ratio
self.test_ratio = test_ratio
self.random_state = random_state
if name == "ml-100k":
self.genres = GENRES_ML_100K
elif name == "ml-1m":
self.genres = GENRES_ML_1M
elif name == "ml-10m":
self.genres = GENRES_ML_10M
else:
raise NotImplementedError
super(MovieLensDataset, self).__init__(
name=name,
url=_get_dgl_url(self._url[name]),
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def check_version(self):
valid_ratio, test_ratio = load_info(self.version_path)
if self.valid_ratio == valid_ratio and (
self.test_ratio == test_ratio if self.name != "ml-100k" else True
):
return True
else:
if self.name == "ml-100k":
print(
f"The current valid ratio ({self.valid_ratio}) "
"is not the same as the last setting "
f"(valid: {valid_ratio}). "
f"MovieLens {self.name} will be re-processed with the new dataset split setting."
)
else:
print(
f"At least one of current valid ({self.valid_ratio}) and test ({self.test_ratio}) ratio "
"are not the same as the last setting "
f"(valid: {valid_ratio}, test: {test_ratio}). "
f"MovieLens {self.name} will be re-processed with the new dataset split setting."
)
return False
def download(self):
zip_file_path = os.path.join(self.raw_dir, self.name + ".zip")
download(self.url, path=zip_file_path)
extract_archive(zip_file_path, self.raw_dir, overwrite=True)
def process(self):
print(f"Starting processing {self.name} ...")
# 0. loading movie features
movie_feat = load_info(
os.path.join(self.raw_path, "movie_feat.pkl")
).to(torch.float)
# 1. dataset split: train + (valid + ) test
if self.name == "ml-100k":
train_rating_data = self._load_raw_rates(
os.path.join(self.raw_path, "u1.base"), "\t"
)
test_rating_data = self._load_raw_rates(
os.path.join(self.raw_path, "u1.test"), "\t"
)
indices = np.arange(len(train_rating_data))
train, valid, _ = split_dataset(
indices,
[1 - self.valid_ratio, self.valid_ratio, 0.0],
shuffle=True,
random_state=self.random_state,
)
train_rating_data, valid_rating_data = (
train_rating_data.iloc[train.indices],
train_rating_data.iloc[valid.indices],
)
all_rating_data = pd.concat(
[train_rating_data, valid_rating_data, test_rating_data]
)
elif self.name == "ml-1m" or self.name == "ml-10m":
all_rating_data = self._load_raw_rates(
os.path.join(self.raw_path, "ratings.dat"), "::"
)
indices = np.arange(len(all_rating_data))
train, valid, test = split_dataset(
indices,
[
1 - self.valid_ratio - self.test_ratio,
self.valid_ratio,
self.test_ratio,
],
shuffle=True,
random_state=self.random_state,
)
train_rating_data, valid_rating_data, test_rating_data = (
all_rating_data.iloc[train.indices],
all_rating_data.iloc[valid.indices],
all_rating_data.iloc[test.indices],
)
# 2. load user and movie data, and drop those unseen in rating_data
user_data = self._load_raw_user_data()
movie_data = self._load_raw_movie_data()
user_data = self._drop_unseen_nodes(
data_df=user_data,
col_name="id",
reserved_ids_set=set(all_rating_data["user_id"].values),
)
movie_data = self._drop_unseen_nodes(
data_df=movie_data,
col_name="id",
reserved_ids_set=set(all_rating_data["movie_id"].values),
)
user_feat = Tensor(self._process_user_feat(user_data))
# 3. generate rating pairs
# Map user/movie to the global id
self._global_user_id_map = {
ele: i for i, ele in enumerate(user_data["id"])
}
self._global_movie_id_map = {
ele: i for i, ele in enumerate(movie_data["id"])
}
# pair value is idx rather than id
u_indices, v_indices, labels = self._generate_pair_value(
all_rating_data
)
all_rating_pairs = (
LongTensor(u_indices),
LongTensor(v_indices),
)
all_rating_values = Tensor(labels)
graph = self.construct_g(
all_rating_pairs, all_rating_values, user_feat, movie_feat
)
self.graph = self.add_masks(
graph, train_rating_data, valid_rating_data, test_rating_data
)
print(f"End processing {self.name} ...")
def construct_g(self, rate_pairs, rate_values, user_feat, movie_feat):
g = heterograph(
{
("user", "user-movie", "movie"): (rate_pairs[0], rate_pairs[1]),
("movie", "movie-user", "user"): (rate_pairs[1], rate_pairs[0]),
}
)
ndata = {"user": user_feat, "movie": movie_feat}
edata = {"user-movie": rate_values, "movie-user": rate_values}
g.ndata["feat"] = ndata
g.edata["rate"] = edata
return g
def add_masks(
self, g, train_rating_data, valid_rating_data, test_rating_data
):
train_u_indices, train_v_indices, _ = self._generate_pair_value(
train_rating_data
)
valid_u_indices, valid_v_indices, _ = self._generate_pair_value(
valid_rating_data
)
test_u_indices, test_v_indices, _ = self._generate_pair_value(
test_rating_data
)
# user-movie
train_mask = torch.zeros((g.num_edges("user-movie"),), dtype=torch.bool)
train_mask[
g.edge_ids(train_u_indices, train_v_indices, etype="user-movie")
] = True
valid_mask = torch.zeros((g.num_edges("user-movie"),), dtype=torch.bool)
valid_mask[
g.edge_ids(valid_u_indices, valid_v_indices, etype="user-movie")
] = True
test_mask = torch.zeros((g.num_edges("user-movie"),), dtype=torch.bool)
test_mask[
g.edge_ids(test_u_indices, test_v_indices, etype="user-movie")
] = True
g.edges["user-movie"].data["train_mask"] = train_mask
g.edges["user-movie"].data["valid_mask"] = valid_mask
g.edges["user-movie"].data["test_mask"] = test_mask
# movie-user
train_mask_rev = torch.zeros(
(g.num_edges("movie-user"),), dtype=torch.bool
)
train_mask_rev[
g.edge_ids(train_v_indices, train_u_indices, etype="movie-user")
] = True
valid_mask_rev = torch.zeros(
(g.num_edges("movie-user"),), dtype=torch.bool
)
valid_mask_rev[
g.edge_ids(valid_v_indices, valid_u_indices, etype="movie-user")
] = True
test_mask_rev = torch.zeros(
(g.num_edges("movie-user"),), dtype=torch.bool
)
test_mask_rev[
g.edge_ids(test_v_indices, test_u_indices, etype="movie-user")
] = True
g.edges["movie-user"].data["train_mask"] = train_mask_rev
g.edges["movie-user"].data["valid_mask"] = valid_mask_rev
g.edges["movie-user"].data["test_mask"] = test_mask_rev
return g
def has_cache(self):
if (
os.path.exists(self.graph_path)
and os.path.exists(self.version_path)
and self.check_version()
):
return True
return False
def save(self):
save_graphs(self.graph_path, [self.graph])
save_info(self.version_path, [self.valid_ratio, self.test_ratio])
if self.verbose:
print(f"Done saving data into {self.raw_path}.")
def load(self):
g_list, _ = load_graphs(self.graph_path)
self.graph = g_list[0]
"""
To avoid the problem each time loading boolean tensor from the disk, boolean values
would be automatically converted into torch.uint8 types, and a deprecation warning would
be raised for using torch.uint8
"""
for e in self.graph.etypes:
self.graph.edges[e].data["train_mask"] = (
self.graph.edges[e].data["train_mask"].to(torch.bool)
)
self.graph.edges[e].data["valid_mask"] = (
self.graph.edges[e].data["valid_mask"].to(torch.bool)
)
self.graph.edges[e].data["test_mask"] = (
self.graph.edges[e].data["test_mask"].to(torch.bool)
)
def __getitem__(self, idx):
assert (
idx == 0
), "This dataset has only one set of training, validation and testing graph"
if self._transform is None:
return self.graph
else:
return self._transform(self.graph)
def __len__(self):
return 1
@property
def raw_path(self):
return os.path.join(self.raw_dir, self.name)
@property
def graph_path(self):
return os.path.join(self.raw_path, self.name + ".bin")
@property
def version_path(self):
return os.path.join(self.raw_path, self.name + "_version.pkl")
def _process_user_feat(self, user_data):
if self.name == "ml-100k" or self.name == "ml-1m":
ages = user_data["age"].values.astype(np.float32)
gender = (user_data["gender"] == "F").values.astype(np.float32)
all_occupations = set(user_data["occupation"])
occupation_map = {ele: i for i, ele in enumerate(all_occupations)}
occupation_one_hot = np.zeros(
shape=(user_data.shape[0], len(all_occupations)),
dtype=np.float32,
)
occupation_one_hot[
np.arange(user_data.shape[0]),
np.array(
[occupation_map[ele] for ele in user_data["occupation"]]
),
] = 1
user_features = np.concatenate(
[
ages.reshape((user_data.shape[0], 1)) / 50.0,
gender.reshape((user_data.shape[0], 1)),
occupation_one_hot,
],
axis=1,
)
elif self.name == "ml-10m":
user_features = np.zeros(
shape=(user_data.shape[0], 1), dtype=np.float32
)
else:
raise NotImplementedError
return user_features
def _load_raw_user_data(self):
if self.name == "ml-100k":
user_data = pd.read_csv(
os.path.join(self.raw_path, "u.user"),
sep="|",
header=None,
names=["id", "age", "gender", "occupation", "zip_code"],
engine="python",
)
elif self.name == "ml-1m":
user_data = pd.read_csv(
os.path.join(self.raw_path, "users.dat"),
sep="::",
header=None,
names=["id", "gender", "age", "occupation", "zip_code"],
engine="python",
)
elif self.name == "ml-10m":
rating_info = pd.read_csv(
os.path.join(self.raw_path, "ratings.dat"),
sep="::",
header=None,
names=["user_id", "movie_id", "rating", "timestamp"],
dtype={
"user_id": np.int32,
"movie_id": np.int32,
"ratings": np.float32,
"timestamp": np.int64,
},
engine="python",
)
user_data = pd.DataFrame(
np.unique(rating_info["user_id"].values.astype(np.int32)),
columns=["id"],
)
else:
raise NotImplementedError
return user_data
def _load_raw_movie_data(self):
file_path = os.path.join(self.raw_path, "u.item")
if self.name == "ml-100k":
movie_data = pd.read_csv(
file_path,
sep="|",
header=None,
names=[
"id",
"title",
"release_date",
"video_release_date",
"url",
]
+ GENRES_ML_100K,
engine="python",
encoding="ISO-8859-1",
)
elif self.name == "ml-1m" or self.name == "ml-10m":
file_path = os.path.join(self.raw_path, "movies.dat")
movie_data = pd.read_csv(
file_path,
sep="::",
header=None,
names=["id", "title", "genres"],
encoding="iso-8859-1",
engine="python",
)
genre_map = {ele: i for i, ele in enumerate(self.genres)}
genre_map["Children's"] = genre_map["Children"]
genre_map["Childrens"] = genre_map["Children"]
movie_genres = np.zeros(
shape=(movie_data.shape[0], len(self.genres)), dtype=np.float32
)
for i, genres in enumerate(movie_data["genres"]):
for ele in genres.split("|"):
if ele in genre_map:
movie_genres[i, genre_map[ele]] = 1.0
else:
movie_genres[i, genre_map["unknown"]] = 1.0
for idx, genre_name in enumerate(self.genres):
movie_data[genre_name] = movie_genres[:, idx]
movie_data = movie_data.drop(columns=["genres"])
else:
raise NotImplementedError
return movie_data
def _load_raw_rates(self, file_path, sep):
rating_data = pd.read_csv(
file_path,
sep=sep,
header=None,
names=["user_id", "movie_id", "rating", "timestamp"],
dtype={
"user_id": np.int32,
"movie_id": np.int32,
"ratings": np.float32,
"timestamp": np.int64,
},
engine="python",
)
rating_data = rating_data.reset_index(drop=True)
return rating_data
def _drop_unseen_nodes(self, data_df, col_name, reserved_ids_set):
data_df = data_df[data_df[col_name].isin(reserved_ids_set)]
data_df.reset_index(drop=True, inplace=True)
return data_df
def _generate_pair_value(self, rating_data):
rating_pairs = (
np.array(
[
self._global_user_id_map[ele]
for ele in rating_data["user_id"]
],
dtype=np.int32,
),
np.array(
[
self._global_movie_id_map[ele]
for ele in rating_data["movie_id"]
],
dtype=np.int32,
),
)
rating_values = rating_data["rating"].values.astype(np.float32)
return rating_pairs[0], rating_pairs[1], rating_values
def __repr__(self):
return (
f'Dataset("{self.name}", num_graphs={len(self)},'
+ f" save_path={self.raw_path}), valid_ratio={self.valid_ratio}, test_ratio={self.test_ratio}"
)
+130
View File
@@ -0,0 +1,130 @@
""" PATTERNDataset for inductive learning. """
import os
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs
class PATTERNDataset(DGLBuiltinDataset):
r"""PATTERN dataset for graph pattern recognition task.
Each graph G contains 5 communities with sizes randomly selected between [5, 35].
The SBM of each community is p = 0.5, q = 0.35, and the node features on G are
generated with a uniform random distribution with a vocabulary of size 3, i.e. {0, 1, 2}.
Then randomly generate 100 patterns P composed of 20 nodes with intra-probability :math:`p_P` = 0.5
and extra-probability :math:`q_P` = 0.5 (i.e. 50% of nodes in P are connected to G). The node features
for P are also generated as a random signal with values {0, 1, 2}. The graphs are of sizes
44-188 nodes. The output node labels have value 1 if the node belongs to P and value 0 if it is in G.
Reference `<https://arxiv.org/pdf/2003.00982.pdf>`_
Statistics:
- Train examples: 10,000
- Valid examples: 2,000
- Test examples: 2,000
- Number of classes for each node: 2
Parameters
----------
mode : str
Must be one of ('train', 'valid', 'test').
Default: 'train'
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose : bool
Whether to print out progress information.
Default: False
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node.
Examples
--------
>>> from dgl.data import PATTERNDataset
>>> data = PATTERNDataset(mode='train')
>>> data.num_classes
2
>>> len(trainset)
10000
>>> data[0]
Graph(num_nodes=108, num_edges=4884, ndata_schemes={'feat': Scheme(shape=(), dtype=torch.int64), 'label': Scheme(shape=(), dtype=torch.int16)}
edata_schemes={'feat': Scheme(shape=(1,), dtype=torch.float32)})
"""
def __init__(
self,
mode="train",
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
assert mode in ["train", "valid", "test"]
self.mode = mode
_url = _get_dgl_url("dataset/SBM_PATTERN.zip")
super(PATTERNDataset, self).__init__(
name="pattern",
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
self.load()
@property
def graph_path(self):
return os.path.join(
self.save_path, "SBM_PATTERN_{}.bin".format(self.mode)
)
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
self._graphs, _ = load_graphs(self.graph_path)
@property
def num_classes(self):
r"""Number of classes for each node."""
return 2
def __len__(self):
r"""The number of examples in the dataset."""
return len(self._graphs)
def __getitem__(self, idx):
r"""Get the idx^th sample.
Parameters
---------
idx : int
The sample index.
Returns
-------
:class:`dgl.DGLGraph`
graph structure, node features, node labels and edge features.
- ``ndata['feat']``: node features
- ``ndata['label']``: node labels
- ``edata['feat']``: edge features
"""
if self._transform is None:
return self._graphs[idx]
else:
return self._transform(self._graphs[idx])
+226
View File
@@ -0,0 +1,226 @@
""" PPIDataset for inductive learning. """
import json
import os
import networkx as nx
import numpy as np
from networkx.readwrite import json_graph
from .. import backend as F
from ..convert import from_networkx
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs, load_info, save_graphs, save_info
class PPIDataset(DGLBuiltinDataset):
r"""Protein-Protein Interaction dataset for inductive node classification
A toy Protein-Protein Interaction network dataset. The dataset contains
24 graphs. The average number of nodes per graph is 2372. Each node has
50 features and 121 labels. 20 graphs for training, 2 for validation
and 2 for testing.
Reference: `<http://snap.stanford.edu/graphsage/>`_
Statistics:
- Train examples: 20
- Valid examples: 2
- Test examples: 2
Parameters
----------
mode : str
Must be one of ('train', 'valid', 'test').
Default: 'train'
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose : bool
Whether to print out progress information.
Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_labels : int
Number of labels for each node
labels : Tensor
Node labels
features : Tensor
Node features
Examples
--------
>>> dataset = PPIDataset(mode='valid')
>>> num_classes = dataset.num_classes
>>> for g in dataset:
.... feat = g.ndata['feat']
.... label = g.ndata['label']
.... # your code here
>>>
"""
def __init__(
self,
mode="train",
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
assert mode in ["train", "valid", "test"]
self.mode = mode
_url = _get_dgl_url("dataset/ppi.zip")
super(PPIDataset, self).__init__(
name="ppi",
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
graph_file = os.path.join(
self.save_path, "{}_graph.json".format(self.mode)
)
label_file = os.path.join(
self.save_path, "{}_labels.npy".format(self.mode)
)
feat_file = os.path.join(
self.save_path, "{}_feats.npy".format(self.mode)
)
graph_id_file = os.path.join(
self.save_path, "{}_graph_id.npy".format(self.mode)
)
g_data = json.load(open(graph_file))
self._labels = np.load(label_file)
self._feats = np.load(feat_file)
self.graph = from_networkx(
nx.DiGraph(json_graph.node_link_graph(g_data))
)
graph_id = np.load(graph_id_file)
# lo, hi means the range of graph ids for different portion of the dataset,
# 20 graphs for training, 2 for validation and 2 for testing.
lo, hi = 1, 21
if self.mode == "valid":
lo, hi = 21, 23
elif self.mode == "test":
lo, hi = 23, 25
graph_masks = []
self.graphs = []
for g_id in range(lo, hi):
g_mask = np.where(graph_id == g_id)[0]
graph_masks.append(g_mask)
g = self.graph.subgraph(g_mask)
g.ndata["feat"] = F.tensor(
self._feats[g_mask], dtype=F.data_type_dict["float32"]
)
g.ndata["label"] = F.tensor(
self._labels[g_mask], dtype=F.data_type_dict["float32"]
)
self.graphs.append(g)
@property
def graph_list_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph_list.bin".format(self.mode)
)
@property
def g_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.mode)
)
@property
def info_path(self):
return os.path.join(self.save_path, "{}_info.pkl".format(self.mode))
def has_cache(self):
return (
os.path.exists(self.graph_list_path)
and os.path.exists(self.g_path)
and os.path.exists(self.info_path)
)
def save(self):
save_graphs(self.graph_list_path, self.graphs)
save_graphs(self.g_path, self.graph)
save_info(
self.info_path, {"labels": self._labels, "feats": self._feats}
)
def load(self):
self.graphs = load_graphs(self.graph_list_path)[0]
g, _ = load_graphs(self.g_path)
self.graph = g[0]
info = load_info(self.info_path)
self._labels = info["labels"]
self._feats = info["feats"]
@property
def num_labels(self):
return 121
@property
def num_classes(self):
return 121
def __len__(self):
"""Return number of samples in this dataset."""
return len(self.graphs)
def __getitem__(self, item):
"""Get the item^th sample.
Parameters
---------
item : int
The sample index.
Returns
-------
:class:`dgl.DGLGraph`
graph structure, node features and node labels.
- ``ndata['feat']``: node features
- ``ndata['label']``: node labels
"""
if self._transform is None:
return self.graphs[item]
else:
return self._transform(self.graphs[item])
class LegacyPPIDataset(PPIDataset):
"""Legacy version of PPI Dataset"""
def __getitem__(self, item):
"""Get the item^th sample.
Paramters
---------
idx : int
The sample index.
Returns
-------
(dgl.DGLGraph, Tensor, Tensor)
The graph, features and its label.
"""
if self._transform is None:
g = self.graphs[item]
else:
g = self._transform(self.graphs[item])
return g, g.ndata["feat"], g.ndata["label"]
+177
View File
@@ -0,0 +1,177 @@
"""QM7b dataset for graph property prediction (regression)."""
import os
from scipy import io
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLDataset
from .utils import check_sha1, download, load_graphs, save_graphs
class QM7bDataset(DGLDataset):
r"""QM7b dataset for graph property prediction (regression)
This dataset consists of 7,211 molecules with 14 regression targets.
Nodes means atoms and edges means bonds. Edge data 'h' means
the entry of Coulomb matrix.
Reference: `<http://quantum-machine.org/datasets/>`_
Statistics:
- Number of graphs: 7,211
- Number of regression targets: 14
- Average number of nodes: 15
- Average number of edges: 245
- Edge feature size: 1
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_tasks : int
Number of prediction tasks
num_labels : int
(DEPRECATED, use num_tasks instead) Number of prediction tasks
Raises
------
UserWarning
If the raw data is changed in the remote server by the author.
Examples
--------
>>> data = QM7bDataset()
>>> data.num_tasks
14
>>>
>>> # iterate over the dataset
>>> for g, label in data:
... edge_feat = g.edata['h'] # get edge feature
... # your code here...
...
>>>
"""
_url = (
"http://deepchem.io.s3-website-us-west-1.amazonaws.com/"
"datasets/qm7b.mat"
)
_sha1_str = "4102c744bb9d6fd7b40ac67a300e49cd87e28392"
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
super(QM7bDataset, self).__init__(
name="qm7b",
url=self._url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
mat_path = os.path.join(self.raw_dir, self.name + ".mat")
self.graphs, self.label = self._load_graph(mat_path)
def _load_graph(self, filename):
data = io.loadmat(filename)
labels = F.tensor(data["T"], dtype=F.data_type_dict["float32"])
feats = data["X"]
num_graphs = labels.shape[0]
graphs = []
for i in range(num_graphs):
edge_list = feats[i].nonzero()
g = dgl_graph(edge_list)
g.edata["h"] = F.tensor(
feats[i][edge_list[0], edge_list[1]].reshape(-1, 1),
dtype=F.data_type_dict["float32"],
)
graphs.append(g)
return graphs, labels
def save(self):
"""save the graph list and the labels"""
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
save_graphs(str(graph_path), self.graphs, {"labels": self.label})
def has_cache(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
return os.path.exists(graph_path)
def load(self):
graphs, label_dict = load_graphs(
os.path.join(self.save_path, "dgl_graph.bin")
)
self.graphs = graphs
self.label = label_dict["labels"]
def download(self):
file_path = os.path.join(self.raw_dir, self.name + ".mat")
download(self.url, path=file_path)
if not check_sha1(file_path, self._sha1_str):
raise UserWarning(
"File {} is downloaded but the content hash does not match."
"The repo may be outdated or download may be incomplete. "
"Otherwise you can create an issue for it.".format(self.name)
)
@property
def num_tasks(self):
"""Number of prediction tasks."""
return self.num_labels
@property
def num_labels(self):
"""Number of prediction tasks."""
return 14
@property
def num_classes(self):
"""Number of prediction tasks."""
return 14
def __getitem__(self, idx):
r"""Get graph and label by index
Parameters
----------
idx : int
Item index
Returns
-------
(:class:`dgl.DGLGraph`, Tensor)
"""
if self._transform is None:
g = self.graphs[idx]
else:
g = self._transform(self.graphs[idx])
return g, self.label[idx]
def __len__(self):
r"""Number of graphs in the dataset.
Return
-------
int
"""
return len(self.graphs)
QM7b = QM7bDataset
+231
View File
@@ -0,0 +1,231 @@
"""QM9 dataset for graph property prediction (regression)."""
import os
import numpy as np
import scipy.sparse as sp
from .. import backend as F
from ..convert import graph as dgl_graph
from ..transforms import to_bidirected
from .dgl_dataset import DGLDataset
from .utils import _get_dgl_url, download
class QM9Dataset(DGLDataset):
r"""QM9 dataset for graph property prediction (regression)
This dataset consists of 130,831 molecules with 12 regression targets.
Nodes correspond to atoms and edges correspond to close atom pairs.
This dataset differs from :class:`~dgl.data.QM9EdgeDataset` in the following aspects:
1. Edges in this dataset are purely distance-based.
2. It only provides atoms' coordinates and atomic numbers as node features
3. It only provides 12 regression targets.
Reference:
- `"Quantum-Machine.org" <http://quantum-machine.org/datasets/>`_,
- `"Directional Message Passing for Molecular Graphs" <https://arxiv.org/abs/2003.03123>`_
Statistics:
- Number of graphs: 130,831
- Number of regression targets: 12
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| Keys | Property | Description | Unit |
+========+==================================+===================================================================================+=============================================+
| mu | :math:`\mu` | Dipole moment | :math:`\textrm{D}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| alpha | :math:`\alpha` | Isotropic polarizability | :math:`{a_0}^3` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| homo | :math:`\epsilon_{\textrm{HOMO}}` | Highest occupied molecular orbital energy | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| lumo | :math:`\epsilon_{\textrm{LUMO}}` | Lowest unoccupied molecular orbital energy | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| gap | :math:`\Delta \epsilon` | Gap between :math:`\epsilon_{\textrm{HOMO}}` and :math:`\epsilon_{\textrm{LUMO}}` | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| r2 | :math:`\langle R^2 \rangle` | Electronic spatial extent | :math:`{a_0}^2` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| zpve | :math:`\textrm{ZPVE}` | Zero point vibrational energy | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| U0 | :math:`U_0` | Internal energy at 0K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| U | :math:`U` | Internal energy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| H | :math:`H` | Enthalpy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| G | :math:`G` | Free energy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| Cv | :math:`c_{\textrm{v}}` | Heat capavity at 298.15K | :math:`\frac{\textrm{cal}}{\textrm{mol K}}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
Parameters
----------
label_keys : list
Names of the regression property, which should be a subset of the keys in the table above.
cutoff : float
Cutoff distance for interatomic interactions, i.e. two atoms are connected in the corresponding graph if the distance between them is no larger than this.
Default: 5.0 Angstrom
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_tasks : int
Number of prediction tasks
num_labels : int
(DEPRECATED, use num_tasks instead) Number of prediction tasks
Raises
------
UserWarning
If the raw data is changed in the remote server by the author.
Examples
--------
>>> data = QM9Dataset(label_keys=['mu', 'gap'], cutoff=5.0)
>>> data.num_tasks
2
>>>
>>> # iterate over the dataset
>>> for g, label in data:
... R = g.ndata['R'] # get coordinates of each atom
... Z = g.ndata['Z'] # get atomic numbers of each atom
... # your code here...
>>>
"""
def __init__(
self,
label_keys,
cutoff=5.0,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
self.cutoff = cutoff
self.label_keys = label_keys
self._url = _get_dgl_url("dataset/qm9_eV.npz")
super(QM9Dataset, self).__init__(
name="qm9",
url=self._url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
npz_path = f"{self.raw_dir}/qm9_eV.npz"
data_dict = np.load(npz_path, allow_pickle=True)
# data_dict['N'] contains the number of atoms in each molecule.
# Atomic properties (Z and R) of all molecules are concatenated as single tensors,
# so you need this value to select the correct atoms for each molecule.
self.N = data_dict["N"]
self.R = data_dict["R"]
self.Z = data_dict["Z"]
self.label = np.stack(
[data_dict[key] for key in self.label_keys], axis=1
)
self.N_cumsum = np.concatenate([[0], np.cumsum(self.N)])
def download(self):
file_path = f"{self.raw_dir}/qm9_eV.npz"
if not os.path.exists(file_path):
download(self._url, path=file_path)
@property
def num_labels(self):
r"""
Returns
--------
int
Number of prediction tasks.
"""
return self.label.shape[1]
@property
def num_classes(self):
r"""
Returns
--------
int
Number of prediction tasks.
"""
return self.label.shape[1]
@property
def num_tasks(self):
r"""
Returns
--------
int
Number of prediction tasks.
"""
return self.label.shape[1]
def __getitem__(self, idx):
r"""Get graph and label by index
Parameters
----------
idx : int
Item index
Returns
-------
dgl.DGLGraph
The graph contains:
- ``ndata['R']``: the coordinates of each atom
- ``ndata['Z']``: the atomic number
Tensor
Property values of molecular graphs
"""
label = F.tensor(self.label[idx], dtype=F.data_type_dict["float32"])
n_atoms = self.N[idx]
R = self.R[self.N_cumsum[idx] : self.N_cumsum[idx + 1]]
dist = np.linalg.norm(R[:, None, :] - R[None, :, :], axis=-1)
adj = sp.csr_matrix(dist <= self.cutoff) - sp.eye(
n_atoms, dtype=np.bool_
)
adj = adj.tocoo()
u, v = F.tensor(adj.row), F.tensor(adj.col)
g = dgl_graph((u, v))
g = to_bidirected(g)
g.ndata["R"] = F.tensor(R, dtype=F.data_type_dict["float32"])
g.ndata["Z"] = F.tensor(
self.Z[self.N_cumsum[idx] : self.N_cumsum[idx + 1]],
dtype=F.data_type_dict["int64"],
)
if self._transform is not None:
g = self._transform(g)
return g, label
def __len__(self):
r"""Number of graphs in the dataset.
Return
-------
int
"""
return self.label.shape[0]
QM9 = QM9Dataset
+296
View File
@@ -0,0 +1,296 @@
""" QM9 dataset for graph property prediction (regression) """
import os
import numpy as np
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLDataset
from .utils import _get_dgl_url, download, extract_archive
class QM9EdgeDataset(DGLDataset):
r"""QM9Edge dataset for graph property prediction (regression)
This dataset consists of 130,831 molecules with 19 regression targets.
Nodes correspond to atoms and edges correspond to bonds.
This dataset differs from :class:`~dgl.data.QM9Dataset` in the following aspects:
1. It includes the bonds in a molecule in the edges of the corresponding graph while the edges in :class:`~dgl.data.QM9Dataset` are purely distance-based.
2. It provides edge features, and node features in addition to the atoms' coordinates and atomic numbers.
3. It provides another 7 regression tasks(from 12 to 19).
This class is built based on a preprocessed version of the dataset, and we provide the preprocessing datails `here <https://gist.github.com/hengruizhang98/a2da30213b2356fff18b25385c9d3cd2>`_.
Reference:
- `"MoleculeNet: A Benchmark for Molecular Machine Learning" <https://arxiv.org/abs/1703.00564>`_
- `"Neural Message Passing for Quantum Chemistry" <https://arxiv.org/abs/1704.01212>`_
For
Statistics:
- Number of graphs: 130,831.
- Number of regression targets: 19.
Node attributes:
- pos: the 3D coordinates of each atom.
- attr: the 11D atom features.
Edge attributes:
- edge_attr: the 4D bond features.
Regression targets:
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| Keys | Property | Description | Unit |
+========+==================================+===================================================================================+=============================================+
| mu | :math:`\mu` | Dipole moment | :math:`\textrm{D}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| alpha | :math:`\alpha` | Isotropic polarizability | :math:`{a_0}^3` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| homo | :math:`\epsilon_{\textrm{HOMO}}` | Highest occupied molecular orbital energy | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| lumo | :math:`\epsilon_{\textrm{LUMO}}` | Lowest unoccupied molecular orbital energy | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| gap | :math:`\Delta \epsilon` | Gap between :math:`\epsilon_{\textrm{HOMO}}` and :math:`\epsilon_{\textrm{LUMO}}` | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| r2 | :math:`\langle R^2 \rangle` | Electronic spatial extent | :math:`{a_0}^2` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| zpve | :math:`\textrm{ZPVE}` | Zero point vibrational energy | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| U0 | :math:`U_0` | Internal energy at 0K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| U | :math:`U` | Internal energy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| H | :math:`H` | Enthalpy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| G | :math:`G` | Free energy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| Cv | :math:`c_{\textrm{v}}` | Heat capavity at 298.15K | :math:`\frac{\textrm{cal}}{\textrm{mol K}}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| U0_atom| :math:`U_0^{\textrm{ATOM}}` | Atomization energy at 0K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| U_atom | :math:`U^{\textrm{ATOM}}` | Atomization energy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| H_atom | :math:`H^{\textrm{ATOM}}` | Atomization enthalpy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| G_atom | :math:`G^{\textrm{ATOM}}` | Atomization free energy at 298.15K | :math:`\textrm{eV}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| A | :math:`A` | Rotational constant | :math:`\textrm{GHz}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| B | :math:`B` | Rotational constant | :math:`\textrm{GHz}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
| C | :math:`C` | Rotational constant | :math:`\textrm{GHz}` |
+--------+----------------------------------+-----------------------------------------------------------------------------------+---------------------------------------------+
Parameters
----------
label_keys : list
Names of the regression property, which should be a subset of the keys in the table above.
If not provided, it will load all the labels.
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False.
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_tasks : int
Number of prediction tasks
num_labels : int
(DEPRECATED, use num_tasks instead) Number of prediction tasks
Raises
------
UserWarning
If the raw data is changed in the remote server by the author.
Examples
--------
>>> data = QM9EdgeDataset(label_keys=['mu', 'alpha'])
>>> data.num_tasks
2
>>> # iterate over the dataset
>>> for graph, labels in data:
... print(graph) # get information of each graph
... print(labels) # get labels of the corresponding graph
... # your code here...
>>>
"""
keys = [
"mu",
"alpha",
"homo",
"lumo",
"gap",
"r2",
"zpve",
"U0",
"U",
"H",
"G",
"Cv",
"U0_atom",
"U_atom",
"H_atom",
"G_atom",
"A",
"B",
"C",
]
map_dict = {}
for i, key in enumerate(keys):
map_dict[key] = i
def __init__(
self,
label_keys=None,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
if label_keys is None:
self.label_keys = None
self.num_labels = 19
else:
self.label_keys = [self.map_dict[i] for i in label_keys]
self.num_labels = len(label_keys)
self._url = _get_dgl_url("dataset/qm9_edge.npz")
super(QM9EdgeDataset, self).__init__(
name="qm9Edge",
raw_dir=raw_dir,
url=self._url,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def download(self):
if not os.path.exists(self.npz_path):
download(self._url, path=self.npz_path)
def process(self):
self.load()
@property
def npz_path(self):
return f"{self.raw_dir}/qm9_edge.npz"
def has_cache(self):
return os.path.exists(self.npz_path)
def save(self):
np.savez_compressed(
self.npz_path,
n_node=self.n_node,
n_edge=self.n_edge,
node_attr=self.node_attr,
node_pos=self.node_pos,
edge_attr=self.edge_attr,
src=self.src,
dst=self.dst,
targets=self.targets,
)
def load(self):
data_dict = np.load(self.npz_path, allow_pickle=True)
self.n_node = data_dict["n_node"]
self.n_edge = data_dict["n_edge"]
self.node_attr = data_dict["node_attr"]
self.node_pos = data_dict["node_pos"]
self.edge_attr = data_dict["edge_attr"]
self.targets = data_dict["targets"]
self.src = data_dict["src"]
self.dst = data_dict["dst"]
self.n_cumsum = np.concatenate([[0], np.cumsum(self.n_node)])
self.ne_cumsum = np.concatenate([[0], np.cumsum(self.n_edge)])
def __getitem__(self, idx):
r"""Get graph and label by index
Parameters
----------
idx : int
Item index
Returns
-------
dgl.DGLGraph
The graph contains:
- ``ndata['pos']``: the coordinates of each atom
- ``ndata['attr']``: the features of each atom
- ``edata['edge_attr']``: the features of each bond
Tensor
Property values of molecular graphs
"""
pos = self.node_pos[self.n_cumsum[idx] : self.n_cumsum[idx + 1]]
src = self.src[self.ne_cumsum[idx] : self.ne_cumsum[idx + 1]]
dst = self.dst[self.ne_cumsum[idx] : self.ne_cumsum[idx + 1]]
g = dgl_graph((src, dst))
g.ndata["pos"] = F.tensor(pos, dtype=F.data_type_dict["float32"])
g.ndata["attr"] = F.tensor(
self.node_attr[self.n_cumsum[idx] : self.n_cumsum[idx + 1]],
dtype=F.data_type_dict["float32"],
)
g.edata["edge_attr"] = F.tensor(
self.edge_attr[self.ne_cumsum[idx] : self.ne_cumsum[idx + 1]],
dtype=F.data_type_dict["float32"],
)
label = F.tensor(
self.targets[idx][self.label_keys],
dtype=F.data_type_dict["float32"],
)
if self._transform is not None:
g = self._transform(g)
return g, label
def __len__(self):
r"""Number of graphs in the dataset.
Returns
-------
int
"""
return self.n_node.shape[0]
@property
def num_tasks(self):
r"""
Returns
-------
int
Number of prediction tasks
"""
return self.num_labels
QM9Edge = QM9EdgeDataset
File diff suppressed because it is too large Load Diff
+223
View File
@@ -0,0 +1,223 @@
""" Reddit dataset for community detection """
from __future__ import absolute_import
import os
import numpy as np
import scipy.sparse as sp
from .. import backend as F
from ..convert import from_scipy
from ..transforms import reorder_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import (
_get_dgl_url,
deprecate_property,
generate_mask_tensor,
load_graphs,
save_graphs,
)
class RedditDataset(DGLBuiltinDataset):
r"""Reddit dataset for community detection (node classification)
This is a graph dataset from Reddit posts made in the month of September, 2014.
The node label in this case is the community, or “subreddit”, that a post belongs to.
The authors sampled 50 large communities and built a post-to-post graph, connecting
posts if the same user comments on both. In total this dataset contains 232,965
posts with an average degree of 492. We use the first 20 days for training and the
remaining days for testing (with 30% used for validation).
Reference: `<http://snap.stanford.edu/graphsage/>`_
Statistics
- Nodes: 232,965
- Edges: 114,615,892
- Node feature size: 602
- Number of training samples: 153,431
- Number of validation samples: 23,831
- Number of test samples: 55,703
Parameters
----------
self_loop : bool
Whether load dataset with self loop connections. Default: False
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of classes for each node
Examples
--------
>>> data = RedditDataset()
>>> g = data[0]
>>> num_classes = data.num_classes
>>>
>>> # get node feature
>>> feat = g.ndata['feat']
>>>
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> test_mask = g.ndata['test_mask']
>>>
>>> # get labels
>>> label = g.ndata['label']
>>>
>>> # Train, Validation and Test
"""
def __init__(
self,
self_loop=False,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
self_loop_str = ""
if self_loop:
self_loop_str = "_self_loop"
_url = _get_dgl_url("dataset/reddit{}.zip".format(self_loop_str))
self._self_loop_str = self_loop_str
super(RedditDataset, self).__init__(
name="reddit{}".format(self_loop_str),
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
# graph
coo_adj = sp.load_npz(
os.path.join(
self.raw_path, "reddit{}_graph.npz".format(self._self_loop_str)
)
)
self._graph = from_scipy(coo_adj)
# features and labels
reddit_data = np.load(os.path.join(self.raw_path, "reddit_data.npz"))
features = reddit_data["feature"]
labels = reddit_data["label"]
# tarin/val/test indices
node_types = reddit_data["node_types"]
train_mask = node_types == 1
val_mask = node_types == 2
test_mask = node_types == 3
self._graph.ndata["train_mask"] = generate_mask_tensor(train_mask)
self._graph.ndata["val_mask"] = generate_mask_tensor(val_mask)
self._graph.ndata["test_mask"] = generate_mask_tensor(test_mask)
self._graph.ndata["feat"] = F.tensor(
features, dtype=F.data_type_dict["float32"]
)
self._graph.ndata["label"] = F.tensor(
labels, dtype=F.data_type_dict["int64"]
)
self._graph = reorder_graph(
self._graph,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
self._print_info()
def has_cache(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
if os.path.exists(graph_path):
return True
return False
def save(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
save_graphs(graph_path, self._graph)
def load(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
graphs, _ = load_graphs(graph_path)
self._graph = graphs[0]
self._graph.ndata["train_mask"] = generate_mask_tensor(
self._graph.ndata["train_mask"].numpy()
)
self._graph.ndata["val_mask"] = generate_mask_tensor(
self._graph.ndata["val_mask"].numpy()
)
self._graph.ndata["test_mask"] = generate_mask_tensor(
self._graph.ndata["test_mask"].numpy()
)
self._print_info()
def _print_info(self):
if self.verbose:
print("Finished data loading.")
print(" NumNodes: {}".format(self._graph.num_nodes()))
print(" NumEdges: {}".format(self._graph.num_edges()))
print(" NumFeats: {}".format(self._graph.ndata["feat"].shape[1]))
print(" NumClasses: {}".format(self.num_classes))
print(
" NumTrainingSamples: {}".format(
F.nonzero_1d(self._graph.ndata["train_mask"]).shape[0]
)
)
print(
" NumValidationSamples: {}".format(
F.nonzero_1d(self._graph.ndata["val_mask"]).shape[0]
)
)
print(
" NumTestSamples: {}".format(
F.nonzero_1d(self._graph.ndata["test_mask"]).shape[0]
)
)
@property
def num_classes(self):
r"""Number of classes for each node."""
return 41
def __getitem__(self, idx):
r"""Get graph by index
Parameters
----------
idx : int
Item index
Returns
-------
:class:`dgl.DGLGraph`
graph structure, node labels, node features and splitting masks:
- ``ndata['label']``: node label
- ``ndata['feat']``: node feature
- ``ndata['train_mask']`` mask for training node set
- ``ndata['val_mask']``: mask for validation node set
- ``ndata['test_mask']:`` mask for test node set
"""
assert idx == 0, "Reddit Dataset only has one graph"
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
r"""Number of graphs in the dataset"""
return 1
+276
View File
@@ -0,0 +1,276 @@
"""Dataset for stochastic block model."""
import math
import os
import random
import numpy as np
import numpy.random as npr
import scipy as sp
from .. import batch
from ..convert import from_scipy
from .dgl_dataset import DGLDataset
from .utils import load_graphs, load_info, save_graphs, save_info
def sbm(n_blocks, block_size, p, q, rng=None):
"""(Symmetric) Stochastic Block Model
Parameters
----------
n_blocks : int
Number of blocks.
block_size : int
Block size.
p : float
Probability for intra-community edge.
q : float
Probability for inter-community edge.
rng : numpy.random.RandomState, optional
Random number generator.
Returns
-------
scipy sparse matrix
The adjacency matrix of generated graph.
"""
n = n_blocks * block_size
p /= n
q /= n
rng = np.random.RandomState() if rng is None else rng
rows = []
cols = []
for i in range(n_blocks):
for j in range(i, n_blocks):
density = p if i == j else q
block = sp.sparse.random(
block_size,
block_size,
density,
random_state=rng,
data_rvs=lambda n: np.ones(n),
)
rows.append(block.row + i * block_size)
cols.append(block.col + j * block_size)
rows = np.hstack(rows)
cols = np.hstack(cols)
a = sp.sparse.coo_matrix(
(np.ones(rows.shape[0]), (rows, cols)), shape=(n, n)
)
adj = sp.sparse.triu(a) + sp.sparse.triu(a, 1).transpose()
return adj
class SBMMixtureDataset(DGLDataset):
r"""Symmetric Stochastic Block Model Mixture
Reference: Appendix C of `Supervised Community Detection with Hierarchical Graph Neural Networks <https://arxiv.org/abs/1705.08415>`_
Parameters
----------
n_graphs : int
Number of graphs.
n_nodes : int
Number of nodes.
n_communities : int
Number of communities.
k : int, optional
Multiplier. Default: 2
avg_deg : int, optional
Average degree. Default: 3
pq : list of pair of nonnegative float or str, optional
Random densities. This parameter is for future extension,
for now it's always using the default value.
Default: Appendix_C
rng : numpy.random.RandomState, optional
Random number generator. If not given, it's numpy.random.RandomState() with `seed=None`,
which read data from /dev/urandom (or the Windows analogue) if available or seed from
the clock otherwise.
Default: None
Raises
------
RuntimeError is raised if pq is not a list or string.
Examples
--------
>>> data = SBMMixtureDataset(n_graphs=16, n_nodes=10000, n_communities=2)
>>> from torch.utils.data import DataLoader
>>> dataloader = DataLoader(data, batch_size=1, collate_fn=data.collate_fn)
>>> for graph, line_graph, graph_degrees, line_graph_degrees, pm_pd in dataloader:
... # your code here
"""
def __init__(
self,
n_graphs,
n_nodes,
n_communities,
k=2,
avg_deg=3,
pq="Appendix_C",
rng=None,
):
self._n_graphs = n_graphs
self._n_nodes = n_nodes
self._n_communities = n_communities
assert n_nodes % n_communities == 0
self._block_size = n_nodes // n_communities
self._k = k
self._avg_deg = avg_deg
self._pq = pq
self._rng = rng
super(SBMMixtureDataset, self).__init__(
name="sbmmixture",
hash_key=(n_graphs, n_nodes, n_communities, k, avg_deg, pq, rng),
)
def process(self):
pq = self._pq
if type(pq) is list:
assert len(pq) == self._n_graphs
elif type(pq) is str:
generator = {"Appendix_C": self._appendix_c}[pq]
pq = [generator() for _ in range(self._n_graphs)]
else:
raise RuntimeError()
self._graphs = [
from_scipy(sbm(self._n_communities, self._block_size, *x))
for x in pq
]
self._line_graphs = [
g.line_graph(backtracking=False) for g in self._graphs
]
in_degrees = lambda g: g.in_degrees().float()
self._graph_degrees = [in_degrees(g) for g in self._graphs]
self._line_graph_degrees = [in_degrees(lg) for lg in self._line_graphs]
self._pm_pds = list(zip(*[g.edges() for g in self._graphs]))[0]
@property
def graph_path(self):
return os.path.join(self.save_path, "graphs_{}.bin".format(self.hash))
@property
def line_graph_path(self):
return os.path.join(
self.save_path, "line_graphs_{}.bin".format(self.hash)
)
@property
def info_path(self):
return os.path.join(self.save_path, "info_{}.pkl".format(self.hash))
def has_cache(self):
return (
os.path.exists(self.graph_path)
and os.path.exists(self.line_graph_path)
and os.path.exists(self.info_path)
)
def save(self):
save_graphs(self.graph_path, self._graphs)
save_graphs(self.line_graph_path, self._line_graphs)
save_info(
self.info_path,
{
"graph_degree": self._graph_degrees,
"line_graph_degree": self._line_graph_degrees,
"pm_pds": self._pm_pds,
},
)
def load(self):
self._graphs, _ = load_graphs(self.graph_path)
self._line_graphs, _ = load_graphs(self.line_graph_path)
info = load_info(self.info_path)
self._graph_degrees = info["graph_degree"]
self._line_graph_degrees = info["line_graph_degree"]
self._pm_pds = info["pm_pds"]
def __len__(self):
r"""Number of graphs in the dataset."""
return len(self._graphs)
def __getitem__(self, idx):
r"""Get one example by index
Parameters
----------
idx : int
Item index
Returns
-------
graph: :class:`dgl.DGLGraph`
The original graph
line_graph: :class:`dgl.DGLGraph`
The line graph of `graph`
graph_degree: numpy.ndarray
In degrees for each node in `graph`
line_graph_degree: numpy.ndarray
In degrees for each node in `line_graph`
pm_pd: numpy.ndarray
Edge indicator matrices Pm and Pd
"""
return (
self._graphs[idx],
self._line_graphs[idx],
self._graph_degrees[idx],
self._line_graph_degrees[idx],
self._pm_pds[idx],
)
def _appendix_c(self):
q = npr.uniform(0, self._avg_deg - math.sqrt(self._avg_deg))
p = self._k * self._avg_deg - q
if random.random() < 0.5:
return p, q
else:
return q, p
def collate_fn(self, x):
r"""The `collate` function for dataloader
Parameters
----------
x : tuple
a batch of data that contains:
- graph: :class:`dgl.DGLGraph`
The original graph
- line_graph: :class:`dgl.DGLGraph`
The line graph of `graph`
- graph_degree: numpy.ndarray
In degrees for each node in `graph`
- line_graph_degree: numpy.ndarray
In degrees for each node in `line_graph`
- pm_pd: numpy.ndarray
Edge indicator matrices Pm and Pd
Returns
-------
g_batch: :class:`dgl.DGLGraph`
Batched graphs
lg_batch: :class:`dgl.DGLGraph`
Batched line graphs
degg_batch: numpy.ndarray
A batch of in degrees for each node in `g_batch`
deglg_batch: numpy.ndarray
A batch of in degrees for each node in `lg_batch`
pm_pd_batch: numpy.ndarray
A batch of edge indicator matrices Pm and Pd
"""
g, lg, deg_g, deg_lg, pm_pd = zip(*x)
g_batch = batch.batch(g)
lg_batch = batch.batch(lg)
degg_batch = np.concatenate(deg_g, axis=0)
deglg_batch = np.concatenate(deg_lg, axis=0)
pm_pd_batch = np.concatenate(
[x + i * self._n_nodes for i, x in enumerate(pm_pd)], axis=0
)
return g_batch, lg_batch, degg_batch, deglg_batch, pm_pd_batch
SBMMixture = SBMMixtureDataset
+435
View File
@@ -0,0 +1,435 @@
import os
import pickle
import numpy as np
from scipy.spatial.distance import cdist
from tqdm.auto import tqdm
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLDataset
from .utils import download, extract_archive, load_graphs, save_graphs, Subset
def sigma(dists, kth=8):
num_nodes = dists.shape[0]
# Compute sigma and reshape.
if kth > num_nodes:
# Handling for graphs with num_nodes less than kth.
sigma = np.array([1] * num_nodes).reshape(num_nodes, 1)
else:
# Get k-nearest neighbors for each node.
knns = np.partition(dists, kth, axis=-1)[:, : kth + 1]
sigma = knns.sum(axis=1).reshape((knns.shape[0], 1)) / kth
return sigma + 1e-8
def compute_adjacency_matrix_images(coord, feat, use_feat=True):
coord = coord.reshape(-1, 2)
# Compute coordinate distance.
c_dist = cdist(coord, coord)
if use_feat:
# Compute feature distance.
f_dist = cdist(feat, feat)
# Compute adjacency.
A = np.exp(
-((c_dist / sigma(c_dist)) ** 2) - (f_dist / sigma(f_dist)) ** 2
)
else:
A = np.exp(-((c_dist / sigma(c_dist)) ** 2))
# Convert to symmetric matrix.
A = 0.5 * (A + A.T)
A[np.diag_indices_from(A)] = 0
return A
def compute_edges_list(A, kth=9):
# Get k-similar neighbor indices for each node.
num_nodes = A.shape[0]
new_kth = num_nodes - kth
if num_nodes > kth:
knns = np.argpartition(A, new_kth - 1, axis=-1)[:, new_kth:-1]
knn_values = np.partition(A, new_kth - 1, axis=-1)[:, new_kth:-1]
else:
# Handling for graphs with less than kth nodes.
# In such cases, the resulting graph will be fully connected.
knns = np.tile(np.arange(num_nodes), num_nodes).reshape(
num_nodes, num_nodes
)
knn_values = A
# Removing self loop.
if num_nodes != 1:
knn_values = A[knns != np.arange(num_nodes)[:, None]].reshape(
num_nodes, -1
)
knns = knns[knns != np.arange(num_nodes)[:, None]].reshape(
num_nodes, -1
)
return knns, knn_values
class SuperPixelDataset(DGLDataset):
def __init__(
self,
raw_dir=None,
name="MNIST",
split="train",
use_feature=False,
force_reload=False,
verbose=False,
transform=None,
):
assert split in ["train", "test"], "split not valid."
assert name in ["MNIST", "CIFAR10"], "name not valid."
self.use_feature = use_feature
self.split = split
self._dataset_name = name
self.graphs = []
self.labels = []
super().__init__(
name="Superpixel",
raw_dir=raw_dir,
url="""
https://www.dropbox.com/s/y2qwa77a0fxem47/superpixels.zip?dl=1
""",
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
@property
def img_size(self):
r"""Size of dataset image."""
if self._dataset_name == "MNIST":
return 28
return 32
@property
def save_path(self):
r"""Directory to save the processed dataset."""
return os.path.join(self.raw_path, "processed")
@property
def raw_data_path(self):
r"""Path to save the raw dataset file."""
return os.path.join(self.raw_path, "superpixels.zip")
@property
def graph_path(self):
r"""Path to save the processed dataset file."""
if self.use_feature:
return os.path.join(
self.save_path,
f"use_feat_{self._dataset_name}_{self.split}.pkl",
)
return os.path.join(
self.save_path, f"{self._dataset_name}_{self.split}.pkl"
)
def download(self):
path = download(self.url, path=self.raw_data_path)
extract_archive(path, target_dir=self.raw_path, overwrite=True)
def process(self):
if self._dataset_name == "MNIST":
plk_file = "mnist_75sp"
elif self._dataset_name == "CIFAR10":
plk_file = "cifar10_150sp"
with open(
os.path.join(
self.raw_path, "superpixels", f"{plk_file}_{self.split}.pkl"
),
"rb",
) as f:
self.labels, self.sp_data = pickle.load(f)
self.labels = F.tensor(self.labels)
self.Adj_matrices = []
self.node_features = []
self.edges_lists = []
self.edge_features = []
for index, sample in enumerate(
tqdm(self.sp_data, desc=f"Processing {self.split} dataset")
):
mean_px, coord = sample[:2]
coord = coord / self.img_size
if self.use_feature:
A = compute_adjacency_matrix_images(
coord, mean_px
) # using super-pixel locations + features
else:
A = compute_adjacency_matrix_images(
coord, mean_px, False
) # using only super-pixel locations
edges_list, edge_values_list = compute_edges_list(A)
N_nodes = A.shape[0]
mean_px = mean_px.reshape(N_nodes, -1)
coord = coord.reshape(N_nodes, 2)
x = np.concatenate((mean_px, coord), axis=1)
edge_values_list = edge_values_list.reshape(-1)
self.node_features.append(x)
self.edge_features.append(edge_values_list)
self.Adj_matrices.append(A)
self.edges_lists.append(edges_list)
for index in tqdm(
range(len(self.sp_data)), desc=f"Dump {self.split} dataset"
):
N = self.node_features[index].shape[0]
src_nodes = []
dst_nodes = []
for src, dsts in enumerate(self.edges_lists[index]):
# handling for 1 node where the self loop would be the only edge
if N == 1:
src_nodes.append(src)
dst_nodes.append(dsts)
else:
dsts = dsts[dsts != src]
srcs = [src] * len(dsts)
src_nodes.extend(srcs)
dst_nodes.extend(dsts)
src_nodes = F.tensor(src_nodes)
dst_nodes = F.tensor(dst_nodes)
g = dgl_graph((src_nodes, dst_nodes), num_nodes=N)
g.ndata["feat"] = F.zerocopy_from_numpy(
self.node_features[index]
).to(F.float32)
g.edata["feat"] = (
F.zerocopy_from_numpy(self.edge_features[index])
.to(F.float32)
.unsqueeze(1)
)
self.graphs.append(g)
def load(self):
self.graphs, label_dict = load_graphs(self.graph_path)
self.labels = label_dict["labels"]
def save(self):
save_graphs(
self.graph_path, self.graphs, labels={"labels": self.labels}
)
def has_cache(self):
return os.path.exists(self.graph_path)
def __len__(self):
return len(self.graphs)
def __getitem__(self, idx):
"""Get the idx-th sample.
Parameters
---------
idx : int or tensor
The sample index.
1-D tensor as `idx` is allowed when transform is None.
Returns
-------
(:class:`dgl.DGLGraph`, Tensor)
Graph with node feature stored in ``feat`` field and its label.
or
:class:`dgl.data.utils.Subset`
Subset of the dataset at specified indices
"""
if F.is_tensor(idx) and idx.dim() == 1:
if self._transform is None:
return Subset(self, idx.cpu())
raise ValueError(
"Tensor idx not supported when transform is not None."
)
if self._transform is None:
return self.graphs[idx], self.labels[idx]
return self._transform(self.graphs[idx]), self.labels[idx]
class MNISTSuperPixelDataset(SuperPixelDataset):
r"""MNIST superpixel dataset for the graph classification task.
DGL dataset of MNIST and CIFAR10 in the benchmark-gnn which contains graphs
converted fromt the original MINST and CIFAR10 images.
Reference `<http://arxiv.org/abs/2003.00982>`_
Statistics:
- Train examples: 60,000
- Test examples: 10,000
- Size of dataset images: 28
Parameters
----------
raw_dir : str
Directory to store all the downloaded raw datasets.
Default: "~/.dgl/".
split : str
Should be chosen from ["train", "test"]
Default: "train".
use_feature: bool
- True: Adj matrix defined from super-pixel locations + features
- False: Adj matrix defined from super-pixel locations (only)
Default: False.
force_reload : bool
Whether to reload the dataset.
Default: False.
verbose : bool
Whether to print out progress information.
Default: False.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Examples
---------
>>> from dgl.data import MNISTSuperPixelDataset
>>> # MNIST dataset
>>> train_dataset = MNISTSuperPixelDataset(split="train")
>>> len(train_dataset)
60000
>>> graph, label = train_dataset[0]
>>> graph
Graph(num_nodes=71, num_edges=568,
ndata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float32)}
edata_schemes={'feat': Scheme(shape=(1,), dtype=torch.float32)})
>>> # support tensor to be index when transform is None
>>> # see details in __getitem__ function
>>> import torch
>>> idx = torch.tensor([0, 1, 2])
>>> train_dataset_subset = train_dataset[idx]
>>> train_dataset_subset[0]
Graph(num_nodes=71, num_edges=568,
ndata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float32)}
edata_schemes={'feat': Scheme(shape=(1,), dtype=torch.float32)})
"""
def __init__(
self,
raw_dir=None,
split="train",
use_feature=False,
force_reload=False,
verbose=False,
transform=None,
):
super().__init__(
raw_dir=raw_dir,
name="MNIST",
split=split,
use_feature=use_feature,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
class CIFAR10SuperPixelDataset(SuperPixelDataset):
r"""CIFAR10 superpixel dataset for the graph classification task.
DGL dataset of CIFAR10 in the benchmark-gnn which contains graphs
converted fromt the original CIFAR10 images.
Reference `<http://arxiv.org/abs/2003.00982>`_
Statistics:
- Train examples: 50,000
- Test examples: 10,000
- Size of dataset images: 32
Parameters
----------
raw_dir : str
Directory to store all the downloaded raw datasets.
Default: "~/.dgl/".
split : str
Should be chosen from ["train", "test"]
Default: "train".
use_feature: bool
- True: Adj matrix defined from super-pixel locations + features
- False: Adj matrix defined from super-pixel locations (only)
Default: False.
force_reload : bool
Whether to reload the dataset.
Default: False.
verbose : bool
Whether to print out progress information.
Default: False.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Examples
---------
>>> from dgl.data import CIFAR10SuperPixelDataset
>>> # CIFAR10 dataset
>>> train_dataset = CIFAR10SuperPixelDataset(split="train")
>>> len(train_dataset)
50000
>>> graph, label = train_dataset[0]
>>> graph
Graph(num_nodes=123, num_edges=984,
ndata_schemes={'feat': Scheme(shape=(5,), dtype=torch.float32)}
edata_schemes={'feat': Scheme(shape=(1,), dtype=torch.float32)}),
>>> # support tensor to be index when transform is None
>>> # see details in __getitem__ function
>>> import torch
>>> idx = torch.tensor([0, 1, 2])
>>> train_dataset_subset = train_dataset[idx]
>>> train_dataset_subset[0]
Graph(num_nodes=123, num_edges=984,
ndata_schemes={'feat': Scheme(shape=(5,), dtype=torch.float32)}
edata_schemes={'feat': Scheme(shape=(1,), dtype=torch.float32)}),
"""
def __init__(
self,
raw_dir=None,
split="train",
use_feature=False,
force_reload=False,
verbose=False,
transform=None,
):
super().__init__(
raw_dir=raw_dir,
name="CIFAR10",
split=split,
use_feature=use_feature,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
+834
View File
@@ -0,0 +1,834 @@
"""Synthetic graph datasets."""
import math
import os
import pickle
import random
import networkx as nx
import numpy as np
from .. import backend as F
from ..batch import batch
from ..convert import graph
from ..transforms import reorder_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, download, load_graphs, save_graphs
class BAShapeDataset(DGLBuiltinDataset):
r"""BA-SHAPES dataset from `GNNExplainer: Generating Explanations for Graph Neural Networks
<https://arxiv.org/abs/1903.03894>`__
This is a synthetic dataset for node classification. It is generated by performing the
following steps in order.
- Construct a base BarabásiAlbert (BA) graph.
- Construct a set of five-node house-structured network motifs.
- Attach the motifs to randomly selected nodes of the base graph.
- Perturb the graph by adding random edges.
- Nodes are assigned to 4 classes. Nodes of label 0 belong to the base BA graph. Nodes of
label 1, 2, 3 are separately at the middle, bottom, or top of houses.
- Generate constant feature for all nodes, which is 1.
Parameters
----------
num_base_nodes : int, optional
Number of nodes in the base BA graph. Default: 300
num_base_edges_per_node : int, optional
Number of edges to attach from a new node to existing nodes in constructing the base BA
graph. Default: 5
num_motifs : int, optional
Number of house-structured network motifs to use. Default: 80
perturb_ratio : float, optional
Number of random edges to add in perturbation divided by the number of edges in the
original graph. Default: 0.01
seed : integer, random_state, or None, optional
Indicator of random number generation state. Default: None
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to always generate the data from scratch rather than load a cached version.
Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import BAShapeDataset
>>> dataset = BAShapeDataset()
>>> dataset.num_classes
4
>>> g = dataset[0]
>>> label = g.ndata['label']
>>> feat = g.ndata['feat']
"""
def __init__(
self,
num_base_nodes=300,
num_base_edges_per_node=5,
num_motifs=80,
perturb_ratio=0.01,
seed=None,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
self.num_base_nodes = num_base_nodes
self.num_base_edges_per_node = num_base_edges_per_node
self.num_motifs = num_motifs
self.perturb_ratio = perturb_ratio
self.seed = seed
super(BAShapeDataset, self).__init__(
name="BA-SHAPES",
url=None,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
g = nx.barabasi_albert_graph(
self.num_base_nodes, self.num_base_edges_per_node, self.seed
)
edges = list(g.edges())
src, dst = map(list, zip(*edges))
n = self.num_base_nodes
# Nodes in the base BA graph belong to class 0
node_labels = [0] * n
# The motifs will be evenly attached to the nodes in the base graph.
spacing = math.floor(n / self.num_motifs)
for motif_id in range(self.num_motifs):
# Construct a five-node house-structured network motif
motif_edges = [
(n, n + 1),
(n + 1, n + 2),
(n + 2, n + 3),
(n + 3, n),
(n + 4, n),
(n + 4, n + 1),
]
motif_src, motif_dst = map(list, zip(*motif_edges))
src.extend(motif_src)
dst.extend(motif_dst)
# Nodes at the middle of a house belong to class 1
# Nodes at the bottom of a house belong to class 2
# Nodes at the top of a house belong to class 3
node_labels.extend([1, 1, 2, 2, 3])
# Attach the motif to the base BA graph
src.append(n)
dst.append(int(motif_id * spacing))
n += 5
g = graph((src, dst), num_nodes=n)
# Perturb the graph by adding non-self-loop random edges
num_real_edges = g.num_edges()
max_ratio = (n * (n - 1) - num_real_edges) / num_real_edges
assert (
self.perturb_ratio <= max_ratio
), "perturb_ratio cannot exceed {:.4f}".format(max_ratio)
num_random_edges = int(num_real_edges * self.perturb_ratio)
if self.seed is not None:
np.random.seed(self.seed)
for _ in range(num_random_edges):
while True:
u = np.random.randint(0, n)
v = np.random.randint(0, n)
if (not g.has_edges_between(u, v)) and (u != v):
break
g.add_edges(u, v)
g.ndata["label"] = F.tensor(node_labels, F.int64)
g.ndata["feat"] = F.ones((n, 1), F.float32, F.cpu())
self._graph = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
@property
def graph_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.name)
)
def save(self):
save_graphs(str(self.graph_path), self._graph)
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
graphs, _ = load_graphs(str(self.graph_path))
self._graph = graphs[0]
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
return 1
@property
def num_classes(self):
return 4
class BACommunityDataset(DGLBuiltinDataset):
r"""BA-COMMUNITY dataset from `GNNExplainer: Generating Explanations for Graph Neural Networks
<https://arxiv.org/abs/1903.03894>`__
This is a synthetic dataset for node classification. It is generated by performing the
following steps in order.
- Construct a base BarabásiAlbert (BA) graph.
- Construct a set of five-node house-structured network motifs.
- Attach the motifs to randomly selected nodes of the base graph.
- Perturb the graph by adding random edges.
- Nodes are assigned to 4 classes. Nodes of label 0 belong to the base BA graph. Nodes of
label 1, 2, 3 are separately at the middle, bottom, or top of houses.
- Generate normally distributed features of length 10
- Repeat the above steps to generate another graph. Its nodes are assigned to class
4, 5, 6, 7. Its node features are generated with a distinct normal distribution.
- Join the two graphs by randomly adding edges between them.
Parameters
----------
num_base_nodes : int, optional
Number of nodes in each base BA graph. Default: 300
num_base_edges_per_node : int, optional
Number of edges to attach from a new node to existing nodes in constructing a base BA
graph. Default: 4
num_motifs : int, optional
Number of house-structured network motifs to use in constructing each graph. Default: 80
perturb_ratio : float, optional
Number of random edges to add to a graph in perturbation divided by the number of original
edges in it. Default: 0.01
num_inter_edges : int, optional
Number of random edges to add between the two graphs. Default: 350
seed : integer, random_state, or None, optional
Indicator of random number generation state. Default: None
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to always generate the data from scratch rather than load a cached version.
Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import BACommunityDataset
>>> dataset = BACommunityDataset()
>>> dataset.num_classes
8
>>> g = dataset[0]
>>> label = g.ndata['label']
>>> feat = g.ndata['feat']
"""
def __init__(
self,
num_base_nodes=300,
num_base_edges_per_node=4,
num_motifs=80,
perturb_ratio=0.01,
num_inter_edges=350,
seed=None,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
self.num_base_nodes = num_base_nodes
self.num_base_edges_per_node = num_base_edges_per_node
self.num_motifs = num_motifs
self.perturb_ratio = perturb_ratio
self.num_inter_edges = num_inter_edges
self.seed = seed
super(BACommunityDataset, self).__init__(
name="BA-COMMUNITY",
url=None,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
if self.seed is not None:
random.seed(self.seed)
np.random.seed(self.seed)
# Construct two BA-SHAPES graphs
g1 = BAShapeDataset(
self.num_base_nodes,
self.num_base_edges_per_node,
self.num_motifs,
self.perturb_ratio,
force_reload=True,
verbose=False,
)[0]
g2 = BAShapeDataset(
self.num_base_nodes,
self.num_base_edges_per_node,
self.num_motifs,
self.perturb_ratio,
force_reload=True,
verbose=False,
)[0]
# Join them and randomly add edges between them
g = batch([g1, g2])
num_nodes = g.num_nodes() // 2
src = np.random.randint(0, num_nodes, (self.num_inter_edges,))
dst = np.random.randint(
num_nodes, 2 * num_nodes, (self.num_inter_edges,)
)
src = F.astype(F.zerocopy_from_numpy(src), g.idtype)
dst = F.astype(F.zerocopy_from_numpy(dst), g.idtype)
g.add_edges(src, dst)
g.ndata["label"] = F.cat(
[g1.ndata["label"], g2.ndata["label"] + 4], dim=0
)
# feature generation
random_mu = [0.0] * 8
random_sigma = [1.0] * 8
mu_1, sigma_1 = np.array([-1.0] * 2 + random_mu), np.array(
[0.5] * 2 + random_sigma
)
feat1 = np.random.multivariate_normal(mu_1, np.diag(sigma_1), num_nodes)
mu_2, sigma_2 = np.array([1.0] * 2 + random_mu), np.array(
[0.5] * 2 + random_sigma
)
feat2 = np.random.multivariate_normal(mu_2, np.diag(sigma_2), num_nodes)
feat = np.concatenate([feat1, feat2])
g.ndata["feat"] = F.zerocopy_from_numpy(feat)
self._graph = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
@property
def graph_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.name)
)
def save(self):
save_graphs(str(self.graph_path), self._graph)
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
graphs, _ = load_graphs(str(self.graph_path))
self._graph = graphs[0]
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
return 1
@property
def num_classes(self):
return 8
class TreeCycleDataset(DGLBuiltinDataset):
r"""TREE-CYCLES dataset from `GNNExplainer: Generating Explanations for Graph Neural Networks
<https://arxiv.org/abs/1903.03894>`__
This is a synthetic dataset for node classification. It is generated by performing the
following steps in order.
- Construct a balanced binary tree as the base graph.
- Construct a set of cycle motifs.
- Attach the motifs to randomly selected nodes of the base graph.
- Perturb the graph by adding random edges.
- Generate constant feature for all nodes, which is 1.
- Nodes in the tree belong to class 0 and nodes in cycles belong to class 1.
Parameters
----------
tree_height : int, optional
Height of the balanced binary tree. Default: 8
num_motifs : int, optional
Number of cycle motifs to use. Default: 60
cycle_size : int, optional
Number of nodes in a cycle motif. Default: 6
perturb_ratio : float, optional
Number of random edges to add in perturbation divided by the
number of original edges in the graph. Default: 0.01
seed : integer, random_state, or None, optional
Indicator of random number generation state. Default: None
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to always generate the data from scratch rather than load a cached version.
Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import TreeCycleDataset
>>> dataset = TreeCycleDataset()
>>> dataset.num_classes
2
>>> g = dataset[0]
>>> label = g.ndata['label']
>>> feat = g.ndata['feat']
"""
def __init__(
self,
tree_height=8,
num_motifs=60,
cycle_size=6,
perturb_ratio=0.01,
seed=None,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
self.tree_height = tree_height
self.num_motifs = num_motifs
self.cycle_size = cycle_size
self.perturb_ratio = perturb_ratio
self.seed = seed
super(TreeCycleDataset, self).__init__(
name="TREE-CYCLES",
url=None,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
if self.seed is not None:
np.random.seed(self.seed)
g = nx.balanced_tree(r=2, h=self.tree_height)
edges = list(g.edges())
src, dst = map(list, zip(*edges))
n = nx.number_of_nodes(g)
# Nodes in the base tree graph belong to class 0
node_labels = [0] * n
# The motifs will be evenly attached to the nodes in the base graph.
spacing = math.floor(n / self.num_motifs)
for motif_id in range(self.num_motifs):
# Construct a six-node cycle
motif_edges = [(n + i, n + i + 1) for i in range(5)]
motif_edges.append((n + 5, n))
motif_src, motif_dst = map(list, zip(*motif_edges))
src.extend(motif_src)
dst.extend(motif_dst)
# Nodes in cycles belong to class 1
node_labels.extend([1] * self.cycle_size)
# Attach the motif to the base tree graph
anchor = int(motif_id * spacing)
src.append(n)
dst.append(anchor)
if np.random.random() > 0.5:
a = np.random.randint(1, 4)
b = np.random.randint(1, 4)
src.append(n + a)
dst.append(anchor + b)
n += self.cycle_size
g = graph((src, dst), num_nodes=n)
# Perturb the graph by adding non-self-loop random edges
num_real_edges = g.num_edges()
max_ratio = (n * (n - 1) - num_real_edges) / num_real_edges
assert (
self.perturb_ratio <= max_ratio
), "perturb_ratio cannot exceed {:.4f}".format(max_ratio)
num_random_edges = int(num_real_edges * self.perturb_ratio)
for _ in range(num_random_edges):
while True:
u = np.random.randint(0, n)
v = np.random.randint(0, n)
if (not g.has_edges_between(u, v)) and (u != v):
break
g.add_edges(u, v)
g.ndata["label"] = F.tensor(node_labels, F.int64)
g.ndata["feat"] = F.ones((n, 1), F.float32, F.cpu())
self._graph = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
@property
def graph_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.name)
)
def save(self):
save_graphs(str(self.graph_path), self._graph)
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
graphs, _ = load_graphs(str(self.graph_path))
self._graph = graphs[0]
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
return 1
@property
def num_classes(self):
return 2
class TreeGridDataset(DGLBuiltinDataset):
r"""TREE-GRIDS dataset from `GNNExplainer: Generating Explanations for Graph Neural Networks
<https://arxiv.org/abs/1903.03894>`__
This is a synthetic dataset for node classification. It is generated by performing the
following steps in order.
- Construct a balanced binary tree as the base graph.
- Construct a set of n-by-n grid motifs.
- Attach the motifs to randomly selected nodes of the base graph.
- Perturb the graph by adding random edges.
- Generate constant feature for all nodes, which is 1.
- Nodes in the tree belong to class 0 and nodes in grids belong to class 1.
Parameters
----------
tree_height : int, optional
Height of the balanced binary tree. Default: 8
num_motifs : int, optional
Number of grid motifs to use. Default: 80
grid_size : int, optional
The number of nodes in a grid motif will be grid_size ^ 2. Default: 3
perturb_ratio : float, optional
Number of random edges to add in perturbation divided by the
number of original edges in the graph. Default: 0.1
seed : integer, random_state, or None, optional
Indicator of random number generation state. Default: None
raw_dir : str, optional
Raw file directory to store the processed data. Default: ~/.dgl/
force_reload : bool, optional
Whether to always generate the data from scratch rather than load a cached version.
Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import TreeGridDataset
>>> dataset = TreeGridDataset()
>>> dataset.num_classes
2
>>> g = dataset[0]
>>> label = g.ndata['label']
>>> feat = g.ndata['feat']
"""
def __init__(
self,
tree_height=8,
num_motifs=80,
grid_size=3,
perturb_ratio=0.1,
seed=None,
raw_dir=None,
force_reload=False,
verbose=True,
transform=None,
):
self.tree_height = tree_height
self.num_motifs = num_motifs
self.grid_size = grid_size
self.perturb_ratio = perturb_ratio
self.seed = seed
super(TreeGridDataset, self).__init__(
name="TREE-GRIDS",
url=None,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
if self.seed is not None:
np.random.seed(self.seed)
g = nx.balanced_tree(r=2, h=self.tree_height)
edges = list(g.edges())
src, dst = map(list, zip(*edges))
n = nx.number_of_nodes(g)
# Nodes in the base tree graph belong to class 0
node_labels = [0] * n
# The motifs will be evenly attached to the nodes in the base graph.
spacing = math.floor(n / self.num_motifs)
# Construct an n-by-n grid
motif_g = nx.grid_graph([self.grid_size, self.grid_size])
grid_size = nx.number_of_nodes(motif_g)
motif_g = nx.convert_node_labels_to_integers(motif_g, first_label=0)
motif_edges = list(motif_g.edges())
motif_src, motif_dst = map(list, zip(*motif_edges))
motif_src, motif_dst = np.array(motif_src), np.array(motif_dst)
for motif_id in range(self.num_motifs):
src.extend((motif_src + n).tolist())
dst.extend((motif_dst + n).tolist())
# Nodes in grids belong to class 1
node_labels.extend([1] * grid_size)
# Attach the motif to the base tree graph
src.append(n)
dst.append(int(motif_id * spacing))
n += grid_size
g = graph((src, dst), num_nodes=n)
# Perturb the graph by adding non-self-loop random edges
num_real_edges = g.num_edges()
max_ratio = (n * (n - 1) - num_real_edges) / num_real_edges
assert (
self.perturb_ratio <= max_ratio
), "perturb_ratio cannot exceed {:.4f}".format(max_ratio)
num_random_edges = int(num_real_edges * self.perturb_ratio)
for _ in range(num_random_edges):
while True:
u = np.random.randint(0, n)
v = np.random.randint(0, n)
if (not g.has_edges_between(u, v)) and (u != v):
break
g.add_edges(u, v)
g.ndata["label"] = F.tensor(node_labels, F.int64)
g.ndata["feat"] = F.ones((n, 1), F.float32, F.cpu())
self._graph = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
@property
def graph_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.name)
)
def save(self):
save_graphs(str(self.graph_path), self._graph)
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
graphs, _ = load_graphs(str(self.graph_path))
self._graph = graphs[0]
def __getitem__(self, idx):
assert idx == 0, "This dataset has only one graph."
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
def __len__(self):
return 1
@property
def num_classes(self):
return 2
class BA2MotifDataset(DGLBuiltinDataset):
r"""BA-2motifs dataset from `Parameterized Explainer for Graph Neural Network
<https://arxiv.org/abs/2011.04573>`__
This is a synthetic dataset for graph classification. It was generated by
performing the following steps in order.
- Construct 1000 base BarabásiAlbert (BA) graphs.
- Attach house-structured network motifs to half of the base BA graphs.
- Attach five-node cycle motifs to the rest base BA graphs.
- Assign each graph to one of two classes according to the type of the attached motif.
Parameters
----------
raw_dir : str, optional
Raw file directory to download and store the data. Default: ~/.dgl/
force_reload : bool, optional
Whether to reload the dataset. Default: False
verbose : bool, optional
Whether to print progress information. Default: True
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access. Default: None
Attributes
----------
num_classes : int
Number of graph classes
Examples
--------
>>> from dgl.data import BA2MotifDataset
>>> dataset = BA2MotifDataset()
>>> dataset.num_classes
2
>>> # Get the first graph and its label
>>> g, label = dataset[0]
>>> feat = g.ndata['feat']
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=True, transform=None
):
super(BA2MotifDataset, self).__init__(
name="BA-2motifs",
url=_get_dgl_url("dataset/BA-2motif.pkl"),
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def download(self):
r"""Automatically download data."""
file_path = os.path.join(self.raw_dir, self.name + ".pkl")
download(self.url, path=file_path)
def process(self):
file_path = os.path.join(self.raw_dir, self.name + ".pkl")
with open(file_path, "rb") as f:
adjs, features, labels = pickle.load(f)
self.graphs = []
self.labels = F.tensor(labels, F.int64)
for i in range(len(adjs)):
g = graph(adjs[i].nonzero())
g.ndata["feat"] = F.zerocopy_from_numpy(features[i])
self.graphs.append(g)
@property
def graph_path(self):
return os.path.join(
self.save_path, "{}_dgl_graph.bin".format(self.name)
)
def save(self):
label_dict = {"labels": self.labels}
save_graphs(str(self.graph_path), self.graphs, label_dict)
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
self.graphs, label_dict = load_graphs(str(self.graph_path))
self.labels = label_dict["labels"]
def __getitem__(self, idx):
g = self.graphs[idx]
if self._transform is not None:
g = self._transform(g)
return g, self.labels[idx]
def __len__(self):
return len(self.graphs)
@property
def num_classes(self):
return 2
+69
View File
@@ -0,0 +1,69 @@
"""For Tensor Serialization"""
from __future__ import absolute_import
from .. import backend as F
from .._ffi.function import _init_api
from ..ndarray import NDArray
__all__ = ["save_tensors", "load_tensors"]
_init_api("dgl.data.tensor_serialize")
def save_tensors(filename, tensor_dict):
"""
Save dict of tensors to file
Parameters
----------
filename : str
File name to store dict of tensors.
tensor_dict: dict of dgl NDArray or backend tensor
Python dict using string as key and tensor as value
Returns
----------
status : bool
Return whether save operation succeeds
"""
nd_dict = {}
is_empty_dict = len(tensor_dict) == 0
for key, value in tensor_dict.items():
if not isinstance(key, str):
raise Exception("Dict key has to be str")
if F.is_tensor(value):
nd_dict[key] = F.zerocopy_to_dgl_ndarray(value)
elif isinstance(value, NDArray):
nd_dict[key] = value
else:
raise Exception(
"Dict value has to be backend tensor or dgl ndarray"
)
return _CAPI_SaveNDArrayDict(filename, nd_dict, is_empty_dict)
def load_tensors(filename, return_dgl_ndarray=False):
"""
load dict of tensors from file
Parameters
----------
filename : str
File name to load dict of tensors.
return_dgl_ndarray: bool
Whether return dict of dgl NDArrays or backend tensors
Returns
---------
tensor_dict : dict
dict of tensor or ndarray based on return_dgl_ndarray flag
"""
nd_dict = _CAPI_LoadNDArrayDict(filename)
tensor_dict = {}
for key, value in nd_dict.items():
if return_dgl_ndarray:
tensor_dict[key] = value
else:
tensor_dict[key] = F.zerocopy_from_dgl_ndarray(value)
return tensor_dict
+305
View File
@@ -0,0 +1,305 @@
"""Tree-structured data.
Including:
- Stanford Sentiment Treebank
"""
from __future__ import absolute_import
import os
from collections import OrderedDict
import networkx as nx
import numpy as np
from .. import backend as F
from ..convert import from_networkx
from .dgl_dataset import DGLBuiltinDataset
from .utils import (
_get_dgl_url,
deprecate_property,
load_graphs,
load_info,
save_graphs,
save_info,
)
__all__ = ["SST", "SSTDataset"]
class SSTDataset(DGLBuiltinDataset):
r"""Stanford Sentiment Treebank dataset.
Each sample is the constituency tree of a sentence. The leaf nodes
represent words. The word is a int value stored in the ``x`` feature field.
The non-leaf node has a special value ``PAD_WORD`` in the ``x`` field.
Each node also has a sentiment annotation: 5 classes (very negative,
negative, neutral, positive and very positive). The sentiment label is a
int value stored in the ``y`` feature field.
Official site: `<http://nlp.stanford.edu/sentiment/index.html>`_
Statistics:
- Train examples: 8,544
- Dev examples: 1,101
- Test examples: 2,210
- Number of classes for each node: 5
Parameters
----------
mode : str, optional
Should be one of ['train', 'dev', 'test', 'tiny']
Default: train
glove_embed_file : str, optional
The path to pretrained glove embedding file.
Default: None
vocab_file : str, optional
Optional vocabulary file. If not given, the default vacabulary file is used.
Default: None
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset. Default: False
verbose : bool
Whether to print out progress information. Default: True.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
vocab : OrderedDict
Vocabulary of the dataset
num_classes : int
Number of classes for each node
pretrained_emb: Tensor
Pretrained glove embedding with respect the vocabulary.
vocab_size : int
The size of the vocabulary
Notes
-----
All the samples will be loaded and preprocessed in the memory first.
Examples
--------
>>> # get dataset
>>> train_data = SSTDataset()
>>> dev_data = SSTDataset(mode='dev')
>>> test_data = SSTDataset(mode='test')
>>> tiny_data = SSTDataset(mode='tiny')
>>>
>>> len(train_data)
8544
>>> train_data.num_classes
5
>>> glove_embed = train_data.pretrained_emb
>>> train_data.vocab_size
19536
>>> train_data[0]
Graph(num_nodes=71, num_edges=70,
ndata_schemes={'x': Scheme(shape=(), dtype=torch.int64), 'y': Scheme(shape=(), dtype=torch.int64), 'mask': Scheme(shape=(), dtype=torch.int64)}
edata_schemes={})
>>> for tree in train_data:
... input_ids = tree.ndata['x']
... labels = tree.ndata['y']
... mask = tree.ndata['mask']
... # your code here
"""
PAD_WORD = -1 # special pad word id
UNK_WORD = -1 # out-of-vocabulary word id
def __init__(
self,
mode="train",
glove_embed_file=None,
vocab_file=None,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
assert mode in ["train", "dev", "test", "tiny"]
_url = _get_dgl_url("dataset/sst.zip")
self._glove_embed_file = glove_embed_file if mode == "train" else None
self.mode = mode
self._vocab_file = vocab_file
super(SSTDataset, self).__init__(
name="sst",
url=_url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
from nltk.corpus.reader import BracketParseCorpusReader
# load vocab file
self._vocab = OrderedDict()
vocab_file = (
self._vocab_file
if self._vocab_file is not None
else os.path.join(self.raw_path, "vocab.txt")
)
with open(vocab_file, encoding="utf-8") as vf:
for line in vf.readlines():
line = line.strip()
self._vocab[line] = len(self._vocab)
# filter glove
if self._glove_embed_file is not None and os.path.exists(
self._glove_embed_file
):
glove_emb = {}
with open(self._glove_embed_file, "r", encoding="utf-8") as pf:
for line in pf.readlines():
sp = line.split(" ")
if sp[0].lower() in self._vocab:
glove_emb[sp[0].lower()] = np.asarray(
[float(x) for x in sp[1:]]
)
files = ["{}.txt".format(self.mode)]
corpus = BracketParseCorpusReader(self.raw_path, files)
sents = corpus.parsed_sents(files[0])
# initialize with glove
pretrained_emb = []
fail_cnt = 0
for line in self._vocab.keys():
if self._glove_embed_file is not None and os.path.exists(
self._glove_embed_file
):
if not line.lower() in glove_emb:
fail_cnt += 1
pretrained_emb.append(
glove_emb.get(
line.lower(), np.random.uniform(-0.05, 0.05, 300)
)
)
self._pretrained_emb = None
if self._glove_embed_file is not None and os.path.exists(
self._glove_embed_file
):
self._pretrained_emb = F.tensor(np.stack(pretrained_emb, 0))
print(
"Miss word in GloVe {0:.4f}".format(
1.0 * fail_cnt / len(self._pretrained_emb)
)
)
# build trees
self._trees = []
for sent in sents:
self._trees.append(self._build_tree(sent))
def _build_tree(self, root):
g = nx.DiGraph()
def _rec_build(nid, node):
for child in node:
cid = g.number_of_nodes()
if isinstance(child[0], str) or isinstance(child[0], bytes):
# leaf node
word = self.vocab.get(child[0].lower(), self.UNK_WORD)
g.add_node(cid, x=word, y=int(child.label()), mask=1)
else:
g.add_node(
cid, x=SSTDataset.PAD_WORD, y=int(child.label()), mask=0
)
_rec_build(cid, child)
g.add_edge(cid, nid)
# add root
g.add_node(0, x=SSTDataset.PAD_WORD, y=int(root.label()), mask=0)
_rec_build(0, root)
ret = from_networkx(g, node_attrs=["x", "y", "mask"])
return ret
@property
def graph_path(self):
return os.path.join(self.save_path, self.mode + "_dgl_graph.bin")
@property
def vocab_path(self):
return os.path.join(self.save_path, "vocab.pkl")
def has_cache(self):
return os.path.exists(self.graph_path) and os.path.exists(
self.vocab_path
)
def save(self):
save_graphs(self.graph_path, self._trees)
save_info(self.vocab_path, {"vocab": self.vocab})
if self.pretrained_emb:
emb_path = os.path.join(self.save_path, "emb.pkl")
save_info(emb_path, {"embed": self.pretrained_emb})
def load(self):
emb_path = os.path.join(self.save_path, "emb.pkl")
self._trees = load_graphs(self.graph_path)[0]
self._vocab = load_info(self.vocab_path)["vocab"]
self._pretrained_emb = None
if os.path.exists(emb_path):
self._pretrained_emb = load_info(emb_path)["embed"]
@property
def vocab(self):
r"""Vocabulary
Returns
-------
OrderedDict
"""
return self._vocab
@property
def pretrained_emb(self):
r"""Pre-trained word embedding, if given."""
return self._pretrained_emb
def __getitem__(self, idx):
r"""Get graph by index
Parameters
----------
idx : int
Returns
-------
:class:`dgl.DGLGraph`
graph structure, word id for each node, node labels and masks.
- ``ndata['x']``: word id of the node
- ``ndata['y']:`` label of the node
- ``ndata['mask']``: 1 if the node is a leaf, otherwise 0
"""
if self._transform is None:
return self._trees[idx]
else:
return self._transform(self._trees[idx])
def __len__(self):
r"""Number of graphs in the dataset."""
return len(self._trees)
@property
def vocab_size(self):
r"""Vocabulary size."""
return len(self._vocab)
@property
def num_classes(self):
r"""Number of classes for each node."""
return 5
SST = SSTDataset
+532
View File
@@ -0,0 +1,532 @@
from __future__ import absolute_import
import os
import numpy as np
from .. import backend as F
from ..convert import graph as dgl_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import load_graphs, load_info, loadtxt, save_graphs, save_info
class LegacyTUDataset(DGLBuiltinDataset):
r"""LegacyTUDataset contains lots of graph kernel datasets for graph classification.
Parameters
----------
name : str
Dataset Name, such as ``ENZYMES``, ``DD``, ``COLLAB``, ``MUTAG``, can be the
datasets name on `<https://chrsmrrs.github.io/datasets/docs/datasets/>`_.
use_pandas : bool
Numpy's file read function has performance issue when file is large,
using pandas can be faster.
Default: False
hidden_size : int
Some dataset doesn't contain features.
Use constant node features initialization instead, with hidden size as ``hidden_size``.
Default : 10
max_allow_node : int
Remove graphs that contains more nodes than ``max_allow_node``.
Default : None
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
max_num_node : int
Maximum number of nodes
num_classes : int
Number of classes
num_labels : numpy.int64
(DEPRECATED, use num_classes instead) Number of classes
Notes
-----
LegacyTUDataset uses provided node feature by default. If no feature provided, it uses one-hot node label instead.
If neither labels provided, it uses constant for node feature.
The dataset sorts graphs by their labels.
Shuffle is preferred before manual train/val split.
Examples
--------
>>> data = LegacyTUDataset('DD')
The dataset instance is an iterable
>>> len(data)
1178
>>> g, label = data[1024]
>>> g
Graph(num_nodes=88, num_edges=410,
ndata_schemes={'feat': Scheme(shape=(89,), dtype=torch.float32), '_ID': Scheme(shape=(), dtype=torch.int64)}
edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)})
>>> label
tensor(1)
Batch the graphs and labels for mini-batch training
>>> graphs, labels = zip(*[data[i] for i in range(16)])
>>> batched_graphs = dgl.batch(graphs)
>>> batched_labels = torch.tensor(labels)
>>> batched_graphs
Graph(num_nodes=9539, num_edges=47382,
ndata_schemes={'feat': Scheme(shape=(89,), dtype=torch.float32), '_ID': Scheme(shape=(), dtype=torch.int64)}
edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)})
"""
_url = r"https://www.chrsmrrs.com/graphkerneldatasets/{}.zip"
def __init__(
self,
name,
use_pandas=False,
hidden_size=10,
max_allow_node=None,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
url = self._url.format(name)
self.hidden_size = hidden_size
self.max_allow_node = max_allow_node
self.use_pandas = use_pandas
super(LegacyTUDataset, self).__init__(
name=name,
url=url,
raw_dir=raw_dir,
hash_key=(name, use_pandas, hidden_size, max_allow_node),
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
self.data_mode = None
if self.use_pandas:
import pandas as pd
DS_edge_list = self._idx_from_zero(
pd.read_csv(
self._file_path("A"), delimiter=",", dtype=int, header=None
).values
)
else:
DS_edge_list = self._idx_from_zero(
np.genfromtxt(self._file_path("A"), delimiter=",", dtype=int)
)
DS_indicator = self._idx_from_zero(
np.genfromtxt(self._file_path("graph_indicator"), dtype=int)
)
if os.path.exists(self._file_path("graph_labels")):
DS_graph_labels = self._idx_from_zero(
np.genfromtxt(self._file_path("graph_labels"), dtype=int)
)
self.num_labels = max(DS_graph_labels) + 1
self.graph_labels = DS_graph_labels
elif os.path.exists(self._file_path("graph_attributes")):
DS_graph_labels = np.genfromtxt(
self._file_path("graph_attributes"), dtype=float
)
self.num_labels = None
self.graph_labels = DS_graph_labels
else:
raise Exception("Unknown graph label or graph attributes")
g = dgl_graph(([], []))
g.add_nodes(int(DS_edge_list.max()) + 1)
g.add_edges(DS_edge_list[:, 0], DS_edge_list[:, 1])
node_idx_list = []
self.max_num_node = 0
for idx in range(np.max(DS_indicator) + 1):
node_idx = np.where(DS_indicator == idx)
node_idx_list.append(node_idx[0])
if len(node_idx[0]) > self.max_num_node:
self.max_num_node = len(node_idx[0])
self.graph_lists = [g.subgraph(node_idx) for node_idx in node_idx_list]
try:
DS_node_labels = self._idx_from_zero(
np.loadtxt(self._file_path("node_labels"), dtype=int)
)
g.ndata["node_label"] = F.tensor(DS_node_labels)
one_hot_node_labels = self._to_onehot(DS_node_labels)
for idxs, g in zip(node_idx_list, self.graph_lists):
g.ndata["feat"] = F.tensor(
one_hot_node_labels[idxs, :], F.float32
)
self.data_mode = "node_label"
except IOError:
print("No Node Label Data")
try:
DS_node_attr = np.loadtxt(
self._file_path("node_attributes"), delimiter=","
)
if DS_node_attr.ndim == 1:
DS_node_attr = np.expand_dims(DS_node_attr, -1)
for idxs, g in zip(node_idx_list, self.graph_lists):
g.ndata["feat"] = F.tensor(DS_node_attr[idxs, :], F.float32)
self.data_mode = "node_attr"
except IOError:
print("No Node Attribute Data")
if "feat" not in g.ndata.keys():
for idxs, g in zip(node_idx_list, self.graph_lists):
g.ndata["feat"] = F.ones(
(g.num_nodes(), self.hidden_size), F.float32, F.cpu()
)
self.data_mode = "constant"
if self.verbose:
print(
"Use Constant one as Feature with hidden size {}".format(
self.hidden_size
)
)
# remove graphs that are too large by user given standard
# optional pre-processing steop in conformity with Rex Ying's original
# DiffPool implementation
if self.max_allow_node:
preserve_idx = []
if self.verbose:
print("original dataset length : ", len(self.graph_lists))
for i, g in enumerate(self.graph_lists):
if g.num_nodes() <= self.max_allow_node:
preserve_idx.append(i)
self.graph_lists = [self.graph_lists[i] for i in preserve_idx]
if self.verbose:
print(
"after pruning graphs that are too big : ",
len(self.graph_lists),
)
self.graph_labels = [self.graph_labels[i] for i in preserve_idx]
self.max_num_node = self.max_allow_node
self.graph_labels = F.tensor(self.graph_labels)
def save(self):
label_dict = {"labels": self.graph_labels}
info_dict = {
"max_num_node": self.max_num_node,
"num_labels": self.num_labels,
}
save_graphs(str(self.graph_path), self.graph_lists, label_dict)
save_info(str(self.info_path), info_dict)
def load(self):
graphs, label_dict = load_graphs(str(self.graph_path))
info_dict = load_info(str(self.info_path))
self.graph_lists = graphs
self.graph_labels = label_dict["labels"]
self.max_num_node = info_dict["max_num_node"]
self.num_labels = info_dict["num_labels"]
@property
def graph_path(self):
return os.path.join(
self.save_path, "legacy_tu_{}_{}.bin".format(self.name, self.hash)
)
@property
def info_path(self):
return os.path.join(
self.save_path, "legacy_tu_{}_{}.pkl".format(self.name, self.hash)
)
def has_cache(self):
if os.path.exists(self.graph_path) and os.path.exists(self.info_path):
return True
return False
def __getitem__(self, idx):
"""Get the idx-th sample.
Parameters
---------
idx : int
The sample index.
Returns
-------
(:class:`dgl.DGLGraph`, Tensor)
Graph with node feature stored in ``feat`` field and node label in ``node_label`` if available.
And its label.
"""
g = self.graph_lists[idx]
if self._transform is not None:
g = self._transform(g)
return g, self.graph_labels[idx]
def __len__(self):
"""Return the number of graphs in the dataset."""
return len(self.graph_lists)
def _file_path(self, category):
return os.path.join(
self.raw_path, self.name, "{}_{}.txt".format(self.name, category)
)
@staticmethod
def _idx_from_zero(idx_tensor):
return idx_tensor - np.min(idx_tensor)
@staticmethod
def _to_onehot(label_tensor):
label_num = label_tensor.shape[0]
assert np.min(label_tensor) == 0
one_hot_tensor = np.zeros((label_num, np.max(label_tensor) + 1))
one_hot_tensor[np.arange(label_num), label_tensor] = 1
return one_hot_tensor
def statistics(self):
return (
self.graph_lists[0].ndata["feat"].shape[1],
self.num_labels,
self.max_num_node,
)
@property
def num_classes(self):
return int(self.num_labels)
class TUDataset(DGLBuiltinDataset):
r"""
TUDataset contains lots of graph kernel datasets for graph classification.
Parameters
----------
name : str
Dataset Name, such as ``ENZYMES``, ``DD``, ``COLLAB``, ``MUTAG``, can be the
datasets name on `<https://chrsmrrs.github.io/datasets/docs/datasets/>`_.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
max_num_node : int
Maximum number of nodes
num_classes : int
Number of classes
num_labels : int
(DEPRECATED, use num_classes instead) Number of classes
Notes
-----
**IMPORTANT:** Some of the datasets have duplicate edges exist in the graphs, e.g.
the edges in ``IMDB-BINARY`` are all duplicated. DGL faithfully keeps the duplicates
as per the original data. Other frameworks such as PyTorch Geometric removes the
duplicates by default. You can remove the duplicate edges with :func:`dgl.to_simple`.
Graphs may have node labels, node attributes, edge labels, and edge attributes,
varing from different dataset.
Labels are mapped to :math:`\lbrace 0,\cdots,n-1 \rbrace` where :math:`n` is the
number of labels (some datasets have raw labels :math:`\lbrace -1, 1 \rbrace` which
will be mapped to :math:`\lbrace 0, 1 \rbrace`). In previous versions, the minimum
label was added so that :math:`\lbrace -1, 1 \rbrace` was mapped to
:math:`\lbrace 0, 2 \rbrace`.
The dataset sorts graphs by their labels.
Shuffle is preferred before manual train/val split.
Examples
--------
>>> data = TUDataset('DD')
The dataset instance is an iterable
>>> len(data)
1178
>>> g, label = data[1024]
>>> g
Graph(num_nodes=88, num_edges=410,
ndata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64), 'node_labels': Scheme(shape=(1,), dtype=torch.int64)}
edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)})
>>> label
tensor([1])
Batch the graphs and labels for mini-batch training
>>> graphs, labels = zip(*[data[i] for i in range(16)])
>>> batched_graphs = dgl.batch(graphs)
>>> batched_labels = torch.tensor(labels)
>>> batched_graphs
Graph(num_nodes=9539, num_edges=47382,
ndata_schemes={'node_labels': Scheme(shape=(1,), dtype=torch.int64), '_ID': Scheme(shape=(), dtype=torch.int64)}
edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64)})
"""
_url = r"https://www.chrsmrrs.com/graphkerneldatasets/{}.zip"
def __init__(
self,
name,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
url = self._url.format(name)
super(TUDataset, self).__init__(
name=name,
url=url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
DS_edge_list = self._idx_from_zero(
loadtxt(self._file_path("A"), delimiter=",").astype(int)
)
DS_indicator = self._idx_from_zero(
loadtxt(self._file_path("graph_indicator"), delimiter=",").astype(
int
)
)
if os.path.exists(self._file_path("graph_labels")):
DS_graph_labels = self._idx_reset(
loadtxt(self._file_path("graph_labels"), delimiter=",").astype(
int
)
)
self.num_labels = int(max(DS_graph_labels) + 1)
self.graph_labels = F.tensor(DS_graph_labels)
elif os.path.exists(self._file_path("graph_attributes")):
DS_graph_labels = loadtxt(
self._file_path("graph_attributes"), delimiter=","
).astype(float)
self.num_labels = None
self.graph_labels = F.tensor(DS_graph_labels)
else:
raise Exception("Unknown graph label or graph attributes")
g = dgl_graph(([], []))
g.add_nodes(int(DS_edge_list.max()) + 1)
g.add_edges(DS_edge_list[:, 0], DS_edge_list[:, 1])
node_idx_list = []
self.max_num_node = 0
for idx in range(np.max(DS_indicator) + 1):
node_idx = np.where(DS_indicator == idx)
node_idx_list.append(node_idx[0])
if len(node_idx[0]) > self.max_num_node:
self.max_num_node = len(node_idx[0])
self.attr_dict = {
"node_labels": ("ndata", "node_labels"),
"node_attributes": ("ndata", "node_attr"),
"edge_labels": ("edata", "edge_labels"),
"edge_attributes": ("edata", "node_labels"),
}
for filename, field_name in self.attr_dict.items():
try:
data = loadtxt(self._file_path(filename), delimiter=",")
if "label" in filename:
data = F.tensor(self._idx_from_zero(data))
else:
data = F.tensor(data)
getattr(g, field_name[0])[field_name[1]] = data
except IOError:
pass
self.graph_lists = [g.subgraph(node_idx) for node_idx in node_idx_list]
@property
def graph_path(self):
return os.path.join(self.save_path, "tu_{}.bin".format(self.name))
@property
def info_path(self):
return os.path.join(self.save_path, "tu_{}.pkl".format(self.name))
def save(self):
label_dict = {"labels": self.graph_labels}
info_dict = {
"max_num_node": self.max_num_node,
"num_labels": self.num_labels,
}
save_graphs(str(self.graph_path), self.graph_lists, label_dict)
save_info(str(self.info_path), info_dict)
def load(self):
graphs, label_dict = load_graphs(str(self.graph_path))
info_dict = load_info(str(self.info_path))
self.graph_lists = graphs
self.graph_labels = label_dict["labels"]
self.max_num_node = info_dict["max_num_node"]
self.num_labels = info_dict["num_labels"]
def has_cache(self):
if os.path.exists(self.graph_path) and os.path.exists(self.info_path):
return True
return False
def __getitem__(self, idx):
"""Get the idx-th sample.
Parameters
---------
idx : int
The sample index.
Returns
-------
(:class:`dgl.DGLGraph`, Tensor)
Graph with node feature stored in ``feat`` field and node label in ``node_labels`` if available.
And its label.
"""
g = self.graph_lists[idx]
if self._transform is not None:
g = self._transform(g)
return g, self.graph_labels[idx]
def __len__(self):
"""Return the number of graphs in the dataset."""
return len(self.graph_lists)
def _file_path(self, category):
return os.path.join(
self.raw_path, self.name, "{}_{}.txt".format(self.name, category)
)
@staticmethod
def _idx_from_zero(idx_tensor):
return idx_tensor - np.min(idx_tensor)
@staticmethod
def _idx_reset(idx_tensor):
"""Maps n unique labels to {0, ..., n-1} in an ordered fashion."""
labels = np.unique(idx_tensor)
relabel_map = {x: i for i, x in enumerate(labels)}
new_idx_tensor = np.vectorize(relabel_map.get)(idx_tensor)
return new_idx_tensor
def statistics(self):
return (
self.graph_lists[0].ndata["feat"].shape[1],
self.num_labels,
self.max_num_node,
)
@property
def num_classes(self):
return self.num_labels
+683
View File
@@ -0,0 +1,683 @@
"""Dataset utilities."""
from __future__ import absolute_import
import errno
import hashlib
import os
import pickle
import sys
import warnings
import networkx.algorithms as A
import numpy as np
import requests
from tqdm.auto import tqdm
from .. import backend as F
from .graph_serialize import load_graphs, load_labels, save_graphs
from .tensor_serialize import load_tensors, save_tensors
__all__ = [
"loadtxt",
"download",
"check_sha1",
"extract_archive",
"get_download_dir",
"Subset",
"split_dataset",
"save_graphs",
"load_graphs",
"load_labels",
"save_tensors",
"load_tensors",
"add_nodepred_split",
"add_node_property_split",
"mask_nodes_by_property",
]
def loadtxt(path, delimiter, dtype=None):
try:
import pandas as pd
df = pd.read_csv(path, delimiter=delimiter, header=None)
return df.values
except ImportError:
warnings.warn(
"Pandas is not installed, now using numpy.loadtxt to load data, "
"which could be extremely slow. Accelerate by installing pandas"
)
return np.loadtxt(path, delimiter=delimiter)
def _get_dgl_url(file_url):
"""Get DGL online url for download."""
dgl_repo_url = "https://data.dgl.ai/"
repo_url = os.environ.get("DGL_REPO", dgl_repo_url)
if repo_url[-1] != "/":
repo_url = repo_url + "/"
return repo_url + file_url
def split_dataset(dataset, frac_list=None, shuffle=False, random_state=None):
"""Split dataset into training, validation and test set.
Parameters
----------
dataset
We assume ``len(dataset)`` gives the number of datapoints and ``dataset[i]``
gives the ith datapoint.
frac_list : list or None, optional
A list of length 3 containing the fraction to use for training,
validation and test. If None, we will use [0.8, 0.1, 0.1].
shuffle : bool, optional
By default we perform a consecutive split of the dataset. If True,
we will first randomly shuffle the dataset.
random_state : None, int or array_like, optional
Random seed used to initialize the pseudo-random number generator.
Can be any integer between 0 and 2**32 - 1 inclusive, an array
(or other sequence) of such integers, or None (the default).
If seed is None, then RandomState will try to read data from /dev/urandom
(or the Windows analogue) if available or seed from the clock otherwise.
Returns
-------
list of length 3
Subsets for training, validation and test.
"""
from itertools import accumulate
if frac_list is None:
frac_list = [0.8, 0.1, 0.1]
frac_list = np.asarray(frac_list)
assert np.allclose(
np.sum(frac_list), 1.0
), "Expect frac_list sum to 1, got {:.4f}".format(np.sum(frac_list))
num_data = len(dataset)
lengths = (num_data * frac_list).astype(int)
lengths[-1] = num_data - np.sum(lengths[:-1])
if shuffle:
indices = np.random.RandomState(seed=random_state).permutation(num_data)
else:
indices = np.arange(num_data)
return [
Subset(dataset, indices[offset - length : offset])
for offset, length in zip(accumulate(lengths), lengths)
]
def download(
url,
path=None,
overwrite=True,
sha1_hash=None,
retries=5,
verify_ssl=True,
log=True,
):
"""Download a given URL.
Codes borrowed from mxnet/gluon/utils.py
Parameters
----------
url : str
URL to download.
path : str, optional
Destination path to store downloaded file. By default stores to the
current directory with the same name as in url.
overwrite : bool, optional
Whether to overwrite the destination file if it already exists.
By default always overwrites the downloaded file.
sha1_hash : str, optional
Expected sha1 hash in hexadecimal digits. Will ignore existing file when hash is specified
but doesn't match.
retries : integer, default 5
The number of times to attempt downloading in case of failure or non 200 return codes.
verify_ssl : bool, default True
Verify SSL certificates.
log : bool, default True
Whether to print the progress for download
Returns
-------
str
The file path of the downloaded file.
"""
if path is None:
fname = url.split("/")[-1]
# Empty filenames are invalid
assert fname, (
"Can't construct file-name from this URL. "
"Please set the `path` option manually."
)
else:
path = os.path.expanduser(path)
if os.path.isdir(path):
fname = os.path.join(path, url.split("/")[-1])
else:
fname = path
assert retries >= 0, "Number of retries should be at least 0"
if not verify_ssl:
warnings.warn(
"Unverified HTTPS request is being made (verify_ssl=False). "
"Adding certificate verification is strongly advised."
)
if (
overwrite
or not os.path.exists(fname)
or (sha1_hash and not check_sha1(fname, sha1_hash))
):
dirname = os.path.dirname(os.path.abspath(os.path.expanduser(fname)))
if not os.path.exists(dirname):
os.makedirs(dirname)
while retries + 1 > 0:
# Disable pyling too broad Exception
# pylint: disable=W0703
try:
if log:
print("Downloading %s from %s..." % (fname, url))
r = requests.get(url, stream=True, verify=verify_ssl)
if r.status_code != 200:
raise RuntimeError("Failed downloading url %s" % url)
# Get the total file size.
total_size = int(r.headers.get("content-length", 0))
with tqdm(
total=total_size, unit="B", unit_scale=True, desc=fname
) as bar:
with open(fname, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
bar.update(len(chunk))
if sha1_hash and not check_sha1(fname, sha1_hash):
raise UserWarning(
"File {} is downloaded but the content hash does not match."
" The repo may be outdated or download may be incomplete. "
'If the "repo_url" is overridden, consider switching to '
"the default repo.".format(fname)
)
break
except Exception as e:
retries -= 1
if retries <= 0:
raise e
else:
if log:
print(
"download failed, retrying, {} attempt{} left".format(
retries, "s" if retries > 1 else ""
)
)
return fname
def check_sha1(filename, sha1_hash):
"""Check whether the sha1 hash of the file content matches the expected hash.
Codes borrowed from mxnet/gluon/utils.py
Parameters
----------
filename : str
Path to the file.
sha1_hash : str
Expected sha1 hash in hexadecimal digits.
Returns
-------
bool
Whether the file content matches the expected hash.
"""
sha1 = hashlib.sha1()
with open(filename, "rb") as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data)
return sha1.hexdigest() == sha1_hash
def extract_archive(file, target_dir, overwrite=True):
"""Extract archive file.
Parameters
----------
file : str
Absolute path of the archive file.
target_dir : str
Target directory of the archive to be uncompressed.
overwrite : bool, default True
Whether to overwrite the contents inside the directory.
By default always overwrites.
"""
if os.path.exists(target_dir) and not overwrite:
return
print("Extracting file to {}".format(target_dir))
if (
file.endswith(".tar.gz")
or file.endswith(".tar")
or file.endswith(".tgz")
):
import tarfile
with tarfile.open(file, "r") as archive:
def is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
prefix = os.path.commonprefix([abs_directory, abs_target])
return prefix == abs_directory
def safe_extract(
tar, path=".", members=None, *, numeric_owner=False
):
for member in tar.getmembers():
member_path = os.path.join(path, member.name)
if not is_within_directory(path, member_path):
raise Exception("Attempted Path Traversal in Tar File")
tar.extractall(path, members, numeric_owner=numeric_owner)
safe_extract(archive, path=target_dir)
elif file.endswith(".gz"):
import gzip
import shutil
with gzip.open(file, "rb") as f_in:
target_file = os.path.join(target_dir, os.path.basename(file)[:-3])
with open(target_file, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
elif file.endswith(".zip"):
import zipfile
with zipfile.ZipFile(file, "r") as archive:
archive.extractall(path=target_dir)
else:
raise Exception("Unrecognized file type: " + file)
def get_download_dir():
"""Get the absolute path to the download directory.
Returns
-------
dirname : str
Path to the download directory
"""
default_dir = os.path.join(os.path.expanduser("~"), ".dgl")
dirname = os.environ.get("DGL_DOWNLOAD_DIR", default_dir)
if not os.path.exists(dirname):
os.makedirs(dirname)
return dirname
def makedirs(path):
try:
os.makedirs(os.path.expanduser(os.path.normpath(path)))
except OSError as e:
if e.errno != errno.EEXIST and os.path.isdir(path):
raise e
def save_info(path, info):
"""Save dataset related information into disk.
Parameters
----------
path : str
File to save information.
info : dict
A python dict storing information to save on disk.
"""
with open(path, "wb") as pf:
pickle.dump(info, pf)
def load_info(path):
"""Load dataset related information from disk.
Parameters
----------
path : str
File to load information from.
Returns
-------
info : dict
A python dict storing information loaded from disk.
"""
with open(path, "rb") as pf:
info = pickle.load(pf)
return info
def deprecate_property(old, new):
warnings.warn(
"Property {} will be deprecated, please use {} instead.".format(
old, new
)
)
def deprecate_function(old, new):
warnings.warn(
"Function {} will be deprecated, please use {} instead.".format(
old, new
)
)
def deprecate_class(old, new):
warnings.warn(
"Class {} will be deprecated, please use {} instead.".format(old, new)
)
def idx2mask(idx, len):
"""Create mask."""
mask = np.zeros(len)
mask[idx] = 1
return mask
def generate_mask_tensor(mask):
"""Generate mask tensor according to different backend
For torch and tensorflow, it will create a bool tensor
For mxnet, it will create a float tensor
Parameters
----------
mask: numpy ndarray
input mask tensor
"""
assert isinstance(mask, np.ndarray), (
"input for generate_mask_tensor" "should be an numpy ndarray"
)
if F.backend_name == "mxnet":
return F.tensor(mask, dtype=F.data_type_dict["float32"])
else:
return F.tensor(mask, dtype=F.data_type_dict["bool"])
class Subset(object):
"""Subset of a dataset at specified indices
Code adapted from PyTorch.
Parameters
----------
dataset
dataset[i] should return the ith datapoint
indices : list
List of datapoint indices to construct the subset
"""
def __init__(self, dataset, indices):
self.dataset = dataset
self.indices = indices
def __getitem__(self, item):
"""Get the datapoint indexed by item
Returns
-------
tuple
datapoint
"""
return self.dataset[self.indices[item]]
def __len__(self):
"""Get subset size
Returns
-------
int
Number of datapoints in the subset
"""
return len(self.indices)
def add_nodepred_split(dataset, ratio, ntype=None):
"""Split the given dataset into training, validation and test sets for
transductive node predction task.
It adds three node mask arrays ``'train_mask'``, ``'val_mask'`` and ``'test_mask'``,
to each graph in the dataset. Each sample in the dataset thus must be a :class:`DGLGraph`.
Fix the random seed of NumPy to make the result deterministic::
numpy.random.seed(42)
Parameters
----------
dataset : DGLDataset
The dataset to modify.
ratio : (float, float, float)
Split ratios for training, validation and test sets. Must sum to one.
ntype : str, optional
The node type to add mask for.
Examples
--------
>>> dataset = dgl.data.AmazonCoBuyComputerDataset()
>>> print('train_mask' in dataset[0].ndata)
False
>>> dgl.data.utils.add_nodepred_split(dataset, [0.8, 0.1, 0.1])
>>> print('train_mask' in dataset[0].ndata)
True
"""
if len(ratio) != 3:
raise ValueError(
f"Split ratio must be a float triplet but got {ratio}."
)
for i in range(len(dataset)):
g = dataset[i]
n = g.num_nodes(ntype)
idx = np.arange(0, n)
np.random.shuffle(idx)
n_train, n_val, n_test = (
int(n * ratio[0]),
int(n * ratio[1]),
int(n * ratio[2]),
)
train_mask = generate_mask_tensor(idx2mask(idx[:n_train], n))
val_mask = generate_mask_tensor(
idx2mask(idx[n_train : n_train + n_val], n)
)
test_mask = generate_mask_tensor(idx2mask(idx[n_train + n_val :], n))
g.nodes[ntype].data["train_mask"] = train_mask
g.nodes[ntype].data["val_mask"] = val_mask
g.nodes[ntype].data["test_mask"] = test_mask
def mask_nodes_by_property(property_values, part_ratios, random_seed=None):
"""Provide the split masks for a node split with distributional shift based on a given
node property, as proposed in `Evaluating Robustness and Uncertainty of Graph Models
Under Structural Distributional Shifts <https://arxiv.org/abs/2302.13875>`__
It considers the in-distribution (ID) and out-of-distribution (OOD) subsets of nodes.
The ID subset includes training, validation and testing parts, while the OOD subset
includes validation and testing parts. It sorts the nodes in the ascending order of
their property values, splits them into 5 non-intersecting parts, and creates 5
associated node mask arrays:
- 3 for the ID nodes: ``'in_train_mask'``, ``'in_valid_mask'``, ``'in_test_mask'``,
- and 2 for the OOD nodes: ``'out_valid_mask'``, ``'out_test_mask'``.
Parameters
----------
property_values : numpy ndarray
The node property (float) values by which the dataset will be split.
The length of the array must be equal to the number of nodes in graph.
part_ratios : list
A list of 5 ratios for training, ID validation, ID test,
OOD validation, OOD testing parts. The values in the list must sum to one.
random_seed : int, optional
Random seed to fix for the initial permutation of nodes. It is
used to create a random order for the nodes that have the same
property values or belong to the ID subset. (default: None)
Returns
----------
split_masks : dict
A python dict storing the mask names as keys and the corresponding
node mask arrays as values.
Examples
--------
>>> num_nodes = 1000
>>> property_values = np.random.uniform(size=num_nodes)
>>> part_ratios = [0.3, 0.1, 0.1, 0.3, 0.2]
>>> split_masks = dgl.data.utils.mask_nodes_by_property(property_values, part_ratios)
>>> print('in_valid_mask' in split_masks)
True
"""
num_nodes = len(property_values)
part_sizes = np.round(num_nodes * np.array(part_ratios)).astype(int)
part_sizes[-1] -= np.sum(part_sizes) - num_nodes
generator = np.random.RandomState(random_seed)
permutation = generator.permutation(num_nodes)
node_indices = np.arange(num_nodes)[permutation]
property_values = property_values[permutation]
in_distribution_size = np.sum(part_sizes[:3])
node_indices_ordered = node_indices[np.argsort(property_values)]
node_indices_ordered[:in_distribution_size] = generator.permutation(
node_indices_ordered[:in_distribution_size]
)
sections = np.cumsum(part_sizes)
node_split = np.split(node_indices_ordered, sections)[:-1]
mask_names = [
"in_train_mask",
"in_valid_mask",
"in_test_mask",
"out_valid_mask",
"out_test_mask",
]
split_masks = {}
for mask_name, node_indices in zip(mask_names, node_split):
split_mask = idx2mask(node_indices, num_nodes)
split_masks[mask_name] = generate_mask_tensor(split_mask)
return split_masks
def add_node_property_split(
dataset, part_ratios, property_name, ascending=True, random_seed=None
):
"""Create a node split with distributional shift based on a given node property,
as proposed in `Evaluating Robustness and Uncertainty of Graph Models Under
Structural Distributional Shifts <https://arxiv.org/abs/2302.13875>`__
It splits the nodes of each graph in the given dataset into 5 non-intersecting
parts based on their structural properties. This can be used for transductive node
prediction task with distributional shifts.
It considers the in-distribution (ID) and out-of-distribution (OOD) subsets of nodes.
The ID subset includes training, validation and testing parts, while the OOD subset
includes validation and testing parts. As a result, it creates 5 associated node mask
arrays for each graph:
- 3 for the ID nodes: ``'in_train_mask'``, ``'in_valid_mask'``, ``'in_test_mask'``,
- and 2 for the OOD nodes: ``'out_valid_mask'``, ``'out_test_mask'``.
This function implements 3 particular strategies for inducing distributional shifts
in graph — based on **popularity**, **locality** or **density**.
Parameters
----------
dataset : :class:`~DGLDataset` or list of :class:`~dgl.DGLGraph`
The dataset to induce structural distributional shift.
part_ratios : list
A list of 5 ratio values for training, ID validation, ID test,
OOD validation and OOD test parts. The values must sum to 1.0.
property_name : str
The name of the node property to be used, which must be
``'popularity'``, ``'locality'`` or ``'density'``.
ascending : bool, optional
Whether to sort nodes in the ascending order of the node property,
so that nodes with greater values of the property are considered
to be OOD (default: True)
random_seed : int, optional
Random seed to fix for the initial permutation of nodes. It is
used to create a random order for the nodes that have the same
property values or belong to the ID subset. (default: None)
Examples
--------
>>> dataset = dgl.data.AmazonCoBuyComputerDataset()
>>> print('in_valid_mask' in dataset[0].ndata)
False
>>> part_ratios = [0.3, 0.1, 0.1, 0.3, 0.2]
>>> property_name = 'popularity'
>>> dgl.data.utils.add_node_property_split(dataset, part_ratios, property_name)
>>> print('in_valid_mask' in dataset[0].ndata)
True
"""
assert property_name in [
"popularity",
"locality",
"density",
], "The name of property has to be 'popularity', 'locality', or 'density'"
assert len(part_ratios) == 5, "part_ratios must contain 5 values"
import networkx as nx
for idx in range(len(dataset)):
graph_dgl = dataset[idx]
graph_nx = nx.Graph(graph_dgl.to_networkx())
compute_property_fn = _property_name_to_compute_fn[property_name]
property_values = compute_property_fn(graph_nx, ascending)
node_masks = mask_nodes_by_property(
property_values, part_ratios, random_seed
)
for mask_name, node_mask in node_masks.items():
graph_dgl.ndata[mask_name] = node_mask
def _compute_popularity_property(graph_nx, ascending=True):
direction = -1 if ascending else 1
property_values = direction * np.array(list(A.pagerank(graph_nx).values()))
return property_values
def _compute_locality_property(graph_nx, ascending=True):
num_nodes = graph_nx.number_of_nodes()
pagerank_values = np.array(list(A.pagerank(graph_nx).values()))
personalization = dict(zip(range(num_nodes), [0.0] * num_nodes))
personalization[np.argmax(pagerank_values)] = 1.0
direction = -1 if ascending else 1
property_values = direction * np.array(
list(A.pagerank(graph_nx, personalization=personalization).values())
)
return property_values
def _compute_density_property(graph_nx, ascending=True):
direction = -1 if ascending else 1
property_values = direction * np.array(
list(A.clustering(graph_nx).values())
)
return property_values
_property_name_to_compute_fn = {
"popularity": _compute_popularity_property,
"locality": _compute_locality_property,
"density": _compute_density_property,
}
+173
View File
@@ -0,0 +1,173 @@
"""Wiki-CS Dataset"""
import itertools
import json
import os
import numpy as np
from .. import backend as F
from ..convert import graph
from ..transforms import reorder_graph, to_bidirected
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, generate_mask_tensor, load_graphs, save_graphs
class WikiCSDataset(DGLBuiltinDataset):
r"""Wiki-CS is a Wikipedia-based dataset for node classification from `Wiki-CS: A Wikipedia-Based
Benchmark for Graph Neural Networks <https://arxiv.org/abs/2007.02901v2>`_
The dataset consists of nodes corresponding to Computer Science articles, with edges based on
hyperlinks and 10 classes representing different branches of the field.
WikiCS dataset statistics:
- Nodes: 11,701
- Edges: 431,726 (note that the original dataset has 216,123 edges but DGL adds
the reverse edges and removes the duplicate edges, hence with a different number)
- Number of classes: 10
- Node feature size: 300
- Number of different train, validation, stopping splits: 20
- Number of test split: 1
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose : bool
Whether to print out progress information.
Default: False
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> from dgl.data import WikiCSDataset
>>> dataset = WikiCSDataset()
>>> dataset.num_classes
10
>>> g = dataset[0]
>>> # get node feature
>>> feat = g.ndata['feat']
>>> # get node labels
>>> labels = g.ndata['label']
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> stopping_mask = g.ndata['stopping_mask']
>>> test_mask = g.ndata['test_mask']
>>> # The shape of train, val and stopping masks are (num_nodes, num_splits).
>>> # The num_splits is the number of different train, validation, stopping splits.
>>> # Due to the number of test spilt is 1, the shape of test mask is (num_nodes,).
>>> print(train_mask.shape, val_mask.shape, stopping_mask.shape)
(11701, 20) (11701, 20) (11701, 20)
>>> print(test_mask.shape)
(11701,)
"""
def __init__(
self, raw_dir=None, force_reload=False, verbose=False, transform=None
):
_url = _get_dgl_url("dataset/wiki_cs.zip")
super(WikiCSDataset, self).__init__(
name="wiki_cs",
raw_dir=raw_dir,
url=_url,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""process raw data to graph, labels and masks"""
with open(os.path.join(self.raw_path, "data.json")) as f:
data = json.load(f)
features = F.tensor(np.array(data["features"]), dtype=F.float32)
labels = F.tensor(np.array(data["labels"]), dtype=F.int64)
train_masks = np.array(data["train_masks"], dtype=bool).T
val_masks = np.array(data["val_masks"], dtype=bool).T
stopping_masks = np.array(data["stopping_masks"], dtype=bool).T
test_mask = np.array(data["test_mask"], dtype=bool)
edges = [[(i, j) for j in js] for i, js in enumerate(data["links"])]
edges = np.array(list(itertools.chain(*edges)))
src, dst = edges[:, 0], edges[:, 1]
g = graph((src, dst))
g = to_bidirected(g)
g.ndata["feat"] = features
g.ndata["label"] = labels
g.ndata["train_mask"] = generate_mask_tensor(train_masks)
g.ndata["val_mask"] = generate_mask_tensor(val_masks)
g.ndata["stopping_mask"] = generate_mask_tensor(stopping_masks)
g.ndata["test_mask"] = generate_mask_tensor(test_mask)
g = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
self._graph = g
def has_cache(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
return os.path.exists(graph_path)
def save(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
save_graphs(graph_path, self._graph)
def load(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
g, _ = load_graphs(graph_path)
self._graph = g[0]
@property
def num_classes(self):
return 10
def __len__(self):
r"""The number of graphs in the dataset."""
return 1
def __getitem__(self, idx):
r"""Get graph object
Parameters
----------
idx : int
Item index, WikiCSDataset has only one graph object
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``ndata['feat']``: node features
- ``ndata['label']``: node labels
- ``ndata['train_mask']``: train mask is for retrieving the nodes for training.
- ``ndata['val_mask']``: val mask is for retrieving the nodes for hyperparameter tuning.
- ``ndata['stopping_mask']``: stopping mask is for retrieving the nodes for early stopping criterion.
- ``ndata['test_mask']``: test mask is for retrieving the nodes for testing.
"""
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
+177
View File
@@ -0,0 +1,177 @@
"""Yelp Dataset"""
import json
import os
import numpy as np
import scipy.sparse as sp
from .. import backend as F
from ..convert import from_scipy
from ..transforms import reorder_graph
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, generate_mask_tensor, load_graphs, save_graphs
class YelpDataset(DGLBuiltinDataset):
r"""Yelp dataset for node classification from `GraphSAINT: Graph Sampling Based Inductive
Learning Method <https://arxiv.org/abs/1907.04931>`_
The task of this dataset is categorizing types of businesses based on customer reviewers and
friendship.
Yelp dataset statistics:
- Nodes: 716,847
- Edges: 13,954,819
- Number of classes: 100 (Multi-class)
- Node feature size: 300
Parameters
----------
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: ~/.dgl/
force_reload : bool
Whether to reload the dataset.
Default: False
verbose : bool
Whether to print out progress information.
Default: False
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
reorder : bool
Whether to reorder the graph using :func:`~dgl.reorder_graph`.
Default: False.
Attributes
----------
num_classes : int
Number of node classes
Examples
--------
>>> dataset = YelpDataset()
>>> dataset.num_classes
100
>>> g = dataset[0]
>>> # get node feature
>>> feat = g.ndata['feat']
>>> # get node labels
>>> labels = g.ndata['label']
>>> # get data split
>>> train_mask = g.ndata['train_mask']
>>> val_mask = g.ndata['val_mask']
>>> test_mask = g.ndata['test_mask']
"""
def __init__(
self,
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
reorder=False,
):
_url = _get_dgl_url("dataset/yelp.zip")
self._reorder = reorder
super(YelpDataset, self).__init__(
name="yelp",
raw_dir=raw_dir,
url=_url,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
"""process raw data to graph, labels and masks"""
coo_adj = sp.load_npz(os.path.join(self.raw_path, "adj_full.npz"))
g = from_scipy(coo_adj)
features = np.load(os.path.join(self.raw_path, "feats.npy"))
features = F.tensor(features, dtype=F.float32)
y = [-1] * features.shape[0]
with open(os.path.join(self.raw_path, "class_map.json")) as f:
class_map = json.load(f)
for key, item in class_map.items():
y[int(key)] = item
labels = F.tensor(np.array(y), dtype=F.int64)
with open(os.path.join(self.raw_path, "role.json")) as f:
role = json.load(f)
train_mask = np.zeros(features.shape[0], dtype=bool)
train_mask[role["tr"]] = True
val_mask = np.zeros(features.shape[0], dtype=bool)
val_mask[role["va"]] = True
test_mask = np.zeros(features.shape[0], dtype=bool)
test_mask[role["te"]] = True
g.ndata["feat"] = features
g.ndata["label"] = labels
g.ndata["train_mask"] = generate_mask_tensor(train_mask)
g.ndata["val_mask"] = generate_mask_tensor(val_mask)
g.ndata["test_mask"] = generate_mask_tensor(test_mask)
if self._reorder:
self._graph = reorder_graph(
g,
node_permute_algo="rcmk",
edge_permute_algo="dst",
store_ids=False,
)
else:
self._graph = g
def has_cache(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
return os.path.exists(graph_path)
def save(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
save_graphs(graph_path, self._graph)
def load(self):
graph_path = os.path.join(self.save_path, "dgl_graph.bin")
g, _ = load_graphs(graph_path)
self._graph = g[0]
@property
def num_classes(self):
return 100
def __len__(self):
r"""The number of graphs in the dataset."""
return 1
def __getitem__(self, idx):
r"""Get graph object
Parameters
----------
idx : int
Item index, FlickrDataset has only one graph object
Returns
-------
:class:`dgl.DGLGraph`
The graph contains:
- ``ndata['label']``: node label
- ``ndata['feat']``: node feature
- ``ndata['train_mask']``: mask for training node set
- ``ndata['val_mask']``: mask for validation node set
- ``ndata['test_mask']``: mask for test node set
"""
assert idx == 0, "This dataset has only one graph"
if self._transform is None:
return self._graph
else:
return self._transform(self._graph)
+137
View File
@@ -0,0 +1,137 @@
import os
from .dgl_dataset import DGLBuiltinDataset
from .utils import _get_dgl_url, load_graphs
class ZINCDataset(DGLBuiltinDataset):
r"""ZINC dataset for the graph regression task.
A subset (12K) of ZINC molecular graphs (250K) dataset is used to
regress a molecular property known as the constrained solubility.
For each molecular graph, the node features are the types of heavy
atoms, between which the edge features are the types of bonds.
Each graph contains 9-37 nodes and 16-84 edges.
Reference `<https://arxiv.org/pdf/2003.00982.pdf>`_
Statistics:
Train examples: 10,000
Valid examples: 1,000
Test examples: 1,000
Average number of nodes: 23.16
Average number of edges: 39.83
Number of atom types: 28
Number of bond types: 4
Parameters
----------
mode : str, optional
Should be chosen from ["train", "valid", "test"]
Default: "train".
raw_dir : str
Raw file directory to download/contains the input data directory.
Default: "~/.dgl/".
force_reload : bool
Whether to reload the dataset.
Default: False.
verbose : bool
Whether to print out progress information.
Default: False.
transform : callable, optional
A transform that takes in a :class:`~dgl.DGLGraph` object and returns
a transformed version. The :class:`~dgl.DGLGraph` object will be
transformed before every access.
Attributes
----------
num_atom_types : int
Number of atom types.
num_bond_types : int
Number of bond types.
Examples
---------
>>> from dgl.data import ZINCDataset
>>> training_set = ZINCDataset(mode="train")
>>> training_set.num_atom_types
28
>>> len(training_set)
10000
>>> graph, label = training_set[0]
>>> graph
Graph(num_nodes=29, num_edges=64,
ndata_schemes={'feat': Scheme(shape=(), dtype=torch.int64)}
edata_schemes={'feat': Scheme(shape=(), dtype=torch.int64)})
"""
def __init__(
self,
mode="train",
raw_dir=None,
force_reload=False,
verbose=False,
transform=None,
):
self._url = _get_dgl_url("dataset/ZINC12k.zip")
self.mode = mode
super(ZINCDataset, self).__init__(
name="zinc",
url=self._url,
raw_dir=raw_dir,
force_reload=force_reload,
verbose=verbose,
transform=transform,
)
def process(self):
self.load()
@property
def graph_path(self):
return os.path.join(self.save_path, "ZincDGL_{}.bin".format(self.mode))
def has_cache(self):
return os.path.exists(self.graph_path)
def load(self):
self._graphs, self._labels = load_graphs(self.graph_path)
@property
def num_atom_types(self):
return 28
@property
def num_bond_types(self):
return 4
def __len__(self):
return len(self._graphs)
def __getitem__(self, idx):
r"""Get one example by index.
Parameters
----------
idx : int
The sample index.
Returns
-------
dgl.DGLGraph
Each graph contains:
- ``ndata['feat']``: Types of heavy atoms as node features
- ``edata['feat']``: Types of bonds as edge features
Tensor
Constrained solubility as graph label
"""
labels = self._labels["g_label"]
if self._transform is None:
return self._graphs[idx], labels[idx]
else:
return self._transform(self._graphs[idx]), labels[idx]
+14
View File
@@ -0,0 +1,14 @@
"""Package for dataloaders and samplers."""
from .. import backend as F
from . import negative_sampler
from .base import *
from .cluster_gcn import *
from .graphsaint import *
from .labor_sampler import *
from .neighbor_sampler import *
from .shadow import *
if F.get_preferred_backend() == "pytorch":
from .spot_target import *
from .dataloader import *
+658
View File
@@ -0,0 +1,658 @@
"""Base classes and functionalities for dataloaders"""
import inspect
from collections.abc import Mapping
from .. import backend as F
from ..base import EID, NID
from ..convert import heterograph
from ..frame import LazyFeature
from ..transforms import compact_graphs
from ..utils import context_of, recursive_apply
def _set_lazy_features(x, xdata, feature_names):
if feature_names is None:
return
if not isinstance(feature_names, Mapping):
xdata.update({k: LazyFeature(k) for k in feature_names})
else:
for type_, names in feature_names.items():
x[type_].data.update({k: LazyFeature(k) for k in names})
def set_node_lazy_features(g, feature_names):
"""Assign lazy features to the ``ndata`` of the input graph for prefetching optimization.
When used in a :class:`~dgl.dataloading.Sampler`, lazy features mark which data
should be fetched before computation in model. See :ref:`guide-minibatch-prefetching`
for a detailed explanation.
If the graph is homogeneous, this is equivalent to:
.. code:: python
g.ndata.update({k: LazyFeature(k, g.ndata[dgl.NID]) for k in feature_names})
If the graph is heterogeneous, this is equivalent to:
.. code:: python
for type_, names in feature_names.items():
g.nodes[type_].data.update(
{k: LazyFeature(k, g.nodes[type_].data[dgl.NID]) for k in names})
Parameters
----------
g : DGLGraph
The graph.
feature_names : list[str] or dict[str, list[str]]
The feature names to prefetch.
See also
--------
dgl.LazyFeature
"""
return _set_lazy_features(g.nodes, g.ndata, feature_names)
def set_edge_lazy_features(g, feature_names):
"""Assign lazy features to the ``edata`` of the input graph for prefetching optimization.
When used in a :class:`~dgl.dataloading.Sampler`, lazy features mark which data
should be fetched before computation in model. See :ref:`guide-minibatch-prefetching`
for a detailed explanation.
If the graph is homogeneous, this is equivalent to:
.. code:: python
g.edata.update({k: LazyFeature(k, g.edata[dgl.EID]) for k in feature_names})
If the graph is heterogeneous, this is equivalent to:
.. code:: python
for type_, names in feature_names.items():
g.edges[type_].data.update(
{k: LazyFeature(k, g.edges[type_].data[dgl.EID]) for k in names})
Parameters
----------
g : DGLGraph
The graph.
feature_names : list[str] or dict[etype, list[str]]
The feature names to prefetch. The ``etype`` key is either a string
or a triplet.
See also
--------
dgl.LazyFeature
"""
return _set_lazy_features(g.edges, g.edata, feature_names)
def set_src_lazy_features(g, feature_names):
"""Assign lazy features to the ``srcdata`` of the input graph for prefetching optimization.
When used in a :class:`~dgl.dataloading.Sampler`, lazy features mark which data
should be fetched before computation in model. See :ref:`guide-minibatch-prefetching`
for a detailed explanation.
If the graph is homogeneous, this is equivalent to:
.. code:: python
g.srcdata.update({k: LazyFeature(k, g.srcdata[dgl.NID]) for k in feature_names})
If the graph is heterogeneous, this is equivalent to:
.. code:: python
for type_, names in feature_names.items():
g.srcnodes[type_].data.update(
{k: LazyFeature(k, g.srcnodes[type_].data[dgl.NID]) for k in names})
Parameters
----------
g : DGLGraph
The graph.
feature_names : list[str] or dict[str, list[str]]
The feature names to prefetch.
See also
--------
dgl.LazyFeature
"""
return _set_lazy_features(g.srcnodes, g.srcdata, feature_names)
def set_dst_lazy_features(g, feature_names):
"""Assign lazy features to the ``dstdata`` of the input graph for prefetching optimization.
When used in a :class:`~dgl.dataloading.Sampler`, lazy features mark which data
should be fetched before computation in model. See :ref:`guide-minibatch-prefetching`
for a detailed explanation.
If the graph is homogeneous, this is equivalent to:
.. code:: python
g.dstdata.update({k: LazyFeature(k, g.dstdata[dgl.NID]) for k in feature_names})
If the graph is heterogeneous, this is equivalent to:
.. code:: python
for type_, names in feature_names.items():
g.dstnodes[type_].data.update(
{k: LazyFeature(k, g.dstnodes[type_].data[dgl.NID]) for k in names})
Parameters
----------
g : DGLGraph
The graph.
feature_names : list[str] or dict[str, list[str]]
The feature names to prefetch.
See also
--------
dgl.LazyFeature
"""
return _set_lazy_features(g.dstnodes, g.dstdata, feature_names)
class Sampler(object):
"""Base class for graph samplers.
All graph samplers must subclass this class and override the ``sample``
method.
.. code:: python
from dgl.dataloading import Sampler
class SubgraphSampler(Sampler):
def __init__(self):
super().__init__()
def sample(self, g, indices):
return g.subgraph(indices)
"""
def sample(self, g, indices):
"""Abstract sample method.
Parameters
----------
g : DGLGraph
The graph.
indices : object
Any object representing the indices selected in the current minibatch.
"""
raise NotImplementedError
class BlockSampler(Sampler):
"""Base class for sampling mini-batches in the form of Message-passing
Flow Graphs (MFGs).
It provides prefetching options to fetch the node features for the first MFG's ``srcdata``,
the node labels for the last MFG's ``dstdata`` and the edge features of all MFG's ``edata``.
Parameters
----------
prefetch_node_feats : list[str] or dict[str, list[str]], optional
The node data to prefetch for the first MFG.
DGL will populate the first layer's MFG's ``srcnodes`` and ``srcdata`` with
the node data of the given names from the original graph.
prefetch_labels : list[str] or dict[str, list[str]], optional
The node data to prefetch for the last MFG.
DGL will populate the last layer's MFG's ``dstnodes`` and ``dstdata`` with
the node data of the given names from the original graph.
prefetch_edge_feats : list[str] or dict[etype, list[str]], optional
The edge data names to prefetch for all the MFGs.
DGL will populate every MFG's ``edges`` and ``edata`` with the edge data
of the given names from the original graph.
output_device : device, optional
The device of the output subgraphs or MFGs. Default is the same as the
minibatch of seed nodes.
"""
def __init__(
self,
prefetch_node_feats=None,
prefetch_labels=None,
prefetch_edge_feats=None,
output_device=None,
):
super().__init__()
self.prefetch_node_feats = prefetch_node_feats or []
self.prefetch_labels = prefetch_labels or []
self.prefetch_edge_feats = prefetch_edge_feats or []
self.output_device = output_device
def sample_blocks(self, g, seed_nodes, exclude_eids=None):
"""Generates a list of blocks from the given seed nodes.
This function must return a triplet where the first element is the input node IDs
for the first GNN layer (a tensor or a dict of tensors for heterogeneous graphs),
the second element is the output node IDs for the last GNN layer, and the third
element is the said list of blocks.
"""
raise NotImplementedError
def assign_lazy_features(self, result):
"""Assign lazy features for prefetching."""
input_nodes, output_nodes, blocks = result
set_src_lazy_features(blocks[0], self.prefetch_node_feats)
set_dst_lazy_features(blocks[-1], self.prefetch_labels)
for block in blocks:
set_edge_lazy_features(block, self.prefetch_edge_feats)
return input_nodes, output_nodes, blocks
def sample(
self, g, seed_nodes, exclude_eids=None
): # pylint: disable=arguments-differ
"""Sample a list of blocks from the given seed nodes."""
result = self.sample_blocks(g, seed_nodes, exclude_eids=exclude_eids)
return self.assign_lazy_features(result)
def _find_exclude_eids_with_reverse_id(g, eids, reverse_eid_map):
if isinstance(eids, Mapping):
eids = {g.to_canonical_etype(k): v for k, v in eids.items()}
exclude_eids = {
k: F.cat([v, F.gather_row(reverse_eid_map[k], v)], 0)
for k, v in eids.items()
}
else:
exclude_eids = F.cat([eids, F.gather_row(reverse_eid_map, eids)], 0)
return exclude_eids
def _find_exclude_eids_with_reverse_types(g, eids, reverse_etype_map):
exclude_eids = {g.to_canonical_etype(k): v for k, v in eids.items()}
reverse_etype_map = {
g.to_canonical_etype(k): g.to_canonical_etype(v)
for k, v in reverse_etype_map.items()
}
for k, v in reverse_etype_map.items():
if k in exclude_eids:
if v in exclude_eids:
exclude_eids[v] = F.unique(
F.cat((exclude_eids[k], exclude_eids[v]), dim=0)
)
else:
exclude_eids[v] = exclude_eids[k]
return exclude_eids
def _find_exclude_eids(g, exclude_mode, eids, **kwargs):
if exclude_mode is None:
return None
elif callable(exclude_mode):
return exclude_mode(eids)
elif F.is_tensor(exclude_mode) or (
isinstance(exclude_mode, Mapping)
and all(F.is_tensor(v) for v in exclude_mode.values())
):
return exclude_mode
elif exclude_mode == "self":
return eids
elif exclude_mode == "reverse_id":
return _find_exclude_eids_with_reverse_id(
g, eids, kwargs["reverse_eid_map"]
)
elif exclude_mode == "reverse_types":
return _find_exclude_eids_with_reverse_types(
g, eids, kwargs["reverse_etype_map"]
)
else:
raise ValueError("unsupported mode {}".format(exclude_mode))
def find_exclude_eids(
g,
seed_edges,
exclude,
reverse_eids=None,
reverse_etypes=None,
output_device=None,
):
"""Find all edge IDs to exclude according to :attr:`exclude_mode`.
Parameters
----------
g : DGLGraph
The graph.
exclude :
Can be either of the following,
None (default)
Does not exclude any edge.
'self'
Exclude the given edges themselves but nothing else.
'reverse_id'
Exclude all edges specified in ``eids``, as well as their reverse edges
of the same edge type.
The mapping from each edge ID to its reverse edge ID is specified in
the keyword argument ``reverse_eid_map``.
This mode assumes that the reverse of an edge with ID ``e`` and type
``etype`` will have ID ``reverse_eid_map[e]`` and type ``etype``.
'reverse_types'
Exclude all edges specified in ``eids``, as well as their reverse
edges of the corresponding edge types.
The mapping from each edge type to its reverse edge type is specified
in the keyword argument ``reverse_etype_map``.
This mode assumes that the reverse of an edge with ID ``e`` and type ``etype``
will have ID ``e`` and type ``reverse_etype_map[etype]``.
callable
Any function that takes in a single argument :attr:`seed_edges` and returns
a tensor or dict of tensors.
eids : Tensor or dict[etype, Tensor]
The edge IDs.
reverse_eids : Tensor or dict[etype, Tensor]
The mapping from edge ID to its reverse edge ID.
reverse_etypes : dict[etype, etype]
The mapping from edge etype to its reverse edge type.
output_device : device
The device of the output edge IDs.
"""
exclude_eids = _find_exclude_eids(
g,
exclude,
seed_edges,
reverse_eid_map=reverse_eids,
reverse_etype_map=reverse_etypes,
)
if exclude_eids is not None and output_device is not None:
exclude_eids = recursive_apply(
exclude_eids, lambda x: F.copy_to(x, output_device)
)
return exclude_eids
class EdgePredictionSampler(Sampler):
"""Sampler class that wraps an existing sampler for node classification into another
one for edge classification or link prediction.
See also
--------
as_edge_prediction_sampler
"""
def __init__(
self,
sampler,
exclude=None,
reverse_eids=None,
reverse_etypes=None,
negative_sampler=None,
prefetch_labels=None,
):
super().__init__()
# Check if the sampler's sample method has an optional third argument.
argspec = inspect.getfullargspec(sampler.sample)
if len(argspec.args) < 4: # ['self', 'g', 'indices', 'exclude_eids']
raise TypeError(
"This sampler does not support edge or link prediction; please add an"
"optional third argument for edge IDs to exclude in its sample() method."
)
self.reverse_eids = reverse_eids
self.reverse_etypes = reverse_etypes
self.exclude = exclude
self.sampler = sampler
self.negative_sampler = negative_sampler
self.prefetch_labels = prefetch_labels or []
self.output_device = sampler.output_device
def _build_neg_graph(self, g, seed_edges):
neg_srcdst = self.negative_sampler(g, seed_edges)
if not isinstance(neg_srcdst, Mapping):
assert len(g.canonical_etypes) == 1, (
"graph has multiple or no edge types; "
"please return a dict in negative sampler."
)
neg_srcdst = {g.canonical_etypes[0]: neg_srcdst}
dtype = F.dtype(list(neg_srcdst.values())[0][0])
ctx = context_of(seed_edges) if seed_edges is not None else g.device
neg_edges = {
etype: neg_srcdst.get(
etype,
(
F.copy_to(F.tensor([], dtype), ctx=ctx),
F.copy_to(F.tensor([], dtype), ctx=ctx),
),
)
for etype in g.canonical_etypes
}
neg_pair_graph = heterograph(
neg_edges, {ntype: g.num_nodes(ntype) for ntype in g.ntypes}
)
return neg_pair_graph
def assign_lazy_features(self, result):
"""Assign lazy features for prefetching."""
pair_graph = result[1]
set_edge_lazy_features(pair_graph, self.prefetch_labels)
# In-place updates
return result
def sample(self, g, seed_edges): # pylint: disable=arguments-differ
"""Samples a list of blocks, as well as a subgraph containing the sampled
edges from the original graph.
If :attr:`negative_sampler` is given, also returns another graph containing the
negative pairs as edges.
"""
if isinstance(seed_edges, Mapping):
seed_edges = {
g.to_canonical_etype(k): v for k, v in seed_edges.items()
}
exclude = self.exclude
pair_graph = g.edge_subgraph(
seed_edges, relabel_nodes=False, output_device=self.output_device
)
eids = pair_graph.edata[EID]
if self.negative_sampler is not None:
neg_graph = self._build_neg_graph(g, seed_edges)
pair_graph, neg_graph = compact_graphs([pair_graph, neg_graph])
else:
pair_graph = compact_graphs(pair_graph)
pair_graph.edata[EID] = eids
seed_nodes = pair_graph.ndata[NID]
exclude_eids = find_exclude_eids(
g,
seed_edges,
exclude,
self.reverse_eids,
self.reverse_etypes,
self.output_device,
)
input_nodes, _, blocks = self.sampler.sample(
g, seed_nodes, exclude_eids
)
if self.negative_sampler is None:
return self.assign_lazy_features((input_nodes, pair_graph, blocks))
else:
return self.assign_lazy_features(
(input_nodes, pair_graph, neg_graph, blocks)
)
def as_edge_prediction_sampler(
sampler,
exclude=None,
reverse_eids=None,
reverse_etypes=None,
negative_sampler=None,
prefetch_labels=None,
):
"""Create an edge-wise sampler from a node-wise sampler.
For each batch of edges, the sampler applies the provided node-wise sampler to
their source and destination nodes to extract subgraphs. It also generates negative
edges if a negative sampler is provided, and extract subgraphs for their incident
nodes as well.
For each iteration, the sampler will yield
* A tensor of input nodes necessary for computing the representation on edges, or
a dictionary of node type names and such tensors.
* A subgraph that contains only the edges in the minibatch and their incident nodes.
Note that the graph has an identical metagraph with the original graph.
* If a negative sampler is given, another graph that contains the "negative edges",
connecting the source and destination nodes yielded from the given negative sampler.
* The subgraphs or MFGs returned by the provided node-wise sampler, generated
from the incident nodes of the edges in the minibatch (as well as those of the
negative edges if applicable).
Parameters
----------
sampler : Sampler
The node-wise sampler object. It additionally requires that the :attr:`sample`
method must have an optional third argument :attr:`exclude_eids` representing the
edge IDs to exclude from neighborhood. The argument will be either a tensor
for homogeneous graphs or a dict of edge types and tensors for heterogeneous
graphs.
exclude : Union[str, callable], optional
Whether and how to exclude dependencies related to the sampled edges in the
minibatch. Possible values are
* None, for not excluding any edges.
* ``self``, for excluding the edges in the current minibatch.
* ``reverse_id``, for excluding not only the edges in the current minibatch but
also their reverse edges according to the ID mapping in the argument
:attr:`reverse_eids`.
* ``reverse_types``, for excluding not only the edges in the current minibatch
but also their reverse edges stored in another type according to
the argument :attr:`reverse_etypes`.
* User-defined exclusion rule. It is a callable with edges in the current
minibatch as a single argument and should return the edges to be excluded.
reverse_eids : Tensor or dict[etype, Tensor], optional
A tensor of reverse edge ID mapping. The i-th element indicates the ID of
the i-th edge's reverse edge.
If the graph is heterogeneous, this argument requires a dictionary of edge
types and the reverse edge ID mapping tensors.
reverse_etypes : dict[etype, etype], optional
The mapping from the original edge types to their reverse edge types.
negative_sampler : callable, optional
The negative sampler.
prefetch_labels : list[str] or dict[etype, list[str]], optional
The edge labels to prefetch for the returned positive pair graph.
See :ref:`guide-minibatch-prefetching` for a detailed explanation of prefetching.
Examples
--------
The following example shows how to train a 3-layer GNN for edge classification on a
set of edges ``train_eid`` on a homogeneous undirected graph. Each node takes
messages from all neighbors.
Given an array of source node IDs ``src`` and another array of destination
node IDs ``dst``, the following code creates a bidirectional graph:
>>> g = dgl.graph((torch.cat([src, dst]), torch.cat([dst, src])))
Edge :math:`i`'s reverse edge in the graph above is edge :math:`i + |E|`. Therefore, we can
create a reverse edge mapping ``reverse_eids`` by:
>>> E = len(src)
>>> reverse_eids = torch.cat([torch.arange(E, 2 * E), torch.arange(0, E)])
By passing ``reverse_eids`` to the edge sampler, the edges in the current mini-batch and their
reversed edges will be excluded from the extracted subgraphs to avoid information leakage.
>>> sampler = dgl.dataloading.as_edge_prediction_sampler(
... dgl.dataloading.NeighborSampler([15, 10, 5]),
... exclude='reverse_id', reverse_eids=reverse_eids)
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_eid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, pair_graph, blocks in dataloader:
... train_on(input_nodes, pair_graph, blocks)
For link prediction, one can provide a negative sampler to sample negative edges.
The code below uses DGL's :class:`~dgl.dataloading.negative_sampler.Uniform`
to generate 5 negative samples per edge:
>>> neg_sampler = dgl.dataloading.negative_sampler.Uniform(5)
>>> sampler = dgl.dataloading.as_edge_prediction_sampler(
... dgl.dataloading.NeighborSampler([15, 10, 5]),
... sampler, exclude='reverse_id', reverse_eids=reverse_eids,
... negative_sampler=neg_sampler)
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_eid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, pos_pair_graph, neg_pair_graph, blocks in dataloader:
... train_on(input_nodes, pair_graph, neg_pair_graph, blocks)
For heterogeneous graphs, reverse edges may belong to a different relation. For example,
the relations "user-click-item" and "item-click-by-user" in the graph below are
mutual reverse.
>>> g = dgl.heterograph({
... ('user', 'click', 'item'): (user, item),
... ('item', 'clicked-by', 'user'): (item, user)})
To correctly exclude edges from each mini-batch, set ``exclude='reverse_types'`` and
pass a dictionary ``{'click': 'clicked-by', 'clicked-by': 'click'}`` to the
``reverse_etypes`` argument.
>>> sampler = dgl.dataloading.as_edge_prediction_sampler(
... dgl.dataloading.NeighborSampler([15, 10, 5]),
... exclude='reverse_types',
... reverse_etypes={'click': 'clicked-by', 'clicked-by': 'click'})
>>> dataloader = dgl.dataloading.DataLoader(
... g, {'click': train_eid}, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, pair_graph, blocks in dataloader:
... train_on(input_nodes, pair_graph, blocks)
For link prediction, provide a negative sampler to generate negative samples:
>>> neg_sampler = dgl.dataloading.negative_sampler.Uniform(5)
>>> sampler = dgl.dataloading.as_edge_prediction_sampler(
... dgl.dataloading.NeighborSampler([15, 10, 5]),
... exclude='reverse_types',
... reverse_etypes={'click': 'clicked-by', 'clicked-by': 'click'},
... negative_sampler=neg_sampler)
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_eid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, pos_pair_graph, neg_pair_graph, blocks in dataloader:
... train_on(input_nodes, pair_graph, neg_pair_graph, blocks)
"""
return EdgePredictionSampler(
sampler,
exclude=exclude,
reverse_eids=reverse_eids,
reverse_etypes=reverse_etypes,
negative_sampler=negative_sampler,
prefetch_labels=prefetch_labels,
)
@@ -0,0 +1,190 @@
"""Capped neighbor sampler."""
from collections import defaultdict
import numpy as np
import torch
from ..sampling.utils import EidExcluder
from .base import Sampler, set_edge_lazy_features, set_node_lazy_features
class CappedNeighborSampler(Sampler):
"""Subgraph sampler that sets an upper bound on the number of nodes included in
each layer of the sampled subgraph. At each layer, the frontier is randomly
subsampled. Rare node types can also be upsampled by taking the scaled square
root of the sampling probabilities. The sampler returns the subgraph induced by
all the sampled nodes.
This code was contributed by a community member
([@ayushnoori](https://github.com/ayushnoori)). There aren't currently any unit
tests in place to verify its functionality, so please be cautious if you need
to make any changes to the code's logic.
Parameters
----------
fanouts : list[int] or dict[etype, int]
List of neighbors to sample per edge type for each GNN layer, with the i-th
element being the fanout for the i-th GNN layer.
- If only a single integer is provided, DGL assumes that every edge type
will have the same fanout.
- If -1 is provided for one edge type on one layer, then all inbound edges
of that edge type will be included.
fixed_k : int
The number of nodes to sample for each GNN layer.
upsample_rare_types : bool
Whether or not to upsample rare node types.
replace : bool, default True
Whether to sample with replacement.
prob : str, optional
If given, the probability of each neighbor being sampled is proportional
to the edge feature value with the given name in ``g.edata``. The feature must be
a scalar on each edge.
"""
def __init__(
self,
fanouts,
fixed_k,
upsample_rare_types,
replace=False,
prob=None,
prefetch_node_feats=None,
prefetch_edge_feats=None,
output_device=None,
):
super().__init__()
self.fanouts = fanouts
self.replace = replace
self.fixed_k = fixed_k
self.upsample_rare_types = upsample_rare_types
self.prob = prob
self.prefetch_node_feats = prefetch_node_feats
self.prefetch_edge_feats = prefetch_edge_feats
self.output_device = output_device
def sample(
self, g, indices, exclude_eids=None
): # pylint: disable=arguments-differ
"""Sampling function.
Parameters
----------
g : DGLGraph
The graph to sample from.
indices : Tensor or dict[str, Tensor]
Nodes which induce the subgraph.
exclude_eids : Tensor or dict[etype, Tensor], optional
The edges to exclude from the sampled subgraph.
Returns
-------
input_nodes : Tensor or dict[str, Tensor]
The node IDs inducing the subgraph.
output_nodes : Tensor or dict[str, Tensor]
The node IDs that are sampled in this minibatch.
subg : DGLGraph
The subgraph itself.
"""
# Define empty dictionary to store reached nodes.
output_nodes = indices
all_reached_nodes = [indices]
# Iterate over fanout.
for fanout in reversed(self.fanouts):
# Sample frontier.
frontier = g.sample_neighbors(
indices,
fanout,
output_device=self.output_device,
replace=self.replace,
prob=self.prob,
exclude_edges=exclude_eids,
)
# Get reached nodes.
curr_reached = defaultdict(list)
for c_etype in frontier.canonical_etypes:
(src_type, _, _) = c_etype
src, _ = frontier.edges(etype=c_etype)
curr_reached[src_type].append(src)
# De-duplication.
curr_reached = {
ntype: torch.unique(torch.cat(srcs))
for ntype, srcs in curr_reached.items()
}
# Generate type sampling probabilties.
type_count = {
node_type: indices.shape[0]
for node_type, indices in curr_reached.items()
}
total_count = sum(type_count.values())
probs = {
node_type: count / total_count
for node_type, count in type_count.items()
}
# Upsample rare node types.
if self.upsample_rare_types:
# Take scaled square root of probabilities.
prob_dist = list(probs.values())
prob_dist = np.sqrt(prob_dist)
prob_dist = prob_dist / prob_dist.sum()
# Update probabilities.
probs = {
node_type: prob_dist[i]
for i, node_type in enumerate(probs.keys())
}
# Generate node counts per type.
n_per_type = {
node_type: int(self.fixed_k * prob)
for node_type, prob in probs.items()
}
remainder = self.fixed_k - sum(n_per_type.values())
for _ in range(remainder):
node_type = np.random.choice(
list(probs.keys()), p=list(probs.values())
)
n_per_type[node_type] += 1
# Downsample nodes.
curr_reached_k = {}
for node_type, node_ids in curr_reached.items():
# Get number of total nodes and number to sample.
num_nodes = node_ids.shape[0]
n_to_sample = min(num_nodes, n_per_type[node_type])
# Downsample nodes of current type.
random_indices = torch.randperm(num_nodes)[:n_to_sample]
curr_reached_k[node_type] = node_ids[random_indices]
# Update seed nodes.
indices = curr_reached_k
all_reached_nodes.append(curr_reached_k)
# Merge all reached nodes before sending to `DGLGraph.subgraph`.
merged_nodes = {}
for ntype in g.ntypes:
merged_nodes[ntype] = torch.unique(
torch.cat(
[reached.get(ntype, []) for reached in all_reached_nodes]
)
)
subg = g.subgraph(
merged_nodes, relabel_nodes=True, output_device=self.output_device
)
if exclude_eids is not None:
subg = EidExcluder(exclude_eids)(subg)
set_node_lazy_features(subg, self.prefetch_node_feats)
set_edge_lazy_features(subg, self.prefetch_edge_feats)
return indices, output_nodes, subg
+155
View File
@@ -0,0 +1,155 @@
"""Cluster-GCN samplers."""
import os
import pickle
import numpy as np
from .. import backend as F
from ..base import DGLError
from ..partition import metis_partition_assignment
from .base import Sampler, set_edge_lazy_features, set_node_lazy_features
class ClusterGCNSampler(Sampler):
"""Cluster sampler from `Cluster-GCN: An Efficient Algorithm for Training
Deep and Large Graph Convolutional Networks
<https://arxiv.org/abs/1905.07953>`__
This sampler first partitions the graph with METIS partitioning, then it caches the nodes of
each partition to a file within the given cache directory.
The sampler then selects the graph partitions according to the provided
partition IDs, take the union of all nodes in those partitions, and return an
induced subgraph in its :attr:`sample` method.
Parameters
----------
g : DGLGraph
The original graph. Must be homogeneous and on CPU.
k : int
The number of partitions.
cache_path : str
The path to the cache directory for storing the partition result.
balance_ntypes, balkance_edges, mode :
Passed to :func:`dgl.metis_partition_assignment`.
prefetch_ndata : list[str], optional
The node data to prefetch for the subgraph.
See :ref:`guide-minibatch-prefetching` for a detailed explanation of prefetching.
prefetch_edata : list[str], optional
The edge data to prefetch for the subgraph.
See :ref:`guide-minibatch-prefetching` for a detailed explanation of prefetching.
output_device : device, optional
The device of the output subgraphs or MFGs. Default is the same as the
minibatch of partition indices.
Examples
--------
**Node classification**
With this sampler, the data loader will accept the list of partition IDs as
indices to iterate over. For instance, the following code first splits the
graph into 1000 partitions using METIS, and at each iteration it gets a subgraph
induced by the nodes covered by 20 randomly selected partitions.
>>> num_parts = 1000
>>> sampler = dgl.dataloading.ClusterGCNSampler(g, num_parts)
>>> dataloader = dgl.dataloading.DataLoader(
... g, torch.arange(num_parts), sampler,
... batch_size=20, shuffle=True, drop_last=False, num_workers=4)
>>> for subg in dataloader:
... train_on(subg)
"""
def __init__(
self,
g,
k,
cache_path="cluster_gcn.pkl",
balance_ntypes=None,
balance_edges=False,
mode="k-way",
prefetch_ndata=None,
prefetch_edata=None,
output_device=None,
):
super().__init__()
if os.path.exists(cache_path):
try:
with open(cache_path, "rb") as f:
(
self.partition_offset,
self.partition_node_ids,
) = pickle.load(f)
except (EOFError, TypeError, ValueError):
raise DGLError(
f"The contents in the cache file {cache_path} is invalid. "
f"Please remove the cache file {cache_path} or specify another path."
)
if len(self.partition_offset) != k + 1:
raise DGLError(
f"Number of partitions in the cache does not match the value of k. "
f"Please remove the cache file {cache_path} or specify another path."
)
if len(self.partition_node_ids) != g.num_nodes():
raise DGLError(
f"Number of nodes in the cache does not match the given graph. "
f"Please remove the cache file {cache_path} or specify another path."
)
else:
partition_ids = metis_partition_assignment(
g,
k,
balance_ntypes=balance_ntypes,
balance_edges=balance_edges,
mode=mode,
)
partition_ids = F.asnumpy(partition_ids)
partition_node_ids = np.argsort(partition_ids)
partition_size = F.zerocopy_from_numpy(
np.bincount(partition_ids, minlength=k)
)
partition_offset = F.zerocopy_from_numpy(
np.insert(np.cumsum(partition_size), 0, 0)
)
partition_node_ids = F.zerocopy_from_numpy(partition_node_ids)
with open(cache_path, "wb") as f:
pickle.dump((partition_offset, partition_node_ids), f)
self.partition_offset = partition_offset
self.partition_node_ids = partition_node_ids
self.prefetch_ndata = prefetch_ndata or []
self.prefetch_edata = prefetch_edata or []
self.output_device = output_device
def sample(self, g, partition_ids): # pylint: disable=arguments-differ
"""Sampling function.
Parameters
----------
g : DGLGraph
The graph to sample from.
partition_ids : Tensor
A 1-D integer tensor of partition IDs.
Returns
-------
DGLGraph
The sampled subgraph.
"""
node_ids = F.cat(
[
self.partition_node_ids[
self.partition_offset[i] : self.partition_offset[i + 1]
]
for i in F.asnumpy(partition_ids)
],
0,
)
sg = g.subgraph(
node_ids, relabel_nodes=True, output_device=self.output_device
)
set_node_lazy_features(sg, self.prefetch_ndata)
set_edge_lazy_features(sg, self.prefetch_edata)
return sg
File diff suppressed because it is too large Load Diff
+163
View File
@@ -0,0 +1,163 @@
"""GraphSAINT samplers."""
from ..base import DGLError
from ..random import choice
from ..sampling import pack_traces, random_walk
from .base import Sampler, set_edge_lazy_features, set_node_lazy_features
try:
import torch
except ImportError:
pass
class SAINTSampler(Sampler):
"""Random node/edge/walk sampler from
`GraphSAINT: Graph Sampling Based Inductive Learning Method
<https://arxiv.org/abs/1907.04931>`__
For each call, the sampler samples a node subset and then returns a node induced subgraph.
There are three options for sampling node subsets:
- For :attr:`'node'` sampler, the probability to sample a node is in proportion
to its out-degree.
- The :attr:`'edge'` sampler first samples an edge subset and then use the
end nodes of the edges.
- The :attr:`'walk'` sampler uses the nodes visited by random walks. It uniformly selects
a number of root nodes and then performs a fixed-length random walk from each root node.
Parameters
----------
mode : str
The sampler to use, which can be :attr:`'node'`, :attr:`'edge'`, or :attr:`'walk'`.
budget : int or tuple[int]
Sampler configuration.
- For :attr:`'node'` sampler, budget specifies the number of nodes
in each sampled subgraph.
- For :attr:`'edge'` sampler, budget specifies the number of edges
to sample for inducing a subgraph.
- For :attr:`'walk'` sampler, budget is a tuple. budget[0] specifies
the number of root nodes to generate random walks. budget[1] specifies
the length of a random walk.
cache : bool, optional
If False, it will not cache the probability arrays for sampling. Setting
it to False is required if you want to use the sampler across different graphs.
prefetch_ndata : list[str], optional
The node data to prefetch for the subgraph.
See :ref:`guide-minibatch-prefetching` for a detailed explanation of prefetching.
prefetch_edata : list[str], optional
The edge data to prefetch for the subgraph.
See :ref:`guide-minibatch-prefetching` for a detailed explanation of prefetching.
output_device : device, optional
The device of the output subgraphs.
Examples
--------
>>> import torch
>>> from dgl.dataloading import SAINTSampler, DataLoader
>>> num_iters = 1000
>>> sampler = SAINTSampler(mode='node', budget=6000)
>>> # Assume g.ndata['feat'] and g.ndata['label'] hold node features and labels
>>> dataloader = DataLoader(g, torch.arange(num_iters), sampler, num_workers=4)
>>> for subg in dataloader:
... train_on(subg)
"""
def __init__(
self,
mode,
budget,
cache=True,
prefetch_ndata=None,
prefetch_edata=None,
output_device="cpu",
):
super().__init__()
self.budget = budget
if mode == "node":
self.sampler = self.node_sampler
elif mode == "edge":
self.sampler = self.edge_sampler
elif mode == "walk":
self.sampler = self.walk_sampler
else:
raise DGLError(
f"Expect mode to be 'node', 'edge' or 'walk', got {mode}."
)
self.cache = cache
self.prob = None
self.prefetch_ndata = prefetch_ndata or []
self.prefetch_edata = prefetch_edata or []
self.output_device = output_device
def node_sampler(self, g):
"""Node ID sampler for random node sampler"""
# Alternatively, this can be realized by uniformly sampling an edge subset,
# and then take the src node of the sampled edges. However, the number of edges
# is typically much larger than the number of nodes.
if self.cache and self.prob is not None:
prob = self.prob
else:
prob = g.out_degrees().float().clamp(min=1)
if self.cache:
self.prob = prob
return (
torch.multinomial(prob, num_samples=self.budget, replacement=True)
.unique()
.type(g.idtype)
)
def edge_sampler(self, g):
"""Node ID sampler for random edge sampler"""
src, dst = g.edges()
if self.cache and self.prob is not None:
prob = self.prob
else:
in_deg = g.in_degrees().float().clamp(min=1)
out_deg = g.out_degrees().float().clamp(min=1)
# We can reduce the sample space by half if graphs are always symmetric.
prob = 1.0 / in_deg[dst.long()] + 1.0 / out_deg[src.long()]
prob /= prob.sum()
if self.cache:
self.prob = prob
sampled_edges = torch.unique(
choice(len(prob), size=self.budget, prob=prob)
)
sampled_nodes = torch.cat([src[sampled_edges], dst[sampled_edges]])
return sampled_nodes.unique().type(g.idtype)
def walk_sampler(self, g):
"""Node ID sampler for random walk sampler"""
num_roots, walk_length = self.budget
sampled_roots = torch.randint(0, g.num_nodes(), (num_roots,))
traces, types = random_walk(g, nodes=sampled_roots, length=walk_length)
sampled_nodes, _, _, _ = pack_traces(traces, types)
return sampled_nodes.unique().type(g.idtype)
def sample(self, g, indices):
"""Sampling function
Parameters
----------
g : DGLGraph
The graph to sample from.
indices : Tensor
Placeholder not used.
Returns
-------
DGLGraph
The sampled subgraph.
"""
node_ids = self.sampler(g)
sg = g.subgraph(
node_ids, relabel_nodes=True, output_device=self.output_device
)
set_node_lazy_features(sg, self.prefetch_ndata)
set_edge_lazy_features(sg, self.prefetch_edata)
return sg
+255
View File
@@ -0,0 +1,255 @@
#
# Copyright (c) 2022 by Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Based off of neighbor_sampler.py
#
"""Data loading components for labor sampling"""
from numpy.random import default_rng
from .. import backend as F
from ..base import EID, NID
from ..random import choice
from ..transforms import to_block
from .base import BlockSampler
class LaborSampler(BlockSampler):
"""Sampler that builds computational dependency of node representations via
labor sampling for multilayer GNN from the NeurIPS 2023 paper
`Layer-Neighbor Sampling -- Defusing Neighborhood Explosion in GNNs
<https://arxiv.org/abs/2210.13339>`__
This sampler will make every node gather messages from a fixed number of
neighbors per edge type. The neighbors are picked uniformly with default
parameters. For every vertex t that will be considered to be sampled, there
will be a single random variate r_t.
Parameters
----------
fanouts : list[int] or list[dict[etype, int]]
List of neighbors to sample per edge type for each GNN layer, with the
i-th element being the fanout for the i-th GNN layer.
If only a single integer is provided, DGL assumes that every edge type
will have the same fanout.
If -1 is provided for one edge type on one layer, then all inbound edges
of that edge type will be included.
edge_dir : str, default ``'in'``
Can be either ``'in'`` where the neighbors will be sampled according to
incoming edges, or ``'out'`` otherwise, same as
:func:`dgl.sampling.sample_neighbors`.
prob : str, optional
If given, the probability of each neighbor being sampled is proportional
to the edge feature value with the given name in ``g.edata``.
The feature must be a scalar on each edge. In this case, the returned
blocks edata include ``'edge_weights'`` that needs to be used in the
message passing operation.
importance_sampling : int, default ``0``
Whether to use importance sampling or uniform sampling, use of negative
values optimizes importance sampling probabilities until convergence
while use of positive values runs optimization steps that many times.
If the value is i, then LABOR-i variant is used. When used with a
nonzero parameter, the returned blocks edata include ``'edge_weights'``
that needs to be used in the message passing operation.
layer_dependency : bool, default ``False``
Specifies whether different layers should use same random variates.
Results into a reduction in the number of vertices sampled, but may
degrade the quality slightly.
batch_dependency : int, default ``1``
Specifies whether different minibatches should use similar random
variates. Results in a higher temporal access locality of sampled
vertices, but may degrade the quality slightly.
prefetch_node_feats : list[str] or dict[ntype, list[str]], optional
The source node data to prefetch for the first MFG, corresponding to the
input node features necessary for the first GNN layer.
prefetch_labels : list[str] or dict[ntype, list[str]], optional
The destination node data to prefetch for the last MFG, corresponding to
the node labels of the minibatch.
prefetch_edge_feats : list[str] or dict[etype, list[str]], optional
The edge data names to prefetch for all the MFGs, corresponding to the
edge features necessary for all GNN layers.
output_device : device, optional
The device of the output subgraphs or MFGs. Default is the same as the
minibatch of seed nodes.
Examples
--------
**Node classification**
To train a 3-layer GNN for node classification on a set of nodes
``train_nid`` on a homogeneous graph where each node takes messages from
5, 10, 15 neighbors for the first, second, and third layer respectively
(assuming the backend is PyTorch):
>>> sampler = dgl.dataloading.LaborSampler([5, 10, 15])
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_nid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, output_nodes, blocks in dataloader:
... train_on(blocks)
If training on a heterogeneous graph and you want different number of
neighbors for each edge type, one should instead provide a list of dicts.
Each dict would specify the number of neighbors to pick per edge type.
>>> sampler = dgl.dataloading.LaborSampler([
... {('user', 'follows', 'user'): 5,
... ('user', 'plays', 'game'): 4,
... ('game', 'played-by', 'user'): 3}] * 3)
If you would like non-uniform labor sampling:
>>> # any non-negative 1D vector works
>>> g.edata['p'] = torch.rand(g.num_edges())
>>> sampler = dgl.dataloading.LaborSampler([5, 10, 15], prob='p')
**Edge classification and link prediction**
This class can also work for edge classification and link prediction
together with :func:`as_edge_prediction_sampler`.
>>> sampler = dgl.dataloading.LaborSampler([5, 10, 15])
>>> sampler = dgl.dataloading.as_edge_prediction_sampler(sampler)
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_eid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
See the documentation :func:`as_edge_prediction_sampler` for more details.
Notes
-----
For the concept of MFGs, please refer to
:ref:`User Guide Section 6 <guide-minibatch>` and
:doc:`Minibatch Training Tutorials
<tutorials/large/L0_neighbor_sampling_overview>`.
"""
def __init__(
self,
fanouts,
edge_dir="in",
prob=None,
importance_sampling=0,
layer_dependency=False,
batch_dependency=1,
prefetch_node_feats=None,
prefetch_labels=None,
prefetch_edge_feats=None,
output_device=None,
):
super().__init__(
prefetch_node_feats=prefetch_node_feats,
prefetch_labels=prefetch_labels,
prefetch_edge_feats=prefetch_edge_feats,
output_device=output_device,
)
self.fanouts = fanouts
self.edge_dir = edge_dir
self.prob = prob
self.importance_sampling = importance_sampling
self.layer_dependency = layer_dependency
self.cnt = F.zeros(2, F.int64, F.cpu())
self.cnt[0] = -1
self.cnt[1] = batch_dependency
self.random_seed = F.zeros(
2 if self.cnt[1] > 1 else 1, F.int64, F.cpu()
)
self.set_seed(None if batch_dependency > 0 else choice(1e18, 1).item())
def set_seed(self, random_seed=None):
"""Updates the underlying seed for the sampler
Calling this function enforces the sampling algorithm to use the same
seed on every edge type. This can reduce the number of nodes being
sampled because the passed random_seed makes it so that for any seed
vertex ``s`` and its neighbor ``t``, the rolled random variate ``r_t``
is the same for any instance of this class with the same random seed.
When sampling as part of the same batch, one would want identical seeds
so that LABOR can globally sample. One example is that for heterogenous
graphs, there is a single random seed passed for each edge type. This
will sample much fewer vertices compared to having unique random seeds
for each edge type. If one called this function individually for each
edge type for a heterogenous graph with different random seeds, then it
would run LABOR locally for each edge type, resulting into a larger
number of vertices being sampled.
If this function is called without any parameters, we get the random
seed by getting a random number from DGL. Call this function if multiple
instances of LaborSampler are used to sample as part of a single batch.
Parameters
----------
random_seed : int, default ``None``
The random seed to be used for next sampling call.
"""
if random_seed is None:
self.cnt[0] += 1
if self.cnt[1] > 0 and self.cnt[0] % self.cnt[1] == 0:
if self.cnt[0] <= 0 or self.cnt[1] <= 1:
if not hasattr(self, "rng"):
self.rng = default_rng(choice(1e18, 1).item())
self.random_seed[0] = self.rng.integers(1e18)
if self.cnt[1] > 1:
self.random_seed[1] = self.rng.integers(1e18)
else:
self.random_seed[0] = self.random_seed[1]
self.random_seed[1] = self.rng.integers(1e18)
else:
self.rng = default_rng(random_seed)
self.random_seed[0] = self.rng.integers(1e18)
if self.cnt[1] > 1:
self.random_seed[1] = self.rng.integers(1e18)
self.cnt[0] = 0
def sample_blocks(self, g, seed_nodes, exclude_eids=None):
output_nodes = seed_nodes
blocks = []
for i, fanout in enumerate(reversed(self.fanouts)):
random_seed_i = F.zerocopy_to_dgl_ndarray(
self.random_seed + (i if not self.layer_dependency else 0)
)
if self.cnt[1] <= 1:
seed2_contr = 0
else:
seed2_contr = ((self.cnt[0] % self.cnt[1]) / self.cnt[1]).item()
frontier, importances = g.sample_labors(
seed_nodes,
fanout,
edge_dir=self.edge_dir,
prob=self.prob,
importance_sampling=self.importance_sampling,
random_seed=random_seed_i,
seed2_contribution=seed2_contr,
output_device=self.output_device,
exclude_edges=exclude_eids,
)
eid = frontier.edata[EID]
block = to_block(
frontier, seed_nodes, include_dst_in_src=True, src_nodes=None
)
block.edata[EID] = eid
if len(g.canonical_etypes) > 1:
for etype, importance in zip(g.canonical_etypes, importances):
if importance.shape[0] == block.num_edges(etype):
block.edata["edge_weights"][etype] = importance
elif importances[0].shape[0] == block.num_edges():
block.edata["edge_weights"] = importances[0]
seed_nodes = block.srcdata[NID]
blocks.insert(0, block)
self.set_seed()
return seed_nodes, output_nodes, blocks
+126
View File
@@ -0,0 +1,126 @@
"""Negative samplers"""
from collections.abc import Mapping
from .. import backend as F
class _BaseNegativeSampler(object):
def _generate(self, g, eids, canonical_etype):
raise NotImplementedError
def __call__(self, g, eids):
"""Returns negative samples.
Parameters
----------
g : DGLGraph
The graph.
eids : Tensor or dict[etype, Tensor]
The sampled edges in the minibatch.
Returns
-------
tuple[Tensor, Tensor] or dict[etype, tuple[Tensor, Tensor]]
The returned source-destination pairs as negative samples.
"""
if isinstance(eids, Mapping):
eids = {g.to_canonical_etype(k): v for k, v in eids.items()}
neg_pair = {k: self._generate(g, v, k) for k, v in eids.items()}
else:
assert (
len(g.canonical_etypes) == 1
), "please specify a dict of etypes and ids for graphs with multiple edge types"
neg_pair = self._generate(g, eids, g.canonical_etypes[0])
return neg_pair
class PerSourceUniform(_BaseNegativeSampler):
"""Negative sampler that randomly chooses negative destination nodes
for each source node according to a uniform distribution.
For each edge ``(u, v)`` of type ``(srctype, etype, dsttype)``, DGL generates
:attr:`k` pairs of negative edges ``(u, v')``, where ``v'`` is chosen
uniformly from all the nodes of type ``dsttype``. The resulting edges will
also have type ``(srctype, etype, dsttype)``.
Parameters
----------
k : int
The number of negative samples per edge.
Examples
--------
>>> g = dgl.graph(([0, 1, 2], [1, 2, 3]))
>>> neg_sampler = dgl.dataloading.negative_sampler.PerSourceUniform(2)
>>> neg_sampler(g, torch.tensor([0, 1]))
(tensor([0, 0, 1, 1]), tensor([1, 0, 2, 3]))
"""
def __init__(self, k):
self.k = k
def _generate(self, g, eids, canonical_etype):
_, _, vtype = canonical_etype
shape = F.shape(eids)
dtype = F.dtype(eids)
ctx = F.context(eids)
shape = (shape[0] * self.k,)
src, _ = g.find_edges(eids, etype=canonical_etype)
src = F.repeat(src, self.k, 0)
dst = F.randint(shape, dtype, ctx, 0, g.num_nodes(vtype))
return src, dst
# Alias
Uniform = PerSourceUniform
class GlobalUniform(_BaseNegativeSampler):
"""Negative sampler that randomly chooses negative source-destination pairs according
to a uniform distribution.
For each edge ``(u, v)`` of type ``(srctype, etype, dsttype)``, DGL generates at most
:attr:`k` pairs of negative edges ``(u', v')``, where ``u'`` is chosen uniformly from
all the nodes of type ``srctype`` and ``v'`` is chosen uniformly from all the nodes
of type ``dsttype``. The resulting edges will also have type
``(srctype, etype, dsttype)``. DGL guarantees that the sampled pairs will not have
edges in between.
Parameters
----------
k : int
The desired number of negative samples to generate per edge.
exclude_self_loops : bool, optional
Whether to exclude self-loops from negative samples. (Default: True)
replace : bool, optional
Whether to sample with replacement. Setting it to True will make things
faster. (Default: False)
Notes
-----
This negative sampler will try to generate as many negative samples as possible, but
it may rarely return less than :attr:`k` negative samples per edge.
This is more likely to happen if a graph is so small or dense that not many unique
negative samples exist.
Examples
--------
>>> g = dgl.graph(([0, 1, 2], [1, 2, 3]))
>>> neg_sampler = dgl.dataloading.negative_sampler.GlobalUniform(2, True)
>>> neg_sampler(g, torch.LongTensor([0, 1]))
(tensor([0, 1, 3, 2]), tensor([2, 0, 2, 1]))
"""
def __init__(self, k, exclude_self_loops=True, replace=False):
self.k = k
self.exclude_self_loops = exclude_self_loops
self.replace = replace
def _generate(self, g, eids, canonical_etype):
return g.global_uniform_negative_sampling(
len(eids) * self.k,
self.exclude_self_loops,
self.replace,
canonical_etype,
)
+246
View File
@@ -0,0 +1,246 @@
"""Data loading components for neighbor sampling"""
from .. import backend as F
from ..base import EID, NID
from ..heterograph import DGLGraph
from ..transforms import to_block
from ..utils import get_num_threads
from .base import BlockSampler
class NeighborSampler(BlockSampler):
"""Sampler that builds computational dependency of node representations via
neighbor sampling for multilayer GNN.
This sampler will make every node gather messages from a fixed number of neighbors
per edge type. The neighbors are picked uniformly.
Parameters
----------
fanouts : list[int] or list[dict[etype, int]]
List of neighbors to sample per edge type for each GNN layer, with the i-th
element being the fanout for the i-th GNN layer.
If only a single integer is provided, DGL assumes that every edge type
will have the same fanout.
If -1 is provided for one edge type on one layer, then all inbound edges
of that edge type will be included.
edge_dir : str, default ``'in'``
Can be either ``'in' `` where the neighbors will be sampled according to
incoming edges, or ``'out'`` otherwise, same as :func:`dgl.sampling.sample_neighbors`.
prob : str, optional
If given, the probability of each neighbor being sampled is proportional
to the edge feature value with the given name in ``g.edata``. The feature must be
a scalar on each edge.
This argument is mutually exclusive with :attr:`mask`. If you want to
specify both a mask and a probability, consider multiplying the probability
with the mask instead.
mask : str, optional
If given, a neighbor could be picked only if the edge mask with the given
name in ``g.edata`` is True. The data must be boolean on each edge.
This argument is mutually exclusive with :attr:`prob`. If you want to
specify both a mask and a probability, consider multiplying the probability
with the mask instead.
replace : bool, default False
Whether to sample with replacement
prefetch_node_feats : list[str] or dict[ntype, list[str]], optional
The source node data to prefetch for the first MFG, corresponding to the
input node features necessary for the first GNN layer.
prefetch_labels : list[str] or dict[ntype, list[str]], optional
The destination node data to prefetch for the last MFG, corresponding to
the node labels of the minibatch.
prefetch_edge_feats : list[str] or dict[etype, list[str]], optional
The edge data names to prefetch for all the MFGs, corresponding to the
edge features necessary for all GNN layers.
output_device : device, optional
The device of the output subgraphs or MFGs. Default is the same as the
minibatch of seed nodes.
fused : bool, default True
If True and device is CPU fused sample neighbors is invoked. This version
requires seed_nodes to be unique
Examples
--------
**Node classification**
To train a 3-layer GNN for node classification on a set of nodes ``train_nid`` on
a homogeneous graph where each node takes messages from 5, 10, 15 neighbors for
the first, second, and third layer respectively (assuming the backend is PyTorch):
>>> sampler = dgl.dataloading.NeighborSampler([5, 10, 15])
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_nid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, output_nodes, blocks in dataloader:
... train_on(blocks)
If training on a heterogeneous graph and you want different number of neighbors for each
edge type, one should instead provide a list of dicts. Each dict would specify the
number of neighbors to pick per edge type.
>>> sampler = dgl.dataloading.NeighborSampler([
... {('user', 'follows', 'user'): 5,
... ('user', 'plays', 'game'): 4,
... ('game', 'played-by', 'user'): 3}] * 3)
If you would like non-uniform neighbor sampling:
>>> g.edata['p'] = torch.rand(g.num_edges()) # any non-negative 1D vector works
>>> sampler = dgl.dataloading.NeighborSampler([5, 10, 15], prob='p')
Or sampling on edge masks:
>>> g.edata['mask'] = torch.rand(g.num_edges()) < 0.2 # any 1D boolean mask works
>>> sampler = dgl.dataloading.NeighborSampler([5, 10, 15], prob='mask')
**Edge classification and link prediction**
This class can also work for edge classification and link prediction together
with :func:`as_edge_prediction_sampler`.
>>> sampler = dgl.dataloading.NeighborSampler([5, 10, 15])
>>> sampler = dgl.dataloading.as_edge_prediction_sampler(sampler)
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_eid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
See the documentation :func:`as_edge_prediction_sampler` for more details.
Notes
-----
For the concept of MFGs, please refer to
:ref:`User Guide Section 6 <guide-minibatch>` and
:doc:`Minibatch Training Tutorials <tutorials/large/L0_neighbor_sampling_overview>`.
"""
def __init__(
self,
fanouts,
edge_dir="in",
prob=None,
mask=None,
replace=False,
prefetch_node_feats=None,
prefetch_labels=None,
prefetch_edge_feats=None,
output_device=None,
fused=True,
):
super().__init__(
prefetch_node_feats=prefetch_node_feats,
prefetch_labels=prefetch_labels,
prefetch_edge_feats=prefetch_edge_feats,
output_device=output_device,
)
self.fanouts = fanouts
self.edge_dir = edge_dir
if mask is not None and prob is not None:
raise ValueError(
"Mask and probability arguments are mutually exclusive. "
"Consider multiplying the probability with the mask "
"to achieve the same goal."
)
self.prob = prob or mask
self.replace = replace
self.fused = fused
self.mapping = {}
self.g = None
def sample_blocks(self, g, seed_nodes, exclude_eids=None):
output_nodes = seed_nodes
blocks = []
# sample_neighbors_fused function requires multithreading to be more efficient
# than sample_neighbors
if self.fused and get_num_threads() > 1:
cpu = F.device_type(g.device) == "cpu"
if isinstance(seed_nodes, dict):
for ntype in list(seed_nodes.keys()):
if not cpu:
break
cpu = (
cpu and F.device_type(seed_nodes[ntype].device) == "cpu"
)
else:
cpu = cpu and F.device_type(seed_nodes.device) == "cpu"
if cpu and isinstance(g, DGLGraph) and F.backend_name == "pytorch":
if self.g != g:
self.mapping = {}
self.g = g
for fanout in reversed(self.fanouts):
block = g.sample_neighbors_fused(
seed_nodes,
fanout,
edge_dir=self.edge_dir,
prob=self.prob,
replace=self.replace,
exclude_edges=exclude_eids,
mapping=self.mapping,
)
seed_nodes = block.srcdata[NID]
blocks.insert(0, block)
return seed_nodes, output_nodes, blocks
for fanout in reversed(self.fanouts):
frontier = g.sample_neighbors(
seed_nodes,
fanout,
edge_dir=self.edge_dir,
prob=self.prob,
replace=self.replace,
output_device=self.output_device,
exclude_edges=exclude_eids,
)
block = to_block(frontier, seed_nodes)
# If sampled from graphbolt-backed DistGraph, `EID` may not be in
# the block. If not exists, we should remove it from the block.
if EID in frontier.edata.keys():
block.edata[EID] = frontier.edata[EID]
else:
del block.edata[EID]
seed_nodes = block.srcdata[NID]
blocks.insert(0, block)
return seed_nodes, output_nodes, blocks
MultiLayerNeighborSampler = NeighborSampler
class MultiLayerFullNeighborSampler(NeighborSampler):
"""Sampler that builds computational dependency of node representations by taking messages
from all neighbors for multilayer GNN.
This sampler will make every node gather messages from every single neighbor per edge type.
Parameters
----------
num_layers : int
The number of GNN layers to sample.
kwargs :
Passed to :class:`dgl.dataloading.NeighborSampler`.
Examples
--------
To train a 3-layer GNN for node classification on a set of nodes ``train_nid`` on
a homogeneous graph where each node takes messages from all neighbors for the first,
second, and third layer respectively (assuming the backend is PyTorch):
>>> sampler = dgl.dataloading.MultiLayerFullNeighborSampler(3)
>>> dataloader = dgl.dataloading.DataLoader(
... g, train_nid, sampler,
... batch_size=1024, shuffle=True, drop_last=False, num_workers=4)
>>> for input_nodes, output_nodes, blocks in dataloader:
... train_on(blocks)
Notes
-----
For the concept of MFGs, please refer to
:ref:`User Guide Section 6 <guide-minibatch>` and
:doc:`Minibatch Training Tutorials <tutorials/large/L0_neighbor_sampling_overview>`.
"""
def __init__(self, num_layers, **kwargs):
super().__init__([-1] * num_layers, **kwargs)

Some files were not shown because too many files have changed in this diff Show More