Unverified Commit b0a9d16f authored by zhjwy9343's avatar zhjwy9343 Committed by GitHub
Browse files

[Doc] Chinese User Guide chapter 1 - 4 (#2351)



* [Feature] Add full graph training with dgl built-in dataset.

* [Feature] Add full graph training with dgl built-in dataset.

* [Feature] Add full graph training with dgl built-in dataset.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Bug] fix model to cuda.

* [Feature] Add test loss and accuracy

* [Feature] Add test loss and accuracy

* [Feature] Add test loss and accuracy

* [Feature] Add test loss and accuracy

* [Feature] Add test loss and accuracy

* [Feature] Add test loss and accuracy

* [Fix] Add random

* [Bug] Fix batch norm error

* [Doc] Test with CN in Sphinx

* [Doc] Test with CN in Sphinx

* [Doc] Remove the test CN docs.

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Feature] Add input embedding layer

* [Doc] fill readme with new performance results

* [Doc] Add Chinese User Guide, graph and 1.5

* [Doc] Add Chinese User Guide, graph and 1.5

* Update README.md

* [Fix] Temporary remove compgcn

* [Doc] Add CN user guide chapter2

* [Test] Tunning format

* [Test] Tunning format

* [Test] Tunning format

* [Test] Tunning format

* [Test] Tunning format

* [Test] Section headers

* [Fix] Fix format errors

* [Fix] Fix format errors

* [Fix] Fix format errors

* [Doc] Add CN-EN EN-CN links

* [Doc] Add CN-EN EN-CN links

* [Doc] Copyedit chapter2

* [Doc] Copyedit chapter2

* [Doc] Remove EN in 2.1

* [Doc] Remove EN in chapter 2

* [Doc] Copyedit first 2 sections

* [Doc] Copyedit first 2 sections

* [Doc] copyedited chapter 2 CN

* [Doc] Add chapter 3 raw texts

* [Doc] Add chapter 3 preface and 3.1

* [Doc] Add chapter 3.2 and 3.3

* [Doc] Add chapter 3.2 and 3.3

* [Doc] Add chapter 3.2 and 3.3

* [Doc] Remove EN parts

* [Doc] Copyediting 3.1

* [Doc] Copyediting 3.2 and 3.3

* [Doc] Proofreading 3.1 and 3.2

* [Doc] Proofreading 3.2 and 3.3

* [Doc] Add chapter 4 CN raw text.

* [Clean] Remove codes in other branches

* [Doc] Start to copyedit chapter 4 preface

* [Doc] copyedit CN section 4.1

* [Doc] Remove EN in User Guide Chapter 4

* [Doc] Copyedit chapter 4.1

* [Doc] copyedit cn chapter 4.2, 4.3, 4.4, and 4.5.

* [Doc] Fix errors in EN user guide graph feature and heterograph

* [Doc] 2nd round copyediting with Murph's comments

* [Doc] 3rd round copyediting with Murph's comments

* [Doc] 3rd round copyediting with Murph's comments

* [Doc] 3rd round copyediting with Murph's comments

* [Sync] syncronize with the dgl master

* [Doc] edited after Minjie's comments, 1st round

