chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
.. _guide_cn-data-pipeline-dataset:
|
||||
|
||||
4.1 DGLDataset类
|
||||
--------------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-dataset>`
|
||||
|
||||
:class:`~dgl.data.DGLDataset` 是处理、导入和保存 :ref:`apidata` 中定义的图数据集的基类。
|
||||
它实现了用于处理图数据的基本模版。下面的流程图展示了这个模版的工作方式。
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/userguide_data_flow.png
|
||||
:align: center
|
||||
|
||||
在类DGLDataset中定义的图数据处理模版的流程图。
|
||||
|
||||
为了处理位于远程服务器或本地磁盘上的图数据集,下面的例子中定义了一个类,称为 ``MyDataset``,
|
||||
它继承自 :class:`dgl.data.DGLDataset`。
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import DGLDataset
|
||||
|
||||
class MyDataset(DGLDataset):
|
||||
""" 用于在DGL中自定义图数据集的模板:
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
下载原始数据集的url。
|
||||
raw_dir : str
|
||||
指定下载数据的存储目录或已下载数据的存储目录。默认: ~/.dgl/
|
||||
save_dir : str
|
||||
处理完成的数据集的保存目录。默认:raw_dir指定的值
|
||||
force_reload : bool
|
||||
是否重新导入数据集。默认:False
|
||||
verbose : bool
|
||||
是否打印进度信息。
|
||||
"""
|
||||
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):
|
||||
# 将原始数据下载到本地磁盘
|
||||
pass
|
||||
|
||||
def process(self):
|
||||
# 将原始数据处理为图、标签和数据集划分的掩码
|
||||
pass
|
||||
|
||||
def __getitem__(self, idx):
|
||||
# 通过idx得到与之对应的一个样本
|
||||
pass
|
||||
|
||||
def __len__(self):
|
||||
# 数据样本的数量
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
# 将处理后的数据保存至 `self.save_path`
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
# 从 `self.save_path` 导入处理后的数据
|
||||
pass
|
||||
|
||||
def has_cache(self):
|
||||
# 检查在 `self.save_path` 中是否存有处理后的数据
|
||||
pass
|
||||
|
||||
:class:`~dgl.data.DGLDataset` 类有抽象函数 ``process()``,
|
||||
``__getitem__(idx)`` 和 ``__len__()``。子类必须实现这些函数。同时DGL也建议实现保存和导入函数,
|
||||
因为对于处理后的大型数据集,这么做可以节省大量的时间,
|
||||
并且有多个已有的API可以简化此操作(请参阅 :ref:`guide_cn-data-pipeline-savenload`)。
|
||||
|
||||
请注意, :class:`~dgl.data.DGLDataset` 的目的是提供一种标准且方便的方式来导入图数据。
|
||||
用户可以存储有关数据集的图、特征、标签、掩码,以及诸如类别数、标签数等基本信息。
|
||||
诸如采样、划分或特征归一化等操作建议在 :class:`~dgl.data.DGLDataset` 子类之外完成。
|
||||
|
||||
本章的后续部分展示了实现这些函数的最佳实践。
|
||||
@@ -0,0 +1,50 @@
|
||||
.. _guide_cn-data-pipeline-download:
|
||||
|
||||
4.2 下载原始数据(可选)
|
||||
--------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-download>`
|
||||
|
||||
如果用户的数据集已经在本地磁盘中,请确保它被存放在目录 ``raw_dir`` 中。
|
||||
如果用户想在任何地方运行代码而又不想自己下载数据并将其移动到正确的目录中,则可以通过实现函数 ``download()`` 来自动完成。
|
||||
|
||||
如果数据集是一个zip文件,可以直接继承 :class:`dgl.data.DGLBuiltinDataset` 类。后者支持解压缩zip文件。
|
||||
否则用户需要自己实现 ``download()``,具体可以参考 :class:`~dgl.data.QM7bDataset` 类:
|
||||
|
||||
.. code::
|
||||
|
||||
import os
|
||||
from dgl.data.utils import download
|
||||
|
||||
def download(self):
|
||||
# 存储文件的路径
|
||||
file_path = os.path.join(self.raw_dir, self.name + '.mat')
|
||||
# 下载文件
|
||||
download(self.url, path=file_path)
|
||||
|
||||
上面的代码将一个.mat文件下载到目录 ``self.raw_dir``。如果文件是.gz、.tar、.tar.gz或.tgz文件,请使用
|
||||
:func:`~dgl.data.utils.extract_archive` 函数进行解压缩。以下代码展示了如何在
|
||||
:class:`~dgl.data.BitcoinOTCDataset` 类中下载一个.gz文件:
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data.utils import download, check_sha1
|
||||
|
||||
def download(self):
|
||||
# 存储文件的路径,请确保使用与原始文件名相同的后缀
|
||||
gz_file_path = os.path.join(self.raw_dir, self.name + '.csv.gz')
|
||||
# 下载文件
|
||||
download(self.url, path=gz_file_path)
|
||||
# 检查 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'))
|
||||
# 将文件解压缩到目录self.raw_dir下的self.name目录中
|
||||
self._extract_gz(gz_file_path, self.raw_path)
|
||||
|
||||
上面的代码会将文件解压缩到 ``self.raw_dir`` 下的目录 ``self.name`` 中。
|
||||
如果该类继承自 :class:`dgl.data.DGLBuiltinDataset` 来处理zip文件,
|
||||
则它也会将文件解压缩到目录 ``self.name`` 中。
|
||||
|
||||
一个可选项是用户可以按照上面的示例检查下载后文件的SHA-1字符串,以防作者在远程服务器上更改了文件。
|
||||
@@ -0,0 +1,76 @@
|
||||
.. _guide_cn-data-pipeline-loadogb:
|
||||
|
||||
4.5 使用ogb包导入OGB数据集
|
||||
----------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-loadogb>`
|
||||
|
||||
`Open Graph Benchmark (OGB) <https://ogb.stanford.edu/docs/home/>`__ 是一个图深度学习的基准数据集。
|
||||
官方的 `ogb <https://github.com/snap-stanford/ogb>`__ 包提供了用于下载和处理OGB数据集到
|
||||
:class:`dgl.data.DGLGraph` 对象的API。本节会介绍它们的基本用法。
|
||||
|
||||
首先使用pip安装ogb包:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install ogb
|
||||
|
||||
|
||||
以下代码显示了如何为 *Graph Property Prediction* 任务加载数据集。
|
||||
|
||||
.. code::
|
||||
|
||||
# 载入OGB的Graph Property Prediction数据集
|
||||
import dgl
|
||||
import torch
|
||||
from ogb.graphproppred import DglGraphPropPredDataset
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
|
||||
def _collate_fn(batch):
|
||||
# 小批次是一个元组(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
|
||||
|
||||
# 载入数据集
|
||||
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)
|
||||
|
||||
加载 *Node Property Prediction* 数据集类似,但要注意的是这种数据集只有一个图对象。
|
||||
|
||||
.. code::
|
||||
|
||||
# 载入OGB的Node Property Prediction数据集
|
||||
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
|
||||
# 在Node Property Prediction数据集里只有一个图
|
||||
g, labels = dataset[0]
|
||||
# 获取划分的标签
|
||||
train_label = dataset.labels[split_idx['train']]
|
||||
valid_label = dataset.labels[split_idx['valid']]
|
||||
test_label = dataset.labels[split_idx['test']]
|
||||
|
||||
每个 *Link Property Prediction* 数据集也只包括一个图。
|
||||
|
||||
.. code::
|
||||
|
||||
# 载入OGB的Link Property Prediction数据集
|
||||
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,300 @@
|
||||
.. _guide_cn-data-pipeline-process:
|
||||
|
||||
4.3 处理数据
|
||||
----------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-process>`
|
||||
|
||||
用户可以在 ``process()`` 函数中实现数据处理。该函数假定原始数据已经位于 ``self.raw_dir`` 目录中。
|
||||
|
||||
图上的机器学习任务通常有三种类型:整图分类、节点分类和链接预测。本节将展示如何处理与这些任务相关的数据集。
|
||||
|
||||
本节重点介绍了处理图、特征和划分掩码的标准方法。用户指南将以内置数据集为例,并跳过从文件构建图的实现。
|
||||
用户可以参考 :ref:`guide_cn-graph-external` 以查看如何从外部数据源构建图的完整指南。
|
||||
|
||||
处理整图分类数据集
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
整图分类数据集与用小批次训练的典型机器学习任务中的大多数数据集类似。
|
||||
因此,需要将原始数据处理为 :class:`dgl.DGLGraph` 对象的列表和标签张量的列表。
|
||||
此外,如果原始数据已被拆分为多个文件,则可以添加参数 ``split`` 以导入数据的特定部分。
|
||||
|
||||
下面以 :class:`~dgl.data.QM7bDataset` 为例:
|
||||
|
||||
.. 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'
|
||||
# 将数据处理为图列表和标签列表
|
||||
self.graphs, self.label = self._load_graph(mat_path)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
""" 通过idx获取对应的图和标签
|
||||
|
||||
Parameters
|
||||
----------
|
||||
idx : int
|
||||
Item index
|
||||
|
||||
Returns
|
||||
-------
|
||||
(dgl.DGLGraph, Tensor)
|
||||
"""
|
||||
return self.graphs[idx], self.label[idx]
|
||||
|
||||
def __len__(self):
|
||||
"""数据集中图的数量"""
|
||||
return len(self.graphs)
|
||||
|
||||
函数 ``process()`` 将原始数据处理为图列表和标签列表。用户必须实现 ``__getitem__(idx)`` 和 ``__len__()`` 以进行迭代。
|
||||
DGL建议让 ``__getitem__(idx)`` 返回如上面代码所示的元组 ``(图,标签)``。
|
||||
用户可以参考 `QM7bDataset源代码 <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/qm7b.html#QM7bDataset>`__
|
||||
以获得 ``self._load_graph()`` 和 ``__getitem__`` 的详细信息。
|
||||
|
||||
用户还可以向类添加属性以指示一些有用的数据集信息。在 :class:`~dgl.data.QM7bDataset` 中,
|
||||
用户可以添加属性 ``num_tasks`` 来指示此多任务数据集中的预测任务总数:
|
||||
|
||||
.. code::
|
||||
|
||||
@property
|
||||
def num_tasks(self):
|
||||
"""每个图的标签数,即预测任务数。"""
|
||||
return 14
|
||||
|
||||
在编写完这些代码之后,用户可以按如下所示的方式来使用 :class:`~dgl.data.QM7bDataset`:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl
|
||||
import torch
|
||||
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
|
||||
# 数据导入
|
||||
dataset = QM7bDataset()
|
||||
num_tasks = dataset.num_tasks
|
||||
|
||||
# 创建 dataloaders
|
||||
dataloader = GraphDataLoader(dataset, batch_size=1, shuffle=True)
|
||||
|
||||
# 训练
|
||||
for epoch in range(100):
|
||||
for g, labels in dataloader:
|
||||
# 用户自己的训练代码
|
||||
pass
|
||||
|
||||
训练整图分类模型的完整指南可以在 :ref:`guide_cn-training-graph-classification` 中找到。
|
||||
|
||||
有关整图分类数据集的更多示例,用户可以参考 :ref:`guide_cn-training-graph-classification`:
|
||||
|
||||
* :ref:`gindataset`
|
||||
|
||||
* :ref:`minigcdataset`
|
||||
|
||||
* :ref:`qm7bdata`
|
||||
|
||||
* :ref:`tudata`
|
||||
|
||||
处理节点分类数据集
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
与整图分类不同,节点分类通常在单个图上进行。因此数据集的划分是在图的节点集上进行。
|
||||
DGL建议使用节点掩码来指定数据集的划分。
|
||||
本节以内置数据集 `CitationGraphDataset <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/citation_graph.html#CitationGraphDataset>`__ 为例:
|
||||
|
||||
此外,DGL推荐重新排列图的节点/边,使得相邻节点/边的ID位于邻近区间内。这个过程
|
||||
可以提高节点/边的邻居的局部性,为后续在图上进行的计算与分析的性能改善提供可能。
|
||||
DGL提供了名为 :func:`dgl.reorder_graph` 的API用于此优化。更多细节,请参考
|
||||
下面例子中的 ``process()`` 的部分。
|
||||
|
||||
.. 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):
|
||||
# 跳过一些处理的代码
|
||||
# === 跳过数据处理 ===
|
||||
|
||||
# 构建图
|
||||
g = dgl.graph(graph)
|
||||
|
||||
# 划分掩码
|
||||
g.ndata['train_mask'] = train_mask
|
||||
g.ndata['val_mask'] = val_mask
|
||||
g.ndata['test_mask'] = test_mask
|
||||
|
||||
# 节点的标签
|
||||
g.ndata['label'] = torch.tensor(labels)
|
||||
|
||||
# 节点的特征
|
||||
g.ndata['feat'] = torch.tensor(_preprocess_features(features),
|
||||
dtype=F.data_type_dict['float32'])
|
||||
self._num_tasks = onehot_labels.shape[1]
|
||||
self._labels = labels
|
||||
# 重排图以获得更优的局部性
|
||||
self._g = dgl.reorder_graph(g)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "这个数据集里只有一个图"
|
||||
return self._g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
为简便起见,这里省略了 ``process()`` 中的一些代码,以突出展示用于处理节点分类数据集的关键部分:划分掩码。
|
||||
节点特征和节点的标签被存储在 ``g.ndata`` 中。详细的实现请参考
|
||||
`CitationGraphDataset源代码 <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/citation_graph.html#CitationGraphDataset>`__ 。
|
||||
|
||||
请注意,这里 ``__getitem__(idx)`` 和 ``__len__()`` 的实现也发生了变化,
|
||||
这是因为节点分类任务通常只用一个图。掩码在PyTorch和TensorFlow中是bool张量,在MXNet中是float张量。
|
||||
|
||||
下面中使用 :class:`dgl.data.CitationGraphDataset` 的子类 :class:`dgl.data.CiteseerGraphDataset`
|
||||
来演示如何使用用于节点分类的数据集:
|
||||
|
||||
.. code::
|
||||
|
||||
# 导入数据
|
||||
dataset = CiteseerGraphDataset(raw_dir='')
|
||||
graph = dataset[0]
|
||||
|
||||
# 获取划分的掩码
|
||||
train_mask = graph.ndata['train_mask']
|
||||
val_mask = graph.ndata['val_mask']
|
||||
test_mask = graph.ndata['test_mask']
|
||||
|
||||
# 获取节点特征
|
||||
feats = graph.ndata['feat']
|
||||
|
||||
# 获取标签
|
||||
labels = graph.ndata['label']
|
||||
|
||||
:ref:`guide_cn-training-node-classification` 提供了训练节点分类模型的完整指南。
|
||||
|
||||
有关节点分类数据集的更多示例,用户可以参考以下内置数据集:
|
||||
|
||||
* :ref:`citationdata`
|
||||
|
||||
* :ref:`corafulldata`
|
||||
|
||||
* :ref:`amazoncobuydata`
|
||||
|
||||
* :ref:`coauthordata`
|
||||
|
||||
* :ref:`karateclubdata`
|
||||
|
||||
* :ref:`ppidata`
|
||||
|
||||
* :ref:`redditdata`
|
||||
|
||||
* :ref:`sbmdata`
|
||||
|
||||
* :ref:`sstdata`
|
||||
|
||||
* :ref:`rdfdata`
|
||||
|
||||
处理链接预测数据集
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
链接预测数据集的处理与节点分类相似,数据集中通常只有一个图。
|
||||
|
||||
本节以内置的数据集 `KnowledgeGraphDataset <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/knowledge_graph.html#KnowledgeGraphDataset>`__
|
||||
为例,同时省略了详细的数据处理代码以突出展示处理链接预测数据集的关键部分:
|
||||
|
||||
.. code::
|
||||
|
||||
# 创建链接预测数据集示例
|
||||
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):
|
||||
# 跳过一些处理的代码
|
||||
# === 跳过数据处理 ===
|
||||
|
||||
# 划分掩码
|
||||
g.edata['train_mask'] = train_mask
|
||||
g.edata['val_mask'] = val_mask
|
||||
g.edata['test_mask'] = test_mask
|
||||
|
||||
# 边类型
|
||||
g.edata['etype'] = etype
|
||||
|
||||
# 节点类型
|
||||
g.ndata['ntype'] = ntype
|
||||
self._g = g
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "这个数据集只有一个图"
|
||||
return self._g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
|
||||
如代码所示,图的 ``edata`` 存储了划分掩码。在
|
||||
`KnowledgeGraphDataset 源代码 <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/knowledge_graph.html#KnowledgeGraphDataset>`__
|
||||
中可以查看完整的代码。下面使用 ``KnowledgeGraphDataset``的子类 :class:`dgl.data.FB15k237Dataset` 来做演示如何使用用于链路预测的数据集:
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import FB15k237Dataset
|
||||
|
||||
# 导入数据
|
||||
dataset = FB15k237Dataset()
|
||||
graph = dataset[0]
|
||||
|
||||
# 获取训练集掩码
|
||||
train_mask = graph.edata['train_mask']
|
||||
train_idx = torch.nonzero(train_mask, as_tuple=False).squeeze()
|
||||
src, dst = graph.edges(train_idx)
|
||||
|
||||
# 获取训练集中的边类型
|
||||
rel = graph.edata['etype'][train_idx]
|
||||
|
||||
有关训练链接预测模型的完整指南,请参见 :ref:`guide_cn-training-link-prediction`。
|
||||
|
||||
有关链接预测数据集的更多示例,请参考DGL的内置数据集:
|
||||
|
||||
* :ref:`kgdata`
|
||||
|
||||
* :ref:`bitcoinotcdata`
|
||||
@@ -0,0 +1,45 @@
|
||||
.. _guide_cn-data-pipeline-savenload:
|
||||
|
||||
4.4 保存和加载数据
|
||||
----------------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-savenload>`
|
||||
|
||||
DGL建议用户实现保存和加载数据的函数,将处理后的数据缓存在本地磁盘中。
|
||||
这样在多数情况下可以帮用户节省大量的数据处理时间。DGL提供了4个函数让任务变得简单。
|
||||
|
||||
- :func:`dgl.save_graphs` 和 :func:`dgl.load_graphs`: 保存DGLGraph对象和标签到本地磁盘和从本地磁盘读取它们。
|
||||
- :func:`dgl.data.utils.save_info` 和 :func:`dgl.data.utils.load_info`: 将数据集的有用信息(python dict对象)保存到本地磁盘和从本地磁盘读取它们。
|
||||
|
||||
下面的示例显示了如何保存和读取图和数据集信息的列表。
|
||||
|
||||
.. code::
|
||||
|
||||
import os
|
||||
from dgl import save_graphs, load_graphs
|
||||
from dgl.data.utils import makedirs, save_info, load_info
|
||||
|
||||
def save(self):
|
||||
# 保存图和标签
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
save_graphs(graph_path, self.graphs, {'labels': self.labels})
|
||||
# 在Python字典里保存其他信息
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
save_info(info_path, {'num_classes': self.num_classes})
|
||||
|
||||
def load(self):
|
||||
# 从目录 `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):
|
||||
# 检查在 `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)
|
||||
|
||||
请注意:有些情况下不适合保存处理过的数据。例如,在内置数据集 :class:`~dgl.data.GDELTDataset` 中,
|
||||
处理过的数据比较大。所以这个时候,在 ``__getitem__(idx)`` 中处理每个数据实例是更高效的方法。
|
||||
@@ -0,0 +1,31 @@
|
||||
.. _guide_cn-data-pipeline:
|
||||
|
||||
第4章:图数据处理管道
|
||||
==============================
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline>`
|
||||
|
||||
DGL在 :ref:`apidata` 里实现了很多常用的图数据集。它们遵循了由 :class:`dgl.data.DGLDataset` 类定义的标准的数据处理管道。
|
||||
DGL推荐用户将图数据处理为 :class:`dgl.data.DGLDataset` 的子类。该类为导入、处理和保存图数据提供了简单而干净的解决方案。
|
||||
|
||||
本章路线图
|
||||
-----------
|
||||
|
||||
本章介绍了如何为用户自己的图数据创建一个DGL数据集。以下内容说明了管道的工作方式,并展示了如何实现管道的每个组件。
|
||||
|
||||
* :ref:`guide_cn-data-pipeline-dataset`
|
||||
* :ref:`guide_cn-data-pipeline-download`
|
||||
* :ref:`guide_cn-data-pipeline-process`
|
||||
* :ref:`guide_cn-data-pipeline-savenload`
|
||||
* :ref:`guide_cn-data-pipeline-loadogb`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
data-dataset
|
||||
data-download
|
||||
data-process
|
||||
data-savenload
|
||||
data-loadogb
|
||||
@@ -0,0 +1,253 @@
|
||||
.. _guide_cn-distributed-apis:
|
||||
|
||||
7.2 分布式计算的API
|
||||
--------------------
|
||||
|
||||
:ref:`(English Version) <guide-distributed-apis>`
|
||||
|
||||
本节介绍了在训练脚本中使用的分布式计算API。DGL提供了三种分布式数据结构和多种API,用于初始化、分布式采样和数据分割。
|
||||
对于分布式训练/推断,DGL提供了三种分布式数据结构:用于分布式图的 :class:`~dgl.distributed.DistGraph`、
|
||||
用于分布式张量的 :class:`~dgl.distributed.DistTensor` 和用于分布式可学习嵌入的
|
||||
:class:`~dgl.distributed.DistEmbedding`。
|
||||
|
||||
DGL分布式模块的初始化
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:func:`~dgl.distributed.initialize` 可以用于初始化分布式模块。当训练脚本在训练器模式下运行时,
|
||||
这个API会与DGL服务器建立连接并创建采样器进程。当脚本在服务器模式下运行时,这个API将运行服务器代码,
|
||||
直到训练任务结束。必须在DGL的任何其他分布式API之前,调用此API。在使用PyTorch时,必须在
|
||||
``torch.distributed.init_process_group`` 之前调用 :func:`~dgl.distributed.initialize`。
|
||||
通常,初始化API应按以下顺序调用:
|
||||
|
||||
.. code:: python
|
||||
|
||||
dgl.distributed.initialize('ip_config.txt')
|
||||
th.distributed.init_process_group(backend='gloo')
|
||||
|
||||
**Note**: 如果训练脚本里包含需要在服务器(细节内容可以在下面的DistTensor和DistEmbedding章节里查看)上调用的用户自定义函数(UDF),
|
||||
这些UDF必须在 :func:`~dgl.distributed.initialize` 之前被声明。
|
||||
|
||||
分布式图
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` 是一个Python类,用于访问计算机集群中的图结构和节点/边特征。每台计算机负责一个且只负责一个分区。
|
||||
它加载分区数据(包括分区中的图结构、节点数据和边数据),并使集群中的所有训练器均可访问它们。
|
||||
:class:`~dgl.distributed.DistGraph` 提供了一小部分 :class:`~dgl.DGLGraph` 的API以方便数据访问。
|
||||
|
||||
**Note**: :class:`~dgl.distributed.DistGraph` 当前仅支持一种节点类型和一种边类型的图。
|
||||
|
||||
分布式模式与独立模式
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` 可以在两种模式下运行:分布式模式和独立模式。
|
||||
当用户在Python命令行或Jupyter Notebook中执行训练脚本时,它将以独立模式运行。也就是说,它在单个进程中运行所有计算,
|
||||
并且不与任何其他进程通信。因此,独立模式要求输入图仅具有一个分区。此模式主要用于开发和测试
|
||||
(例如,在Jupyter Notebook中开发和运行代码)。当用户使用启动脚本执行训练脚本时(请参见启动脚本部分),
|
||||
:class:`~dgl.distributed.DistGraph` 将以分布式模式运行。启动脚本在后台启动服务器(包括访问节点/边特征和图采样),
|
||||
并将分区数据自动加载到每台计算机中。:class:`~dgl.distributed.DistGraph` 与集群中的服务器连接并通过网络访问它们。
|
||||
|
||||
创建DistGraph
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
在分布式模式下,:class:`~dgl.distributed.DistGraph` 的创建需要(定义)在图划分期间的图名称。
|
||||
图名称标识了集群中所需加载的图。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name')
|
||||
|
||||
在独立模式下运行时,DistGraph将图数据加载到本地计算机中。因此,用户需要提供分区配置文件,其中包含有关输入图的所有信息。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name', part_config='data/graph_name.json')
|
||||
|
||||
**Note**: 在当前实现中,DGL仅允许创建单个DistGraph对象。销毁DistGraph并创建一个新DistGraph的行为没有被定义。
|
||||
|
||||
访问图结构
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` 提供了几个API来访问图结构。当前,它们主要被用来提供图信息,例如节点和边的数量。
|
||||
主要应用场景是运行采样API以支持小批量训练(请参阅下文里分布式图采样部分)。
|
||||
|
||||
.. code:: python
|
||||
|
||||
print(g.num_nodes())
|
||||
|
||||
访问节点/边数据
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
与 :class:`~dgl.DGLGraph` 一样, :class:`~dgl.distributed.DistGraph` 也提供了
|
||||
``ndata`` 和 ``edata`` 来访问节点和边中的数据。它们的区别在于
|
||||
:class:`~dgl.distributed.DistGraph` 中的 ``ndata`` / ``edata`` 返回的是 :class:`~dgl.distributed.DistTensor`,
|
||||
而不是底层框架里的张量。用户还可以将新的 :class:`~dgl.distributed.DistTensor` 分配给
|
||||
:class:`~dgl.distributed.DistGraph` 作为节点数据或边数据。
|
||||
|
||||
.. code:: python
|
||||
|
||||
g.ndata['train_mask']
|
||||
<dgl.distributed.dist_graph.DistTensor at 0x7fec820937b8>
|
||||
g.ndata['train_mask'][0]
|
||||
tensor([1], dtype=torch.uint8)
|
||||
|
||||
分布式张量
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
如前所述,在分布式模式下,DGL会划分节点和边特征,并将它们存储在计算机集群中。
|
||||
DGL为分布式张量提供了类似于单机普通张量的接口,以访问群集中的分区节点和边特征。
|
||||
在分布式设置中,DGL仅支持密集节点和边特征,暂不支持稀疏节点和边特征。
|
||||
|
||||
:class:`~dgl.distributed.DistTensor` 管理在多个计算机中被划分和存储的密集张量。
|
||||
目前,分布式张量必须与图的节点或边相关联。换句话说,DistTensor中的行数必须与图中的节点数或边数相同。
|
||||
以下代码创建一个分布式张量。 除了张量的形状和数据类型之外,用户还可以提供唯一的张量名称。
|
||||
如果用户要引用一个固定的分布式张量(即使 :class:`~dgl.distributed.DistTensor` 对象消失,该名称仍存在于群集中),
|
||||
则(使用这样的)名称就很有用。
|
||||
|
||||
.. code:: python
|
||||
|
||||
tensor = dgl.distributed.DistTensor((g.num_nodes(), 10), th.float32, name='test')
|
||||
|
||||
**Note**: :class:`~dgl.distributed.DistTensor` 的创建是一个同步操作。所有训练器都必须调用创建,
|
||||
并且只有当所有训练器都调用它时,此创建过程才能成功。
|
||||
|
||||
用户可以将 :class:`~dgl.distributed.DistTensor` 作为节点数据或边数据之一添加到
|
||||
:class:`~dgl.distributed.DistGraph` 对象。
|
||||
|
||||
.. code:: python
|
||||
|
||||
g.ndata['feat'] = tensor
|
||||
|
||||
**Note**: 节点数据名称和张量名称不必相同。前者在 :class:`~dgl.distributed.DistGraph` 中标识节点数据(在训练器进程中),
|
||||
而后者则标识DGL服务器中的分布式张量。
|
||||
|
||||
:class:`~dgl.distributed.DistTensor` 提供了一些功能。它具有与常规张量相同的API,用于访问其元数据,
|
||||
例如形状和数据类型。:class:`~dgl.distributed.DistTensor` 支持索引读取和写入,
|
||||
但不支持一些计算运算符,例如求和以及求均值。
|
||||
|
||||
.. code:: python
|
||||
|
||||
data = g.ndata['feat'][[1, 2, 3]]
|
||||
print(data)
|
||||
g.ndata['feat'][[3, 4, 5]] = data
|
||||
|
||||
**Note**: 当前,当一台机器运行多个服务器时,DGL不提供对来自多个训练器的并发写入的保护。
|
||||
这可能会导致数据损坏。
|
||||
|
||||
分布式嵌入
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL提供 :class:`~dgl.distributed.DistEmbedding` 以支持需要节点嵌入的直推(transductive)模型。
|
||||
分布式嵌入的创建与分布式张量的创建非常相似。
|
||||
|
||||
.. 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)
|
||||
|
||||
在内部,分布式嵌入建立在分布式张量之上,因此,其行为与分布式张量非常相似。
|
||||
例如,创建嵌入时,DGL会将它们分片并存储在集群中的所有计算机上。(分布式嵌入)可以通过名称唯一标识。
|
||||
|
||||
**Note**: 服务器进程负责调用初始化函数。因此,必须在初始化( :class:`~dgl.distributed.initialize` )之前声明分布式嵌入。
|
||||
|
||||
因为嵌入是模型的一部分,所以用户必须将其附加到优化器上以进行小批量训练。当前,
|
||||
DGL提供了一个稀疏的Adagrad优化器 :class:`~dgl.distributed.SparseAdagrad` (DGL以后将为稀疏嵌入添加更多的优化器)。
|
||||
用户需要从模型中收集所有分布式嵌入,并将它们传递给稀疏优化器。如果模型同时具有节点嵌入和规则的密集模型参数,
|
||||
并且用户希望对嵌入执行稀疏更新,则需要创建两个优化器,一个用于节点嵌入,另一个用于密集模型参数,如以下代码所示:
|
||||
|
||||
.. 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` 不是PyTorch的nn模块,因此用户无法从nn模块的参数访问它。
|
||||
|
||||
分布式采样
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL提供了两个级别的API,用于对节点和边进行采样以生成小批次训练数据(请参阅小批次训练的章节)。
|
||||
底层API要求用户编写代码以明确定义如何对节点层进行采样(例如,使用 :func:`dgl.sampling.sample_neighbors` )。
|
||||
高层采样API为节点分类和链接预测任务实现了一些流行的采样算法(例如
|
||||
:class:`~dgl.dataloading.pytorch.NodeDataLoader`
|
||||
和
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader` )。
|
||||
|
||||
分布式采样模块遵循相同的设计,也提供两个级别的采样API。对于底层的采样API,它为
|
||||
:class:`~dgl.distributed.DistGraph` 上的分布式邻居采样提供了
|
||||
:func:`~dgl.distributed.sample_neighbors`。另外,DGL提供了用于分布式采样的分布式数据加载器(
|
||||
:class:`~dgl.distributed.DistDataLoader`)。除了用户在创建数据加载器时无法指定工作进程的数量,
|
||||
分布式数据加载器具有与PyTorch DataLoader相同的接口。其中的工作进程(worker)在 :func:`dgl.distributed.initialize` 中创建。
|
||||
|
||||
**Note**: 在 :class:`~dgl.distributed.DistGraph` 上运行 :func:`dgl.distributed.sample_neighbors` 时,
|
||||
采样器无法在具有多个工作进程的PyTorch DataLoader中运行。主要原因是PyTorch DataLoader在每个训练周期都会创建新的采样工作进程,
|
||||
从而导致多次创建和删除 :class:`~dgl.distributed.DistGraph` 对象。
|
||||
|
||||
使用底层API时,采样代码类似于单进程采样。唯一的区别是用户需要使用
|
||||
:func:`dgl.distributed.sample_neighbors`
|
||||
和
|
||||
: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:
|
||||
...
|
||||
|
||||
:class:`~dgl.dataloading.pytorch.NodeDataLoader`
|
||||
和
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader` 有分布式的版本
|
||||
:class:`~dgl.dataloading.pytorch.DistNodeDataLoader`
|
||||
和
|
||||
:class:`~dgl.dataloading.pytorch.DistEdgeDataLoader` 。使用
|
||||
时分布式采样代码与单进程采样几乎完全相同。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.sampling.MultiLayerNeighborSampler([10, 25])
|
||||
dataloader = dgl.sampling.DistNodeDataLoader(g, train_nid, sampler,
|
||||
batch_size=batch_size, shuffle=True)
|
||||
for batch in dataloader:
|
||||
...
|
||||
|
||||
|
||||
分割数据集
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
用户需要分割训练集,以便每个训练器都可以使用自己的训练集子集。同样,用户还需要以相同的方式分割验证和测试集。
|
||||
|
||||
对于分布式训练和评估,推荐的方法是使用布尔数组表示训练、验证和测试集。对于节点分类任务,
|
||||
这些布尔数组的长度是图中节点的数量,并且它们的每个元素都表示训练/验证/测试集中是否存在对应节点。
|
||||
链接预测任务也应使用类似的布尔数组。
|
||||
|
||||
DGL提供了 :func:`~dgl.distributed.node_split` 和 :func:`~dgl.distributed.edge_split`
|
||||
函数来在运行时拆分训练、验证和测试集,以进行分布式训练。这两个函数将布尔数组作为输入,对其进行拆分,并向本地训练器返回一部分。
|
||||
默认情况下,它们确保所有部分都具有相同数量的节点和边。这对于同步SGD非常重要,
|
||||
因为同步SGD会假定每个训练器具有相同数量的小批次。
|
||||
|
||||
下面的示例演示了训练集拆分,并向本地进程返回节点的子集。
|
||||
|
||||
.. code:: python
|
||||
|
||||
train_nids = dgl.distributed.node_split(g.ndata['train_mask'])
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
.. _guide_cn-distributed-preprocessing:
|
||||
|
||||
7.1 分布式训练所需的图数据预处理
|
||||
------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-distributed-preprocessing>`
|
||||
|
||||
DGL要求预处理图数据以进行分布式训练,这包括两个步骤:1)将一张图划分为多张子图(分区),2)为节点和边分配新的ID。
|
||||
DGL提供了一个API以执行这两个步骤。该API支持随机划分和一个基于
|
||||
`Metis <http://glaros.dtc.umn.edu/gkhome/views/metis>`__ 的划分。Metis划分的好处在于,
|
||||
它可以用最少的边分割以生成分区,从而减少了用于分布式训练和推理的网络通信。DGL使用最新版本的Metis,
|
||||
并针对真实世界中具有幂律分布的图进行了优化。在图划分后,API以易于在训练期间加载的格式构造划分结果。
|
||||
|
||||
**Note**: 图划分API当前在一台机器上运行。 因此如果一张图很大,用户将需要一台大内存的机器来对图进行划分。
|
||||
未来DGL将支持分布式图划分。
|
||||
|
||||
默认情况下,为了在分布式训练/推理期间定位节点/边,API将新ID分配给输入图的节点和边。
|
||||
分配ID后,该API会相应地打乱所有节点数据和边数据。在训练期间,用户只需使用新的节点和边的ID。
|
||||
与此同时,用户仍然可以通过 ``g.ndata['orig_id']`` 和 ``g.edata['orig_id']`` 获取原始ID。
|
||||
其中 ``g`` 是 ``DistGraph`` 对象(详细解释,请参见:ref:`guide-distributed-apis`)。
|
||||
|
||||
DGL将图划分结果存储在输出目录中的多个文件中。输出目录里始终包含一个名为xxx.json的JSON文件,其中xxx是提供给划分API的图的名称。
|
||||
JSON文件包含所有划分的配置。如果该API没有为节点和边分配新ID,它将生成两个额外的NumPy文件:`node_map.npy` 和 `edge_map.npy`。
|
||||
它们存储节点和边ID与分区ID之间的映射。对于具有十亿级数量节点和边的图,两个文件中的NumPy数组会很大,
|
||||
这是因为图中的每个节点和边都对应一个条目。在每个分区的文件夹内,有3个文件以DGL格式存储分区数据。
|
||||
`graph.dgl` 存储分区的图结构以及节点和边上的一些元数据。`node_feats.dgl` 和 `edge_feats.dgl` 存储属于该分区的节点和边的所有特征。
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
data_root_dir/
|
||||
|-- xxx.json # JSON中的分区配置文件
|
||||
|-- node_map.npy # 存储在NumPy数组中的每个节点的分区ID(可选)
|
||||
|-- edge_map.npy # 存储在NumPy数组中的每个边的分区ID(可选)
|
||||
|-- part0/ # 分区0的数据
|
||||
|-- node_feats.dgl # 以二进制格式存储的节点特征
|
||||
|-- edge_feats.dgl # 以二进制格式存储的边特征
|
||||
|-- graph.dgl # 以二进制格式存储的子图结构
|
||||
|-- part1/ # 分区1的数据
|
||||
|-- node_feats.dgl
|
||||
|-- edge_feats.dgl
|
||||
|-- graph.dgl
|
||||
|
||||
负载均衡
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
在对图进行划分时,默认情况下,Metis仅平衡每个子图中的节点数。根据当前的任务情况,这可能带来非最优的配置。
|
||||
例如,在半监督节点分类的场景里,训练器会对局部分区中带标签节点的子集进行计算。
|
||||
一个仅平衡图中节点(带标签和未带标签)的划分可能会导致计算负载不平衡。为了在每个分区中获得平衡的工作负载,
|
||||
划分API通过在 :func:`dgl.distributed.partition_graph` 中指定 ``balance_ntypes``
|
||||
在每个节点类型中的节点数上实现分区间的平衡。用户可以利用这一点将训练集、验证集和测试集中的节点看作不同类型的节点。
|
||||
|
||||
以下示例将训练集内和训练集外的节点看作两种类型的节点:
|
||||
|
||||
.. code:: python
|
||||
|
||||
dgl.distributed.partition_graph(g, 'graph_name', 4, '/tmp/test', balance_ntypes=g.ndata['train_mask'])
|
||||
|
||||
除了平衡节点的类型之外, :func:`dgl.distributed.partition_graph` 还允许通过指定
|
||||
``balance_edges`` 来平衡每个类型节点在子图中的入度。这平衡了不同类型节点的连边数量。
|
||||
|
||||
**Note**: 传给 :func:`dgl.distributed.partition_graph` 的图名称是一个重要的参数。
|
||||
:class:`dgl.distributed.DistGraph` 使用该名称来识别一个分布式的图。一个有效的图名称应该仅包含字母和下划线。
|
||||
@@ -0,0 +1,61 @@
|
||||
.. _guide_cn-distributed-tools:
|
||||
|
||||
7.3 运行分布式训练/推断所需的工具
|
||||
------------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-distributed-tools>`
|
||||
|
||||
DGL提供了两个脚本来帮助用户进行分布式训练:
|
||||
|
||||
* *tools/copy_files.py* 用于将图分区复制到集群,
|
||||
* *tools/launch.py* 用于在机器集群中启动分布式训练任务。
|
||||
|
||||
*copy_files.py* 将计算机(对图进行分区的计算机)中的分区数据和相关文件(例如,训练脚本)
|
||||
复制到(负责分布式训练的)机器集群上。在这些机器上,分布式训练将需要用到这些分区。该脚本包含四个参数:
|
||||
|
||||
* ``--part_config`` 指定分区配置文件,该文件包含本地计算机中分区数据的信息。
|
||||
* ``--ip_config`` 指定集群的IP配置文件。
|
||||
* ``--workspace`` 指定训练机器中存储与分布式训练有关的所有数据的目录。
|
||||
* ``--rel_data_path`` 指定工作空间目录下存储分区数据的相对路径。
|
||||
* ``--script_folder`` 指定工作空间目录下存储用户的训练脚本的相对路径。
|
||||
|
||||
**Note**: *copy_files.py* 会根据IP配置文件找到对应的计算机来存储图分区。因此,copy_files.py和launch.py应该使用相同的IP配置文件。
|
||||
|
||||
DGL提供了用于启动集群中的分布式训练任务的tools/launch.py。该脚本有以下假设:
|
||||
|
||||
* 分区数据和训练脚本都已被复制到集群或存在集群中所有计算机均可访问的全局存储空间(例如NFS)。
|
||||
* 主计算机(执行启动脚本的计算机)具有对集群内所有其他计算机的无密码ssh访问权限。
|
||||
|
||||
**Note**: 必须在集群中的一台计算机上调用启动脚本。
|
||||
|
||||
下面展示了在集群中启动分布式训练任务的示例。
|
||||
|
||||
.. code:: none
|
||||
|
||||
python3 tools/launch.py \
|
||||
--workspace ~graphsage/ \
|
||||
--num_trainers 2 \
|
||||
--num_samplers 4 \
|
||||
--num_servers 1 \
|
||||
--part_config data/ogb-product.json \
|
||||
--ip_config ip_config.txt \
|
||||
"python3 code/train_dist.py --graph-name ogb-product --ip_config ip_config.txt --num-epochs 5 --batch-size 1000 --lr 0.1 --num_workers 4"
|
||||
|
||||
配置文件 *ip_config.txt* 包含了集群中计算机的IP地址。*ip_config.txt* 的典型示例如下:
|
||||
|
||||
.. code:: none
|
||||
|
||||
172.31.19.1
|
||||
172.31.23.205
|
||||
172.31.29.175
|
||||
172.31.16.98
|
||||
|
||||
每行是一个计算机的IP地址。IP地址后面还可以有一个端口,用来指定不同训练器之间的网络通信所使用的端口。
|
||||
如果未提供具体端口,则默认值为 ``30050``。
|
||||
|
||||
启动脚本中指定的工作空间(--workspace)是计算机中的工作目录,里面保存了训练脚本、IP配置文件、分区配置文件以及图分区。
|
||||
文件的所有路径都应指定为工作空间的相对路径。
|
||||
|
||||
启动脚本会在每台计算机上创建指定数量的训练任务(``--num_trainers``)。另外,
|
||||
用户需要为每个训练器指定采样器进程的数量(``--num_samplers``)。
|
||||
采样器进程的数量必须匹配 :func:`~dgl.distributed.initialize` 中指定的工作进程的数量。
|
||||
@@ -0,0 +1,94 @@
|
||||
.. _guide_cn-distributed:
|
||||
|
||||
第7章:分布式训练
|
||||
=====================================
|
||||
|
||||
:ref:`(English Version) <guide-distributed>`
|
||||
|
||||
DGL采用完全分布式的方法,可将数据和计算同时分布在一组计算资源中。在本节中,
|
||||
我们默认使用一个集群的环境设置(即一组机器)。DGL会将一张图划分为多张子图,
|
||||
集群中的每台机器各自负责一张子图(分区)。为了并行化计算,DGL在集群所有机器上运行相同的训练脚本,
|
||||
并在同样的机器上运行服务器以将分区数据提供给训练器。
|
||||
|
||||
对于训练脚本,DGL提供了分布式的API。它们与小批次训练的API相似。用户仅需对单机小批次训练的代码稍作修改就可实现分布式训练。
|
||||
以下代码给出了一个用分布式方式训练GraphSage的示例。仅有的代码修改出现在第4-7行:1)初始化DGL的分布式模块,2)创建分布式图对象,以及
|
||||
3)拆分训练集,并计算本地进程的节点。其余代码保持不变,与 :ref:`mini_cn-batch training <guide_cn-minibatch>` 类似,
|
||||
包括:创建采样器,模型定义,模型训练的循环。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
import torch as th
|
||||
|
||||
dgl.distributed.initialize('ip_config.txt')
|
||||
th.distributed.init_process_group(backend='gloo')
|
||||
g = dgl.distributed.DistGraph('graph_name', 'part_config.json')
|
||||
pb = g.get_partition_book()
|
||||
train_nid = dgl.distributed.node_split(g.ndata['train_mask'], pb, force_even=True)
|
||||
|
||||
# 创建采样器
|
||||
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)
|
||||
|
||||
# 定义模型和优化器
|
||||
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)
|
||||
|
||||
# 模型训练的循环
|
||||
for epoch in range(args.num_epochs):
|
||||
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提供了一些工具,可将数据复制到集群的计算机上,并在所有机器上启动训练任务。
|
||||
|
||||
**Note**: 当前版本的分布式训练API仅支持PyTorch后端。
|
||||
|
||||
**Note**: 当前版本的实现仅支持具有一种节点类型和一种边类型的图。
|
||||
|
||||
DGL实现了一些分布式组件以支持分布式训练,下图显示了这些组件及它们间的相互作用。
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/distributed.png
|
||||
:alt: Imgur
|
||||
|
||||
具体来说,DGL的分布式训练具有三种类型的交互进程:
|
||||
*服务器*,
|
||||
*采样器* 和 *训练器*。
|
||||
|
||||
* *服务器进程* 在存储图分区数据(这包括图结构和节点/边特征)的每台计算机上运行。
|
||||
这些服务器一起工作以将图数据提供给训练器。请注意,一台机器可能同时运行多个服务器进程,以并行化计算和网络通信。
|
||||
* *采样器进程* 与服务器进行交互,并对节点和边采样以生成用于训练的小批次数据。
|
||||
* *训练器进程* 包含多个与服务器交互的类。它用 :class:`~dgl.distributed.DistGraph` 来获取被划分的图分区数据,
|
||||
用 :class:`~dgl.distributed.DistEmbedding` 和
|
||||
:class:`~dgl.distributed.DistTensor` 来获取节点/边特征/嵌入,用
|
||||
:class:`~dgl.distributed.dist_dataloader.DistDataLoader` 与采样器进行交互以获得小批次数据。
|
||||
|
||||
在初步了解了分布式组件后,本章的剩余部分将介绍以下分布式组件:
|
||||
|
||||
* :ref:`guide_cn-distributed-preprocessing`
|
||||
* :ref:`guide_cn-distributed-apis`
|
||||
* :ref:`guide_cn-distributed-tools`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
distributed-preprocessing
|
||||
distributed-apis
|
||||
distributed-tools
|
||||
@@ -0,0 +1,23 @@
|
||||
.. _guide_cn-graph-basic:
|
||||
|
||||
1.1 关于图的基本概念
|
||||
-----------------
|
||||
|
||||
:ref:`(English Version) <guide-graph-basic>`
|
||||
|
||||
图是用以表示实体及其关系的结构,记为 :math:`G=(V, E)` 。图由两个集合组成,一是节点的集合 :math:`V` ,一个是边的集合 :math:`E` 。
|
||||
在边集 :math:`E` 中,一条边 :math:`(u, v)` 连接一对节点 :math:`u` 和 :math:`v` ,表明两节点间存在关系。关系可以是无向的,
|
||||
如描述节点之间的对称关系;也可以是有向的,如描述非对称关系。例如,若用图对社交网络中人们的友谊关系进行建模,因为友谊是相互的,则边是无向的;
|
||||
若用图对Twitter用户的关注行为进行建模,则边是有向的。图可以是 *有向的* 或 *无向的* ,这取决于图中边的方向性。
|
||||
|
||||
图可以是 *加权的* 或 *未加权的* 。在加权图中,每条边都与一个标量权重值相关联。例如,该权重可以表示长度或连接的强度。
|
||||
|
||||
图可以是 *同构的* 或是 *异构的* 。在同构图中,所有节点表示同一类型的实体,所有边表示同一类型的关系。
|
||||
例如,社交网络的图由表示同一实体类型的人及其相互之间的社交关系组成。
|
||||
|
||||
相对地,在异构图中,节点和边的类型可以是不同的。例如,编码市场的图可以有表示"顾客"、"商家"和"商品"的节点,
|
||||
它们通过“想购买”、“已经购买”、“是顾客”和“正在销售”的边互相连接。二分图是一类特殊的、常用的异构图,
|
||||
其中的边连接两类不同类型的节点。例如,在推荐系统中,可以使用二分图表示"用户"和"物品"之间的关系。想了解更多信息,读者可参考 :ref:`guide_cn-graph-heterogeneous`。
|
||||
|
||||
在多重图中,同一对节点之间可以有多条(有向)边,包括自循环的边。例如,两名作者可以在不同年份共同署名文章,
|
||||
这就带来了具有不同特征的多条边。
|
||||
@@ -0,0 +1,109 @@
|
||||
.. _guide_cn-graph-external:
|
||||
|
||||
1.4 从外部源创建图
|
||||
---------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-external>`
|
||||
|
||||
可以从外部来源构造一个 :class:`~dgl.DGLGraph` 对象,包括:
|
||||
|
||||
- 从用于图和稀疏矩阵的外部Python库(NetworkX 和 SciPy)创建而来。
|
||||
- 从磁盘加载图数据。
|
||||
|
||||
本节不涉及通过转换其他图来生成图的函数,相关概述请阅读API参考手册。
|
||||
|
||||
从外部库创建图
|
||||
^^^^^^^^^^^
|
||||
|
||||
以下代码片段为从SciPy稀疏矩阵和NetworkX图创建DGL图的示例。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> import scipy.sparse as sp
|
||||
>>> spmat = sp.rand(100, 100, density=0.05) # 5%非零项
|
||||
>>> dgl.from_scipy(spmat) # 来自SciPy
|
||||
Graph(num_nodes=100, num_edges=500,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
>>> import networkx as nx
|
||||
>>> nx_g = nx.path_graph(5) # 一条链路0-1-2-3-4
|
||||
>>> dgl.from_networkx(nx_g) # 来自NetworkX
|
||||
Graph(num_nodes=5, num_edges=8,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
注意,当使用 `nx.path_graph(5)` 进行创建时, :class:`~dgl.DGLGraph` 对象有8条边,而非4条。
|
||||
这是由于 `nx.path_graph(5)` 构建了一个无向的NetworkX图 :class:`networkx.Graph` ,而 :class:`~dgl.DGLGraph` 的边总是有向的。
|
||||
所以当将无向的NetworkX图转换为 :class:`~dgl.DGLGraph` 对象时,DGL会在内部将1条无向边转换为2条有向边。
|
||||
使用有向的NetworkX图 :class:`networkx.DiGraph` 可避免该行为。
|
||||
|
||||
.. 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在内部将SciPy矩阵和NetworkX图转换为张量来创建图。因此,这些构建方法并不适用于重视性能的场景。
|
||||
|
||||
相关API: :func:`dgl.from_scipy`、 :func:`dgl.from_networkx`。
|
||||
|
||||
从磁盘加载图
|
||||
^^^^^^^^^^
|
||||
|
||||
有多种文件格式可储存图,所以这里难以枚举所有选项。本节仅给出一些常见格式的一般情况。
|
||||
|
||||
逗号分隔值(CSV)
|
||||
""""""""""""""
|
||||
|
||||
CSV是一种常见的格式,以表格格式储存节点、边及其特征:
|
||||
|
||||
.. table:: nodes.csv
|
||||
|
||||
+-----------+
|
||||
|age, title |
|
||||
+===========+
|
||||
|43, 1 |
|
||||
+-----------+
|
||||
|23, 3 |
|
||||
+-----------+
|
||||
|... |
|
||||
+-----------+
|
||||
|
||||
.. table:: edges.csv
|
||||
|
||||
+-----------------+
|
||||
|src, dst, weight |
|
||||
+=================+
|
||||
|0, 1, 0.4 |
|
||||
+-----------------+
|
||||
|0, 3, 0.9 |
|
||||
+-----------------+
|
||||
|... |
|
||||
+-----------------+
|
||||
|
||||
许多知名Python库(如Pandas)可以将该类型数据加载到python对象(如 :class:`numpy.ndarray`)中,
|
||||
进而使用这些对象来构建DGLGraph对象。如果后端框架也提供了从磁盘中保存或加载张量的工具(如 :func:`torch.save`, :func:`torch.load` ),
|
||||
可以遵循相同的原理来构建图。
|
||||
|
||||
另见: `从成对的边 CSV 文件中加载 Karate Club Network 的教程 <https://github.com/dglai/WWW20-Hands-on-Tutorial/blob/master/basic_tasks/1_load_data.ipynb>`_。
|
||||
|
||||
JSON/GML 格式
|
||||
""""""""""""
|
||||
|
||||
如果对速度不太关注的话,读者可以使用NetworkX提供的工具来解析 `各种数据格式 <https://networkx.github.io/documentation/stable/reference/readwrite/index.html>`_,
|
||||
DGL可以间接地从这些来源创建图。
|
||||
|
||||
DGL 二进制格式
|
||||
""""""""""""
|
||||
|
||||
DGL提供了API以从磁盘中加载或向磁盘里保存二进制格式的图。除了图结构,API也能处理特征数据和图级别的标签数据。
|
||||
DGL也支持直接从S3/HDFS中加载或向S3/HDFS保存图。参考手册提供了该用法的更多细节。
|
||||
|
||||
相关API: :func:`dgl.save_graphs`、 :func:`dgl.load_graphs`。
|
||||
@@ -0,0 +1,60 @@
|
||||
.. _guide_cn-graph-feature:
|
||||
|
||||
1.3 节点和边的特征
|
||||
---------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-feature>`
|
||||
|
||||
:class:`~dgl.DGLGraph` 对象的节点和边可具有多个用户定义的、可命名的特征,以储存图的节点和边的属性。
|
||||
通过 :py:attr:`~dgl.DGLGraph.ndata` 和 :py:attr:`~dgl.DGLGraph.edata` 接口可访问这些特征。
|
||||
例如,以下代码创建了2个节点特征(分别在第8、15行命名为 ``'x'`` 、 ``'y'`` )和1个边特征(在第9行命名为 ``'x'`` )。
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> g = dgl.graph(([0, 0, 1, 5], [1, 2, 2, 0])) # 6个节点,4条边
|
||||
>>> g
|
||||
Graph(num_nodes=6, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
>>> g.ndata['x'] = th.ones(g.num_nodes(), 3) # 长度为3的节点特征
|
||||
>>> g.edata['x'] = th.ones(g.num_edges(), dtype=th.int32) # 标量整型特征
|
||||
>>> 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)})
|
||||
>>> # 不同名称的特征可以具有不同形状
|
||||
>>> g.ndata['y'] = th.randn(g.num_nodes(), 5)
|
||||
>>> g.ndata['x'][1] # 获取节点1的特征
|
||||
tensor([1., 1., 1.])
|
||||
>>> g.edata['x'][th.tensor([0, 3])] # 获取边0和3的特征
|
||||
tensor([1, 1], dtype=torch.int32)
|
||||
|
||||
关于 :py:attr:`~dgl.DGLGraph.ndata` 和 :py:attr:`~dgl.DGLGraph.edata` 接口的重要说明:
|
||||
|
||||
- 仅允许使用数值类型(如单精度浮点型、双精度浮点型和整型)的特征。这些特征可以是标量、向量或多维张量。
|
||||
- 每个节点特征具有唯一名称,每个边特征也具有唯一名称。节点和边的特征可以具有相同的名称(如上述示例代码中的 ``'x'`` )。
|
||||
- 通过张量分配创建特征时,DGL会将特征赋给图中的每个节点和每条边。该张量的第一维必须与图中节点或边的数量一致。
|
||||
不能将特征赋给图中节点或边的子集。
|
||||
- 相同名称的特征必须具有相同的维度和数据类型。
|
||||
- 特征张量使用"行优先"的原则,即每个行切片储存1个节点或1条边的特征(参考上述示例代码的第16和18行)。
|
||||
|
||||
对于加权图,用户可以将权重储存为一个边特征,如下。
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> # 边 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]) # 每条边的权重
|
||||
>>> g = dgl.graph(edges)
|
||||
>>> g.edata['w'] = weights # 将其命名为 'w'
|
||||
>>> g
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={'w' : Scheme(shape=(,), dtype=torch.float32)})
|
||||
|
||||
|
||||
|
||||
相关API: :py:attr:`~dgl.DGLGraph.ndata`、 :py:attr:`~dgl.DGLGraph.edata`。
|
||||
@@ -0,0 +1,45 @@
|
||||
.. _guide_cn-graph-gpu:
|
||||
|
||||
1.6 在GPU上使用DGLGraph
|
||||
----------------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-gpu>`
|
||||
|
||||
用户可以通过在构造过程中传入两个GPU张量来创建GPU上的 :class:`~dgl.DGLGraph` 。
|
||||
另一种方法是使用 :func:`~dgl.DGLGraph.to` API将 :class:`~dgl.DGLGraph` 复制到GPU,这会将图结构和特征数据都拷贝到指定的设备。
|
||||
|
||||
.. 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) # 原始特征在CPU上
|
||||
>>> g.device
|
||||
device(type='cpu')
|
||||
>>> cuda_g = g.to('cuda:0') # 接受来自后端框架的任何设备对象
|
||||
>>> cuda_g.device
|
||||
device(type='cuda', index=0)
|
||||
>>> cuda_g.ndata['x'].device # 特征数据也拷贝到了GPU上
|
||||
device(type='cuda', index=0)
|
||||
|
||||
>>> # 由GPU张量构造的图也在GPU上
|
||||
>>> u, v = u.to('cuda:0'), v.to('cuda:0')
|
||||
>>> g = dgl.graph((u, v))
|
||||
>>> g.device
|
||||
device(type='cuda', index=0)
|
||||
|
||||
任何涉及GPU图的操作都是在GPU上运行的。因此,这要求所有张量参数都已经放在GPU上,其结果(图或张量)也将在GPU上。
|
||||
此外,GPU图只接受GPU上的特征数据。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> cuda_g.in_degrees()
|
||||
tensor([0, 0, 1, 1, 1], device='cuda:0')
|
||||
>>> cuda_g.in_edges([2, 3, 4]) # 可以接受非张量类型的参数
|
||||
(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')) # 张量类型的参数必须在GPU上
|
||||
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
|
||||
>>> cuda_g.ndata['h'] = th.randn(5, 4) # ERROR! 特征也必须在GPU上!
|
||||
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,88 @@
|
||||
.. _guide_cn-graph-graphs-nodes-edges:
|
||||
|
||||
1.2 图、节点和边
|
||||
--------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-graphs-nodes-edges>`
|
||||
|
||||
DGL使用一个唯一的整数来表示一个节点,称为点ID;并用对应的两个端点ID表示一条边。同时,DGL也会根据边被添加的顺序,
|
||||
给每条边分配一个唯一的整数编号,称为边ID。节点和边的ID都是从0开始构建的。在DGL的图里,所有的边都是有方向的,
|
||||
即边 :math:`(u, v)` 表示它是从节点 :math:`u` 指向节点 :math:`v` 的。
|
||||
|
||||
对于多个节点,DGL使用一个一维的整型张量(如,PyTorch的Tensor类,TensorFlow的Tensor类或MXNet的ndarray类)来保存图的点ID,
|
||||
DGL称之为"节点张量"。为了指代多条边,DGL使用一个包含2个节点张量的元组 :math:`(U, V)` ,其中,用 :math:`(U[i], V[i])` 指代一条
|
||||
:math:`U[i]` 到 :math:`V[i]` 的边。
|
||||
|
||||
创建一个 :class:`~dgl.DGLGraph` 对象的一种方法是使用 :func:`dgl.graph` 函数。它接受一个边的集合作为输入。DGL也支持从其他的数据源来创建图对象。
|
||||
读者可参考 :ref:`guide_cn-graph-external`。
|
||||
|
||||
下面的代码段使用了 :func:`dgl.graph` 函数来构建一个 :class:`~dgl.DGLGraph` 对象,对应着下图所示的包含4个节点的图。
|
||||
其中一些代码演示了查询图结构的部分API的使用方法。
|
||||
|
||||
.. 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
|
||||
|
||||
>>> # 边 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) # 图中节点的数量是DGL通过给定的图的边列表中最大的点ID推断所得出的
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
>>> # 获取节点的ID
|
||||
>>> print(g.nodes())
|
||||
tensor([0, 1, 2, 3])
|
||||
>>> # 获取边的对应端点
|
||||
>>> print(g.edges())
|
||||
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]))
|
||||
>>> # 获取边的对应端点和边ID
|
||||
>>> print(g.edges(form='all'))
|
||||
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]), tensor([0, 1, 2, 3]))
|
||||
|
||||
>>> # 如果具有最大ID的节点没有边,在创建图的时候,用户需要明确地指明节点的数量。
|
||||
>>> g = dgl.graph((u, v), num_nodes=8)
|
||||
|
||||
对于无向的图,用户需要为每条边都创建两个方向的边。可以使用 :func:`dgl.to_bidirected` 函数来实现这个目的。
|
||||
如下面的代码段所示,这个函数可以把原图转换成一个包含反向边的图。
|
||||
|
||||
.. 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类内部使用C来存储,且显性定义了数据类型以及存储的设备信息,DGL推荐使用Tensor作为DGL API的输入。
|
||||
不过大部分的DGL API也支持Python的可迭代类型(比如列表)或numpy.ndarray类型作为API的输入,方便用户快速进行开发验证。
|
||||
|
||||
DGL支持使用 :math:`32` 位或 :math:`64` 位的整数作为节点ID和边ID。节点和边ID的数据类型必须一致。如果使用 :math:`64` 位整数,
|
||||
DGL可以处理最多 :math:`2^{63} - 1` 个节点或边。不过,如果图里的节点或者边的数量小于 :math:`2^{31} - 1` ,用户最好使用 :math:`32` 位整数。
|
||||
这样不仅能提升速度,还能减少内存的使用。DGL提供了进行数据类型转换的方法,如下例所示。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> edges = th.tensor([2, 5, 3]), th.tensor([3, 5, 0]) # 边:2->3, 5->5, 3->0
|
||||
>>> g64 = dgl.graph(edges) # DGL默认使用int64
|
||||
>>> print(g64.idtype)
|
||||
torch.int64
|
||||
>>> g32 = dgl.graph(edges, idtype=th.int32) # 使用int32构建图
|
||||
>>> g32.idtype
|
||||
torch.int32
|
||||
>>> g64_2 = g32.long() # 转换成int64
|
||||
>>> g64_2.idtype
|
||||
torch.int64
|
||||
>>> g32_2 = g64.int() # 转换成int32
|
||||
>>> g32_2.idtype
|
||||
torch.int32
|
||||
|
||||
相关API::func:`dgl.graph`、 :func:`dgl.DGLGraph.nodes`、 :func:`dgl.DGLGraph.edges`、 :func:`dgl.to_bidirected`、
|
||||
:func:`dgl.DGLGraph.int`、 :func:`dgl.DGLGraph.long` 和 :py:attr:`dgl.DGLGraph.idtype`。
|
||||
@@ -0,0 +1,264 @@
|
||||
.. _guide_cn-graph-heterogeneous:
|
||||
|
||||
1.5 异构图
|
||||
---------
|
||||
|
||||
:ref:`(English Version)<guide-graph-heterogeneous>`
|
||||
|
||||
相比同构图,异构图里可以有不同类型的节点和边。这些不同类型的节点和边具有独立的ID空间和特征。
|
||||
例如在下图中,"用户"和"游戏"节点的ID都是从0开始的,而且两种节点具有不同的特征。
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/user_guide_graphch_2.png
|
||||
|
||||
一个异构图示例。该图具有两种类型的节点("用户"和"游戏")和两种类型的边("关注"和"玩")。
|
||||
|
||||
创建异构图
|
||||
^^^^^^^^
|
||||
|
||||
在DGL中,一个异构图由一系列子图构成,一个子图对应一种关系。每个关系由一个字符串三元组
|
||||
定义 ``(源节点类型, 边类型, 目标节点类型)`` 。由于这里的关系定义消除了边类型的歧义,DGL称它们为规范边类型。
|
||||
|
||||
下面的代码是一个在DGL中创建异构图的示例。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
|
||||
>>> # 创建一个具有3种节点类型和3种边类型的异构图
|
||||
>>> 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')]
|
||||
|
||||
注意,同构图和二分图只是一种特殊的异构图,它们只包括一种关系。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # 一个同构图
|
||||
>>> dgl.heterograph({('node_type', 'edge_type', 'node_type'): (u, v)})
|
||||
>>> # 一个二分图
|
||||
>>> dgl.heterograph({('source_type', 'edge_type', 'destination_type'): (u, v)})
|
||||
|
||||
与异构图相关联的 *metagraph* 就是图的模式。它指定节点集和节点之间的边的类型约束。
|
||||
*metagraph* 中的一个节点 :math:`u` 对应于相关异构图中的一个节点类型。
|
||||
*metagraph* 中的边 :math:`(u,v)` 表示在相关异构图中存在从 :math:`u` 型节点到 :math:`v` 型节点的边。
|
||||
|
||||
.. 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')])
|
||||
|
||||
相关API: :func:`dgl.heterograph`、 :py:attr:`~dgl.DGLGraph.ntypes`、 :py:attr:`~dgl.DGLGraph.etypes`、
|
||||
:py:attr:`~dgl.DGLGraph.canonical_etypes`、 :py:attr:`~dgl.DGLGraph.metagraph`。
|
||||
|
||||
使用多种类型
|
||||
^^^^^^^^^^
|
||||
|
||||
当引入多种节点和边类型后,用户在调用DGLGraph API以获取特定类型的信息时,需要指定具体的节点和边类型。此外,不同类型的节点和边具有单独的ID。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # 获取图中所有节点的数量
|
||||
>>> g.num_nodes()
|
||||
10
|
||||
>>> # 获取drug节点的数量
|
||||
>>> g.num_nodes('drug')
|
||||
3
|
||||
>>> # 不同类型的节点有单独的ID。因此,没有指定节点类型就没有明确的返回值。
|
||||
>>> g.nodes()
|
||||
DGLError: Node type name must be specified if there are more than one node types.
|
||||
>>> g.nodes('drug')
|
||||
tensor([0, 1, 2])
|
||||
|
||||
为了设置/获取特定节点和边类型的特征,DGL提供了两种新类型的语法: `g.nodes['node_type'].data['feat_name']` 和 `g.edges['edge_type'].data['feat_name']` 。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # 设置/获取"drug"类型的节点的"hv"特征
|
||||
>>> g.nodes['drug'].data['hv'] = th.ones(3, 1)
|
||||
>>> g.nodes['drug'].data['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.]])
|
||||
>>> # 设置/获取"treats"类型的边的"he"特征
|
||||
>>> g.edges['treats'].data['he'] = th.zeros(1, 1)
|
||||
>>> g.edges['treats'].data['he']
|
||||
tensor([[0.]])
|
||||
|
||||
如果图里只有一种节点或边类型,则不需要指定节点或边的类型。
|
||||
|
||||
.. 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])
|
||||
>>> # 设置/获取单一类型的节点或边特征,不必使用新的语法
|
||||
>>> g.ndata['hv'] = th.ones(4, 1)
|
||||
|
||||
.. note::
|
||||
|
||||
当边类型唯一地确定了源节点和目标节点的类型时,用户可以只使用一个字符串而不是字符串三元组来指定边类型。例如,
|
||||
对于具有两个关系 ``('user', 'plays', 'game')`` 和 ``('user', 'likes', 'game')`` 的异构图,
|
||||
只使用 ``'plays'`` 或 ``'like'`` 来指代这两个关系是可以的。
|
||||
|
||||
从磁盘加载异构图
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
逗号分隔值(CSV)
|
||||
""""""""""""""
|
||||
|
||||
一种存储异构图的常见方法是在不同的CSV文件中存储不同类型的节点和边。下面是一个例子。
|
||||
|
||||
.. code::
|
||||
|
||||
# 数据文件夹
|
||||
data/
|
||||
|-- drug.csv # drug节点
|
||||
|-- gene.csv # gene节点
|
||||
|-- disease.csv # disease节点
|
||||
|-- drug-interact-drug.csv # drug-drug相互作用边
|
||||
|-- drug-interact-gene.csv # drug-gene相互作用边
|
||||
|-- drug-treat-disease.csv # drug-disease治疗边
|
||||
|
||||
与同构图的情况类似,用户可以使用像Pandas这样的包先将CSV文件解析为numpy数组或框架张量,再构建一个关系字典,并用它构造一个异构图。
|
||||
这种方法也适用于其他流行的文件格式,比如GML或JSON。
|
||||
|
||||
DGL二进制格式
|
||||
"""""""""""
|
||||
|
||||
DGL提供了 :func:`dgl.save_graphs` 和 :func:`dgl.load_graphs` 函数,分别用于以二进制格式保存异构图和加载它们。
|
||||
|
||||
边类型子图
|
||||
^^^^^^^^
|
||||
|
||||
用户可以通过指定要保留的关系来创建异构图的子图,相关的特征也会被拷贝。
|
||||
|
||||
.. 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)
|
||||
|
||||
>>> # 保留关系 ('drug', 'interacts', 'drug') 和 ('drug', 'treats', 'disease') 。
|
||||
>>> # 'drug' 和 'disease' 类型的节点也会被保留
|
||||
>>> 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')])
|
||||
>>> # 相关的特征也会被拷贝
|
||||
>>> eg.nodes['drug'].data['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.]])
|
||||
|
||||
|
||||
将异构图转化为同构图
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
异构图为管理不同类型的节点和边及其相关特征提供了一个清晰的接口。这在以下情况下尤其有用:
|
||||
|
||||
1. 不同类型的节点和边的特征具有不同的数据类型或大小。
|
||||
2. 用户希望对不同类型的节点和边应用不同的操作。
|
||||
|
||||
如果上述情况不适用,并且用户不希望在建模中区分节点和边的类型,则DGL允许使用 :func:`dgl.DGLGraph.to_homogeneous` API将异构图转换为同构图。
|
||||
具体行为如下:
|
||||
|
||||
1. 用从0开始的连续整数重新标记所有类型的节点和边。
|
||||
2. 对所有的节点和边合并用户指定的特征。
|
||||
|
||||
.. 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)
|
||||
|
||||
>>> # 默认情况下不进行特征合并
|
||||
>>> hg = dgl.to_homogeneous(g)
|
||||
>>> 'hv' in hg.ndata
|
||||
False
|
||||
|
||||
>>> # 拷贝边的特征
|
||||
>>> # 对于要拷贝的特征,DGL假定不同类型的节点或边的需要合并的特征具有相同的大小和数据类型
|
||||
>>> 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)
|
||||
|
||||
>>> # 拷贝节点特征
|
||||
>>> hg = dgl.to_homogeneous(g, ndata=['hv'])
|
||||
>>> hg.ndata['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.],
|
||||
[0.],
|
||||
[0.],
|
||||
[0.]])
|
||||
|
||||
原始的节点或边的类型和对应的ID被存储在 :py:attr:`~dgl.DGLGraph.ndata` 和 :py:attr:`~dgl.DGLGraph.edata` 中。
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # 异构图中节点类型的顺序
|
||||
>>> g.ntypes
|
||||
['disease', 'drug']
|
||||
>>> # 原始节点类型
|
||||
>>> hg.ndata[dgl.NTYPE]
|
||||
tensor([0, 0, 0, 1, 1, 1])
|
||||
>>> # 原始的特定类型节点ID
|
||||
>>> hg.ndata[dgl.NID]
|
||||
tensor([0, 1, 2, 0, 1, 2])
|
||||
|
||||
>>> # 异构图中边类型的顺序
|
||||
>>> g.etypes
|
||||
['interacts', 'treats']
|
||||
>>> # 原始边类型
|
||||
>>> hg.edata[dgl.ETYPE]
|
||||
tensor([0, 0, 1])
|
||||
>>> # 原始的特定类型边ID
|
||||
>>> hg.edata[dgl.EID]
|
||||
tensor([0, 1, 0])
|
||||
|
||||
出于建模的目的,用户可能需要将一些关系合并,并对它们应用相同的操作。为了实现这一目的,可以先抽取异构图的边类型子图,然后将该子图转换为同构图。
|
||||
|
||||
.. 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,35 @@
|
||||
.. _guide_cn-graph:
|
||||
|
||||
第1章:图
|
||||
=============
|
||||
|
||||
:ref:`(English Version)<guide-graph>`
|
||||
|
||||
图表示实体(节点)和它们的关系(边),其中节点和边可以是有类型的 (例如,``"用户"`` 和 ``"物品"`` 是两种不同类型的节点)。
|
||||
DGL通过其核心数据结构 :class:`~dgl.DGLGraph` 提供了一个以图为中心的编程抽象。 :class:`~dgl.DGLGraph` 提供了接口以处理图的结构、节点/边
|
||||
的特征,以及使用这些组件可以执行的计算。
|
||||
|
||||
|
||||
本章路线图
|
||||
--------------
|
||||
|
||||
本章首先简要介绍了图的定义(见1.1节),然后介绍了一些 :class:`~dgl.DGLGraph` 相关的核心概念:
|
||||
|
||||
* :ref:`guide_cn-graph-basic`
|
||||
* :ref:`guide_cn-graph-graphs-nodes-edges`
|
||||
* :ref:`guide_cn-graph-feature`
|
||||
* :ref:`guide_cn-graph-external`
|
||||
* :ref:`guide_cn-graph-heterogeneous`
|
||||
* :ref:`guide_cn-graph-gpu`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
graph-basic
|
||||
graph-graphs-nodes-edges
|
||||
graph-feature
|
||||
graph-external
|
||||
graph-heterogeneous
|
||||
graph-gpu
|
||||
@@ -0,0 +1,140 @@
|
||||
用户指南【包含过时信息】
|
||||
===================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
graph
|
||||
message
|
||||
nn
|
||||
data
|
||||
training
|
||||
minibatch
|
||||
distributed
|
||||
|
||||
2020年9月,DGL社区的一群热心贡献者把DGL用户指南译成了中文,方便广大中文用户群学习和使用DGL。
|
||||
|
||||
特此致谢下述贡献者:
|
||||
|
||||
.. list-table::
|
||||
:widths: 20 20 20
|
||||
:header-rows: 1
|
||||
|
||||
* - 章节
|
||||
- 个人姓名/昵称
|
||||
- 个人链接
|
||||
* - :ref:`guide_cn-graph`
|
||||
- 张怀文/Huaiwen Zhang
|
||||
- https://github.com/huaiwen
|
||||
* - :ref:`guide_cn-graph-basic`
|
||||
- 沈成 / mlsoar
|
||||
- https://github.com/mlsoar
|
||||
* - :ref:`guide_cn-graph-graphs-nodes-edges`
|
||||
- 张建 / zhjwy9343
|
||||
- https://github.com/zhjwy9343
|
||||
* - :ref:`guide_cn-graph-feature`
|
||||
- 沈成 / mlsoar
|
||||
- https://github.com/mlsoar
|
||||
* - :ref:`guide_cn-graph-external`
|
||||
- 沈成 / mlsoar
|
||||
- https://github.com/mlsoar
|
||||
* - :ref:`guide_cn-graph-heterogeneous`
|
||||
- 张怀文/Huaiwen Zhang
|
||||
- https://github.com/huaiwen
|
||||
* - :ref:`guide_cn-message-passing`,
|
||||
- 黄崟/Brook Huang
|
||||
- https://github.com/brookhuang16211
|
||||
* - :ref:`guide_cn-message-passing-api`
|
||||
- 黄崟/Brook Huang
|
||||
- https://github.com/brookhuang16211
|
||||
* - :ref:`guide_cn-message-passing-efficient`
|
||||
- 黄崟/Brook Huang
|
||||
- https://github.com/brookhuang16211
|
||||
* - :ref:`guide_cn-message-passing-part`
|
||||
- 陈知雨/Zhiyu Chen
|
||||
- https://www.zhiyuchen.com
|
||||
* - :ref:`guide_cn-message-passing-edge`
|
||||
- 陈知雨/Zhiyu Chen
|
||||
- https://www.zhiyuchen.com
|
||||
* - :ref:`guide_cn-message-passing-heterograph`
|
||||
- 陈知雨/Zhiyu Chen
|
||||
- https://www.zhiyuchen.com
|
||||
* - :ref:`guide_cn-nn`
|
||||
- 陈知雨/Zhiyu Chen
|
||||
- https://www.zhiyuchen.com
|
||||
* - :ref:`guide_cn-nn-construction`
|
||||
- 陈知雨/Zhiyu Chen
|
||||
- https://www.zhiyuchen.com
|
||||
* - :ref:`guide_cn-nn-forward`
|
||||
- 栩栩的夏天
|
||||
-
|
||||
* - :ref:`guide_cn-nn-heterograph`
|
||||
- 栩栩的夏天
|
||||
-
|
||||
* - :ref:`guide_cn-data-pipeline`
|
||||
- 吴紫薇/ Maggie Wu
|
||||
- https://github.com/hhhiddleston
|
||||
* - :ref:`guide_cn-data-pipeline-dataset`
|
||||
- 吴紫薇/ Maggie Wu
|
||||
- https://github.com/hhhiddleston
|
||||
* - :ref:`guide_cn-data-pipeline-download`
|
||||
- 吴紫薇/ Maggie Wu
|
||||
- https://github.com/hhhiddleston
|
||||
* - :ref:`guide_cn-data-pipeline-process`
|
||||
- 吴紫薇/ Maggie Wu
|
||||
- https://github.com/hhhiddleston
|
||||
* - :ref:`guide_cn-data-pipeline-savenload`
|
||||
- 王建民/DrugAI
|
||||
- https://github.com/AspirinCode
|
||||
* - :ref:`guide_cn-data-pipeline-loadogb`
|
||||
- 王建民/DrugAI
|
||||
- https://github.com/AspirinCode
|
||||
* - :ref:`guide_cn-training`
|
||||
- 王建民/DrugAI
|
||||
- https://github.com/AspirinCode
|
||||
* - :ref:`guide_cn-training-node-classification`,
|
||||
- 王建民/DrugAI
|
||||
- https://github.com/AspirinCode
|
||||
* - :ref:`guide_cn-training-edge-classification`
|
||||
- 徐东辉/DonghuiXu
|
||||
- https://github.com/rewonderful
|
||||
* - :ref:`guide_cn-training-link-prediction`
|
||||
- 徐东辉/DonghuiXu
|
||||
- https://github.com/rewonderful
|
||||
* - :ref:`guide_cn-training-graph-classification`
|
||||
- 莫佳帅子/Molasses
|
||||
- https://github.com/sleeplessai
|
||||
* - :ref:`guide_cn-minibatch`
|
||||
- 莫佳帅子/Molasses
|
||||
- https://github.com/sleeplessai
|
||||
* - :ref:`guide_cn-minibatch-node-classification-sampler`
|
||||
- 孟凡荣/kevin-meng
|
||||
- https://github.com/kevin-meng
|
||||
* - :ref:`guide_cn-minibatch-edge-classification-sampler`
|
||||
- 莫佳帅子/Molasses
|
||||
- https://github.com/sleeplessai
|
||||
* - :ref:`guide_cn-minibatch-link-classification-sampler`
|
||||
- 孟凡荣/kevin-meng
|
||||
- https://github.com/kevin-meng
|
||||
* - :ref:`guide_cn-minibatch-customizing-neighborhood-sampler`
|
||||
- 孟凡荣/kevin-meng
|
||||
- https://github.com/kevin-meng
|
||||
* - :ref:`guide_cn-minibatch-custom-gnn-module`
|
||||
- 胡骏
|
||||
- https://github.com/CrawlScript
|
||||
* - :ref:`guide_cn-minibatch-inference`
|
||||
- 胡骏
|
||||
- https://github.com/CrawlScript
|
||||
* - :ref:`guide_cn-distributed`
|
||||
- 宋怡然/Yiran Song
|
||||
- https://github.com/rr-Yiran
|
||||
* - :ref:`guide_cn-distributed-preprocessing`
|
||||
- 宋怡然/Yiran Song
|
||||
- https://github.com/rr-Yiran
|
||||
* - :ref:`guide_cn-distributed-apis`
|
||||
- 李庆标/Qingbiao Li
|
||||
- https://qingbiaoli.github.io/
|
||||
* - :ref:`guide_cn-distributed-tools`
|
||||
- 李庆标/Qingbiao Li
|
||||
- https://qingbiaoli.github.io/
|
||||
@@ -0,0 +1,78 @@
|
||||
.. _guide_cn-message-passing-api:
|
||||
|
||||
2.1 内置函数和消息传递API
|
||||
----------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-api>`
|
||||
|
||||
在DGL中,**消息函数** 接受一个参数 ``edges``,这是一个 :class:`~dgl.udf.EdgeBatch` 的实例,
|
||||
在消息传递时,它被DGL在内部生成以表示一批边。 ``edges`` 有 ``src``、 ``dst`` 和 ``data`` 共3个成员属性,
|
||||
分别用于访问源节点、目标节点和边的特征。
|
||||
|
||||
**聚合函数** 接受一个参数 ``nodes``,这是一个 :class:`~dgl.udf.NodeBatch` 的实例,
|
||||
在消息传递时,它被DGL在内部生成以表示一批节点。 ``nodes`` 的成员属性 ``mailbox`` 可以用来访问节点收到的消息。
|
||||
一些最常见的聚合操作包括 ``sum``、``max``、``min`` 等。
|
||||
|
||||
**更新函数** 接受一个如上所述的参数 ``nodes``。此函数对 ``聚合函数`` 的聚合结果进行操作,
|
||||
通常在消息传递的最后一步将其与节点的特征相结合,并将输出作为节点的新特征。
|
||||
|
||||
DGL在命名空间 ``dgl.function`` 中实现了常用的消息函数和聚合函数作为 **内置函数**。
|
||||
一般来说,DGL建议 **尽可能** 使用内置函数,因为它们经过了大量优化,并且可以自动处理维度广播。
|
||||
|
||||
如果用户的消息传递函数无法用内置函数实现,则可以实现自己的消息或聚合函数(也称为 **用户定义函数** )。
|
||||
|
||||
内置消息函数可以是一元函数或二元函数。对于一元函数,DGL支持 ``copy`` 函数。对于二元函数,
|
||||
DGL现在支持 ``add``、 ``sub``、 ``mul``、 ``div``、 ``dot`` 函数。消息的内置函数的命名约定是 ``u`` 表示 ``源`` 节点,
|
||||
``v`` 表示 ``目标`` 节点,``e`` 表示 ``边``。这些函数的参数是字符串,指示相应节点和边的输入和输出特征字段名。
|
||||
关于内置函数的列表,请参见 :ref:`api-built-in`。例如,要对源节点的 ``hu`` 特征和目标节点的 ``hv`` 特征求和,
|
||||
然后将结果保存在边的 ``he`` 特征上,用户可以使用内置函数 ``dgl.function.u_add_v('hu', 'hv', 'he')``。
|
||||
而以下用户定义消息函数与此内置函数等价。
|
||||
|
||||
.. code::
|
||||
|
||||
def message_func(edges):
|
||||
return {'he': edges.src['hu'] + edges.dst['hv']}
|
||||
|
||||
DGL支持内置的聚合函数 ``sum``、 ``max``、 ``min`` 和 ``mean`` 操作。
|
||||
聚合函数通常有两个参数,它们的类型都是字符串。一个用于指定 ``mailbox`` 中的字段名,一个用于指示目标节点特征的字段名,
|
||||
例如, ``dgl.function.sum('m', 'h')`` 等价于如下所示的对接收到消息求和的用户定义函数:
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
def reduce_func(nodes):
|
||||
return {'h': torch.sum(nodes.mailbox['m'], dim=1)}
|
||||
|
||||
关于用户定义函数的进阶用法,参见 :ref:`apiudf`。
|
||||
|
||||
在DGL中,也可以在不涉及消息传递的情况下,通过 :meth:`~dgl.DGLGraph.apply_edges` 单独调用逐边计算。
|
||||
:meth:`~dgl.DGLGraph.apply_edges` 的参数是一个消息函数。并且在默认情况下,这个接口将更新所有的边。例如:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
graph.apply_edges(fn.u_add_v('el', 'er', 'e'))
|
||||
|
||||
对于消息传递, :meth:`~dgl.DGLGraph.update_all` 是一个高级API。它在单个API调用里合并了消息生成、
|
||||
消息聚合和节点特征更新,这为从整体上进行系统优化提供了空间。
|
||||
|
||||
:meth:`~dgl.DGLGraph.update_all` 的参数是一个消息函数、一个聚合函数和一个更新函数。
|
||||
更新函数是一个可选择的参数,用户也可以不使用它,而是在 ``update_all`` 执行完后直接对节点特征进行操作。
|
||||
由于更新函数通常可以用纯张量操作实现,所以DGL不推荐在 ``update_all`` 中指定更新函数。例如:
|
||||
|
||||
.. code::
|
||||
|
||||
def update_all_example(graph):
|
||||
# 在graph.ndata['ft']中存储结果
|
||||
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
|
||||
fn.sum('m', 'ft'))
|
||||
# 在update_all外调用更新函数
|
||||
final_ft = graph.ndata['ft'] * 2
|
||||
return final_ft
|
||||
|
||||
此调用通过将源节点特征 ``ft`` 与边特征 ``a`` 相乘生成消息 ``m``,
|
||||
然后对所有消息求和来更新节点特征 ``ft``,再将 ``ft`` 乘以2得到最终结果 ``final_ft``。
|
||||
|
||||
调用后,中间消息 ``m`` 将被清除。上述函数的数学公式为:
|
||||
|
||||
.. math:: {final\_ft}_i = 2 * \sum_{j\in\mathcal{N}(i)} ({ft}_j * a_{ij})
|
||||
@@ -0,0 +1,47 @@
|
||||
.. _guide_cn-message-passing-efficient:
|
||||
|
||||
2.2 编写高效的消息传递代码
|
||||
----------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-efficient>`
|
||||
|
||||
DGL优化了消息传递的内存消耗和计算速度。利用这些优化的一个常见实践是通过基于内置函数的 :meth:`~dgl.DGLGraph.update_all` 来开发消息传递功能。
|
||||
|
||||
除此之外,考虑到某些图边的数量远远大于节点的数量,DGL建议避免不必要的从点到边的内存拷贝。对于某些情况,比如 :class:`~dgl.nn.pytorch.conv.GATConv`,计算必须在边上保存消息,
|
||||
那么用户就需要调用基于内置函数的 :meth:`~dgl.DGLGraph.apply_edges`。有时边上的消息可能是高维的,这会非常消耗内存。
|
||||
DGL建议用户尽量减少边的特征维数。
|
||||
|
||||
下面是一个如何通过对节点特征降维来减少消息维度的示例。该做法执行以下操作:拼接 ``源`` 节点和 ``目标`` 节点特征,
|
||||
然后应用一个线性层,即 :math:`W\times (u || v)`。 ``源`` 节点和 ``目标`` 节点特征维数较高,而线性层输出维数较低。
|
||||
一个直截了当的实现方式如下:
|
||||
|
||||
.. 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
|
||||
|
||||
建议的实现是将线性操作分成两部分,一个应用于 ``源`` 节点特征,另一个应用于 ``目标`` 节点特征。
|
||||
在最后一个阶段,在边上将以上两部分线性操作的结果相加,即执行 :math:`W_l\times u + W_r \times v`,
|
||||
因为 :math:`W \times (u||v) = W_l \times u + W_r \times v`,其中 :math:`W_l` 和 :math:`W_r` 分别是矩阵
|
||||
:math:`W` 的左半部分和右半部分:
|
||||
|
||||
.. 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'))
|
||||
|
||||
以上两个实现在数学上是等价的。后一种方法效率高得多,因为不需要在边上保存feat_src和feat_dst,
|
||||
从内存角度来说是高效的。另外,加法可以通过DGL的内置函数 ``u_add_v`` 进行优化,从而进一步加快计算速度并节省内存占用。
|
||||
@@ -0,0 +1,38 @@
|
||||
.. _guide_cn-message-passing-heterograph:
|
||||
|
||||
2.5 在异构图上进行消息传递
|
||||
----------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-heterograph>`
|
||||
|
||||
异构图(参考用户指南 :ref:`1.5 异构图 <guide_cn-graph-heterogeneous>` )是包含不同类型的节点和边的图。
|
||||
不同类型的节点和边常常具有不同类型的属性。这些属性旨在刻画每一种节点和边的特征。在使用图神经网络时,根据其复杂性,
|
||||
可能需要使用不同维度的表示来对不同类型的节点和边进行建模。
|
||||
|
||||
异构图上的消息传递可以分为两个部分:
|
||||
|
||||
1. 对每个关系计算和聚合消息。
|
||||
2. 对每个结点聚合来自不同关系的消息。
|
||||
|
||||
在DGL中,对异构图进行消息传递的接口是 :meth:`~dgl.DGLGraph.multi_update_all`。
|
||||
:meth:`~dgl.DGLGraph.multi_update_all` 接受一个字典。这个字典的每一个键值对里,键是一种关系,
|
||||
值是这种关系对应 :meth:`~dgl.DGLGraph.update_all` 的参数。
|
||||
:meth:`~dgl.DGLGraph.multi_update_all` 还接受一个字符串来表示跨类型整合函数,来指定整合不同关系聚合结果的方式。
|
||||
这个整合方式可以是 ``sum``、 ``min``、 ``max``、 ``mean`` 和 ``stack`` 中的一个。以下是一个例子:
|
||||
|
||||
.. 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])
|
||||
# 把它存在图中用来做消息传递
|
||||
G.nodes[srctype].data['Wh_%s' % etype] = Wh
|
||||
# 指定每个关系的消息传递函数:(message_func, reduce_func).
|
||||
# 注意结果保存在同一个目标特征“h”,说明聚合是逐类进行的。
|
||||
funcs[etype] = (fn.copy_u('Wh_%s' % etype, 'm'), fn.mean('m', 'h'))
|
||||
# 将每个类型消息聚合的结果相加。
|
||||
G.multi_update_all(funcs, 'sum')
|
||||
# 返回更新过的节点特征字典
|
||||
return {ntype : G.nodes[ntype].data['h'] for ntype in G.ntypes}
|
||||
@@ -0,0 +1,17 @@
|
||||
.. _guide_cn-message-passing-part:
|
||||
|
||||
2.3 在图的一部分上进行消息传递
|
||||
-------------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-part>`
|
||||
|
||||
如果用户只想更新图中的部分节点,可以先通过想要囊括的节点编号创建一个子图,
|
||||
然后在子图上调用 :meth:`~dgl.DGLGraph.update_all` 方法。例如:
|
||||
|
||||
.. code::
|
||||
|
||||
nid = [0, 2, 3, 6, 7, 9]
|
||||
sg = g.subgraph(nid)
|
||||
sg.update_all(message_func, reduce_func, apply_node_func)
|
||||
|
||||
这是小批量训练中的常见用法。更多详细用法请参考用户指南 :ref:`guide_cn-minibatch`。
|
||||
@@ -0,0 +1,41 @@
|
||||
.. _guide_cn-message-passing:
|
||||
|
||||
第2章:消息传递范式
|
||||
===========================
|
||||
|
||||
:ref:`(English Version) <guide-message-passing>`
|
||||
|
||||
消息传递是实现GNN的一种通用框架和编程范式。它从聚合与更新的角度归纳总结了多种GNN模型的实现。
|
||||
|
||||
消息传递范式
|
||||
----------------------
|
||||
|
||||
假设节点 :math:`v` 上的的特征为 :math:`x_v\in\mathbb{R}^{d_1}`,边 :math:`({u}, {v})` 上的特征为 :math:`w_{e}\in\mathbb{R}^{d_2}`。
|
||||
**消息传递范式** 定义了以下逐节点和边上的计算:
|
||||
|
||||
.. math:: \text{边上计算: } m_{e}^{(t+1)} = \phi \left( x_v^{(t)}, x_u^{(t)}, w_{e}^{(t)} \right) , ({u}, {v},{e}) \in \mathcal{E}.
|
||||
|
||||
.. math:: \text{点上计算: } 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).
|
||||
|
||||
在上面的等式中, :math:`\phi` 是定义在每条边上的消息函数,它通过将边上特征与其两端节点的特征相结合来生成消息。
|
||||
**聚合函数** :math:`\rho` 会聚合节点接受到的消息。 **更新函数** :math:`\psi` 会结合聚合后的消息和节点本身的特征来更新节点的特征。
|
||||
|
||||
本章路线图
|
||||
--------------------
|
||||
|
||||
本章首先介绍了DGL的消息传递API。然后讲解了如何高效地在点和边上使用这些API。本章的最后一节解释了如何在异构图上实现消息传递。
|
||||
|
||||
* :ref:`guide_cn-message-passing-api`
|
||||
* :ref:`guide_cn-message-passing-efficient`
|
||||
* :ref:`guide_cn-message-passing-part`
|
||||
* :ref:`guide_cn-message-passing-heterograph`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
message-api
|
||||
message-efficient
|
||||
message-part
|
||||
message-heterograph
|
||||
@@ -0,0 +1,389 @@
|
||||
.. _guide_cn-minibatch-customizing-neighborhood-sampler:
|
||||
|
||||
6.4 定制用户自己的邻居采样器
|
||||
----------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-customizing-neighborhood-sampler>`
|
||||
|
||||
虽然DGL提供了一些邻居采样器,但有时用户还是希望编写自己的采样器。
|
||||
本节会说明如何编写用户自己的采样器并将其加入到GNN的训练框架中。
|
||||
|
||||
回想一下在
|
||||
`How Powerful are Graph Neural Networks <https://arxiv.org/pdf/1810.00826.pdf>`__
|
||||
的论文中,消息传递的定义是:
|
||||
|
||||
.. math::
|
||||
|
||||
\begin{gathered}
|
||||
\boldsymbol{a}_v^{(l)} = \rho^{(l)} \left(
|
||||
\left\lbrace
|
||||
\boldsymbol{h}_u^{(l-1)} : u \in \mathcal{N} \left( v \right)
|
||||
\right\rbrace
|
||||
\right)
|
||||
\\
|
||||
\boldsymbol{h}_v^{(l)} = \phi^{(l)} \left(
|
||||
\boldsymbol{h}_v^{(l-1)}, \boldsymbol{a}_v^{(l)}
|
||||
\right)
|
||||
\end{gathered}
|
||||
|
||||
其中, :math:`\rho^{(l)}` 和 :math:`\phi^{(l)}` 分别是可自定义的消息函数与聚合函数,
|
||||
:math:`\mathcal{N}(v)` 为有向图 :math:`\mathcal{G}` 上的节点 :math:`v` 的前驱节点(或无向图中的邻居)。
|
||||
|
||||
以下图为例,假设红色节点为需要更新的目标节点:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_0.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
消息传递需要聚集其邻居(绿色节点)的节点特征,如下图所示:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_1.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
理解邻居采样的工作原理
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在介绍DGL中邻居采样的用法之前,这里先解释一下邻居采样的工作原理。下文继续使用上述的例子。
|
||||
首先定义一个如上图所示的DGLGraph。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch
|
||||
import dgl
|
||||
|
||||
src = torch.LongTensor(
|
||||
[0, 0, 0, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 10,
|
||||
1, 2, 3, 3, 3, 4, 5, 5, 6, 5, 8, 6, 8, 9, 8, 11, 11, 10, 11])
|
||||
dst = torch.LongTensor(
|
||||
[1, 2, 3, 3, 3, 4, 5, 5, 6, 5, 8, 6, 8, 9, 8, 11, 11, 10, 11,
|
||||
0, 0, 0, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 10])
|
||||
g = dgl.graph((src, dst))
|
||||
|
||||
该例子的目标是计算单个节点(节点8)的输出。DGL将需要计算GNN输出的节点称为 *种子节点* 。
|
||||
|
||||
找出消息传递的依赖
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
假设要使用2层GNN计算种子节点8(红色点)的输出:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_2.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
其消息传递的计算公式如下:
|
||||
|
||||
.. math::
|
||||
|
||||
\begin{gathered}
|
||||
\boldsymbol{a}_8^{(2)} = \rho^{(2)} \left(
|
||||
\left\lbrace
|
||||
\boldsymbol{h}_u^{(1)} : u \in \mathcal{N} \left( 8 \right)
|
||||
\right\rbrace
|
||||
\right) = \rho^{(2)} \left(
|
||||
\left\lbrace
|
||||
\boldsymbol{h}_4^{(1)}, \boldsymbol{h}_5^{(1)},
|
||||
\boldsymbol{h}_7^{(1)}, \boldsymbol{h}_{11}^{(1)}
|
||||
\right\rbrace
|
||||
\right)
|
||||
\\
|
||||
\boldsymbol{h}_8^{(2)} = \phi^{(2)} \left(
|
||||
\boldsymbol{h}_8^{(1)}, \boldsymbol{a}_8^{(2)}
|
||||
\right)
|
||||
\end{gathered}
|
||||
|
||||
从公式中可以看出,要计算 :math:`\boldsymbol{h}_8^{(2)}`,需要下图中的来自节点4、5、7和11(绿色点)的消息。
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_3.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
上图中隐去了和计算不相关的边,仅仅保留了输出节点所需要收集消息的边。DGL称它们为红色节点8在第二个GNN层的 *边界子图*。
|
||||
|
||||
DGL实现了多个可用于生成边界的函数。例如,
|
||||
:func:`dgl.in_subgraph()` 是一个生成子图的函数,该子图包括初始图中的所有节点和指定节点的入边。
|
||||
用户可以将其用作沿所有入边传递消息的边界。
|
||||
|
||||
.. code:: python
|
||||
|
||||
frontier = dgl.in_subgraph(g, [8])
|
||||
print(frontier.all_edges())
|
||||
|
||||
想了解更多的相关函数,用户可以参考 :ref:`api-subgraph-extraction` 和 :ref:`api-sampling`。
|
||||
|
||||
在DGL中,任何具有与初始图相同的节点的图都可以用作边界。这点在之后的
|
||||
:ref:`guide_cn-minibatch-customizing-neighborhood-sampler-impl`
|
||||
章节中也会提到。
|
||||
|
||||
多层小批量消息传递的二分计算图
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
从上图中可以看到,从 :math:`\boldsymbol{h}_\cdot^{(1)}` 计算
|
||||
:math:`\boldsymbol{h}_8^{(2)}` 只需要节点4, 5, 7, 8和11(绿色和红色节点)作为输入。
|
||||
原图上的其他节点是不参与计算的,因此直接在边界子图上执行消息传递有很大开销。
|
||||
因此,DGL对边界子图做了一个转换,把它的计算依赖关系变成了一个小的二分图。
|
||||
DGL称这种仅包含必要的输入节点和输出节点的二分图为一个 *块* (block)。
|
||||
下图显示了以节点8为种子节点时第二个GNN层所需的块。
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_4.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
请注意,输出节点也出现在输入节点中。原因是消息传递后的特征组合需要前一层的输出节点表示
|
||||
(即 :math:`\phi^{(2)}`)。
|
||||
|
||||
DGL提供了 :func:`dgl.to_block` 以将任何边界转换为块。其中第一个参数指定边界,
|
||||
第二个参数指定输出节点。例如,可以使用以下代码将上述边界转换为输出节点为8的块。
|
||||
|
||||
.. code:: python
|
||||
|
||||
output_nodes = torch.LongTensor([8])
|
||||
block = dgl.to_block(frontier, output_nodes)
|
||||
|
||||
要查找给定节点类型的输入节点和输出节点的数量,可以使用
|
||||
:meth:`dgl.DGLGraph.number_of_src_nodes` 和
|
||||
:meth:`dgl.DGLGraph.number_of_dst_nodes` 方法。
|
||||
|
||||
.. code:: python
|
||||
|
||||
num_input_nodes, num_output_nodes = block.number_of_src_nodes(), block.number_of_dst_nodes()
|
||||
print(num_input_nodes, num_output_nodes)
|
||||
|
||||
可以通过 :attr:`dgl.DGLGraph.srcdata` 和
|
||||
:attr:`dgl.DGLGraph.srcnodes` 访问该块的输入节点特征,
|
||||
并且可以通过 :attr:`dgl.DGLGraph.dstdata` 和
|
||||
:attr:`dgl.DGLGraph.dstnodes` 访问其输出节点特征。
|
||||
``srcdata``/``dstdata`` 和 ``srcnodes``/``dstnodes``
|
||||
的语法与常规图中的 :attr:`dgl.DGLGraph.ndata` 和 :attr:`dgl.DGLGraph.nodes` 相同。
|
||||
|
||||
.. code:: python
|
||||
|
||||
block.srcdata['h'] = torch.randn(num_input_nodes, 5)
|
||||
block.dstdata['h'] = torch.randn(num_output_nodes, 5)
|
||||
|
||||
如果是从图中得到的边界,再由边界转换成块,则可以通过以下方式直接读取块的输入和输出节点的特征。
|
||||
|
||||
.. code:: python
|
||||
|
||||
print(block.srcdata['x'])
|
||||
print(block.dstdata['y'])
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<div class="alert alert-info">
|
||||
|
||||
::
|
||||
|
||||
用户可以通过 ``dgl.NID`` 得到块中输入节点和输出节点的初始节点ID,可以通过 ``dgl.EID``
|
||||
得到边ID到输入边界中边的初始ID的映射。
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</div>
|
||||
|
||||
**输出节点**
|
||||
|
||||
DGL确保块的输出节点将始终出现在输入节点中。如下代码所演示的,在输入节点中,输出节点的ID位于其它节点之前。
|
||||
|
||||
.. code:: python
|
||||
|
||||
input_nodes = block.srcdata[dgl.NID]
|
||||
output_nodes = block.dstdata[dgl.NID]
|
||||
assert torch.equal(input_nodes[:len(output_nodes)], output_nodes)
|
||||
|
||||
因此,在用多层图神经网络时,中间某一层对应的边界需要包含该层及所有后续层计算涉及边的目标节点。例如,考虑以下边界
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_5.png
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
其中红色和绿色节点(即节点4、5、7、8和11)都是后续图神经网络层计算中某条边的目标节点。
|
||||
以下代码由于输出节点未覆盖所有这些节点,将会报错。
|
||||
|
||||
.. code:: python
|
||||
|
||||
dgl.to_block(frontier2, torch.LongTensor([4, 5])) # ERROR
|
||||
|
||||
但是,输出节点可以比以上节点包含更多节点。下例的输出节点包含了没有入边的孤立节点。
|
||||
输入节点和输出节点将同时包含这些孤立节点。
|
||||
|
||||
.. code:: python
|
||||
|
||||
# 节点3是一个孤立节点,没有任何指向它的边.
|
||||
block3 = dgl.to_block(frontier2, torch.LongTensor([4, 5, 7, 8, 11, 3]))
|
||||
print(block3.srcdata[dgl.NID])
|
||||
print(block3.dstdata[dgl.NID])
|
||||
|
||||
异构图上的采样
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
块也可用于异构图。假设有如下的边界:
|
||||
|
||||
.. code:: python
|
||||
|
||||
hetero_frontier = dgl.heterograph({
|
||||
('user', 'follow', 'user'): ([1, 3, 7], [3, 6, 8]),
|
||||
('user', 'play', 'game'): ([5, 5, 4], [6, 6, 2]),
|
||||
('game', 'played-by', 'user'): ([2], [6])
|
||||
}, num_nodes_dict={'user': 10, 'game': 10})
|
||||
|
||||
可以创建一个如下的块,块的输出节点为 ``User`` 节点3、6、8和 ``Game`` 节点2、6。
|
||||
|
||||
.. code:: python
|
||||
|
||||
hetero_block = dgl.to_block(hetero_frontier, {'user': [3, 6, 8], 'block': [2, 6]})
|
||||
|
||||
对于这个块,用户可以按节点类型来获取输入节点和输出节点:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# 输入的User和Game节点
|
||||
print(hetero_block.srcnodes['user'].data[dgl.NID], hetero_block.srcnodes['game'].data[dgl.NID])
|
||||
# 输出的User和Game节点
|
||||
print(hetero_block.dstnodes['user'].data[dgl.NID], hetero_block.dstnodes['game'].data[dgl.NID])
|
||||
|
||||
|
||||
.. _guide_cn-minibatch-customizing-neighborhood-sampler-impl:
|
||||
|
||||
实现一个自定义邻居采样器
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
前面章节里给出了以下用在节点分类任务的邻居采样器。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
|
||||
想实现自定义的邻居采样策略,用户可以将采样器对象替换为自定义的采样器对象。
|
||||
为此,先来看一下
|
||||
:class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler`
|
||||
的父类
|
||||
:class:`~dgl.dataloading.dataloader.BlockSampler`。
|
||||
|
||||
:class:`~dgl.dataloading.dataloader.BlockSampler`
|
||||
负责使用
|
||||
:meth:`~dgl.dataloading.dataloader.BlockSampler.sample_blocks`
|
||||
方法从最后一层开始生成一个块的列表。 ``sample_blocks`` 的默认实现是向后迭代,生成边界,并将其转换为块。
|
||||
|
||||
因此,对于邻居采样,**用户仅需要实现**\ :meth:`~dgl.dataloading.dataloader.BlockSampler.sample_frontier`\ **方法**。
|
||||
给定GNN层、初始图和要计算表示的节点,该方法负责为它们生成边界。
|
||||
|
||||
同时,用户还必须将GNN的层数传递给父类。
|
||||
|
||||
例如, :class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler` 的实现如下。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MultiLayerFullNeighborSampler(dgl.dataloading.BlockSampler):
|
||||
def __init__(self, n_layers):
|
||||
super().__init__(n_layers)
|
||||
|
||||
def sample_frontier(self, block_id, g, seed_nodes):
|
||||
frontier = dgl.in_subgraph(g, seed_nodes)
|
||||
return frontier
|
||||
|
||||
:class:`dgl.dataloading.neighbor.MultiLayerNeighborSampler`
|
||||
是一个更复杂的邻居采样器类,它允许用户为每个节点采样部分邻居节点以汇聚信息,如下所示。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MultiLayerNeighborSampler(dgl.dataloading.BlockSampler):
|
||||
def __init__(self, fanouts):
|
||||
super().__init__(len(fanouts))
|
||||
|
||||
self.fanouts = fanouts
|
||||
|
||||
def sample_frontier(self, block_id, g, seed_nodes):
|
||||
fanout = self.fanouts[block_id]
|
||||
if fanout is None:
|
||||
frontier = dgl.in_subgraph(g, seed_nodes)
|
||||
else:
|
||||
frontier = dgl.sampling.sample_neighbors(g, seed_nodes, fanout)
|
||||
return frontier
|
||||
|
||||
虽然上面的函数可以生成边界,但是任何拥有与初始图相同节点的图都可用作边界。
|
||||
|
||||
例如,如果要以某种概率将种子节点的入边随机剔除,则可以按照以下方式简单地定义采样器:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MultiLayerDropoutSampler(dgl.dataloading.BlockSampler):
|
||||
def __init__(self, p, num_layers):
|
||||
super().__init__(num_layers)
|
||||
|
||||
self.p = p
|
||||
|
||||
def sample_frontier(self, block_id, g, seed_nodes, *args, **kwargs):
|
||||
# 获取种 `seed_nodes` 的所有入边
|
||||
src, dst = dgl.in_subgraph(g, seed_nodes).all_edges()
|
||||
# 以概率p随机选择边
|
||||
mask = torch.zeros_like(src).bernoulli_(self.p)
|
||||
src = src[mask]
|
||||
dst = dst[mask]
|
||||
# 返回一个与初始图有相同节点的边界
|
||||
frontier = dgl.graph((src, dst), num_nodes=g.num_nodes())
|
||||
return frontier
|
||||
|
||||
def __len__(self):
|
||||
return self.num_layers
|
||||
|
||||
在实现自定义采样器后,用户可以创建一个数据加载器。这个数据加载器使用用户自定义的采样器,
|
||||
并且遍历种子节点生成一系列的块。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = MultiLayerDropoutSampler(0.5, 2)
|
||||
dataloader = dgl.dataloading.NodeDataLoader(
|
||||
g, train_nids, sampler,
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
model = StochasticTwoLayerRGCN(in_features, hidden_features, out_features)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
input_features = blocks[0].srcdata # 返回一个字典
|
||||
output_labels = blocks[-1].dstdata # 返回一个字典
|
||||
output_predictions = model(blocks, input_features)
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
异构图上自定义采样器
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
为异构图生成边界与为同构图生成边界没有什么不同。只要使返回的图具有与初始图相同的节点,
|
||||
就可以正常工作。例如,可以重写上面的 ``MultiLayerDropoutSampler`` 以遍历所有的边类型,
|
||||
以便它也可以在异构图上使用。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MultiLayerDropoutSampler(dgl.dataloading.BlockSampler):
|
||||
def __init__(self, p, num_layers):
|
||||
super().__init__(num_layers)
|
||||
|
||||
self.p = p
|
||||
|
||||
def sample_frontier(self, block_id, g, seed_nodes, *args, **kwargs):
|
||||
# 获取 `seed_nodes` 的所有入边
|
||||
sg = dgl.in_subgraph(g, seed_nodes)
|
||||
|
||||
new_edges_masks = {}
|
||||
# 遍历所有边的类型
|
||||
for etype in sg.canonical_etypes:
|
||||
edge_mask = torch.zeros(sg.num_edges(etype))
|
||||
edge_mask.bernoulli_(self.p)
|
||||
new_edges_masks[etype] = edge_mask.bool()
|
||||
|
||||
# 返回一个与初始图有相同节点的图作为边界
|
||||
frontier = dgl.edge_subgraph(new_edges_masks, relabel_nodes=False)
|
||||
return frontier
|
||||
|
||||
def __len__(self):
|
||||
return self.num_layers
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
.. _guide_cn-minibatch-edge-classification-sampler:
|
||||
|
||||
6.2 针对边分类任务的邻居采样训练方法
|
||||
----------------------------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-edge-classification-sampler>`
|
||||
|
||||
边分类/回归的训练与节点分类/回归的训练类似,但还是有一些明显的区别。
|
||||
|
||||
定义邻居采样器和数据加载器
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
用户可以使用
|
||||
:ref:`和节点分类一样的邻居采样器 <guide_cn-minibatch-node-classification-sampler>`。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
|
||||
想要用DGL提供的邻居采样器做边分类,需要将其与
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader` 结合使用。
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader` 以小批次的形式对一组边进行迭代,
|
||||
从而产生包含边小批次的子图以及供下文中模块使用的 ``块``。
|
||||
|
||||
例如,以下代码创建了一个PyTorch数据加载器,该PyTorch数据加载器以批的形式迭代训练边ID的数组
|
||||
``train_eids``,并将生成的块列表放到GPU上。
|
||||
|
||||
.. code:: python
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
有关DGL的内置采样器的完整列表,用户可以参考
|
||||
:ref:`neighborhood sampler API reference <api-dataloading-neighbor-sampling>`。
|
||||
|
||||
如果用户希望开发自己的邻居采样器,或者想要对块的概念有更详细的了解,请参考
|
||||
:ref:`guide_cn-minibatch-customizing-neighborhood-sampler`。
|
||||
|
||||
小批次邻居采样训练时删边
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
用户在训练边分类模型时,有时希望从计算依赖中删除出现在训练数据中的边,就好像这些边根本不存在一样。
|
||||
否则,模型将 "知道" 两个节点之间存在边的联系,并有可能利用这点 "作弊" 。
|
||||
|
||||
因此,在基于邻居采样的边分类中,用户有时会希望从采样得到的小批次图中删去部分边及其对应的反向边。
|
||||
用户可以在实例化
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader`
|
||||
时设置 ``exclude='reverse_id'``,同时将边ID映射到其反向边ID。
|
||||
通常这样做会导致采样过程变慢很多,这是因为DGL要定位并删除包含在小批次中的反向边。
|
||||
|
||||
.. code:: python
|
||||
|
||||
n_edges = g.num_edges()
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
|
||||
# 下面的两个参数专门用于在邻居采样时删除小批次的一些边和它们的反向边
|
||||
exclude='reverse_id',
|
||||
reverse_eids=torch.cat([
|
||||
torch.arange(n_edges // 2, n_edges), torch.arange(0, n_edges // 2)]),
|
||||
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
调整模型以适用小批次训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
边分类模型通常由两部分组成:
|
||||
|
||||
- 获取边两端节点的表示。
|
||||
- 用边两端节点表示为每个类别打分。
|
||||
|
||||
第一部分与
|
||||
:ref:`随机批次训练节点分类 <guide_cn-minibatch-node-classification-model>`
|
||||
完全相同,用户可以简单地复用它。输入仍然是DGL的数据加载器生成的块列表和输入特征。
|
||||
|
||||
.. 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
|
||||
|
||||
第二部分的输入通常是前一部分的输出,以及由小批次边导出的原始图的子图。
|
||||
子图是从相同的数据加载器产生的。用户可以调用 :meth:`dgl.DGLGraph.apply_edges` 计算边子图中边的得分。
|
||||
|
||||
以下代码片段实现了通过合并边两端节点的特征并将其映射到全连接层来预测边的得分。
|
||||
|
||||
.. 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 apply_edges(self, edges):
|
||||
data = torch.cat([edges.src['x'], edges.dst['x']], 1)
|
||||
return {'score': self.W(data)}
|
||||
|
||||
def forward(self, edge_subgraph, x):
|
||||
with edge_subgraph.local_scope():
|
||||
edge_subgraph.ndata['x'] = x
|
||||
edge_subgraph.apply_edges(self.apply_edges)
|
||||
return edge_subgraph.edata['score']
|
||||
|
||||
模型接受数据加载器生成的块列表、边子图以及输入节点特征进行前向传播,如下所示:
|
||||
|
||||
.. 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, edge_subgraph, blocks, x):
|
||||
x = self.gcn(blocks, x)
|
||||
return self.predictor(edge_subgraph, x)
|
||||
|
||||
DGL保证边子图中的节点与生成的块列表中最后一个块的输出节点相同。
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
模型的训练与节点分类的随机批次训练的情况非常相似。用户可以遍历数据加载器以获得由小批次边组成的子图,
|
||||
以及计算其两端节点表示所需的块列表。
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = Model(in_features, hidden_features, out_features, num_classes)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, edge_subgraph, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
edge_subgraph = edge_subgraph.to(torch.device('cuda'))
|
||||
input_features = blocks[0].srcdata['features']
|
||||
edge_labels = edge_subgraph.edata['labels']
|
||||
edge_predictions = model(edge_subgraph, blocks, input_features)
|
||||
loss = compute_loss(edge_labels, edge_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
异构图上的模型训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在异构图上,计算节点表示的模型也可以用于计算边分类/回归所需的两端节点的表示。
|
||||
|
||||
.. 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
|
||||
|
||||
在同构图和异构图上做评分预测时,代码实现的唯一不同在于调用
|
||||
:meth:`~dgl.DGLGraph.apply_edges`
|
||||
时需要在特定类型的边上进行迭代。
|
||||
|
||||
.. 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 apply_edges(self, edges):
|
||||
data = torch.cat([edges.src['x'], edges.dst['x']], 1)
|
||||
return {'score': self.W(data)}
|
||||
|
||||
def forward(self, edge_subgraph, x):
|
||||
with edge_subgraph.local_scope():
|
||||
edge_subgraph.ndata['x'] = x
|
||||
for etype in edge_subgraph.canonical_etypes:
|
||||
edge_subgraph.apply_edges(self.apply_edges, etype=etype)
|
||||
return edge_subgraph.edata['score']
|
||||
|
||||
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, edge_subgraph, blocks, x):
|
||||
x = self.rgcn(blocks, x)
|
||||
return self.pred(edge_subgraph, x)
|
||||
|
||||
数据加载器的定义也与节点分类的非常相似。唯一的区别是用户需要使用
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader`
|
||||
而不是
|
||||
:class:`~dgl.dataloading.pytorch.NodeDataLoader`,
|
||||
并且提供边类型和边ID张量的字典,而不是节点类型和节点ID张量的字典。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
如果用户希望删除异构图中的反向边,情况会有所不同。在异构图上,
|
||||
反向边通常具有与正向边本身不同的边类型,以便区分 ``向前`` 和 ``向后`` 关系。
|
||||
例如,``关注`` 和 ``被关注`` 是一对相反的关系, ``购买`` 和 ``被买下`` 也是一对相反的关系。
|
||||
|
||||
如果一个类型中的每个边都有一个与之对应的ID相同、属于另一类型的反向边,
|
||||
则用户可以指定边类型及其反向边类型之间的映射。删除小批次中的边及其反向边的方法如下。
|
||||
|
||||
.. code:: python
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
|
||||
# 下面的两个参数专门用于在邻居采样时删除小批次的一些边和它们的反向边
|
||||
exclude='reverse_types',
|
||||
reverse_etypes={'follow': 'followed by', 'followed by': 'follow',
|
||||
'purchase': 'purchased by', 'purchased by': 'purchase'}
|
||||
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
除了 ``compute_loss`` 的代码实现有所不同,异构图的训练循环与同构图中的训练循环几乎相同,
|
||||
计算损失函数接受节点类型和预测的两个字典。
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = Model(in_features, hidden_features, out_features, num_classes, etypes)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, edge_subgraph, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
edge_subgraph = edge_subgraph.to(torch.device('cuda'))
|
||||
input_features = blocks[0].srcdata['features']
|
||||
edge_labels = edge_subgraph.edata['labels']
|
||||
edge_predictions = model(edge_subgraph, blocks, input_features)
|
||||
loss = compute_loss(edge_labels, edge_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
`GCMC <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__
|
||||
是一个在二分图上做边分类的代码示例。
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
.. _guide_cn-minibatch-inference:
|
||||
|
||||
6.6 超大图上的精准离线推断
|
||||
------------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-inference>`
|
||||
|
||||
子图采样和邻居采样都是为了减少用GPU训练GNN模型的内存和时间消耗。在进行推断时,
|
||||
通常更好的方法是将所有邻居进行真正的聚合,以避免采样所带来的随机性。
|
||||
然而,在GPU上进行全图前向传播通常由于显存大小的限制而不可行,而在CPU上进行则计算速度很慢。
|
||||
本节介绍了在GPU显存有限的情况下通过小批次处理和邻居采样实现全图前向传播的方法。
|
||||
|
||||
推断算法不同于训练算法,因为需要从第一层开始对节点表示逐层计算。具体来说,对于一个指定的层,
|
||||
需要以小批次的方式计算这个GNN层所有节点的输出表示。其结果是,推断算法将包含一个外循环以迭代执行各层,
|
||||
和一个内循环以迭代处理各个节点小批次。相比之下,训练算法有一个外循环以迭代处理各个节点小批次,
|
||||
和一个内循环以迭代执行各层(包含邻居采样和消息传递)。
|
||||
|
||||
下面的动画展示了计算的过程(注意,每层只展示前3个小批次):
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_6_0.gif
|
||||
:alt: Imgur
|
||||
|
||||
|
||||
实现离线推断
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
这里以6.1节中 :ref:`guide_cn-minibatch-node-classification-model`
|
||||
提到的两层GCN为例。实现离线推断的方法依然需要使用 ``MultiLayerFullNeighborSampler``,
|
||||
但它每次只为一层进行采样。注意,这里的离线推断被实现为GNN模块的一个方法,
|
||||
这是因为它对一层的计算依赖于消息的聚合和结合。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class StochasticTwoLayerGCN(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.hidden_features = hidden_features
|
||||
self.out_features = out_features
|
||||
self.conv1 = dgl.nn.GraphConv(in_features, hidden_features)
|
||||
self.conv2 = dgl.nn.GraphConv(hidden_features, out_features)
|
||||
self.n_layers = 2
|
||||
|
||||
def forward(self, blocks, x):
|
||||
x_dst = x[:blocks[0].number_of_dst_nodes()]
|
||||
x = F.relu(self.conv1(blocks[0], (x, x_dst)))
|
||||
x_dst = x[:blocks[1].number_of_dst_nodes()]
|
||||
x = F.relu(self.conv2(blocks[1], (x, x_dst)))
|
||||
return x
|
||||
|
||||
def inference(self, g, x, batch_size, device):
|
||||
""" 用该模块进行离线推断 """
|
||||
# 逐层计算表示
|
||||
for l, layer in enumerate([self.conv1, self.conv2]):
|
||||
y = torch.zeros(g.num_nodes(),
|
||||
self.hidden_features
|
||||
if l != self.n_layers - 1
|
||||
else self.out_features)
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(1)
|
||||
dataloader = dgl.dataloading.NodeDataLoader(
|
||||
g, torch.arange(g.num_nodes()), sampler,
|
||||
batch_size=batch_size,
|
||||
shuffle=True,
|
||||
drop_last=False)
|
||||
|
||||
# 在一层中,依批次对节点进行迭代
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
block = blocks[0]
|
||||
|
||||
# 将必要输入节点的特征复制到GPU上
|
||||
h = x[input_nodes].to(device)
|
||||
|
||||
# 计算输出,注意计算方法是一样的,但只对一层进行计算
|
||||
h_dst = h[:block.number_of_dst_nodes()]
|
||||
h = F.relu(layer(block, (h, h_dst)))
|
||||
|
||||
# 将输出复制回CPU
|
||||
y[output_nodes] = h.cpu()
|
||||
|
||||
x = y
|
||||
|
||||
return y
|
||||
|
||||
注意,如果以模型选择为目的在验证集上计算评价指标,则通常不需要进行计算精确的离线推断。
|
||||
原因是这需要为每一层上的每个节点计算表示,会非常消耗资源,尤其是在包含大量未标记数据的半监督系统中。
|
||||
邻居采样在这个时候可以更好地发挥作用。
|
||||
|
||||
对于离线推断的示例,用户可以参照
|
||||
`GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_sampling.py>`__
|
||||
和
|
||||
`RGCN <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/entity_classify_mb.py>`__。
|
||||
@@ -0,0 +1,272 @@
|
||||
.. _guide_cn-minibatch-link-classification-sampler:
|
||||
|
||||
6.3 针对链接预测任务的邻居采样训练方法
|
||||
--------------------------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-link-classification-sampler>`
|
||||
|
||||
结合负采样来定义邻居采样器和数据加载器
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
用户仍然可以使用与节点/边分类中相同的邻居采样器。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
|
||||
DGL中的
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader`
|
||||
还支持生成用于链接预测的负样本。为此,用户需要定义负采样函数。例如,
|
||||
:class:`~dgl.dataloading.negative_sampler.Uniform`
|
||||
函数是基于均匀分布的采样函数,它对于每个边的源节点,采样 ``k`` 个负样本的目标节点。
|
||||
|
||||
以下数据加载器将为每个边的源节点均匀采样5个负样本的目标节点。
|
||||
|
||||
.. code:: python
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_seeds, sampler,
|
||||
negative_sampler=dgl.dataloading.negative_sampler.Uniform(5),
|
||||
batch_size=args.batch_size,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
pin_memory=True,
|
||||
num_workers=args.num_workers)
|
||||
|
||||
关于内置的负采样方法,用户可以参考 :ref:`api-dataloading-negative-sampling`。
|
||||
|
||||
用户还可以自定义负采样函数,它应当以原图 ``g`` 和小批量的边ID数组 ``eid`` 作为入参,
|
||||
并返回源节点ID数组和目标节点ID数组。
|
||||
|
||||
下面给出了一个自定义的负采样方法的示例,该采样方法根据与节点的度的幂成正比的概率分布对负样本目标节点进行采样。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class NegativeSampler(object):
|
||||
def __init__(self, g, k):
|
||||
# 缓存概率分布
|
||||
self.weights = g.in_degrees().float() ** 0.75
|
||||
self.k = k
|
||||
|
||||
def __call__(self, g, eids):
|
||||
src, _ = g.find_edges(eids)
|
||||
src = src.repeat_interleave(self.k)
|
||||
dst = self.weights.multinomial(len(src), replacement=True)
|
||||
return src, dst
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_seeds, sampler,
|
||||
negative_sampler=NegativeSampler(g, 5),
|
||||
batch_size=args.batch_size,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
pin_memory=True,
|
||||
num_workers=args.num_workers)
|
||||
|
||||
调整模型以进行小批次训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
如 :ref:`guide_cn-training-link-prediction` 中所介绍的,
|
||||
用户可以通过比较边(正样本)与不存在的边(负样本)的得分来训练链路模型。用户可以重用在边分类/回归中的节点表示模型,
|
||||
来计算边的分数。
|
||||
|
||||
.. 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
|
||||
|
||||
对于得分的预测,只需要预测每个边的标量分数而不是类别的概率分布,
|
||||
因此本示例说明了如何使用边的两个端点的向量的点积来计算分数。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class ScorePredictor(nn.Module):
|
||||
def forward(self, edge_subgraph, x):
|
||||
with edge_subgraph.local_scope():
|
||||
edge_subgraph.ndata['x'] = x
|
||||
edge_subgraph.apply_edges(dgl.function.u_dot_v('x', 'x', 'score'))
|
||||
return edge_subgraph.edata['score']
|
||||
|
||||
使用负采样方法后,DGL的数据加载器将为每个小批次生成三项:
|
||||
|
||||
- 一个正样本图,其中包含采样得到的小批次内所有的边。
|
||||
- 一个负样本图,其中包含由负采样方法生成的所有不存在的边。
|
||||
- 邻居采样方法生成的块的列表。
|
||||
|
||||
因此,可以如下定义链接预测模型,该模型的输入包括上述三项以及输入的特征。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Model(nn.Module):
|
||||
def __init__(self, in_features, hidden_features, out_features):
|
||||
super().__init__()
|
||||
self.gcn = StochasticTwoLayerGCN(
|
||||
in_features, hidden_features, out_features)
|
||||
|
||||
def forward(self, positive_graph, negative_graph, blocks, x):
|
||||
x = self.gcn(blocks, x)
|
||||
pos_score = self.predictor(positive_graph, x)
|
||||
neg_score = self.predictor(negative_graph, x)
|
||||
return pos_score, neg_score
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
训练循环通过数据加载器去遍历数据,将得到的图和输入特征传入上述模型。
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = Model(in_features, hidden_features, out_features)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, positive_graph, negative_graph, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
positive_graph = positive_graph.to(torch.device('cuda'))
|
||||
negative_graph = negative_graph.to(torch.device('cuda'))
|
||||
input_features = blocks[0].srcdata['features']
|
||||
pos_score, neg_score = model(positive_graph, negative_graph, blocks, input_features)
|
||||
loss = compute_loss(pos_score, neg_score)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
DGL提供了在同构图上做链路预测的一个示例:
|
||||
`无监督学习GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_sampling_unsupervised.py>`__。
|
||||
|
||||
异构图上的随机批次训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
计算异构图上的节点表示的模型也可以用于计算边分类/回归中的边两端节点的表示。
|
||||
|
||||
.. 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
|
||||
|
||||
对于得分的预测,同构图和异构图之间唯一的实现差异是后者需要用
|
||||
:meth:`dgl.DGLGraph.apply_edges`
|
||||
来遍历所有的边类型。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class ScorePredictor(nn.Module):
|
||||
def forward(self, edge_subgraph, x):
|
||||
with edge_subgraph.local_scope():
|
||||
edge_subgraph.ndata['x'] = x
|
||||
for etype in edge_subgraph.canonical_etypes:
|
||||
edge_subgraph.apply_edges(
|
||||
dgl.function.u_dot_v('x', 'x', 'score'), etype=etype)
|
||||
return edge_subgraph.edata['score']
|
||||
|
||||
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()
|
||||
|
||||
def forward(self, positive_graph, negative_graph, blocks, x):
|
||||
x = self.rgcn(blocks, x)
|
||||
pos_score = self.pred(positive_graph, x)
|
||||
neg_score = self.pred(negative_graph, x)
|
||||
return pos_score, neg_score
|
||||
|
||||
数据加载器的定义也与边分类/回归里的定义非常相似。唯一的区别是用户需要提供负采样方法,
|
||||
并且提供边类型和边ID张量的字典,而不是节点类型和节点ID张量的字典。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
negative_sampler=dgl.dataloading.negative_sampler.Uniform(5),
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
如果用户想自定义负采样函数,那么该函数应以初始图以及由边类型和边ID张量构成的字典作为输入。
|
||||
它返回以边类型为键、源节点-目标节点数组对为值的字典。示例如下所示:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class NegativeSampler(object):
|
||||
def __init__(self, g, k):
|
||||
# 缓存概率分布
|
||||
self.weights = {
|
||||
etype: g.in_degrees(etype=etype).float() ** 0.75
|
||||
for _, etype, _ in g.canonical_etypes
|
||||
}
|
||||
self.k = k
|
||||
|
||||
def __call__(self, g, eids_dict):
|
||||
result_dict = {}
|
||||
for etype, eids in eids_dict.items():
|
||||
src, _ = g.find_edges(eids, etype=etype)
|
||||
src = src.repeat_interleave(self.k)
|
||||
dst = self.weights[etype].multinomial(len(src), replacement=True)
|
||||
result_dict[etype] = (src, dst)
|
||||
return result_dict
|
||||
|
||||
随后,需要向数据载入器提供边类型和对应边ID的字典,以及负采样器。示例如下所示:
|
||||
|
||||
.. code:: python
|
||||
|
||||
train_eid_dict = {
|
||||
g.edges(etype=etype, form='eid')
|
||||
for etype in g.etypes}
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
negative_sampler=NegativeSampler(g, 5),
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
异构图上的随机批次模型训练与同构图中的训练几乎相同,不同之处在于,
|
||||
``compute_loss`` 是以边类型字典和预测结果字典作为输入。
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = Model(in_features, hidden_features, out_features, num_classes, etypes)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, positive_graph, negative_graph, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
positive_graph = positive_graph.to(torch.device('cuda'))
|
||||
negative_graph = negative_graph.to(torch.device('cuda'))
|
||||
input_features = blocks[0].srcdata['features']
|
||||
pos_score, neg_score = model(positive_graph, negative_graph, blocks, input_features)
|
||||
loss = compute_loss(pos_score, neg_score)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
.. _guide_cn-minibatch-custom-gnn-module:
|
||||
|
||||
6.5 为小批次训练实现定制化的GNN模块
|
||||
-------------------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-custom-gnn-module>`
|
||||
|
||||
如果用户熟悉如何定制用于更新整个同构图或异构图的GNN模块(参见
|
||||
:ref:`guide_cn-nn`),那么在块上计算的代码也是类似的,区别只在于节点被划分为输入节点和输出节点。
|
||||
|
||||
以下面的自定义图卷积模块代码为例。注意,该代码并不一定是最高效的实现,
|
||||
此处只是将其作为自定义GNN模块的一个示例。
|
||||
|
||||
.. 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))
|
||||
|
||||
如果用户已有一个用于整个图的自定义消息传递模块,并且想将其用于块,则只需要按照如下的方法重写forward函数。
|
||||
注意,以下代码在注释里保留了整图实现的语句,用户可以将用于块的语句和原先用于整图的语句进行比较。
|
||||
|
||||
.. 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现在是输入和输出节点的特征张量对,而不是一个单独的特征张量
|
||||
|
||||
# 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))
|
||||
|
||||
通常,需要对用于整图的GNN模块进行如下调整以将其用于块作为输入的情况:
|
||||
|
||||
- 切片取输入特征的前几行,得到输出节点的特征。切片行数可以通过
|
||||
:meth:`block.number_of_dst_nodes <dgl.DGLGraph.number_of_dst_nodes>` 获得。
|
||||
- 如果原图只包含一种节点类型,对输入节点特征,将 :attr:`g.ndata <dgl.DGLGraph.ndata>` 替换为
|
||||
:attr:`block.srcdata <dgl.DGLGraph.srcdata>`;对于输出节点特征,将
|
||||
:attr:`g.ndata <dgl.DGLGraph.ndata>` 替换为
|
||||
:attr:`block.dstdata <dgl.DGLGraph.dstdata>`。
|
||||
- 如果原图包含多种节点类型,对于输入节点特征,将
|
||||
:attr:`g.nodes <dgl.DGLGraph.nodes>` 替换为
|
||||
:attr:`block.srcnodes <dgl.DGLGraph.srcnodes>`;对于输出节点特征,将
|
||||
:attr:`g.nodes <dgl.DGLGraph.nodes>` 替换为
|
||||
:attr:`block.dstnodes <dgl.DGLGraph.dstnodes>`。
|
||||
- 对于输入节点数量,将 :meth:`g.num_nodes <dgl.DGLGraph.num_nodes>` 替换为
|
||||
:meth:`block.number_of_src_nodes <dgl.DGLGraph.number_of_src_nodes>` ;
|
||||
对于输出节点数量,将 :meth:`g.num_nodes <dgl.DGLGraph.num_nodes>` 替换为
|
||||
:meth:`block.number_of_dst_nodes <dgl.DGLGraph.number_of_dst_nodes>` 。
|
||||
|
||||
异构图上的模型定制
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
为异构图修改GNN模块的方法是类似的。例如,以下面用于全图的GNN模块为例:
|
||||
|
||||
.. 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}
|
||||
|
||||
对于 ``CustomHeteroGraphConv``,原则是将 ``g.nodes`` 替换为 ``g.srcnodes`` 或
|
||||
``g.dstnodes`` (根据需要输入还是输出节点的特征来选择)。
|
||||
|
||||
.. 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}
|
||||
|
||||
实现能够处理同构图、二分图和块的模块
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL中所有的消息传递模块(参见 :ref:`apinn`)都能够处理同构图、
|
||||
单向二分图(包含两种节点类型和一种边类型)和包含一种边类型的块。
|
||||
本质上,内置的DGL神经网络模块的输入图及特征必须满足下列情况之一:
|
||||
|
||||
- 如果输入特征是一个张量对,则输入图必须是一个单向二分图
|
||||
- 如果输入特征是一个单独的张量且输入图是一个块,则DGL会自动将输入节点特征前一部分设为输出节点的特征。
|
||||
- 如果输入特征是一个单独的张量且输入图不是块,则输入图必须是同构图。
|
||||
|
||||
例如,下面的代码是 :class:`dgl.nn.pytorch.SAGEConv` 的简化版(DGL同样支持它在MXNet和TensorFlow后端里的实现)。
|
||||
代码里移除了归一化,且只考虑平均聚合函数的情况。
|
||||
|
||||
.. 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_cn-nn` 提供了对 :class:`dgl.nn.pytorch.SAGEConv` 代码的详细解读,
|
||||
其适用于单向二分图、同构图和块。
|
||||
@@ -0,0 +1,211 @@
|
||||
.. _guide_cn-minibatch-node-classification-sampler:
|
||||
|
||||
6.1 针对节点分类任务的邻居采样训练方法
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-node-classification-sampler>`
|
||||
|
||||
为了随机(批次)训练模型,需要进行以下操作:
|
||||
|
||||
- 定义邻居采样器。
|
||||
- 调整模型以进行小批次训练。
|
||||
- 修改模型训练循环部分。
|
||||
|
||||
以下小节将逐一介绍这些步骤。
|
||||
|
||||
定义邻居采样器和数据加载器
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL提供了几个邻居采样类,这些类会生成需计算的节点在每一层计算时所需的依赖图。
|
||||
|
||||
最简单的邻居采样器是
|
||||
:class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler`,它可获取节点的所有邻居。
|
||||
|
||||
要使用DGL提供的采样器,还需要将其与
|
||||
:class:`~dgl.dataloading.pytorch.NodeDataLoader`
|
||||
结合使用,后者可以以小批次的形式对一个节点的集合进行迭代。
|
||||
|
||||
例如,以下代码创建了一个PyTorch的 DataLoader,它分批迭代训练节点ID数组 ``train_nids``,
|
||||
并将生成的子图列表放到GPU上。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
import dgl.nn as dglnn
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
dataloader = dgl.dataloading.NodeDataLoader(
|
||||
g, train_nids, sampler,
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
对DataLoader进行迭代,将会创建一个特定图的列表,这些图表示每层的计算依赖。在DGL中称之为 *块*。
|
||||
|
||||
.. code:: python
|
||||
|
||||
input_nodes, output_nodes, blocks = next(iter(dataloader))
|
||||
print(blocks)
|
||||
|
||||
上面的dataloader一次迭代会生成三个输出。 ``input_nodes`` 代表计算 ``output_nodes`` 的表示所需的节点。
|
||||
``块`` 包含了每个GNN层要计算哪些节点表示作为输出,要将哪些节点表示作为输入,以及来自输入节点的表示如何传播到输出节点。
|
||||
|
||||
完整的内置采样方法清单,用户可以参考
|
||||
:ref:`neighborhood sampler API reference <api-dataloading-neighbor-sampling>`。
|
||||
|
||||
如果用户希望编写自己的邻居采样器,或者想要关于块的更深入的介绍,读者可以参考
|
||||
:ref:`guide_cn-minibatch-customizing-neighborhood-sampler`。
|
||||
|
||||
.. _guide_cn-minibatch-node-classification-model:
|
||||
|
||||
调整模型以进行小批次训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
如果用户的消息传递模块全使用的是DGL内置模块,则模型在进行小批次训练时只需做很小的调整。
|
||||
以多层GCN为例。如果用户模型在全图上是按以下方式实现的:
|
||||
|
||||
.. 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
|
||||
|
||||
然后,用户所需要做的就是用上面生成的块( ``block`` )来替换图( ``g`` )。
|
||||
|
||||
.. 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
|
||||
|
||||
上面的DGL ``GraphConv`` 模块接受的一个参数是数据加载器生成的 ``块`` 中的一个元素。
|
||||
|
||||
用户可以查阅 :ref:`NN模块的API参考 <apinn>` 来查看DGL的内置模型模块是否支持接受 ``块`` 作为参数。
|
||||
|
||||
如果希望使用自定义的消息传递模块,用户可以参考
|
||||
:ref:`guide_cn-minibatch-custom-gnn-module`。
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
这里的模型的训练循环仅包含使用定制的批处理迭代器遍历数据集的内容。在每个生成块列表的迭代中:
|
||||
|
||||
|
||||
1. 将与输入节点相对应的节点特征加载到GPU上。节点特征可以存储在内存或外部存储中。
|
||||
请注意,用户只需要加载输入节点的特征,而不是像整图训练那样加载所有节点的特征。
|
||||
|
||||
如果特征存储在 ``g.ndata`` 中,则可以通过 ``blocks[0].srcdata`` 来加载第一个块的输入节点的特征,
|
||||
这些节点是计算节点最终表示所需的所有必需的节点。
|
||||
|
||||
2. 将块列表和输入节点特征传入多层GNN并获取输出。
|
||||
|
||||
3. 将与输出节点相对应的节点标签加载到GPU上。同样,节点标签可以存储在内存或外部存储器中。
|
||||
再次提醒下,用户只需要加载输出节点的标签,而不是像整图训练那样加载所有节点的标签。
|
||||
|
||||
如果特征存储在 ``g.ndata`` 中,则可以通过访问 ``blocks[-1].dstdata`` 中的特征来加载标签,
|
||||
它是最后一个块的输出节点的特征,这些节点与用户希望计算最终表示的节点相同。
|
||||
|
||||
4. 计算损失并反向传播。
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = StochasticTwoLayerGCN(in_features, hidden_features, out_features)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
input_features = blocks[0].srcdata['features']
|
||||
output_labels = blocks[-1].dstdata['label']
|
||||
output_predictions = model(blocks, input_features)
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
DGL提供了一个端到端的随机批次训练示例
|
||||
`GraphSAGE的实现 <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/node_classification.py>`__。
|
||||
|
||||
|
||||
异构图上模型的训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在异构图上训练图神经网络进行节点分类的方法也是类似的。
|
||||
|
||||
例如,在
|
||||
:ref:`guide_cn-training-rgcn-node-classification`
|
||||
中介绍了如何在整图上训练一个2层的RGCN模型。
|
||||
RGCN小批次训练的代码与它非常相似(为简单起见,这里删除了自环、非线性和基分解):
|
||||
|
||||
.. 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
|
||||
|
||||
DGL提供的一些采样方法也支持异构图。例如,用户仍然可以使用
|
||||
:class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler` 类和
|
||||
:class:`~dgl.dataloading.pytorch.NodeDataLoader` 类进行随机批次训练。
|
||||
对于全邻居采样,唯一的区别是用户需要为训练集指定节点类型和节点ID的字典。
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
dataloader = dgl.dataloading.NodeDataLoader(
|
||||
g, train_nid_dict, sampler,
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
模型的训练与同构图几乎相同。不同之处在于, ``compute_loss`` 的实现会包含两个字典:节点类型和预测结果。
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = StochasticTwoLayerRGCN(in_features, hidden_features, out_features, etypes)
|
||||
model = model.cuda()
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
blocks = [b.to(torch.device('cuda')) for b in blocks]
|
||||
input_features = blocks[0].srcdata # returns a dict
|
||||
output_labels = blocks[-1].dstdata # returns a dict
|
||||
output_predictions = model(blocks, input_features)
|
||||
loss = compute_loss(output_labels, output_predictions)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
DGL提供了端到端随机批次训练的
|
||||
`RGCN的实现 <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/entity_classify_mb.py>`__。
|
||||
@@ -0,0 +1,56 @@
|
||||
.. _guide_cn-minibatch:
|
||||
|
||||
第6章:在大图上的随机(批次)训练
|
||||
=======================================================
|
||||
|
||||
:ref:`(English Version) <guide-minibatch>`
|
||||
|
||||
如果用户有包含数百万甚至数十亿个节点或边的大图,通常无法进行
|
||||
:ref:`guide_cn-training`
|
||||
中所述的全图训练。考虑在一个有 :math:`N` 个节点的图上运行的、隐层大小为 :math:`H` 的 :math:`L` 层图卷积网络,
|
||||
存储隐层表示需要 :math:`O(NLH)` 的内存空间,当 :math:`N` 较大时,这很容易超过一块GPU的显存限制。
|
||||
|
||||
本章介绍了一种在大图上进行随机小批次训练的方法,可以让用户不用一次性把所有节点特征拷贝到GPU上。
|
||||
|
||||
邻居采样方法概述
|
||||
--------------------------------------------
|
||||
|
||||
邻居节点采样的工作流程通常如下:每次梯度下降,选择一个小批次的图节点,
|
||||
其最终表示将在神经网络的第 :math:`L` 层进行计算,然后在网络的第 :math:`L-1` 层选择该批次节点的全部或部分邻居节点。
|
||||
重复这个过程,直到到达输入层。这个迭代过程会构建计算的依赖关系图,从输出开始,一直到输入,如下图所示:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_0_0.png
|
||||
:alt: Imgur
|
||||
|
||||
该方法能节省在大图上训练图神经网络的开销和计算资源。
|
||||
|
||||
DGL实现了一些邻居节点采样的方法和使用邻居节点采样训练图神经网络的管道,同时也支持让用户自定义采样策略。
|
||||
|
||||
本章路线图
|
||||
-----------
|
||||
|
||||
本章的前半部分介绍了不同场景下如何进行随机训练的方法。
|
||||
|
||||
* :ref:`guide_cn-minibatch-node-classification-sampler`
|
||||
* :ref:`guide_cn-minibatch-edge-classification-sampler`
|
||||
* :ref:`guide_cn-minibatch-link-classification-sampler`
|
||||
|
||||
本章余下的小节介绍了更多的高级主题,面向那些想要开发新的采样算法、
|
||||
想要实现与小批次训练兼容的图神经网络模块、以及想要了解如何在小批次数据上进行评估和推理模型的用户。
|
||||
|
||||
* :ref:`guide_cn-minibatch-customizing-neighborhood-sampler`
|
||||
* :ref:`guide_cn-minibatch-custom-gnn-module`
|
||||
* :ref:`guide_cn-minibatch-inference`
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
minibatch-node
|
||||
minibatch-edge
|
||||
minibatch-link
|
||||
minibatch-custom-sampler
|
||||
minibatch-nn
|
||||
minibatch-inference
|
||||
@@ -0,0 +1,73 @@
|
||||
.. _guide_cn-nn-construction:
|
||||
|
||||
3.1 DGL NN模块的构造函数
|
||||
-----------------------------
|
||||
|
||||
:ref:`(English Version) <guide-nn-construction>`
|
||||
|
||||
构造函数完成以下几个任务:
|
||||
|
||||
1. 设置选项。
|
||||
2. 注册可学习的参数或者子模块。
|
||||
3. 初始化参数。
|
||||
|
||||
.. 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
|
||||
|
||||
在构造函数中,用户首先需要设置数据的维度。对于一般的PyTorch模块,维度通常包括输入的维度、输出的维度和隐层的维度。
|
||||
对于图神经网络,输入维度可被分为源节点特征维度和目标节点特征维度。
|
||||
|
||||
除了数据维度,图神经网络的一个典型选项是聚合类型(``self._aggre_type``)。对于特定目标节点,聚合类型决定了如何聚合不同边上的信息。
|
||||
常用的聚合类型包括 ``mean``、 ``sum``、 ``max`` 和 ``min``。一些模块可能会使用更加复杂的聚合函数,比如 ``lstm``。
|
||||
|
||||
上面代码里的 ``norm`` 是用于特征归一化的可调用函数。在SAGEConv论文里,归一化可以是L2归一化:
|
||||
:math:`h_v = h_v / \lVert h_v \rVert_2`。
|
||||
|
||||
.. code::
|
||||
|
||||
# 聚合类型: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()
|
||||
|
||||
注册参数和子模块。在SAGEConv中,子模块根据聚合类型而有所不同。这些模块是纯PyTorch NN模块,例如 ``nn.Linear``、 ``nn.LSTM`` 等。
|
||||
构造函数的最后调用了 ``reset_parameters()`` 进行权重初始化。
|
||||
|
||||
.. code::
|
||||
|
||||
def reset_parameters(self):
|
||||
"""重新初始化可学习的参数"""
|
||||
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,138 @@
|
||||
.. _guide_cn-nn-forward:
|
||||
|
||||
3.2 编写DGL NN模块的forward函数
|
||||
---------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-nn-forward>`
|
||||
|
||||
在NN模块中, ``forward()`` 函数执行了实际的消息传递和计算。与通常以张量为参数的PyTorch NN模块相比,
|
||||
DGL NN模块额外增加了1个参数 :class:`dgl.DGLGraph`。``forward()`` 函数的内容一般可以分为3项操作:
|
||||
|
||||
- 检测输入图对象是否符合规范。
|
||||
|
||||
- 消息传递和聚合。
|
||||
|
||||
- 聚合后,更新特征作为输出。
|
||||
|
||||
下文展示了SAGEConv示例中的 ``forward()`` 函数。
|
||||
|
||||
输入图对象的规范检测
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
def forward(self, graph, feat):
|
||||
with graph.local_scope():
|
||||
# 指定图类型,然后根据图类型扩展输入特征
|
||||
feat_src, feat_dst = expand_as_pair(feat, graph)
|
||||
|
||||
``forward()`` 函数需要处理输入的许多极端情况,这些情况可能导致计算和消息传递中的值无效。
|
||||
比如在 :class:`~dgl.nn.pytorch.conv.GraphConv` 等conv模块中,DGL会检查输入图中是否有入度为0的节点。
|
||||
当1个节点入度为0时, ``mailbox`` 将为空,并且聚合函数的输出值全为0,
|
||||
这可能会导致模型性能不佳。但是,在 :class:`~dgl.nn.pytorch.conv.SAGEConv` 模块中,被聚合的特征将会与节点的初始特征拼接起来,
|
||||
``forward()`` 函数的输出不会全为0。在这种情况下,无需进行此类检验。
|
||||
|
||||
DGL NN模块可在不同类型的图输入中重复使用,包括:同构图、异构图(:ref:`guide_cn-graph-heterogeneous`)和子图块(:ref:`guide_cn-minibatch`)。
|
||||
|
||||
SAGEConv的数学公式如下:
|
||||
|
||||
.. 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})
|
||||
|
||||
源节点特征 ``feat_src`` 和目标节点特征 ``feat_dst`` 需要根据图类型被指定。
|
||||
用于指定图类型并将 ``feat`` 扩展为 ``feat_src`` 和 ``feat_dst`` 的函数是 :meth:`~dgl.utils.expand_as_pair`。
|
||||
该函数的细节如下所示。
|
||||
|
||||
.. code::
|
||||
|
||||
def expand_as_pair(input_, g=None):
|
||||
if isinstance(input_, tuple):
|
||||
# 二分图的情况
|
||||
return input_
|
||||
elif g is not None and g.is_block:
|
||||
# 子图块的情况
|
||||
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:
|
||||
# 同构图的情况
|
||||
return input_, input_
|
||||
|
||||
对于同构图上的全图训练,源节点和目标节点相同,它们都是图中的所有节点。
|
||||
|
||||
在异构图的情况下,图可以分为几个二分图,每种关系对应一个。关系表示为 ``(src_type, edge_type, dst_dtype)``。
|
||||
当输入特征 ``feat`` 是1个元组时,图将会被视为二分图。元组中的第1个元素为源节点特征,第2个元素为目标节点特征。
|
||||
|
||||
在小批次训练中,计算应用于给定的一堆目标节点所采样的子图。子图在DGL中称为区块(``block``)。
|
||||
在区块创建的阶段,``dst nodes`` 位于节点列表的最前面。通过索引 ``[0:g.number_of_dst_nodes()]`` 可以找到 ``feat_dst``。
|
||||
|
||||
确定 ``feat_src`` 和 ``feat_dst`` 之后,以上3种图类型的计算方法是相同的。
|
||||
|
||||
消息传递和聚合
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. 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'))
|
||||
# 除以入度
|
||||
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聚合不需要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)
|
||||
|
||||
上面的代码执行了消息传递和聚合的计算。这部分代码会因模块而异。请注意,代码中的所有消息传递均使用 :meth:`~dgl.DGLGraph.update_all` API和
|
||||
DGL内置的消息/聚合函数来实现,以充分利用 :ref:`guide_cn-message-passing-efficient` 里所介绍的性能优化。
|
||||
|
||||
聚合后,更新特征作为输出
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
# 激活函数
|
||||
if self.activation is not None:
|
||||
rst = self.activation(rst)
|
||||
# 归一化
|
||||
if self.norm is not None:
|
||||
rst = self.norm(rst)
|
||||
return rst
|
||||
|
||||
``forward()`` 函数的最后一部分是在完成消息聚合后更新节点的特征。
|
||||
常见的更新操作是根据构造函数中设置的选项来应用激活函数和进行归一化。
|
||||
@@ -0,0 +1,91 @@
|
||||
.. _guide_cn-nn-heterograph:
|
||||
|
||||
3.3 异构图上的GraphConv模块
|
||||
--------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-nn-heterograph>`
|
||||
|
||||
DGL提供了 :class:`~dgl.nn.pytorch.HeteroGraphConv`,用于定义异构图上GNN模块。
|
||||
实现逻辑与消息传递级别的API :meth:`~dgl.DGLGraph.multi_update_all` 相同,它包括:
|
||||
|
||||
- 每个关系上的DGL NN模块。
|
||||
- 聚合来自不同关系上的结果。
|
||||
|
||||
其数学定义为:
|
||||
|
||||
.. 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))
|
||||
|
||||
其中 :math:`f_r` 是对应每个关系 :math:`r` 的NN模块,:math:`AGG` 是聚合函数。
|
||||
|
||||
HeteroGraphConv的实现逻辑
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. 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):
|
||||
# 获取聚合函数的内部函数
|
||||
self.agg_fn = get_aggregate_fn(aggregate)
|
||||
else:
|
||||
self.agg_fn = aggregate
|
||||
|
||||
异构图的卷积操作接受一个字典类型参数 ``mods``。这个字典的键为关系名,值为作用在该关系上NN模块对象。参数 ``aggregate``
|
||||
则指定了如何聚合来自不同关系的结果。
|
||||
|
||||
.. 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}
|
||||
|
||||
除了输入图和输入张量,``forward()`` 函数还使用2个额外的字典参数 ``mod_args`` 和 ``mod_kwargs``。
|
||||
这2个字典与 ``self.mods`` 具有相同的键,值则为对应NN模块的自定义参数。
|
||||
|
||||
``forward()`` 函数的输出结果也是一个字典类型的对象。其键为 ``nty``,其值为每个目标节点类型 ``nty`` 的输出张量的列表,
|
||||
表示来自不同关系的计算结果。``HeteroGraphConv`` 会对这个列表进一步聚合,并将结果返回给用户。
|
||||
|
||||
.. 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)
|
||||
|
||||
输入 ``g`` 可以是异构图或来自异构图的子图区块。和普通的NN模块一样,``forward()`` 函数需要分别处理不同的输入图类型。
|
||||
|
||||
上述代码中的for循环为处理异构图计算的主要逻辑。首先我们遍历图中所有的关系(通过调用 ``canonical_etypes``)。
|
||||
通过关系名,我们可以使用g[ ``stype, etype, dtype`` ]的语法将只包含该关系的子图( ``rel_graph`` )抽取出来。
|
||||
对于二分图,输入特征将被组织为元组 ``(src_inputs[stype], dst_inputs[dtype])``。
|
||||
接着调用用户预先注册在该关系上的NN模块,并将结果保存在outputs字典中。
|
||||
|
||||
.. code::
|
||||
|
||||
rsts = {}
|
||||
for nty, alist in outputs.items():
|
||||
if len(alist) != 0:
|
||||
rsts[nty] = self.agg_fn(alist, nty)
|
||||
|
||||
最后,``HeteroGraphConv`` 会调用用户注册的 ``self.agg_fn`` 函数聚合来自多个关系的结果。
|
||||
读者可以在API文档中找到 :class:~dgl.nn.pytorch.HeteroGraphConv 的示例。
|
||||
@@ -0,0 +1,35 @@
|
||||
.. _guide_cn-nn:
|
||||
|
||||
第3章:构建图神经网络(GNN)模块
|
||||
===================================
|
||||
|
||||
:ref:`(English Version) <guide-nn>`
|
||||
|
||||
DGL NN模块是用户构建GNN模型的基本模块。根据DGL所使用的后端深度神经网络框架,
|
||||
DGL NN模块的父类取决于后端所使用的深度神经网络框架。对于PyTorch后端,
|
||||
它应该继承 `PyTorch的NN模块 <https://pytorch.org/docs/1.2.0/_modules/torch/nn/modules/module.html>`__;对于MXNet后端,它应该继承
|
||||
`MXNet Gluon的NN块 <http://mxnet.incubator.apache.org/versions/1.6/api/python/docs/api/gluon/nn/index.html>`__;
|
||||
对于TensorFlow后端,它应该继承 `Tensorflow的Keras层 <https://www.tensorflow.org/api_docs/python/tf/keras/layers>`__。
|
||||
在DGL NN模块中,构造函数中的参数注册和前向传播函数中使用的张量操作与后端框架一样。这种方式使得DGL的代码可以无缝嵌入到后端框架的代码中。
|
||||
DGL和这些深度神经网络框架的主要差异是其独有的消息传递操作。
|
||||
|
||||
DGL已经集成了很多常用的 :ref:`apinn-pytorch-conv`、 :ref:`apinn-pytorch-dense-conv`、
|
||||
:ref:`apinn-pytorch-pooling` 和 :ref:`apinn-pytorch-util`。欢迎给DGL贡献更多的模块!
|
||||
|
||||
本章将使用PyTorch作为后端,用 :class:`~dgl.nn.pytorch.conv.SAGEConv` 作为例子来介绍如何构建用户自己的DGL NN模块。
|
||||
|
||||
本章路线图
|
||||
------------
|
||||
|
||||
* :ref:`guide_cn-nn-construction`
|
||||
* :ref:`guide_cn-nn-forward`
|
||||
* :ref:`guide_cn-nn-heterograph`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
nn-construction
|
||||
nn-forward
|
||||
nn-heterograph
|
||||
@@ -0,0 +1,282 @@
|
||||
.. _guide_cn-training-edge-classification:
|
||||
|
||||
5.2 边分类/回归
|
||||
---------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-edge-classification>`
|
||||
|
||||
有时用户希望预测图中边的属性值,这种情况下,用户需要构建一个边分类/回归的模型。
|
||||
|
||||
以下代码生成了一个随机图用于演示边分类/回归。
|
||||
|
||||
.. code:: python
|
||||
|
||||
src = np.random.randint(0, 100, 500)
|
||||
dst = np.random.randint(0, 100, 500)
|
||||
# 同时建立反向边
|
||||
edge_pred_graph = dgl.graph((np.concatenate([src, dst]), np.concatenate([dst, src])))
|
||||
# 建立点和边特征,以及边的标签
|
||||
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)
|
||||
# 进行训练、验证和测试集划分
|
||||
edge_pred_graph.edata['train_mask'] = torch.zeros(1000, dtype=torch.bool).bernoulli(0.6)
|
||||
|
||||
概述
|
||||
~~~~~~~~
|
||||
|
||||
上一节介绍了如何使用多层GNN进行节点分类。同样的方法也可以被用于计算任何节点的隐藏表示。
|
||||
并从边的两个端点的表示,通过计算得出对边属性的预测。
|
||||
|
||||
对一条边计算预测值最常见的情况是将预测表示为一个函数,函数的输入为两个端点的表示,
|
||||
输入还可以包括边自身的特征。
|
||||
|
||||
与节点分类在模型实现上的差别
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
如果用户使用上一节中的模型计算了节点的表示,那么用户只需要再编写一个用
|
||||
:meth:`~dgl.DGLGraph.apply_edges` 方法计算边预测的组件即可进行边分类/回归任务。
|
||||
|
||||
例如,对于边回归任务,如果用户想为每条边计算一个分数,可按下面的代码对每一条边计算它的两端节点隐藏表示的点积来作为分数。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.function as fn
|
||||
class DotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h):
|
||||
# h是从5.1节的GNN模型中计算出的节点表示
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
|
||||
return graph.edata['score']
|
||||
|
||||
用户也可以使用MLP(多层感知机)对每条边生成一个向量表示(例如,作为一个未经过归一化的类别的分布),
|
||||
并在下游任务中使用。
|
||||
|
||||
.. 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是从5.1节的GNN模型中计算出的节点表示
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(self.apply_edges)
|
||||
return graph.edata['score']
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
给定计算节点和边上表示的模型后,用户可以轻松地编写在所有边上进行预测的全图训练代码。
|
||||
|
||||
以下代码用了 :ref:`guide_cn-message-passing` 中定义的 ``SAGE`` 作为节点表示计算模型以及前一小节中定义的
|
||||
``DotPredictor`` 作为边预测模型。
|
||||
|
||||
.. 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)
|
||||
|
||||
在训练模型时可以使用布尔掩码区分训练、验证和测试数据集。该例子里省略了训练早停和模型保存部分的代码。
|
||||
|
||||
.. 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_cn-training-edge-classification-heterogeneous-graph:
|
||||
|
||||
异构图上的边预测模型的训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
例如想在某一特定类型的边上进行分类任务,用户只需要计算所有节点类型的节点表示,
|
||||
然后同样通过调用 :meth:`~dgl.DGLGraph.apply_edges` 方法计算预测值即可。
|
||||
唯一的区别是在调用 ``apply_edges`` 时需要指定边的类型。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroDotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h, etype):
|
||||
# h是从5.1节中对每种类型的边所计算的节点表示
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h #一次性为所有节点类型的 'h'赋值
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
同样地,用户也可以编写一个 ``HeteroMLPPredictor``。
|
||||
|
||||
.. 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, etype):
|
||||
# h是从5.1节中对异构图的每种类型的边所计算的节点表示
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h #一次性为所有节点类型的 'h'赋值
|
||||
graph.apply_edges(self.apply_edges, etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
在某种类型的边上为每一条边预测的端到端模型的定义如下所示:
|
||||
|
||||
.. 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)
|
||||
|
||||
使用模型时只需要简单地向模型提供一个包含节点类型和数据特征的字典。
|
||||
|
||||
.. 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}
|
||||
|
||||
|
||||
训练部分和同构图的训练基本一致。例如,如果用户想预测边类型为 ``click`` 的边的标签,只需要按下例编写代码。
|
||||
|
||||
.. 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())
|
||||
|
||||
|
||||
在异构图中预测已有边的类型
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
预测图中已经存在的边属于哪个类型是一个非常常见的任务类型。例如,根据
|
||||
:ref:`本章的异构图样例数据 <guide_cn-training-heterogeneous-graph-example>`,
|
||||
用户的任务是给定一条连接 ``user`` 节点和 ``item`` 节点的边,预测它的类型是 ``click`` 还是 ``dislike``。
|
||||
这个例子是评分预测的一个简化版本,在推荐场景中很常见。
|
||||
|
||||
边类型预测的第一步仍然是计算节点表示。可以通过类似
|
||||
:ref:`节点分类的RGCN模型 <guide_cn-training-rgcn-node-classification>`
|
||||
这一章中提到的图卷积网络获得。第二步是计算边上的预测值。
|
||||
在这里可以复用上述提到的 ``HeteroDotProductPredictor``。
|
||||
这里需要注意的是输入的图数据不能包含边的类型信息,
|
||||
因此需要将所要预测的边类型(如 ``click`` 和 ``dislike``)合并成一种边的图,
|
||||
并为每条边计算出每种边类型的可能得分。下面的例子使用一个拥有 ``user``
|
||||
和 ``item`` 两种节点类型和一种边类型的图。该边类型是通过合并所有从 ``user``
|
||||
到 ``item`` 的边类型(如 ``like`` 和 ``dislike``)得到。
|
||||
用户可以很方便地用关系切片的方式创建这个图。
|
||||
|
||||
.. code:: python
|
||||
|
||||
dec_graph = hetero_graph['user', :, 'item']
|
||||
|
||||
这个方法会返回一个异构图,它具有 ``user`` 和 ``item`` 两种节点类型,
|
||||
以及把它们之间的所有边的类型进行合并后的单一边类型。
|
||||
|
||||
由于上面这行代码将原来的边类型存成边特征 ``dgl.ETYPE``,用户可以将它作为标签使用。
|
||||
|
||||
.. code:: python
|
||||
|
||||
edge_label = dec_graph.edata[dgl.ETYPE]
|
||||
|
||||
将上述图作为边类型预测模块的输入,用户可以按如下方式编写预测模块:
|
||||
|
||||
.. 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是从5.1节中对异构图的每种类型的边所计算的节点表示
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h #一次性为所有节点类型的 'h'赋值
|
||||
graph.apply_edges(self.apply_edges)
|
||||
return graph.edata['score']
|
||||
|
||||
结合了节点表示模块和边类型预测模块的模型如下所示:
|
||||
|
||||
.. 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)
|
||||
|
||||
训练部分如下所示:
|
||||
|
||||
.. 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())
|
||||
|
||||
读者可以进一步参考
|
||||
`Graph Convolutional Matrix
|
||||
Completion <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__
|
||||
这一示例来了解如何预测异构图中的边类型。
|
||||
`模型实现文件中 <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__
|
||||
的节点表示模块称作 ``GCMCLayer``。边类型预测模块称作 ``BiDecoder``。
|
||||
虽然这两个模块都比上述的示例代码要复杂,但其基本思想和本章描述的流程是一致的。
|
||||
@@ -0,0 +1,74 @@
|
||||
.. _guide_cn-training-eweight:
|
||||
|
||||
5.5 使用边权重
|
||||
----------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-eweight>`
|
||||
|
||||
在一个加权图里,每条边都有一个有意义的标量权重。例如,边权重可以是连接强度或者信心指数。
|
||||
人们自然会想要在模型开发中使用它们。
|
||||
|
||||
使用边权重的消息传递
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
大部分图神经网络在前馈计算中仅通过消息传递引入图结构信息。一个消息传递运算可以视为一个函数。
|
||||
这个函数的输入变量是一个邻接矩阵和其他输入特征。对于一个不带权重的图,邻接矩阵里的元素不是零就是一。
|
||||
值为一的元素表示一条边。对于一个加权图,非零的元素可以取任意标量值。这等价于把每条消息和对应的边权重相乘,
|
||||
即`图注意力网络 <https://arxiv.org/pdf/1710.10903.pdf>`__中的做法。
|
||||
|
||||
在DGL里可以通过以下步骤实现这一需求:
|
||||
|
||||
- 把边权重保存为一个边特征
|
||||
- 在消息函数里,用保存的边特征与对应边的原始消息相乘
|
||||
|
||||
考虑以下基于DGL的消息传递示例:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
# 假定graph.ndata['ft']存储了输入节点特征
|
||||
graph.update_all(fn.copy_u('ft', 'm'), fn.sum('m', 'ft'))
|
||||
|
||||
可以将其按以下方式修改以支持边权重:
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
# 将边权重保存为一个边特征。边权重是一个形状为(E, *)的张量。
|
||||
# E是边的数量
|
||||
graph.edata['w'] = eweight
|
||||
|
||||
# 假定graph.ndata['ft']存储了输入节点特征
|
||||
graph.update_all(fn.u_mul_e('ft', 'w', 'm'), fn.sum('m', 'ft'))
|
||||
|
||||
在NN模块中使用边权重
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
用户可以通过修改NN模块中所有的消息传递操作来给NN模块增加边权重支持。以下代码块提供了一个例子。
|
||||
|
||||
.. 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内置的NN模块如果在forward函数中支持一个可选的:attr:`edge_weight`变量,那么它们已经支持了边权重。
|
||||
|
||||
用户可能会需要标准化原始边权重。DGL提供了一个满足这个功能的函数
|
||||
:func:`~dgl.nn.pytorch.conv.EdgeWeightNorm`。
|
||||
@@ -0,0 +1,245 @@
|
||||
.. _guide_cn-training-graph-classification:
|
||||
|
||||
5.4 整图分类
|
||||
----------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-graph-classification>`
|
||||
|
||||
许多场景中的图数据是由多个图组成,而不是单个的大图数据。例如不同类型的人群社区。
|
||||
通过用图刻画同一社区里人与人间的友谊,可以得到多张用于分类的图。
|
||||
在这个场景里,整图分类模型可以识别社区的类型,即根据结构和整体信息对图进行分类。
|
||||
|
||||
概述
|
||||
~~~~~~~~
|
||||
|
||||
整图分类与节点分类或链接预测的主要区别是:预测结果刻画了整个输入图的属性。
|
||||
与之前的任务类似,用户还是在节点或边上进行消息传递。但不同的是,整图分类任务还需要得到整个图的表示。
|
||||
|
||||
整图分类的处理流程如下图所示:
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/batch/graph_classifier.png
|
||||
:alt: Graph Classification Process
|
||||
|
||||
整图分类流程
|
||||
|
||||
从左至右,一般流程是:
|
||||
|
||||
- 准备一个批次的图;
|
||||
- 在这个批次的图上进行消息传递以更新节点或边的特征;
|
||||
- 将一张图里的节点或边特征聚合成整张图的图表示;
|
||||
- 根据任务设计分类层。
|
||||
|
||||
批次的图
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
整图分类任务通常需要在很多图上进行训练。如果用户在训练模型时一次仅使用一张图,训练效率会很低。
|
||||
借用深度学习实践中常用的小批次训练方法,用户可将多张图组成一个批次,在整个图批次上进行一次训练迭代。
|
||||
|
||||
使用DGL,用户可将一系列的图建立成一个图批次。一个图批次可以被看作是一张大图,图中的每个连通子图对应一张原始小图。
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/batch/batch.png
|
||||
:alt: Batched Graph
|
||||
|
||||
批次化的图
|
||||
|
||||
需要注意,DGL里对图进行变换的函数会去掉图上的批次信息。用户可以通过 :func:`dgl.DGLGraph.set_batch_num_nodes`
|
||||
和 :func:`dgl.DGLGraph.set_batch_num_edges` 两个函数在变换后的图上重新加入批次信息。
|
||||
|
||||
图读出
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
数据集中的每一张图都有它独特的结构和节点与边的特征。为了完成单个图的预测,通常会聚合并汇总单个图尽可能多的信息。
|
||||
这类操作叫做“读出”。常见的聚合方法包括:对所有节点或边特征求和、取平均值、逐元素求最大值或最小值。
|
||||
|
||||
给定一张图 :math:`g`,对它所有节点特征取平均值的聚合读出公式如下:
|
||||
|
||||
.. math:: h_g = \frac{1}{|\mathcal{V}|}\sum_{v\in \mathcal{V}}h_v
|
||||
|
||||
其中,:math:`h_g` 是图 :math:`g` 的表征, :math:`\mathcal{V}` 是图 :math:`g` 中节点的集合,
|
||||
:math:`h_v` 是节点 :math:`v` 的特征。
|
||||
|
||||
DGL内置了常见的图读出函数,例如 :func:`dgl.readout_nodes` 就实现了上述的平均值读出计算。
|
||||
|
||||
在得到 :math:`h_g` 后,用户可将其传给一个多层感知机(MLP)来获得分类输出。
|
||||
|
||||
编写神经网络模型
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
模型的输入是带节点和边特征的批次化图。需要注意的是批次化图中的节点和边属性没有批次大小对应的维度。
|
||||
模型中应特别注意以下几点。
|
||||
|
||||
批次化图上的计算
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
首先,一个批次中不同的图是完全分开的,即任意两个图之间没有边连接。
|
||||
根据这个良好的性质,所有消息传递函数(的计算)仍然具有相同的结果。
|
||||
|
||||
其次,读出函数会分别作用在图批次中的每张图上。假设批次大小为 :math:`B`,要聚合的特征大小为 :math:`D`,
|
||||
则图读出的张量形状为 :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]
|
||||
|
||||
最后,批次化图中的每个节点或边特征张量均通过将所有图上的相应特征拼接得到。
|
||||
|
||||
.. code:: python
|
||||
|
||||
bg.ndata['h']
|
||||
# tensor([1., 2., 1., 2., 3.])
|
||||
|
||||
模型定义
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
了解了上述计算规则后,用户可以定义一个非常简单的模型。
|
||||
|
||||
.. 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):
|
||||
# 应用图卷积和激活函数
|
||||
h = F.relu(self.conv1(g, h))
|
||||
h = F.relu(self.conv2(g, h))
|
||||
with g.local_scope():
|
||||
g.ndata['h'] = h
|
||||
# 使用平均读出计算图表示
|
||||
hg = dgl.mean_nodes(g, 'h')
|
||||
return self.classify(hg)
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
数据加载
|
||||
^^^^^^^^^^^^
|
||||
|
||||
|
||||
模型定义完成后,用户就可以开始训练模型。由于整图分类处理的是很多相对较小的图,而不是一个大图,
|
||||
因此通常可以在随机抽取的小批次图上进行高效的训练,而无需设计复杂的图采样算法。
|
||||
|
||||
以下例子中使用了 :ref:`guide_cn-data-pipeline` 中的整图分类数据集。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.data
|
||||
dataset = dgl.data.GINDataset('MUTAG', False)
|
||||
|
||||
整图分类数据集里的每个数据点是一个图和它对应标签的元组。为提升数据加载速度,
|
||||
用户可以调用GraphDataLoader,从而以小批次遍历整个图数据集。
|
||||
|
||||
.. code:: python
|
||||
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
dataloader = GraphDataLoader(
|
||||
dataset,
|
||||
batch_size=1024,
|
||||
drop_last=False,
|
||||
shuffle=True)
|
||||
|
||||
训练过程包括遍历dataloader和更新模型参数的部分。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch.nn.functional as F
|
||||
|
||||
# 这仅是个例子,特征尺寸是7
|
||||
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()
|
||||
|
||||
DGL实现了一个整图分类的样例:
|
||||
`DGL的GIN样例 <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gin>`__。
|
||||
模型训练的代码请参考位于
|
||||
`main.py <https://github.com/dmlc/dgl/blob/master/examples/pytorch/gin/main.py>`__ 源文件中的 ``train`` 函数。
|
||||
模型实现位于
|
||||
`gin.py <https://github.com/dmlc/dgl/blob/master/examples/pytorch/gin/gin.py>`__ ,
|
||||
其中使用了更多的模块组件,例如使用 :class:`dgl.nn.pytorch.GINConv`
|
||||
模块作为图卷积层(DGL同样支持它在MXNet和TensorFlow后端里的实现)、批量归一化等。
|
||||
|
||||
异构图上的整图分类模型的训练
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在异构图上做整图分类和在同构图上做整图分类略有不同。用户除了需要使用异构图卷积模块,还需要在读出函数中聚合不同类别的节点。
|
||||
|
||||
以下代码演示了如何对每种节点类型的节点表示取平均值并求和。
|
||||
|
||||
.. 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是节点的特征
|
||||
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
|
||||
# 通过平均读出值来计算单图的表征
|
||||
hg = 0
|
||||
for ntype in g.ntypes:
|
||||
hg = hg + dgl.mean_nodes(g, 'h', ntype=ntype)
|
||||
return self.classify(hg)
|
||||
|
||||
剩余部分的训练代码和同构图代码相同。
|
||||
|
||||
.. code:: python
|
||||
|
||||
# etypes是一个列表,元素是字符串类型的边类型
|
||||
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,191 @@
|
||||
.. _guide_cn-training-link-prediction:
|
||||
|
||||
5.3 链接预测
|
||||
---------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-link-prediction>`
|
||||
|
||||
在某些场景中,用户可能希望预测给定节点之间是否存在边,这样的任务称作 **链接预测** 任务。
|
||||
|
||||
概述
|
||||
~~~~~~~~
|
||||
|
||||
基于GNN的链接预测模型的基本思想是通过使用所需预测的节点对
|
||||
:math:`u`, :math:`v` 的节点表示 :math:`\boldsymbol{h}_u^{(L)}` 和
|
||||
:math:`\boldsymbol{h}_v^{(L)}`,计算它们之间存在链接可能性的得分 :math:`y_{u,v}`。
|
||||
其中 :math:`\boldsymbol{h}_u^{(L)}` 和 :math:`\boldsymbol{h}_v^{(L)}` 由多层GNN计算得出。
|
||||
|
||||
.. math::
|
||||
|
||||
y_{u,v} = \phi(\boldsymbol{h}_u^{(L)}, \boldsymbol{h}_v^{(L)})
|
||||
|
||||
本节把节点 :math:`u` 和 :math:`v` 之间存在连接可能性的 *得分* 记作 :math:`y_{u,v}`。
|
||||
|
||||
训练一个链接预测模型涉及到比对两个相连接节点之间的得分与任意一对节点之间的得分的差异。
|
||||
例如,给定一条连接 :math:`u` 和 :math:`v` 的边,一个好的模型希望 :math:`u` 和 :math:`v` 之间的得分要高于
|
||||
:math:`u` 和从一个任意的噪声分布 :math:`v′∼Pn(v)` 中所采样的节点 :math:`v′` 之间的得分。
|
||||
这样的方法称作 *负采样*。
|
||||
|
||||
许多损失函数都可以实现上述目标,包括但不限于。
|
||||
|
||||
- 交叉熵损失:
|
||||
: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]`
|
||||
- 贝叶斯个性化排序损失:
|
||||
:math:`\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} - \log \sigma (y_{u,v} - y_{u,v_i})`
|
||||
- 间隔损失:
|
||||
:math:`\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} \max(0, M - y_{u, v} + y_{u, v_i})`,
|
||||
其中 :math:`M` 是常数项超参数。
|
||||
|
||||
如果用户熟悉 `implicit feedback <https://arxiv.org/ftp/arxiv/papers/1205/1205.2618.pdf>`__ 和
|
||||
`noise-contrastive estimation <http://proceedings.mlr.press/v9/gutmann10a/gutmann10a.pdf>`__ ,
|
||||
可能会发现这些工作的想法都很类似。
|
||||
|
||||
计算 :math:`u` 和 :math:`v` 之间分数的神经网络模型与 :ref:`guide_cn-training-edge-classification`
|
||||
中所述的边回归模型相同。
|
||||
|
||||
下面是使用点积计算边得分的例子。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class DotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h):
|
||||
# h是从5.1节的GNN模型中计算出的节点表示
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
|
||||
return graph.edata['score']
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
因为上述的得分预测模型在图上进行计算,用户需要将负采样的样本表示为另外一个图,
|
||||
其中包含所有负采样的节点对作为边。
|
||||
|
||||
下面的例子展示了将负采样的样本表示为一个图。每一条边 :math:`(u,v)` 都有 :math:`k`
|
||||
个对应的负采样样本 :math:`(u,v_i)`,其中 :math:`v_i` 是从均匀分布中采样的。
|
||||
|
||||
.. 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())
|
||||
|
||||
预测边得分的模型和边分类/回归模型中的预测边得分模型相同。
|
||||
|
||||
.. 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)
|
||||
|
||||
训练的循环部分里会重复构建负采样图并计算损失函数值。
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# 间隔损失
|
||||
n_edges = pos_score.shape[0]
|
||||
return (1 - pos_score.unsqueeze(1) + 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())
|
||||
|
||||
训练后,节点表示可以通过以下代码获取。
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_embeddings = model.sage(graph, node_features)
|
||||
|
||||
(实际应用中),有着许多使用节点嵌入的方法,例如,训练下游任务的分类器,或为相关实体推荐进行最近邻搜索或最大内积搜索。
|
||||
|
||||
异构图上的链接预测模型的训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
异构图上的链接预测和同构图上的链接预测没有太大区别。下文是在一种边类型上进行预测,
|
||||
用户可以很容易地将其拓展为对多种边类型上进行预测。
|
||||
|
||||
例如,为某一种边类型,用户可以重复使用
|
||||
:ref:`guide_cn-training-edge-classification-heterogeneous-graph`
|
||||
里的 ``HeteroDotProductPredictor`` 来计算节点间存在连接可能性的得分。
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroDotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h, etype):
|
||||
# h是从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']
|
||||
|
||||
要执行负采样,用户可以对要进行链接预测的边类型构造一个负采样图。
|
||||
|
||||
.. 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})
|
||||
|
||||
该模型与异构图上边分类的模型有些不同,因为用户需要指定在哪种边类型上进行链接预测。
|
||||
|
||||
.. 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)
|
||||
|
||||
训练的循环部分和同构图时一致。
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# 间隔损失
|
||||
n_edges = pos_score.shape[0]
|
||||
return (1 - pos_score.unsqueeze(1) + 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,207 @@
|
||||
.. _guide_cn-training-node-classification:
|
||||
|
||||
5.1 节点分类/回归
|
||||
--------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-node-classification>`
|
||||
|
||||
对于图神经网络来说,最常见和被广泛使用的任务之一就是节点分类。
|
||||
图数据中的训练、验证和测试集中的每个节点都具有从一组预定义的类别中分配的一个类别,即正确的标注。
|
||||
节点回归任务也类似,训练、验证和测试集中的每个节点都被标注了一个正确的数字。
|
||||
|
||||
概述
|
||||
~~~~~~~~
|
||||
|
||||
为了对节点进行分类,图神经网络执行了 :ref:`guide_cn-message-passing`
|
||||
中介绍的消息传递机制,利用节点自身的特征和其邻节点及边的特征来计算节点的隐藏表示。
|
||||
消息传递可以重复多轮,以利用更大范围的邻居信息。
|
||||
|
||||
编写神经网络模型
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL提供了一些内置的图卷积模块,可以完成一轮消息传递计算。
|
||||
本章中选择 :class:`dgl.nn.pytorch.SAGEConv` 作为演示的样例代码(针对MXNet和PyTorch后端也有对应的模块),
|
||||
它是GraphSAGE模型中使用的图卷积模块。
|
||||
|
||||
对于图上的深度学习模型,通常需要一个多层的图神经网络,并在这个网络中要进行多轮的信息传递。
|
||||
可以通过堆叠图卷积模块来实现这种网络架构,具体如下所示。
|
||||
|
||||
.. code:: python
|
||||
|
||||
# 构建一个2层的GNN模型
|
||||
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__()
|
||||
# 实例化SAGEConve,in_feats是输入特征的维度,out_feats是输出特征的维度,aggregator_type是聚合函数的类型
|
||||
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):
|
||||
# 输入是节点的特征
|
||||
h = self.conv1(graph, inputs)
|
||||
h = F.relu(h)
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
请注意,这个模型不仅可以做节点分类,还可以为其他下游任务获取隐藏节点表示,如:
|
||||
:ref:`guide_cn-training-edge-classification`、
|
||||
:ref:`guide_cn-training-link-prediction` 和
|
||||
:ref:`guide_cn-training-graph-classification`。
|
||||
|
||||
关于DGL内置图卷积模块的完整列表,读者可以参考 :ref:`apinn`。
|
||||
|
||||
有关DGL神经网络模块如何工作,以及如何编写一个自定义的带有消息传递的GNN模块的更多细节,请参考 :ref:`guide_cn-nn` 中的例子。
|
||||
|
||||
模型的训练
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
全图(使用所有的节点和边的特征)上的训练只需要使用上面定义的模型进行前向传播计算,并通过在训练节点上比较预测和真实标签来计算损失,从而完成后向传播。
|
||||
|
||||
本节使用DGL内置的数据集 :class:`dgl.data.CiteseerGraphDataset` 来展示模型的训练。
|
||||
节点特征和标签存储在其图上,训练、验证和测试的分割也以布尔掩码的形式存储在图上。这与在
|
||||
:ref:`guide_cn-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)
|
||||
|
||||
下面是通过使用准确性来评估模型的一个例子。
|
||||
|
||||
.. 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)
|
||||
|
||||
用户可以按如下方式实现模型的训练。
|
||||
|
||||
.. 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()
|
||||
# 使用所有节点(全图)进行前向传播计算
|
||||
logits = model(graph, node_features)
|
||||
# 计算损失值
|
||||
loss = F.cross_entropy(logits[train_mask], node_labels[train_mask])
|
||||
# 计算验证集的准确度
|
||||
acc = evaluate(model, graph, node_features, node_labels, valid_mask)
|
||||
# 进行反向传播计算
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
# 如果需要的话,保存训练好的模型。本例中省略。
|
||||
|
||||
|
||||
`DGL的GraphSAGE样例 <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_full.py>`__
|
||||
提供了一个端到端的同构图节点分类的例子。用户可以在 ``GraphSAGE`` 类中看到模型实现的细节。
|
||||
这个模型具有可调节的层数、dropout概率,以及可定制的聚合函数和非线性函数。
|
||||
|
||||
.. _guide_cn-training-rgcn-node-classification:
|
||||
|
||||
异构图上的节点分类模型的训练
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
如果图是异构的,用户可能希望沿着所有边类型从邻居那里收集消息。
|
||||
用户可以使用 :class:`dgl.nn.pytorch.HeteroGraphConv`
|
||||
模块(针对MXNet和PyTorch后端也有对应的模块)在所有边类型上执行消息传递,
|
||||
并为每种边类型使用一种图卷积模块。
|
||||
|
||||
下面的代码定义了一个异构图卷积模块。模块首先对每种边类型进行单独的图卷积计算,然后将每种边类型上的消息聚合结果再相加,
|
||||
并作为所有节点类型的最终结果。
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Define a Heterograph Conv model
|
||||
|
||||
class RGCN(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats, rel_names):
|
||||
super().__init__()
|
||||
# 实例化HeteroGraphConv,in_feats是输入特征的维度,out_feats是输出特征的维度,aggregate是聚合函数的类型
|
||||
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):
|
||||
# 输入是节点的特征字典
|
||||
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`` 接收一个节点类型和节点特征张量的字典作为输入,并返回另一个节点类型和节点特征的字典。
|
||||
|
||||
本章的 :ref:`guide_cn-training-heterogeneous-graph-example`
|
||||
中已经有了 ``user`` 和 ``item`` 的特征,用户可用如下代码获取。
|
||||
|
||||
.. 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']
|
||||
|
||||
然后,用户可以简单地按如下形式进行前向传播计算:
|
||||
|
||||
.. 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']
|
||||
|
||||
异构图上模型的训练和同构图的模型训练是一样的,只是这里使用了一个包括节点表示的字典来计算预测值。
|
||||
例如,如果只预测 ``user`` 节点的类别,用户可以从返回的字典中提取 ``user`` 的节点嵌入。
|
||||
|
||||
.. code:: python
|
||||
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for epoch in range(5):
|
||||
model.train()
|
||||
# 使用所有节点的特征进行前向传播计算,并提取输出的user节点嵌入
|
||||
logits = model(hetero_graph, node_features)['user']
|
||||
# 计算损失值
|
||||
loss = F.cross_entropy(logits[train_mask], labels[train_mask])
|
||||
# 计算验证集的准确度。在本例中省略。
|
||||
# 进行反向传播计算
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
# 如果需要的话,保存训练好的模型。本例中省略。
|
||||
|
||||
DGL提供了一个用于节点分类的RGCN的端到端的例子
|
||||
`RGCN <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/entity_classify.py>`__
|
||||
。用户可以在 `RGCN模型实现文件
|
||||
<https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/model.py>`__
|
||||
中查看异构图卷积 ``RelGraphConvLayer`` 的具体定义。
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
.. _guide_cn-training:
|
||||
|
||||
第5章:训练图神经网络
|
||||
=====================================================
|
||||
|
||||
:ref:`(English Version) <guide-training>`
|
||||
|
||||
概述
|
||||
--------
|
||||
|
||||
本章通过使用 :ref:`guide_cn-message-passing` 中介绍的消息传递方法和 :ref:`guide_cn-nn` 中介绍的图神经网络模块,
|
||||
讲解了如何对小规模的图数据进行节点分类、边分类、链接预测和整图分类的图神经网络的训练。
|
||||
|
||||
本章假设用户的图以及所有的节点和边特征都能存进GPU。对于无法全部载入的情况,请参考用户指南的 :ref:`guide_cn-minibatch`。
|
||||
|
||||
后续章节的内容均假设用户已经准备好了图和节点/边的特征数据。如果用户希望使用DGL提供的数据集或其他兼容
|
||||
``DGLDataset`` 的数据(如 :ref:`guide_cn-data-pipeline` 所述),
|
||||
可以使用类似以下代码的方法获取单个图数据集的图数据。
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
|
||||
dataset = dgl.data.CiteseerGraphDataset()
|
||||
graph = dataset[0]
|
||||
|
||||
注意: 本章代码使用PyTorch作为DGL的后端框架。
|
||||
|
||||
.. _guide_cn-training-heterogeneous-graph-example:
|
||||
|
||||
异构图训练的样例数据
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
有时用户会想在异构图上进行图神经网络的训练。本章会以下面代码所创建的一个异构图为例,来演示如何进行节点分类、边分类和链接预测的训练。
|
||||
|
||||
这个 ``hetero_graph`` 异构图有以下这些边的类型:
|
||||
|
||||
- ``('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()
|
||||
# 在user类型的节点和click类型的边上随机生成训练集的掩码
|
||||
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)
|
||||
|
||||
本章路线图
|
||||
------------
|
||||
|
||||
本章共有四节,每节对应一种图学习任务。
|
||||
|
||||
* :ref:`guide_cn-training-node-classification`
|
||||
* :ref:`guide_cn-training-edge-classification`
|
||||
* :ref:`guide_cn-training-link-prediction`
|
||||
* :ref:`guide_cn-training-graph-classification`
|
||||
* :ref:`guide_cn-training-graph-eweight`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
training-node
|
||||
training-edge
|
||||
training-link
|
||||
training-graph
|
||||
Reference in New Issue
Block a user