chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
.. _guide-data-pipeline-dataset:
|
||||
|
||||
4.1 DGLDataset class
|
||||
--------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-data-pipeline-dataset>`
|
||||
|
||||
:class:`~dgl.data.DGLDataset` is the base class for processing, loading and saving
|
||||
graph datasets defined in :ref:`apidata`. It implements the basic pipeline
|
||||
for processing graph data. The following flow chart shows how the
|
||||
pipeline works.
|
||||
|
||||
To process a graph dataset located in a remote server or local disk, one can
|
||||
define a class, say ``MyDataset``, inheriting from :class:`dgl.data.DGLDataset`. The
|
||||
template of ``MyDataset`` is as follows.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/userguide_data_flow.png
|
||||
:align: center
|
||||
|
||||
Flow chart for graph data input pipeline defined in class DGLDataset.
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import DGLDataset
|
||||
|
||||
class MyDataset(DGLDataset):
|
||||
""" Template for customizing graph datasets in DGL.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
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/
|
||||
save_dir : str
|
||||
Directory to save the processed dataset.
|
||||
Default: the value of `raw_dir`
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information
|
||||
"""
|
||||
def __init__(self,
|
||||
url=None,
|
||||
raw_dir=None,
|
||||
save_dir=None,
|
||||
force_reload=False,
|
||||
verbose=False):
|
||||
super(MyDataset, self).__init__(name='dataset_name',
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
save_dir=save_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose)
|
||||
|
||||
def download(self):
|
||||
# download raw data to local disk
|
||||
pass
|
||||
|
||||
def process(self):
|
||||
# process raw data to graphs, labels, splitting masks
|
||||
pass
|
||||
|
||||
def __getitem__(self, idx):
|
||||
# get one example by index
|
||||
pass
|
||||
|
||||
def __len__(self):
|
||||
# number of data examples
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
# save processed data to directory `self.save_path`
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
# load processed data from directory `self.save_path`
|
||||
pass
|
||||
|
||||
def has_cache(self):
|
||||
# check whether there are processed data in `self.save_path`
|
||||
pass
|
||||
|
||||
|
||||
:class:`~dgl.data.DGLDataset` class has abstract functions ``process()``,
|
||||
``__getitem__(idx)`` and ``__len__()`` that must be implemented in the
|
||||
subclass. DGL also recommends implementing saving and loading as well,
|
||||
since they can save significant time for processing large datasets, and
|
||||
there are several APIs making it easy (see :ref:`guide-data-pipeline-savenload`).
|
||||
|
||||
Note that the purpose of :class:`~dgl.data.DGLDataset` is to provide a standard and
|
||||
convenient way to load graph data. One can store graphs, features,
|
||||
labels, masks and basic information about the dataset, such as number of
|
||||
classes, number of labels, etc. Operations such as sampling, partition
|
||||
or feature normalization are done outside of the :class:`~dgl.data.DGLDataset`
|
||||
subclass.
|
||||
|
||||
The rest of this chapter shows the best practices to implement the
|
||||
functions in the pipeline.
|
||||
@@ -0,0 +1,58 @@
|
||||
.. _guide-data-pipeline-download:
|
||||
|
||||
4.2 Download raw data (optional)
|
||||
--------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-data-pipeline-download>`
|
||||
|
||||
If a dataset is already in local disk, make sure it’s in directory
|
||||
``raw_dir``. If one wants to run the code anywhere without bothering to
|
||||
download and move data to the right directory, one can do it
|
||||
automatically by implementing function ``download()``.
|
||||
|
||||
If the dataset is a zip file, make ``MyDataset`` inherit from
|
||||
:class:`dgl.data.DGLBuiltinDataset` class, which handles the zip file extraction for us. Otherwise,
|
||||
one needs to implement ``download()`` like in :class:`~dgl.data.QM7bDataset`:
|
||||
|
||||
.. code::
|
||||
|
||||
import os
|
||||
from dgl.data.utils import download
|
||||
|
||||
def download(self):
|
||||
# path to store the file
|
||||
file_path = os.path.join(self.raw_dir, self.name + '.mat')
|
||||
# download file
|
||||
download(self.url, path=file_path)
|
||||
|
||||
The above code downloads a .mat file to directory ``self.raw_dir``. If
|
||||
the file is a .gz, .tar, .tar.gz or .tgz file, use :func:`~dgl.data.utils.extract_archive`
|
||||
function to extract. The following code shows how to download a .gz file
|
||||
in :class:`~dgl.data.BitcoinOTCDataset`:
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data.utils import download, check_sha1
|
||||
|
||||
def download(self):
|
||||
# path to store the file
|
||||
# make sure to use the same suffix as the original file name's
|
||||
gz_file_path = os.path.join(self.raw_dir, self.name + '.csv.gz')
|
||||
# download file
|
||||
download(self.url, path=gz_file_path)
|
||||
# check SHA-1
|
||||
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'))
|
||||
# extract file to directory `self.name` under `self.raw_dir`
|
||||
self._extract_gz(gz_file_path, self.raw_path)
|
||||
|
||||
The above code will extract the file into directory ``self.name`` under
|
||||
``self.raw_dir``. If the class inherits from :class:`dgl.data.DGLBuiltinDataset`
|
||||
to handle zip file, it will extract the file into directory ``self.name``
|
||||
as well.
|
||||
|
||||
Optionally, one can check SHA-1 string of the downloaded file as the
|
||||
example above does, in case the author changed the file in the remote
|
||||
server some day.
|
||||
@@ -0,0 +1,575 @@
|
||||
.. _guide-data-pipeline-loadcsv:
|
||||
|
||||
4.6 Loading data from CSV files
|
||||
----------------------------------------------
|
||||
|
||||
Comma Separated Value (CSV) is a widely used data storage format. DGL provides
|
||||
:class:`~dgl.data.CSVDataset` for loading and parsing graph data stored in
|
||||
CSV format.
|
||||
|
||||
To create a ``CSVDataset`` object:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
ds = dgl.data.CSVDataset('/path/to/dataset')
|
||||
|
||||
The returned ``ds`` object is a standard :class:`~dgl.data.DGLDataset`. For
|
||||
example, one can get graph samples using ``__getitem__`` as well as node/edge
|
||||
features using ``ndata``/``edata``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# A demonstration of how to use the loaded dataset. The feature names
|
||||
# may vary depending on the CSV contents.
|
||||
g = ds[0] # get the graph
|
||||
label = g.ndata['label']
|
||||
feat = g.ndata['feat']
|
||||
|
||||
Data folder structure
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
/path/to/dataset/
|
||||
|-- meta.yaml # metadata of the dataset
|
||||
|-- edges_0.csv # edge data including src_id, dst_id, feature, label and so on
|
||||
|-- ... # you can have as many CSVs for edge data as you want
|
||||
|-- nodes_0.csv # node data including node_id, feature, label and so on
|
||||
|-- ... # you can have as many CSVs for node data as you want
|
||||
|-- graphs.csv # graph-level features
|
||||
|
||||
Node/edge/graph-level data are stored in CSV files. ``meta.yaml`` is a metadata file specifying
|
||||
where to read nodes/edges/graphs data and how to parse them to construct the dataset
|
||||
object. A minimal data folder contains one ``meta.yaml`` and two CSVs, one for node data and one
|
||||
for edge data, in which case the dataset contains only a single graph with no graph-level data.
|
||||
|
||||
Dataset of a single feature-less graph
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When the dataset contains only one graph with no node or edge features, there need only three
|
||||
files in the data folder: ``meta.yaml``, one CSV for node IDs and one CSV for edges:
|
||||
|
||||
.. code::
|
||||
|
||||
./mini_featureless_dataset/
|
||||
|-- meta.yaml
|
||||
|-- nodes.csv
|
||||
|-- edges.csv
|
||||
|
||||
``meta.yaml`` contains the following information:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
dataset_name: mini_featureless_dataset
|
||||
edge_data:
|
||||
- file_name: edges.csv
|
||||
node_data:
|
||||
- file_name: nodes.csv
|
||||
|
||||
``nodes.csv`` lists the node IDs under the ``node_id`` field:
|
||||
|
||||
.. code::
|
||||
|
||||
node_id
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
|
||||
``edges.csv`` lists all the edges in two columns (``src_id`` and ``dst_id``) specifying the
|
||||
source and destination node ID of each edge:
|
||||
|
||||
.. code::
|
||||
|
||||
src_id,dst_id
|
||||
4,4
|
||||
4,1
|
||||
3,0
|
||||
4,1
|
||||
4,0
|
||||
1,2
|
||||
1,3
|
||||
3,3
|
||||
1,1
|
||||
4,1
|
||||
|
||||
After loaded, the dataset has one graph without any features:
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> dataset = dgl.data.CSVDataset('./mini_featureless_dataset')
|
||||
>>> g = dataset[0] # only one graph
|
||||
>>> print(g)
|
||||
Graph(num_nodes=5, num_edges=10,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
.. note::
|
||||
Non-integer node IDs are allowed. When constructing the graph, ``CSVDataset`` will
|
||||
map each raw ID to an integer ID starting from zero.
|
||||
If the node IDs are already distinct integers from 0 to ``num_nodes-1``, no mapping
|
||||
is applied.
|
||||
|
||||
.. note::
|
||||
Edges are always directed. To have both directions, add reversed edges in the edge
|
||||
CSV file or use :class:`~dgl.transforms.AddReverse` to transform the loaded graph.
|
||||
|
||||
|
||||
A graph without any feature is often of less interest. In the next example, we will show
|
||||
how to load and parse node or edge features.
|
||||
|
||||
Dataset of a single graph with features and labels
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When the dataset contains a single graph with node or edge features and labels, there still
|
||||
need only three files in the data folder: ``meta.yaml``, one CSV for node IDs and one CSV
|
||||
for edges:
|
||||
|
||||
.. code::
|
||||
|
||||
./mini_feature_dataset/
|
||||
|-- meta.yaml
|
||||
|-- nodes.csv
|
||||
|-- edges.csv
|
||||
|
||||
``meta.yaml``:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
dataset_name: mini_feature_dataset
|
||||
edge_data:
|
||||
- file_name: edges.csv
|
||||
node_data:
|
||||
- file_name: nodes.csv
|
||||
|
||||
``edges.csv`` with five synthetic edge data (``label``, ``train_mask``, ``val_mask``, ``test_mask``, ``feat``):
|
||||
|
||||
.. code::
|
||||
|
||||
src_id,dst_id,label,train_mask,val_mask,test_mask,feat
|
||||
4,0,2,False,True,True,"0.5477868606453535, 0.4470617033458436, 0.936706701616337"
|
||||
4,0,0,False,False,True,"0.9794634290792008, 0.23682038840665198, 0.049629338970987646"
|
||||
0,3,1,True,True,True,"0.8586722047523594, 0.5746912787380253, 0.6462162561249654"
|
||||
0,1,2,True,False,False,"0.2730008213674695, 0.5937484188166621, 0.765544096939567"
|
||||
0,2,1,True,True,True,"0.45441619816038514, 0.1681403185591509, 0.9952376085297715"
|
||||
0,0,0,False,False,False,"0.4197669213305396, 0.849983324532477, 0.16974127573016262"
|
||||
2,2,1,False,True,True,"0.5495035052928215, 0.21394654203489705, 0.7174910641836348"
|
||||
1,0,2,False,True,False,"0.008790817766266334, 0.4216530595907526, 0.529195480661293"
|
||||
3,0,0,True,True,True,"0.6598715708878852, 0.1932390907048961, 0.9774471538377553"
|
||||
4,0,1,False,False,False,"0.16846068931179736, 0.41516080644186737, 0.002158116134429955"
|
||||
|
||||
|
||||
``nodes.csv`` with five synthetic node data (``label``, ``train_mask``, ``val_mask``, ``test_mask``, ``feat``):
|
||||
|
||||
.. code::
|
||||
|
||||
node_id,label,train_mask,val_mask,test_mask,feat
|
||||
0,1,False,True,True,"0.07816474278491703, 0.9137336384979067, 0.4654086994009452"
|
||||
1,1,True,True,True,"0.05354099924658973, 0.8753101998792645, 0.33929432608774135"
|
||||
2,1,True,False,True,"0.33234211884156384, 0.9370522452510665, 0.6694943496824788"
|
||||
3,0,False,True,False,"0.9784264442230887, 0.22131880861864428, 0.3161154827254189"
|
||||
4,1,True,True,False,"0.23142237259162102, 0.8715767748481147, 0.19117861103555467"
|
||||
|
||||
After loaded, the dataset has one graph. Node/edge features are stored in ``ndata`` and ``edata``
|
||||
with the same column names. The example demonstrates how to specify a vector-shaped feature
|
||||
using comma-separated list enclosed by double quotes ``"..."``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> dataset = dgl.data.CSVDataset('./mini_feature_dataset')
|
||||
>>> g = dataset[0] # only one graph
|
||||
>>> print(g)
|
||||
Graph(num_nodes=5, num_edges=10,
|
||||
ndata_schemes={'label': Scheme(shape=(), dtype=torch.int64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'val_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool), 'feat': Scheme(shape=(3,), dtype=torch.float64)}
|
||||
edata_schemes={'label': Scheme(shape=(), dtype=torch.int64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'val_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool), 'feat': Scheme(shape=(3,), dtype=torch.float64)})
|
||||
|
||||
.. note::
|
||||
By default, ``CSVDatatset`` assumes all feature data to be numerical values (e.g., int, float, bool or
|
||||
list) and missing values are not allowed. Users could provide custom data parser for these cases.
|
||||
See `Custom Data Parser`_ for more details.
|
||||
|
||||
Dataset of a single heterogeneous graph
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
One can specify multiple node and edge CSV files (each for one type) to represent a heterogeneous graph.
|
||||
Here is an example data with two node types and two edge types:
|
||||
|
||||
.. code::
|
||||
|
||||
./mini_hetero_dataset/
|
||||
|-- meta.yaml
|
||||
|-- nodes_0.csv
|
||||
|-- nodes_1.csv
|
||||
|-- edges_0.csv
|
||||
|-- edges_1.csv
|
||||
|
||||
The ``meta.yaml`` specifies the node type name (using ``ntype``) and edge type name (using ``etype``)
|
||||
of each CSV file. The edge type name is a string triplet containing the source node type name, relation
|
||||
name and the destination node type name.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
dataset_name: mini_hetero_dataset
|
||||
edge_data:
|
||||
- file_name: edges_0.csv
|
||||
etype: [user, follow, user]
|
||||
- file_name: edges_1.csv
|
||||
etype: [user, like, item]
|
||||
node_data:
|
||||
- file_name: nodes_0.csv
|
||||
ntype: user
|
||||
- file_name: nodes_1.csv
|
||||
ntype: item
|
||||
|
||||
The node and edge CSV files follow the same format as in homogeneous graphs. Here are some synthetic
|
||||
data for demonstration purposes:
|
||||
|
||||
``edges_0.csv`` and ``edges_1.csv``:
|
||||
|
||||
.. code::
|
||||
|
||||
src_id,dst_id,label,feat
|
||||
4,4,1,"0.736833152378035,0.10522806046048205,0.9418796835016118"
|
||||
3,4,2,"0.5749339182767451,0.20181320245665535,0.490938012147181"
|
||||
1,4,2,"0.7697294432580938,0.49397782380750765,0.10864079337442234"
|
||||
0,4,0,"0.1364240150959487,0.1393107840629273,0.7901988878812207"
|
||||
2,3,1,"0.42988138237505735,0.18389137408509248,0.18431292077750894"
|
||||
0,4,2,"0.8613368738351794,0.67985810014162,0.6580438064356824"
|
||||
2,4,1,"0.6594951663841697,0.26499036865016423,0.7891429392727503"
|
||||
4,1,0,"0.36649684241348557,0.9511783938523962,0.8494919263589972"
|
||||
1,1,2,"0.698592283371875,0.038622249776255946,0.5563827995742111"
|
||||
0,4,1,"0.5227112950269823,0.3148264185956532,0.47562693094002173"
|
||||
|
||||
``nodes_0.csv`` and ``nodes_1.csv``:
|
||||
|
||||
.. code::
|
||||
|
||||
node_id,label,feat
|
||||
0,2,"0.5400687466285844,0.7588441197954202,0.4268254673041745"
|
||||
1,1,"0.08680051341900807,0.11446843700743892,0.7196969604886617"
|
||||
2,2,"0.8964389655603473,0.23368113896545695,0.8813472954005022"
|
||||
3,1,"0.5454703921677284,0.7819383771535038,0.3027939452162367"
|
||||
4,1,"0.5365210052235699,0.8975240205792763,0.7613943085507672"
|
||||
|
||||
After loaded, the dataset has one heterograph with features and labels:
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> dataset = dgl.data.CSVDataset('./mini_hetero_dataset')
|
||||
>>> g = dataset[0] # only one graph
|
||||
>>> print(g)
|
||||
Graph(num_nodes={'item': 5, 'user': 5},
|
||||
num_edges={('user', 'follow', 'user'): 10, ('user', 'like', 'item'): 10},
|
||||
metagraph=[('user', 'user', 'follow'), ('user', 'item', 'like')])
|
||||
>>> g.nodes['user'].data
|
||||
{'label': tensor([2, 1, 2, 1, 1]), 'feat': tensor([[0.5401, 0.7588, 0.4268],
|
||||
[0.0868, 0.1145, 0.7197],
|
||||
[0.8964, 0.2337, 0.8813],
|
||||
[0.5455, 0.7819, 0.3028],
|
||||
[0.5365, 0.8975, 0.7614]], dtype=torch.float64)}
|
||||
>>> g.edges['like'].data
|
||||
{'label': tensor([1, 2, 2, 0, 1, 2, 1, 0, 2, 1]), 'feat': tensor([[0.7368, 0.1052, 0.9419],
|
||||
[0.5749, 0.2018, 0.4909],
|
||||
[0.7697, 0.4940, 0.1086],
|
||||
[0.1364, 0.1393, 0.7902],
|
||||
[0.4299, 0.1839, 0.1843],
|
||||
[0.8613, 0.6799, 0.6580],
|
||||
[0.6595, 0.2650, 0.7891],
|
||||
[0.3665, 0.9512, 0.8495],
|
||||
[0.6986, 0.0386, 0.5564],
|
||||
[0.5227, 0.3148, 0.4756]], dtype=torch.float64)}
|
||||
|
||||
Dataset of multiple graphs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When there are multiple graphs, one can include an additional CSV file for storing graph-level features.
|
||||
Here is an example:
|
||||
|
||||
.. code::
|
||||
|
||||
./mini_multi_dataset/
|
||||
|-- meta.yaml
|
||||
|-- nodes.csv
|
||||
|-- edges.csv
|
||||
|-- graphs.csv
|
||||
|
||||
Accordingly, the ``meta.yaml`` should include an extra ``graph_data`` key to tell which CSV file to
|
||||
load graph-level features from.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
dataset_name: mini_multi_dataset
|
||||
edge_data:
|
||||
- file_name: edges.csv
|
||||
node_data:
|
||||
- file_name: nodes.csv
|
||||
graph_data:
|
||||
file_name: graphs.csv
|
||||
|
||||
To distinguish nodes and edges of different graphs, the ``node.csv`` and ``edge.csv`` must contain
|
||||
an extra column ``graph_id``:
|
||||
|
||||
``edges.csv``:
|
||||
|
||||
.. code::
|
||||
|
||||
graph_id,src_id,dst_id,feat
|
||||
0,0,4,"0.39534097273254654,0.9422093637539785,0.634899790318452"
|
||||
0,3,0,"0.04486384200747007,0.6453746567017163,0.8757520744192612"
|
||||
0,3,2,"0.9397636966928355,0.6526403892728874,0.8643238446466464"
|
||||
0,1,1,"0.40559906615287566,0.9848072295736628,0.493888090726854"
|
||||
0,4,1,"0.253458867276219,0.9168191778828504,0.47224962583565544"
|
||||
0,0,1,"0.3219496197945605,0.3439899477636117,0.7051530741717352"
|
||||
0,2,1,"0.692873149428549,0.4770019763881086,0.21937428942781778"
|
||||
0,4,0,"0.620118223673067,0.08691420300562658,0.86573472329756"
|
||||
0,2,1,"0.00743445923710373,0.5251800239734318,0.054016385555202384"
|
||||
0,4,1,"0.6776417760682221,0.7291568018841328,0.4523600060547709"
|
||||
1,1,3,"0.6375445528248924,0.04878384701995819,0.4081642382536248"
|
||||
1,0,4,"0.776002616178397,0.8851294998284638,0.7321742043493028"
|
||||
1,1,0,"0.0928555079874982,0.6156748364694707,0.6985674921582508"
|
||||
1,0,2,"0.31328748118329997,0.8326121496142408,0.04133991340612775"
|
||||
1,1,0,"0.36786902637778773,0.39161865931662243,0.9971749359397111"
|
||||
1,1,1,"0.4647410679872376,0.8478810655406659,0.6746269314422184"
|
||||
1,0,2,"0.8117650553546695,0.7893727601272978,0.41527155506593394"
|
||||
1,1,3,"0.40707309111756307,0.2796588354307046,0.34846782265758314"
|
||||
1,1,0,"0.18626464175355095,0.3523777809254057,0.7863421810531344"
|
||||
1,3,0,"0.28357022069634585,0.13774964202156292,0.5913335505943637"
|
||||
|
||||
``nodes.csv``:
|
||||
|
||||
.. code::
|
||||
|
||||
graph_id,node_id,feat
|
||||
0,0,"0.5725330322207948,0.8451870383322376,0.44412796119211184"
|
||||
0,1,"0.6624186423087752,0.6118386331195641,0.7352138669985214"
|
||||
0,2,"0.7583372765843964,0.15218126307872892,0.6810484348765842"
|
||||
0,3,"0.14627522432017592,0.7457985352827006,0.1037097085190507"
|
||||
0,4,"0.49037522512771525,0.8778998699783784,0.0911194482288028"
|
||||
1,0,"0.11158102039672668,0.08543289788089736,0.6901745368284345"
|
||||
1,1,"0.28367647637469273,0.07502571020414439,0.01217200152200748"
|
||||
1,2,"0.2472495901894738,0.24285506608575758,0.6494437360242048"
|
||||
1,3,"0.5614197853127827,0.059172654879085296,0.4692371689047904"
|
||||
1,4,"0.17583413999295983,0.5191278830882644,0.8453123358491914"
|
||||
|
||||
The ``graphs.csv`` contains a ``graph_id`` column and arbitrary number of feature columns.
|
||||
The example dataset here has two graphs, each with a ``feat`` and a ``label`` graph-level
|
||||
data.
|
||||
|
||||
.. code::
|
||||
|
||||
graph_id,feat,label
|
||||
0,"0.7426272601929126,0.5197462471155317,0.8149104951283953",0
|
||||
1,"0.534822233529295,0.2863627767733977,0.1154897249106891",0
|
||||
|
||||
After loaded, the dataset has multiple homographs with features and labels:
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> dataset = dgl.data.CSVDataset('./mini_multi_dataset')
|
||||
>>> print(len(dataset))
|
||||
2
|
||||
>>> graph0, data0 = dataset[0]
|
||||
>>> print(graph0)
|
||||
Graph(num_nodes=5, num_edges=10,
|
||||
ndata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)}
|
||||
edata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)})
|
||||
>>> print(data0)
|
||||
{'feat': tensor([0.7426, 0.5197, 0.8149], dtype=torch.float64), 'label': tensor(0)}
|
||||
>>> graph1, data1 = dataset[1]
|
||||
>>> print(graph1)
|
||||
Graph(num_nodes=5, num_edges=10,
|
||||
ndata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)}
|
||||
edata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)})
|
||||
>>> print(data1)
|
||||
{'feat': tensor([0.5348, 0.2864, 0.1155], dtype=torch.float64), 'label': tensor(0)}
|
||||
|
||||
If there is a single feature column in ``graphs.csv``, ``data0`` will directly be a tensor for the feature.
|
||||
|
||||
|
||||
Custom Data Parser
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, ``CSVDataset`` assumes that all the stored node-/edge-/graph- level data are numerical
|
||||
values. Users can provide custom ``DataParser`` to ``CSVDataset`` to handle more complex
|
||||
data type. A ``DataParser`` needs to implement the ``__call__`` method which takes in the
|
||||
:class:`pandas.DataFrame` object created from CSV file and should return a dictionary of
|
||||
parsed feature data. The parsed feature data will be saved to the ``ndata`` and ``edata`` of
|
||||
the corresponding ``DGLGraph`` object, and thus must be tensors or numpy arrays. Below shows an example
|
||||
``DataParser`` which converts string type labels to integers:
|
||||
|
||||
Given a dataset as follows,
|
||||
|
||||
.. code::
|
||||
|
||||
./customized_parser_dataset/
|
||||
|-- meta.yaml
|
||||
|-- nodes.csv
|
||||
|-- edges.csv
|
||||
|
||||
``meta.yaml``:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
dataset_name: customized_parser_dataset
|
||||
edge_data:
|
||||
- file_name: edges.csv
|
||||
node_data:
|
||||
- file_name: nodes.csv
|
||||
|
||||
``edges.csv``:
|
||||
|
||||
.. code::
|
||||
|
||||
src_id,dst_id,label
|
||||
4,0,positive
|
||||
4,0,negative
|
||||
0,3,positive
|
||||
0,1,positive
|
||||
0,2,negative
|
||||
0,0,positive
|
||||
2,2,negative
|
||||
1,0,positive
|
||||
3,0,negative
|
||||
4,0,positive
|
||||
|
||||
``nodes.csv``:
|
||||
|
||||
.. code::
|
||||
|
||||
node_id,label
|
||||
0,positive
|
||||
1,negative
|
||||
2,positive
|
||||
3,negative
|
||||
4,positive
|
||||
|
||||
To parse the string type labels, one can define a ``DataParser`` class as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
class MyDataParser:
|
||||
def __call__(self, df: pd.DataFrame):
|
||||
parsed = {}
|
||||
for header in df:
|
||||
if 'Unnamed' in header: # Handle Unnamed column
|
||||
print("Unnamed column is found. Ignored...")
|
||||
continue
|
||||
dt = df[header].to_numpy().squeeze()
|
||||
if header == 'label':
|
||||
dt = np.array([1 if e == 'positive' else 0 for e in dt])
|
||||
parsed[header] = dt
|
||||
return parsed
|
||||
|
||||
Create a ``CSVDataset`` using the defined ``DataParser``:
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> dataset = dgl.data.CSVDataset('./customized_parser_dataset',
|
||||
... ndata_parser=MyDataParser(),
|
||||
... edata_parser=MyDataParser())
|
||||
>>> print(dataset[0].ndata['label'])
|
||||
tensor([1, 0, 1, 0, 1])
|
||||
>>> print(dataset[0].edata['label'])
|
||||
tensor([1, 0, 1, 1, 0, 1, 0, 1, 0, 1])
|
||||
|
||||
.. note::
|
||||
|
||||
To specify different ``DataParser``\s for different node/edge types, pass a dictionary to
|
||||
``ndata_parser`` and ``edata_parser``, where the key is type name (a single string for
|
||||
node type; a string triplet for edge type) and the value is the ``DataParser`` to use.
|
||||
|
||||
|
||||
Full YAML Specification
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``CSVDataset`` allows more flexible control over the loading and parsing process. For example, one
|
||||
can change the ID column names via ``meta.yaml``. The example below lists all the supported keys.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
version: 1.0.0
|
||||
dataset_name: some_complex_data
|
||||
separator: ',' # CSV separator symbol. Default: ','
|
||||
edge_data:
|
||||
- file_name: edges_0.csv
|
||||
etype: [user, follow, user]
|
||||
src_id_field: src_id # Column name for source node IDs. Default: src_id
|
||||
dst_id_field: dst_id # Column name for destination node IDs. Default: dst_id
|
||||
- file_name: edges_1.csv
|
||||
etype: [user, like, item]
|
||||
src_id_field: src_id
|
||||
dst_id_field: dst_id
|
||||
node_data:
|
||||
- file_name: nodes_0.csv
|
||||
ntype: user
|
||||
node_id_field: node_id # Column name for node IDs. Default: node_id
|
||||
- file_name: nodes_1.csv
|
||||
ntype: item
|
||||
node_id_field: node_id # Column name for node IDs. Default: node_id
|
||||
graph_data:
|
||||
file_name: graphs.csv
|
||||
graph_id_field: graph_id # Column name for graph IDs. Default: graph_id
|
||||
|
||||
Top-level
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
At the top level, only 6 keys are available:
|
||||
|
||||
- ``version``: Optional. String.
|
||||
It specifies which version of ``meta.yaml`` is used. More feature may be added in the future.
|
||||
- ``dataset_name``: Required. String.
|
||||
It specifies the dataset name.
|
||||
- ``separator``: Optional. String.
|
||||
It specifies how to parse data in CSV files. Default: ``','``.
|
||||
- ``edge_data``: Required. List of ``EdgeData``.
|
||||
Meta data for parsing edge CSV files.
|
||||
- ``node_data``: Required. List of ``NodeData``.
|
||||
Meta data for parsing node CSV files.
|
||||
- ``graph_data``: Optional. ``GraphData``.
|
||||
Meta data for parsing the graph CSV file.
|
||||
|
||||
``EdgeData``
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are 4 keys:
|
||||
|
||||
- ``file_name``: Required. String.
|
||||
The CSV file to load data from.
|
||||
- ``etype``: Optional. List of string.
|
||||
Edge type name in string triplet: [source node type, relation type, destination node type].
|
||||
- ``src_id_field``: Optional. String.
|
||||
Which column to read for source node IDs. Default: ``src_id``.
|
||||
- ``dst_id_field``: Optional. String.
|
||||
Which column to read for destination node IDs. Default: ``dst_id``.
|
||||
|
||||
``NodeData``
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are 3 keys:
|
||||
|
||||
- ``file_name``: Required. String.
|
||||
The CSV file to load data from.
|
||||
- ``ntype``: Optional. String.
|
||||
Node type name.
|
||||
- ``node_id_field``: Optional. String.
|
||||
Which column to read for node IDs. Default: ``node_id``.
|
||||
|
||||
``GraphData``
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are 2 keys:
|
||||
|
||||
- ``file_name``: Required. String.
|
||||
The CSV file to load data from.
|
||||
- ``graph_id_field``: Optional. String.
|
||||
Which column to read for graph IDs. Default: ``graph_id``.
|
||||
@@ -0,0 +1,79 @@
|
||||
.. _guide-data-pipeline-loadogb:
|
||||
|
||||
4.5 Loading OGB datasets using ``ogb`` package
|
||||
----------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-data-pipeline-loadogb>`
|
||||
|
||||
`Open Graph Benchmark (OGB) <https://ogb.stanford.edu/docs/home/>`__ is
|
||||
a collection of benchmark datasets. The official OGB package
|
||||
`ogb <https://github.com/snap-stanford/ogb>`__ provides APIs for
|
||||
downloading and processing OGB datasets into :class:`dgl.data.DGLGraph` objects. The section
|
||||
introduce their basic usage here.
|
||||
|
||||
First install ogb package using pip:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install ogb
|
||||
|
||||
The following code shows how to load datasets for *Graph Property
|
||||
Prediction* tasks.
|
||||
|
||||
.. code::
|
||||
|
||||
# Load Graph Property Prediction datasets in OGB
|
||||
import dgl
|
||||
import torch
|
||||
from ogb.graphproppred import DglGraphPropPredDataset
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
|
||||
|
||||
def _collate_fn(batch):
|
||||
# batch is a list of tuple (graph, label)
|
||||
graphs = [e[0] for e in batch]
|
||||
g = dgl.batch(graphs)
|
||||
labels = [e[1] for e in batch]
|
||||
labels = torch.stack(labels, 0)
|
||||
return g, labels
|
||||
|
||||
# load dataset
|
||||
dataset = DglGraphPropPredDataset(name='ogbg-molhiv')
|
||||
split_idx = dataset.get_idx_split()
|
||||
# dataloader
|
||||
train_loader = GraphDataLoader(dataset[split_idx["train"]], batch_size=32, shuffle=True, collate_fn=_collate_fn)
|
||||
valid_loader = GraphDataLoader(dataset[split_idx["valid"]], batch_size=32, shuffle=False, collate_fn=_collate_fn)
|
||||
test_loader = GraphDataLoader(dataset[split_idx["test"]], batch_size=32, shuffle=False, collate_fn=_collate_fn)
|
||||
|
||||
Loading *Node Property Prediction* datasets is similar, but note that
|
||||
there is only one graph object in this kind of dataset.
|
||||
|
||||
.. code::
|
||||
|
||||
# Load Node Property Prediction datasets in OGB
|
||||
from ogb.nodeproppred import DglNodePropPredDataset
|
||||
|
||||
dataset = DglNodePropPredDataset(name='ogbn-proteins')
|
||||
split_idx = dataset.get_idx_split()
|
||||
|
||||
# there is only one graph in Node Property Prediction datasets
|
||||
g, labels = dataset[0]
|
||||
# get split labels
|
||||
train_label = dataset.labels[split_idx['train']]
|
||||
valid_label = dataset.labels[split_idx['valid']]
|
||||
test_label = dataset.labels[split_idx['test']]
|
||||
|
||||
*Link Property Prediction* datasets also contain one graph per dataset.
|
||||
|
||||
.. code::
|
||||
|
||||
# Load Link Property Prediction datasets in OGB
|
||||
from ogb.linkproppred import DglLinkPropPredDataset
|
||||
|
||||
dataset = DglLinkPropPredDataset(name='ogbl-ppa')
|
||||
split_edge = dataset.get_edge_split()
|
||||
|
||||
graph = dataset[0]
|
||||
print(split_edge['train'].keys())
|
||||
print(split_edge['valid'].keys())
|
||||
print(split_edge['test'].keys())
|
||||
@@ -0,0 +1,326 @@
|
||||
.. _guide-data-pipeline-process:
|
||||
|
||||
4.3 Process data
|
||||
----------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-data-pipeline-process>`
|
||||
|
||||
One can implement the data processing code in function ``process()``, and it
|
||||
assumes that the raw data is located in ``self.raw_dir`` already. There
|
||||
are typically three types of tasks in machine learning on graphs: graph
|
||||
classification, node classification, and link prediction. This section will show
|
||||
how to process datasets related to these tasks.
|
||||
|
||||
The section focuses on the standard way to process graphs, features and masks.
|
||||
It will use builtin datasets as examples and skip the implementations
|
||||
for building graphs from files, but add links to the detailed
|
||||
implementations. Please refer to :ref:`guide-graph-external` to see a
|
||||
complete guide on how to build graphs from external sources.
|
||||
|
||||
Processing Graph Classification datasets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Graph classification datasets are almost the same as most datasets in
|
||||
typical machine learning tasks, where mini-batch training is used. So one can
|
||||
process the raw data to a list of :class:`dgl.DGLGraph` objects and a list of
|
||||
label tensors. In addition, if the raw data has been split into
|
||||
several files, one can add a parameter ``split`` to load specific part of
|
||||
the data.
|
||||
|
||||
Take :class:`~dgl.data.QM7bDataset` as example:
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import DGLDataset
|
||||
|
||||
class QM7bDataset(DGLDataset):
|
||||
_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):
|
||||
super(QM7bDataset, self).__init__(name='qm7b',
|
||||
url=self._url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose)
|
||||
|
||||
def process(self):
|
||||
mat_path = self.raw_path + '.mat'
|
||||
# process data to a list of graphs and a list of labels
|
||||
self.graphs, self.label = self._load_graph(mat_path)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
""" Get graph and label by index
|
||||
|
||||
Parameters
|
||||
----------
|
||||
idx : int
|
||||
Item index
|
||||
|
||||
Returns
|
||||
-------
|
||||
(dgl.DGLGraph, Tensor)
|
||||
"""
|
||||
return self.graphs[idx], self.label[idx]
|
||||
|
||||
def __len__(self):
|
||||
"""Number of graphs in the dataset"""
|
||||
return len(self.graphs)
|
||||
|
||||
|
||||
In ``process()``, the raw data is processed to a list of graphs and a
|
||||
list of labels. One must implement ``__getitem__(idx)`` and ``__len__()``
|
||||
for iteration. DGL recommends making ``__getitem__(idx)`` return a
|
||||
tuple ``(graph, label)`` as above. Please check the `QM7bDataset source
|
||||
code <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/qm7b.html#QM7bDataset>`__
|
||||
for details of ``self._load_graph()`` and ``__getitem__``.
|
||||
|
||||
One can also add properties to the class to indicate some useful
|
||||
information of the dataset. In :class:`~dgl.data.QM7bDataset`, one can add a property
|
||||
``num_tasks`` to indicate the total number of prediction tasks in this
|
||||
multi-task dataset:
|
||||
|
||||
.. code::
|
||||
|
||||
@property
|
||||
def num_tasks(self):
|
||||
"""Number of labels for each graph, i.e. number of prediction tasks."""
|
||||
return 14
|
||||
|
||||
After all these coding, one can finally use :class:`~dgl.data.QM7bDataset` as
|
||||
follows:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl
|
||||
import torch
|
||||
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
|
||||
# load data
|
||||
dataset = QM7bDataset()
|
||||
num_tasks = dataset.num_tasks
|
||||
|
||||
# create dataloaders
|
||||
dataloader = GraphDataLoader(dataset, batch_size=1, shuffle=True)
|
||||
|
||||
# training
|
||||
for epoch in range(100):
|
||||
for g, labels in dataloader:
|
||||
# your training code here
|
||||
pass
|
||||
|
||||
A complete guide for training graph classification models can be found
|
||||
in :ref:`guide-training-graph-classification`.
|
||||
|
||||
For more examples of graph classification datasets, please refer to DGL's builtin graph classification
|
||||
datasets:
|
||||
|
||||
* :ref:`gindataset`
|
||||
|
||||
* :ref:`minigcdataset`
|
||||
|
||||
* :ref:`qm7bdata`
|
||||
|
||||
* :ref:`tudata`
|
||||
|
||||
Processing Node Classification datasets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Different from graph classification, node classification is typically on
|
||||
a single graph. As such, splits of the dataset are on the nodes of the
|
||||
graph. DGL recommends using node masks to specify the splits. The section uses
|
||||
builtin dataset `CitationGraphDataset <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/citation_graph.html#CitationGraphDataset>`__ as an example:
|
||||
|
||||
In addition, DGL recommends re-arrange the nodes and edges so that nodes
|
||||
near to each other have IDs in a close range. The procedure could improve
|
||||
the locality to access a node's neighbors, which may benefit follow-up
|
||||
computation and analysis conducted on the graph. DGL provides an API called
|
||||
:func:`dgl.reorder_graph` for this purpose. Please refer to ``process()``
|
||||
part in below example for more details.
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import DGLBuiltinDataset
|
||||
from dgl.data.utils import _get_dgl_url
|
||||
|
||||
class CitationGraphDataset(DGLBuiltinDataset):
|
||||
_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):
|
||||
assert name.lower() in ['cora', 'citeseer', 'pubmed']
|
||||
if name.lower() == 'cora':
|
||||
name = 'cora_v2'
|
||||
url = _get_dgl_url(self._urls[name])
|
||||
super(CitationGraphDataset, self).__init__(name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose)
|
||||
|
||||
def process(self):
|
||||
# Skip some processing code
|
||||
# === data processing skipped ===
|
||||
|
||||
# build graph
|
||||
g = dgl.graph(graph)
|
||||
# splitting masks
|
||||
g.ndata['train_mask'] = train_mask
|
||||
g.ndata['val_mask'] = val_mask
|
||||
g.ndata['test_mask'] = test_mask
|
||||
# node labels
|
||||
g.ndata['label'] = torch.tensor(labels)
|
||||
# node features
|
||||
g.ndata['feat'] = torch.tensor(_preprocess_features(features),
|
||||
dtype=F.data_type_dict['float32'])
|
||||
self._num_tasks = onehot_labels.shape[1]
|
||||
self._labels = labels
|
||||
# reorder graph to obtain better locality.
|
||||
self._g = dgl.reorder_graph(g)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
return self._g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
For brevity, this section skips some code in ``process()`` to highlight the key
|
||||
part for processing node classification dataset: splitting masks. Node
|
||||
features and node labels are stored in ``g.ndata``. For detailed
|
||||
implementation, please refer to `CitationGraphDataset source
|
||||
code <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/citation_graph.html#CitationGraphDataset>`__.
|
||||
|
||||
Note that the implementations of ``__getitem__(idx)`` and
|
||||
``__len__()`` are changed as well, since there is often only one graph
|
||||
for node classification tasks. The masks are ``bool tensors`` in PyTorch
|
||||
and TensorFlow, and ``float tensors`` in MXNet.
|
||||
|
||||
The section uses a subclass of ``CitationGraphDataset``, :class:`dgl.data.CiteseerGraphDataset`,
|
||||
to show the usage of it:
|
||||
|
||||
.. code::
|
||||
|
||||
# load data
|
||||
dataset = CiteseerGraphDataset(raw_dir='')
|
||||
graph = dataset[0]
|
||||
|
||||
# get split masks
|
||||
train_mask = graph.ndata['train_mask']
|
||||
val_mask = graph.ndata['val_mask']
|
||||
test_mask = graph.ndata['test_mask']
|
||||
|
||||
# get node features
|
||||
feats = graph.ndata['feat']
|
||||
|
||||
# get labels
|
||||
labels = graph.ndata['label']
|
||||
|
||||
A complete guide for training node classification models can be found in
|
||||
:ref:`guide-training-node-classification`.
|
||||
|
||||
For more examples of node classification datasets, please refer to DGL's
|
||||
builtin datasets:
|
||||
|
||||
* :ref:`citationdata`
|
||||
|
||||
* :ref:`corafulldata`
|
||||
|
||||
* :ref:`amazoncobuydata`
|
||||
|
||||
* :ref:`coauthordata`
|
||||
|
||||
* :ref:`karateclubdata`
|
||||
|
||||
* :ref:`ppidata`
|
||||
|
||||
* :ref:`redditdata`
|
||||
|
||||
* :ref:`sbmdata`
|
||||
|
||||
* :ref:`sstdata`
|
||||
|
||||
* :ref:`rdfdata`
|
||||
|
||||
Processing dataset for Link Prediction datasets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The processing of link prediction datasets is similar to that for node
|
||||
classification’s, there is often one graph in the dataset.
|
||||
|
||||
The section uses builtin dataset
|
||||
`KnowledgeGraphDataset <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/knowledge_graph.html#KnowledgeGraphDataset>`__
|
||||
as an example, and still skips the detailed data processing code to
|
||||
highlight the key part for processing link prediction datasets:
|
||||
|
||||
.. code::
|
||||
|
||||
# Example for creating Link Prediction datasets
|
||||
class KnowledgeGraphDataset(DGLBuiltinDataset):
|
||||
def __init__(self, name, reverse=True, raw_dir=None, force_reload=False, verbose=True):
|
||||
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)
|
||||
|
||||
def process(self):
|
||||
# Skip some processing code
|
||||
# === data processing skipped ===
|
||||
|
||||
# splitting mask
|
||||
g.edata['train_mask'] = train_mask
|
||||
g.edata['val_mask'] = val_mask
|
||||
g.edata['test_mask'] = test_mask
|
||||
# edge type
|
||||
g.edata['etype'] = etype
|
||||
# node type
|
||||
g.ndata['ntype'] = ntype
|
||||
self._g = g
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
return self._g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
As shown in the code, it adds splitting masks into ``edata`` field of the
|
||||
graph. Check `KnowledgeGraphDataset source
|
||||
code <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/knowledge_graph.html#KnowledgeGraphDataset>`__
|
||||
to see the complete code. The following code uses a subclass of ``KnowledgeGraphDataset``,
|
||||
:class:`dgl.data.FB15k237Dataset`, to show the usage of it:
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import FB15k237Dataset
|
||||
|
||||
# load data
|
||||
dataset = FB15k237Dataset()
|
||||
graph = dataset[0]
|
||||
|
||||
# get training mask
|
||||
train_mask = graph.edata['train_mask']
|
||||
train_idx = torch.nonzero(train_mask, as_tuple=False).squeeze()
|
||||
src, dst = graph.edges(train_idx)
|
||||
# get edge types in training set
|
||||
rel = graph.edata['etype'][train_idx]
|
||||
|
||||
|
||||
A complete guide for training link prediction models can be found in
|
||||
:ref:`guide-training-link-prediction`.
|
||||
|
||||
For more examples of link prediction datasets, please refer to DGL's
|
||||
builtin datasets:
|
||||
|
||||
* :ref:`kgdata`
|
||||
|
||||
* :ref:`bitcoinotcdata`
|
||||
@@ -0,0 +1,49 @@
|
||||
.. _guide-data-pipeline-savenload:
|
||||
|
||||
4.4 Save and load data
|
||||
----------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-data-pipeline-savenload>`
|
||||
|
||||
DGL recommends implementing saving and loading functions to cache the
|
||||
processed data in local disk. This saves a lot of data processing time
|
||||
in most cases. DGL provides four functions to make things simple:
|
||||
|
||||
- :func:`dgl.save_graphs` and :func:`dgl.load_graphs`: save/load DGLGraph objects and labels to/from local disk.
|
||||
- :func:`dgl.data.utils.save_info` and :func:`dgl.data.utils.load_info`: save/load useful information of the dataset (python ``dict`` object) to/from local disk.
|
||||
|
||||
The following example shows how to save and load a list of graphs and
|
||||
dataset information.
|
||||
|
||||
.. code::
|
||||
|
||||
import os
|
||||
from dgl import save_graphs, load_graphs
|
||||
from dgl.data.utils import makedirs, save_info, load_info
|
||||
|
||||
def save(self):
|
||||
# save graphs and labels
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
save_graphs(graph_path, self.graphs, {'labels': self.labels})
|
||||
# save other information in python dict
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
save_info(info_path, {'num_classes': self.num_classes})
|
||||
|
||||
def load(self):
|
||||
# load processed data from directory `self.save_path`
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
self.graphs, label_dict = load_graphs(graph_path)
|
||||
self.labels = label_dict['labels']
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
self.num_classes = load_info(info_path)['num_classes']
|
||||
|
||||
def has_cache(self):
|
||||
# check whether there are processed data in `self.save_path`
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
return os.path.exists(graph_path) and os.path.exists(info_path)
|
||||
|
||||
Note that there are cases not suitable to save processed data. For
|
||||
example, in the builtin dataset :class:`~dgl.data.GDELTDataset`,
|
||||
the processed data is quite large, so it’s more effective to process
|
||||
each data example in ``__getitem__(idx)``.
|
||||
@@ -0,0 +1,38 @@
|
||||
.. _guide-data-pipeline:
|
||||
|
||||
Chapter 4: Graph Data Pipeline
|
||||
==============================
|
||||
|
||||
:ref:`(中文版) <guide_cn-data-pipeline>`
|
||||
|
||||
DGL implements many commonly used graph datasets in :ref:`apidata`. They
|
||||
follow a standard pipeline defined in class :class:`dgl.data.DGLDataset`. DGL highly
|
||||
recommends processing graph data into a :class:`dgl.data.DGLDataset` subclass, as the
|
||||
pipeline provides simple and clean solution for loading, processing and
|
||||
saving graph data.
|
||||
|
||||
Roadmap
|
||||
-------
|
||||
|
||||
This chapter introduces how to create a custom DGL-Dataset.
|
||||
The following sections explain how the pipeline works, and
|
||||
shows how to implement each component of it.
|
||||
|
||||
* :ref:`guide-data-pipeline-dataset`
|
||||
* :ref:`guide-data-pipeline-download`
|
||||
* :ref:`guide-data-pipeline-process`
|
||||
* :ref:`guide-data-pipeline-savenload`
|
||||
* :ref:`guide-data-pipeline-loadogb`
|
||||
* :ref:`guide-data-pipeline-loadcsv`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
data-dataset
|
||||
data-download
|
||||
data-process
|
||||
data-savenload
|
||||
data-loadogb
|
||||
data-loadcsv
|
||||
@@ -0,0 +1,321 @@
|
||||
.. _guide-distributed-apis:
|
||||
|
||||
7.3 Programming APIs
|
||||
-----------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-distributed-apis>`
|
||||
|
||||
This section covers the core python components commonly used in a training script. DGL
|
||||
provides three distributed data structures and various APIs for initialization,
|
||||
distributed sampling and workload split.
|
||||
|
||||
* :class:`~dgl.distributed.DistGraph` for accessing structure and feature of a distributedly
|
||||
stored graph.
|
||||
* :class:`~dgl.distributed.DistTensor` for accessing node/edge feature tensor that
|
||||
is partitioned across machines.
|
||||
* :class:`~dgl.distributed.DistEmbedding` for accessing learnable node/edge embedding
|
||||
tensor that is partitioned across machines.
|
||||
|
||||
Initialization of the DGL distributed module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:func:`dgl.distributed.initialize` initializes the distributed module. If invoked
|
||||
by a trainer, this API creates sampler processes and builds connections with graph
|
||||
servers; if invoked by graph server, this API starts a service loop to listen to
|
||||
trainer/sampler requests. The API *must* be called before
|
||||
:func:`torch.distributed.init_process_group` and any other ``dgl.distributed`` APIs
|
||||
as shown in the order below:
|
||||
|
||||
.. code:: python
|
||||
|
||||
dgl.distributed.initialize('ip_config.txt')
|
||||
th.distributed.init_process_group(backend='gloo')
|
||||
|
||||
.. note::
|
||||
|
||||
If the training script contains user-defined functions (UDFs) that have to be invoked on
|
||||
the servers (see the section of DistTensor and DistEmbedding for more details), these UDFs have to
|
||||
be declared before :func:`~dgl.distributed.initialize`.
|
||||
|
||||
Distributed graph
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` is a Python class to access the graph
|
||||
structure and node/edge features in a cluster of machines. Each machine is
|
||||
responsible for one and only one partition. It loads the partition data (the
|
||||
graph structure and the node data and edge data in the partition) and makes it
|
||||
accessible to all trainers in the cluster. :class:`~dgl.distributed.DistGraph`
|
||||
provides a small subset of :class:`~dgl.DGLGraph` APIs for data access.
|
||||
|
||||
Distributed mode vs. standalone mode
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` can run in two modes: *distributed mode* and *standalone mode*.
|
||||
When a user executes a training script in a Python command line or Jupyter Notebook, it runs in
|
||||
a standalone mode. That is, it runs all computation in a single process and does not communicate
|
||||
with any other processes. Thus, the standalone mode requires the input graph to have only one partition.
|
||||
This mode is mainly used for development and testing (e.g., develop and run the code in Jupyter Notebook).
|
||||
When a user executes a training script with a launch script (see the section of launch script),
|
||||
:class:`~dgl.distributed.DistGraph` runs in the distributed mode. The launch tool starts servers
|
||||
(node/edge feature access and graph sampling) behind the scene and loads the partition data in
|
||||
each machine automatically. :class:`~dgl.distributed.DistGraph` connects with the servers in the cluster
|
||||
of machines and access them through the network.
|
||||
|
||||
DistGraph creation
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In the distributed mode, the creation of :class:`~dgl.distributed.DistGraph`
|
||||
requires the graph name given during graph partitioning. The graph name
|
||||
identifies the graph loaded in the cluster.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name')
|
||||
|
||||
When running in the standalone mode, it loads the graph data in the local
|
||||
machine. Therefore, users need to provide the partition configuration file,
|
||||
which contains all information about the input graph.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name', part_config='data/graph_name.json')
|
||||
|
||||
.. note::
|
||||
|
||||
DGL only allows one single ``DistGraph`` object. The behavior
|
||||
of destroying a DistGraph and creating a new one is undefined.
|
||||
|
||||
Accessing graph structure
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` provides a set of APIs to
|
||||
access the graph structure. Currently, most APIs provide graph information,
|
||||
such as the number of nodes and edges. The main use case of DistGraph is to run
|
||||
sampling APIs to support mini-batch training (see `Distributed sampling`_).
|
||||
|
||||
.. code:: python
|
||||
|
||||
print(g.num_nodes())
|
||||
|
||||
Access node/edge data
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Like :class:`~dgl.DGLGraph`, :class:`~dgl.distributed.DistGraph` provides ``ndata`` and ``edata``
|
||||
to access data in nodes and edges.
|
||||
The difference is that ``ndata``/``edata`` in :class:`~dgl.distributed.DistGraph` returns
|
||||
:class:`~dgl.distributed.DistTensor`, instead of the tensor of the underlying framework.
|
||||
Users can also assign a new :class:`~dgl.distributed.DistTensor` to
|
||||
:class:`~dgl.distributed.DistGraph` as node data or edge data.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g.ndata['train_mask'] # <dgl.distributed.dist_graph.DistTensor at 0x7fec820937b8>
|
||||
g.ndata['train_mask'][0] # tensor([1], dtype=torch.uint8)
|
||||
|
||||
Distributed Tensor
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As mentioned earlier, DGL shards node/edge features and stores them in a cluster of machines.
|
||||
DGL provides distributed tensors with a tensor-like interface to access the partitioned
|
||||
node/edge features in the cluster. In the distributed setting, DGL only supports dense node/edge
|
||||
features.
|
||||
|
||||
:class:`~dgl.distributed.DistTensor` manages the dense tensors partitioned and stored in
|
||||
multiple machines. Right now, a distributed tensor has to be associated with nodes or edges
|
||||
of a graph. In other words, the number of rows in a DistTensor has to be the same as the number
|
||||
of nodes or the number of edges in a graph. The following code creates a distributed tensor.
|
||||
In addition to the shape and dtype for the tensor, a user can also provide a unique tensor name.
|
||||
This name is useful if a user wants to reference a persistent distributed tensor (the one exists
|
||||
in the cluster even if the :class:`~dgl.distributed.DistTensor` object disappears).
|
||||
|
||||
.. code:: python
|
||||
|
||||
tensor = dgl.distributed.DistTensor((g.num_nodes(), 10), th.float32, name='test')
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`~dgl.distributed.DistTensor` creation is a synchronized operation. All trainers
|
||||
have to invoke the creation and the creation succeeds only when all trainers call it.
|
||||
|
||||
A user can add a :class:`~dgl.distributed.DistTensor` to a :class:`~dgl.distributed.DistGraph`
|
||||
object as one of the node data or edge data.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g.ndata['feat'] = tensor
|
||||
|
||||
.. note::
|
||||
|
||||
The node data name and the tensor name do not have to be the same. The former identifies
|
||||
node data from :class:`~dgl.distributed.DistGraph` (in the trainer process) while the latter
|
||||
identifies a distributed tensor in DGL servers.
|
||||
|
||||
:class:`~dgl.distributed.DistTensor` has the same APIs as
|
||||
regular tensors to access its metadata, such as the shape and dtype. It also
|
||||
supports indexed reads and writes but does not support
|
||||
computation operators, such as sum and mean.
|
||||
|
||||
.. code:: python
|
||||
|
||||
data = g.ndata['feat'][[1, 2, 3]]
|
||||
print(data)
|
||||
g.ndata['feat'][[3, 4, 5]] = data
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, DGL does not provide protection for concurrent writes from
|
||||
multiple trainers when a machine runs multiple servers. This may result in
|
||||
data corruption. One way to avoid concurrent writes to the same row of data
|
||||
is to run one server process on a machine.
|
||||
|
||||
Distributed DistEmbedding
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL provides :class:`~dgl.distributed.DistEmbedding` to support transductive models that require
|
||||
node embeddings. Creating distributed embeddings is very similar to creating distributed tensors.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def initializer(shape, dtype):
|
||||
arr = th.zeros(shape, dtype=dtype)
|
||||
arr.uniform_(-1, 1)
|
||||
return arr
|
||||
emb = dgl.distributed.DistEmbedding(g.num_nodes(), 10, init_func=initializer)
|
||||
|
||||
Internally, distributed embeddings are built on top of distributed tensors,
|
||||
and, thus, has very similar behaviors to distributed tensors. For example, when
|
||||
embeddings are created, they are sharded and stored across all machines in the
|
||||
cluster. It can be uniquely identified by a name.
|
||||
|
||||
.. note::
|
||||
|
||||
The initializer function is invoked in the server process. Therefore, it has to be
|
||||
declared before :class:`dgl.distributed.initialize`.
|
||||
|
||||
Because the embeddings are part of the model, a user has to attach them to an
|
||||
optimizer for mini-batch training. Currently, DGL provides a sparse Adagrad
|
||||
optimizer :class:`~dgl.distributed.SparseAdagrad` (DGL will add more optimizers
|
||||
for sparse embeddings later). Users need to collect all distributed embeddings
|
||||
from a model and pass them to the sparse optimizer. If a model has both node
|
||||
embeddings and regular dense model parameters and users want to perform sparse
|
||||
updates on the embeddings, they need to create two optimizers, one for node
|
||||
embeddings and the other for dense model parameters, as shown in the code
|
||||
below:
|
||||
|
||||
.. code:: python
|
||||
|
||||
sparse_optimizer = dgl.distributed.SparseAdagrad([emb], lr=lr1)
|
||||
optimizer = th.optim.Adam(model.parameters(), lr=lr2)
|
||||
feats = emb(nids)
|
||||
loss = model(feats)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
sparse_optimizer.step()
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`~dgl.distributed.DistEmbedding` does not inherit :class:`torch.nn.Module`,
|
||||
so we recommend using it outside of your own NN module.
|
||||
|
||||
Distributed sampling
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL provides two levels of APIs for sampling nodes and edges to generate
|
||||
mini-batches (see the section of mini-batch training). The low-level APIs
|
||||
require users to write code to explicitly define how a layer of nodes are
|
||||
sampled (e.g., using :func:`dgl.sampling.sample_neighbors` ). The high-level
|
||||
sampling APIs implement a few popular sampling algorithms for node
|
||||
classification and link prediction tasks (e.g.,
|
||||
:class:`~dgl.dataloading.NodeDataLoader` and
|
||||
:class:`~dgl.dataloading.EdgeDataLoader` ).
|
||||
|
||||
The distributed sampling module follows the same design and provides two levels
|
||||
of sampling APIs. For the lower-level sampling API, it provides
|
||||
:func:`~dgl.distributed.sample_neighbors` for distributed neighborhood sampling
|
||||
on :class:`~dgl.distributed.DistGraph`. In addition, DGL provides a distributed
|
||||
DataLoader (:class:`~dgl.distributed.DistDataLoader` ) for distributed
|
||||
sampling. The distributed DataLoader has the same interface as Pytorch
|
||||
DataLoader except that users cannot specify the number of worker processes when
|
||||
creating a dataloader. The worker processes are created in
|
||||
:func:`dgl.distributed.initialize`.
|
||||
|
||||
.. note::
|
||||
|
||||
When running :func:`dgl.distributed.sample_neighbors` on
|
||||
:class:`~dgl.distributed.DistGraph`, the sampler cannot run in Pytorch
|
||||
DataLoader with multiple worker processes. The main reason is that Pytorch
|
||||
DataLoader creates new sampling worker processes in every epoch, which
|
||||
leads to creating and destroying :class:`~dgl.distributed.DistGraph`
|
||||
objects many times.
|
||||
|
||||
When using the low-level API, the sampling code is similar to single-process sampling. The only
|
||||
difference is that users need to use :func:`dgl.distributed.sample_neighbors` and
|
||||
:class:`~dgl.distributed.DistDataLoader`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def sample_blocks(seeds):
|
||||
seeds = th.LongTensor(np.asarray(seeds))
|
||||
blocks = []
|
||||
for fanout in [10, 25]:
|
||||
frontier = dgl.distributed.sample_neighbors(g, seeds, fanout, replace=True)
|
||||
block = dgl.to_block(frontier, seeds)
|
||||
seeds = block.srcdata[dgl.NID]
|
||||
blocks.insert(0, block)
|
||||
return blocks
|
||||
dataloader = dgl.distributed.DistDataLoader(dataset=train_nid,
|
||||
batch_size=batch_size,
|
||||
collate_fn=sample_blocks,
|
||||
shuffle=True)
|
||||
for batch in dataloader:
|
||||
...
|
||||
|
||||
The high-level sampling APIs (:class:`~dgl.dataloading.NodeDataLoader` and
|
||||
:class:`~dgl.dataloading.EdgeDataLoader` ) has distributed counterparts
|
||||
(:class:`~dgl.distributed.DistNodeDataLoader` and
|
||||
:class:`~dgl.distributed.DistEdgeDataLoader`). The code is exactly the same as
|
||||
single-process sampling otherwise.
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.sampling.MultiLayerNeighborSampler([10, 25])
|
||||
dataloader = dgl.distributed.DistNodeDataLoader(g, train_nid, sampler,
|
||||
batch_size=batch_size, shuffle=True)
|
||||
for batch in dataloader:
|
||||
...
|
||||
|
||||
|
||||
Split workloads
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To train a model, users first need to split the dataset into training,
|
||||
validation and test sets. For distributed training, this step is usually done
|
||||
before we invoke :func:`dgl.distributed.partition_graph` to partition a graph.
|
||||
We recommend to store the data split in boolean arrays as node data or edge
|
||||
data. For node classification tasks, the length of these boolean arrays is the
|
||||
number of nodes in a graph and each of their elements indicates the existence
|
||||
of a node in a training/validation/test set. Similar boolean arrays should be
|
||||
used for link prediction tasks. :func:`dgl.distributed.partition_graph` splits
|
||||
these boolean arrays (because they are stored as the node data or edge data of
|
||||
the graph) based on the graph partitioning result and store them with graph
|
||||
partitions.
|
||||
|
||||
During distributed training, users need to assign training nodes/edges to each
|
||||
trainer. Similarly, we also need to split the validation and test set in the
|
||||
same way. DGL provides :func:`~dgl.distributed.node_split` and
|
||||
:func:`~dgl.distributed.edge_split` to split the training, validation and test
|
||||
set at runtime for distributed training. The two functions take the boolean
|
||||
arrays constructed before graph partitioning as input, split them and return a
|
||||
portion for the local trainer. By default, they ensure that all portions have
|
||||
the same number of nodes/edges. This is important for synchronous SGD, which
|
||||
assumes each trainer has the same number of mini-batches.
|
||||
|
||||
The example below splits the training set and returns a subset of nodes for the
|
||||
local process.
|
||||
|
||||
.. code:: python
|
||||
|
||||
train_nids = dgl.distributed.node_split(g.ndata['train_mask'])
|
||||
@@ -0,0 +1,189 @@
|
||||
.. _guide-distributed-hetero:
|
||||
|
||||
7.5 Heterogeneous Graph Under The Hood
|
||||
--------------------------------------------
|
||||
|
||||
The chapter covers the implementation details of distributed heterogeneous
|
||||
graph. They are transparent to users in most scenarios but could be useful
|
||||
for advanced customization.
|
||||
|
||||
In DGL, a node or edge in a heterogeneous graph has a unique ID in its own node
|
||||
type or edge type. Therefore, DGL can identify a node or an edge
|
||||
with a tuple: ``(node/edge type, type-wise ID)``. We call IDs of such form as
|
||||
**heterogeneous IDs**. To patition a heterogeneous graph for distributed training,
|
||||
DGL converts it to a homogeneous graph so that we can reuse the partitioning
|
||||
algorithms designed for homogeneous graphs. Each node/edge is thus uniquely mapped
|
||||
to an integer ID in a consecutive ID range (e.g., from 0 to the total number of
|
||||
nodes of all types). We call the IDs after conversion as **homogeneous IDs**.
|
||||
|
||||
Below is an illustration of the ID conversion process. Here, the graph has two
|
||||
types of nodes (:math:`T0` and :math:`T1` ), and four types of edges
|
||||
(:math:`R0`, :math:`R1`, :math:`R2`, :math:`R3` ). There are a total of 400
|
||||
nodes in the graph and each type has 200 nodes. Nodes of :math:`T0` have IDs in
|
||||
[0,200), while nodes of :math:`T1` have IDs in [200, 400). In this example, if
|
||||
we use a tuple to identify the nodes, nodes of :math:`T0` are identified as
|
||||
(T0, type-wise ID), where type-wise ID falls in [0, 200); nodes of :math:`T1`
|
||||
are identified as (T1, type-wise ID), where type-wise ID also falls in [0,
|
||||
200).
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/hetero/heterograph_ids.png
|
||||
:alt: Imgur
|
||||
|
||||
ID Conversion Utilities
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
During Preprocessing
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The steps of :ref:`Parallel Processing Pipeline <guide-distributed-preprocessing>`
|
||||
all use heterogeneous IDs for their inputs and outputs. Nevertheless, some steps such as
|
||||
ParMETIS partitioning are easier to be implemented using homogeneous IDs, thus
|
||||
requiring a utility to perform ID conversion.
|
||||
The code below implements a simple ``IDConverter`` using the metadata information
|
||||
in the metadata JSON from the chunked graph data format. It starts from some
|
||||
node type :math:`A` as node type 0, then assigns all its nodes with IDs
|
||||
in range :math:`[0, |V_A|-1)`. It then moves to the next node
|
||||
type B as node type 1 and assigns all its nodes with IDs in range
|
||||
:math:`[|V_A|, |V_A|+|V_B|-1)`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from bisect import bisect_left
|
||||
import numpy as np
|
||||
|
||||
class IDConverter:
|
||||
def __init__(self, meta):
|
||||
# meta is the JSON object loaded from metadata.json
|
||||
self.node_type = meta['node_type']
|
||||
self.edge_type = meta['edge_type']
|
||||
self.ntype2id_map = {ntype : i for i, ntype in enumerate(self.node_type)}
|
||||
self.etype2id_map = {etype : i for i, etype in enumerate(self.edge_type)}
|
||||
self.num_nodes = [sum(ns) for ns in meta['num_nodes_per_chunk']]
|
||||
self.num_edges = [sum(ns) for ns in meta['num_edges_per_chunk']]
|
||||
self.nid_offset = np.cumsum([0] + self.num_nodes)
|
||||
self.eid_offset = np.cumsum([0] + self.num_edges)
|
||||
|
||||
def ntype2id(self, ntype):
|
||||
"""From node type name to node type ID"""
|
||||
return self.ntype2id_map[ntype]
|
||||
|
||||
def etype2id(self, etype):
|
||||
"""From edge type name to edge type ID"""
|
||||
return self.etype2id_map[etype]
|
||||
|
||||
def id2ntype(self, id):
|
||||
"""From node type ID to node type name"""
|
||||
return self.node_type[id]
|
||||
|
||||
def id2etype(self, id):
|
||||
"""From edge type ID to edge type name"""
|
||||
return self.edge_type[id]
|
||||
|
||||
def nid_het2hom(self, ntype, id):
|
||||
"""From heterogeneous node ID to homogeneous node ID"""
|
||||
tid = self.ntype2id(ntype)
|
||||
if id < 0 or id >= self.num_nodes[tid]:
|
||||
raise ValueError(f'Invalid node ID of type {ntype}. Must be within range [0, {self.num_nodes[tid]})')
|
||||
return self.nid_offset[tid] + id
|
||||
|
||||
def nid_hom2het(self, id):
|
||||
"""From heterogeneous node ID to homogeneous node ID"""
|
||||
if id < 0 or id >= self.nid_offset[-1]:
|
||||
raise ValueError(f'Invalid homogeneous node ID. Must be within range [0, self.nid_offset[-1])')
|
||||
tid = bisect_left(self.nid_offset, id) - 1
|
||||
# Return a pair (node_type, type_wise_id)
|
||||
return self.id2ntype(tid), id - self.nid_offset[tid]
|
||||
|
||||
def eid_het2hom(self, etype, id):
|
||||
"""From heterogeneous edge ID to homogeneous edge ID"""
|
||||
tid = self.etype2id(etype)
|
||||
if id < 0 or id >= self.num_edges[tid]:
|
||||
raise ValueError(f'Invalid edge ID of type {etype}. Must be within range [0, {self.num_edges[tid]})')
|
||||
return self.eid_offset[tid] + id
|
||||
|
||||
def eid_hom2het(self, id):
|
||||
"""From heterogeneous edge ID to homogeneous edge ID"""
|
||||
if id < 0 or id >= self.eid_offset[-1]:
|
||||
raise ValueError(f'Invalid homogeneous edge ID. Must be within range [0, self.eid_offset[-1])')
|
||||
tid = bisect_left(self.eid_offset, id) - 1
|
||||
# Return a pair (edge_type, type_wise_id)
|
||||
return self.id2etype(tid), id - self.eid_offset[tid]
|
||||
|
||||
After Partition Loading
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
After the partitions are loaded into trainer or server processes, the loaded
|
||||
:class:`~dgl.distributed.GraphPartitionBook` provides utilities for conversion
|
||||
between homogeneous IDs and heterogeneous IDs.
|
||||
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_per_ntype`: convert a homogeneous node ID to type-wise ID and node type ID.
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_per_etype`: convert a homogeneous edge ID to type-wise ID and edge type ID.
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_homo_nid`: convert type-wise ID and node type to a homogeneous node ID.
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_homo_eid`: convert type-wise ID and edge type to a homogeneous edge ID.
|
||||
|
||||
Because all DGL's low-level :ref:`distributed graph sampling operators
|
||||
<api-distributed-sampling-ops>` use homogeneous IDs, DGL internally converts
|
||||
the heterogeneous IDs specified by users to homogeneous IDs before invoking
|
||||
sampling operators. Below shows an example of sampling a subgraph by
|
||||
:func:`~dgl.distributed.sample_neighbors` from nodes of type ``"paper"``. It
|
||||
first performs ID conversion, and after getting the sampled subgraph, converts
|
||||
the homogeneous node/edge IDs back to heterogeneous ones.
|
||||
|
||||
.. code:: python
|
||||
|
||||
gpb = g.get_partition_book()
|
||||
# We need to map the type-wise node IDs to homogeneous IDs.
|
||||
cur = gpb.map_to_homo_nid(seeds, 'paper')
|
||||
# For a heterogeneous input graph, the returned frontier is stored in
|
||||
# the homogeneous graph format.
|
||||
frontier = dgl.distributed.sample_neighbors(g, cur, fanout, replace=False)
|
||||
block = dgl.to_block(frontier, cur)
|
||||
cur = block.srcdata[dgl.NID]
|
||||
|
||||
block.edata[dgl.EID] = frontier.edata[dgl.EID]
|
||||
# Map the homogeneous edge Ids to their edge type.
|
||||
block.edata[dgl.ETYPE], block.edata[dgl.EID] = gpb.map_to_per_etype(block.edata[dgl.EID])
|
||||
# Map the homogeneous node Ids to their node types and per-type Ids.
|
||||
block.srcdata[dgl.NTYPE], block.srcdata[dgl.NID] = gpb.map_to_per_ntype(block.srcdata[dgl.NID])
|
||||
block.dstdata[dgl.NTYPE], block.dstdata[dgl.NID] = gpb.map_to_per_ntype(block.dstdata[dgl.NID])
|
||||
|
||||
Note that getting node/edge types from type IDs is simple -- just getting them
|
||||
from the ``ntypes`` attributes of a ``DistGraph``, i.e., ``g.ntypes[node_type_id]``.
|
||||
|
||||
Access distributed graph data
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The :class:`~dgl.distributed.DistGraph` class supports similar interface as
|
||||
:class:`~dgl.DGLGraph`. Below shows an example of getting the feature data of
|
||||
nodes 0, 10, 20 of type :math:`T0`. When accessing data in
|
||||
:class:`~dgl.distributed.DistGraph`, a user needs to use type-wise IDs and
|
||||
corresponding node types or edge types.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name', part_config='data/graph_name.json')
|
||||
feat = g.nodes['T0'].data['feat'][[0, 10, 20]]
|
||||
|
||||
A user can create distributed tensors and distributed embeddings for a
|
||||
particular node type or edge type. Distributed tensors and embeddings are split
|
||||
and stored in multiple machines. To create one, a user needs to specify how it
|
||||
is partitioned with :class:`~dgl.distributed.PartitionPolicy`. By default, DGL
|
||||
chooses the right partition policy based on the size of the first dimension.
|
||||
However, if multiple node types or edge types have the same number of nodes or
|
||||
edges, DGL cannot determine the partition policy automatically. A user needs to
|
||||
explicitly specify the partition policy. Below shows an example of creating a
|
||||
distributed tensor for node type :math:`T0` by using the partition policy for :math:`T0`
|
||||
and store it as node data of :math:`T0`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g.nodes['T0'].data['feat1'] = dgl.distributed.DistTensor(
|
||||
(g.num_nodes('T0'), 1), th.float32, 'feat1',
|
||||
part_policy=g.get_node_partition_policy('T0'))
|
||||
|
||||
The partition policies used for creating distributed tensors and embeddings are
|
||||
initialized when a heterogeneous graph is loaded into the graph server. A user
|
||||
cannot create a new partition policy at runtime. Therefore, a user can only
|
||||
create distributed tensors or embeddings for a node type or edge type.
|
||||
Accessing distributed tensors and embeddings also requires type-wise IDs.
|
||||
@@ -0,0 +1,258 @@
|
||||
.. _guide-distributed-partition:
|
||||
|
||||
7.4 Advanced Graph Partitioning
|
||||
---------------------------------------
|
||||
|
||||
The chapter covers some of the advanced topics for graph partitioning.
|
||||
|
||||
METIS partition algorithm
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
`METIS <http://glaros.dtc.umn.edu/gkhome/views/metis>`__ is a state-of-the-art
|
||||
graph partitioning algorithm that can generate partitions with minimal number
|
||||
of cross-partition edges, making it suitable for distributed message passing
|
||||
where the amount of network communication is proportional to the number of
|
||||
cross-partition edges. DGL has integrated METIS as the default partitioning
|
||||
algorithm in its :func:`dgl.distributed.partition_graph` API.
|
||||
|
||||
Output format
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Regardless of the partitioning algorithm in use, the partitioned results are stored
|
||||
in data files organized as follows:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
data_root_dir/
|
||||
|-- graph_name.json # partition configuration file in JSON
|
||||
|-- part0/ # data for partition 0
|
||||
| |-- node_feats.dgl # node features stored in binary format
|
||||
| |-- edge_feats.dgl # edge features stored in binary format
|
||||
| |-- graph.dgl # graph structure of this partition stored in binary format
|
||||
|
|
||||
|-- part1/ # data for partition 1
|
||||
| |-- node_feats.dgl
|
||||
| |-- edge_feats.dgl
|
||||
| |-- graph.dgl
|
||||
|
|
||||
|-- ... # data for other partitions
|
||||
|
||||
When distributed to a cluster, the metadata JSON should be copied to all the machines
|
||||
while the ``partX`` folders should be dispatched accordingly.
|
||||
|
||||
DGL provides a :func:`dgl.distributed.load_partition` function to load one partition
|
||||
for inspection.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> # load partition 0
|
||||
>>> part_data = dgl.distributed.load_partition('data_root_dir/graph_name.json', 0)
|
||||
>>> g, nfeat, efeat, partition_book, graph_name, ntypes, etypes = part_data # unpack
|
||||
>>> print(g)
|
||||
Graph(num_nodes=966043, num_edges=34270118,
|
||||
ndata_schemes={'orig_id': Scheme(shape=(), dtype=torch.int64),
|
||||
'part_id': Scheme(shape=(), dtype=torch.int64),
|
||||
'_ID': Scheme(shape=(), dtype=torch.int64),
|
||||
'inner_node': Scheme(shape=(), dtype=torch.int32)}
|
||||
edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64),
|
||||
'inner_edge': Scheme(shape=(), dtype=torch.int8),
|
||||
'orig_id': Scheme(shape=(), dtype=torch.int64)})
|
||||
|
||||
As mentioned in the `ID mapping`_ section, each partition carries auxiliary information
|
||||
saved as ndata or edata such as original node/edge IDs, partition IDs, etc. Each partition
|
||||
not only saves nodes/edges it owns, but also includes node/edges that are adjacent to
|
||||
the partition (called **HALO** nodes/edges). The ``inner_node`` and ``inner_edge``
|
||||
indicate whether a node/edge truely belongs to the partition (value is ``True``)
|
||||
or is a HALO node/edge (value is ``False``).
|
||||
|
||||
The :func:`~dgl.distributed.load_partition` function loads all data at once. Users can
|
||||
load features or the partition book using the :func:`dgl.distributed.load_partition_feats`
|
||||
and :func:`dgl.distributed.load_partition_book` APIs respectively.
|
||||
|
||||
|
||||
Parallel METIS partitioning
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For massive graphs where parallel preprocessing is desired, DGL supports
|
||||
`ParMETIS <http://glaros.dtc.umn.edu/gkhome/metis/parmetis/overview>`__ as one
|
||||
of the choices of partitioning algorithms.
|
||||
|
||||
.. note::
|
||||
|
||||
Because ParMETIS does not support heterogeneous graph, users need to
|
||||
conduct ID conversion before and after running ParMETIS.
|
||||
Check out chapter :ref:`guide-distributed-hetero` for explanation.
|
||||
|
||||
.. note::
|
||||
|
||||
Please make sure that the input graph to ParMETIS does not have
|
||||
duplicate edges (or parallel edges) and self-loop edges.
|
||||
|
||||
ParMETIS Installation
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
ParMETIS requires METIS and GKLib. Please follow the instructions `here
|
||||
<https://github.com/KarypisLab/GKlib>`__ to compile and install GKLib. For
|
||||
compiling and install METIS, please follow the instructions below to clone
|
||||
METIS with GIT and compile it with int64 support.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/KarypisLab/METIS.git
|
||||
make config shared=1 cc=gcc prefix=~/local i64=1
|
||||
make install
|
||||
|
||||
|
||||
For now, we need to compile and install ParMETIS manually. We clone the DGL branch of ParMETIS as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone --branch dgl https://github.com/KarypisLab/ParMETIS.git
|
||||
|
||||
Then compile and install ParMETIS.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
make config cc=mpicc prefix=~/local
|
||||
make install
|
||||
|
||||
Before running ParMETIS, we need to set two environment variables: ``PATH`` and ``LD_LIBRARY_PATH``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export PATH=$PATH:$HOME/local/bin
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/local/lib/
|
||||
|
||||
Input format
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. note::
|
||||
|
||||
As a prerequisite, read chapter :doc:`guide-distributed-hetero` to understand
|
||||
how DGL organize heterogeneous graph for distributed training.
|
||||
|
||||
The input graph for ParMETIS is stored in three files with the following names:
|
||||
``xxx_nodes.txt``, ``xxx_edges.txt`` and ``xxx_stats.txt``, where ``xxx`` is a
|
||||
graph name.
|
||||
|
||||
Each row in ``xxx_nodes.txt`` stores the information of a node. Row ID is
|
||||
also the *homogeneous* ID of a node, e.g., row 0 is for node 0; row 1 is for
|
||||
node 1, etc. Each row has the following format:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<node_type_id> <node_weight_list> <type_wise_node_id>
|
||||
|
||||
All fields are separated by whitespace:
|
||||
|
||||
* ``<node_type_id>`` is an integer starting from 0. Each node type is mapped to
|
||||
an integer. For a homogeneous graph, its value is always 0.
|
||||
* ``<node_weight_list>`` are integers (separated by whitespace) that indicate
|
||||
the node weights used by ParMETIS to balance graph partitions. For homogeneous
|
||||
graphs, the list has only one integer while for heterogeneous graphs with
|
||||
:math:`T` node types, the list should has :math:`T` integers. If the node
|
||||
belongs to node type :math:`t`, then all the integers except the :math:`t^{th}`
|
||||
one are zero; the :math:`t^{th}` integer is the weight of that node. ParMETIS
|
||||
will try to balance the total node weight of each partition. For heterogeneous
|
||||
graph, it will try to distribute nodes of the same type to all partitions.
|
||||
The recommended node weights are 1 for balancing the number of nodes in each
|
||||
partition or node degrees for balancing the number of edges in each partition.
|
||||
* ``<type_wise_node_id>`` is an integer representing the node ID in its own type.
|
||||
|
||||
Below shows an example of a node file for a heterogeneous graph with two node
|
||||
types. Node type 0 has three nodes; node type 1 has four nodes. It uses two
|
||||
node weights to ensure that ParMETIS will generate partitions with roughly the
|
||||
same number of nodes for type 0 and the same number of nodes for type 1.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
0 1 0 0
|
||||
0 1 0 1
|
||||
0 1 0 2
|
||||
1 0 1 0
|
||||
1 0 1 1
|
||||
1 0 1 2
|
||||
1 0 1 3
|
||||
|
||||
Similarly, each row in ``xxx_edges.txt`` stores the information of an edge. Row ID is
|
||||
also the *homogeneous* ID of an edge, e.g., row 0 is for edge 0; row 1 is for
|
||||
edge 1, etc. Each row has the following format:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<src_node_id> <dst_node_id> <type_wise_edge_id> <edge_type_id>
|
||||
|
||||
All fields are separated by whitespace:
|
||||
|
||||
* ``<src_node_id>`` is the *homogeneous* ID of the source node.
|
||||
* ``<dst_node_id>`` is the *homogeneous* ID of the destination node.
|
||||
* ``<type_wise_edge_id>`` is the edge ID for the edge type.
|
||||
* ``<edge_type_id>`` is an integer starting from 0. Each edge type is mapped to
|
||||
an integer. For a homogeneous graph, its value is always 0.
|
||||
|
||||
``xxx_stats.txt`` stores some basic statistics of the graph. It has only one line with three fields
|
||||
separated by whitespace:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<num_nodes> <num_edges> <total_node_weights>
|
||||
|
||||
* ``num_nodes`` stores the total number of nodes regardless of node types.
|
||||
* ``num_edges`` stores the total number of edges regardless of edge types.
|
||||
* ``total_node_weights`` stores the number of node weights in the node file.
|
||||
|
||||
Run ParMETIS and output format
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
ParMETIS contains a command called ``pm_dglpart``, which loads the graph stored
|
||||
in the three files from the machine where ``pm_dglpart`` is invoked, distributes
|
||||
data to all machines in the cluster and invokes ParMETIS to partition the
|
||||
graph. When it completes, it generates three files for each partition:
|
||||
``p<part_id>-xxx_nodes.txt``, ``p<part_id>-xxx_edges.txt``,
|
||||
``p<part_id>-xxx_stats.txt``.
|
||||
|
||||
.. note::
|
||||
|
||||
ParMETIS reassigns IDs to nodes during the partitioning. After ID reassignment,
|
||||
the nodes in a partition are assigned with contiguous IDs; furthermore, the nodes of
|
||||
the same type are assigned with contiguous IDs.
|
||||
|
||||
``p<part_id>-xxx_nodes.txt`` stores the node data of the partition. Each row represents
|
||||
a node with the following fields:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<node_id> <node_type_id> <node_weight_list> <type_wise_node_id>
|
||||
|
||||
* ``<node_id>`` is the *homogeneous* node ID after ID reassignment.
|
||||
* ``<node_type_id>`` is the node type ID.
|
||||
* ``<node_weight_list>`` is the node weight used by ParMETIS (copied from the input file).
|
||||
* ``<type_wise_node_id>`` is an integer representing the node ID in its own type.
|
||||
|
||||
``p<part_id>-xxx_edges.txt`` stores the edge data of the partition. Each row represents
|
||||
an edge with the following fields:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<src_id> <dst_id> <orig_src_id> <orig_dst_id> <type_wise_edge_id> <edge_type_id>
|
||||
|
||||
* ``<src_id>`` is the *homogeneous* ID of the source node after ID reassignment.
|
||||
* ``<dst_id>`` is the *homogeneous* ID of the destination node after ID reassignment.
|
||||
* ``<orig_src_id>`` is the *homogeneous* ID of the source node in the input graph.
|
||||
* ``<orig_dst_id>`` is the *homogeneous* ID of the destination node in the input graph.
|
||||
* ``<type_wise_edge_id>`` is the edge ID in its own type.
|
||||
* ``<edge_type_id>`` is the edge type ID.
|
||||
|
||||
When invoking ``pm_dglpart``, the three input files: ``xxx_nodes.txt``,
|
||||
``xxx_edges.txt``, ``xxx_stats.txt`` should be located in the directory where
|
||||
``pm_dglpart`` runs. The following command run four ParMETIS processes to
|
||||
partition the graph named ``xxx`` into eight partitions (each process handles
|
||||
two partitions).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
mpirun -np 4 pm_dglpart xxx 2
|
||||
|
||||
The output files from ParMETIS then need to be converted to the
|
||||
:ref:`partition assignment format <guide-distributed-prep-partition>` to in
|
||||
order to run subsequent preprocessing steps.
|
||||
@@ -0,0 +1,507 @@
|
||||
.. _guide-distributed-preprocessing:
|
||||
|
||||
7.1 Data Preprocessing
|
||||
------------------------------------------
|
||||
|
||||
Before launching training jobs, DGL requires the input data to be partitioned
|
||||
and distributed to the target machines. In order to handle different scales
|
||||
of graphs, DGL provides 2 partitioning approaches:
|
||||
|
||||
* A partitioning API for graphs that can fit in a single machine memory.
|
||||
* A distributed partition pipeline for graphs beyond a single machine capacity.
|
||||
|
||||
7.1.1 Partitioning API
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
For relatively small graphs, DGL provides a partitioning API
|
||||
:func:`~dgl.distributed.partition_graph` that partitions
|
||||
an in-memory :class:`~dgl.DGLGraph` object. It supports
|
||||
multiple partitioning algorithms such as random partitioning and
|
||||
`Metis <http://glaros.dtc.umn.edu/gkhome/views/metis>`__.
|
||||
The benefit of Metis partitioning is that it can generate partitions with
|
||||
minimal edge cuts to reduce network communication for distributed training and
|
||||
inference. DGL uses the latest version of Metis with the options optimized for
|
||||
the real-world graphs with power-law distribution. After partitioning, the API
|
||||
constructs the partitioned results in a format that is easy to load during the
|
||||
training. For example,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import dgl
|
||||
|
||||
g = ... # create or load a DGLGraph object
|
||||
dgl.distributed.partition_graph(g, 'mygraph', 2, 'data_root_dir')
|
||||
|
||||
will outputs the following data file.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
data_root_dir/
|
||||
|-- mygraph.json # metadata JSON. File name is the given graph name.
|
||||
|-- part0/ # data for partition 0
|
||||
| |-- node_feats.dgl # node features stored in binary format
|
||||
| |-- edge_feats.dgl # edge features stored in binary format
|
||||
| |-- graph.dgl # graph structure of this partition stored in binary format
|
||||
|
|
||||
|-- part1/ # data for partition 1
|
||||
|-- node_feats.dgl
|
||||
|-- edge_feats.dgl
|
||||
|-- graph.dgl
|
||||
|
||||
Chapter :ref:`guide-distributed-partition` covers more details about the
|
||||
partition format. To distribute the partitions to a cluster, users can either save
|
||||
the data in some shared folder accessible by all machines, or copy the metadata
|
||||
JSON as well as the corresponding partition folder ``partX`` to the X^th machine.
|
||||
|
||||
Using :func:`~dgl.distributed.partition_graph` requires an instance with large enough
|
||||
CPU RAM to hold the entire graph structure and features, which may not be viable for
|
||||
graphs with hundreds of billions of edges or large features. We describe how to use
|
||||
the *parallel data preparation pipeline* for such cases next.
|
||||
|
||||
Load balancing
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
When partitioning a graph, by default, METIS only balances the number of nodes
|
||||
in each partition. This can result in suboptimal configuration, depending on
|
||||
the task at hand. For example, in the case of semi-supervised node
|
||||
classification, a trainer performs computation on a subset of labeled nodes in
|
||||
a local partition. A partitioning that only balances nodes in a graph (both
|
||||
labeled and unlabeled), may end up with computational load imbalance. To get a
|
||||
balanced workload in each partition, the partition API allows balancing between
|
||||
partitions with respect to the number of nodes in each node type, by specifying
|
||||
``balance_ntypes`` in :func:`~dgl.distributed.partition_graph`. Users can take
|
||||
advantage of this and consider nodes in the training set, validation set and
|
||||
test set are of different node types.
|
||||
|
||||
The following example considers nodes inside the training set and outside the
|
||||
training set are two types of nodes:
|
||||
|
||||
.. code:: python
|
||||
|
||||
dgl.distributed.partition_graph(g, 'graph_name', 4, '/tmp/test', balance_ntypes=g.ndata['train_mask'])
|
||||
|
||||
In addition to balancing the node types,
|
||||
:func:`dgl.distributed.partition_graph` also allows balancing between
|
||||
in-degrees of nodes of different node types by specifying ``balance_edges``.
|
||||
This balances the number of edges incident to the nodes of different types.
|
||||
|
||||
ID mapping
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
After partitioning, :func:`~dgl.distributed.partition_graph` remap node
|
||||
and edge IDs so that nodes of the same partition are aranged together
|
||||
(in a consecutive ID range), making it easier to store partitioned node/edge
|
||||
features. The API also automatically shuffles the node/edge features
|
||||
according to the new IDs. However, some downstream tasks may want to
|
||||
recover the original node/edge IDs (such as extracting the computed node
|
||||
embeddings for later use). For such cases, pass ``return_mapping=True``
|
||||
to :func:`~dgl.distributed.partition_graph`, which makes the API returns
|
||||
the ID mappings between the remapped node/edge IDs and their origianl ones.
|
||||
For a homogeneous graph, it returns two vectors. The first vector maps every new
|
||||
node ID to its original ID; the second vector maps every new edge ID to
|
||||
its original ID. For a heterogeneous graph, it returns two dictionaries of
|
||||
vectors. The first dictionary contains the mapping for each node type; the
|
||||
second dictionary contains the mapping for each edge type.
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_map, edge_map = dgl.distributed.partition_graph(g, 'graph_name', 4, '/tmp/test',
|
||||
balance_ntypes=g.ndata['train_mask'],
|
||||
return_mapping=True)
|
||||
# Let's assume that node_emb is saved from the distributed training.
|
||||
orig_node_emb = th.zeros(node_emb.shape, dtype=node_emb.dtype)
|
||||
orig_node_emb[node_map] = node_emb
|
||||
|
||||
|
||||
Load partitioned graphs
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DGL provides a :func:`dgl.distributed.load_partition` function to load one partition
|
||||
for inspection.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import dgl
|
||||
>>> # load partition 0
|
||||
>>> part_data = dgl.distributed.load_partition('data_root_dir/graph_name.json', 0)
|
||||
>>> g, nfeat, efeat, partition_book, graph_name, ntypes, etypes = part_data # unpack
|
||||
>>> print(g)
|
||||
Graph(num_nodes=966043, num_edges=34270118,
|
||||
ndata_schemes={'orig_id': Scheme(shape=(), dtype=torch.int64),
|
||||
'part_id': Scheme(shape=(), dtype=torch.int64),
|
||||
'_ID': Scheme(shape=(), dtype=torch.int64),
|
||||
'inner_node': Scheme(shape=(), dtype=torch.int32)}
|
||||
edata_schemes={'_ID': Scheme(shape=(), dtype=torch.int64),
|
||||
'inner_edge': Scheme(shape=(), dtype=torch.int8),
|
||||
'orig_id': Scheme(shape=(), dtype=torch.int64)})
|
||||
|
||||
As mentioned in the `ID mapping`_ section, each partition carries auxiliary information
|
||||
saved as ndata or edata such as original node/edge IDs, partition IDs, etc. Each partition
|
||||
not only saves nodes/edges it owns, but also includes node/edges that are adjacent to
|
||||
the partition (called **HALO** nodes/edges). The ``inner_node`` and ``inner_edge``
|
||||
indicate whether a node/edge truely belongs to the partition (value is ``True``)
|
||||
or is a HALO node/edge (value is ``False``).
|
||||
|
||||
The :func:`~dgl.distributed.load_partition` function loads all data at once. Users can
|
||||
load features or the partition book using the :func:`dgl.distributed.load_partition_feats`
|
||||
and :func:`dgl.distributed.load_partition_book` APIs respectively.
|
||||
|
||||
|
||||
7.1.2 Distributed Graph Partitioning Pipeline
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To handle massive graph data that cannot fit in the CPU RAM of a
|
||||
single machine, DGL utilizes data chunking and parallel processing to reduce
|
||||
memory footprint and running time. The figure below illustrates the
|
||||
pipeline:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_7_distdataprep.png
|
||||
|
||||
* The pipeline takes input data stored in *Chunked Graph Format* and
|
||||
produces and dispatches data partitions to the target machines.
|
||||
* **Step.1 Graph Partitioning:** It calculates the ownership of each partition
|
||||
and saves the results as a set of files called *partition assignment*.
|
||||
To speedup the step, some algorithms (e.g., ParMETIS) support parallel computing
|
||||
using multiple machines.
|
||||
* **Step.2 Data Dispatching:** Given the partition assignment, the step then
|
||||
physically partitions the graph data and dispatches them to the machines user
|
||||
specified. It also converts the graph data into formats that are suitable for
|
||||
distributed training and evaluation.
|
||||
|
||||
The whole pipeline is modularized so that each step can be invoked
|
||||
individually. For example, users can replace Step.1 with some custom graph partition
|
||||
algorithm as long as it produces partition assignment files
|
||||
correctly.
|
||||
|
||||
.. _guide-distributed-prep-chunk:
|
||||
Chunked Graph Format
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To run the pipeline, DGL requires the input graph to be stored in multiple data
|
||||
chunks. Each data chunk is the unit of data preprocessing and thus should fit
|
||||
into CPU RAM. In this section, we use the MAG240M-LSC data from `Open Graph
|
||||
Benchmark <https://ogb.stanford.edu/docs/lsc/mag240m/>`__ as an example to
|
||||
describe the overall design, followed by a formal specification and
|
||||
tips for creating data in such format.
|
||||
|
||||
Example: MAG240M-LSC
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The MAG240M-LSC graph is a heterogeneous academic graph
|
||||
extracted from the Microsoft Academic Graph (MAG), whose schema diagram is
|
||||
illustrated below:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_7_mag240m.png
|
||||
|
||||
Its raw data files are organized as follows:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
/mydata/MAG240M-LSC/
|
||||
|-- meta.pt # # A dictionary of the number of nodes for each type saved by torch.save,
|
||||
| # as well as num_classes
|
||||
|-- processed/
|
||||
|-- author___affiliated_with___institution/
|
||||
| |-- edge_index.npy # graph, 713 MB
|
||||
|
|
||||
|-- paper/
|
||||
| |-- node_feat.npy # feature, 187 GB, (numpy memmap format)
|
||||
| |-- node_label.npy # label, 974 MB
|
||||
| |-- node_year.npy # year, 974 MB
|
||||
|
|
||||
|-- paper___cites___paper/
|
||||
| |-- edge_index.npy # graph, 21 GB
|
||||
|
|
||||
|-- author___writes___paper/
|
||||
|-- edge_index.npy # graph, 6GB
|
||||
|
||||
The graph has three node types (``"paper"``, ``"author"`` and ``"institution"``),
|
||||
three edge types/relations (``"cites"``, ``"writes"`` and ``"affiliated_with"``). The
|
||||
``"paper"`` nodes have three attributes (``"feat"``, ``"label"``, ``"year"'``), while
|
||||
other types of nodes and edges are featureless. Below shows the data files when
|
||||
it is stored in DGL Chunked Graph Format:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
/mydata/MAG240M-LSC_chunked/
|
||||
|-- metadata.json # metadata json file
|
||||
|-- edges/ # stores edge ID data
|
||||
| |-- writes-part1.csv
|
||||
| |-- writes-part2.csv
|
||||
| |-- affiliated_with-part1.csv
|
||||
| |-- affiliated_with-part2.csv
|
||||
| |-- cites-part1.csv
|
||||
| |-- cites-part1.csv
|
||||
|
|
||||
|-- node_data/ # stores node feature data
|
||||
|-- paper-feat-part1.npy
|
||||
|-- paper-feat-part2.npy
|
||||
|-- paper-label-part1.npy
|
||||
|-- paper-label-part2.npy
|
||||
|-- paper-year-part1.npy
|
||||
|-- paper-year-part2.npy
|
||||
|
||||
All the data files are chunked into two parts, including the edges of each relation
|
||||
(e.g., writes, affiliates, cites) and node features. If the graph has edge features,
|
||||
they will be chunked into multiple files too. All ID data are stored in
|
||||
CSV (we will illustrate the contents soon) while node features are stored in
|
||||
numpy arrays.
|
||||
|
||||
The ``metadata.json`` stores all the metadata information such as file names
|
||||
and chunk sizes (e.g., number of nodes, number of edges).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"graph_name" : "MAG240M-LSC", # given graph name
|
||||
"node_type": ["author", "paper", "institution"],
|
||||
"num_nodes_per_chunk": [
|
||||
[61191556, 61191556], # number of author nodes per chunk
|
||||
[61191553, 61191552], # number of paper nodes per chunk
|
||||
[12861, 12860] # number of institution nodes per chunk
|
||||
],
|
||||
# The edge type name is a colon-joined string of source, edge, and destination type.
|
||||
"edge_type": [
|
||||
"author:writes:paper",
|
||||
"author:affiliated_with:institution",
|
||||
"paper:cites:paper"
|
||||
],
|
||||
"num_edges_per_chunk": [
|
||||
[193011360, 193011360], # number of author:writes:paper edges per chunk
|
||||
[22296293, 22296293], # number of author:affiliated_with:institution edges per chunk
|
||||
[648874463, 648874463] # number of paper:cites:paper edges per chunk
|
||||
],
|
||||
"edges" : {
|
||||
"author:writes:paper" : { # edge type
|
||||
"format" : {"name": "csv", "delimiter": " "},
|
||||
# The list of paths. Can be relative or absolute.
|
||||
"data" : ["edges/writes-part1.csv", "edges/writes-part2.csv"]
|
||||
},
|
||||
"author:affiliated_with:institution" : {
|
||||
"format" : {"name": "csv", "delimiter": " "},
|
||||
"data" : ["edges/affiliated_with-part1.csv", "edges/affiliated_with-part2.csv"]
|
||||
},
|
||||
"paper:cites:paper" : {
|
||||
"format" : {"name": "csv", "delimiter": " "},
|
||||
"data" : ["edges/cites-part1.csv", "edges/cites-part2.csv"]
|
||||
}
|
||||
},
|
||||
"node_data" : {
|
||||
"paper": { # node type
|
||||
"feat": { # feature key
|
||||
"format": {"name": "numpy"},
|
||||
"data": ["node_data/paper-feat-part1.npy", "node_data/paper-feat-part2.npy"]
|
||||
},
|
||||
"label": { # feature key
|
||||
"format": {"name": "numpy"},
|
||||
"data": ["node_data/paper-label-part1.npy", "node_data/paper-label-part2.npy"]
|
||||
},
|
||||
"year": { # feature key
|
||||
"format": {"name": "numpy"},
|
||||
"data": ["node_data/paper-year-part1.npy", "node_data/paper-year-part2.npy"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"edge_data" : {} # MAG240M-LSC does not have edge features
|
||||
}
|
||||
|
||||
There are three parts in ``metadata.json``:
|
||||
|
||||
* Graph schema information and chunk sizes, e.g., ``"node_type"`` , ``"num_nodes_per_chunk"``, etc.
|
||||
* Edge index data under key ``"edges"``.
|
||||
* Node/edge feature data under keys ``"node_data"`` and ``"edge_data"``.
|
||||
|
||||
The edge index files contain edges in the form of node ID pairs:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# writes-part1.csv
|
||||
0 0
|
||||
0 1
|
||||
0 20
|
||||
0 29
|
||||
0 1203
|
||||
...
|
||||
|
||||
Specification
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In general, a chunked graph data folder just needs a ``metadata.json`` and a
|
||||
bunch of data files. The folder structure in the MAG240M-LSC example is not a
|
||||
strict requirement as long as ``metadata.json`` contains valid file paths.
|
||||
|
||||
``metadata.json`` top-level keys:
|
||||
|
||||
* ``graph_name``: String. Unique name used by :class:`dgl.distributed.DistGraph`
|
||||
to load graph.
|
||||
* ``node_type``: List of string. Node type names.
|
||||
* ``num_nodes_per_chunk``: List of list of integer. For graphs with :math:`T` node
|
||||
types stored in :math:`P` chunks, the value contains :math:`T` integer lists.
|
||||
Each list contains :math:`P` integers, which specify the number of nodes
|
||||
in each chunk.
|
||||
* ``edge_type``: List of string. Edge type names in the form of
|
||||
``<source node type>:<relation>:<destination node type>``.
|
||||
* ``num_edges_per_chunk``: List of list of integer. For graphs with :math:`R` edge
|
||||
types stored in :math:`P` chunks, the value contains :math:`R` integer lists.
|
||||
Each list contains :math:`P` integers, which specify the number of edges
|
||||
in each chunk.
|
||||
* ``edges``: Dict of ``ChunkFileSpec``. Edge index files.
|
||||
Dictionary keys are edge type names in the form of
|
||||
``<source node type>:<relation>:<destination node type>``.
|
||||
* ``node_data``: Dict of ``ChunkFileSpec``. Data files that store node attributes
|
||||
could have arbitrary number of files regardless of ``num_parts``. Dictionary
|
||||
keys are node type names.
|
||||
* ``edge_data``: Dict of ``ChunkFileSpec``. Data files that store edge attributes
|
||||
could have arbitrary number of files regardless of ``num_parts``. Dictionary
|
||||
keys are edge type names in the form of
|
||||
``<source node type>:<relation>:<destination node type>``.
|
||||
|
||||
``ChunkFileSpec`` has two keys:
|
||||
|
||||
* ``format``: File format. Depending on the format ``name``, users can configure more
|
||||
details about how to parse each data file.
|
||||
- ``"csv"``: CSV file. Use the ``delimiter`` key to specify delimiter in use.
|
||||
- ``"numpy"``: NumPy array binary file created by :func:`numpy.save`.
|
||||
- ``"parquet"``: parquet table binary file created by :func:`pyarrow.parquet.write_table`.
|
||||
* ``data``: List of string. File path to each data chunk. Support absolute path.
|
||||
|
||||
Tips for making chunked graph data
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Depending on the raw data, the implementation could include:
|
||||
|
||||
* Construct graphs out of non-structured data such as texts or tabular data.
|
||||
* Augment or transform the input graph struture or features. E.g., adding reverse
|
||||
or self-loop edges, normalizing features, etc.
|
||||
* Chunk the input graph structure and features into multiple data files so that
|
||||
each one can fit in CPU RAM for subsequent preprocessing steps.
|
||||
|
||||
To avoid running into out-of-memory error, it is recommended to process graph
|
||||
structures and feature data separately. Processing one chunk at a time can also
|
||||
reduce the maximal runtime memory footprint. As an example, DGL provides a
|
||||
`tools/chunk_graph.py
|
||||
<https://github.com/dmlc/dgl/blob/master/tools/chunk_graph.py>`_ script that
|
||||
chunks an in-memory feature-less :class:`~dgl.DGLGraph` and feature tensors
|
||||
stored in :class:`numpy.memmap`.
|
||||
|
||||
|
||||
.. _guide-distributed-prep-partition:
|
||||
Step.1 Graph Partitioning
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This step reads the chunked graph data and calculates which partition each node
|
||||
should belong to. The results are saved in a set of *partition assignment files*.
|
||||
For example, to randomly partition MAG240M-LSC to two parts, run the
|
||||
``partition_algo/random_partition.py`` script in the ``tools`` folder:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python /my/repo/dgl/tools/partition_algo/random_partition.py
|
||||
--in_dir /mydata/MAG240M-LSC_chunked
|
||||
--out_dir /mydata/MAG240M-LSC_2parts
|
||||
--num_partitions 2
|
||||
|
||||
, which outputs files as follows:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
MAG240M-LSC_2parts/
|
||||
|-- paper.txt
|
||||
|-- author.txt
|
||||
|-- institution.txt
|
||||
|
||||
Each file stores the partition assignment of the corresponding node type.
|
||||
The contents are the partition ID of each node stored in lines, i.e., line i is
|
||||
the partition ID of node i.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# paper.txt
|
||||
0
|
||||
1
|
||||
1
|
||||
0
|
||||
0
|
||||
1
|
||||
0
|
||||
...
|
||||
|
||||
Despite its simplicity, random partitioning may result in frequent
|
||||
cross-machine communication. Check out chapter
|
||||
:ref:`guide-distributed-partition` for more advanced options.
|
||||
|
||||
Step.2 Data Dispatching
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DGL provides a ``dispatch_data.py`` script to physically partition the data and
|
||||
dispatch partitions to each training machines. It will also convert the data
|
||||
once again to data objects that can be loaded by DGL training processes
|
||||
efficiently. The entire step can be further accelerated using multi-processing.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python /myrepo/dgl/tools/dispatch_data.py \
|
||||
--in-dir /mydata/MAG240M-LSC_chunked/ \
|
||||
--partitions-dir /mydata/MAG240M-LSC_2parts/ \
|
||||
--out-dir data/MAG_LSC_partitioned \
|
||||
--ip-config ip_config.txt
|
||||
|
||||
* ``--in-dir`` specifies the path to the folder of the input chunked graph data produced
|
||||
* ``--partitions-dir`` specifies the path to the partition assignment folder produced by Step.1.
|
||||
* ``--out-dir`` specifies the path to stored the data partition on each machine.
|
||||
* ``--ip-config`` specifies the IP configuration file of the cluster.
|
||||
|
||||
An example IP configuration file is as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
172.31.19.1
|
||||
172.31.23.205
|
||||
|
||||
As a counterpart of ``return_mapping=True`` in :func:`~dgl.distributed.partition_graph`, the
|
||||
:ref:`distributed partitioning pipeline <guide-distributed-preprocessing>`
|
||||
provides two arguments in ``dispatch_data.py`` to save the original node/edge IDs to disk.
|
||||
|
||||
* ``--save-orig-nids`` save original node IDs into files.
|
||||
* ``--save-orig-eids`` save original edge IDs into files.
|
||||
|
||||
Specifying the two options will create two files ``orig_nids.dgl`` and ``orig_eids.dgl``
|
||||
under each partition folder.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
data_root_dir/
|
||||
|-- graph_name.json # partition configuration file in JSON
|
||||
|-- part0/ # data for partition 0
|
||||
| |-- orig_nids.dgl # original node IDs
|
||||
| |-- orig_eids.dgl # original edge IDs
|
||||
| |-- ... # other data such as graph and node/edge feats
|
||||
|
|
||||
|-- part1/ # data for partition 1
|
||||
| |-- orig_nids.dgl
|
||||
| |-- orig_eids.dgl
|
||||
| |-- ...
|
||||
|
|
||||
|-- ... # data for other partitions
|
||||
|
||||
The two files store the original IDs as a dictionary of tensors, where keys are node/edge
|
||||
type names and values are ID tensors. Users can use the :func:`dgl.data.load_tensors`
|
||||
utility to load them:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Load the original IDs for the nodes in partition 0.
|
||||
orig_nids_0 = dgl.data.load_tensors('/path/to/data/part0/orig_nids.dgl')
|
||||
# Get the original node IDs for node type 'user'
|
||||
user_orig_nids_0 = orig_nids_0['user']
|
||||
|
||||
# Load the original IDs for the edges in partition 0.
|
||||
orig_eids_0 = dgl.data.load_tensors('/path/to/data/part0/orig_eids.dgl')
|
||||
# Get the original edge IDs for edge type 'like'
|
||||
like_orig_eids_0 = orig_nids_0['like']
|
||||
|
||||
During data dispatching, DGL assumes that the combined CPU RAM of the cluster
|
||||
is able to hold the entire graph data. Node ownership is determined by the result
|
||||
of partitioning algorithm where as for edges the owner of the destination node
|
||||
also owns the edge as well.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
.. _guide-distributed-tools:
|
||||
|
||||
7.2 Tools for launching distributed training/inference
|
||||
------------------------------------------------------
|
||||
|
||||
DGL provides a launching script ``launch.py`` under
|
||||
`dgl/tools <https://github.com/dmlc/dgl/tree/master/tools>`__ to launch a distributed
|
||||
training job in a cluster. This script makes the following assumptions:
|
||||
|
||||
* The partitioned data and the training script have been provisioned to the cluster or
|
||||
a shared storage (e.g., NFS) accessible to all the worker machines.
|
||||
* The machine that invokes ``launch.py`` has passwordless ssh access
|
||||
to all other machines. The launching machine must be one of the worker machines.
|
||||
|
||||
Below shows an example of launching a distributed training job in a cluster.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python3 tools/launch.py \
|
||||
--workspace /my/workspace/ \
|
||||
--num_trainers 2 \
|
||||
--num_samplers 4 \
|
||||
--num_servers 1 \
|
||||
--part_config data/mygraph.json \
|
||||
--ip_config ip_config.txt \
|
||||
"python3 my_train_script.py"
|
||||
|
||||
The argument specifies the workspace path, where to find the partition metadata JSON
|
||||
and machine IP configurations, how many trainer, sampler, and server processes to be launched
|
||||
on each machine. The last argument is the command to launch which is usually the
|
||||
model training/evaluation script.
|
||||
|
||||
Each line of ``ip_config.txt`` is the IP address of a machine in the cluster.
|
||||
Optionally, the IP address can be followed by a network port (default is ``30050``).
|
||||
A typical example is as follows:
|
||||
|
||||
.. code:: none
|
||||
|
||||
172.31.19.1
|
||||
172.31.23.205
|
||||
172.31.29.175
|
||||
172.31.16.98
|
||||
|
||||
The workspace specified in the launch script is the working directory in the
|
||||
machines, which contains the training script, the IP configuration file, the
|
||||
partition configuration file as well as the graph partitions. All paths of the
|
||||
files should be specified as relative paths to the workspace.
|
||||
|
||||
The launch script creates a specified number of training jobs
|
||||
(``--num_trainers``) on each machine. In addition, users need to specify the
|
||||
number of sampler processes for each trainer (``--num_samplers``).
|
||||
@@ -0,0 +1,123 @@
|
||||
.. _guide-distributed:
|
||||
|
||||
Chapter 7: Distributed Training
|
||||
=====================================
|
||||
|
||||
:ref:`(中文版) <guide_cn-distributed>`
|
||||
|
||||
.. note::
|
||||
|
||||
Distributed training is only available for PyTorch backend.
|
||||
|
||||
DGL adopts a fully distributed approach that distributes both data and computation
|
||||
across a collection of computation resources. In the context of this section, we
|
||||
will assume a cluster setting (i.e., a group of machines). DGL partitions a graph
|
||||
into subgraphs and each machine in a cluster is responsible for one subgraph (partition).
|
||||
DGL runs an identical training script on all machines in the cluster to parallelize
|
||||
the computation and runs servers on the same machines to serve partitioned data to the trainers.
|
||||
|
||||
For the training script, DGL provides distributed APIs that are similar to the ones for
|
||||
mini-batch training. This makes distributed training require only small code modifications
|
||||
from mini-batch training on a single machine. Below shows an example of training GraphSage
|
||||
in a distributed fashion. The notable code modifications are:
|
||||
1) initialization of DGL's distributed module, 2) create a distributed graph object, and
|
||||
3) split the training set and calculate the nodes for the local process.
|
||||
The rest of the code, including sampler creation, model definition, training loops
|
||||
are the same as :ref:`mini-batch training <guide-minibatch>`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
from dgl.dataloading import NeighborSampler
|
||||
from dgl.distributed import DistGraph, DistDataLoader, node_split
|
||||
import torch as th
|
||||
|
||||
# initialize distributed contexts
|
||||
dgl.distributed.initialize('ip_config.txt')
|
||||
th.distributed.init_process_group(backend='gloo')
|
||||
# load distributed graph
|
||||
g = DistGraph('graph_name', 'part_config.json')
|
||||
pb = g.get_partition_book()
|
||||
# get training workload, i.e., training node IDs
|
||||
train_nid = node_split(g.ndata['train_mask'], pb, force_even=True)
|
||||
|
||||
|
||||
# Create sampler
|
||||
sampler = NeighborSampler(g, [10,25],
|
||||
dgl.distributed.sample_neighbors,
|
||||
device)
|
||||
|
||||
dataloader = DistDataLoader(
|
||||
dataset=train_nid.numpy(),
|
||||
batch_size=batch_size,
|
||||
collate_fn=sampler.sample_blocks,
|
||||
shuffle=True,
|
||||
drop_last=False)
|
||||
|
||||
# Define model and optimizer
|
||||
model = SAGE(in_feats, num_hidden, n_classes, num_layers, F.relu, dropout)
|
||||
model = th.nn.parallel.DistributedDataParallel(model)
|
||||
loss_fcn = nn.CrossEntropyLoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=args.lr)
|
||||
|
||||
# training loop
|
||||
for epoch in range(args.num_epochs):
|
||||
with model.join():
|
||||
for step, blocks in enumerate(dataloader):
|
||||
batch_inputs, batch_labels = load_subtensor(g, blocks[0].srcdata[dgl.NID],
|
||||
blocks[-1].dstdata[dgl.NID])
|
||||
batch_pred = model(blocks, batch_inputs)
|
||||
loss = loss_fcn(batch_pred, batch_labels)
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
DGL implements a few distributed components to support distributed training. The figure below
|
||||
shows the components and their interactions.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/distributed.png
|
||||
:alt: Imgur
|
||||
|
||||
Specifically, DGL's distributed training has three types of interacting processes:
|
||||
*server*, *sampler* and *trainer*.
|
||||
|
||||
* **Servers** store graph partitions which includes both structure data and node/edge
|
||||
features. They provide services such as sampling, getting or updating node/edge
|
||||
features. Note that each machine may run multiple server processes simultaneously
|
||||
to increase service throughput. One of them is *main server* in charge of data
|
||||
loading and sharing data via shared memory with *backup servers* that provide
|
||||
services.
|
||||
* **Sampler processes** interact with the servers and sample nodes and edges to
|
||||
generate mini-batches for training.
|
||||
* **Trainers** are in charge of training networks on mini-batches. They utilize
|
||||
APIs such as :class:`~dgl.distributed.DistGraph` to access partitioned graph data,
|
||||
:class:`~dgl.distributed.DistEmbedding` and :class:`~dgl.distributed.DistTensor` to access
|
||||
node/edge features/embeddings and :class:`~dgl.distributed.DistDataLoader` to interact
|
||||
with samplers to get mini-batches. Trainers communicate gradients among each other
|
||||
using PyTorch's native ``DistributedDataParallel`` paradigm.
|
||||
|
||||
Besides Python APIs, DGL also provides `tools <https://github.com/dmlc/dgl/tree/master/tools>`__
|
||||
for provisioning graph data and processes to the entire cluster.
|
||||
|
||||
Having the distributed components in mind, the rest of the section will cover
|
||||
the following distributed components:
|
||||
|
||||
* :ref:`guide-distributed-preprocessing`
|
||||
* :ref:`guide-distributed-tools`
|
||||
* :ref:`guide-distributed-apis`
|
||||
|
||||
For more advanced users who are interested in more details:
|
||||
|
||||
* :ref:`guide-distributed-partition`
|
||||
* :ref:`guide-distributed-hetero`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
distributed-preprocessing
|
||||
distributed-tools
|
||||
distributed-apis
|
||||
distributed-partition
|
||||
distributed-hetero
|
||||
@@ -0,0 +1,36 @@
|
||||
.. _guide-graph-basic:
|
||||
|
||||
1.1 Some Basic Definitions about Graphs (Graphs 101)
|
||||
----------------------------------------------------
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph-basic>`
|
||||
|
||||
A graph :math:`G=(V, E)` is a structure used to represent entities and their relations. It consists of
|
||||
two sets -- the set of nodes :math:`V` (also called vertices) and the set of edges :math:`E` (also called
|
||||
arcs). An edge :math:`(u, v) \in E` connecting a pair of nodes :math:`u` and :math:`v` indicates that there is a
|
||||
relation between them. The relation can either be undirected, e.g., capturing symmetric
|
||||
relations between nodes, or directed, capturing asymmetric relations. For example, if a
|
||||
graph is used to model the friendships relations of people in a social network, then the edges
|
||||
will be undirected as friendship is mutual; however, if the graph is used to model how people
|
||||
follow each other on Twitter, then the edges are directed. Depending on the edges'
|
||||
directionality, a graph can be *directed* or *undirected*.
|
||||
|
||||
Graphs can be *weighted* or *unweighted*. In a weighted graph, each edge is associated with a
|
||||
scalar weight. For example, such weights might represent lengths or connectivity strengths.
|
||||
|
||||
Graphs can also be either *homogeneous* or *heterogeneous*. In a homogeneous graph, all
|
||||
the nodes represent instances of the same type and all the edges represent relations of the
|
||||
same type. For instance, a social network is a graph consisting of people and their
|
||||
connections, representing the same entity type.
|
||||
|
||||
In contrast, in a heterogeneous graph, the nodes and edges can be of different types. For
|
||||
instance, the graph encoding a marketplace will have buyer, seller, and product nodes that
|
||||
are connected via wants-to-buy, has-bought, is-customer-of, and is-selling edges. The
|
||||
bipartite graph is a special, commonly-used type of heterogeneous graph, where edges
|
||||
exist between nodes of two different types. For example, in a recommender system, one can
|
||||
use a bipartite graph to represent the interactions between users and items. For working
|
||||
with heterogeneous graphs in DGL, see :ref:`guide-graph-heterogeneous`.
|
||||
|
||||
Multigraphs are graphs that can have multiple (directed) edges between the same pair of nodes,
|
||||
including self loops. For instance, two authors can coauthor a paper in different years,
|
||||
resulting in edges with different features.
|
||||
@@ -0,0 +1,117 @@
|
||||
.. _guide-graph-external:
|
||||
|
||||
1.4 Creating Graphs from External Sources
|
||||
-----------------------------------------
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph-external>`
|
||||
|
||||
The options to construct a :class:`~dgl.DGLGraph` from external sources include:
|
||||
|
||||
- Conversion from external python libraries for graphs and sparse matrices (NetworkX and SciPy).
|
||||
- Loading graphs from disk.
|
||||
|
||||
The section does not cover functions that generate graphs by transforming from other
|
||||
graphs. See the API reference manual for an overview of them.
|
||||
|
||||
Creating Graphs from External Libraries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The following code snippet is an example for creating a graph from a SciPy sparse matrix and a NetworkX graph.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> import scipy.sparse as sp
|
||||
>>> spmat = sp.rand(100, 100, density=0.05) # 5% nonzero entries
|
||||
>>> dgl.from_scipy(spmat) # from SciPy
|
||||
Graph(num_nodes=100, num_edges=500,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
>>> import networkx as nx
|
||||
>>> nx_g = nx.path_graph(5) # a chain 0-1-2-3-4
|
||||
>>> dgl.from_networkx(nx_g) # from networkx
|
||||
Graph(num_nodes=5, num_edges=8,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
Note that when constructing from the `nx.path_graph(5)`, the resulting :class:`~dgl.DGLGraph` has 8
|
||||
edges instead of 4. This is because `nx.path_graph(5)` constructs an undirected NetworkX graph
|
||||
:class:`networkx.Graph` while a :class:`~dgl.DGLGraph` is always directed. In converting an undirected
|
||||
NetworkX graph into a :class:`~dgl.DGLGraph`, DGL internally converts undirected edges to two directed edges.
|
||||
Using directed NetworkX graphs :class:`networkx.DiGraph` can avoid such behavior.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> nxg = nx.DiGraph([(2, 1), (1, 2), (2, 3), (0, 0)])
|
||||
>>> dgl.from_networkx(nxg)
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
.. note::
|
||||
|
||||
DGL internally converts SciPy matrices and NetworkX graphs to tensors to construct graphs.
|
||||
Hence, these construction methods are not meant for performance critical parts.
|
||||
|
||||
See APIs: :func:`dgl.from_scipy`, :func:`dgl.from_networkx`.
|
||||
|
||||
Loading Graphs from Disk
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are many data formats for storing graphs and it isn't possible to enumerate every option.
|
||||
Thus, this section only gives some general pointers on certain common ones.
|
||||
|
||||
Comma Separated Values (CSV)
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
One very common format is CSV, which stores nodes, edges, and their features in a tabular format:
|
||||
|
||||
.. table:: nodes.csv
|
||||
|
||||
+-----------+
|
||||
|age, title |
|
||||
+===========+
|
||||
|43, 1 |
|
||||
+-----------+
|
||||
|23, 3 |
|
||||
+-----------+
|
||||
|... |
|
||||
+-----------+
|
||||
|
||||
.. table:: edges.csv
|
||||
|
||||
+-----------------+
|
||||
|src, dst, weight |
|
||||
+=================+
|
||||
|0, 1, 0.4 |
|
||||
+-----------------+
|
||||
|0, 3, 0.9 |
|
||||
+-----------------+
|
||||
|... |
|
||||
+-----------------+
|
||||
|
||||
There are known Python libraries (e.g. pandas) for loading this type of data into python
|
||||
objects (e.g., :class:`numpy.ndarray`), which can then be used to construct a DGLGraph. If the
|
||||
backend framework also provides utilities to save/load tensors from disk (e.g., :func:`torch.save`,
|
||||
:func:`torch.load`), one can follow the same principle to build a graph.
|
||||
|
||||
See also: `Tutorial for loading a Karate Club Network from edge pairs CSV <https://github.com/dglai/WWW20-Hands-on-Tutorial/blob/master/basic_tasks/1_load_data.ipynb>`_.
|
||||
|
||||
JSON/GML Format
|
||||
"""""""""""""""
|
||||
|
||||
Though not particularly fast, NetworkX provides many utilities to parse
|
||||
`a variety of data formats <https://networkx.github.io/documentation/stable/reference/readwrite/index.html>`_
|
||||
which indirectly allows DGL to create graphs from these sources.
|
||||
|
||||
DGL Binary Format
|
||||
"""""""""""""""""
|
||||
|
||||
DGL provides APIs to save and load graphs from disk stored in binary format. Apart from the
|
||||
graph structure, the APIs also handle feature data and graph-level label data. DGL also
|
||||
supports checkpointing graphs directly to S3 or HDFS. The reference manual provides more
|
||||
details about the usage.
|
||||
|
||||
See APIs: :func:`dgl.save_graphs`, :func:`dgl.load_graphs`.
|
||||
@@ -0,0 +1,65 @@
|
||||
.. _guide-graph-feature:
|
||||
|
||||
1.3 Node and Edge Features
|
||||
--------------------------
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph-feature>`
|
||||
|
||||
The nodes and edges of a :class:`~dgl.DGLGraph` can have several user-defined named features for
|
||||
storing graph-specific properties of the nodes and edges. These features can be accessed
|
||||
via the :py:attr:`~dgl.DGLGraph.ndata` and :py:attr:`~dgl.DGLGraph.edata` interface. For example, the following code creates two node
|
||||
features (named ``'x'`` and ``'y'`` in line 8 and 15) and one edge feature (named ``'x'`` in line 9).
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> g = dgl.graph(([0, 0, 1, 5], [1, 2, 2, 0])) # 6 nodes, 4 edges
|
||||
>>> g
|
||||
Graph(num_nodes=6, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
>>> g.ndata['x'] = th.ones(g.num_nodes(), 3) # node feature of length 3
|
||||
>>> g.edata['x'] = th.ones(g.num_edges(), dtype=th.int32) # scalar integer feature
|
||||
>>> g
|
||||
Graph(num_nodes=6, num_edges=4,
|
||||
ndata_schemes={'x' : Scheme(shape=(3,), dtype=torch.float32)}
|
||||
edata_schemes={'x' : Scheme(shape=(,), dtype=torch.int32)})
|
||||
>>> # different names can have different shapes
|
||||
>>> g.ndata['y'] = th.randn(g.num_nodes(), 5)
|
||||
>>> g.ndata['x'][1] # get node 1's feature
|
||||
tensor([1., 1., 1.])
|
||||
>>> g.edata['x'][th.tensor([0, 3])] # get features of edge 0 and 3
|
||||
tensor([1, 1], dtype=torch.int32)
|
||||
|
||||
Important facts about the :py:attr:`~dgl.DGLGraph.ndata`/:py:attr:`~dgl.DGLGraph.edata` interface:
|
||||
|
||||
- Only features of numerical types (e.g., float, double, and int) are allowed. They can
|
||||
be scalars, vectors or multi-dimensional tensors.
|
||||
- Each node feature has a unique name and each edge feature has a unique name.
|
||||
The features of nodes and edges can have the same name. (e.g., 'x' in the above example).
|
||||
- A feature is created via tensor assignment, which assigns a feature to each
|
||||
node/edge in the graph. The leading dimension of that tensor must be equal to the
|
||||
number of nodes/edges in the graph. You cannot assign a feature to a subset of the
|
||||
nodes/edges in the graph.
|
||||
- Features of the same name must have the same dimensionality and data type.
|
||||
- The feature tensor is in row-major layout -- each row-slice stores the feature of one
|
||||
node or edge (e.g., see lines 16 and 18 in the above example).
|
||||
|
||||
For weighted graphs, one can store the weights as an edge feature as below.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> # edges 0->1, 0->2, 0->3, 1->3
|
||||
>>> edges = th.tensor([0, 0, 0, 1]), th.tensor([1, 2, 3, 3])
|
||||
>>> weights = th.tensor([0.1, 0.6, 0.9, 0.7]) # weight of each edge
|
||||
>>> g = dgl.graph(edges)
|
||||
>>> g.edata['w'] = weights # give it a name 'w'
|
||||
>>> g
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={'w' : Scheme(shape=(,), dtype=torch.float32)})
|
||||
|
||||
|
||||
See APIs: :py:attr:`~dgl.DGLGraph.ndata`, :py:attr:`~dgl.DGLGraph.edata`.
|
||||
@@ -0,0 +1,47 @@
|
||||
.. _guide-graph-gpu:
|
||||
|
||||
1.6 Using DGLGraph on a GPU
|
||||
---------------------------
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph-gpu>`
|
||||
|
||||
One can create a :class:`~dgl.DGLGraph` on a GPU by passing two GPU tensors during construction.
|
||||
Another approach is to use the :func:`~dgl.DGLGraph.to` API to copy a :class:`~dgl.DGLGraph` to a GPU, which
|
||||
copies the graph structure as well as the feature data to the given device.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> u, v = th.tensor([0, 1, 2]), th.tensor([2, 3, 4])
|
||||
>>> g = dgl.graph((u, v))
|
||||
>>> g.ndata['x'] = th.randn(5, 3) # original feature is on CPU
|
||||
>>> g.device
|
||||
device(type='cpu')
|
||||
>>> cuda_g = g.to('cuda:0') # accepts any device objects from backend framework
|
||||
>>> cuda_g.device
|
||||
device(type='cuda', index=0)
|
||||
>>> cuda_g.ndata['x'].device # feature data is copied to GPU too
|
||||
device(type='cuda', index=0)
|
||||
|
||||
>>> # A graph constructed from GPU tensors is also on GPU
|
||||
>>> u, v = u.to('cuda:0'), v.to('cuda:0')
|
||||
>>> g = dgl.graph((u, v))
|
||||
>>> g.device
|
||||
device(type='cuda', index=0)
|
||||
|
||||
Any operations involving a GPU graph are performed on a GPU. Thus, they require all
|
||||
tensor arguments to be placed on GPU already and the results (graph or tensor) will be on
|
||||
GPU too. Furthermore, a GPU graph only accepts feature data on a GPU.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> cuda_g.in_degrees()
|
||||
tensor([0, 0, 1, 1, 1], device='cuda:0')
|
||||
>>> cuda_g.in_edges([2, 3, 4]) # ok for non-tensor type arguments
|
||||
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
|
||||
>>> cuda_g.in_edges(th.tensor([2, 3, 4]).to('cuda:0')) # tensor type must be on GPU
|
||||
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
|
||||
>>> cuda_g.ndata['h'] = th.randn(5, 4) # ERROR! feature must be on GPU too!
|
||||
DGLError: Cannot assign node feature "h" on device cpu to a graph on device
|
||||
cuda:0. Call DGLGraph.to() to copy the graph to the same device.
|
||||
@@ -0,0 +1,96 @@
|
||||
.. _guide-graph-graphs-nodes-edges:
|
||||
|
||||
1.2 Graphs, Nodes, and Edges
|
||||
----------------------------
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph-graphs-nodes-edges>`
|
||||
|
||||
DGL represents each node by a unique integer, called its node ID, and each edge by a pair
|
||||
of integers corresponding to the IDs of its end nodes. DGL assigns to each edge a unique
|
||||
integer, called its **edge ID**, based on the order in which it was added to the graph. The
|
||||
numbering of node and edge IDs starts from 0. In DGL, all the edges are directed, and an
|
||||
edge :math:`(u, v)` indicates that the direction goes from node :math:`u` to node :math:`v`.
|
||||
|
||||
To specify multiple nodes, DGL uses a 1-D integer tensor (i.e., PyTorch's tensor,
|
||||
TensorFlow's Tensor, or MXNet's ndarray) of node IDs. DGL calls this format "node-tensors".
|
||||
To specify multiple edges, it uses a tuple of node-tensors :math:`(U, V)`. :math:`(U[i], V[i])`
|
||||
decides an edge from :math:`U[i]` to :math:`V[i]`.
|
||||
|
||||
One way to create a :class:`~dgl.DGLGraph` is to use the :func:`dgl.graph` method, which takes
|
||||
as input a set of edges. DGL also supports creating graphs from other data sources, see :ref:`guide-graph-external`.
|
||||
|
||||
The following code snippet uses the :func:`dgl.graph` method to create a :class:`~dgl.DGLGraph`
|
||||
corresponding to the four-node graph shown below and illustrates some of its APIs for
|
||||
querying the graph's structure.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/user_guide_graphch_1.png
|
||||
:height: 200px
|
||||
:width: 300px
|
||||
:align: center
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
|
||||
>>> # edges 0->1, 0->2, 0->3, 1->3
|
||||
>>> u, v = th.tensor([0, 0, 0, 1]), th.tensor([1, 2, 3, 3])
|
||||
>>> g = dgl.graph((u, v))
|
||||
>>> print(g) # number of nodes are inferred from the max node IDs in the given edges
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
>>> # Node IDs
|
||||
>>> print(g.nodes())
|
||||
tensor([0, 1, 2, 3])
|
||||
>>> # Edge end nodes
|
||||
>>> print(g.edges())
|
||||
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]))
|
||||
>>> # Edge end nodes and edge IDs
|
||||
>>> print(g.edges(form='all'))
|
||||
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]), tensor([0, 1, 2, 3]))
|
||||
|
||||
>>> # If the node with the largest ID is isolated (meaning no edges),
|
||||
>>> # then one needs to explicitly set the number of nodes
|
||||
>>> g = dgl.graph((u, v), num_nodes=8)
|
||||
|
||||
For an undirected graph, one needs to create edges for both directions. :func:`dgl.to_bidirected`
|
||||
can be helpful in this case, which converts a graph into a new one with edges for both directions.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> bg = dgl.to_bidirected(g)
|
||||
>>> bg.edges()
|
||||
(tensor([0, 0, 0, 1, 1, 2, 3, 3]), tensor([1, 2, 3, 0, 3, 0, 0, 1]))
|
||||
|
||||
.. note::
|
||||
|
||||
Tensor types are generally preferred throughout DGL APIs due to their efficient internal
|
||||
storage in C and explicit data type and device context information. However, most DGL APIs
|
||||
do support python iterable (e.g., list) or numpy.ndarray as arguments for quick prototyping.
|
||||
|
||||
DGL can use either :math:`32`- or :math:`64`-bit integers to store the node and edge IDs. The data types for
|
||||
the node and edge IDs should be the same. By using :math:`64` bits, DGL can handle graphs with
|
||||
up to :math:`2^{63} - 1` nodes or edges. However, if a graph contains less than :math:`2^{31} - 1` nodes or edges,
|
||||
one should use :math:`32`-bit integers as it leads to better speed and requires less memory.
|
||||
DGL provides methods for making such conversions. See below for an example.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> edges = th.tensor([2, 5, 3]), th.tensor([3, 5, 0]) # edges 2->3, 5->5, 3->0
|
||||
>>> g64 = dgl.graph(edges) # DGL uses int64 by default
|
||||
>>> print(g64.idtype)
|
||||
torch.int64
|
||||
>>> g32 = dgl.graph(edges, idtype=th.int32) # create a int32 graph
|
||||
>>> g32.idtype
|
||||
torch.int32
|
||||
>>> g64_2 = g32.long() # convert to int64
|
||||
>>> g64_2.idtype
|
||||
torch.int64
|
||||
>>> g32_2 = g64.int() # convert to int32
|
||||
>>> g32_2.idtype
|
||||
torch.int32
|
||||
|
||||
See APIs: :func:`dgl.graph`, :func:`dgl.DGLGraph.nodes`, :func:`dgl.DGLGraph.edges`, :func:`dgl.to_bidirected`,
|
||||
:func:`dgl.DGLGraph.int`, :func:`dgl.DGLGraph.long`, and :py:attr:`dgl.DGLGraph.idtype`.
|
||||
@@ -0,0 +1,282 @@
|
||||
.. _guide-graph-heterogeneous:
|
||||
|
||||
1.5 Heterogeneous Graphs
|
||||
------------------------
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph-heterogeneous>`
|
||||
|
||||
A heterogeneous graph can have nodes and edges of different types. Nodes/Edges of
|
||||
different types have independent ID space and feature storage. For example in the figure below, the
|
||||
user and game node IDs both start from zero and they have different features.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/user_guide_graphch_2.png
|
||||
|
||||
An example heterogeneous graph with two types of nodes (user and game) and two types of edges (follows and plays).
|
||||
|
||||
Creating a Heterogeneous Graph
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In DGL, a heterogeneous graph (heterograph for short) is specified with a series of graphs as below, one per
|
||||
relation. Each relation is a string triplet ``(source node type, edge type, destination node type)``.
|
||||
Since relations disambiguate the edge types, DGL calls them canonical edge types.
|
||||
|
||||
The following code snippet is an example for creating a heterogeneous graph in DGL.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
|
||||
>>> # Create a heterograph with 3 node types and 3 edges types.
|
||||
>>> graph_data = {
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
|
||||
... ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
|
||||
... }
|
||||
>>> g = dgl.heterograph(graph_data)
|
||||
>>> g.ntypes
|
||||
['disease', 'drug', 'gene']
|
||||
>>> g.etypes
|
||||
['interacts', 'interacts', 'treats']
|
||||
>>> g.canonical_etypes
|
||||
[('drug', 'interacts', 'drug'),
|
||||
('drug', 'interacts', 'gene'),
|
||||
('drug', 'treats', 'disease')]
|
||||
|
||||
Note that homogeneous and bipartite graphs are just special heterogeneous graphs with one
|
||||
relation.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # A homogeneous graph
|
||||
>>> dgl.heterograph({('node_type', 'edge_type', 'node_type'): (u, v)})
|
||||
>>> # A bipartite graph
|
||||
>>> dgl.heterograph({('source_type', 'edge_type', 'destination_type'): (u, v)})
|
||||
|
||||
The *metagraph* associated with a heterogeneous graph is the schema of the graph. It specifies
|
||||
type constraints on the sets of nodes and edges between the nodes. A node :math:`u` in a metagraph
|
||||
corresponds to a node type in the associated heterograph. An edge :math:`(u, v)` in a metagraph indicates that
|
||||
there are edges from nodes of type :math:`u` to nodes of type :math:`v` in the associated heterograph.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> g
|
||||
Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},
|
||||
num_edges={('drug', 'interacts', 'drug'): 2,
|
||||
('drug', 'interacts', 'gene'): 2,
|
||||
('drug', 'treats', 'disease'): 1},
|
||||
metagraph=[('drug', 'drug', 'interacts'),
|
||||
('drug', 'gene', 'interacts'),
|
||||
('drug', 'disease', 'treats')])
|
||||
>>> g.metagraph().edges()
|
||||
OutMultiEdgeDataView([('drug', 'drug'), ('drug', 'gene'), ('drug', 'disease')])
|
||||
|
||||
See APIs: :func:`dgl.heterograph`, :py:attr:`~dgl.DGLGraph.ntypes`, :py:attr:`~dgl.DGLGraph.etypes`,
|
||||
:py:attr:`~dgl.DGLGraph.canonical_etypes`, :py:attr:`~dgl.DGLGraph.metagraph`.
|
||||
|
||||
Working with Multiple Types
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When multiple node/edge types are introduced, users need to specify the particular
|
||||
node/edge type when invoking a DGLGraph API for type-specific information. In addition,
|
||||
nodes/edges of different types have separate IDs.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # Get the number of all nodes in the graph
|
||||
>>> g.num_nodes()
|
||||
10
|
||||
>>> # Get the number of drug nodes
|
||||
>>> g.num_nodes('drug')
|
||||
3
|
||||
>>> # Nodes of different types have separate IDs,
|
||||
>>> # hence not well-defined without a type specified
|
||||
>>> g.nodes()
|
||||
DGLError: Node type name must be specified if there are more than one node types.
|
||||
>>> g.nodes('drug')
|
||||
tensor([0, 1, 2])
|
||||
|
||||
To set/get features for a specific node/edge type, DGL provides two new types of syntax --
|
||||
`g.nodes['node_type'].data['feat_name']` and `g.edges['edge_type'].data['feat_name']`.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # Set/get feature 'hv' for nodes of type 'drug'
|
||||
>>> g.nodes['drug'].data['hv'] = th.ones(3, 1)
|
||||
>>> g.nodes['drug'].data['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.]])
|
||||
>>> # Set/get feature 'he' for edge of type 'treats'
|
||||
>>> g.edges['treats'].data['he'] = th.zeros(1, 1)
|
||||
>>> g.edges['treats'].data['he']
|
||||
tensor([[0.]])
|
||||
|
||||
If the graph only has one node/edge type, there is no need to specify the node/edge type.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> g = dgl.heterograph({
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'is similar', 'drug'): (th.tensor([0, 1]), th.tensor([2, 3]))
|
||||
... })
|
||||
>>> g.nodes()
|
||||
tensor([0, 1, 2, 3])
|
||||
>>> # To set/get feature with a single type, no need to use the new syntax
|
||||
>>> g.ndata['hv'] = th.ones(4, 1)
|
||||
|
||||
.. note::
|
||||
|
||||
When the edge type uniquely determines the types of source and destination nodes, one
|
||||
can just use one string instead of a string triplet to specify the edge type. For example, for a
|
||||
heterograph with two relations ``('user', 'plays', 'game')`` and ``('user', 'likes', 'game')``, it
|
||||
is safe to just use ``'plays'`` or ``'likes'`` to refer to the two relations.
|
||||
|
||||
Loading Heterographs from Disk
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Comma Separated Values (CSV)
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
A common way to store a heterograph is to store nodes and edges of different types in different CSV files.
|
||||
An example is as follows.
|
||||
|
||||
.. code::
|
||||
|
||||
# data folder
|
||||
data/
|
||||
|-- drug.csv # drug nodes
|
||||
|-- gene.csv # gene nodes
|
||||
|-- disease.csv # disease nodes
|
||||
|-- drug-interact-drug.csv # drug-drug interaction edges
|
||||
|-- drug-interact-gene.csv # drug-gene interaction edges
|
||||
|-- drug-treat-disease.csv # drug-treat-disease edges
|
||||
|
||||
Similar to the case of homogeneous graphs, one can use packages like Pandas to parse
|
||||
CSV files into numpy arrays or framework tensors, build a relation dictionary and
|
||||
construct a heterograph from that. The approach also applies to other popular formats like
|
||||
GML/JSON.
|
||||
|
||||
DGL Binary Format
|
||||
"""""""""""""""""
|
||||
|
||||
DGL provides :func:`dgl.save_graphs` and :func:`dgl.load_graphs` respectively for saving
|
||||
heterogeneous graphs in binary format and loading them from binary format.
|
||||
|
||||
Edge Type Subgraph
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
One can create a subgraph of a heterogeneous graph by specifying the relations to retain, with
|
||||
features copied if any.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> g = dgl.heterograph({
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
|
||||
... ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
|
||||
... })
|
||||
>>> g.nodes['drug'].data['hv'] = th.ones(3, 1)
|
||||
|
||||
>>> # Retain relations ('drug', 'interacts', 'drug') and ('drug', 'treats', 'disease')
|
||||
>>> # All nodes for 'drug' and 'disease' will be retained
|
||||
>>> eg = dgl.edge_type_subgraph(g, [('drug', 'interacts', 'drug'),
|
||||
... ('drug', 'treats', 'disease')])
|
||||
>>> eg
|
||||
Graph(num_nodes={'disease': 3, 'drug': 3},
|
||||
num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'treats', 'disease'): 1},
|
||||
metagraph=[('drug', 'drug', 'interacts'), ('drug', 'disease', 'treats')])
|
||||
>>> # The associated features will be copied as well
|
||||
>>> eg.nodes['drug'].data['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.]])
|
||||
|
||||
Converting Heterogeneous Graphs to Homogeneous Graphs
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Heterographs provide a clean interface for managing nodes/edges of different types and
|
||||
their associated features. This is particularly helpful when:
|
||||
|
||||
1. The features for nodes/edges of different types have different data types or sizes.
|
||||
2. We want to apply different operations to nodes/edges of different types.
|
||||
|
||||
If the above conditions do not hold and one does not want to distinguish node/edge types in
|
||||
modeling, then DGL allows converting a heterogeneous graph to a homogeneous graph with :func:`dgl.DGLGraph.to_homogeneous` API.
|
||||
It proceeds as follows:
|
||||
|
||||
1. Relabels nodes/edges of all types using consecutive integers starting from 0
|
||||
2. Merges the features across node/edge types specified by the user.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> g = dgl.heterograph({
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))})
|
||||
>>> g.nodes['drug'].data['hv'] = th.zeros(3, 1)
|
||||
>>> g.nodes['disease'].data['hv'] = th.ones(3, 1)
|
||||
>>> g.edges['interacts'].data['he'] = th.zeros(2, 1)
|
||||
>>> g.edges['treats'].data['he'] = th.zeros(1, 2)
|
||||
|
||||
>>> # By default, it does not merge any features
|
||||
>>> hg = dgl.to_homogeneous(g)
|
||||
>>> 'hv' in hg.ndata
|
||||
False
|
||||
|
||||
>>> # Copy edge features
|
||||
>>> # For feature copy, it expects features to have
|
||||
>>> # the same size and dtype across node/edge types
|
||||
>>> hg = dgl.to_homogeneous(g, edata=['he'])
|
||||
DGLError: Cannot concatenate column ‘he’ with shape Scheme(shape=(2,), dtype=torch.float32) and shape Scheme(shape=(1,), dtype=torch.float32)
|
||||
|
||||
>>> # Copy node features
|
||||
>>> hg = dgl.to_homogeneous(g, ndata=['hv'])
|
||||
>>> hg.ndata['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.],
|
||||
[0.],
|
||||
[0.],
|
||||
[0.]])
|
||||
|
||||
The original node/edge types and type-specific IDs are stored in :py:attr:`~dgl.DGLGraph.ndata` and :py:attr:`~dgl.DGLGraph.edata`.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # Order of node types in the heterograph
|
||||
>>> g.ntypes
|
||||
['disease', 'drug']
|
||||
>>> # Original node types
|
||||
>>> hg.ndata[dgl.NTYPE]
|
||||
tensor([0, 0, 0, 1, 1, 1])
|
||||
>>> # Original type-specific node IDs
|
||||
>>> hg.ndata[dgl.NID]
|
||||
tensor([0, 1, 2, 0, 1, 2])
|
||||
|
||||
>>> # Order of edge types in the heterograph
|
||||
>>> g.etypes
|
||||
['interacts', 'treats']
|
||||
>>> # Original edge types
|
||||
>>> hg.edata[dgl.ETYPE]
|
||||
tensor([0, 0, 1])
|
||||
>>> # Original type-specific edge IDs
|
||||
>>> hg.edata[dgl.EID]
|
||||
tensor([0, 1, 0])
|
||||
|
||||
For modeling purposes, one may want to group some relations together and apply the same
|
||||
operation to them. To address this need, one can first take an edge type subgraph of the
|
||||
heterograph and then convert the subgraph to a homogeneous graph.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> g = dgl.heterograph({
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
|
||||
... ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
|
||||
... })
|
||||
>>> sub_g = dgl.edge_type_subgraph(g, [('drug', 'interacts', 'drug'),
|
||||
... ('drug', 'interacts', 'gene')])
|
||||
>>> h_sub_g = dgl.to_homogeneous(sub_g)
|
||||
>>> h_sub_g
|
||||
Graph(num_nodes=7, num_edges=4,
|
||||
...)
|
||||
@@ -0,0 +1,37 @@
|
||||
.. _guide-graph:
|
||||
|
||||
Chapter 1: Graph
|
||||
======================
|
||||
|
||||
:ref:`(中文版)<guide_cn-graph>`
|
||||
|
||||
Graphs express entities (nodes) along with their relations (edges), and both nodes and
|
||||
edges can be typed (e.g., ``"user"`` and ``"item"`` are two different types of nodes). DGL provides a
|
||||
graph-centric programming abstraction with its core data structure -- :class:`~dgl.DGLGraph`. :class:`~dgl.DGLGraph`
|
||||
provides its interface to handle a graph's structure, its node/edge features, and the resulting
|
||||
computations that can be performed using these components.
|
||||
|
||||
Roadmap
|
||||
-------
|
||||
|
||||
The chapter starts with a brief introduction to graph definitions in 1.1 and then introduces some core
|
||||
concepts of :class:`~dgl.DGLGraph`:
|
||||
|
||||
* :ref:`guide-graph-basic`
|
||||
* :ref:`guide-graph-graphs-nodes-edges`
|
||||
* :ref:`guide-graph-feature`
|
||||
* :ref:`guide-graph-external`
|
||||
* :ref:`guide-graph-heterogeneous`
|
||||
* :ref:`guide-graph-gpu`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
graph-basic
|
||||
graph-graphs-nodes-edges
|
||||
graph-feature
|
||||
graph-external
|
||||
graph-heterogeneous
|
||||
graph-gpu
|
||||
@@ -0,0 +1,15 @@
|
||||
User Guide
|
||||
==========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
graph
|
||||
message
|
||||
nn
|
||||
data
|
||||
training
|
||||
minibatch
|
||||
distributed
|
||||
mixed_precision
|
||||
@@ -0,0 +1,108 @@
|
||||
.. _guide-message-passing-api:
|
||||
|
||||
2.1 Built-in Functions and Message Passing APIs
|
||||
-----------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-message-passing-api>`
|
||||
|
||||
In DGL, **message function** takes a single argument ``edges``,
|
||||
which is an :class:`~dgl.udf.EdgeBatch` instance. During message passing,
|
||||
DGL generates it internally to represent a batch of edges. It has three
|
||||
members ``src``, ``dst`` and ``data`` to access features of source nodes,
|
||||
destination nodes, and edges, respectively.
|
||||
|
||||
**reduce function** takes a single argument ``nodes``, which is a
|
||||
:class:`~dgl.udf.NodeBatch` instance. During message passing,
|
||||
DGL generates it internally to represent a batch of nodes. It has member
|
||||
``mailbox`` to access the messages received for the nodes in the batch.
|
||||
Some of the most common reduce operations include ``sum``, ``max``, ``min``, etc.
|
||||
|
||||
**update function** takes a single argument ``nodes`` as described above.
|
||||
This function operates on the aggregation result from ``reduce function``, typically
|
||||
combining it with a node’s original feature at the the last step and saving the result
|
||||
as a node feature.
|
||||
|
||||
DGL has implemented commonly used message functions and reduce functions
|
||||
as **built-in** in the namespace ``dgl.function``. In general, DGL
|
||||
suggests using built-in functions **whenever possible** since they are
|
||||
heavily optimized and automatically handle dimension broadcasting.
|
||||
|
||||
If your message passing functions cannot be implemented with built-ins,
|
||||
you can implement user-defined message/reduce function (aka. **UDF**).
|
||||
|
||||
Built-in message functions can be unary or binary. DGL supports ``copy``
|
||||
for unary. For binary funcs, DGL supports ``add``, ``sub``, ``mul``, ``div``,
|
||||
``dot``. The naming convention for message built-in funcs is that ``u``
|
||||
represents ``src`` nodes, ``v`` represents ``dst`` nodes, and ``e`` represents ``edges``.
|
||||
The parameters for those functions are strings indicating the input and output field names for
|
||||
the corresponding nodes and edges. The list of supported built-in functions
|
||||
can be found in :ref:`api-built-in`. For example, to add the ``hu`` feature from src
|
||||
nodes and ``hv`` feature from dst nodes then save the result on the edge
|
||||
at ``he`` field, one can use built-in function ``dgl.function.u_add_v('hu', 'hv', 'he')``.
|
||||
This is equivalent to the Message UDF:
|
||||
|
||||
.. code::
|
||||
|
||||
def message_func(edges):
|
||||
return {'he': edges.src['hu'] + edges.dst['hv']}
|
||||
|
||||
Built-in reduce functions support operations ``sum``, ``max``, ``min``,
|
||||
and ``mean``. Reduce functions usually have two parameters, one
|
||||
for field name in ``mailbox``, one for field name in node features, both
|
||||
are strings. For example, ``dgl.function.sum('m', 'h')`` is equivalent
|
||||
to the Reduce UDF that sums up the message ``m``:
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
def reduce_func(nodes):
|
||||
return {'h': torch.sum(nodes.mailbox['m'], dim=1)}
|
||||
|
||||
For advanced usage of UDF, see :ref:`apiudf`.
|
||||
|
||||
It is also possible to invoke only edge-wise computation by :meth:`~dgl.DGLGraph.apply_edges`
|
||||
without invoking message passing. :meth:`~dgl.DGLGraph.apply_edges` takes a message function
|
||||
for parameter and by default updates the features of all edges. For example:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
graph.apply_edges(fn.u_add_v('el', 'er', 'e'))
|
||||
|
||||
For message passing, :meth:`~dgl.DGLGraph.update_all` is a high-level
|
||||
API that merges message generation, message aggregation and node update
|
||||
in a single call, which leaves room for optimization as a whole.
|
||||
|
||||
The parameters for :meth:`~dgl.DGLGraph.update_all` are a message function, a
|
||||
reduce function and an update function. One can call update function outside of
|
||||
``update_all`` and not specify it in invoking :meth:`~dgl.DGLGraph.update_all`.
|
||||
DGL recommends this approach since the update function can usually be
|
||||
written as pure tensor operations to make the code concise. For
|
||||
example:
|
||||
|
||||
.. code::
|
||||
|
||||
def update_all_example(graph):
|
||||
# store the result in graph.ndata['ft']
|
||||
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
|
||||
fn.sum('m', 'ft'))
|
||||
# Call update function outside of update_all
|
||||
final_ft = graph.ndata['ft'] * 2
|
||||
return final_ft
|
||||
|
||||
This call will generate the messages ``m`` by multiply src node features
|
||||
``ft`` and edge features ``a``, sum up the messages ``m`` to update node
|
||||
features ``ft``, and finally multiply ``ft`` by 2 to get the result
|
||||
``final_ft``. After the call, DGL will clean the intermediate messages ``m``.
|
||||
The math formula for the above function is:
|
||||
|
||||
.. math:: {final\_ft}_i = 2 * \sum_{j\in\mathcal{N}(i)} ({ft}_j * a_{ji})
|
||||
|
||||
DGL's built-in functions support floating point data types, i.e. the feature must
|
||||
be ``half`` (``float16``) /``float``/``double`` tensors.
|
||||
``float16`` data type support is disabled by default as it has a minimum GPU
|
||||
compute capacity requirement of ``sm_53`` (Pascal, Volta, Turing and Ampere
|
||||
architectures).
|
||||
|
||||
User can enable float16 for mixed precision training by compiling DGL from source
|
||||
(see :doc:`Mixed Precision Training <mixed_precision>` tutorial for details).
|
||||
@@ -0,0 +1,63 @@
|
||||
.. _guide-message-passing-efficient:
|
||||
|
||||
2.2 Writing Efficient Message Passing Code
|
||||
------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-message-passing-efficient>`
|
||||
|
||||
DGL optimizes memory consumption and computing speed for message
|
||||
passing. A common practise to leverage those
|
||||
optimizations is to construct one's own message passing functionality as
|
||||
a combination of :meth:`~dgl.DGLGraph.update_all` calls with built-in
|
||||
functions as parameters.
|
||||
|
||||
Besides that, considering that the number of edges is much larger than the number of nodes for some graphs, avoiding unnecessary memory copy from nodes to edges is beneficial. For some cases like
|
||||
:class:`~dgl.nn.pytorch.conv.GATConv`,
|
||||
where it is necessary to save message on the edges, one needs to call
|
||||
:meth:`~dgl.DGLGraph.apply_edges` with built-in functions. Sometimes the
|
||||
messages on the edges can be high dimensional, which is memory consuming.
|
||||
DGL recommends keeping the dimension of edge features as low as possible.
|
||||
|
||||
Here’s an example on how to achieve this by splitting operations on the
|
||||
edges to nodes. The approach does the following: concatenate the ``src``
|
||||
feature and ``dst`` feature, then apply a linear layer, i.e.
|
||||
:math:`W\times (u || v)`. The ``src`` and ``dst`` feature dimension is
|
||||
high, while the linear layer output dimension is low. A straight forward
|
||||
implementation would be like:
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
linear = nn.Parameter(torch.FloatTensor(size=(node_feat_dim * 2, out_dim)))
|
||||
def concat_message_function(edges):
|
||||
return {'cat_feat': torch.cat([edges.src['feat'], edges.dst['feat']], dim=1)}
|
||||
g.apply_edges(concat_message_function)
|
||||
g.edata['out'] = g.edata['cat_feat'] @ linear
|
||||
|
||||
The suggested implementation splits the linear operation into two,
|
||||
one applies on ``src`` feature, the other applies on ``dst`` feature.
|
||||
It then adds the output of the linear operations on the edges at the final stage,
|
||||
i.e. performing :math:`W_l\times u + W_r \times v`. This is because
|
||||
:math:`W \times (u||v) = W_l \times u + W_r \times v`, where :math:`W_l`
|
||||
and :math:`W_r` are the left and the right half of the matrix :math:`W`,
|
||||
respectively:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
linear_src = nn.Parameter(torch.FloatTensor(size=(node_feat_dim, out_dim)))
|
||||
linear_dst = nn.Parameter(torch.FloatTensor(size=(node_feat_dim, out_dim)))
|
||||
out_src = g.ndata['feat'] @ linear_src
|
||||
out_dst = g.ndata['feat'] @ linear_dst
|
||||
g.srcdata.update({'out_src': out_src})
|
||||
g.dstdata.update({'out_dst': out_dst})
|
||||
g.apply_edges(fn.u_add_v('out_src', 'out_dst', 'out'))
|
||||
|
||||
The above two implementations are mathematically equivalent. The latter
|
||||
one is more efficient because it does not need to save feat_src and
|
||||
feat_dst on edges, which is not memory-efficient. Plus, addition could
|
||||
be optimized with DGL’s built-in function :func:`~dgl.function.u_add_v`, which further
|
||||
speeds up computation and saves memory footprint.
|
||||
@@ -0,0 +1,46 @@
|
||||
.. _guide-message-passing-heterograph:
|
||||
|
||||
2.5 Message Passing on Heterogeneous Graph
|
||||
------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-message-passing-heterograph>`
|
||||
|
||||
Heterogeneous graphs (:ref:`guide-graph-heterogeneous`), or
|
||||
heterographs for short, are graphs that contain different types of nodes
|
||||
and edges. The different types of nodes and edges tend to have different
|
||||
types of attributes that are designed to capture the characteristics of
|
||||
each node and edge type. Within the context of graph neural networks,
|
||||
depending on their complexity, certain node and edge types might need to
|
||||
be modeled with representations that have a different number of
|
||||
dimensions.
|
||||
|
||||
The message passing on heterographs can be split into two parts:
|
||||
|
||||
1. Message computation and aggregation for each relation r.
|
||||
2. Reduction that merges the aggregation results from all relations for each node type.
|
||||
|
||||
DGL’s interface to call message passing on heterographs is
|
||||
:meth:`~dgl.DGLGraph.multi_update_all`.
|
||||
:meth:`~dgl.DGLGraph.multi_update_all` takes a dictionary containing
|
||||
the parameters for :meth:`~dgl.DGLGraph.update_all` within each relation
|
||||
using relation as the key, and a string representing the cross type reducer.
|
||||
The reducer can be one of ``sum``, ``min``, ``max``, ``mean``, ``stack``.
|
||||
Here’s an example:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
for c_etype in G.canonical_etypes:
|
||||
srctype, etype, dsttype = c_etype
|
||||
Wh = self.weight[etype](feat_dict[srctype])
|
||||
# Save it in graph for message passing
|
||||
G.nodes[srctype].data['Wh_%s' % etype] = Wh
|
||||
# Specify per-relation message passing functions: (message_func, reduce_func).
|
||||
# Note that the results are saved to the same destination feature 'h', which
|
||||
# hints the type wise reducer for aggregation.
|
||||
funcs[etype] = (fn.copy_u('Wh_%s' % etype, 'm'), fn.mean('m', 'h'))
|
||||
# Trigger message passing of multiple types.
|
||||
G.multi_update_all(funcs, 'sum')
|
||||
# return the updated node feature dictionary
|
||||
return {ntype : G.nodes[ntype].data['h'] for ntype in G.ntypes}
|
||||
@@ -0,0 +1,20 @@
|
||||
.. _guide-message-passing-part:
|
||||
|
||||
2.3 Apply Message Passing On Part Of The Graph
|
||||
----------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-message-passing-part>`
|
||||
|
||||
If one only wants to update part of the nodes in the graph, the practice
|
||||
is to create a subgraph by providing the IDs for the nodes to
|
||||
include in the update, then call :meth:`~dgl.DGLGraph.update_all` on the
|
||||
subgraph. For example:
|
||||
|
||||
.. code::
|
||||
|
||||
nid = [0, 2, 3, 6, 7, 9]
|
||||
sg = g.subgraph(nid)
|
||||
sg.update_all(message_func, reduce_func, apply_node_func)
|
||||
|
||||
This is a common usage in mini-batch training. Check :ref:`guide-minibatch` for more detailed
|
||||
usages.
|
||||
@@ -0,0 +1,46 @@
|
||||
.. _guide-message-passing:
|
||||
|
||||
Chapter 2: Message Passing
|
||||
==========================
|
||||
|
||||
:ref:`(中文版) <guide_cn-message-passing>`
|
||||
|
||||
Message Passing Paradigm
|
||||
------------------------
|
||||
|
||||
Let :math:`x_v\in\mathbb{R}^{d_1}` be the feature for node :math:`v`,
|
||||
and :math:`w_{e}\in\mathbb{R}^{d_2}` be the feature for edge
|
||||
:math:`({u}, {v})`. The **message passing paradigm** defines the
|
||||
following node-wise and edge-wise computation at step :math:`t+1`:
|
||||
|
||||
.. math:: \text{Edge-wise: } m_{e}^{(t+1)} = \phi \left( x_v^{(t)}, x_u^{(t)}, w_{e}^{(t)} \right) , ({u}, {v},{e}) \in \mathcal{E}.
|
||||
|
||||
.. math:: \text{Node-wise: } x_v^{(t+1)} = \psi \left(x_v^{(t)}, \rho\left(\left\lbrace m_{e}^{(t+1)} : ({u}, {v},{e}) \in \mathcal{E} \right\rbrace \right) \right).
|
||||
|
||||
In the above equations, :math:`\phi` is a **message function**
|
||||
defined on each edge to generate a message by combining the edge feature
|
||||
with the features of its incident nodes; :math:`\psi` is an
|
||||
**update function** defined on each node to update the node feature
|
||||
by aggregating its incoming messages using the **reduce function**
|
||||
:math:`\rho`.
|
||||
|
||||
Roadmap
|
||||
-------
|
||||
|
||||
This chapter introduces DGL's message passing APIs, and how to efficiently use them on both nodes and edges.
|
||||
The last section of it explains how to implement message passing on heterogeneous graphs.
|
||||
|
||||
* :ref:`guide-message-passing-api`
|
||||
* :ref:`guide-message-passing-efficient`
|
||||
* :ref:`guide-message-passing-part`
|
||||
* :ref:`guide-message-passing-heterograph`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
message-api
|
||||
message-efficient
|
||||
message-part
|
||||
message-heterograph
|
||||
@@ -0,0 +1,133 @@
|
||||
.. _guide-minibatch-customizing-neighborhood-sampler:
|
||||
|
||||
6.4 Implementing Custom Graph Samplers
|
||||
----------------------------------------------
|
||||
|
||||
Implementing custom samplers involves subclassing the
|
||||
:class:`dgl.graphbolt.SubgraphSampler` base class and implementing its abstract
|
||||
:attr:`sample_subgraphs` method. The :attr:`sample_subgraphs` method should
|
||||
take in seed nodes which are the nodes to sample neighbors from:
|
||||
|
||||
.. code:: python
|
||||
|
||||
def sample_subgraphs(self, seed_nodes):
|
||||
return input_nodes, sampled_subgraphs
|
||||
|
||||
The method should return the input node IDs list and a list of subgraphs. Each
|
||||
subgraph is a :class:`~dgl.graphbolt.SampledSubgraph` object.
|
||||
|
||||
|
||||
Any other data that are required during sampling such as the graph structure,
|
||||
fanout size, etc. should be passed to the sampler via the constructor.
|
||||
|
||||
The code below implements a classical neighbor sampler:
|
||||
|
||||
.. code:: python
|
||||
|
||||
@functional_datapipe("customized_sample_neighbor")
|
||||
class CustomizedNeighborSampler(dgl.graphbolt.SubgraphSampler):
|
||||
def __init__(self, datapipe, graph, fanouts):
|
||||
super().__init__(datapipe)
|
||||
self.graph = graph
|
||||
self.fanouts = fanouts
|
||||
|
||||
def sample_subgraphs(self, seed_nodes):
|
||||
subgs = []
|
||||
for fanout in reversed(self.fanouts):
|
||||
# Sample a fixed number of neighbors of the current seed nodes.
|
||||
input_nodes, sg = g.sample_neighbors(seed_nodes, fanout)
|
||||
subgs.insert(0, sg)
|
||||
seed_nodes = input_nodes
|
||||
return input_nodes, subgs
|
||||
|
||||
To use this sampler with :class:`~dgl.graphbolt.DataLoader`:
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.customized_sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
for data in dataloader:
|
||||
input_features = data.node_features["feat"]
|
||||
output_labels = data.labels
|
||||
output_predictions = model(data.blocks, input_features)
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
|
||||
Sampler for Heterogeneous Graphs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To write a sampler for heterogeneous graphs, one needs to be aware that
|
||||
the argument `graph` is a heterogeneous graph while `seeds` could be a
|
||||
dictionary of ID tensors. Most of DGL's graph sampling operators (e.g.,
|
||||
the ``sample_neighbors`` and ``to_block`` functions in the above example) can
|
||||
work on heterogeneous graph natively, so many samplers are automatically
|
||||
ready for heterogeneous graph. For example, the above ``CustomizedNeighborSampler``
|
||||
can be used on heterogeneous graphs:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.graphbolt as gb
|
||||
hg = gb.FusedCSCSamplingGraph()
|
||||
train_set = item_set = gb.HeteroItemSet(
|
||||
{
|
||||
"user": gb.ItemSet(
|
||||
(torch.arange(0, 5), torch.arange(5, 10)),
|
||||
names=("seeds", "labels"),
|
||||
),
|
||||
"item": gb.ItemSet(
|
||||
(torch.arange(5, 10), torch.arange(10, 15)),
|
||||
names=("seeds", "labels"),
|
||||
),
|
||||
}
|
||||
)
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.customized_sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.fetch_feature(
|
||||
feature, node_feature_keys={"user": ["feat"], "item": ["feat"]}
|
||||
)
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
for data in dataloader:
|
||||
input_features = {
|
||||
ntype: data.node_features[(ntype, "feat")]
|
||||
for ntype in data.blocks[0].srctypes
|
||||
}
|
||||
output_labels = data.labels["user"]
|
||||
output_predictions = model(data.blocks, input_features)["user"]
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
|
||||
Exclude Edges After Sampling
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In some cases, we may want to exclude seed edges from the sampled subgraph. For
|
||||
example, in link prediction tasks, we want to exclude the edges in the
|
||||
training set from the sampled subgraph to prevent information leakage. To
|
||||
do so, we need to add an additional datapipe right after sampling as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = datapipe.customized_sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.transform(gb.exclude_seed_edges)
|
||||
|
||||
Please check the API page of :func:`~dgl.graphbolt.exclude_seed_edges` for more
|
||||
details.
|
||||
|
||||
The above API is based on :meth:`~dgl.graphbolt.SampledSubgrahp.exclude_edges`.
|
||||
If you want to exclude edges from the sampled subgraph based on some other
|
||||
criteria, you could write your own transform function. Please check the method
|
||||
for reference.
|
||||
|
||||
You could also refer to examples in
|
||||
`Link Prediction <https://github.com/dmlc/dgl/blob/master/examples/graphbolt/link_prediction.py>`__.
|
||||
@@ -0,0 +1,324 @@
|
||||
.. _guide-minibatch-edge-classification-sampler:
|
||||
|
||||
6.2 Training GNN for Edge Classification with Neighborhood Sampling
|
||||
----------------------------------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-minibatch-edge-classification-sampler>`
|
||||
|
||||
Training for edge classification/regression is somewhat similar to that
|
||||
of node classification/regression with several notable differences.
|
||||
|
||||
Define a neighborhood sampler and data loader
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can use the
|
||||
:ref:`same neighborhood samplers as node classification <guide-minibatch-node-classification-sampler>`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10])
|
||||
# Or equivalently
|
||||
datapipe = dgl.graphbolt.NeighborSampler(datapipe, g, [10, 10])
|
||||
|
||||
The code for defining a data loader is also the same as that of node
|
||||
classification. The only difference is that it iterates over the
|
||||
edges(namely, node pairs) in the training set instead of the nodes.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.graphbolt as gb
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
g = gb.SamplingGraph()
|
||||
seeds = torch.arange(0, 1000).reshape(-1, 2)
|
||||
labels = torch.randint(0, 2, (5,))
|
||||
train_set = gb.ItemSet((seeds, labels), names=("seeds", "labels"))
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=128, shuffle=True)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
# Or equivalently:
|
||||
# datapipe = gb.NeighborSampler(datapipe, g, [10, 10])
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
Iterating over the DataLoader will yield :class:`~dgl.graphbolt.MiniBatch`
|
||||
which contains a list of specially created graphs representing the computation
|
||||
dependencies on each layer. You can access the *message flow graphs* (MFGs) via
|
||||
`mini_batch.blocks`.
|
||||
|
||||
.. code:: python
|
||||
mini_batch = next(iter(dataloader))
|
||||
print(mini_batch.blocks)
|
||||
|
||||
.. note::
|
||||
|
||||
See the :doc:`Stochastic Training Tutorial
|
||||
<../notebooks/stochastic_training/neighbor_sampling_overview.nblink>`__
|
||||
for the concept of message flow graph.
|
||||
|
||||
If you wish to develop your own neighborhood sampler or you want a more
|
||||
detailed explanation of the concept of MFGs, please refer to
|
||||
:ref:`guide-minibatch-customizing-neighborhood-sampler`.
|
||||
|
||||
.. _guide-minibatch-edge-classification-sampler-exclude:
|
||||
|
||||
Removing edges in the minibatch from the original graph for neighbor sampling
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When training edge classification models, sometimes you wish to remove
|
||||
the edges appearing in the training data from the computation dependency
|
||||
as if they never existed. Otherwise, the model will “know” the fact that
|
||||
an edge exists between the two nodes, and potentially use it for
|
||||
advantage.
|
||||
|
||||
Therefore in edge classification you sometimes would like to exclude the
|
||||
seed edges as well as their reverse edges from the sampled minibatch.
|
||||
You can use :func:`~dgl.graphbolt.exclude_seed_edges` alongside with
|
||||
:class:`~dgl.graphbolt.MiniBatchTransformer` to achieve this.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.graphbolt as gb
|
||||
from functools import partial
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
g = gb.SamplingGraph()
|
||||
seeds = torch.arange(0, 1000).reshape(-1, 2)
|
||||
labels = torch.randint(0, 2, (5,))
|
||||
train_set = gb.ItemSet((seeds, labels), names=("seeds", "labels"))
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=128, shuffle=True)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
exclude_seed_edges = partial(gb.exclude_seed_edges, include_reverse_edges=True)
|
||||
datapipe = datapipe.transform(exclude_seed_edges)
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
|
||||
Adapt your model for minibatch training
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The edge classification model usually consists of two parts:
|
||||
|
||||
- One part that obtains the representation of incident nodes.
|
||||
- The other part that computes the edge score from the incident node
|
||||
representations.
|
||||
|
||||
The former part is exactly the same as
|
||||
:ref:`that from node classification <guide-minibatch-node-classification-model>`
|
||||
and we can simply reuse it. The input is still the list of
|
||||
MFGs generated from a data loader provided by DGL, as well as the
|
||||
input features.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class StochasticTwoLayerGCN(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.conv1 = dglnn.GraphConv(in_features, hidden_features)
|
||||
self.conv2 = dglnn.GraphConv(hidden_features, out_features)
|
||||
|
||||
def forward(self, blocks, x):
|
||||
x = F.relu(self.conv1(blocks[0], x))
|
||||
x = F.relu(self.conv2(blocks[1], x))
|
||||
return x
|
||||
|
||||
The input to the latter part is usually the output from the
|
||||
former part, as well as the subgraph(node pairs) of the original graph induced
|
||||
by the edges in the minibatch. The subgraph is yielded from the same data
|
||||
loader.
|
||||
|
||||
The following code shows an example of predicting scores on the edges by
|
||||
concatenating the incident node features and projecting it with a dense layer.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class ScorePredictor(nn.Module):
|
||||
def __init__(self, num_classes, in_features):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(2 * in_features, num_classes)
|
||||
|
||||
def forward(self, seeds, x):
|
||||
src_x = x[seeds[:, 0]]
|
||||
dst_x = x[seeds[:, 1]]
|
||||
data = torch.cat([src_x, dst_x], 1)
|
||||
return self.W(data)
|
||||
|
||||
|
||||
The entire model will take the list of MFGs and the edges generated by the data
|
||||
loader, as well as the input node features as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features, num_classes):
|
||||
super().__init__()
|
||||
self.gcn = StochasticTwoLayerGCN(
|
||||
in_features, hidden_features, out_features)
|
||||
self.predictor = ScorePredictor(num_classes, out_features)
|
||||
|
||||
def forward(self, blocks, x, seeds):
|
||||
x = self.gcn(blocks, x)
|
||||
return self.predictor(seeds, x)
|
||||
|
||||
DGL ensures that that the nodes in the edge subgraph are the same as the
|
||||
output nodes of the last MFG in the generated list of MFGs.
|
||||
|
||||
Training Loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The training loop is very similar to node classification. You can
|
||||
iterate over the dataloader and get a subgraph induced by the edges in
|
||||
the minibatch, as well as the list of MFGs necessary for computing
|
||||
their incident node representations.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch.nn.functional as F
|
||||
model = Model(in_features, hidden_features, out_features, num_classes)
|
||||
model = model.to(device)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for data in dataloader:
|
||||
blocks = data.blocks
|
||||
x = data.edge_features("feat")
|
||||
y_hat = model(data.blocks, x, data.compacted_seeds)
|
||||
loss = F.cross_entropy(data.labels, y_hat)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
|
||||
For heterogeneous graphs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The models computing the node representations on heterogeneous graphs
|
||||
can also be used for computing incident node representations for edge
|
||||
classification/regression.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class StochasticTwoLayerRGCN(nn.Module):
|
||||
def __init__(self, in_feat, hidden_feat, out_feat, rel_names):
|
||||
super().__init__()
|
||||
self.conv1 = dglnn.HeteroGraphConv({
|
||||
rel : dglnn.GraphConv(in_feat, hidden_feat, norm='right')
|
||||
for rel in rel_names
|
||||
})
|
||||
self.conv2 = dglnn.HeteroGraphConv({
|
||||
rel : dglnn.GraphConv(hidden_feat, out_feat, norm='right')
|
||||
for rel in rel_names
|
||||
})
|
||||
|
||||
def forward(self, blocks, x):
|
||||
x = self.conv1(blocks[0], x)
|
||||
x = self.conv2(blocks[1], x)
|
||||
return x
|
||||
|
||||
For score prediction, the only implementation difference between the
|
||||
homogeneous graph and the heterogeneous graph is that we are looping
|
||||
over the edge types.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class ScorePredictor(nn.Module):
|
||||
def __init__(self, num_classes, in_features):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(2 * in_features, num_classes)
|
||||
|
||||
def forward(self, seeds, x):
|
||||
scores = {}
|
||||
for etype in seeds.keys():
|
||||
src, dst = seeds[etype].T
|
||||
data = torch.cat([x[etype][src], x[etype][dst]], 1)
|
||||
scores[etype] = self.W(data)
|
||||
return scores
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features, num_classes,
|
||||
etypes):
|
||||
super().__init__()
|
||||
self.rgcn = StochasticTwoLayerRGCN(
|
||||
in_features, hidden_features, out_features, etypes)
|
||||
self.pred = ScorePredictor(num_classes, out_features)
|
||||
|
||||
def forward(self, seeds, blocks, x):
|
||||
x = self.rgcn(blocks, x)
|
||||
return self.pred(seeds, x)
|
||||
|
||||
Data loader definition is almost identical to that of homogeneous graph. The
|
||||
only difference is that the train_set is now an instance of
|
||||
:class:`~dgl.graphbolt.HeteroItemSet` instead of :class:`~dgl.graphbolt.ItemSet`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.graphbolt as gb
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
g = gb.SamplingGraph()
|
||||
seeds = torch.arange(0, 1000).reshape(-1, 2)
|
||||
labels = torch.randint(0, 3, (1000,))
|
||||
seeds_labels = {
|
||||
"user:like:item": gb.ItemSet(
|
||||
(seeds, labels), names=("seeds", "labels")
|
||||
),
|
||||
"user:follow:user": gb.ItemSet(
|
||||
(seeds, labels), names=("seeds", "labels")
|
||||
),
|
||||
}
|
||||
train_set = gb.HeteroItemSet(seeds_labels)
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=128, shuffle=True)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.fetch_feature(
|
||||
feature, node_feature_keys={"item": ["feat"], "user": ["feat"]}
|
||||
)
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
Things become a little different if you wish to exclude the reverse
|
||||
edges on heterogeneous graphs. On heterogeneous graphs, reverse edges
|
||||
usually have a different edge type from the edges themselves, in order
|
||||
to differentiate the “forward” and “backward” relationships (e.g.
|
||||
``follow`` and ``followed_by`` are reverse relations of each other,
|
||||
``like`` and ``liked_by`` are reverse relations of each other,
|
||||
etc.).
|
||||
|
||||
If each edge in a type has a reverse edge with the same ID in another
|
||||
type, you can specify the mapping between edge types and their reverse
|
||||
types. The way to exclude the edges in the minibatch as well as their
|
||||
reverse edges then goes as follows.
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
||||
exclude_seed_edges = partial(
|
||||
gb.exclude_seed_edges,
|
||||
include_reverse_edges=True,
|
||||
reverse_etypes_mapping={
|
||||
"user:like:item": "item:liked_by:user",
|
||||
"user:follow:user": "user:followed_by:user",
|
||||
},
|
||||
)
|
||||
datapipe = datapipe.transform(exclude_seed_edges)
|
||||
|
||||
|
||||
The training loop is again almost the same as that on homogeneous graph,
|
||||
except for the implementation of ``compute_loss`` that will take in two
|
||||
dictionaries of node types and predictions here.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch.nn.functional as F
|
||||
model = Model(in_features, hidden_features, out_features, num_classes, etypes)
|
||||
model = model.to(device)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for data in dataloader:
|
||||
blocks = data.blocks
|
||||
x = data.edge_features(("user:like:item", "feat"))
|
||||
y_hat = model(data.blocks, x, data.compacted_seeds)
|
||||
loss = F.cross_entropy(data.labels, y_hat)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
.. _guide-minibatch-gpu-sampling:
|
||||
|
||||
6.8 Using GPU for Neighborhood Sampling
|
||||
---------------------------------------
|
||||
|
||||
.. note::
|
||||
GraphBolt does not support GPU-based neighborhood sampling yet. So this guide is
|
||||
utilizing :class:`~dgl.dataloading.DataLoader` for illustration.
|
||||
|
||||
DGL since 0.7 has been supporting GPU-based neighborhood sampling, which has a significant
|
||||
speed advantage over CPU-based neighborhood sampling. If you estimate that your graph
|
||||
can fit onto GPU and your model does not take a lot of GPU memory, then it is best to
|
||||
put the graph onto GPU memory and use GPU-based neighbor sampling.
|
||||
|
||||
For example, `OGB Products <https://ogb.stanford.edu/docs/nodeprop/#ogbn-products>`_ has
|
||||
2.4M nodes and 61M edges. The graph takes less than 1GB since the memory consumption of
|
||||
a graph depends on the number of edges. Therefore it is entirely possible to fit the
|
||||
whole graph onto GPU.
|
||||
|
||||
|
||||
Using GPU-based neighborhood sampling in DGL data loaders
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
One can use GPU-based neighborhood sampling with DGL data loaders via:
|
||||
|
||||
* Put the graph onto GPU.
|
||||
|
||||
* Put the ``train_nid`` onto GPU.
|
||||
|
||||
* Set ``device`` argument to a GPU device.
|
||||
|
||||
* Set ``num_workers`` argument to 0, because CUDA does not allow multiple processes
|
||||
accessing the same context.
|
||||
|
||||
All the other arguments for the :class:`~dgl.dataloading.DataLoader` can be
|
||||
the same as the other user guides and tutorials.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g = g.to('cuda:0')
|
||||
train_nid = train_nid.to('cuda:0')
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g, # The graph must be on GPU.
|
||||
train_nid, # train_nid must be on GPU.
|
||||
sampler,
|
||||
device=torch.device('cuda:0'), # The device argument must be GPU.
|
||||
num_workers=0, # Number of workers must be 0.
|
||||
batch_size=1000,
|
||||
drop_last=False,
|
||||
shuffle=True)
|
||||
|
||||
.. note::
|
||||
|
||||
GPU-based neighbor sampling also works for custom neighborhood samplers as long as
|
||||
(1) your sampler is subclassed from :class:`~dgl.dataloading.BlockSampler`, and (2)
|
||||
your sampler entirely works on GPU.
|
||||
|
||||
|
||||
Using CUDA UVA-based neighborhood sampling in DGL data loaders
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. note::
|
||||
New feature introduced in DGL 0.8.
|
||||
|
||||
For the case where the graph is too large to fit onto the GPU memory, we introduce the
|
||||
CUDA UVA (Unified Virtual Addressing)-based sampling, in which GPUs perform the sampling
|
||||
on the graph pinned in CPU memory via zero-copy access.
|
||||
You can enable UVA-based neighborhood sampling in DGL data loaders via:
|
||||
|
||||
* Put the ``train_nid`` onto GPU.
|
||||
|
||||
* Set ``device`` argument to a GPU device.
|
||||
|
||||
* Set ``num_workers`` argument to 0, because CUDA does not allow multiple processes
|
||||
accessing the same context.
|
||||
|
||||
* Set ``use_uva=True``.
|
||||
|
||||
All the other arguments for the :class:`~dgl.dataloading.DataLoader` can be
|
||||
the same as the other user guides and tutorials.
|
||||
|
||||
.. code:: python
|
||||
|
||||
train_nid = train_nid.to('cuda:0')
|
||||
dataloader = dgl.dataloading.DataLoader(
|
||||
g,
|
||||
train_nid, # train_nid must be on GPU.
|
||||
sampler,
|
||||
device=torch.device('cuda:0'), # The device argument must be GPU.
|
||||
num_workers=0, # Number of workers must be 0.
|
||||
batch_size=1000,
|
||||
drop_last=False,
|
||||
shuffle=True,
|
||||
use_uva=True) # Set use_uva=True
|
||||
|
||||
UVA-based sampling is the recommended solution for mini-batch training on large graphs,
|
||||
especially for multi-GPU training.
|
||||
|
||||
.. note::
|
||||
|
||||
To use UVA-based sampling in multi-GPU training, you should first materialize all the
|
||||
necessary sparse formats of the graph before spawning training processes.
|
||||
Refer to our `GraphSAGE example <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/multi_gpu_node_classification.py>`_ for more details.
|
||||
|
||||
|
||||
UVA and GPU support for PinSAGESampler/RandomWalkNeighborSampler
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PinSAGESampler and RandomWalkNeighborSampler support UVA and GPU sampling.
|
||||
You can enable them via:
|
||||
|
||||
* Pin the graph (for UVA sampling) or put the graph onto GPU (for GPU sampling).
|
||||
|
||||
* Put the ``train_nid`` onto GPU.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g = dgl.heterograph({
|
||||
('item', 'bought-by', 'user'): ([0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 0, 1, 2, 3, 2, 3]),
|
||||
('user', 'bought', 'item'): ([0, 1, 0, 1, 2, 3, 2, 3], [0, 0, 1, 1, 2, 2, 3, 3])})
|
||||
|
||||
# UVA setup
|
||||
# g.create_formats_()
|
||||
# g.pin_memory_()
|
||||
|
||||
# GPU setup
|
||||
device = torch.device('cuda:0')
|
||||
g = g.to(device)
|
||||
|
||||
sampler1 = dgl.sampling.PinSAGESampler(g, 'item', 'user', 4, 0.5, 3, 2)
|
||||
sampler2 = dgl.sampling.RandomWalkNeighborSampler(g, 4, 0.5, 3, 2, ['bought-by', 'bought'])
|
||||
|
||||
train_nid = torch.tensor([0, 2], dtype=g.idtype, device=device)
|
||||
sampler1(train_nid)
|
||||
sampler2(train_nid)
|
||||
|
||||
|
||||
Using GPU-based neighbor sampling with DGL functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can build your own GPU sampling pipelines with the following functions that support
|
||||
operating on GPU:
|
||||
|
||||
* :func:`dgl.sampling.sample_neighbors`
|
||||
* :func:`dgl.sampling.random_walk`
|
||||
|
||||
Subgraph extraction ops:
|
||||
|
||||
* :func:`dgl.node_subgraph`
|
||||
* :func:`dgl.edge_subgraph`
|
||||
* :func:`dgl.in_subgraph`
|
||||
* :func:`dgl.out_subgraph`
|
||||
|
||||
Graph transform ops for subgraph construction:
|
||||
|
||||
* :func:`dgl.to_block`
|
||||
* :func:`dgl.compact_graph`
|
||||
@@ -0,0 +1,128 @@
|
||||
.. _guide-minibatch-inference:
|
||||
|
||||
6.7 Exact Offline Inference on Large Graphs
|
||||
------------------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-minibatch-inference>`
|
||||
|
||||
Both subgraph sampling and neighborhood sampling are to reduce the
|
||||
memory and time consumption for training GNNs with GPUs. When performing
|
||||
inference it is usually better to truly aggregate over all neighbors
|
||||
instead to get rid of the randomness introduced by sampling. However,
|
||||
full-graph forward propagation is usually infeasible on GPU due to
|
||||
limited memory, and slow on CPU due to slow computation. This section
|
||||
introduces the methodology of full-graph forward propagation with
|
||||
limited GPU memory via minibatch and neighborhood sampling.
|
||||
|
||||
The inference algorithm is different from the training algorithm, as the
|
||||
representations of all nodes should be computed layer by layer, starting
|
||||
from the first layer. Specifically, for a particular layer, we need to
|
||||
compute the output representations of all nodes from this GNN layer in
|
||||
minibatches. The consequence is that the inference algorithm will have
|
||||
an outer loop iterating over the layers, and an inner loop iterating
|
||||
over the minibatches of nodes. In contrast, the training algorithm has
|
||||
an outer loop iterating over the minibatches of nodes, and an inner loop
|
||||
iterating over the layers for both neighborhood sampling and message
|
||||
passing.
|
||||
|
||||
The following animation shows how the computation would look like (note
|
||||
that for every layer only the first three minibatches are drawn).
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_6_0.gif
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
|
||||
Implementing Offline Inference
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Consider the two-layer GCN we have mentioned in Section 6.1
|
||||
:ref:`guide-minibatch-node-classification-model`. The way
|
||||
to implement offline inference still involves using
|
||||
:class:`~dgl.graphbolt.NeighborSampler`, but sampling for
|
||||
only one layer at a time.
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = gb.ItemSampler(all_nodes_set, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.sample_neighbor(g, [-1]) # 1 layers.
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
|
||||
Note that offline inference is implemented as a method of the GNN module
|
||||
because the computation on one layer depends on how messages are aggregated
|
||||
and combined as well.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class SAGE(nn.Module):
|
||||
def __init__(self, in_size, hidden_size, out_size):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
# Three-layer GraphSAGE-mean.
|
||||
self.layers.append(dglnn.SAGEConv(in_size, hidden_size, "mean"))
|
||||
self.layers.append(dglnn.SAGEConv(hidden_size, hidden_size, "mean"))
|
||||
self.layers.append(dglnn.SAGEConv(hidden_size, out_size, "mean"))
|
||||
self.dropout = nn.Dropout(0.5)
|
||||
self.hidden_size = hidden_size
|
||||
self.out_size = out_size
|
||||
|
||||
def forward(self, blocks, x):
|
||||
hidden_x = x
|
||||
for layer_idx, (layer, block) in enumerate(zip(self.layers, blocks)):
|
||||
hidden_x = layer(block, hidden_x)
|
||||
is_last_layer = layer_idx == len(self.layers) - 1
|
||||
if not is_last_layer:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
hidden_x = self.dropout(hidden_x)
|
||||
return hidden_x
|
||||
|
||||
def inference(self, graph, features, dataloader, device):
|
||||
"""
|
||||
Offline inference with this module
|
||||
"""
|
||||
feature = features.read("node", None, "feat")
|
||||
|
||||
# Compute representations layer by layer
|
||||
for layer_idx, layer in enumerate(self.layers):
|
||||
is_last_layer = layer_idx == len(self.layers) - 1
|
||||
|
||||
y = torch.empty(
|
||||
graph.total_num_nodes,
|
||||
self.out_size if is_last_layer else self.hidden_size,
|
||||
dtype=torch.float32,
|
||||
device=buffer_device,
|
||||
pin_memory=pin_memory,
|
||||
)
|
||||
feature = feature.to(device)
|
||||
|
||||
for step, data in tqdm(enumerate(dataloader)):
|
||||
x = feature[data.input_nodes]
|
||||
hidden_x = layer(data.blocks[0], x) # len(blocks) = 1
|
||||
if not is_last_layer:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
hidden_x = self.dropout(hidden_x)
|
||||
# By design, our output nodes are contiguous.
|
||||
y[
|
||||
data.seeds[0] : data.seeds[-1] + 1
|
||||
] = hidden_x.to(device)
|
||||
feature = y
|
||||
|
||||
return y
|
||||
|
||||
|
||||
Note that for the purpose of computing evaluation metric on the
|
||||
validation set for model selection we usually don’t have to compute
|
||||
exact offline inference. The reason is that we need to compute the
|
||||
representation for every single node on every single layer, which is
|
||||
usually very costly especially in the semi-supervised regime with a lot
|
||||
of unlabeled data. Neighborhood sampling will work fine for model
|
||||
selection and validation.
|
||||
|
||||
One can see
|
||||
`GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/graphbolt/node_classification.py>`__
|
||||
and
|
||||
`RGCN <https://github.com/dmlc/dgl/blob/master/examples/graphbolt/rgcn/hetero_rgcn.py>`__
|
||||
for examples of offline inference.
|
||||
@@ -0,0 +1,268 @@
|
||||
.. _guide-minibatch-link-classification-sampler:
|
||||
|
||||
6.3 Training GNN for Link Prediction with Neighborhood Sampling
|
||||
--------------------------------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-minibatch-link-classification-sampler>`
|
||||
|
||||
Define a data loader with neighbor and negative sampling
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can still use the same data loader as the one in node/edge classification.
|
||||
The only difference is that you need to add an additional stage
|
||||
`negative sampling` before neighbor sampling stage. The following data loader
|
||||
will pick 5 negative destination nodes uniformly for each source node of an
|
||||
edge.
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = datapipe.sample_uniform_negative(graph, 5)
|
||||
|
||||
The whole data loader pipeline is as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = gb.ItemSampler(itemset, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.sample_uniform_negative(graph, 5)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.transform(gb.exclude_seed_edges)
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
|
||||
For the details about the builtin uniform negative sampler please see
|
||||
:class:`~dgl.graphbolt.UniformNegativeSampler`.
|
||||
|
||||
You can also give your own negative sampler function, as long as it inherits
|
||||
from :class:`~dgl.graphbolt.NegativeSampler` and overrides the
|
||||
:meth:`~dgl.graphbolt.NegativeSampler._sample_with_etype` method which takes in
|
||||
the node pairs in minibatch, and returns the negative node pairs back.
|
||||
|
||||
The following gives an example of custom negative sampler that samples
|
||||
negative destination nodes according to a probability distribution
|
||||
proportional to a power of degrees.
|
||||
|
||||
.. code:: python
|
||||
|
||||
@functional_datapipe("customized_sample_negative")
|
||||
class CustomizedNegativeSampler(dgl.graphbolt.NegativeSampler):
|
||||
def __init__(self, datapipe, k, node_degrees):
|
||||
super().__init__(datapipe, k)
|
||||
# caches the probability distribution
|
||||
self.weights = node_degrees ** 0.75
|
||||
self.k = k
|
||||
|
||||
def _sample_with_etype(self, seeds, etype=None):
|
||||
src, _ = seeds.T
|
||||
src = src.repeat_interleave(self.k)
|
||||
dst = self.weights.multinomial(len(src), replacement=True)
|
||||
return src, dst
|
||||
|
||||
datapipe = datapipe.customized_sample_negative(5, node_degrees)
|
||||
|
||||
|
||||
Define a GraphSAGE model for minibatch training
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
class SAGE(nn.Module):
|
||||
def __init__(self, in_size, hidden_size):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.append(dglnn.SAGEConv(in_size, hidden_size, "mean"))
|
||||
self.layers.append(dglnn.SAGEConv(hidden_size, hidden_size, "mean"))
|
||||
self.layers.append(dglnn.SAGEConv(hidden_size, hidden_size, "mean"))
|
||||
self.hidden_size = hidden_size
|
||||
self.predictor = nn.Sequential(
|
||||
nn.Linear(hidden_size, hidden_size),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_size, hidden_size),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_size, 1),
|
||||
)
|
||||
|
||||
def forward(self, blocks, x):
|
||||
hidden_x = x
|
||||
for layer_idx, (layer, block) in enumerate(zip(self.layers, blocks)):
|
||||
hidden_x = layer(block, hidden_x)
|
||||
is_last_layer = layer_idx == len(self.layers) - 1
|
||||
if not is_last_layer:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
return hidden_x
|
||||
|
||||
|
||||
When a negative sampler is provided, the data loader will generate positive and
|
||||
negative node pairs for each minibatch besides the *Message Flow Graphs* (MFGs).
|
||||
Use `compacted_seeds` and `labels` to get compact node pairs and corresponding
|
||||
labels.
|
||||
|
||||
|
||||
Training loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The training loop simply involves iterating over the data loader and
|
||||
feeding in the graphs as well as the input features to the model defined
|
||||
above.
|
||||
|
||||
.. code:: python
|
||||
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
|
||||
|
||||
for epoch in tqdm.trange(args.epochs):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
start_epoch_time = time.time()
|
||||
for step, data in enumerate(dataloader):
|
||||
# Unpack MiniBatch.
|
||||
compacted_seeds = data.compacted_seeds.T
|
||||
labels = data.labels
|
||||
node_feature = data.node_features["feat"]
|
||||
# Convert sampled subgraphs to DGL blocks.
|
||||
blocks = data.blocks
|
||||
|
||||
# Get the embeddings of the input nodes.
|
||||
y = model(blocks, node_feature)
|
||||
logits = model.predictor(
|
||||
y[compacted_seeds[0]] * y[compacted_seeds[1]]
|
||||
).squeeze()
|
||||
|
||||
# Compute loss.
|
||||
loss = F.binary_cross_entropy_with_logits(logits, labels)
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
end_epoch_time = time.time()
|
||||
|
||||
|
||||
DGL provides the
|
||||
`unsupervised learning GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/graphbolt/link_prediction.py>`__
|
||||
that shows an example of link prediction on homogeneous graphs.
|
||||
|
||||
For heterogeneous graphs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The previous model could be easily extended to heterogeneous graphs. The only
|
||||
difference is that you need to use :class:`~dgl.nn.HeteroGraphConv` to wrap
|
||||
:class:`~dgl.nn.SAGEConv` according to edge types.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class SAGE(nn.Module):
|
||||
def __init__(self, in_size, hidden_size):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.append(dglnn.HeteroGraphConv({
|
||||
rel : dglnn.SAGEConv(in_size, hidden_size, "mean")
|
||||
for rel in rel_names
|
||||
}))
|
||||
self.layers.append(dglnn.HeteroGraphConv({
|
||||
rel : dglnn.SAGEConv(hidden_size, hidden_size, "mean")
|
||||
for rel in rel_names
|
||||
}))
|
||||
self.layers.append(dglnn.HeteroGraphConv({
|
||||
rel : dglnn.SAGEConv(hidden_size, hidden_size, "mean")
|
||||
for rel in rel_names
|
||||
}))
|
||||
self.hidden_size = hidden_size
|
||||
self.predictor = nn.Sequential(
|
||||
nn.Linear(hidden_size, hidden_size),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_size, hidden_size),
|
||||
nn.ReLU(),
|
||||
nn.Linear(hidden_size, 1),
|
||||
)
|
||||
|
||||
def forward(self, blocks, x):
|
||||
hidden_x = x
|
||||
for layer_idx, (layer, block) in enumerate(zip(self.layers, blocks)):
|
||||
hidden_x = layer(block, hidden_x)
|
||||
is_last_layer = layer_idx == len(self.layers) - 1
|
||||
if not is_last_layer:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
return hidden_x
|
||||
|
||||
|
||||
Data loader definition is also very similar to that for homogeneous graph. The
|
||||
only difference is that you need to give edge types for feature fetching.
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = gb.ItemSampler(itemset, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.sample_uniform_negative(graph, 5)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.transform(gb.exclude_seed_edges)
|
||||
datapipe = datapipe.fetch_feature(
|
||||
feature,
|
||||
node_feature_keys={"user": ["feat"], "item": ["feat"]}
|
||||
)
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
If you want to give your own negative sampling function, just inherit from the
|
||||
:class:`~dgl.graphbolt.NegativeSampler` class and override the
|
||||
:meth:`~dgl.graphbolt.NegativeSampler._sample_with_etype` method.
|
||||
|
||||
.. code:: python
|
||||
|
||||
@functional_datapipe("customized_sample_negative")
|
||||
class CustomizedNegativeSampler(dgl.graphbolt.NegativeSampler):
|
||||
def __init__(self, datapipe, k, node_degrees):
|
||||
super().__init__(datapipe, k)
|
||||
# caches the probability distribution
|
||||
self.weights = {
|
||||
etype: node_degrees[etype] ** 0.75 for etype in node_degrees
|
||||
}
|
||||
self.k = k
|
||||
|
||||
def _sample_with_etype(self, seeds, etype):
|
||||
src, _ = seeds.T
|
||||
src = src.repeat_interleave(self.k)
|
||||
dst = self.weights[etype].multinomial(len(src), replacement=True)
|
||||
return src, dst
|
||||
|
||||
datapipe = datapipe.customized_sample_negative(5, node_degrees)
|
||||
|
||||
|
||||
For heterogeneous graphs, node pairs are grouped by edge types. The training
|
||||
loop is again almost the same as that on homogeneous graph, except for computing
|
||||
loss on specific edge type.
|
||||
|
||||
.. code:: python
|
||||
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
|
||||
|
||||
category = "user"
|
||||
for epoch in tqdm.trange(args.epochs):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
start_epoch_time = time.time()
|
||||
for step, data in enumerate(dataloader):
|
||||
# Unpack MiniBatch.
|
||||
compacted_seeds = data.compacted_seeds
|
||||
labels = data.labels
|
||||
node_features = {
|
||||
ntype: data.node_features[(ntype, "feat")]
|
||||
for ntype in data.blocks[0].srctypes
|
||||
}
|
||||
# Convert sampled subgraphs to DGL blocks.
|
||||
blocks = data.blocks
|
||||
# Get the embeddings of the input nodes.
|
||||
y = model(blocks, node_feature)
|
||||
logits = model.predictor(
|
||||
y[category][compacted_pairs[category][:, 0]]
|
||||
* y[category][compacted_pairs[category][:, 1]]
|
||||
).squeeze()
|
||||
|
||||
# Compute loss.
|
||||
loss = F.binary_cross_entropy_with_logits(logits, labels[category])
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
total_loss += loss.item()
|
||||
end_epoch_time = time.time()
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
.. _guide-minibatch-custom-gnn-module:
|
||||
|
||||
6.6 Implementing Custom GNN Module for Mini-batch Training
|
||||
-------------------------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-minibatch-custom-gnn-module>`
|
||||
|
||||
.. note::
|
||||
|
||||
:doc:`This tutorial <tutorials/large/L4_message_passing>` has similar
|
||||
content to this section for the homogeneous graph case.
|
||||
|
||||
|
||||
If you were familiar with how to write a custom GNN module for updating
|
||||
the entire graph for homogeneous or heterogeneous graphs (see
|
||||
:ref:`guide-nn`), the code for computing on
|
||||
MFGs is similar, with the exception that the nodes are divided into
|
||||
input nodes and output nodes.
|
||||
|
||||
For example, consider the following custom graph convolution module
|
||||
code. Note that it is not necessarily among the most efficient implementations
|
||||
- they only serve for an example of how a custom GNN module could look
|
||||
like.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class CustomGraphConv(nn.Module):
|
||||
def __init__(self, in_feats, out_feats):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_feats * 2, out_feats)
|
||||
|
||||
def forward(self, g, h):
|
||||
with g.local_scope():
|
||||
g.ndata['h'] = h
|
||||
g.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))
|
||||
return self.W(torch.cat([g.ndata['h'], g.ndata['h_neigh']], 1))
|
||||
|
||||
If you have a custom message passing NN module for the full graph, and
|
||||
you would like to make it work for MFGs, you only need to rewrite the
|
||||
forward function as follows. Note that the corresponding statements from
|
||||
the full-graph implementation are commented; you can compare the
|
||||
original statements with the new statements.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class CustomGraphConv(nn.Module):
|
||||
def __init__(self, in_feats, out_feats):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_feats * 2, out_feats)
|
||||
|
||||
# h is now a pair of feature tensors for input and output nodes, instead of
|
||||
# a single feature tensor.
|
||||
# def forward(self, g, h):
|
||||
def forward(self, block, h):
|
||||
# with g.local_scope():
|
||||
with block.local_scope():
|
||||
# g.ndata['h'] = h
|
||||
h_src = h
|
||||
h_dst = h[:block.number_of_dst_nodes()]
|
||||
block.srcdata['h'] = h_src
|
||||
block.dstdata['h'] = h_dst
|
||||
|
||||
# g.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))
|
||||
block.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))
|
||||
|
||||
# return self.W(torch.cat([g.ndata['h'], g.ndata['h_neigh']], 1))
|
||||
return self.W(torch.cat(
|
||||
[block.dstdata['h'], block.dstdata['h_neigh']], 1))
|
||||
|
||||
In general, you need to do the following to make your NN module work for
|
||||
MFGs.
|
||||
|
||||
- Obtain the features for output nodes from the input features by
|
||||
slicing the first few rows. The number of rows can be obtained by
|
||||
:meth:`block.number_of_dst_nodes <dgl.DGLGraph.number_of_dst_nodes>`.
|
||||
- Replace
|
||||
:attr:`g.ndata <dgl.DGLGraph.ndata>` with either
|
||||
:attr:`block.srcdata <dgl.DGLGraph.srcdata>` for features on input nodes or
|
||||
:attr:`block.dstdata <dgl.DGLGraph.dstdata>` for features on output nodes, if
|
||||
the original graph has only one node type.
|
||||
- Replace
|
||||
:attr:`g.nodes <dgl.DGLGraph.nodes>` with either
|
||||
:attr:`block.srcnodes <dgl.DGLGraph.srcnodes>` for features on input nodes or
|
||||
:attr:`block.dstnodes <dgl.DGLGraph.dstnodes>` for features on output nodes,
|
||||
if the original graph has multiple node types.
|
||||
- Replace
|
||||
:meth:`g.num_nodes <dgl.DGLGraph.num_nodes>` with either
|
||||
:meth:`block.number_of_src_nodes <dgl.DGLGraph.number_of_src_nodes>` or
|
||||
:meth:`block.number_of_dst_nodes <dgl.DGLGraph.number_of_dst_nodes>` for the number of
|
||||
input nodes or output nodes respectively.
|
||||
|
||||
Heterogeneous graphs
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For heterogeneous graph the way of writing custom GNN modules is
|
||||
similar. For instance, consider the following module that work on full
|
||||
graph.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class CustomHeteroGraphConv(nn.Module):
|
||||
def __init__(self, g, in_feats, out_feats):
|
||||
super().__init__()
|
||||
self.Ws = nn.ModuleDict()
|
||||
for etype in g.canonical_etypes:
|
||||
utype, _, vtype = etype
|
||||
self.Ws[etype] = nn.Linear(in_feats[utype], out_feats[vtype])
|
||||
for ntype in g.ntypes:
|
||||
self.Vs[ntype] = nn.Linear(in_feats[ntype], out_feats[ntype])
|
||||
|
||||
def forward(self, g, h):
|
||||
with g.local_scope():
|
||||
for ntype in g.ntypes:
|
||||
g.nodes[ntype].data['h_dst'] = self.Vs[ntype](h[ntype])
|
||||
g.nodes[ntype].data['h_src'] = h[ntype]
|
||||
for etype in g.canonical_etypes:
|
||||
utype, _, vtype = etype
|
||||
g.update_all(
|
||||
fn.copy_u('h_src', 'm'), fn.mean('m', 'h_neigh'),
|
||||
etype=etype)
|
||||
g.nodes[vtype].data['h_dst'] = g.nodes[vtype].data['h_dst'] + \
|
||||
self.Ws[etype](g.nodes[vtype].data['h_neigh'])
|
||||
return {ntype: g.nodes[ntype].data['h_dst'] for ntype in g.ntypes}
|
||||
|
||||
For ``CustomHeteroGraphConv``, the principle is to replace ``g.nodes``
|
||||
with ``g.srcnodes`` or ``g.dstnodes`` depend on whether the features
|
||||
serve for input or output.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class CustomHeteroGraphConv(nn.Module):
|
||||
def __init__(self, g, in_feats, out_feats):
|
||||
super().__init__()
|
||||
self.Ws = nn.ModuleDict()
|
||||
for etype in g.canonical_etypes:
|
||||
utype, _, vtype = etype
|
||||
self.Ws[etype] = nn.Linear(in_feats[utype], out_feats[vtype])
|
||||
for ntype in g.ntypes:
|
||||
self.Vs[ntype] = nn.Linear(in_feats[ntype], out_feats[ntype])
|
||||
|
||||
def forward(self, g, h):
|
||||
with g.local_scope():
|
||||
for ntype in g.ntypes:
|
||||
h_src, h_dst = h[ntype]
|
||||
g.dstnodes[ntype].data['h_dst'] = self.Vs[ntype](h[ntype])
|
||||
g.srcnodes[ntype].data['h_src'] = h[ntype]
|
||||
for etype in g.canonical_etypes:
|
||||
utype, _, vtype = etype
|
||||
g.update_all(
|
||||
fn.copy_u('h_src', 'm'), fn.mean('m', 'h_neigh'),
|
||||
etype=etype)
|
||||
g.dstnodes[vtype].data['h_dst'] = \
|
||||
g.dstnodes[vtype].data['h_dst'] + \
|
||||
self.Ws[etype](g.dstnodes[vtype].data['h_neigh'])
|
||||
return {ntype: g.dstnodes[ntype].data['h_dst']
|
||||
for ntype in g.ntypes}
|
||||
|
||||
Writing modules that work on homogeneous graphs, bipartite graphs, and MFGs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
All message passing modules in DGL work on homogeneous graphs,
|
||||
unidirectional bipartite graphs (that have two node types and one edge
|
||||
type), and a MFG with one edge type. Essentially, the input graph and
|
||||
feature of a builtin DGL neural network module must satisfy either of
|
||||
the following cases.
|
||||
|
||||
- If the input feature is a pair of tensors, then the input graph must
|
||||
be unidirectional bipartite.
|
||||
- If the input feature is a single tensor and the input graph is a
|
||||
MFG, DGL will automatically set the feature on the output nodes as
|
||||
the first few rows of the input node features.
|
||||
- If the input feature must be a single tensor and the input graph is
|
||||
not a MFG, then the input graph must be homogeneous.
|
||||
|
||||
For example, the following is simplified from the PyTorch implementation
|
||||
of :class:`dgl.nn.pytorch.SAGEConv` (also available in MXNet and Tensorflow)
|
||||
(removing normalization and dealing with only mean aggregation etc.).
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.function as fn
|
||||
class SAGEConv(nn.Module):
|
||||
def __init__(self, in_feats, out_feats):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_feats * 2, out_feats)
|
||||
|
||||
def forward(self, g, h):
|
||||
if isinstance(h, tuple):
|
||||
h_src, h_dst = h
|
||||
elif g.is_block:
|
||||
h_src = h
|
||||
h_dst = h[:g.number_of_dst_nodes()]
|
||||
else:
|
||||
h_src = h_dst = h
|
||||
|
||||
g.srcdata['h'] = h_src
|
||||
g.dstdata['h'] = h_dst
|
||||
g.update_all(fn.copy_u('h', 'm'), fn.sum('m', 'h_neigh'))
|
||||
return F.relu(
|
||||
self.W(torch.cat([g.dstdata['h'], g.dstdata['h_neigh']], 1)))
|
||||
|
||||
:ref:`guide-nn` also provides a walkthrough on :class:`dgl.nn.pytorch.SAGEConv`,
|
||||
which works on unidirectional bipartite graphs, homogeneous graphs, and MFGs.
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
.. _guide-minibatch-node-classification-sampler:
|
||||
|
||||
6.1 Training GNN for Node Classification with Neighborhood Sampling
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-minibatch-node-classification-sampler>`
|
||||
|
||||
To make your model been trained stochastically, you need to do the
|
||||
followings:
|
||||
|
||||
- Define a neighborhood sampler.
|
||||
- Adapt your model for minibatch training.
|
||||
- Modify your training loop.
|
||||
|
||||
The following sub-subsections address these steps one by one.
|
||||
|
||||
Define a neighborhood sampler and data loader
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL provides several neighborhood sampler classes that generates the
|
||||
computation dependencies needed for each layer given the nodes we wish
|
||||
to compute on.
|
||||
|
||||
The simplest neighborhood sampler is :class:`~dgl.graphbolt.NeighborSampler`
|
||||
or the equivalent function-like interface :func:`~dgl.graphbolt.sample_neighbor`
|
||||
which makes the node gather messages from its neighbors.
|
||||
|
||||
To use a sampler provided by DGL, one also need to combine it with
|
||||
:class:`~dgl.graphbolt.DataLoader`, which iterates
|
||||
over a set of indices (nodes in this case) in minibatches.
|
||||
|
||||
For example, the following code creates a DataLoader that
|
||||
iterates over the training node ID set of ``ogbn-arxiv`` in batches,
|
||||
putting the list of generated MFGs onto GPU.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
import dgl.graphbolt as gb
|
||||
import dgl.nn as dglnn
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
dataset = gb.BuiltinDataset("ogbn-arxiv").load()
|
||||
g = dataset.graph
|
||||
feature = dataset.feature
|
||||
train_set = dataset.tasks[0].train_set
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
# Or equivalently:
|
||||
# datapipe = gb.NeighborSampler(datapipe, g, [10, 10])
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
|
||||
Iterating over the DataLoader will yield :class:`~dgl.graphbolt.MiniBatch`
|
||||
which contains a list of specially created graphs representing the computation
|
||||
dependencies on each layer. In order to train with DGL, you can access the
|
||||
*message flow graphs* (MFGs) by calling `mini_batch.blocks`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
mini_batch = next(iter(dataloader))
|
||||
print(mini_batch.blocks)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
See the `Stochastic Training Tutorial
|
||||
<../notebooks/stochastic_training/neighbor_sampling_overview.nblink>`__
|
||||
for the concept of message flow graph.
|
||||
|
||||
If you wish to develop your own neighborhood sampler or you want a more
|
||||
detailed explanation of the concept of MFGs, please refer to
|
||||
:ref:`guide-minibatch-customizing-neighborhood-sampler`.
|
||||
|
||||
|
||||
.. _guide-minibatch-node-classification-model:
|
||||
|
||||
Adapt your model for minibatch training
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If your message passing modules are all provided by DGL, the changes
|
||||
required to adapt your model to minibatch training is minimal. Take a
|
||||
multi-layer GCN as an example. If your model on full graph is
|
||||
implemented as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class TwoLayerGCN(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.conv1 = dglnn.GraphConv(in_features, hidden_features)
|
||||
self.conv2 = dglnn.GraphConv(hidden_features, out_features)
|
||||
|
||||
def forward(self, g, x):
|
||||
x = F.relu(self.conv1(g, x))
|
||||
x = F.relu(self.conv2(g, x))
|
||||
return x
|
||||
|
||||
Then all you need is to replace ``g`` with ``blocks`` generated above.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class StochasticTwoLayerGCN(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.conv1 = dgl.nn.GraphConv(in_features, hidden_features)
|
||||
self.conv2 = dgl.nn.GraphConv(hidden_features, out_features)
|
||||
|
||||
def forward(self, blocks, x):
|
||||
x = F.relu(self.conv1(blocks[0], x))
|
||||
x = F.relu(self.conv2(blocks[1], x))
|
||||
return x
|
||||
|
||||
The DGL ``GraphConv`` modules above accepts an element in ``blocks``
|
||||
generated by the data loader as an argument.
|
||||
|
||||
:ref:`The API reference of each NN module <apinn>` will tell you
|
||||
whether it supports accepting a MFG as an argument.
|
||||
|
||||
If you wish to use your own message passing module, please refer to
|
||||
:ref:`guide-minibatch-custom-gnn-module`.
|
||||
|
||||
Training Loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The training loop simply consists of iterating over the dataset with the
|
||||
customized batching iterator. During each iteration that yields
|
||||
:class:`~dgl.graphbolt.MiniBatch`, we:
|
||||
|
||||
1. Access the node features corresponding to the input nodes via
|
||||
``data.node_features["feat"]``. These features are already moved to the
|
||||
target device (CPU or GPU) by the data loader.
|
||||
|
||||
2. Access the node labels corresponding to the output nodes via
|
||||
``data.labels``. These labels are already moved to the target device
|
||||
(CPU or GPU) by the data loader.
|
||||
|
||||
3. Feed the list of MFGs and the input node features to the multilayer
|
||||
GNN and get the outputs.
|
||||
|
||||
4. Compute the loss and backpropagate.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = StochasticTwoLayerGCN(in_features, hidden_features, out_features)
|
||||
model = model.to(device)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for data in dataloader:
|
||||
input_features = data.node_features["feat"]
|
||||
output_labels = data.labels
|
||||
output_predictions = model(data.blocks, input_features)
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
|
||||
DGL provides an end-to-end stochastic training example `GraphSAGE
|
||||
implementation <https://github.com/dmlc/dgl/blob/master/examples/graphbolt/node_classification.py>`__.
|
||||
|
||||
For heterogeneous graphs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Training a graph neural network for node classification on heterogeneous
|
||||
graph is similar.
|
||||
|
||||
For instance, we have previously seen
|
||||
:ref:`how to train a 2-layer RGCN on full graph <guide-training-rgcn-node-classification>`.
|
||||
The code for RGCN implementation on minibatch training looks very
|
||||
similar to that (with self-loops, non-linearity and basis decomposition
|
||||
removed for simplicity):
|
||||
|
||||
.. code:: python
|
||||
|
||||
class StochasticTwoLayerRGCN(nn.Module):
|
||||
def __init__(self, in_feat, hidden_feat, out_feat, rel_names):
|
||||
super().__init__()
|
||||
self.conv1 = dglnn.HeteroGraphConv({
|
||||
rel : dglnn.GraphConv(in_feat, hidden_feat, norm='right')
|
||||
for rel in rel_names
|
||||
})
|
||||
self.conv2 = dglnn.HeteroGraphConv({
|
||||
rel : dglnn.GraphConv(hidden_feat, out_feat, norm='right')
|
||||
for rel in rel_names
|
||||
})
|
||||
|
||||
def forward(self, blocks, x):
|
||||
x = self.conv1(blocks[0], x)
|
||||
x = self.conv2(blocks[1], x)
|
||||
return x
|
||||
|
||||
The samplers provided by DGL also support heterogeneous graphs.
|
||||
For example, one can still use the provided
|
||||
:class:`~dgl.graphbolt.NeighborSampler` class and
|
||||
:class:`~dgl.graphbolt.DataLoader` class for
|
||||
stochastic training. The only difference is that the itemset is now an
|
||||
instance of :class:`~dgl.graphbolt.HeteroItemSet` which is a dictionary
|
||||
of node types to node IDs.
|
||||
|
||||
.. code:: python
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
dataset = gb.BuiltinDataset("ogbn-mag").load()
|
||||
g = dataset.graph
|
||||
feature = dataset.feature
|
||||
train_set = dataset.tasks[0].train_set
|
||||
datapipe = gb.ItemSampler(train_set, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
# Or equivalently:
|
||||
# datapipe = gb.NeighborSampler(datapipe, g, [10, 10])
|
||||
# For heterogeneous graphs, we need to specify the node feature keys
|
||||
# for each node type.
|
||||
datapipe = datapipe.fetch_feature(
|
||||
feature, node_feature_keys={"author": ["feat"], "paper": ["feat"]}
|
||||
)
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
The training loop is almost the same as that of homogeneous graphs,
|
||||
except for the implementation of ``compute_loss`` that will take in two
|
||||
dictionaries of node types and predictions here.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = StochasticTwoLayerRGCN(in_features, hidden_features, out_features, etypes)
|
||||
model = model.to(device)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for data in dataloader:
|
||||
# For heterogeneous graphs, we need to specify the node types and
|
||||
# feature name when accessing the node features. So does the labels.
|
||||
input_features = {
|
||||
"author": data.node_features[("author", "feat")],
|
||||
"paper": data.node_features[("paper", "feat")]
|
||||
}
|
||||
output_labels = data.labels["paper"]
|
||||
output_predictions = model(data.blocks, input_features)
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
DGL provides an end-to-end stochastic training example `RGCN
|
||||
implementation <https://github.com/dmlc/dgl/blob/master/examples/graphbolt/rgcn/hetero_rgcn.py>`__.
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.. _guide-minibatch-parallelism:
|
||||
|
||||
6.9 Data Loading Parallelism
|
||||
-----------------------
|
||||
|
||||
In minibatch training of GNNs, we usually need to cover several stages to
|
||||
generate a minibatch, including:
|
||||
|
||||
* Iterate over item set and generate minibatch seeds in batch size.
|
||||
* Sample negative items for each seed from graph.
|
||||
* Sample neighbors for each seed from graph.
|
||||
* Exclude seed edges from the sampled subgraphs.
|
||||
* Fetch node and edge features for the sampled subgraphs.
|
||||
* Copy the MiniBatches to the target device.
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = gb.ItemSampler(itemset, batch_size=1024, shuffle=True)
|
||||
datapipe = datapipe.sample_uniform_negative(g, 5)
|
||||
datapipe = datapipe.sample_neighbor(g, [10, 10]) # 2 layers.
|
||||
datapipe = datapipe.transform(gb.exclude_seed_edges)
|
||||
datapipe = datapipe.fetch_feature(feature, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
All these stages are implemented in separate
|
||||
`IterableDataPipe <https://pytorch.org/data/0.7/torchdata.datapipes.iter.html>`__
|
||||
and stacked together with `PyTorch DataLoader <https://pytorch.org/docs/stable/data
|
||||
.html#torch.utils.data.DataLoader>`__.
|
||||
This design allows us to easily customize the data loading process by
|
||||
chaining different data pipes together. For example, if we want to sample
|
||||
negative items for each seed from graph, we can simply chain the
|
||||
:class:`~dgl.graphbolt.NegativeSampler` after the :class:`~dgl.graphbolt.ItemSampler`.
|
||||
|
||||
But simply chaining data pipes together incurs performance overheads as various
|
||||
hardware resources such as CPU, GPU, PCIe, etc. are utilized by different stages.
|
||||
As a result, the data loading mechanism is optimized to minimize the overheads
|
||||
and achieve the best performance.
|
||||
|
||||
In specific, GraphBolt wraps the data pipes before ``fetch_feature`` with
|
||||
multiprocessing which enables multiple processes to run in parallel. As for
|
||||
``fetch_feature`` data pipe, we keep it running in the main process to avoid
|
||||
data movement overheads between processes.
|
||||
|
||||
What's more, in order to overlap the data movement and model computation, we
|
||||
wrap data pipes before ``copy_to`` with
|
||||
`torchdata.datapipes.iter.Perfetcher <https://pytorch.org/data/0.7/generated/
|
||||
torchdata.datapipes.iter.Prefetcher.html>`__
|
||||
which prefetches elements from previous data pipes and puts them into a buffer.
|
||||
Such prefetching is totally transparent to users and requires no extra code. It
|
||||
brings a significant performance boost to minibatch training of GNNs.
|
||||
|
||||
Please refer to the source code of :class:`~dgl.graphbolt.DataLoader`
|
||||
for more details.
|
||||
@@ -0,0 +1,174 @@
|
||||
.. _guide-minibatch-sparse:
|
||||
|
||||
6.5 Training GNN with DGL sparse
|
||||
---------------------------------
|
||||
|
||||
This tutorial demonstrates how to use dgl sparse library to sample on graph and
|
||||
train model. It trains and tests a GraphSAGE model using the sparse sample and
|
||||
compact operators to sample submatrix from the whole matrix.
|
||||
|
||||
Training GNN with DGL sparse is quite similar to
|
||||
:ref:`guide-minibatch-node-classification-sampler`. The major difference is
|
||||
the customized sampler and matrix that represents graph.
|
||||
|
||||
We have cutomized one sampler in
|
||||
:ref:`guide-minibatch-customizing-neighborhood-sampler`. In this tutorial, we
|
||||
will customize another sampler with DGL sparse library as shown below.
|
||||
|
||||
.. code:: python
|
||||
|
||||
@functional_datapipe("sample_sparse_neighbor")
|
||||
class SparseNeighborSampler(SubgraphSampler):
|
||||
def __init__(self, datapipe, matrix, fanouts):
|
||||
super().__init__(datapipe)
|
||||
self.matrix = matrix
|
||||
# Convert fanouts to a list of tensors.
|
||||
self.fanouts = []
|
||||
for fanout in fanouts:
|
||||
if not isinstance(fanout, torch.Tensor):
|
||||
fanout = torch.LongTensor([int(fanout)])
|
||||
self.fanouts.insert(0, fanout)
|
||||
|
||||
def sample_subgraphs(self, seeds):
|
||||
sampled_matrices = []
|
||||
src = seeds
|
||||
|
||||
#####################################################################
|
||||
# (HIGHLIGHT) Using the sparse sample operator to preform random
|
||||
# sampling on the neighboring nodes of the seeds nodes. The sparse
|
||||
# compact operator is then employed to compact and relabel the sampled
|
||||
# matrix, resulting in the sampled matrix and the relabel index.
|
||||
#####################################################################
|
||||
for fanout in self.fanouts:
|
||||
# Sample neighbors.
|
||||
sampled_matrix = self.matrix.sample(1, fanout, ids=src).coalesce()
|
||||
# Compact the sampled matrix.
|
||||
compacted_mat, row_ids = sampled_matrix.compact(0)
|
||||
sampled_matrices.insert(0, compacted_mat)
|
||||
src = row_ids
|
||||
|
||||
return src, sampled_matrices
|
||||
|
||||
Another major difference is the matrix that represents graph. Previously we use
|
||||
:class:`~dgl.graphbolt.FusedCSCSamplingGraph` for sampling. In this tutorial,
|
||||
we use :class:`~dgl.sparse.SparseMatrix` to represent graph.
|
||||
|
||||
.. code:: python
|
||||
|
||||
dataset = gb.BuiltinDataset("ogbn-products").load()
|
||||
g = dataset.graph
|
||||
# Create sparse.
|
||||
N = g.num_nodes
|
||||
A = dglsp.from_csc(g.csc_indptr, g.indices, shape=(N, N))
|
||||
|
||||
|
||||
The remaining code is almost same as node classification tutorial.
|
||||
|
||||
To use this sampler with :class:`~dgl.graphbolt.DataLoader`:
|
||||
|
||||
.. code:: python
|
||||
|
||||
datapipe = gb.ItemSampler(ids, batch_size=1024)
|
||||
# Customize graphbolt sampler by sparse.
|
||||
datapipe = datapipe.sample_sparse_neighbor(A, fanouts)
|
||||
# Use grapbolt to fetch features.
|
||||
datapipe = datapipe.fetch_feature(features, node_feature_keys=["feat"])
|
||||
datapipe = datapipe.copy_to(device)
|
||||
dataloader = gb.DataLoader(datapipe)
|
||||
|
||||
Model definition is shown below:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class SAGEConv(nn.Module):
|
||||
r"""GraphSAGE layer from `Inductive Representation Learning on
|
||||
Large Graphs <https://arxiv.org/pdf/1706.02216.pdf>`__
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_feats,
|
||||
out_feats,
|
||||
):
|
||||
super(SAGEConv, self).__init__()
|
||||
self._in_src_feats, self._in_dst_feats = in_feats, in_feats
|
||||
self._out_feats = out_feats
|
||||
|
||||
self.fc_neigh = nn.Linear(self._in_src_feats, out_feats, bias=False)
|
||||
self.fc_self = nn.Linear(self._in_dst_feats, out_feats, bias=True)
|
||||
self.reset_parameters()
|
||||
|
||||
def reset_parameters(self):
|
||||
gain = nn.init.calculate_gain("relu")
|
||||
nn.init.xavier_uniform_(self.fc_self.weight, gain=gain)
|
||||
nn.init.xavier_uniform_(self.fc_neigh.weight, gain=gain)
|
||||
|
||||
def forward(self, A, feat):
|
||||
feat_src = feat
|
||||
feat_dst = feat[: A.shape[1]]
|
||||
|
||||
# Aggregator type: mean.
|
||||
srcdata = self.fc_neigh(feat_src)
|
||||
# Divided by degree.
|
||||
D_hat = dglsp.diag(A.sum(0)) ** -1
|
||||
A_div = A @ D_hat
|
||||
# Conv neighbors.
|
||||
dstdata = A_div.T @ srcdata
|
||||
|
||||
rst = self.fc_self(feat_dst) + dstdata
|
||||
return rst
|
||||
|
||||
|
||||
class SAGE(nn.Module):
|
||||
def __init__(self, in_size, hid_size, out_size):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
# Three-layer GraphSAGE-gcn.
|
||||
self.layers.append(SAGEConv(in_size, hid_size))
|
||||
self.layers.append(SAGEConv(hid_size, hid_size))
|
||||
self.layers.append(SAGEConv(hid_size, out_size))
|
||||
self.dropout = nn.Dropout(0.5)
|
||||
self.hid_size = hid_size
|
||||
self.out_size = out_size
|
||||
|
||||
def forward(self, sampled_matrices, x):
|
||||
hidden_x = x
|
||||
for layer_idx, (layer, sampled_matrix) in enumerate(
|
||||
zip(self.layers, sampled_matrices)
|
||||
):
|
||||
hidden_x = layer(sampled_matrix, hidden_x)
|
||||
if layer_idx != len(self.layers) - 1:
|
||||
hidden_x = F.relu(hidden_x)
|
||||
hidden_x = self.dropout(hidden_x)
|
||||
return hidden_x
|
||||
|
||||
|
||||
Launch training:
|
||||
|
||||
.. code:: python
|
||||
|
||||
features = dataset.feature
|
||||
# Create GraphSAGE model.
|
||||
in_size = features.size("node", None, "feat")[0]
|
||||
num_classes = dataset.tasks[0].metadata["num_classes"]
|
||||
out_size = num_classes
|
||||
model = SAGE(in_size, 256, out_size).to(device)
|
||||
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=5e-4)
|
||||
|
||||
for epoch in range(10):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
for it, data in enumerate(dataloader):
|
||||
node_feature = data.node_features["feat"].float()
|
||||
blocks = data.sampled_subgraphs
|
||||
y = data.labels
|
||||
y_hat = model(blocks, node_feature)
|
||||
loss = F.cross_entropy(y_hat, y)
|
||||
optimizer.zero_grad()
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
For more details, please refer to the `full example
|
||||
<https://github.com/dmlc/dgl/blob/master/examples/graphbolt/sparse/graphsage.py>`__.
|
||||
@@ -0,0 +1,81 @@
|
||||
.. _guide-minibatch:
|
||||
|
||||
Chapter 6: Stochastic Training on Large Graphs
|
||||
=======================================================
|
||||
|
||||
:ref:`(中文版) <guide_cn-minibatch>`
|
||||
|
||||
If we have a massive graph with, say, millions or even billions of nodes
|
||||
or edges, usually full-graph training as described in
|
||||
:ref:`guide-training`
|
||||
would not work. Consider an :math:`L`-layer graph convolutional network
|
||||
with hidden state size :math:`H` running on an :math:`N`-node graph.
|
||||
Storing the intermediate hidden states requires :math:`O(NLH)` memory,
|
||||
easily exceeding one GPU’s capacity with large :math:`N`.
|
||||
|
||||
This section provides a way to perform stochastic minibatch training,
|
||||
where we do not have to fit the feature of all the nodes into GPU.
|
||||
|
||||
Overview of Neighborhood Sampling Approaches
|
||||
--------------------------------------------
|
||||
|
||||
Neighborhood sampling methods generally work as the following. For each
|
||||
gradient descent step, we select a minibatch of nodes whose final
|
||||
representations at the :math:`L`-th layer are to be computed. We then
|
||||
take all or some of their neighbors at the :math:`L-1` layer. This
|
||||
process continues until we reach the input. This iterative process
|
||||
builds the dependency graph starting from the output and working
|
||||
backwards to the input, as the figure below shows:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_0_0.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
|
||||
With this, one can save the workload and computation resources for
|
||||
training a GNN on a large graph.
|
||||
|
||||
DGL provides a few neighborhood samplers and a pipeline for training a
|
||||
GNN with neighborhood sampling, as well as ways to customize your
|
||||
sampling strategies.
|
||||
|
||||
Roadmap
|
||||
-----------
|
||||
|
||||
The chapter starts with sections for training GNNs stochastically under
|
||||
different scenarios.
|
||||
|
||||
* :ref:`guide-minibatch-node-classification-sampler`
|
||||
* :ref:`guide-minibatch-edge-classification-sampler`
|
||||
* :ref:`guide-minibatch-link-classification-sampler`
|
||||
|
||||
The remaining sections cover more advanced topics, suitable for those who
|
||||
wish to develop new sampling algorithms, new GNN modules compatible with
|
||||
mini-batch training and understand how evaluation and inference can be
|
||||
conducted in mini-batches.
|
||||
|
||||
* :ref:`guide-minibatch-customizing-neighborhood-sampler`
|
||||
* :ref:`guide-minibatch-sparse`
|
||||
* :ref:`guide-minibatch-custom-gnn-module`
|
||||
* :ref:`guide-minibatch-inference`
|
||||
|
||||
The following are performance tips for implementing and using neighborhood
|
||||
sampling:
|
||||
|
||||
* :ref:`guide-minibatch-gpu-sampling`
|
||||
* :ref:`guide-minibatch-parallelism`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
minibatch-node
|
||||
minibatch-edge
|
||||
minibatch-link
|
||||
minibatch-custom-sampler
|
||||
minibatch-sparse
|
||||
minibatch-nn
|
||||
minibatch-inference
|
||||
minibatch-gpu-sampling
|
||||
minibatch-parallelism
|
||||
@@ -0,0 +1,257 @@
|
||||
.. _guide-mixed_precision:
|
||||
|
||||
Chapter 8: Mixed Precision Training
|
||||
===================================
|
||||
DGL is compatible with the `PyTorch Automatic Mixed Precision (AMP) package
|
||||
<https://pytorch.org/docs/stable/amp.html>`_
|
||||
for mixed precision training, thus saving both training time and GPU/CPU memory
|
||||
consumption. This feature requires DGL 0.9+ and 1.1+ for CPU bloat16.
|
||||
|
||||
Message-Passing with Half Precision
|
||||
-----------------------------------
|
||||
DGL allows message-passing on ``float16 (fp16)`` / ``bfloat16 (bf16)``
|
||||
features for both UDFs (User Defined Functions) and built-in functions
|
||||
(e.g., ``dgl.function.sum``, ``dgl.function.copy_u``).
|
||||
|
||||
.. note::
|
||||
Please check bfloat16 support via ``torch.cuda.is_bf16_supported()`` before using it.
|
||||
Typically it requires CUDA >= 11.0 and GPU compute capability >= 8.0.
|
||||
|
||||
The following example shows how to use DGL's message-passing APIs on half-precision
|
||||
features:
|
||||
|
||||
>>> import torch
|
||||
>>> import dgl
|
||||
>>> import dgl.function as fn
|
||||
>>> dev = torch.device('cuda')
|
||||
>>> g = dgl.rand_graph(30, 100).to(dev) # Create a graph on GPU w/ 30 nodes and 100 edges.
|
||||
>>> g.ndata['h'] = torch.rand(30, 16).to(dev).half() # Create fp16 node features.
|
||||
>>> g.edata['w'] = torch.rand(100, 1).to(dev).half() # Create fp16 edge features.
|
||||
>>> # Use DGL's built-in functions for message passing on fp16 features.
|
||||
>>> g.update_all(fn.u_mul_e('h', 'w', 'm'), fn.sum('m', 'x'))
|
||||
>>> g.ndata['x'].dtype
|
||||
torch.float16
|
||||
>>> g.apply_edges(fn.u_dot_v('h', 'x', 'hx'))
|
||||
>>> g.edata['hx'].dtype
|
||||
torch.float16
|
||||
|
||||
>>> # Use UDFs for message passing on fp16 features.
|
||||
>>> def message(edges):
|
||||
... return {'m': edges.src['h'] * edges.data['w']}
|
||||
...
|
||||
>>> def reduce(nodes):
|
||||
... return {'y': torch.sum(nodes.mailbox['m'], 1)}
|
||||
...
|
||||
>>> def dot(edges):
|
||||
... return {'hy': (edges.src['h'] * edges.dst['y']).sum(-1, keepdims=True)}
|
||||
...
|
||||
>>> g.update_all(message, reduce)
|
||||
>>> g.ndata['y'].dtype
|
||||
torch.float16
|
||||
>>> g.apply_edges(dot)
|
||||
>>> g.edata['hy'].dtype
|
||||
torch.float16
|
||||
|
||||
End-to-End Mixed Precision Training
|
||||
-----------------------------------
|
||||
DGL relies on PyTorch's AMP package for mixed precision training,
|
||||
and the user experience is exactly
|
||||
the same as `PyTorch's <https://pytorch.org/docs/stable/notes/amp_examples.html>`_.
|
||||
|
||||
By wrapping the forward pass with ``torch.amp.autocast()``, PyTorch automatically
|
||||
selects the appropriate datatype for each op and tensor. Half precision tensors are memory
|
||||
efficient, most operators on half precision tensors are faster as they leverage GPU tensorcores
|
||||
and CPU special instructon set.
|
||||
|
||||
.. code::
|
||||
|
||||
import torch.nn.functional as F
|
||||
from torch.amp import autocast
|
||||
|
||||
def forward(device_type, g, feat, label, mask, model, amp_dtype):
|
||||
amp_enabled = amp_dtype in (torch.float16, torch.bfloat16)
|
||||
with autocast(device_type, enabled=amp_enabled, dtype=amp_dtype):
|
||||
logit = model(g, feat)
|
||||
loss = F.cross_entropy(logit[mask], label[mask])
|
||||
return loss
|
||||
|
||||
Small Gradients in ``float16`` format have underflow problems (flush to zero).
|
||||
PyTorch provides a ``GradScaler`` module to address this issue. It multiplies
|
||||
the loss by a factor and invokes backward pass on the scaled loss to prevent
|
||||
the underflow problem. It then unscales the computed gradients before the optimizer
|
||||
updates the parameters. The scale factor is determined automatically.
|
||||
Note that ``bfloat16`` doesn't require a ``GradScaler``.
|
||||
|
||||
.. code::
|
||||
|
||||
from torch.cuda.amp import GradScaler
|
||||
|
||||
scaler = GradScaler()
|
||||
|
||||
def backward(scaler, loss, optimizer):
|
||||
scaler.scale(loss).backward()
|
||||
scaler.step(optimizer)
|
||||
scaler.update()
|
||||
|
||||
The following example trains a 3-layer GAT on the Reddit dataset (w/ 114 million edges).
|
||||
Pay attention to the differences in the code when AMP is activated or not.
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import dgl
|
||||
from dgl.data import RedditDataset
|
||||
from dgl.nn import GATConv
|
||||
from dgl.transforms import AddSelfLoop
|
||||
|
||||
amp_dtype = torch.bfloat16 # or torch.float16
|
||||
|
||||
class GAT(nn.Module):
|
||||
def __init__(self,
|
||||
in_feats,
|
||||
n_hidden,
|
||||
n_classes,
|
||||
heads):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.append(GATConv(in_feats, n_hidden, heads[0], activation=F.elu))
|
||||
self.layers.append(GATConv(n_hidden * heads[0], n_hidden, heads[1], activation=F.elu))
|
||||
self.layers.append(GATConv(n_hidden * heads[1], n_classes, heads[2], activation=F.elu))
|
||||
|
||||
def forward(self, g, h):
|
||||
for l, layer in enumerate(self.layers):
|
||||
h = layer(g, h)
|
||||
if l != len(self.layers) - 1:
|
||||
h = h.flatten(1)
|
||||
else:
|
||||
h = h.mean(1)
|
||||
return h
|
||||
|
||||
# Data loading
|
||||
transform = AddSelfLoop()
|
||||
data = RedditDataset(transform)
|
||||
device_type = 'cuda' # or 'cpu'
|
||||
dev = torch.device(device_type)
|
||||
|
||||
g = data[0]
|
||||
g = g.int().to(dev)
|
||||
train_mask = g.ndata['train_mask']
|
||||
feat = g.ndata['feat']
|
||||
label = g.ndata['label']
|
||||
|
||||
in_feats = feat.shape[1]
|
||||
n_hidden = 256
|
||||
n_classes = data.num_classes
|
||||
heads = [1, 1, 1]
|
||||
model = GAT(in_feats, n_hidden, n_classes, heads)
|
||||
model = model.to(dev)
|
||||
model.train()
|
||||
|
||||
# Create optimizer
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=5e-4)
|
||||
|
||||
for epoch in range(100):
|
||||
optimizer.zero_grad()
|
||||
loss = forward(device_type, g, feat, label, train_mask, model, amp_dtype)
|
||||
|
||||
if amp_dtype == torch.float16:
|
||||
# Backprop w/ gradient scaling
|
||||
backward(scaler, loss, optimizer)
|
||||
else:
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
print('Epoch {} | Loss {}'.format(epoch, loss.item()))
|
||||
|
||||
On a NVIDIA V100 (16GB) machine, training this model without fp16 consumes
|
||||
15.2GB GPU memory; with fp16 turned on, the training consumes 12.8G
|
||||
GPU memory, the loss converges to similar values in both settings.
|
||||
If we change the number of heads to ``[2, 2, 2]``, training without fp16
|
||||
triggers GPU OOM(out-of-memory) issue while training with fp16 consumes
|
||||
15.7G GPU memory.
|
||||
|
||||
BFloat16 CPU example
|
||||
-----------------------------------
|
||||
DGL supports running training in the bfloat16 data type on the CPU.
|
||||
This data type doesn't require any CPU feature and can improve the performance of a memory-bound model.
|
||||
Starting with Intel Xeon 4th Generation, which has `AMX
|
||||
<https://www.intel.com/content/www/us/en/products/docs/accelerator-engines/advanced-matrix-extensions/overview.html>`_ instructon set, bfloat16 should significantly improve training and inference performance without huge code changes.
|
||||
Here is an example of simple GCN bfloat16 training:
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import dgl
|
||||
from dgl.data import CiteseerGraphDataset
|
||||
from dgl.nn import GraphConv
|
||||
from dgl.transforms import AddSelfLoop
|
||||
|
||||
|
||||
class GCN(nn.Module):
|
||||
def __init__(self, in_size, hid_size, out_size):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
# two-layer GCN
|
||||
self.layers.append(
|
||||
GraphConv(in_size, hid_size, activation=F.relu)
|
||||
)
|
||||
self.layers.append(GraphConv(hid_size, out_size))
|
||||
self.dropout = nn.Dropout(0.5)
|
||||
|
||||
def forward(self, g, features):
|
||||
h = features
|
||||
for i, layer in enumerate(self.layers):
|
||||
if i != 0:
|
||||
h = self.dropout(h)
|
||||
h = layer(g, h)
|
||||
return h
|
||||
|
||||
|
||||
# Data loading
|
||||
transform = AddSelfLoop()
|
||||
data = CiteseerGraphDataset(transform=transform)
|
||||
|
||||
g = data[0]
|
||||
g = g.int()
|
||||
train_mask = g.ndata['train_mask']
|
||||
feat = g.ndata['feat']
|
||||
label = g.ndata['label']
|
||||
|
||||
in_size = feat.shape[1]
|
||||
hid_size = 16
|
||||
out_size = data.num_classes
|
||||
model = GCN(in_size, hid_size, out_size)
|
||||
|
||||
# Convert model and graph to bfloat16
|
||||
g = dgl.to_bfloat16(g)
|
||||
feat = feat.to(dtype=torch.bfloat16)
|
||||
model = model.to(dtype=torch.bfloat16)
|
||||
|
||||
model.train()
|
||||
|
||||
# Create optimizer
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2, weight_decay=5e-4)
|
||||
loss_fcn = nn.CrossEntropyLoss()
|
||||
|
||||
for epoch in range(100):
|
||||
logits = model(g, feat)
|
||||
loss = loss_fcn(logits[train_mask], label[train_mask])
|
||||
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
print('Epoch {} | Loss {}'.format(epoch, loss.item()))
|
||||
|
||||
The only difference with common training is model and graph conversion before training/inference.
|
||||
|
||||
.. code::
|
||||
g = dgl.to_bfloat16(g)
|
||||
feat = feat.to(dtype=torch.bfloat16)
|
||||
model = model.to(dtype=torch.bfloat16)
|
||||
|
||||
|
||||
DGL is still improving its half-precision support and the compute kernel's
|
||||
performance is far from optimal, please stay tuned to our future updates.
|
||||
@@ -0,0 +1,84 @@
|
||||
.. _guide-nn-construction:
|
||||
|
||||
3.1 DGL NN Module Construction Function
|
||||
---------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-nn-construction>`
|
||||
|
||||
The construction function performs the following steps:
|
||||
|
||||
1. Set options.
|
||||
2. Register learnable parameters or submodules.
|
||||
3. Reset parameters.
|
||||
|
||||
.. code::
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
from dgl.utils import expand_as_pair
|
||||
|
||||
class SAGEConv(nn.Module):
|
||||
def __init__(self,
|
||||
in_feats,
|
||||
out_feats,
|
||||
aggregator_type,
|
||||
bias=True,
|
||||
norm=None,
|
||||
activation=None):
|
||||
super(SAGEConv, self).__init__()
|
||||
|
||||
self._in_src_feats, self._in_dst_feats = expand_as_pair(in_feats)
|
||||
self._out_feats = out_feats
|
||||
self._aggre_type = aggregator_type
|
||||
self.norm = norm
|
||||
self.activation = activation
|
||||
|
||||
In construction function, one first needs to set the data dimensions. For
|
||||
general PyTorch module, the dimensions are usually input dimension,
|
||||
output dimension and hidden dimensions. For graph neural networks, the input
|
||||
dimension can be split into source node dimension and destination node
|
||||
dimension.
|
||||
|
||||
Besides data dimensions, a typical option for graph neural network is
|
||||
aggregation type (``self._aggre_type``). Aggregation type determines how
|
||||
messages on different edges are aggregated for a certain destination
|
||||
node. Commonly used aggregation types include ``mean``, ``sum``,
|
||||
``max``, ``min``. Some modules may apply more complicated aggregation
|
||||
like an ``lstm``.
|
||||
|
||||
``norm`` here is a callable function for feature normalization. In the
|
||||
SAGEConv paper, such normalization can be l2 normalization:
|
||||
:math:`h_v = h_v / \lVert h_v \rVert_2`.
|
||||
|
||||
.. code::
|
||||
|
||||
# aggregator type: mean, pool, lstm, gcn
|
||||
if aggregator_type not in ['mean', 'pool', 'lstm', 'gcn']:
|
||||
raise KeyError('Aggregator type {} not supported.'.format(aggregator_type))
|
||||
if aggregator_type == 'pool':
|
||||
self.fc_pool = nn.Linear(self._in_src_feats, self._in_src_feats)
|
||||
if aggregator_type == 'lstm':
|
||||
self.lstm = nn.LSTM(self._in_src_feats, self._in_src_feats, batch_first=True)
|
||||
if aggregator_type in ['mean', 'pool', 'lstm']:
|
||||
self.fc_self = nn.Linear(self._in_dst_feats, out_feats, bias=bias)
|
||||
self.fc_neigh = nn.Linear(self._in_src_feats, out_feats, bias=bias)
|
||||
self.reset_parameters()
|
||||
|
||||
Register parameters and submodules. In SAGEConv, submodules vary
|
||||
according to the aggregation type. Those modules are pure PyTorch nn
|
||||
modules like ``nn.Linear``, ``nn.LSTM``, etc. At the end of construction
|
||||
function, weight initialization is applied by calling
|
||||
``reset_parameters()``.
|
||||
|
||||
.. code::
|
||||
|
||||
def reset_parameters(self):
|
||||
"""Reinitialize learnable parameters."""
|
||||
gain = nn.init.calculate_gain('relu')
|
||||
if self._aggre_type == 'pool':
|
||||
nn.init.xavier_uniform_(self.fc_pool.weight, gain=gain)
|
||||
if self._aggre_type == 'lstm':
|
||||
self.lstm.reset_parameters()
|
||||
if self._aggre_type != 'gcn':
|
||||
nn.init.xavier_uniform_(self.fc_self.weight, gain=gain)
|
||||
nn.init.xavier_uniform_(self.fc_neigh.weight, gain=gain)
|
||||
@@ -0,0 +1,163 @@
|
||||
.. _guide-nn-forward:
|
||||
|
||||
3.2 DGL NN Module Forward Function
|
||||
----------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-nn-forward>`
|
||||
|
||||
In NN module, ``forward()`` function does the actual message passing and
|
||||
computation. Compared with PyTorch’s NN module which usually takes
|
||||
tensors as the parameters, DGL NN module takes an additional parameter
|
||||
:class:`dgl.DGLGraph`. The
|
||||
workload for ``forward()`` function can be split into three parts:
|
||||
|
||||
- Graph checking and graph type specification.
|
||||
|
||||
- Message passing.
|
||||
|
||||
- Feature update.
|
||||
|
||||
The rest of the section takes a deep dive into the ``forward()`` function in SAGEConv example.
|
||||
|
||||
Graph checking and graph type specification
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
def forward(self, graph, feat):
|
||||
with graph.local_scope():
|
||||
# Specify graph type then expand input feature according to graph type
|
||||
feat_src, feat_dst = expand_as_pair(feat, graph)
|
||||
|
||||
``forward()`` needs to handle many corner cases on the input that can
|
||||
lead to invalid values in computing and message passing. One typical check in conv modules
|
||||
like :class:`~dgl.nn.pytorch.conv.GraphConv` is to verify that the input graph has no 0-in-degree nodes.
|
||||
When a node has 0 in-degree, the ``mailbox`` will be empty and the reduce function will produce
|
||||
all-zero values. This may cause silent regression in model performance. However, in
|
||||
:class:`~dgl.nn.pytorch.conv.SAGEConv` module, the aggregated representation will be concatenated
|
||||
with the original node feature, the output of ``forward()`` will not be all-zero. No such check is
|
||||
needed in this case.
|
||||
|
||||
DGL NN module should be reusable across different types of graph input
|
||||
including: homogeneous graph, heterogeneous
|
||||
graph (:ref:`guide-graph-heterogeneous`), subgraph
|
||||
block (:ref:`guide-minibatch`).
|
||||
|
||||
The math formulas for SAGEConv are:
|
||||
|
||||
.. math::
|
||||
|
||||
|
||||
h_{\mathcal{N}(dst)}^{(l+1)} = \mathrm{aggregate}
|
||||
\left(\{h_{src}^{l}, \forall src \in \mathcal{N}(dst) \}\right)
|
||||
|
||||
.. math::
|
||||
|
||||
h_{dst}^{(l+1)} = \sigma \left(W \cdot \mathrm{concat}
|
||||
(h_{dst}^{l}, h_{\mathcal{N}(dst)}^{l+1}) + b \right)
|
||||
|
||||
.. math::
|
||||
|
||||
h_{dst}^{(l+1)} = \mathrm{norm}(h_{dst}^{l+1})
|
||||
|
||||
One needs to specify the source node feature ``feat_src`` and destination
|
||||
node feature ``feat_dst`` according to the graph type.
|
||||
:meth:`~dgl.utils.expand_as_pair` is a function that specifies the graph
|
||||
type and expand ``feat`` into ``feat_src`` and ``feat_dst``.
|
||||
The detail of this function is shown below.
|
||||
|
||||
.. code::
|
||||
|
||||
def expand_as_pair(input_, g=None):
|
||||
if isinstance(input_, tuple):
|
||||
# Bipartite graph case
|
||||
return input_
|
||||
elif g is not None and g.is_block:
|
||||
# Subgraph block case
|
||||
if isinstance(input_, Mapping):
|
||||
input_dst = {
|
||||
k: F.narrow_row(v, 0, g.number_of_dst_nodes(k))
|
||||
for k, v in input_.items()}
|
||||
else:
|
||||
input_dst = F.narrow_row(input_, 0, g.number_of_dst_nodes())
|
||||
return input_, input_dst
|
||||
else:
|
||||
# Homogeneous graph case
|
||||
return input_, input_
|
||||
|
||||
For homogeneous whole graph training, source nodes and destination nodes
|
||||
are the same. They are all the nodes in the graph.
|
||||
|
||||
For heterogeneous case, the graph can be split into several bipartite
|
||||
graphs, one for each relation. The relations are represented as
|
||||
``(src_type, edge_type, dst_dtype)``. When it identifies that the input feature
|
||||
``feat`` is a tuple, it will treat the graph as bipartite. The first
|
||||
element in the tuple will be the source node feature and the second
|
||||
element will be the destination node feature.
|
||||
|
||||
In mini-batch training, the computing is applied on a subgraph sampled
|
||||
based on a bunch of destination nodes. The subgraph is called as
|
||||
``block`` in DGL. In the block creation phase,
|
||||
``dst nodes`` are in the front of the node list. One can find the
|
||||
``feat_dst`` by the index ``[0:g.number_of_dst_nodes()]``.
|
||||
|
||||
After determining ``feat_src`` and ``feat_dst``, the computing for the
|
||||
above three graph types are the same.
|
||||
|
||||
Message passing and reducing
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
import torch.nn.functional as F
|
||||
from dgl.utils import check_eq_shape
|
||||
|
||||
if self._aggre_type == 'mean':
|
||||
graph.srcdata['h'] = feat_src
|
||||
graph.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'neigh'))
|
||||
h_neigh = graph.dstdata['neigh']
|
||||
elif self._aggre_type == 'gcn':
|
||||
check_eq_shape(feat)
|
||||
graph.srcdata['h'] = feat_src
|
||||
graph.dstdata['h'] = feat_dst
|
||||
graph.update_all(fn.copy_u('h', 'm'), fn.sum('m', 'neigh'))
|
||||
# divide in_degrees
|
||||
degs = graph.in_degrees().to(feat_dst)
|
||||
h_neigh = (graph.dstdata['neigh'] + graph.dstdata['h']) / (degs.unsqueeze(-1) + 1)
|
||||
elif self._aggre_type == 'pool':
|
||||
graph.srcdata['h'] = F.relu(self.fc_pool(feat_src))
|
||||
graph.update_all(fn.copy_u('h', 'm'), fn.max('m', 'neigh'))
|
||||
h_neigh = graph.dstdata['neigh']
|
||||
else:
|
||||
raise KeyError('Aggregator type {} not recognized.'.format(self._aggre_type))
|
||||
|
||||
# GraphSAGE GCN does not require fc_self.
|
||||
if self._aggre_type == 'gcn':
|
||||
rst = self.fc_neigh(h_neigh)
|
||||
else:
|
||||
rst = self.fc_self(h_self) + self.fc_neigh(h_neigh)
|
||||
|
||||
The code actually does message passing and reducing computing. This part
|
||||
of code varies module by module. Note that all the message passing in
|
||||
the above code are implemented using :meth:`~dgl.DGLGraph.update_all` API and
|
||||
``built-in`` message/reduce functions to fully utilize DGL’s performance
|
||||
optimization as described in :ref:`guide-message-passing-efficient`.
|
||||
|
||||
Update feature after reducing for output
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
# activation
|
||||
if self.activation is not None:
|
||||
rst = self.activation(rst)
|
||||
# normalization
|
||||
if self.norm is not None:
|
||||
rst = self.norm(rst)
|
||||
return rst
|
||||
|
||||
The last part of ``forward()`` function is to update the feature after
|
||||
the ``reduce function``. Common update operations are applying
|
||||
activation function and normalization according to the option set in the
|
||||
object construction phase.
|
||||
@@ -0,0 +1,108 @@
|
||||
.. _guide-nn-heterograph:
|
||||
|
||||
3.3 Heterogeneous GraphConv Module
|
||||
------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-nn-heterograph>`
|
||||
|
||||
:class:`~dgl.nn.pytorch.HeteroGraphConv`
|
||||
is a module-level encapsulation to run DGL NN module on heterogeneous
|
||||
graphs. The implementation logic is the same as message passing level API
|
||||
:meth:`~dgl.DGLGraph.multi_update_all`, including:
|
||||
|
||||
- DGL NN module within each relation :math:`r`.
|
||||
- Reduction that merges the results on the same node type from multiple
|
||||
relations.
|
||||
|
||||
This can be formulated as:
|
||||
|
||||
.. math:: h_{dst}^{(l+1)} = \underset{r\in\mathcal{R}, r_{dst}=dst}{AGG} (f_r(g_r, h_{r_{src}}^l, h_{r_{dst}}^l))
|
||||
|
||||
where :math:`f_r` is the NN module for each relation :math:`r`,
|
||||
:math:`AGG` is the aggregation function.
|
||||
|
||||
HeteroGraphConv implementation logic:
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
class HeteroGraphConv(nn.Module):
|
||||
def __init__(self, mods, aggregate='sum'):
|
||||
super(HeteroGraphConv, self).__init__()
|
||||
self.mods = nn.ModuleDict(mods)
|
||||
if isinstance(aggregate, str):
|
||||
# An internal function to get common aggregation functions
|
||||
self.agg_fn = get_aggregate_fn(aggregate)
|
||||
else:
|
||||
self.agg_fn = aggregate
|
||||
|
||||
The heterograph convolution takes a dictionary ``mods`` that maps each
|
||||
relation to an nn module and sets the function that aggregates results on
|
||||
the same node type from multiple relations.
|
||||
|
||||
.. code::
|
||||
|
||||
def forward(self, g, inputs, mod_args=None, mod_kwargs=None):
|
||||
if mod_args is None:
|
||||
mod_args = {}
|
||||
if mod_kwargs is None:
|
||||
mod_kwargs = {}
|
||||
outputs = {nty : [] for nty in g.dsttypes}
|
||||
|
||||
Besides input graph and input tensors, the ``forward()`` function takes
|
||||
two additional dictionary parameters ``mod_args`` and ``mod_kwargs``.
|
||||
These two dictionaries have the same keys as ``self.mods``. They are
|
||||
used as customized parameters when calling their corresponding NN
|
||||
modules in ``self.mods`` for different types of relations.
|
||||
|
||||
An output dictionary is created to hold output tensor for each
|
||||
destination type ``nty`` . Note that the value for each ``nty`` is a
|
||||
list, indicating a single node type may get multiple outputs if more
|
||||
than one relations have ``nty`` as the destination type. ``HeteroGraphConv``
|
||||
will perform a further aggregation on the lists.
|
||||
|
||||
.. code::
|
||||
|
||||
if g.is_block:
|
||||
src_inputs = inputs
|
||||
dst_inputs = {k: v[:g.number_of_dst_nodes(k)] for k, v in inputs.items()}
|
||||
else:
|
||||
src_inputs = dst_inputs = inputs
|
||||
|
||||
for stype, etype, dtype in g.canonical_etypes:
|
||||
rel_graph = g[stype, etype, dtype]
|
||||
if rel_graph.num_edges() == 0:
|
||||
continue
|
||||
if stype not in src_inputs or dtype not in dst_inputs:
|
||||
continue
|
||||
dstdata = self.mods[etype](
|
||||
rel_graph,
|
||||
(src_inputs[stype], dst_inputs[dtype]),
|
||||
*mod_args.get(etype, ()),
|
||||
**mod_kwargs.get(etype, {}))
|
||||
outputs[dtype].append(dstdata)
|
||||
|
||||
The input ``g`` can be a heterogeneous graph or a subgraph block from a
|
||||
heterogeneous graph. As in ordinary NN module, the ``forward()``
|
||||
function need to handle different input graph types separately.
|
||||
|
||||
Each relation is represented as a ``canonical_etype``, which is
|
||||
``(stype, etype, dtype)``. Using ``canonical_etype`` as the key, one can
|
||||
extract out a bipartite graph ``rel_graph``. For bipartite graph, the
|
||||
input feature will be organized as a tuple
|
||||
``(src_inputs[stype], dst_inputs[dtype])``. The NN module for each
|
||||
relation is called and the output is saved. To avoid unnecessary call,
|
||||
relations with no edges or no nodes with the src type will be skipped.
|
||||
|
||||
.. code::
|
||||
|
||||
rsts = {}
|
||||
for nty, alist in outputs.items():
|
||||
if len(alist) != 0:
|
||||
rsts[nty] = self.agg_fn(alist, nty)
|
||||
|
||||
Finally, the results on the same destination node type from multiple
|
||||
relations are aggregated using ``self.agg_fn`` function. Examples can
|
||||
be found in the API Doc for :class:`~dgl.nn.pytorch.HeteroGraphConv`.
|
||||
@@ -0,0 +1,39 @@
|
||||
.. _guide-nn:
|
||||
|
||||
Chapter 3: Building GNN Modules
|
||||
===============================
|
||||
|
||||
:ref:`(中文版) <guide_cn-nn>`
|
||||
|
||||
DGL NN module consists of building blocks for GNN models. An NN module inherits
|
||||
from `Pytorch’s NN Module <https://pytorch.org/docs/1.2.0/_modules/torch/nn/modules/module.html>`__, `MXNet Gluon’s NN Block <http://mxnet.incubator.apache.org/versions/1.6/api/python/docs/api/gluon/nn/index.html>`__ and `TensorFlow’s Keras
|
||||
Layer <https://www.tensorflow.org/api_docs/python/tf/keras/layers>`__, depending on the DNN framework backend in use. In a DGL NN
|
||||
module, the parameter registration in construction function and tensor
|
||||
operation in forward function are the same with the backend framework.
|
||||
In this way, DGL code can be seamlessly integrated into the backend
|
||||
framework code. The major difference lies in the message passing
|
||||
operations that are unique in DGL.
|
||||
|
||||
DGL has integrated many commonly used
|
||||
:ref:`apinn-pytorch-conv`, :ref:`apinn-pytorch-dense-conv`, :ref:`apinn-pytorch-pooling`,
|
||||
and
|
||||
:ref:`apinn-pytorch-util`. We welcome your contribution!
|
||||
|
||||
This chapter takes :class:`~dgl.nn.pytorch.conv.SAGEConv` with Pytorch backend as an example
|
||||
to introduce how to build a custom DGL NN Module.
|
||||
|
||||
Roadmap
|
||||
-------
|
||||
|
||||
* :ref:`guide-nn-construction`
|
||||
* :ref:`guide-nn-forward`
|
||||
* :ref:`guide-nn-heterograph`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
nn-construction
|
||||
nn-forward
|
||||
nn-heterograph
|
||||
@@ -0,0 +1,330 @@
|
||||
.. _guide-training-edge-classification:
|
||||
|
||||
5.2 Edge Classification/Regression
|
||||
---------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-training-edge-classification>`
|
||||
|
||||
Sometimes you wish to predict the attributes on the edges of the graph. In that
|
||||
case, you would like to have an *edge classification/regression* model.
|
||||
|
||||
Here we generate a random graph for edge prediction as a demonstration.
|
||||
|
||||
.. code:: python
|
||||
|
||||
src = np.random.randint(0, 100, 500)
|
||||
dst = np.random.randint(0, 100, 500)
|
||||
# make it symmetric
|
||||
edge_pred_graph = dgl.graph((np.concatenate([src, dst]), np.concatenate([dst, src])))
|
||||
# synthetic node and edge features, as well as edge labels
|
||||
edge_pred_graph.ndata['feature'] = torch.randn(100, 10)
|
||||
edge_pred_graph.edata['feature'] = torch.randn(1000, 10)
|
||||
edge_pred_graph.edata['label'] = torch.randn(1000)
|
||||
# synthetic train-validation-test splits
|
||||
edge_pred_graph.edata['train_mask'] = torch.zeros(1000, dtype=torch.bool).bernoulli(0.6)
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
From the previous section you have learned how to do node classification
|
||||
with a multilayer GNN. The same technique can be applied for computing a
|
||||
hidden representation of any node. The prediction on edges can then be
|
||||
derived from the representation of their incident nodes.
|
||||
|
||||
The most common case of computing the prediction on an edge is to
|
||||
express it as a parameterized function of the representation of its
|
||||
incident nodes, and optionally the features on the edge itself.
|
||||
|
||||
Model Implementation Difference from Node Classification
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Assuming that you compute the node representation with the model from
|
||||
the previous section, you only need to write another component that
|
||||
computes the edge prediction with the
|
||||
:meth:`~dgl.DGLGraph.apply_edges` method.
|
||||
|
||||
For instance, if you would like to compute a score for each edge for
|
||||
edge regression, the following code computes the dot product of incident
|
||||
node representations on each edge.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.function as fn
|
||||
class DotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations computed from the GNN defined
|
||||
# in the node classification section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
|
||||
return graph.edata['score']
|
||||
|
||||
One can also write a prediction function that predicts a vector for each
|
||||
edge with an MLP. Such vector can be used in further downstream tasks,
|
||||
e.g. as logits of a categorical distribution.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MLPPredictor(nn.Module):
|
||||
def __init__(self, in_features, out_classes):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_features * 2, out_classes)
|
||||
|
||||
def apply_edges(self, edges):
|
||||
h_u = edges.src['h']
|
||||
h_v = edges.dst['h']
|
||||
score = self.W(torch.cat([h_u, h_v], 1))
|
||||
return {'score': score}
|
||||
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations computed from the GNN defined
|
||||
# in the node classification section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(self.apply_edges)
|
||||
return graph.edata['score']
|
||||
|
||||
Training loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Given the node representation computation model and an edge predictor
|
||||
model, we can easily write a full-graph training loop where we compute
|
||||
the prediction on all edges.
|
||||
|
||||
The following example takes ``SAGE`` in the previous section as the node
|
||||
representation computation model and ``DotPredictor`` as an edge
|
||||
predictor model.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.sage = SAGE(in_features, hidden_features, out_features)
|
||||
self.pred = DotProductPredictor()
|
||||
def forward(self, g, x):
|
||||
h = self.sage(g, x)
|
||||
return self.pred(g, h)
|
||||
|
||||
In this example, we also assume that the training/validation/test edge
|
||||
sets are identified by boolean masks on edges. This example also does
|
||||
not include early stopping and model saving.
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_features = edge_pred_graph.ndata['feature']
|
||||
edge_label = edge_pred_graph.edata['label']
|
||||
train_mask = edge_pred_graph.edata['train_mask']
|
||||
model = Model(10, 20, 5)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
pred = model(edge_pred_graph, node_features)
|
||||
loss = ((pred[train_mask] - edge_label[train_mask]) ** 2).mean()
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
.. _guide-training-edge-classification-heterogeneous-graph:
|
||||
|
||||
Heterogeneous graph
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Edge classification on heterogeneous graphs is not very different from
|
||||
that on homogeneous graphs. If you wish to perform edge classification
|
||||
on one edge type, you only need to compute the node representation for
|
||||
all node types, and predict on that edge type with
|
||||
:meth:`~dgl.DGLGraph.apply_edges` method.
|
||||
|
||||
For example, to make ``DotProductPredictor`` work on one edge type of a
|
||||
heterogeneous graph, you only need to specify the edge type in
|
||||
``apply_edges`` method.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroDotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h, etype):
|
||||
# h contains the node representations for each edge type computed from
|
||||
# the GNN for heterogeneous graphs defined in the node classification
|
||||
# section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h # assigns 'h' of all node types in one shot
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
You can similarly write a ``HeteroMLPPredictor``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroMLPPredictor(nn.Module):
|
||||
def __init__(self, in_features, out_classes):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_features * 2, out_classes)
|
||||
|
||||
def apply_edges(self, edges):
|
||||
h_u = edges.src['h']
|
||||
h_v = edges.dst['h']
|
||||
score = self.W(torch.cat([h_u, h_v], 1))
|
||||
return {'score': score}
|
||||
|
||||
def forward(self, graph, h, etype):
|
||||
# h contains the node representations for each edge type computed from
|
||||
# the GNN for heterogeneous graphs defined in the node classification
|
||||
# section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h # assigns 'h' of all node types in one shot
|
||||
graph.apply_edges(self.apply_edges, etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
The end-to-end model that predicts a score for each edge on a single
|
||||
edge type will look like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features, rel_names):
|
||||
super().__init__()
|
||||
self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
|
||||
self.pred = HeteroDotProductPredictor()
|
||||
def forward(self, g, x, etype):
|
||||
h = self.sage(g, x)
|
||||
return self.pred(g, h, etype)
|
||||
|
||||
Using the model simply involves feeding the model a dictionary of node
|
||||
types and features.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = Model(10, 20, 5, hetero_graph.etypes)
|
||||
user_feats = hetero_graph.nodes['user'].data['feature']
|
||||
item_feats = hetero_graph.nodes['item'].data['feature']
|
||||
label = hetero_graph.edges['click'].data['label']
|
||||
train_mask = hetero_graph.edges['click'].data['train_mask']
|
||||
node_features = {'user': user_feats, 'item': item_feats}
|
||||
|
||||
Then the training loop looks almost the same as that in homogeneous
|
||||
graph. For instance, if you wish to predict the edge labels on edge type
|
||||
``click``, then you can simply do
|
||||
|
||||
.. code:: python
|
||||
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
pred = model(hetero_graph, node_features, 'click')
|
||||
loss = ((pred[train_mask] - label[train_mask]) ** 2).mean()
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
|
||||
Predicting Edge Type of an Existing Edge on a Heterogeneous Graph
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sometimes you may want to predict which type an existing edge belongs
|
||||
to.
|
||||
|
||||
For instance, given the
|
||||
:ref:`heterogeneous graph example <guide-training-heterogeneous-graph-example>`,
|
||||
your task is given an edge connecting a user and an item, to predict whether
|
||||
the user would ``click`` or ``dislike`` an item.
|
||||
|
||||
This is a simplified version of rating prediction, which is common in
|
||||
recommendation literature.
|
||||
|
||||
You can use a heterogeneous graph convolution network to obtain the node
|
||||
representations. For instance, you can still use the
|
||||
:ref:`RGCN defined previously <guide-training-rgcn-node-classification>`
|
||||
for this purpose.
|
||||
|
||||
To predict the type of an edge, you can simply repurpose the
|
||||
``HeteroDotProductPredictor`` above so that it takes in another graph
|
||||
with only one edge type that “merges” all the edge types to be
|
||||
predicted, and emits the score of each type for every edge.
|
||||
|
||||
In the example here, you will need a graph that has two node types
|
||||
``user`` and ``item``, and one single edge type that “merges” all the
|
||||
edge types from ``user`` and ``item``, i.e. ``click`` and ``dislike``.
|
||||
This can be conveniently created using the following syntax:
|
||||
|
||||
.. code:: python
|
||||
|
||||
dec_graph = hetero_graph['user', :, 'item']
|
||||
|
||||
which returns a heterogeneous graphs with node type ``user`` and ``item``,
|
||||
as well as a single edge type combining all edge types in between, i.e.
|
||||
``click`` and ``dislike``.
|
||||
|
||||
Since the statement above also returns the original edge types as a
|
||||
feature named ``dgl.ETYPE``, we can use that as labels.
|
||||
|
||||
.. code:: python
|
||||
|
||||
edge_label = dec_graph.edata[dgl.ETYPE]
|
||||
|
||||
Given the graph above as input to the edge type predictor module, you
|
||||
can write your predictor module as follows.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroMLPPredictor(nn.Module):
|
||||
def __init__(self, in_dims, n_classes):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_dims * 2, n_classes)
|
||||
|
||||
def apply_edges(self, edges):
|
||||
x = torch.cat([edges.src['h'], edges.dst['h']], 1)
|
||||
y = self.W(x)
|
||||
return {'score': y}
|
||||
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations for each edge type computed from
|
||||
# the GNN for heterogeneous graphs defined in the node classification
|
||||
# section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h # assigns 'h' of all node types in one shot
|
||||
graph.apply_edges(self.apply_edges)
|
||||
return graph.edata['score']
|
||||
|
||||
The model that combines the node representation module and the edge type
|
||||
predictor module is the following:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features, rel_names):
|
||||
super().__init__()
|
||||
self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
|
||||
self.pred = HeteroMLPPredictor(out_features, len(rel_names))
|
||||
def forward(self, g, x, dec_graph):
|
||||
h = self.sage(g, x)
|
||||
return self.pred(dec_graph, h)
|
||||
|
||||
The training loop then simply be the following:
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = Model(10, 20, 5, hetero_graph.etypes)
|
||||
user_feats = hetero_graph.nodes['user'].data['feature']
|
||||
item_feats = hetero_graph.nodes['item'].data['feature']
|
||||
node_features = {'user': user_feats, 'item': item_feats}
|
||||
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
logits = model(hetero_graph, node_features, dec_graph)
|
||||
loss = F.cross_entropy(logits, edge_label)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
|
||||
DGL provides `Graph Convolutional Matrix
|
||||
Completion <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__
|
||||
as an example of rating prediction, which is formulated by predicting
|
||||
the type of an existing edge on a heterogeneous graph. The node
|
||||
representation module in the `model implementation
|
||||
file <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__
|
||||
is called ``GCMCLayer``. The edge type predictor module is called
|
||||
``BiDecoder``. Both of them are more complicated than the setting
|
||||
described here.
|
||||
@@ -0,0 +1,80 @@
|
||||
.. _guide-training-eweight:
|
||||
|
||||
5.5 Use of Edge Weights
|
||||
----------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-training-eweight>`
|
||||
|
||||
In a weighted graph, each edge is associated with a semantically meaningful scalar weight. For
|
||||
example, the edge weights can be connectivity strengths or confidence scores. Naturally, one
|
||||
may want to utilize edge weights in model development.
|
||||
|
||||
Message Passing with Edge Weights
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Most graph neural networks (GNNs) integrate the graph topology information in forward computation
|
||||
by and only by the message passing mechanism. A message passing operation can be viewed as
|
||||
a function that takes an adjacency matrix and additional input features as input arguments. For an
|
||||
unweighted graph, the entries in the adjacency matrix can be zero or one, where a one-valued entry
|
||||
indicates an edge. If this graph is weighted, the non-zero entries can take arbitrary scalar
|
||||
values. This is equivalent to multiplying each message by its corresponding edge weight as in
|
||||
`GAT <https://arxiv.org/pdf/1710.10903.pdf>`__.
|
||||
|
||||
With DGL, one can achieve this by:
|
||||
|
||||
- Saving the edge weights as an edge feature
|
||||
- Multplying the original message by the edge feature in the message function
|
||||
|
||||
Consider the message passing example with DGL below.
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
# Suppose graph.ndata['ft'] stores the input node features
|
||||
graph.update_all(fn.copy_u('ft', 'm'), fn.sum('m', 'ft'))
|
||||
|
||||
One can modify it for edge weight support as follows.
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
# Save edge weights as an edge feature, which is a tensor of shape (E, *)
|
||||
# E is the number of edges
|
||||
graph.edata['w'] = eweight
|
||||
|
||||
# Suppose graph.ndata['ft'] stores the input node features
|
||||
graph.update_all(fn.u_mul_e('ft', 'w', 'm'), fn.sum('m', 'ft'))
|
||||
|
||||
Using NN Modules with Edge Weights
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
One can modify an NN module for edge weight support by modifying all message passing operations
|
||||
in it. The following code snippet is an example for NN module supporting edge weights.
|
||||
|
||||
.. code::
|
||||
import dgl.function as fn
|
||||
import torch.nn as nn
|
||||
|
||||
class GNN(nn.Module):
|
||||
def __init__(self, in_feats, out_feats):
|
||||
super().__init__()
|
||||
self.linear = nn.Linear(in_feats, out_feats)
|
||||
|
||||
def forward(self, g, feat, edge_weight=None):
|
||||
with g.local_scope():
|
||||
g.ndata['ft'] = self.linear(feat)
|
||||
if edge_weight is None:
|
||||
msg_func = fn.copy_u('ft', 'm')
|
||||
else:
|
||||
g.edata['w'] = edge_weight
|
||||
msg_func = fn.u_mul_e('ft', 'w', 'm')
|
||||
g.update_all(msg_func, fn.sum('m', 'ft'))
|
||||
return g.ndata['ft']
|
||||
|
||||
DGL's built-in NN modules support edge weights if they take an optional :attr:`edge_weight`
|
||||
argument in the forward function.
|
||||
|
||||
One may need to normalize raw edge weights. In this regard, DGL provides
|
||||
:func:`~dgl.nn.pytorch.conv.EdgeWeightNorm`.
|
||||
@@ -0,0 +1,304 @@
|
||||
.. _guide-training-graph-classification:
|
||||
|
||||
5.4 Graph Classification
|
||||
----------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-training-graph-classification>`
|
||||
|
||||
Instead of a big single graph, sometimes one might have the data in the
|
||||
form of multiple graphs, for example a list of different types of
|
||||
communities of people. By characterizing the friendship among people in
|
||||
the same community by a graph, one can get a list of graphs to classify. In
|
||||
this scenario, a graph classification model could help identify the type
|
||||
of the community, i.e. to classify each graph based on the structure and
|
||||
overall information.
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
The major difference between graph classification and node
|
||||
classification or link prediction is that the prediction result
|
||||
characterizes the property of the entire input graph. One can perform the
|
||||
message passing over nodes/edges just like the previous tasks, but also
|
||||
needs to retrieve a graph-level representation.
|
||||
|
||||
The graph classification pipeline proceeds as follows:
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/batch/graph_classifier.png
|
||||
:alt: Graph Classification Process
|
||||
|
||||
Graph Classification Process
|
||||
|
||||
From left to right, the common practice is:
|
||||
|
||||
- Prepare a batch of graphs
|
||||
- Perform message passing on the batched graphs to update node/edge features
|
||||
- Aggregate node/edge features into graph-level representations
|
||||
- Classify graphs based on graph-level representations
|
||||
|
||||
Batch of Graphs
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Usually a graph classification task trains on a lot of graphs, and it
|
||||
will be very inefficient to use only one graph at a time when
|
||||
training the model. Borrowing the idea of mini-batch training from
|
||||
common deep learning practice, one can build a batch of multiple graphs
|
||||
and send them together for one training iteration.
|
||||
|
||||
In DGL, one can build a single batched graph from a list of graphs. This
|
||||
batched graph can be simply used as a single large graph, with connected
|
||||
components corresponding to the original small graphs.
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/batch/batch.png
|
||||
:alt: Batched Graph
|
||||
|
||||
Batched Graph
|
||||
|
||||
The following example calls :func:`dgl.batch` on a list of graphs.
|
||||
A batched graph is a single graph, while it also carries information
|
||||
about the list.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
import torch as th
|
||||
|
||||
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])))
|
||||
|
||||
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]))
|
||||
|
||||
Please note that most dgl transformation functions will discard the batch information.
|
||||
In order to maintain such information, please use :func:`dgl.DGLGraph.set_batch_num_nodes`
|
||||
and :func:`dgl.DGLGraph.set_batch_num_edges` on the transformed graph.
|
||||
|
||||
Graph Readout
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
Every graph in the data may have its unique structure, as well as its
|
||||
node and edge features. In order to make a single prediction, one usually
|
||||
aggregates and summarizes over the possibly abundant information. This
|
||||
type of operation is named *readout*. Common readout operations include
|
||||
summation, average, maximum or minimum over all node or edge features.
|
||||
|
||||
Given a graph :math:`g`, one can define the average node feature readout as
|
||||
|
||||
.. math:: h_g = \frac{1}{|\mathcal{V}|}\sum_{v\in \mathcal{V}}h_v
|
||||
|
||||
where :math:`h_g` is the representation of :math:`g`, :math:`\mathcal{V}` is
|
||||
the set of nodes in :math:`g`, :math:`h_v` is the feature of node :math:`v`.
|
||||
|
||||
DGL provides built-in support for common readout operations. For example,
|
||||
:func:`dgl.mean_nodes` implements the above readout operation.
|
||||
|
||||
Once :math:`h_g` is available, one can pass it through an MLP layer for
|
||||
classification output.
|
||||
|
||||
Writing Neural Network Model
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The input to the model is the batched graph with node and edge features.
|
||||
|
||||
Computation on a Batched Graph
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
First, different graphs in a batch are entirely separated, i.e. no edges
|
||||
between any two graphs. With this nice property, all message passing
|
||||
functions still have the same results.
|
||||
|
||||
Second, the readout function on a batched graph will be conducted over
|
||||
each graph separately. Assuming the batch size is :math:`B` and the
|
||||
feature to be aggregated has dimension :math:`D`, the shape of the
|
||||
readout result will be :math:`(B, D)`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
import torch
|
||||
|
||||
g1 = dgl.graph(([0, 1], [1, 0]))
|
||||
g1.ndata['h'] = torch.tensor([1., 2.])
|
||||
g2 = dgl.graph(([0, 1], [1, 2]))
|
||||
g2.ndata['h'] = torch.tensor([1., 2., 3.])
|
||||
|
||||
dgl.readout_nodes(g1, 'h')
|
||||
# tensor([3.]) # 1 + 2
|
||||
|
||||
bg = dgl.batch([g1, g2])
|
||||
dgl.readout_nodes(bg, 'h')
|
||||
# tensor([3., 6.]) # [1 + 2, 1 + 2 + 3]
|
||||
|
||||
Finally, each node/edge feature in a batched graph is obtained by
|
||||
concatenating the corresponding features from all graphs in order.
|
||||
|
||||
.. code:: python
|
||||
|
||||
bg.ndata['h']
|
||||
# tensor([1., 2., 1., 2., 3.])
|
||||
|
||||
Model Definition
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Being aware of the above computation rules, one can define a model as follows.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.nn.pytorch as dglnn
|
||||
import torch.nn as nn
|
||||
|
||||
class Classifier(nn.Module):
|
||||
def __init__(self, in_dim, hidden_dim, n_classes):
|
||||
super(Classifier, self).__init__()
|
||||
self.conv1 = dglnn.GraphConv(in_dim, hidden_dim)
|
||||
self.conv2 = dglnn.GraphConv(hidden_dim, hidden_dim)
|
||||
self.classify = nn.Linear(hidden_dim, n_classes)
|
||||
|
||||
def forward(self, g, h):
|
||||
# Apply graph convolution and activation.
|
||||
h = F.relu(self.conv1(g, h))
|
||||
h = F.relu(self.conv2(g, h))
|
||||
with g.local_scope():
|
||||
g.ndata['h'] = h
|
||||
# Calculate graph representation by average readout.
|
||||
hg = dgl.mean_nodes(g, 'h')
|
||||
return self.classify(hg)
|
||||
|
||||
Training Loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Data Loading
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Once the model is defined, one can start training. Since graph
|
||||
classification deals with lots of relatively small graphs instead of a big
|
||||
single one, one can train efficiently on stochastic mini-batches
|
||||
of graphs, without the need to design sophisticated graph sampling
|
||||
algorithms.
|
||||
|
||||
Assuming that one have a graph classification dataset as introduced in
|
||||
:ref:`guide-data-pipeline`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.data
|
||||
dataset = dgl.data.GINDataset('MUTAG', False)
|
||||
|
||||
Each item in the graph classification dataset is a pair of a graph and
|
||||
its label. One can speed up the data loading process by taking advantage
|
||||
of the GraphDataLoader to iterate over the dataset of
|
||||
graphs in mini-batches.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
dataloader = GraphDataLoader(
|
||||
dataset,
|
||||
batch_size=1024,
|
||||
drop_last=False,
|
||||
shuffle=True)
|
||||
|
||||
Training loop then simply involves iterating over the dataloader and
|
||||
updating the model.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch.nn.functional as F
|
||||
|
||||
# Only an example, 7 is the input feature size
|
||||
model = Classifier(7, 20, 5)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(20):
|
||||
for batched_graph, labels in dataloader:
|
||||
feats = batched_graph.ndata['attr']
|
||||
logits = model(batched_graph, feats)
|
||||
loss = F.cross_entropy(logits, labels)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
For an end-to-end example of graph classification, see
|
||||
`DGL's GIN example <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gin>`__.
|
||||
The training loop is inside the
|
||||
function ``train`` in
|
||||
`main.py <https://github.com/dmlc/dgl/blob/master/examples/pytorch/gin/main.py>`__.
|
||||
The model implementation is inside
|
||||
`gin.py <https://github.com/dmlc/dgl/blob/master/examples/pytorch/gin/gin.py>`__
|
||||
with more components such as using
|
||||
:class:`dgl.nn.pytorch.GINConv` (also available in MXNet and Tensorflow)
|
||||
as the graph convolution layer, batch normalization, etc.
|
||||
|
||||
Heterogeneous graph
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Graph classification with heterogeneous graphs is a little different
|
||||
from that with homogeneous graphs. In addition to graph convolution modules
|
||||
compatible with heterogeneous graphs, one also needs to aggregate over the nodes of
|
||||
different types in the readout function.
|
||||
|
||||
The following shows an example of summing up the average of node
|
||||
representations for each node type.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class RGCN(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats, rel_names):
|
||||
super().__init__()
|
||||
|
||||
self.conv1 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(in_feats, hid_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
self.conv2 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(hid_feats, out_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
|
||||
def forward(self, graph, inputs):
|
||||
# inputs is features of nodes
|
||||
h = self.conv1(graph, inputs)
|
||||
h = {k: F.relu(v) for k, v in h.items()}
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
class HeteroClassifier(nn.Module):
|
||||
def __init__(self, in_dim, hidden_dim, n_classes, rel_names):
|
||||
super().__init__()
|
||||
|
||||
self.rgcn = RGCN(in_dim, hidden_dim, hidden_dim, rel_names)
|
||||
self.classify = nn.Linear(hidden_dim, n_classes)
|
||||
|
||||
def forward(self, g):
|
||||
h = g.ndata['feat']
|
||||
h = self.rgcn(g, h)
|
||||
with g.local_scope():
|
||||
g.ndata['h'] = h
|
||||
# Calculate graph representation by average readout.
|
||||
hg = 0
|
||||
for ntype in g.ntypes:
|
||||
hg = hg + dgl.mean_nodes(g, 'h', ntype=ntype)
|
||||
return self.classify(hg)
|
||||
|
||||
The rest of the code is not different from that for homogeneous graphs.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# etypes is the list of edge types as strings.
|
||||
model = HeteroClassifier(10, 20, 5, etypes)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(20):
|
||||
for batched_graph, labels in dataloader:
|
||||
logits = model(batched_graph)
|
||||
loss = F.cross_entropy(logits, labels)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
@@ -0,0 +1,216 @@
|
||||
.. _guide-training-link-prediction:
|
||||
|
||||
5.3 Link Prediction
|
||||
---------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-training-link-prediction>`
|
||||
|
||||
In some other settings you may want to predict whether an edge exists
|
||||
between two given nodes or not. Such task is called a *link prediction*
|
||||
task.
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
A GNN-based link prediction model represents the likelihood of
|
||||
connectivity between two nodes :math:`u` and :math:`v` as a function of
|
||||
:math:`\boldsymbol{h}_u^{(L)}` and :math:`\boldsymbol{h}_v^{(L)}`, their
|
||||
node representation computed from the multi-layer GNN.
|
||||
|
||||
.. math::
|
||||
|
||||
|
||||
y_{u,v} = \phi(\boldsymbol{h}_u^{(L)}, \boldsymbol{h}_v^{(L)})
|
||||
|
||||
In this section we refer to :math:`y_{u,v}` the *score* between node
|
||||
:math:`u` and node :math:`v`.
|
||||
|
||||
Training a link prediction model involves comparing the scores between
|
||||
nodes connected by an edge against the scores between an arbitrary pair
|
||||
of nodes. For example, given an edge connecting :math:`u` and :math:`v`,
|
||||
we encourage the score between node :math:`u` and :math:`v` to be higher
|
||||
than the score between node :math:`u` and a sampled node :math:`v'` from
|
||||
an arbitrary *noise* distribution :math:`v' \sim P_n(v)`. Such
|
||||
methodology is called *negative sampling*.
|
||||
|
||||
There are lots of loss functions that can achieve the behavior above if
|
||||
minimized. A non-exhaustive list include:
|
||||
|
||||
- Cross-entropy loss:
|
||||
:math:`\mathcal{L} = - \log \sigma (y_{u,v}) - \sum_{v_i \sim P_n(v), i=1,\dots,k}\log \left[ 1 - \sigma (y_{u,v_i})\right]`
|
||||
- BPR loss:
|
||||
:math:`\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} - \log \sigma (y_{u,v} - y_{u,v_i})`
|
||||
- Margin loss:
|
||||
:math:`\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} \max(0, M - y_{u, v} + y_{u, v_i})`,
|
||||
where :math:`M` is a constant hyperparameter.
|
||||
|
||||
You may find this idea familiar if you know what `implicit
|
||||
feedback <https://arxiv.org/ftp/arxiv/papers/1205/1205.2618.pdf>`__ or
|
||||
`noise-contrastive
|
||||
estimation <http://proceedings.mlr.press/v9/gutmann10a/gutmann10a.pdf>`__
|
||||
is.
|
||||
|
||||
The neural network model to compute the score between :math:`u` and
|
||||
:math:`v` is identical to the edge regression model described
|
||||
:ref:`above <guide-training-edge-classification>`.
|
||||
|
||||
Here is an example of using dot product to compute the scores on edges.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class DotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations computed from the GNN defined
|
||||
# in the node classification section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
|
||||
return graph.edata['score']
|
||||
|
||||
Training loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Because our score prediction model operates on graphs, we need to
|
||||
express the negative examples as another graph. The graph will contain
|
||||
all negative node pairs as edges.
|
||||
|
||||
The following shows an example of expressing negative examples as a
|
||||
graph. Each edge :math:`(u,v)` gets :math:`k` negative examples
|
||||
:math:`(u,v_i)` where :math:`v_i` is sampled from a uniform
|
||||
distribution.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def construct_negative_graph(graph, k):
|
||||
src, dst = graph.edges()
|
||||
|
||||
neg_src = src.repeat_interleave(k)
|
||||
neg_dst = torch.randint(0, graph.num_nodes(), (len(src) * k,))
|
||||
return dgl.graph((neg_src, neg_dst), num_nodes=graph.num_nodes())
|
||||
|
||||
The model that predicts edge scores is the same as that of edge
|
||||
classification/regression.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.sage = SAGE(in_features, hidden_features, out_features)
|
||||
self.pred = DotProductPredictor()
|
||||
def forward(self, g, neg_g, x):
|
||||
h = self.sage(g, x)
|
||||
return self.pred(g, h), self.pred(neg_g, h)
|
||||
|
||||
The training loop then repeatedly constructs the negative graph and
|
||||
computes loss.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# Margin loss
|
||||
n_edges = pos_score.shape[0]
|
||||
return (1 - pos_score + neg_score.view(n_edges, -1)).clamp(min=0).mean()
|
||||
|
||||
node_features = graph.ndata['feat']
|
||||
n_features = node_features.shape[1]
|
||||
k = 5
|
||||
model = Model(n_features, 100, 100)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
negative_graph = construct_negative_graph(graph, k)
|
||||
pos_score, neg_score = model(graph, negative_graph, node_features)
|
||||
loss = compute_loss(pos_score, neg_score)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
|
||||
After training, the node representation can be obtained via
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_embeddings = model.sage(graph, node_features)
|
||||
|
||||
There are multiple ways of using the node embeddings. Examples include
|
||||
training downstream classifiers, or doing nearest neighbor search or
|
||||
maximum inner product search for relevant entity recommendation.
|
||||
|
||||
Heterogeneous graphs
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Link prediction on heterogeneous graphs is not very different from that
|
||||
on homogeneous graphs. The following assumes that we are predicting on
|
||||
one edge type, and it is easy to extend it to multiple edge types.
|
||||
|
||||
For example, you can reuse the ``HeteroDotProductPredictor``
|
||||
:ref:`above <guide-training-edge-classification-heterogeneous-graph>`
|
||||
for computing the scores of the edges of an edge type for link prediction.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroDotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h, etype):
|
||||
# h contains the node representations for each node type computed from
|
||||
# the GNN defined in the previous section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
To perform negative sampling, one can construct a negative graph for the
|
||||
edge type you are performing link prediction on as well.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def construct_negative_graph(graph, k, etype):
|
||||
utype, _, vtype = etype
|
||||
src, dst = graph.edges(etype=etype)
|
||||
neg_src = src.repeat_interleave(k)
|
||||
neg_dst = torch.randint(0, graph.num_nodes(vtype), (len(src) * k,))
|
||||
return dgl.heterograph(
|
||||
{etype: (neg_src, neg_dst)},
|
||||
num_nodes_dict={ntype: graph.num_nodes(ntype) for ntype in graph.ntypes})
|
||||
|
||||
The model is a bit different from that in edge classification on
|
||||
heterogeneous graphs since you need to specify edge type where you
|
||||
perform link prediction.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features, rel_names):
|
||||
super().__init__()
|
||||
self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
|
||||
self.pred = HeteroDotProductPredictor()
|
||||
def forward(self, g, neg_g, x, etype):
|
||||
h = self.sage(g, x)
|
||||
return self.pred(g, h, etype), self.pred(neg_g, h, etype)
|
||||
|
||||
The training loop is similar to that of homogeneous graphs.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# Margin loss
|
||||
n_edges = pos_score.shape[0]
|
||||
return (1 - pos_score + neg_score.view(n_edges, -1)).clamp(min=0).mean()
|
||||
|
||||
k = 5
|
||||
model = Model(10, 20, 5, hetero_graph.etypes)
|
||||
user_feats = hetero_graph.nodes['user'].data['feature']
|
||||
item_feats = hetero_graph.nodes['item'].data['feature']
|
||||
node_features = {'user': user_feats, 'item': item_feats}
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
negative_graph = construct_negative_graph(hetero_graph, k, ('user', 'click', 'item'))
|
||||
pos_score, neg_score = model(hetero_graph, negative_graph, node_features, ('user', 'click', 'item'))
|
||||
loss = compute_loss(pos_score, neg_score)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
.. _guide-training-node-classification:
|
||||
|
||||
5.1 Node Classification/Regression
|
||||
--------------------------------------------------
|
||||
|
||||
:ref:`(中文版) <guide_cn-training-node-classification>`
|
||||
|
||||
One of the most popular and widely adopted tasks for graph neural
|
||||
networks is node classification, where each node in the
|
||||
training/validation/test set is assigned a ground truth category from a
|
||||
set of predefined categories. Node regression is similar, where each
|
||||
node in the training/validation/test set is assigned a ground truth
|
||||
number.
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
To classify nodes, graph neural network performs message passing
|
||||
discussed in :ref:`guide-message-passing` to utilize the node’s own
|
||||
features, but also its neighboring node and edge features. Message
|
||||
passing can be repeated multiple rounds to incorporate information from
|
||||
larger range of neighborhood.
|
||||
|
||||
Writing neural network model
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL provides a few built-in graph convolution modules that can perform
|
||||
one round of message passing. In this guide, we choose
|
||||
:class:`dgl.nn.pytorch.SAGEConv` (also available in MXNet and Tensorflow),
|
||||
the graph convolution module for GraphSAGE.
|
||||
|
||||
Usually for deep learning models on graphs we need a multi-layer graph
|
||||
neural network, where we do multiple rounds of message passing. This can
|
||||
be achieved by stacking graph convolution modules as follows.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Contruct a two-layer GNN model
|
||||
import dgl.nn as dglnn
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
class SAGE(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats):
|
||||
super().__init__()
|
||||
self.conv1 = dglnn.SAGEConv(
|
||||
in_feats=in_feats, out_feats=hid_feats, aggregator_type='mean')
|
||||
self.conv2 = dglnn.SAGEConv(
|
||||
in_feats=hid_feats, out_feats=out_feats, aggregator_type='mean')
|
||||
|
||||
def forward(self, graph, inputs):
|
||||
# inputs are features of nodes
|
||||
h = self.conv1(graph, inputs)
|
||||
h = F.relu(h)
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
Note that you can use the model above for not only node classification,
|
||||
but also obtaining hidden node representations for other downstream
|
||||
tasks such as
|
||||
:ref:`guide-training-edge-classification`,
|
||||
:ref:`guide-training-link-prediction`, or
|
||||
:ref:`guide-training-graph-classification`.
|
||||
|
||||
For a complete list of built-in graph convolution modules, please refer
|
||||
to :ref:`apinn`.
|
||||
|
||||
For more details in how DGL
|
||||
neural network modules work and how to write a custom neural network
|
||||
module with message passing please refer to the example in :ref:`guide-nn`.
|
||||
|
||||
Training loop
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Training on the full graph simply involves a forward propagation of the
|
||||
model defined above, and computing the loss by comparing the prediction
|
||||
against ground truth labels on the training nodes.
|
||||
|
||||
This section uses a DGL built-in dataset
|
||||
:class:`dgl.data.CiteseerGraphDataset` to
|
||||
show a training loop. The node features
|
||||
and labels are stored on its graph instance, and the
|
||||
training-validation-test split are also stored on the graph as boolean
|
||||
masks. This is similar to what you have seen in :ref:`guide-data-pipeline`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_features = graph.ndata['feat']
|
||||
node_labels = graph.ndata['label']
|
||||
train_mask = graph.ndata['train_mask']
|
||||
valid_mask = graph.ndata['val_mask']
|
||||
test_mask = graph.ndata['test_mask']
|
||||
n_features = node_features.shape[1]
|
||||
n_labels = int(node_labels.max().item() + 1)
|
||||
|
||||
The following is an example of evaluating your model by accuracy.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def evaluate(model, graph, features, labels, mask):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
logits = model(graph, features)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
_, indices = torch.max(logits, dim=1)
|
||||
correct = torch.sum(indices == labels)
|
||||
return correct.item() * 1.0 / len(labels)
|
||||
|
||||
You can then write our training loop as follows.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = SAGE(in_feats=n_features, hid_feats=100, out_feats=n_labels)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for epoch in range(10):
|
||||
model.train()
|
||||
# forward propagation by using all nodes
|
||||
logits = model(graph, node_features)
|
||||
# compute loss
|
||||
loss = F.cross_entropy(logits[train_mask], node_labels[train_mask])
|
||||
# compute validation accuracy
|
||||
acc = evaluate(model, graph, node_features, node_labels, valid_mask)
|
||||
# backward propagation
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
# Save model if necessary. Omitted in this example.
|
||||
|
||||
|
||||
`GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_full.py>`__
|
||||
provides an end-to-end homogeneous graph node classification example.
|
||||
You could see the corresponding model implementation is in the
|
||||
``GraphSAGE`` class in the example with adjustable number of layers,
|
||||
dropout probabilities, and customizable aggregation functions and
|
||||
nonlinearities.
|
||||
|
||||
.. _guide-training-rgcn-node-classification:
|
||||
|
||||
Heterogeneous graph
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If your graph is heterogeneous, you may want to gather message from
|
||||
neighbors along all edge types. You can use the module
|
||||
:class:`dgl.nn.pytorch.HeteroGraphConv` (also available in MXNet and Tensorflow)
|
||||
to perform message passing
|
||||
on all edge types, then combining different graph convolution modules
|
||||
for each edge type.
|
||||
|
||||
The following code will define a heterogeneous graph convolution module
|
||||
that first performs a separate graph convolution on each edge type, then
|
||||
sums the message aggregations on each edge type as the final result for
|
||||
all node types.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Define a Heterograph Conv model
|
||||
|
||||
class RGCN(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats, rel_names):
|
||||
super().__init__()
|
||||
|
||||
self.conv1 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(in_feats, hid_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
self.conv2 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(hid_feats, out_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
|
||||
def forward(self, graph, inputs):
|
||||
# inputs are features of nodes
|
||||
h = self.conv1(graph, inputs)
|
||||
h = {k: F.relu(v) for k, v in h.items()}
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
``dgl.nn.HeteroGraphConv`` takes in a dictionary of node types and node
|
||||
feature tensors as input, and returns another dictionary of node types
|
||||
and node features.
|
||||
|
||||
So given that we have the user and item features in the
|
||||
:ref:`heterogeneous graph example <guide-training-heterogeneous-graph-example>`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = RGCN(n_hetero_features, 20, n_user_classes, hetero_graph.etypes)
|
||||
user_feats = hetero_graph.nodes['user'].data['feature']
|
||||
item_feats = hetero_graph.nodes['item'].data['feature']
|
||||
labels = hetero_graph.nodes['user'].data['label']
|
||||
train_mask = hetero_graph.nodes['user'].data['train_mask']
|
||||
|
||||
One can simply perform a forward propagation as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_features = {'user': user_feats, 'item': item_feats}
|
||||
h_dict = model(hetero_graph, {'user': user_feats, 'item': item_feats})
|
||||
h_user = h_dict['user']
|
||||
h_item = h_dict['item']
|
||||
|
||||
Training loop is the same as the one for homogeneous graph, except that
|
||||
now you have a dictionary of node representations from which you compute
|
||||
the predictions. For instance, if you are only predicting the ``user``
|
||||
nodes, you can just extract the ``user`` node embeddings from the
|
||||
returned dictionary:
|
||||
|
||||
.. code:: python
|
||||
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for epoch in range(5):
|
||||
model.train()
|
||||
# forward propagation by using all nodes and extracting the user embeddings
|
||||
logits = model(hetero_graph, node_features)['user']
|
||||
# compute loss
|
||||
loss = F.cross_entropy(logits[train_mask], labels[train_mask])
|
||||
# Compute validation accuracy. Omitted in this example.
|
||||
# backward propagation
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
# Save model if necessary. Omitted in the example.
|
||||
|
||||
|
||||
DGL provides an end-to-end example of
|
||||
`RGCN <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/entity_classify.py>`__
|
||||
for node classification. You can see the definition of heterogeneous
|
||||
graph convolution in ``RelGraphConvLayer`` in the `model implementation
|
||||
file <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/model.py>`__.
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
.. _guide-training:
|
||||
|
||||
Chapter 5: Training Graph Neural Networks
|
||||
=====================================================
|
||||
|
||||
:ref:`(中文版) <guide_cn-training>`
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
This chapter discusses how to train a graph neural network for node
|
||||
classification, edge classification, link prediction, and graph
|
||||
classification for small graph(s), by message passing methods introduced
|
||||
in :ref:`guide-message-passing` and neural network modules introduced in
|
||||
:ref:`guide-nn`.
|
||||
|
||||
This chapter assumes that your graph as well as all of its node and edge
|
||||
features can fit into GPU; see :ref:`guide-minibatch` if they cannot.
|
||||
|
||||
The following text assumes that the graph(s) and node/edge features are
|
||||
already prepared. If you plan to use the dataset DGL provides or other
|
||||
compatible ``DGLDataset`` as is described in :ref:`guide-data-pipeline`, you can
|
||||
get the graph for a single-graph dataset with something like
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
|
||||
dataset = dgl.data.CiteseerGraphDataset()
|
||||
graph = dataset[0]
|
||||
|
||||
|
||||
Note: In this chapter we will use PyTorch as backend.
|
||||
|
||||
.. _guide-training-heterogeneous-graph-example:
|
||||
|
||||
Heterogeneous Graphs
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sometimes you would like to work on heterogeneous graphs. Here we take a
|
||||
synthetic heterogeneous graph as an example for demonstrating node
|
||||
classification, edge classification, and link prediction tasks.
|
||||
|
||||
The synthetic heterogeneous graph ``hetero_graph`` has these edge types:
|
||||
|
||||
- ``('user', 'follow', 'user')``
|
||||
- ``('user', 'followed-by', 'user')``
|
||||
- ``('user', 'click', 'item')``
|
||||
- ``('item', 'clicked-by', 'user')``
|
||||
- ``('user', 'dislike', 'item')``
|
||||
- ``('item', 'disliked-by', 'user')``
|
||||
|
||||
.. code:: python
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
n_users = 1000
|
||||
n_items = 500
|
||||
n_follows = 3000
|
||||
n_clicks = 5000
|
||||
n_dislikes = 500
|
||||
n_hetero_features = 10
|
||||
n_user_classes = 5
|
||||
n_max_clicks = 10
|
||||
|
||||
follow_src = np.random.randint(0, n_users, n_follows)
|
||||
follow_dst = np.random.randint(0, n_users, n_follows)
|
||||
click_src = np.random.randint(0, n_users, n_clicks)
|
||||
click_dst = np.random.randint(0, n_items, n_clicks)
|
||||
dislike_src = np.random.randint(0, n_users, n_dislikes)
|
||||
dislike_dst = np.random.randint(0, n_items, n_dislikes)
|
||||
|
||||
hetero_graph = dgl.heterograph({
|
||||
('user', 'follow', 'user'): (follow_src, follow_dst),
|
||||
('user', 'followed-by', 'user'): (follow_dst, follow_src),
|
||||
('user', 'click', 'item'): (click_src, click_dst),
|
||||
('item', 'clicked-by', 'user'): (click_dst, click_src),
|
||||
('user', 'dislike', 'item'): (dislike_src, dislike_dst),
|
||||
('item', 'disliked-by', 'user'): (dislike_dst, dislike_src)})
|
||||
|
||||
hetero_graph.nodes['user'].data['feature'] = torch.randn(n_users, n_hetero_features)
|
||||
hetero_graph.nodes['item'].data['feature'] = torch.randn(n_items, n_hetero_features)
|
||||
hetero_graph.nodes['user'].data['label'] = torch.randint(0, n_user_classes, (n_users,))
|
||||
hetero_graph.edges['click'].data['label'] = torch.randint(1, n_max_clicks, (n_clicks,)).float()
|
||||
# randomly generate training masks on user nodes and click edges
|
||||
hetero_graph.nodes['user'].data['train_mask'] = torch.zeros(n_users, dtype=torch.bool).bernoulli(0.6)
|
||||
hetero_graph.edges['click'].data['train_mask'] = torch.zeros(n_clicks, dtype=torch.bool).bernoulli(0.6)
|
||||
|
||||
|
||||
Roadmap
|
||||
------------
|
||||
|
||||
The chapter has four sections, each for one type of graph learning tasks.
|
||||
|
||||
* :ref:`guide-training-node-classification`
|
||||
* :ref:`guide-training-edge-classification`
|
||||
* :ref:`guide-training-link-prediction`
|
||||
* :ref:`guide-training-graph-classification`
|
||||
* :ref:`guide-training-eweight`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
training-node
|
||||
training-edge
|
||||
training-link
|
||||
training-graph
|
||||
training-eweight
|
||||
Reference in New Issue
Block a user