* update cub
Co-authored-by: default avatarMinjie Wang <wmjlyjemaine@gmail.com>
parent 9c08cd6b
.. _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_labels`` 来指示此多任务数据集中的预测任务总数:
.. code::
@property
def num_labels(self):
"""每个图的标签数,即预测任务数。"""
return 14
在编写完这些代码之后,用户可以按如下所示的方式来使用 :class:`~dgl.data.QM7bDataset`:
.. code::
import dgl
import torch
from torch.utils.data import DataLoader
# 数据导入
dataset = QM7bDataset()
num_labels = dataset.num_labels
# 创建collate_fn函数
def _collate_fn(batch):
graphs, labels = batch
g = dgl.batch(graphs)
labels = torch.tensor(labels, dtype=torch.long)
return g, labels
# 创建 dataloaders
dataloader = DataLoader(dataset, batch_size=1, shuffle=True, collate_fn=_collate_fn)
# 训练
for epoch in range(100):
for g, labels in dataloader:
# 用户自己的训练代码
pass
训练整图分类模型的完整指南可以在 :ref:`guide-training-graph-classification` 中找到。
有关整图分类数据集的更多示例,用户可以参考 :ref:`guide-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>`__ 为例:
.. code::
from dgl.data import DGLBuiltinDataset
from dgl.data.utils import _get_dgl_url, generate_mask_tensor
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'] = generate_mask_tensor(train_mask)
g.ndata['val_mask'] = generate_mask_tensor(val_mask)
g.ndata['test_mask'] = generate_mask_tensor(test_mask)
# 节点的标签
g.ndata['label'] = torch.tensor(labels)
# 节点的特征
g.ndata['feat'] = torch.tensor(_preprocess_features(features),
dtype=F.data_type_dict['float32'])
self._num_labels = onehot_labels.shape[1]
self._labels = labels
self._g = 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-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).squeeze()
src, dst = graph.edges(train_idx)
# 获取训练集中的边类型
rel = graph.edata['etype'][train_idx]
有关训练链接预测模型的完整指南,请参见 :ref:`guide-training-link-prediction`。
有关链接预测数据集的更多示例,请参考DGL的内置数据集:
* :ref:`kgdata`
* :ref:`bitcoinotcdata`
.. _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)`` 中处理每个数据实例是更高效的方法。
.. _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
\ No newline at end of file
.. _guide_cn-graph:
第1章:图
========
=============
:ref:`(English Version)<guide-graph>`
......@@ -11,7 +11,7 @@ DGL通过其核心数据结构 :class:`~dgl.DGLGraph` 提供了一个以图为
本章路线图
--------
--------------
本章首先简要介绍了图的定义(见1.1节),然后介绍了一些 :class:`~dgl.DGLGraph` 相关的核心概念:
......
.. _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)}
在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 updata_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})
.. _guide_cn-message-passing-edge:
2.4 在消息传递中使用边的权重
-----------------------
:ref:`(English Version) <guide-message-passing-edge>`
一类常见的图神经网络建模的做法是在消息聚合前使用边的权重,
比如在 `图注意力网络(GAT) <https://arxiv.org/pdf/1710.10903.pdf>`__ 和一些 `GCN的变种 <https://arxiv.org/abs/2004.00445>`__ 。
DGL的处理方法是:
- 将权重存为边的特征。
- 在消息函数中用边的特征与源节点的特征相乘。
例如:
.. code::
import dgl.function as fn
graph.edata['a'] = affinity
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
fn.sum('m', 'ft'))
在以上代码中,affinity被用作边的权重。边权重通常是一个标量。
\ No newline at end of file
.. _guide_cn-message-passing-efficient:
2.2 编写高效的消息传递代码
----------------------
:ref:`(English Version) <guide-message-passing-efficient>`
DGL优化了消息传递的内存消耗和计算速度,这包括:
- 将多个内核合并到一个内核中:这是通过使用 :meth:`~dgl.DGLGraph.update_all` 一次调用多个内置函数来实现的。(速度优化)
- 节点和边上的并行计算:DGL抽象了逐边计算,将 :meth:`~dgl.DGLGraph.apply_edges` 作为一种广义抽样稠密-稠密矩阵乘法
**(gSDDMM)** 运算,并实现了跨边并行计算。同样,DGL将逐节点计算 :meth:`~dgl.DGLGraph.update_all` 抽象为广义稀疏-稠密矩阵乘法(gSPMM)运算,
并实现了跨节点并行计算。(速度优化)
- 避免不必要的从点到边的内存拷贝:想要生成带有源节点和目标节点特征的消息,一个选项是将源节点和目标节点的特征拷贝到边上。
对于某些图,边的数量远远大于节点的数量。这个拷贝的代价会很大。DGL内置的消息函数通过使用条目索引对节点特征进行采集来避免这种内存拷贝。
(内存和速度优化)
- 避免具体化边上的特征向量:完整的消息传递过程包括消息生成、消息聚合和节点更新。
在调用 :meth:`~dgl.DGLGraph.update_all` 时,如果消息函数和聚合函数是内置的,则它们会被合并到一个内核中,
从而避免存储消息对象。(内存优化)
根据以上所述,利用这些优化的一个常见实践是通过基于内置函数的 :meth:`~dgl.DGLGraph.update_all` 来开发消息传递功能。
对于某些情况,比如 :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=(1, node_feat_dim * 2)))
def concat_message_function(edges):
return {'cat_feat': torch.cat([edges.src.ndata['feat'], edges.dst.ndata['feat']])}
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=(1, node_feat_dim)))
linear_dst = nn.Parameter(torch.FloatTensor(size=(1, node_feat_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`` 进行优化,从而进一步加快计算速度并节省内存占用。
.. _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}
.. _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:`第6章:在大图上的随机(批次)训练 <guide-minibatch>`。
\ No newline at end of file
.. _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-edge`
* :ref:`guide_cn-message-passing-heterograph`
.. toctree::
:maxdepth: 1
:hidden:
:glob:
message-api
message-efficient
message-part
message-edge
message-heterograph
.. _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、max_pool、lstm、gcn
if aggregator_type not in ['mean', 'max_pool', 'lstm', 'gcn']:
raise KeyError('Aggregator type {} not supported.'.format(aggregator_type))
if aggregator_type == 'max_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', 'max_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 == 'max_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)
.. _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-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})
源节点特征 ``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 == 'max_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()`` 函数的最后一部分是在完成消息聚合后更新节点的特征。
常见的更新操作是根据构造函数中设置的选项来应用激活函数和进行归一化。
.. _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 的示例。
\ No newline at end of file
.. _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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment