chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
.. _guide_ko-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 클래스에 정의된 그래프 데이터 입력 파이프라인에 대한 순서도
|
||||
|
||||
|
||||
원격 또는 로컬 디스크에 있는 그래프 데이터셋을 처리하기 위해서, :class:`dgl.data.DGLDataset` 를 상속해서 클래스를 정의하나. 예로, ``MyDataset`` 이라고 하자. ``MyDataset`` 템플릿은 다음과 같다.
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import DGLDataset
|
||||
|
||||
class MyDataset(DGLDataset):
|
||||
""" Template for customizing graph datasets in DGL.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
URL to download the raw dataset
|
||||
raw_dir : str
|
||||
Specifying the directory that will store the
|
||||
downloaded data or the directory that
|
||||
already stores the input data.
|
||||
Default: ~/.dgl/
|
||||
save_dir : str
|
||||
Directory to save the processed dataset.
|
||||
Default: the value of `raw_dir`
|
||||
force_reload : bool
|
||||
Whether to reload the dataset. Default: False
|
||||
verbose : bool
|
||||
Whether to print out progress information
|
||||
"""
|
||||
def __init__(self,
|
||||
url=None,
|
||||
raw_dir=None,
|
||||
save_dir=None,
|
||||
force_reload=False,
|
||||
verbose=False):
|
||||
super(MyDataset, self).__init__(name='dataset_name',
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
save_dir=save_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose)
|
||||
|
||||
def download(self):
|
||||
# download raw data to local disk
|
||||
pass
|
||||
|
||||
def process(self):
|
||||
# process raw data to graphs, labels, splitting masks
|
||||
pass
|
||||
|
||||
def __getitem__(self, idx):
|
||||
# get one example by index
|
||||
pass
|
||||
|
||||
def __len__(self):
|
||||
# number of data examples
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
# save processed data to directory `self.save_path`
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
# load processed data from directory `self.save_path`
|
||||
pass
|
||||
|
||||
def has_cache(self):
|
||||
# check whether there are processed data in `self.save_path`
|
||||
pass
|
||||
|
||||
:class:`~dgl.data.DGLDataset` 클래스에는 서브클래스에서 꼭 구현되어야 하는 함수들 ``process()`` ,
|
||||
``__getitem__(idx)`` 와 ``__len__()`` 이 있다. 또한 DGL은 저장과 로딩을 구현하는 것을 권장하는데, 그 이유는 큰 데이터셋 처리 시간을 많이 줄일 수 있고, 이를 쉽게 구현하는데 필요한 API들이 있기 때문이다. (:ref:`guide_ko-data-pipeline-savenload` 참고)
|
||||
|
||||
:class:`~dgl.data.DGLDataset` 의 목적은 그래프 데이터 로드에 필요한 편리하고 표준적인 방법을 제공하는 것이다. 그래프, 피쳐, 레이블, 그리고 데이터셋에 대한 기본적인 정보 (클래스 개수, 레이블 개수 등)을 저장할 수 있다. 샘플링, 파티셔닝 또는 파쳐 normalization과 같은 작업은 :class:`~dgl.data.DGLDataset` 의 서브클래스 밖에서 수행된다.
|
||||
|
||||
이 장의 나머지에서는 파이프라인에서 함수를 구현하는 best practice들을 소개한다.
|
||||
@@ -0,0 +1,45 @@
|
||||
.. _guide_ko-data-pipeline-download:
|
||||
|
||||
4.2 Raw 데이터 다운로드하기 (optional)
|
||||
---------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-download>`
|
||||
|
||||
로컬 디스크에 데이터셋이 이미 존재한다면, ``raw_dir`` 디렉토리에 있어야 한다. 만약 데이터를 다운로드하고 특정 디렉토리에 옮기는 일을 직접 수행하지 않고 코드를 실행하고 어디서나 실행하고 싶다면, ``download()`` 구현해서 이를 자동화할 수 있다.
|
||||
|
||||
데이터셋이 zip 파일 포멧인 경우, zip 파일 추출을 자동을 해주는 :class:`dgl.data.DGLBuiltinDataset` 클래스를 상속해서 ``MyDataset`` 클래스를 만들자. 그렇지 않은 경우 :class:`~dgl.data.QM7bDataset` 처럼 ``download()`` 함수를 직접 구현한다:
|
||||
|
||||
.. code::
|
||||
|
||||
import os
|
||||
from dgl.data.utils import download
|
||||
|
||||
def download(self):
|
||||
# path to store the file
|
||||
file_path = os.path.join(self.raw_dir, self.name + '.mat')
|
||||
# download file
|
||||
download(self.url, path=file_path)
|
||||
|
||||
위 코드는 .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):
|
||||
# path to store the file
|
||||
# make sure to use the same suffix as the original file name's
|
||||
gz_file_path = os.path.join(self.raw_dir, self.name + '.csv.gz')
|
||||
# download file
|
||||
download(self.url, path=gz_file_path)
|
||||
# check SHA-1
|
||||
if not check_sha1(gz_file_path, self._sha1_str):
|
||||
raise UserWarning('File {} is downloaded but the content hash does not match.'
|
||||
'The repo may be outdated or download may be incomplete. '
|
||||
'Otherwise you can create an issue for it.'.format(self.name + '.csv.gz'))
|
||||
# extract file to directory `self.name` under `self.raw_dir`
|
||||
self._extract_gz(gz_file_path, self.raw_path)
|
||||
|
||||
위 코드는 ``self.raw_dir`` 디렉토리 아래의 ``self.name`` 서브 디렉토리에 파일을 추출한다. 만약 zip 파일을 다루기 위해서 :class:`dgl.data.DGLBuiltinDataset` 를 상속해서 사용했다면, 파일들은 자동으로 ``self.name`` 디렉토리로 추출될 것이다.
|
||||
|
||||
추가적으로, 다운로드한 파일에 대한 SHA-1 값 검증을 수행해서 파일이 변경되었는지 확인하는 것도 위 예제처럼 구현할 수 있다.
|
||||
@@ -0,0 +1,73 @@
|
||||
.. _guide_ko-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 패키지 `ogb <https://github.com/snap-stanford/ogb>`__ 는 OBG 데이터셋들을 다운로드해서 :class:`dgl.data.DGLGraph` 객체로 프로세싱하는 API들을 제공한다. 이 절은 기본적인 사용법을 설명한다.
|
||||
|
||||
우선 obg 패키지를 pip 명령으로 설치한다.
|
||||
|
||||
.. code::
|
||||
|
||||
pip install ogb
|
||||
|
||||
다음 코드는 *Graph Property Prediction* 테스크를 위한 데이터셋 로딩 방법을 보여준다.
|
||||
|
||||
.. code::
|
||||
|
||||
# Load Graph Property Prediction datasets in OGB
|
||||
import dgl
|
||||
import torch
|
||||
from ogb.graphproppred import DglGraphPropPredDataset
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
|
||||
|
||||
def _collate_fn(batch):
|
||||
# batch is a list of tuple (graph, label)
|
||||
graphs = [e[0] for e in batch]
|
||||
g = dgl.batch(graphs)
|
||||
labels = [e[1] for e in batch]
|
||||
labels = torch.stack(labels, 0)
|
||||
return g, labels
|
||||
|
||||
# load dataset
|
||||
dataset = DglGraphPropPredDataset(name='ogbg-molhiv')
|
||||
split_idx = dataset.get_idx_split()
|
||||
# dataloader
|
||||
train_loader = GraphDataLoader(dataset[split_idx["train"]], batch_size=32, shuffle=True, collate_fn=_collate_fn)
|
||||
valid_loader = GraphDataLoader(dataset[split_idx["valid"]], batch_size=32, shuffle=False, collate_fn=_collate_fn)
|
||||
test_loader = GraphDataLoader(dataset[split_idx["test"]], batch_size=32, shuffle=False, collate_fn=_collate_fn)
|
||||
|
||||
*Node Property Prediction* 데이터셋을 로딩하는 것이 비슷하지만, 이런 종류의 데이터셋은 오직 한 개의 그래프 객체만 존재한다는 것이 다름을 유의하자.
|
||||
|
||||
.. code::
|
||||
|
||||
# Load Node Property Prediction datasets in OGB
|
||||
from ogb.nodeproppred import DglNodePropPredDataset
|
||||
|
||||
dataset = DglNodePropPredDataset(name='ogbn-proteins')
|
||||
split_idx = dataset.get_idx_split()
|
||||
|
||||
# there is only one graph in Node Property Prediction datasets
|
||||
g, labels = dataset[0]
|
||||
# get split labels
|
||||
train_label = dataset.labels[split_idx['train']]
|
||||
valid_label = dataset.labels[split_idx['valid']]
|
||||
test_label = dataset.labels[split_idx['test']]
|
||||
|
||||
*Link Property Prediction* 데이터셋 역시 데이터셋에 한개의 그래프를 갖고 있다.
|
||||
|
||||
.. code::
|
||||
|
||||
# Load Link Property Prediction datasets in OGB
|
||||
from ogb.linkproppred import DglLinkPropPredDataset
|
||||
|
||||
dataset = DglLinkPropPredDataset(name='ogbl-ppa')
|
||||
split_edge = dataset.get_edge_split()
|
||||
|
||||
graph = dataset[0]
|
||||
print(split_edge['train'].keys())
|
||||
print(split_edge['valid'].keys())
|
||||
print(split_edge['test'].keys())
|
||||
@@ -0,0 +1,271 @@
|
||||
.. _guide_ko-data-pipeline-process:
|
||||
|
||||
4.3 데이터 프로세싱
|
||||
---------------
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline-process>`
|
||||
|
||||
데이터 프로세싱 코드를 ``process()`` 함수에 구현할 수 있으며, 이때 처리되지 않은 데이터는 ``self.raw_dir`` 디렉토리에 있어야 한다. 그래프 머신러닝에는 일반적으로 3가지 종류의 일이 있다: 그래프 분류, 노드 분류, 그리고 링크 예측. 이 절에서는 이 일들에 관련된 데이터셋 처리 방법을 설명한다.
|
||||
|
||||
이 절에서 그래프들, 피쳐들, 그리고 마스크들을 처리하는 표준 방법에 집중해서 알아본다. 빌트인 데이터셋을 예제로 사용할 것이고, 파일로 부터 그래프를 만드는 방법은 생략한다. 하지만, 이와 관련된 구현에 대한 링크를 제공할 것이다. 외부 소스들로 부터 그래프를 만드는 방법에 대한 완벽한 가이드는 :ref:`guide_ko-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'
|
||||
# process data to a list of graphs and a list of labels
|
||||
self.graphs, self.label = self._load_graph(mat_path)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
""" Get graph and label by index
|
||||
|
||||
Parameters
|
||||
----------
|
||||
idx : int
|
||||
Item index
|
||||
|
||||
Returns
|
||||
-------
|
||||
(dgl.DGLGraph, Tensor)
|
||||
"""
|
||||
return self.graphs[idx], self.label[idx]
|
||||
|
||||
def __len__(self):
|
||||
"""Number of graphs in the dataset"""
|
||||
return len(self.graphs)
|
||||
|
||||
``process()`` 함수에서 처리되지 않은 데이터는 그래프들의 리스트와 레이블들의 리스트로 변환된다. Iteration을 위해서 ``__getitem__(idx)`` 와 ``__len__()`` 를 구현해야 한다. 위의 예제에서와 같이, DGL에서는 ``__getitem__(idx)`` 가 ``(graph, label)`` tuple을 리턴하도록 권장한다. ``self._load_graph()`` 와 ``__getitem__`` 함수의 구체적인 구현은 `QM7bDataset source
|
||||
code <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/qm7b.html#QM7bDataset>`__ 를 확인하자.
|
||||
|
||||
데이터셋의 유용한 정보들을 지정하기 위해서 클래스에 프로퍼티들을 추가하는 것이 가능하다. :class:`~dgl.data.QM7bDataset` 에 이 멀티 테스크 데이터셋의 예측 테스트의 총 개숫를 지정하기 위해 ``num_tasks`` 라는 프로퍼티를 추가할 수 있다.
|
||||
|
||||
.. code::
|
||||
|
||||
@property
|
||||
def num_tasks(self):
|
||||
"""Number of labels for each graph, i.e. number of prediction tasks."""
|
||||
return 14
|
||||
|
||||
구현 코드를 마친 후에, :class:`~dgl.data.QM7bDataset` 를 다음과 같이 사용한다.
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl
|
||||
import torch
|
||||
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
|
||||
# load data
|
||||
dataset = QM7bDataset()
|
||||
num_tasks = dataset.num_tasks
|
||||
|
||||
# create dataloaders
|
||||
dataloader = GraphDataLoader(dataset, batch_size=1, shuffle=True)
|
||||
|
||||
# training
|
||||
for epoch in range(100):
|
||||
for g, labels in dataloader:
|
||||
# your training code here
|
||||
pass
|
||||
|
||||
그래프 분류 모델 학습에 대한 전체 가이드는 :ref:`guide_ko-training-graph-classification` 를 참고하자.
|
||||
|
||||
DGL의 빌트인 그래프 분류 데이터셋을 참고하면 그래프 분류 데이터셋의 더 많은 예들을 확인할 수 있다.
|
||||
|
||||
* :ref:`gindataset`
|
||||
* :ref:`minigcdataset`
|
||||
* :ref:`qm7bdata`
|
||||
* :ref:`tudata`
|
||||
|
||||
노드 분류 데이터셋 프로세싱
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
그래프 분류와는 다르게 노드 분류는 일번적으로 단일 그래프에서 이뤄진다. 따라서, 데이터셋의 분할(split)은 그래프 노드에서 일어난다. DGL은 노드 마스크를 사용해서 분할을 지정하는 것을 권장한다. 이 절에서는 빌트인 데이터셋 `CitationGraphDataset <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/citation_graph.html#CitationGraphDataset>`__ 을 예로 들겠다.
|
||||
|
||||
추가로, DGL은 노드들와 에지들이 서로 가까운 ID값들이 서로 가까운 범위에 있도록 재배열하는 것을 권장한다. 이 절차는 노드의 neighbor들에 대한 접근성을 향상시켜서, 이 후의 연산 및 그래프에 대한 분석을 빠르게 하기 위함이다. 이를 위해서 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):
|
||||
# Skip some processing code
|
||||
# === data processing skipped ===
|
||||
|
||||
# build graph
|
||||
g = dgl.graph(graph)
|
||||
# splitting masks
|
||||
g.ndata['train_mask'] = train_mask
|
||||
g.ndata['val_mask'] = val_mask
|
||||
g.ndata['test_mask'] = test_mask
|
||||
# node labels
|
||||
g.ndata['label'] = torch.tensor(labels)
|
||||
# node features
|
||||
g.ndata['feat'] = torch.tensor(_preprocess_features(features),
|
||||
dtype=F.data_type_dict['float32'])
|
||||
self._num_tasks = onehot_labels.shape[1]
|
||||
self._labels = labels
|
||||
# reorder graph to obtain better locality.
|
||||
self._g = dgl.reorder_graph(g)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
return self._g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
분류 데이터셋 프로세싱 코드의 중요한 부분(마스크 분할하기)을 강조하기 위해서 ``process()`` 함수의 코드 일부는 생략해서 간략하게 만들었다.
|
||||
|
||||
일반적으로 노드 분류 테스크에서 하나의 그래프만 사용되기 때문에, ``__getitem__(idx)`` 와 ``__len__()`` 함수 구현이 바뀐 점을 알아두자. 마스크는 PyTorch와 TensorFlow에서는 ``bool tensors`` 이고 MXNet에서는 ``float tensors`` 이다.
|
||||
|
||||
다음 예는 ``CitationGraphDataset`` 의 서브 클래스인 :class:`dgl.data.CiteseerGraphDataset` 를 사용하는 방법이다.
|
||||
|
||||
.. code::
|
||||
|
||||
# load data
|
||||
dataset = CiteseerGraphDataset(raw_dir='')
|
||||
graph = dataset[0]
|
||||
|
||||
# get split masks
|
||||
train_mask = graph.ndata['train_mask']
|
||||
val_mask = graph.ndata['val_mask']
|
||||
test_mask = graph.ndata['test_mask']
|
||||
|
||||
# get node features
|
||||
feats = graph.ndata['feat']
|
||||
|
||||
# get labels
|
||||
labels = graph.ndata['label']
|
||||
|
||||
노드 분류 모델에 대한 전체 가이드는 :ref:`guide_ko-training-node-classification` 를 참고하자.
|
||||
|
||||
DGL의 빌트인 데이터셋들은 노드 분류 데이터셋의 여러 예제들을 포함하고 있다.
|
||||
|
||||
* :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::
|
||||
|
||||
# Example for creating Link Prediction datasets
|
||||
class KnowledgeGraphDataset(DGLBuiltinDataset):
|
||||
def __init__(self, name, reverse=True, raw_dir=None, force_reload=False, verbose=True):
|
||||
self._name = name
|
||||
self.reverse = reverse
|
||||
url = _get_dgl_url('dataset/') + '{}.tgz'.format(name)
|
||||
super(KnowledgeGraphDataset, self).__init__(name,
|
||||
url=url,
|
||||
raw_dir=raw_dir,
|
||||
force_reload=force_reload,
|
||||
verbose=verbose)
|
||||
|
||||
def process(self):
|
||||
# Skip some processing code
|
||||
# === data processing skipped ===
|
||||
|
||||
# splitting mask
|
||||
g.edata['train_mask'] = train_mask
|
||||
g.edata['val_mask'] = val_mask
|
||||
g.edata['test_mask'] = test_mask
|
||||
# edge type
|
||||
g.edata['etype'] = etype
|
||||
# node type
|
||||
g.ndata['ntype'] = ntype
|
||||
self._g = g
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert idx == 0, "This dataset has only one graph"
|
||||
return self._g
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
|
||||
위 코드에서 볼 수 있듯이 분할 마스크들을 그래프의 ``edata`` 필드에 추가한다. 전체 구현은 `KnowledgeGraphDataset 소스 코드 <https://docs.dgl.ai/en/0.5.x/_modules/dgl/data/knowledge_graph.html#KnowledgeGraphDataset>`__ 를 참고하자.
|
||||
|
||||
.. code::
|
||||
|
||||
from dgl.data import FB15k237Dataset
|
||||
|
||||
# load data
|
||||
dataset = FB15k237Dataset()
|
||||
graph = dataset[0]
|
||||
|
||||
# get training mask
|
||||
train_mask = graph.edata['train_mask']
|
||||
train_idx = torch.nonzero(train_mask, as_tuple=False).squeeze()
|
||||
src, dst = graph.edges(train_idx)
|
||||
# get edge types in training set
|
||||
rel = graph.edata['etype'][train_idx]
|
||||
|
||||
링크 예측 모델에 대한 전체 가이드는 :ref:`guide_ko-training-link-prediction` 에 있다.
|
||||
|
||||
DGL의 빌트인 데이터셋들은 링크 예측 데이터셋의 여러 예제들을 포함하고 있다.
|
||||
|
||||
* :ref:`kgdata`
|
||||
|
||||
* :ref:`bitcoinotcdata`
|
||||
@@ -0,0 +1,43 @@
|
||||
.. _guide_ko-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):
|
||||
# save graphs and labels
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
save_graphs(graph_path, self.graphs, {'labels': self.labels})
|
||||
# save other information in python dict
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
save_info(info_path, {'num_classes': self.num_classes})
|
||||
|
||||
def load(self):
|
||||
# load processed data from directory `self.save_path`
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
self.graphs, label_dict = load_graphs(graph_path)
|
||||
self.labels = label_dict['labels']
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
self.num_classes = load_info(info_path)['num_classes']
|
||||
|
||||
def has_cache(self):
|
||||
# check whether there are processed data in `self.save_path`
|
||||
graph_path = os.path.join(self.save_path, self.mode + '_dgl_graph.bin')
|
||||
info_path = os.path.join(self.save_path, self.mode + '_info.pkl')
|
||||
return os.path.exists(graph_path) and os.path.exists(info_path)
|
||||
|
||||
단, 프로세싱된 데이터를 저장하는 것이 적합하지 않은 경우도 있다. 예를 들어, 빌트인 데이터셋 중 :class:`~dgl.data.GDELTDataset` 의 경우 프로세스된 데이터가 굉장히 크기 때문에 ``__getitem__(idx)`` 에서 각 데이터 예제들을 처리하는 것이 더 효율적이다.
|
||||
@@ -0,0 +1,30 @@
|
||||
.. _guide_ko-data-pipeline:
|
||||
|
||||
4장: 그래프 데이터 파이프라인
|
||||
======================
|
||||
|
||||
:ref:`(English Version) <guide-data-pipeline>`
|
||||
|
||||
DGL은 :ref:`apidata` 에서 일반적으로 많이 사용되는 그래프 데이터셋을 구현하고 있다. 이것들은 :class:`dgl.data.DGLDataset` 클래스에서 정의하고 있는 표준 파이프라인을 따른다. DGL은 :class:`dgl.data.DGLDataset` 의 서브클래스로 그래프 데이터 프로세싱하는 것을 강하게 권장한다. 이는 파이프라인이 그래프 데이터를 로딩하고, 처리하고, 저장하는데 대한 간단하고 깔끔한 방법을 제공하기 때문이다.
|
||||
|
||||
로드맵
|
||||
----
|
||||
|
||||
이 장은 커스텀 DGL-Dataset를 만드는 방법을 소개한다. 이를 위해 다음 절들에서 파이프라인이 어떻게 동작하는지 설명하고, 각 파이프라인의 컴포넌트를 구현하는 방법을 보여준다.
|
||||
|
||||
* :ref:`guide_ko-data-pipeline-dataset`
|
||||
* :ref:`guide_ko-data-pipeline-download`
|
||||
* :ref:`guide_ko-data-pipeline-process`
|
||||
* :ref:`guide_ko-data-pipeline-savenload`
|
||||
* :ref:`guide_ko-data-pipeline-loadogb`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
data-dataset
|
||||
data-download
|
||||
data-process
|
||||
data-savenload
|
||||
data-loadogb
|
||||
@@ -0,0 +1,185 @@
|
||||
.. _guide_ko-distributed-apis:
|
||||
|
||||
7.2 분산 APIs
|
||||
--------------------
|
||||
|
||||
:ref:`(English Version) <guide-distributed-apis>`
|
||||
|
||||
이 절은 학습 스크립트에 사용할 분산 API들을 다룬다. DGL은 초기화, 분산 샘플링, 그리고 워크로드 분할(split)을 위한 세가지 분산 데이터 구조와 다양한 API들을 제공한다. 분산 학습/추론에 사용되는 세가지 분산 자료 구조는 분산 그래프를 위한 :class:`~dgl.distributed.DistGraph` , 분산 텐서를 위한 :class:`~dgl.distributed.DistTensor` , 그리고 분산 learnable 임베딩을 위한 :class:`~dgl.distributed.DistEmbedding` 이다.
|
||||
|
||||
DGL 분산 모듈 초기화
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
:func:`~dgl.distributed.initialize` 은 분산 모듈을 초기화한다. 학습 스크립트가 학습 모드로 수행되면, 이 API는 DGL 서버들간의 연결을 만들고, 샘플러 프로세스들을 생성한다; 스크립트가 서버 모드로 실행되면, 이 API는 서버 코드를 실행하고 절대로 리턴되지 않는다. 이 API는 어떤 DGL 분산 API들 보다 먼저 호출되어야 한다. PyTorch와 함께 사용될 때, :func:`~dgl.distributed.initialize` 는 ``torch.distributed.init_process_group`` 전에 호출되어야 한다. 일반적으로 초기화 API들은 다음 순서로 실행된다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
dgl.distributed.initialize('ip_config.txt')
|
||||
th.distributed.init_process_group(backend='gloo')
|
||||
|
||||
Distributed 그래프
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` 는 클러스터에서 그래프 구조와 노드/에지 피쳐들을 접근하기 위한 Python 클래스이다. 각 컴퓨터는 단 하나의 파티션을 담당한다. 이 클래스는 파티션 데이터(그 파티션의 그래프 구조, 노드 데이터와 에지 데이터)를 로드하고, 클러스터의 모든 트레이너들이 접근할 수 있도록 만들어 준다. :class:`~dgl.distributed.DistGraph` 는 데이터 접근을 위한 :class:`~dgl.DGLGraph` API들의 작은 서브셋을 지원한다.
|
||||
|
||||
**Note**: :class:`~dgl.distributed.DistGraph` 는 현재 한 개의 노드 타입과 한 개의 에지 타입만을 지원한다.
|
||||
|
||||
분산 모드 vs. 단독(standalone) 모드
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` 는 두가지 모드로 실행된다: 분산 모드와 단독 모드. 사용자가 학습 스크립트를 Python 명령행이나 Jupyter notebook에서 실행하면, 단독 모드로 수행된다. 즉, 모든 계산이 단일 프로세스에서 수행되고, 다른 어떤 프로세스들과의 통신이 없다. 따라서, 단독 모드에서는 입력 그래프가 한 개의 파티션이다. 이 모드는 주로 개발 및 테스트를 위해서 사용된다 (즉, Jupyter notebook에서 코드를 개발하고 수행할 때). 학습 스크립트가 launch 스크립트를 사용해서 실행되면 (launch 스크립트 섹션 참조), :class:`~dgl.distributed.DistGraph` 가 분산 모드로 동작한다. Launch 툴은 자동으로 (노드/에지 피쳐 접근 및 그래프 샘플링을 하는) 서버들을 구동하고, 클러스터의 각 컴퓨터에 파티션 데이터를 자동으로 로드한다. :class:`~dgl.distributed.DistGraph` 는 클러스터의 서버들과 네트워크를 통해서 연결한다.
|
||||
|
||||
DistGraph 생성
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
분산 모드에서는, :class:`~dgl.distributed.DistGraph` 를 생성할 때 파티션에서 사용된 그래프 이름이 필요하다. 그래프 이름은 클러스터에서 로드될 그래프를 지정한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name')
|
||||
|
||||
단독 모드로 수행될 때, 로컬 머신의 그래프 데이터를 로드한다. 따라서, 사용자는 입력 그래프에 대한 모든 정보를 담고 있는 파티션 설정 파일을 제공해야 한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name', part_config='data/graph_name.json')
|
||||
|
||||
**Note**: DGL의 현재 구현은 `DistGraph` 객체를 한 개만 만들 수 있다. `DistGraph` 를 없애고 새로운 것을 다시 만드는 것은 정의되어 있지 않다.
|
||||
|
||||
그래프 구조 접근
|
||||
^^^^^^^^^^^^
|
||||
|
||||
:class:`~dgl.distributed.DistGraph` 는 그래프 구조 접근을 위한 적은 수의 API들을 갖고 있다. 현재 대부분 API들은 노드 및 에지 수와 같은 그래프 정보를 제공한다. DistGraph의 주요 사용 케이스는 미니-배치 학습을 지원하기 위한 샘플링 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)
|
||||
|
||||
분산 텐서(Distributed Tensor)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
앞에서 언급했듯이, DGL은 노드/에치 피쳐들을 샤드(shard)해서, 머신들의 클러스터에 이것들을 저장한다. DGL은 클러스터에서 파티션된 노드/에지 피쳐들을 접근하기 위해서 tensor-like 인터패이스를 갖는 분산 텐서를 제공한다. 분산 세팅에서 DGL은 덴스 노드/에지 피쳐들만 지원한다.
|
||||
|
||||
:class:`~dgl.distributed.DistTensor` 는 파티션되어 여러 머신들에 저장되어 있는 덴스 텐서들을 관리한다. 지금은 부산 텐서는 그래프의 노드 또는 에지와 연결되어 있어야만 한다. 다르게 말하자면, `DistTensor` 의 행 개수는 그래프의 노드 개수 또는 에지의 개수과 같아야만 한다. 아래 코드는 분산 텐서를 생성하고 있다. `shape` 과 `dtype` 뿐만아니라, 유일한 텐서 이름을 지정할 수 있다. 사용자가 영속적인 분산 텐서를 참고하고자 할 경우 이 이름은 유용하다 (즉, :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` 는 적은 수의 함수들을 제공한다. 이는 일반 텐서가 `shape` 또는 `dtype` 과 같은 메타데이터를 접근하는 것과 같은 API들이다. :class:`~dgl.distributed.DistTensor` 는 인덱스를 사용한 읽기와 쓰기를 지원하지만, `sum` 또는 `mean` 과 같은 연산 오퍼레이터는 지원하지 않는다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
data = g.ndata['feat'][[1, 2, 3]]
|
||||
print(data)
|
||||
g.ndata['feat'][[3, 4, 5]] = data
|
||||
|
||||
**Note**: 현재 DGL은 한 머신이 여러 서버들을 수행할 때, 다중의 서버들이 동시에 쓰기를 동시에 수행하는 경우에 대한 보호를 지원하지 않는다. 이 경우 데이터 깨짐(data corruption)이 발생할 수 있다. 같은 행의 데이터에 동시 쓰기를 방지하는 방법 중에 하나로 한 머신에서 한 개의 서버 프로세스만 실행하는 것이다.
|
||||
|
||||
분산 DistEmbedding
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL은 노드 임베딩들을 필요로 하는 변환 모델(transductive models)을 지원하기 위해서 :class:`~dgl.distributed.DistEmbedding` 를 제공한다. 분산 임베딩을 생성하는 것은 분산 텐서를 생성하는 것과 비슷하다.
|
||||
|
||||
.. 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)
|
||||
|
||||
내부적으로는 분산 임배딩은 분산 텐서를 사용해서 만들어진다. 따라서, 분산 텐서와 비슷하게 동작한다. 예를 들어, 임베딩이 만들어지면, 그것들은 클러스터의 여러 머신들에 나눠져서(shard) 저장된다. 이는 이름을 통해서 고유하게 식별될 수 있다.
|
||||
|
||||
**Note**: 초기화 함수가 서버 프로세스에서 호출된다. 따라서, :class:`~dgl.distributed.initialize` 전에 선언되야 한다.
|
||||
|
||||
임배딩은 모델의 일부이기 때문에, 미니배치 학습을 위해서 이를 optimizer에 붙여줘야 한다. 현재는, DGL은 sparse Adagrad optimizer, :class:`~dgl.distributed.SparseAdagrad` 를 지원한다 (DGL은 sparse 임베딩을 위핸 더 많은 optimizer들을 추가할 예정이다). 사용자는 모델로 부터 모든 분산 임베딩을 수집하고, 이를 sparse optimizer에 전달해야 한다. 만약 모델이 노드 임베딩과 정상적인 dense 모델 파라메터들을 갖고, 사용자가 임베딩들에 sparse 업데이트를 수행하고 싶은 경우, optimizer 두 개를 만들어야 한다. 하나는 노드 임베딩을 위한 것이고, 다른 하나는 dense model 파라메터들을 위한 것이다. 다음 코드를 보자.
|
||||
|
||||
.. 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 모듈이 아니다. 따라서, PyTorch nn 모듈의 파라메터들을 통해서 접근할 수 없다.
|
||||
|
||||
분산 샘플링
|
||||
~~~~~~~~
|
||||
|
||||
DGL은 미니-배치를 생성하기 위해 노드 및 에지 샘플링을 하는 두 수준의 API를 제공한다 (미니-배치 학습 섹션 참조). Low-level API는 노드들의 레이어가 어떻게 샘플링될지를 명시적으로 정의하는 코드를 직접 작성해야한다 (예를 들면, :func:`dgl.sampling.sample_neighbors` 사용해서). High-level API는 노드 분류 및 링크 예측(예, :class:`~dgl.dataloading.pytorch.NodeDataLoader` 와
|
||||
:class:`~dgl.dataloading.pytorch.EdgeDataLoader`) 에 사용되는 몇 가지 유명한 샘플링 알고리즘을 구현하고 있다.
|
||||
|
||||
분산 샘플링 모듈도 같은 디자인을 따르고 있고, 두 level의 샘플링 API를 제공한다. Low-level 샘플링 API의 경우, :class:`~dgl.distributed.DistGraph` 에 대한 분산 이웃 샘플링을 위해 :func:`~dgl.distributed.sample_neighbors` 가 있다. 또한, DGL은 분산 샘플링을 위해 분산 데이터 로더, :class:`~dgl.distributed.DistDataLoader` 를 제공한다. 분산 DataLoader는 PyTorch DataLoader와 같은 인터페이스를 갖는데, 다른 점은 사용자가 데이터 로더를 생성할 때 worker 프로세스의 개수를 지정할 수 없다는 점이다. Worker 프로세스들은 :func:`dgl.distributed.initialize` 에서 만들어진다.
|
||||
|
||||
**Note**: :class:`~dgl.distributed.DistGraph` 에 :func:`dgl.distributed.sample_neighbors` 를 실행할 때, 샘플러는 다중의 worker 프로세스를 갖는 PyTorch DataLoader에서 실행될 수 없다. 주요 이유는 PyTorch DataLoader는 매 epoch 마다 새로운 샘플링 worker 프로세스는 생성하는데, 이는 :class:`~dgl.distributed.DistGraph` 객체들을 여러번 생성하고 삭제하게하기 때문이다.
|
||||
|
||||
Low-level 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:
|
||||
...
|
||||
|
||||
동일한 high-level 샘플링 API들(:class:`~dgl.dataloading.pytorch.NodeDataLoader` 와 :class:`~dgl.dataloading.pytorch.EdgeDataLoader` )이 :class:`~dgl.DGLGraph` 와 :class:`~dgl.distributed.DistGraph` 에 대해서 동작한다. :class:`~dgl.dataloading.pytorch.NodeDataLoader` 과 :class:`~dgl.dataloading.pytorch.EdgeDataLoader` 를 사용할 때, 분산 샘플링 코드는 싱글-프로세스 샘플링 코드와 정확하게 같다.
|
||||
|
||||
.. 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:
|
||||
...
|
||||
|
||||
|
||||
워크로드 나누기(Split workloads)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
모델을 학습하기 위해서, 사용자는 우선 데이터를 학습, 검증 그리고 테스트 셋으로 나눠야한다. 분산 학습에서는, 이 단계가 보통은 그래프를 파터션하기 위해 :func:`dgl.distributed.partition_graph` 를 호출하기 전에 일어난다. 우리는 데이터 split를 노드 데이 또는 에지 데이터로서 boolean array들에 저장하는 것을 권장한다. 노드 분류 테스크의 경우에 이 boolean array들의 길이는 그래프의 노드의 개수와 같고, 각 원소들은 노드가 학습/검증/테스트 셋에 속하는지를 지정한다. 링크 예측 테스크에도 비슷한 boolean array들을 사용해야 한다. :func:`dgl.distributed.partition_graph` 는 그래프 파티션 결과에 따라서 이 boolean array들을 나누고, 이를 그래프 파타션과 함께 저장한다.
|
||||
|
||||
분산 학습을 수행하는 동안에 사용자는 학습 노드들/에지들을 각 트레이너에게 할당해야 한다. 비슷하게, 검증 및 테스트 셋도 같은 방법으로 나눠야만 한다. DGL은 분산학습이 수행될 때 학습, 검증, 테스트 셋을 나누는 :func:`~dgl.distributed.node_split` 와 :func:`~dgl.distributed.edge_split` 를 제공한다. 이 두 함수는 그래프 파티셔닝 전에 생성된 boolean array들을 입력으로 받고, 그것들을 나누고 나눠진 부분을 로컬 트레이너에게 리턴한다. 기본 설정으로는 모든 부분들이 같은 개수의 노드와 에지를 갖도록 해준다. 이는 각 트레이너가 같은 크기의 미니-배치들을 갖는다고 가정하는 synchronous SDG에서 중요하다.
|
||||
|
||||
아래 예제는 학습 셋을 나누고, 노들의 서브셋을 로컬 프로세스에 리턴한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
train_nids = dgl.distributed.node_split(g.ndata['train_mask'])
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
.. _guide_ko-distributed-hetero:
|
||||
|
||||
7.3 분산 heterogeneous 그래프 학습하기
|
||||
---------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-distributed-hetero>`
|
||||
|
||||
DGL v0.6.0은 heterogeneous 그래프들을 위한 분산 학습을 실험적으로 지원한다. DGL에서 heterogeneous 그래프의 노드와 에지는 그 노드 타입 및 에지 타입에서 고유한 ID를 갖는다. DGL은 노드/에지 타입과 타입별 ID의 tuple을 사용해서 노드 및 에지를 지정한다. 분산 학습에서는 노드/에지 타입과 타입별 ID의 tuple과 더불어서 노드 또는 에지는 homogeneous ID를 통해서 지정될 수 있다. Homogeneous ID는 노드 타입이나 에지 타입과 관련없이 고유하다. DGL은 같은 타입의 모든 노드들이 연속된 homogeneous ID값들을 갖도록 노드와 에지를 정렬한다.
|
||||
|
||||
아래 그림은 homegeneous ID 할당을 보여주는 heterogeneous 그래프의 adjacency matrix이다. 여기서 그래프틑 두가지 노드 타입( `T0` 와 `T1` )을, 네가지 에지 타입(`R0` , `R1` , `R2` , `R3` )를 갖는다. 그래프는 총 400개의 노드를 갖고, 각 타입은 200개 노드를 갖는다. `T0` 의 노드들은 [0,200)의 ID를 갖고, `T1` 의 노드들은 [200, 400)의 ID 값을 갖는다. 여기서 만약 tuple을 사용해서 노드를 구분한다면, `T0` 의 노드들은 (T0, type-wise ID)로 지정될 수 있다. 여기서 type-wise ID는 [0,200)에 속한다; `T1` 의 노드들은 (T1, type-wise ID)으로 지정되고, type-wise ID는 [0, 200)에 속한다.
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/hetero/heterograph_ids.png
|
||||
:alt: Imgur
|
||||
|
||||
7.3.1 분산 그래프 데이터 접근하기
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
분산 학습을 위해 :class:`~dgl.distributed.DistGraph` 은 :class:`~dgl.DGLGraph` 에서 heterogeneous 그래프 API를 지원한다. 아래 코드는 `T0` 의 노드 데이터를 type-wise 노드 ID를 사용해서 얻는 것을 보여준다. :class:`~dgl.DGLGraph` 의 데이터를 접근할 때, 사용자는 type-wise ID와 연관된 노드 타입 또는 에지 타입을 사용해야 한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
g = dgl.distributed.DistGraph('graph_name', part_config='data/graph_name.json')
|
||||
feat = g.nodes['T0'].data['feat'][type_wise_ids]
|
||||
|
||||
사용자는 특정 노드 타입 또는 에지 타입에 대한 분산 텐서 및 분산 임베딩을 생성할 수 있다. 분산 텐서들과 분산 임베딩들은 여러 머신에 나눠져서 저장된다. 만들 때는 :class:`~dgl.distributed.PartitionPolicy` 로 파티션을 어떻게 할지를 명시해야 한다. 기본 설정으로 DGL은 첫 차원 값의 크기를 기반으로 적절한 파티션 정책을 선택한다. 하지만, 다중 노드 타입 또는 에지 타입이 같은 수의 노드 또는 에지를 갖는 다면, DGL은 파티션 정책을 자동으로 결정할 수 없고, 사용자는 직접 파티션 정책을 지정해야 한다. 아래 코드는 노드 타입 `T0` 의 분산 텐서를 `T0` 를 위한 파티션 정책을 사용해서 생성하고, 이를 `T0` 의 노드 데이터로 저장한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g.nodes['T0'].data['feat1'] = dgl.distributed.DistTensor((g.num_nodes('T0'), 1), th.float32, 'feat1',
|
||||
part_policy=g.get_node_partition_policy('T0'))
|
||||
|
||||
분산 텐서 및 분산 임베딩을 만들기 위한 파티션 정책은 heterogeneous 그래프가 그래프 서버에 로드될 때 초기화된다. 사용자는 새로운 파티션 정책을 실행 중에 생성할 수 없다. 따라서, 사용자는 노드 타입 이나 에지 타입에 대한 분산 텐서 또는 분산 임베딩 만을 만들 수 있다.
|
||||
|
||||
7.3.2 분산 샘플링
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
DGL v0.6은 분산 샘플링에서 homogeneous ID를 사용한다. **Note**: 이는 앞으로 릴리즈에서 바뀔 수도 있다. DGL은 homogeneous ID와 type-wise ID 간에 노드 ID와 에지 ID를 변환하는 네 개의 API를 제공한다.
|
||||
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_per_ntype` : homogeneous 노드 ID를 type-wise ID와 노드 타입 ID로 변환한다.
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_per_etype` : homogeneous 에지 ID를 type-wise ID와 에지 타입 ID로 변환한다.
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_homo_nid` : type-wise ID와 노드 타입을 homogeneous 노드 ID로 변환한다.
|
||||
* :func:`~dgl.distributed.GraphPartitionBook.map_to_homo_eid` : type-wise ID와 에지 타입을 homogeneous 에지 ID로 변환한다.
|
||||
|
||||
다음 예제는 `paper` 라는 노드 타입을 갖는 heterogeneous 그래프로부터 :func:`~dgl.distributed.sample_neighbors` 를 사용해서 서브 그래프를 샘플링한다. 이는 우선 type-wise 노드 ID들을 homogeneous 노드 ID들로 변환한다. 시드 노드들로 서브 그래프를 샘플링 한 다음, homogeneous 노드 ID들과 에지 ID들을 type-wise ID들로 바꾸고, 타입 ID를 노드 데이터와 에지 데이터에 저장한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
gpb = g.get_partition_book()
|
||||
# We need to map the type-wise node IDs to homogeneous IDs.
|
||||
cur = gpb.map_to_homo_nid(seeds, 'paper')
|
||||
# For a heterogeneous input graph, the returned frontier is stored in
|
||||
# the homogeneous graph format.
|
||||
frontier = dgl.distributed.sample_neighbors(g, cur, fanout, replace=False)
|
||||
block = dgl.to_block(frontier, cur)
|
||||
cur = block.srcdata[dgl.NID]
|
||||
|
||||
block.edata[dgl.EID] = frontier.edata[dgl.EID]
|
||||
# Map the homogeneous edge Ids to their edge type.
|
||||
block.edata[dgl.ETYPE], block.edata[dgl.EID] = gpb.map_to_per_etype(block.edata[dgl.EID])
|
||||
# Map the homogeneous node Ids to their node types and per-type Ids.
|
||||
block.srcdata[dgl.NTYPE], block.srcdata[dgl.NID] = gpb.map_to_per_ntype(block.srcdata[dgl.NID])
|
||||
block.dstdata[dgl.NTYPE], block.dstdata[dgl.NID] = gpb.map_to_per_ntype(block.dstdata[dgl.NID])
|
||||
|
||||
노드/에지 타입 ID를 위해서, 사용자는 노드/에지 타입을 검색할 수 있다. 예를 들어, `g.ntypes[node_type_id]` . 노드/에지 타입들과 type-wise ID들을 사용해서, 사용자는 미니배치 계산을 위해서 `DistGraph` 로부터 노드/에지 데이터를 검색할 수 있다.
|
||||
@@ -0,0 +1,331 @@
|
||||
.. _guide_ko-distributed-preprocessing:
|
||||
|
||||
7.1 분산 학습을 위한 전처리
|
||||
---------------------
|
||||
|
||||
:ref:`(English Version) <guide-distributed-preprocessing>`
|
||||
|
||||
DGL의 분산 학습을 사용하기 위해서는 그래프 데이터에 대한 전처리가 필요하다. 이 전처리는 두 단계로 구성된다: 1) 그래프를 서브 그래프들로 파티션하기, 2) 노드/에지들에 새로운 ID를 부여하기. 상대적으로 작은 그래프들의 경우, DGL이 제공하는 파티셔닝 API :func:`dgl.distributed.partition_graph` 를 사용해서 위 두 단계를 수행할 수 있다. 이 API는 한 컴퓨터에서 수행된다. 따라서, 그래프가 큰 경우, 이 API를 사용하고 싶다면 큰 컴퓨터를 사용해야 한다. 이 API과 더불어, 여기서는 큰 그래프를 컴퓨터들의 클러스터에서 파티션을 하는 솔루션을 소개한다. (7.1.1 절을 보라)
|
||||
|
||||
:func:`dgl.distributed.partition_graph` 는 랜덤 파티션과 `Metis <http://glaros.dtc.umn.edu/gkhome/views/metis>`__ 기반의 파티셔닝을 모두 지원한다. Metis 파티셔닝의 장점은 최소의 에지 컷(edge cut)을 갖는 파티션들을 만들 수 있다는 것이다. 이는 분산 학습 및 추론에서 네트워크 통신을 줄여준다. DGL은 최신 버전의 Metis은 실제(real world)에서 거듭 제곱 법칙의 분포를 갖는 그래프에 최적화되어 있다. 파타셔닝 후, API는 학습시 쉽게 로딩될 수 있는 형태로 파티션된 결과를 만든다.
|
||||
|
||||
기본 설정으로 파티션 API는 분산 학습/추론이 실행될 때 노드/에지를 구별하는 것을 돕기 위해서 입력 그래프의 노드와 에지에 새로운 ID를 부여한다. ID를 할당한 후, 파티션 API은 모든 노드 데이터와 에지 데이터를 섞는다. 파티션된 서브 그래프를 생선한 후, 각 서브 그래프는 ``DGLGraph`` 객체로 저장된다. 섞기전의 원본 노드/에지 ID들은 서브 그래프들의 노드/에지 데이터에 `orig_id` 필드에 저장된다. 서브 그래프의 노드 데이터 `dgl.NID` 와 에지 데이터 `dgl.EID` 는 노드/에지들이 reshuffle 후의 전체 그래프의 새로운 노드/에지 ID를 저장한다. 학습이 실행되는 동안, 사용자는 새로운 노드/에지 ID만을 사용한다.
|
||||
|
||||
파티션된 결과는 출력 디렉토리의 여러 파일로 저장된다. 이는 한개의 JSON 파일을 포함하는데, 파일 이름은 xxx.json 형태이고, xxx는 파티션 API에 사용된 그래프 이름이다. JSON 파일은 모든 파티션 설정들을 갖는다. 먄약 파티션 API가 새로운 ID를 노드와 에지에 할당하지 않은 경우에는, 추가적으로 두 개의 Numpy 파일; `node_map.npy` 와 `edge_map.npy` 를 생성하는데, 이는 노드/에지 ID와 파티션 ID의 매핑을 저장한다. 만약 그래프에 수십억 개의 노드와 에지가 있다면, 두 파일의 Numpy array는 커질 것인다. 그 이유는 그래프의 각 노드 및 에지에 대해서 하나의 엔트리를 갖기 때문이다. 각 파티션에 대한 폴더는 DGL 포멧으로 파티션 데이터를 저장하는 세 개의 파일이 있다. `graph.dgl` 은 파티션의 그래프 구조와 노드 및 에지에 대한 메타 데이터를 저장하고 있고, `node_feats.dgl` 과 `edge_feats.dlg` 은 파티션에 속하는 노드와 에지의 모든 피쳐들을 저장하고 있다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
data_root_dir/
|
||||
|-- xxx.json # partition configuration file in JSON
|
||||
|-- node_map.npy # partition id of each node stored in a numpy array (optional)
|
||||
|-- edge_map.npy # partition id of each edge stored in a numpy array (optional)
|
||||
|-- part0/ # data for partition 0
|
||||
|-- node_feats.dgl # node features stored in binary format
|
||||
|-- edge_feats.dgl # edge features stored in binary format
|
||||
|-- graph.dgl # graph structure of this partition stored in binary format
|
||||
|-- part1/
|
||||
|-- node_feats.dgl
|
||||
|-- edge_feats.dgl
|
||||
|-- graph.dgl
|
||||
|
||||
로드 밸런싱
|
||||
~~~~~~~~
|
||||
|
||||
그래프를 파티셔닝할 때, Metis의 기본 설정은 각 파티션의 노드 수에 대해서 균형을 맞춘다. 그 결과 주어진 테스크에 따라서 최적이지 않은 구성(suboptimal configuration)이 될 수 있다. 예를 들어, semi-supervised 노드 분류의 경우, 트레이너는 로컬 파티션의 레이블이 있는 노들의 서브셋에 대해서 계산을 수행한다. 그래프의 노드들(레이블이 있는 것과 없는 모든 노드)에 균형을 맞추는 파티셔닝은 계산적인 로드(computational node)가 불균형하게 될 수 있다. 각 파티션에 균형잡힌 워크로드를 얻기 위해서 파티션 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`` 설정을 통해서 다른 노드 타입들의 노드들의 in-degree들 사이의 균형을 잡는 것을 지원한다. 이는 다른 타입의 노드들에 부속되는 에지들의 개수에 대한 균형을 만든다.
|
||||
|
||||
**Note**: :func:`dgl.distributed.partition_graph` 에 전달되는 그래프 이름은 중요한 인자이다. 그 그래프 이름은 :class:`dgl.distributed.DistGraph` 이 분산 그래프를 지정하는데 사용된다. 그래프 이름은 알파벳 문자들과 밑줄 기호만으로 구성되어야 한다.
|
||||
|
||||
ID 매핑
|
||||
~~~~~~
|
||||
|
||||
:func:`dgl.distributed.partition_graph` 는 파티셔닝을 하는 과정에서 노드 ID와 에지 ID를 섞고, 노드 데이터와 에지 데이터도 그에 따라서 섞어준다. 학습이 끝나면, 다운스트림 과제를 위해서 계산된 노드 임베딩들을 저장할 필요가 있다. 따라서, 저장된 노드 임베딩을 원본 ID에 따라서 다시 섞어야한다.
|
||||
|
||||
`return_mapping=True` 인 경우, :func:`dgl.distributed.partition_graph` 는 섞인 노드/에지 ID와 그것들의 원본 ID 사이의 매핑을 리턴한다. Homogeneous 그래프의 경우, 두 벡터를 리턴한다. 첫번째 벡터는 모든 섞인 노드 ID와 그것의 원본 ID 메핑을, 두번째 벡터는 모든 섞인 에지 ID와 그것의 원본 ID 매핑이다. Heterogeneous 그래프의 경우에는 벡터들의 dictionary 두 개가 리턴된다. 첫번째 dictionary는 각 노드 타입에 대한 매핑을, 두번째 dictionary는 각 에지 타입에 대한 매핑이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_map, edge_map = dgl.distributed.partition_graph(g, 'graph_name', 4, '/tmp/test',
|
||||
balance_ntypes=g.ndata['train_mask'],
|
||||
return_mapping=True)
|
||||
# Let's assume that node_emb is saved from the distributed training.
|
||||
orig_node_emb = th.zeros(node_emb.shape, dtype=node_emb.dtype)
|
||||
orig_node_emb[node_map] = node_emb
|
||||
|
||||
7.1.1 분산 파티셔닝
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
큰 그래프를 위해서 DGL은 `ParMetis <http://glaros.dtc.umn.edu/gkhome/metis/parmetis/overview>`__ 을 사용해서 컴퓨터들의 클러스터에서 그래프를 파티셔닝한다. 이 솔루션은 사용자가 ParMETIS에 맞도록 데이터를 준비하고, ParMETIS에 의해 만들어질 파티션들을 위한 :class:`dgl.DGLGraph` 를 만들기 위해서 DGL 스크립트 `tools/convert_partition.py` 를 사용해야 한다.
|
||||
|
||||
**Note**: `convert_partition.py` 는 `pyarrow` 패키지를 사용해서 csv 파일을 로드안다. `pyarrow` 설치하자.
|
||||
|
||||
ParMETIS 설치
|
||||
~~~~~~~~~~~~
|
||||
|
||||
ParMETIS는 METIS와 GKLib을 필요로 한다. GKLib 컴파일과 설치는 `here <https://github.com/KarypisLab/GKlib>`__ 에 있는 설명을 참고하자. METIS 컴파일과 설치는 아래 설명을 따라 GIT에서 METIRS를 클론하고 int64 지원을 활성화해서 컴파일한다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
git clone https://github.com/KarypisLab/METIS.git
|
||||
make config shared=1 cc=gcc prefix=~/local i64=1
|
||||
make install
|
||||
|
||||
여기서부터는 PartMETIS를 직접 컴파일하고 설치하는 것이 필요하다. 아래 명령을 사용해서 ParMETIS의 DGL 브랜치를 클론한다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
git clone --branch dgl https://github.com/KarypisLab/ParMETIS.git
|
||||
|
||||
그리고, ParMETIS를 컴파일하고 설치한다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
make config cc=mpicc prefix=~/local
|
||||
make install
|
||||
|
||||
ParMETIS를 실행하기 전에, 두 환경 변수들, `PATH`와 `LD_LIBRARY_PATH`을 설정해야 한다:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
export PATH=$PATH:$HOME/local/bin
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/local/lib/
|
||||
|
||||
ParMETIS를 위한 입력 포멧
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
ParMETIS의 입력 그래프는 다음 이름들을 사용해서 세 개의 파일들에 저장된다: `xxx_nodes.txt` , `xxx_edges.txt` 와 `xxx_stats.txt`. 여기서 `xxx` 는 그래프 이름이다.
|
||||
|
||||
`xxx_nodes.txt` 의 각 행은 다음 형식으로 노드에 대한 정보를 담고 있다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<node_type> <weight1> ... <orig_type_node_id> <attributes>
|
||||
|
||||
모든 필드들은 공백 문자로 구분된다.
|
||||
|
||||
* `<node_type>` 은 정수 값이다. Homogeneous 그래프에서는 항상 0이고, heterogenous 그래프에서는 그 값이 각 노드의 타입을 의미한다.
|
||||
* `<weight1>`, `<weight2>`, 등은 정수 값들인데, ParMETIS가 그래프 파티션들의 균형을 맞출 때 노드 가중치로 사용하는 값들이다. 사용자가 노드 가중치를 명시하지 않는 경우, ParMETIS는 각 파티션의 노드 수에 대한 균형을 고려해서 파티션을 나눈다 (좋은 학습 속도를 얻기 위해서는 그래프 파티션들의 균헝을 맞추는 것이 중요하다). 하지만, 이 기본 전략은 많은 use case들에 충분하지 않을 수 있다. 예를 들어, heterogeneous 그래프의 경우, 우리는 모든 파티션들이 각 노드 타입별로 비슷한 개수의 노드들을 갖도록 그래프에 대한 파티션을 나누고 싶다. 아래 토이 예제는 노드 가중치를 사용해서 다른 테입들의 노드 개수의 균형을 맞추것을 어떻게 하는지 보여준다.
|
||||
* `<orig_type_node_id>` 은 노드 타입에서의 노드 ID를 표현하는 정수 값이다. DGL에서 각 타입의 노드들은 0부터 시작하는 ID가 부여된다. Homogeneous 그래프에서 이 필드는 노드 ID의 값도 동일하다.
|
||||
* `<attributes>` 는 선택적인 필드들이다. 이는 임의의 값을 저장하는데 사용될 수 있으며, ParMETIS는 이 필드들을 사용하지 않는다. 잠재적으로는 homogenous 그래프들의 경우 노드 피쳐들과 에지 피쳐들을 이 필드에 저장할 수 있다.
|
||||
* 행(row) ID는 그래프의 *homogeneous* ID를 의미한다 (모든 노드에 고유한 ID가 할당된다). 같은 타입의 모든 노드들에 ID는 연속된 값으로 부여된다. 즉, 같은 타입의 노드들은 `xxx_notes.txt` 파일에 함께 저장되어야 한다.
|
||||
|
||||
다음은 두 노드 타입을 갖는 heterogenous 그래프의 노트 파일 예이다. 노드 타입 0은 세 개의 노드를 갖고 있고, 노드 타입 1은 네 개의 노드들을 갖는다. 두 노드 가중치를 사용해서 ParMETIS느 노드 타입 0에 속한 노드 개수와 노드 타입 1에 속한 노드 개수가 대략 같도록 파티션 나눈다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
0 1 0 0
|
||||
0 1 0 1
|
||||
0 1 0 2
|
||||
1 0 1 0
|
||||
1 0 1 1
|
||||
1 0 1 2
|
||||
1 0 1 3
|
||||
|
||||
비슷하게, `xxx_edges.txt` 의 각 행은 아래 형식으로 에지에 대한 정보를 저장한다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<src_id> <dst_id> <type_edge_id> <edge_type> <attributes>
|
||||
|
||||
모든 필드들은 공백 문자로 구분된다.
|
||||
|
||||
* `<src_id>` 는 소스 노드의 *homogeneous* ID이다.
|
||||
* `<dst_id>` 는 목적지 노드의 *homogeneous* ID이다.
|
||||
* `<type_edge_id>` 는 에지 타입에 대한 에지 ID이다.
|
||||
* `<attributes>` 는 선택적인 필드들이다. 임의의 값을 저장하는데 사용할 수 있는데, ParMETIS는 이 필드를 사용하지 않는다.
|
||||
|
||||
**Note**: 에지 파일에 중복된 에지나 셀프-룹을 갖는 에지가 없어야 한다.
|
||||
|
||||
`xxx_stats.txt` 는 그래프에 대한 기본적인 통계들을 저장한다. 이 파일은 공백으로 구분되는 세 필드들로 구성된 단 한 줄만 갖는다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<num_nodes> <num_edges> <num_node_weights>
|
||||
|
||||
* `num_nodes` 는 노드 타입을 상관하지 않고 전체 노드 수를 저장한다.
|
||||
* `num_edges` 는 에지 타입을 상관하지 않고 전체 에지 수를 저장한다.
|
||||
* `num_node_weights` 는 노드 파일의 노드 가중치 수를 저장한다.
|
||||
|
||||
ParMETIS 실행하기 및 결과 포멧들
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
ParMETIS는 `pm_dglpart` 명령이 실행된 머신에서 세 파일들에 저장된 그래프를 로드하고, 클러스터의 모든 머신에 데이터를 분산하고, ParMETIS를 실행해서 그래프의 파티션을 나누는 명령 `pm_dglpart` 을 포함하고 있다. 이 명령의 수행이 완료되면, 각 파타션에 대해서 세 개의 파일이 생성된다: `p<part_id>-xxx_nodes.txt`, `p<part_id>-xxx_edges.txt`, `p<part_id>-xxx_stats.txt`
|
||||
|
||||
**Note**: ParMETIS는 파티셔닝을 수행하면서 노드들에 ID를 재할당한다. ID 재할당이 끝나면, 한 파티션의 노드들은 연속된 ID값을 갖는다; 더 나아가, 같은 타입의 노드들은 연속된 ID들을 부여 받는다.
|
||||
|
||||
`p<part_id>-xxx_nodes.txt` 는 파티션의 노드 데이터를 저장한다. 각 행은 한 노드에 대한 다음 정보들을 담고 있다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<node_id> <node_type> <weight1> ... <orig_type_node_id> <attributes>
|
||||
|
||||
* `<node_id>` 는 ID 재할당 후의 *homogeneous* 노드 ID이다.
|
||||
* `<node_type>` 는 노드 타입이다.
|
||||
* `<weight1>` 는 ParMETIS가 사용하는 노드 가중치이다.
|
||||
* `<orig_type_node_id>` 는 입력 heterogeneous 그래프의 특정 노드 티입에 대한 원본 노드 ID이다.
|
||||
* `<attributes>` 는 선택적인 필드들로 입력 노드 파일에서 임의의 값을 갖는다.
|
||||
|
||||
`p<part_id>-xxx_edges.txt` 는 파티션의 에지 데이터를 저장한다. 각 행은 한 에지에 대한 다음 정보를 담고 있다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
<src_id> <dst_id> <orig_src_id> <orig_dst_id> <orig_type_edge_id> <edge_type> <attributes>
|
||||
|
||||
* `<src_id>` 는 ID 재할당 후의 소스 노드의 *homogeneous* ID이다.
|
||||
* `<dst_id>` 는 ID 재할당 후의 목적지 노드의 *homogeneous* ID이다.
|
||||
* `<orig_src_id>` 는 입력 그래프의 소스 노드에 대한 *homogeneous* ID이다.
|
||||
* `<orig_dst_id>` 는 입력 그래프의 목적지 노드에 대한 *homogeneous* ID이다.
|
||||
* `<orig_type_edge_id>` 는 입력 그래프의 특정 에지 타입에 대한 에지 ID이다.
|
||||
* `<edge_type>` 은 에지 타입이다.
|
||||
* `<attributes>` 는 선택적인 필드들로 입력 에지 파일에서 임의의 에지 속성 값을 갖는다.
|
||||
|
||||
`pm_dglpart` 이 실행된 때, 세 입력 파일들(`xxx_nodes.txt`, `xxx_edges.txt`, `xxx_stats.txt`)은 `pm_dglpart` 명령이 실행된 디렉토리와 같은 곳에 있어야 한다. 다음 명령은 네 개의 ParMETIS 프로세스를 실행해서, `xxx` 라는 이름의 그래프를 8개의 파티션으로 나눈다 (각 프로세스는 2개의 파티션을 담당한다).
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
mpirun -np 4 pm_dglpart xxx 2
|
||||
|
||||
ParMETIS 결과들을 DGLGraph로 변환하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL은 `convert_partition.py` 라는 스크립트를 제공한다. 이는 `tool` 디렉토리에 있는데, 파티션 파일들에 있는 데이터를 :class:`dgl.DGLGraph` 객체로 변환하고 파일들에 저장하는 역할을 한다. **Note** `convert_partition.py` 는 단일 머신에서 실행된다. 향후, 우리는 이를 확장해서 여러 머신들에 걸쳐서 데이터를 병렬로 변환하도록 만들 것이다. **Note**: csv 파일로 저장된 데이터를 로딩하기 위해서 `pyarrow` 패키지를 설치하자.
|
||||
|
||||
`convert_partition.py` 는 다음 인자들을 받는다:
|
||||
|
||||
* `--input-dir INPUT_DIR` 는 ParMETIS가 생성한 파티션 파일들이 있는 디렉토리를 지정한다.
|
||||
* `--graph-name GRAPH_NAME` 는 그래프 이름을 지정한다.
|
||||
* `--schema SCHEMA` 는 입력 heterogeneous 그래프의 스키마를 명시하는 파일이다. 스키마 파일은 JSON 파일로서, 노드 타입들과 에지 타입들을 나열하고, 또한 각 노드 타입 및 에지 타입에 대한 homogeneous ID의 범위를 포함한다.
|
||||
* `--num-parts NUM_PARTS` 는 파티션의 개수를 명시한다.
|
||||
* `--num-node-weights NUM_NODE_WEIGHTS` 는 ParMETIS가 파티션들의 균형을 위해서 사용한 노드 가중치의 개수를 지정한다.
|
||||
* `[--workspace WORKSPACE]` 는 선택적인 인자로, 중간 결과들을 저장할 workspace 디렉토리를 지정한다.
|
||||
* `[--node-attr-dtype NODE_ATTR_DTYPE]` 는 선택적인 인자로, 노드 파일들의 나머지 필드인 `<attributes>` 에 저장된 노드 속성들의 데이터 타입을 명시한다.
|
||||
* `[--edge-attr-dtype EDGE_ATTR_DTYPE]` 는 선택적인 인자로, 에지 파일들의 나머지 필드인 `<attributes>` 에 저장된 에지 속성들의 데이터 타입을 명시한다.
|
||||
* `--output OUTPUT` 는 파티션 결과들이 저장될 출력 디렉토리를 지정한다.
|
||||
|
||||
`convert_partition.py` 의 결과 파일들은 다음과 같다:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
data_root_dir/
|
||||
|-- xxx.json # partition configuration file in JSON
|
||||
|-- part0/ # data for partition 0
|
||||
|-- node_feats.dgl # node features stored in binary format (optional)
|
||||
|-- edge_feats.dgl # edge features stored in binary format (optional)
|
||||
|-- graph.dgl # graph structure of this partition stored in binary format
|
||||
|-- part1/
|
||||
|-- node_feats.dgl
|
||||
|-- edge_feats.dgl
|
||||
|-- graph.dgl
|
||||
|
||||
**Note**: 노드 속성 또는 에지 속성의 데이터 타입이 명시된다면, `convert_partition.py` 는 모든 타입의 모든 노드들 및 에지들이 꼭 이 속성들을 갖는다고 가정한다. 따라서, 다른 타입의 노드들이나 에지들이 서로 다른 개수의 속성을 갖는다면, 사용자는 이를 직접 만들어야 한다.
|
||||
|
||||
다음은 `convert_partition.py` 를 위한 OGBN-MAG의 스키마 예제이다. 이는 두 필드를 갖는다: `nid` 와 `eid`. `nid` 안에는, 모든 노드 타입들이 나열되어 있고, 각 노드 타입에 대한 homogeneous ID 범위도 포함되어 있다; `eid` 안에는, 모든 에지 타입들이 나열되어 있고, 각 에지 타입에 대한 homogeneous ID 범위도 포함되어 있다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
{
|
||||
"nid": {
|
||||
"author": [
|
||||
0,
|
||||
1134649
|
||||
],
|
||||
"field_of_study": [
|
||||
1134649,
|
||||
1194614
|
||||
],
|
||||
"institution": [
|
||||
1194614,
|
||||
1203354
|
||||
],
|
||||
"paper": [
|
||||
1203354,
|
||||
1939743
|
||||
]
|
||||
},
|
||||
"eid": {
|
||||
"affiliated_with": [
|
||||
0,
|
||||
1043998
|
||||
],
|
||||
"writes": [
|
||||
1043998,
|
||||
8189658
|
||||
],
|
||||
"rev-has_topic": [
|
||||
8189658,
|
||||
15694736
|
||||
],
|
||||
"rev-affiliated_with": [
|
||||
15694736,
|
||||
16738734
|
||||
],
|
||||
"cites": [
|
||||
16738734,
|
||||
22155005
|
||||
],
|
||||
"has_topic": [
|
||||
22155005,
|
||||
29660083
|
||||
],
|
||||
"rev-cites": [
|
||||
29660083,
|
||||
35076354
|
||||
],
|
||||
"rev-writes": [
|
||||
35076354,
|
||||
42222014
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
아래 코드는 스키마 파일을 만드는 예제이다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
nid_ranges = {}
|
||||
eid_ranges = {}
|
||||
for ntype in hg.ntypes:
|
||||
ntype_id = hg.get_ntype_id(ntype)
|
||||
nid = th.nonzero(g.ndata[dgl.NTYPE] == ntype_id, as_tuple=True)[0]
|
||||
nid_ranges[ntype] = [int(nid[0]), int(nid[-1] + 1)]
|
||||
|
||||
for etype in hg.etypes:
|
||||
etype_id = hg.get_etype_id(etype)
|
||||
eid = th.nonzero(g.edata[dgl.ETYPE] == etype_id, as_tuple=True)[0]
|
||||
eid_ranges[etype] = [int(eid[0]), int(eid[-1] + 1)]
|
||||
with open('mag.json', 'w') as outfile:
|
||||
json.dump({'nid': nid_ranges, 'eid': eid_ranges}, outfile, indent=4)
|
||||
|
||||
Heterogeneous 그래프에 대한 노드/에지 피처들 생성하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
`convert_partition.py` 이 만든 :class:`dgl.DGLGraph` 아웃풋은 heterogeneous 그래프 파티션들을 homogeneous 그래프로 저장한다. 노드 데이터는 `orig_id` 라는 필드를 갖는데, 이는 원본 heterogeneous 그래프의 특정 노드 타입의 노드 ID들을 저장하고, `NTYPE` 의 필드는 노드 타입을 저장한다. 추가로, 이는 `inner_node` 라는 노드 데이터를 저장하는데, 이는 그래프 파티션의 노드가 파티션이 할당되어 있는지 여부를 알려준다. 만약 어떤 노드가 파티션에 할당되었다면, `inner_node` 는 1을 갖고, 반대의 경우에는 0을 갖는다. **Note**: 그래프 파티션은 몇 개의 HALO 노드들을 포함하는데, 이는 다른 파티션에 할당된 것지만, 이 그래프 파티션의 몇 개의 에지와 연결되어 있는 것들이다. 이 정보를 사용해서, 우리는 별도로 각 노드 타입에 대한 노드 피쳐들을 구성할 수 있으며, 이들을 `<node_type>/<feature_name>` 를 키로 갖고 값은 노드 피쳐 벡터인 dictionary에 저장할 수 있다. 아래 코드는 노드 피쳐 dictionary를 구성하는 방법을 보여준다. 텐서들의 dictionary가 만들어지면, 이는 파일에 저장된다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
node_data = {}
|
||||
for ntype in hg.ntypes:
|
||||
local_node_idx = th.logical_and(part.ndata['inner_node'].bool(),
|
||||
part.ndata[dgl.NTYPE] == hg.get_ntype_id(ntype))
|
||||
local_nodes = part.ndata['orig_id'][local_node_idx].numpy()
|
||||
for name in hg.nodes[ntype].data:
|
||||
node_data[ntype + '/' + name] = hg.nodes[ntype].data[name][local_nodes]
|
||||
dgl.data.utils.save_tensors(metadata['part-{}'.format(part_id)]['node_feats'], node_data)
|
||||
|
||||
에지 피쳐도 비슷한 방법으로 구성할 수 있다. 차이점은 :class:`dgl.DGLGraph` 의 모든 에지들이 파티션에 포함된다는 점이다. 그래서, 구성 방법은 더 간단하다.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
edge_data = {}
|
||||
for etype in hg.etypes:
|
||||
local_edges = subg.edata['orig_id'][subg.edata[dgl.ETYPE] == hg.get_etype_id(etype)]
|
||||
for name in hg.edges[etype].data:
|
||||
edge_data[etype + '/' + name] = hg.edges[etype].data[name][local_edges]
|
||||
dgl.data.utils.save_tensors(metadata['part-{}'.format(part_id)]['edge_feats'], edge_data)
|
||||
@@ -0,0 +1,56 @@
|
||||
.. _guide_ko-distributed-tools:
|
||||
|
||||
7.4 분산 학습/추론을 런칭하기 위한 툴들
|
||||
-------------------------------
|
||||
|
||||
: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`` 는 파티션된 데이터가 저장될 workspace 디렉토리 아래 상대 경로를 지정한다.
|
||||
* ``--script_folder`` 는 사용자의 학습 스크립트가 저장될 workspace 디렉토리 아래 상대 경로를 지정한다.
|
||||
|
||||
**Note**: *copy_files.py* 는 IP 설정 파일을 기반으로 파티션을 저장할 머신을 찾는다. 따라서, 같은 IP 설정 파일이 *copy_files.py* 과 *launch.py* 에 사용되어야 한다.
|
||||
|
||||
DGL은 클러스터에서 분산 학습 잡을 시작하기 위해서 *tools/launch.py* 를 제공한다. 이 스크립트는 다음을 가정한다.
|
||||
|
||||
* 파티션된 데이터와 학습 스크립트는 클러스터 또는 클러스터의 모든 머신이 접근 가능한 클로벌 스토리지(예, NFS)로 복사된다.
|
||||
* (런치 스크립트가 실행되는) 마스터 머신은 다른 모든 머신에 패스워드 없이(passwordless) 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 설정 파일, 파티션 설정 파일 그리고 그래프 파티션들이 저장되는 위치이다. 파일들의 모든 경로들은 workspace의 상대 경로로 지정되어야 한다.
|
||||
|
||||
런치 스크립트는 한 머신에서 지정된 수의 학습 잡(``--num_trainers`` )을 생성한다. 또한, 사용자는 각 트레이너에 대한 샘플러 프로세스의 개수(``--num_samplers``)를 정해야 한다. 샘플러 프로세스의 개수는 :func:`~dgl.distributed.initialize` 에서 명시된 worker 프로세스의 개수과 같아야 한다.
|
||||
@@ -0,0 +1,83 @@
|
||||
.. _guide_ko-distributed:
|
||||
|
||||
7장: 분산 학습
|
||||
===========
|
||||
|
||||
:ref:`(English Version) <guide-distributed>`
|
||||
|
||||
DGL은 데이터와 연산을 컴퓨터 리소스들의 집합들에 분산하는 완전한 분산 방식을 채택하고 있다. 이 절에서는 클러스터 설정(컴퓨터들의 그룹)을 가정하고 있다. DGL은 그래프를 서브 그래프들로 나누고, 클러스터의 각 컴퓨터는 한개의 서브 그래프 (또는 파티션)에 대해 책임을 진다. DGL은 클러스터이 모든 컴퓨터에서 동일한 학습 스크립트를 실행해서 계산을 병렬화시키고, trainer에게 파티션된 데이터를 제공하기 위해서 같은 컴퓨터에서 서버들을 실행한다.
|
||||
|
||||
학습 스크립트를 위해서 DGL은 미니-배치 학습과 비슷한 분산 API를 제공한다. 이는 단일 컴퓨터에서 미니-배치 학습을 수행하는 코드를 아주 조금만 수정하면 되게 해준다. 아래 코드는 GraphSAGE를 분산 형태로 학습하는 예제이다. 유일한 코드 변경은 4-7 라인이다: 1) DGL의 분산 모듈 초기화하기, 2) 분산 그래프 객체 생성하기, 3) 학습 셋을 나누고 로컬 프로세스를 위해서 노드들을 계산하기. 샘플러 생성, 모델 정의, 학습 룹과 같은 나머지 코드는 :ref:`mini-batch training <guide_ko-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)
|
||||
|
||||
|
||||
# Create sampler
|
||||
sampler = NeighborSampler(g, [10,25],
|
||||
dgl.distributed.sample_neighbors,
|
||||
device)
|
||||
|
||||
dataloader = DistDataLoader(
|
||||
dataset=train_nid.numpy(),
|
||||
batch_size=batch_size,
|
||||
collate_fn=sampler.sample_blocks,
|
||||
shuffle=True,
|
||||
drop_last=False)
|
||||
|
||||
# Define model and optimizer
|
||||
model = SAGE(in_feats, num_hidden, n_classes, num_layers, F.relu, dropout)
|
||||
model = th.nn.parallel.DistributedDataParallel(model)
|
||||
loss_fcn = nn.CrossEntropyLoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=args.lr)
|
||||
|
||||
# training loop
|
||||
for epoch in range(args.num_epochs):
|
||||
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 백앤드만 지원한다.
|
||||
|
||||
DGL은 분산 학습을 지원하기 위해서 몇 가지 분산 컴포넌트를 구현하고 있다. 아래 그림은 컴포넌트들과 그것들의 인터엑션을 보여준다.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/distributed.png
|
||||
:alt: Imgur
|
||||
|
||||
특히, DGL의 분산 학습은 3가지 종류의 프로세스들을 갖는다: *서버*, *샘플러*, 그리고 *트레이너*
|
||||
|
||||
* 서버 프로세스는 그래프 파티션(그래프 구조와 노드/에지 피처를 포함)을 저장하고 있는 각 컴퓨터에서 실행된다. 이 서버들은 함께 작동하면서 그래프 데이터를 트레이너에게 제공한다. 한 컴퓨터는 여러 서버 프로세스들을 동시에 수행하면서 연산과 네트워크 통신을 병렬화 한다.
|
||||
* 샘플러 프로세스들은 서버들과 상호작용을 하면서, 학습에 사용될 미니-배치를 만들기 위해서 노드와 에지를 샘플링한다.
|
||||
* 트레이너들은 서버들과 상호작용을 하기 위한 여러 클래스를 포함하고 있다. 파티션된 그래프 데이터를 접근하기 위한 :class:`~dgl.distributed.DistGraph` , 노드/에지의 피쳐/임베딩을 접근하기 위한 :class:`~dgl.distributed.DistEmbedding` 와 :class:`~dgl.distributed.DistTensor` 를 갖는다. 미니-배치를 얻기 위해서 샘플러와 상호작용을 하는 :class:`~dgl.distributed.dist_dataloader.DistDataLoader` 가 있다.
|
||||
|
||||
분산 컴포넌드들을 염두해두고, 이 절의 나머지에서는 다음과 같은 분산 컴포넌트들을 다룬다.
|
||||
|
||||
* :ref:`guide_ko-distributed-preprocessing`
|
||||
* :ref:`guide_ko-distributed-apis`
|
||||
* :ref:`guide_ko-distributed-hetero`
|
||||
* :ref:`guide_ko-distributed-tools`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
distributed-preprocessing
|
||||
distributed-apis
|
||||
distributed-hetero
|
||||
distributed-tools
|
||||
@@ -0,0 +1,16 @@
|
||||
.. _guide_ko-graph-basic:
|
||||
|
||||
1.1 그래프에 대한 몇가지 기본적인 정의 (그래프 101)
|
||||
----------------------------------------------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-basic>`
|
||||
|
||||
그래프 :math:`G=(V, E)` 는 인티티들과 그것들의 관계를 표현하기 위한 자료 구조이다. 그래프는 노드들의 집합(또는 버틱스들):math:`V` 과 에지들의 집합(또는 아크들) :math:`E` , 두개의 집합으로 구성된다. 두 노드 :math:`u` 와 :math:`v` 의 쌍을 연결하는 에지 :math:`(u, v) \in E` 는 이들 사이에 관계가 있음을 나타낸다. 이 관계는 노드들간의 대칭적인 관계를 표현하는 것과 같이 방향성이 없거나, 비대칭적인 관계를 표현하기 위해서 방향성을 갖을 수 있다. 예를 들어, 소셜 네트워크에서 사람들 간의 친구 관계 모델링에 그래프를 사용한다면, 친구 관계는 양방향이기 때문에 에지는 방향성이 없을 것이다. 하지만, 그래프가 트위터의 팔로우 관계를 모델링하는데 사용된다면, 에지는 방향성이 있다. 에지의 방향성에 따라서, 그래프는 *방향성(directed)* 또는 *비방향성(undirected)* 이 된다.
|
||||
|
||||
그래프는 *가중치를 갖거나(unweight)* , *가중치를 갖지 않는다(unweighted)*. 가중치 그래프에서 각 에지는 스칼라 가중치와 연결된다. 예를 들어, 가중치는 길이 또는 연결 강도를 의미할 수 있다.
|
||||
|
||||
그래프는 *동종(homogeneous)* 또는 *이종(heterogeneous)* 일 수 있다. 동종 그래프(homogeneous graph)에서 모든 노드들은 같은 타입의 인스턴스를 표현하고, 모든 에지들도 같은 타입의 관계를 나타낸다. 예를 들어, 소셜 네트워크는 사람들과 그들의 연결로 구성된 그래프이고, 이들은 모두 같은 타입을 갖는다.
|
||||
|
||||
그와 반대로 이종 그래프(heterogeneous graph)에서는 노드들과 에지들이 여러 타입을 갖는다. 예들 들어, 메켓플래이스를 인코딩한 그래프는 구매자, 판매자, 그리고 상품 노드들이 구입-원함(want-to-buy), 구입했음(has-bought), ~의-고객(is-coustomer-of), 그리고 ~을-판매함(is-selling) 에지로 연결되어 있다. 이분 그래프(bipartite graph)는 이종 그래프의 특별한 형태로 흔히 사용되는 그래프 타입으로, 에지는 서로 다른 두 타입의 노드를 연결한다. 예를 들어, 추천 시스템에서 이분 그래프를 사용해서 사용자들과 아이템들의 상호관계를 표현할 수 있다. DGL에서 이종 그래프를 어떻게 사용하는지는 :ref:`guide_ko-graph-heterogeneous` 를 참고하자.
|
||||
|
||||
다중 그래프(multigraph)는 자체 루프(self loop)를 포함한 노드들의 같은 쌍들 사이에 (방향성이 있는) 여러 에지들을 갖는 그래프이다. 예를 들어, 두 저자가 서로 다른 해에 공동 저작을 했다면, 다른 피처들을 갖는 여러 에지가 만들어진다.
|
||||
@@ -0,0 +1,102 @@
|
||||
.. _guide_ko-graph-external:
|
||||
|
||||
1.4 외부 소스를 사용한 그래프 생성하기
|
||||
-----------------------------------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-external>`
|
||||
|
||||
외부 소스들로부터 :class:`~dgl.DGLGraph` 를 만드는 옵션들:
|
||||
|
||||
- 그래프 및 회소 행렬을 위한 python 라이브러리(NetworkX 및 SciPy)로부터 변환하기
|
||||
- 디스크에서 그래프를 로딩하기
|
||||
|
||||
이 절에서는 다른 그래프를 변환해서 그래프를 생성하는 함수들은 다루지 않겠다. 그 방법들에 대한 소개는 매뉴얼의 API를 참조하자.
|
||||
|
||||
외부 라이브러리를 사용해서 그래프 생성하기
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
아래 코드는 SciPy 희소행렬과 NetworkX 그래프로부터 그래프를 생성하는 예제이다.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> import scipy.sparse as sp
|
||||
>>> spmat = sp.rand(100, 100, density=0.05) # 5% nonzero entries
|
||||
>>> dgl.from_scipy(spmat) # from SciPy
|
||||
Graph(num_nodes=100, num_edges=500,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
>>> import networkx as nx
|
||||
>>> nx_g = nx.path_graph(5) # a chain 0-1-2-3-4
|
||||
>>> dgl.from_networkx(nx_g) # from networkx
|
||||
Graph(num_nodes=5, num_edges=8,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
`nx.path_graph(5)` 로부터 만들면 생성된 :class:`~dgl.DGLGraph` 는 4개가 아니라 8개의 에지를 갖는 점을 유의하자. 이유는 `nx.path_graph(5)` 는 방향이 없는 NetworkX 그래프 :class:`networkx.Graph` 를 만드는데, :class:`~dgl.DGLGraph` 는 항상 방향이 있는 그래프이기 때문이다. 방향이 없는 NetworkX 그래프를 :class:`~dgl.DGLGraph` 로 변환하면, DGL은 내부적으로 방향이 없는 에지를 두개의 방향이 있는 에지로 변환한다. :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` .
|
||||
|
||||
디스크에서 그래프 로딩하기
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
그래프를 저장하기 위한 여러 데이터 포멧들이 있는데, 모든 옵션들을 나열하기는 불가능하다. 그래서 이 절에서는 공통적인 것들에 대한 일반적인 참조만 소개한다.
|
||||
|
||||
Comma Separated Values (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` )을 제공한다면, 그래프를 만드는데 이용할 수 있다.
|
||||
|
||||
함께 참조하기: `Tutorial for loading a Karate Club Network from edge pairs CSV <https://github.com/dglai/WWW20-Hands-on-Tutorial/blob/master/basic_tasks/1_load_data.ipynb>`_.
|
||||
|
||||
JSON/GML 포멧
|
||||
""""""""""""
|
||||
|
||||
특별히 빠르지는 않지만 NetworkX는 `다양한 데이터 포멧 <https://networkx.github.io/documentation/stable/reference/readwrite/index.html>`_ 을 파싱하는 유틸리티들을 제공하는데, 이를 통해서 DGL 그래프를 만들 수 있다.
|
||||
|
||||
DGL 바이너리 포멧
|
||||
""""""""""""""
|
||||
|
||||
DGL은 디스크에 그래프를 바이너리 형태로 저장하고 로딩하는 API들을 제공한다. 그래프 구조와 더불어, API들은 피처 데이터와 그래프 수준의 레이블 데이터도 다룰 수 있다. DGL은 그래프를 직접 S3 또는 HDFS에 체크포인트를 할 수 있는 기능을 제공한다. 러퍼런스 메뉴얼에 자세한 내용이 있으니 참고하자.
|
||||
|
||||
참고할 API들: :func:`dgl.save_graphs` , :func:`dgl.load_graphs`
|
||||
@@ -0,0 +1,55 @@
|
||||
.. _guide_ko-graph-feature:
|
||||
|
||||
1.3 노드와 에지의 피처
|
||||
--------------------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-feature>`
|
||||
|
||||
노드들과 에지들의 그래프별 속성을 저장하기 위해서, :class:`~dgl.DGLGraph` 의 노드들과 에지들은 이름을 갖는 사용자 정의 피쳐를 갖을 수 있다. :py:attr:`~dgl.DGLGraph.ndata` 와 :py:attr:`~dgl.DGLGraph.edata` 인터페이스를 이용해서 이 피쳐들을 접근할 수 있다. 예를 들어, 아래 코드는 두 노드에 대한 피쳐를 생성하고(라인 8과 15에서 ``'x'`` 와 ``'y'`` 이름 피처), 한개의 에지 피처(라인 9에서 ``'x'`` 이름 피처)를 생성한다.
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
>>> g = dgl.graph(([0, 0, 1, 5], [1, 2, 2, 0])) # 6 nodes, 4 edges
|
||||
>>> g
|
||||
Graph(num_nodes=6, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
>>> g.ndata['x'] = th.ones(g.num_nodes(), 3) # node feature of length 3
|
||||
>>> g.edata['x'] = th.ones(g.num_edges(), dtype=th.int32) # scalar integer feature
|
||||
>>> g
|
||||
Graph(num_nodes=6, num_edges=4,
|
||||
ndata_schemes={'x' : Scheme(shape=(3,), dtype=torch.float32)}
|
||||
edata_schemes={'x' : Scheme(shape=(,), dtype=torch.int32)})
|
||||
>>> # different names can have different shapes
|
||||
>>> g.ndata['y'] = th.randn(g.num_nodes(), 5)
|
||||
>>> g.ndata['x'][1] # get node 1's feature
|
||||
tensor([1., 1., 1.])
|
||||
>>> g.edata['x'][th.tensor([0, 3])] # get features of edge 0 and 3
|
||||
tensor([1, 1], dtype=torch.int32)
|
||||
|
||||
:py:attr:`~dgl.DGLGraph.ndata`/:py:attr:`~dgl.DGLGraph.edata` 인터페이스의 중요한 사실들:
|
||||
|
||||
- 숫자 타입(예, float, double, int)의 피처들만 허용된다. 피처는 스칼라, 벡터, 또는 다차원 텐서가 가능하다.
|
||||
- 각 노드 피처는 고유한 이름을 갖고, 각 에지 피쳐도 고유한 이름을 갖는다. 노드와 에지의 피쳐는 같은 이름을 갖을 수 있다. (예, 위 예의 'x')
|
||||
- 턴서 할당으로 피처가 만들어진다. 즉, 피처를 그래프의 각 노드/에지에 할당하는 것이다. 텐서의 첫번째 차원은 그래프의 노드/에지들의 개수와 같아야 한다. 그래프의 노드/에지의 일부에만 피쳐를 할당하는 것은 불가능하다.
|
||||
- 같은 이름의 피처들은 같은 차원 및 같은 타입을 갖아야 한다.
|
||||
- 피처 텐서는 행 위주(row-major)의 레이아웃을 따른다. 각 행-슬라이스는 한 노드 또는 이제의 피처를 저장한다. (아래 예제의 16줄 및 18줄을 보자)
|
||||
|
||||
가중치 그래프인 경우, 에지 피처로 가중치를 저장할 수 있다.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> # edges 0->1, 0->2, 0->3, 1->3
|
||||
>>> edges = th.tensor([0, 0, 0, 1]), th.tensor([1, 2, 3, 3])
|
||||
>>> weights = th.tensor([0.1, 0.6, 0.9, 0.7]) # weight of each edge
|
||||
>>> g = dgl.graph(edges)
|
||||
>>> g.edata['w'] = weights # give it a name 'w'
|
||||
>>> g
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={'w' : Scheme(shape=(,), dtype=torch.float32)})
|
||||
|
||||
참고할 API들: :py:attr:`~dgl.DGLGraph.ndata` , :py:attr:`~dgl.DGLGraph.edata`
|
||||
@@ -0,0 +1,43 @@
|
||||
.. _guide_ko-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) # original feature is on CPU
|
||||
>>> g.device
|
||||
device(type='cpu')
|
||||
>>> cuda_g = g.to('cuda:0') # accepts any device objects from backend framework
|
||||
>>> cuda_g.device
|
||||
device(type='cuda', index=0)
|
||||
>>> cuda_g.ndata['x'].device # feature data is copied to GPU too
|
||||
device(type='cuda', index=0)
|
||||
|
||||
>>> # A graph constructed from GPU tensors is also on GPU
|
||||
>>> u, v = u.to('cuda:0'), v.to('cuda:0')
|
||||
>>> g = dgl.graph((u, v))
|
||||
>>> g.device
|
||||
device(type='cuda', index=0)
|
||||
|
||||
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]) # ok for non-tensor type arguments
|
||||
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
|
||||
>>> cuda_g.in_edges(th.tensor([2, 3, 4]).to('cuda:0')) # tensor type must be on GPU
|
||||
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
|
||||
>>> cuda_g.ndata['h'] = th.randn(5, 4) # ERROR! feature must be on GPU too!
|
||||
DGLError: Cannot assign node feature "h" on device cpu to a graph on device
|
||||
cuda:0. Call DGLGraph.to() to copy the graph to the same device.
|
||||
@@ -0,0 +1,80 @@
|
||||
.. _guide_ko-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는 노드 ID로 1차원 정수 텐서를 사용한다. (PyTorch의 tensor, TensorFlow의 Tensor, 또는 MXNet의 ndarry) DGL은 이 포멧을 "노드-텐서"라고 부른다. DGL에서 에지들은 노드-텐서의 튜플 :math:`(U, V)` 로 표현된다. :math:`(U[i], V[i])` 는 :math:`U[i]` 에서 :math:`V[i]` 로의 에지이다.
|
||||
|
||||
:class:`~dgl.DGLGraph` 를 만드는 방법 중의 하나는 :func:`dgl.graph` 메소드를 사용하는 것이다. 이는 에지 집합을 입력으로 받는다. 또한 DGL은 다른 데이터 소스로부터 그래프들을 생성하는 것도 지원한다. :ref:`guide_ko-graph-external` 참고하자.
|
||||
|
||||
다음 코드는 아래와 같은 4개의 노드를 갖는 그래프를 :func:`dgl.graph` 를 사용해서 :class:`~dgl.DGLGraph` 만들고, 그래프 구조를 쿼리하는 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
|
||||
|
||||
>>> # edges 0->1, 0->2, 0->3, 1->3
|
||||
>>> u, v = th.tensor([0, 0, 0, 1]), th.tensor([1, 2, 3, 3])
|
||||
>>> g = dgl.graph((u, v))
|
||||
>>> print(g) # number of nodes are inferred from the max node IDs in the given edges
|
||||
Graph(num_nodes=4, num_edges=4,
|
||||
ndata_schemes={}
|
||||
edata_schemes={})
|
||||
|
||||
>>> # Node IDs
|
||||
>>> print(g.nodes())
|
||||
tensor([0, 1, 2, 3])
|
||||
>>> # Edge end nodes
|
||||
>>> print(g.edges())
|
||||
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]))
|
||||
>>> # Edge end nodes and edge IDs
|
||||
>>> print(g.edges(form='all'))
|
||||
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]), tensor([0, 1, 2, 3]))
|
||||
|
||||
>>> # If the node with the largest ID is isolated (meaning no edges),
|
||||
>>> # then one needs to explicitly set the number of nodes
|
||||
>>> g = dgl.graph((u, v), num_nodes=8)
|
||||
|
||||
비방향성 그래프를 만들기 위해서는 양방향에 대한 에지들을 만들어야 한다. :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::
|
||||
|
||||
DGL API에서는 일반적으로 텐서 타입이 사용된다. 이는 C 언어에서 효율적으로 저장되는 특징과, 명시적인 데이터 타입, 그리고 디바이스 컨택스트 정보 때문이다. 하지만, 빠른 프로토타입 개발을 지원하기 위해서, 대부분 DGL API는 파이선 iterable (예 list) 및 numpy.array를 함수 인자로 지원하고 있다.
|
||||
|
||||
DGL은 노드 및 에지 ID를 저장하는데 :math:`32` 비트 또는 :math:`64` 비트 정수를 사용할 수 있다. 노드와 에지 ID의 데이터 타입은 같아야 한다. :math:`64` 비트를 사용하면 DGL은 노드 또는 에지를 :math:`2^{64} - 1` 개까지 다룰 수 있다. 하지만 그래프의 노드 또는 에지가 :math:`2^{31} - 1` 개 이하인 경우에는 :math:`32` 비트 정수를 사용해야한다. 이유는 속도도 빠르고 저장공간도 적게 사용하기 때문이다. DGL은 이 변환을 위한 방법들을 제공한다. 아래 예제를 보자.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> edges = th.tensor([2, 5, 3]), th.tensor([3, 5, 0]) # edges 2->3, 5->5, 3->0
|
||||
>>> g64 = dgl.graph(edges) # DGL uses int64 by default
|
||||
>>> print(g64.idtype)
|
||||
torch.int64
|
||||
>>> g32 = dgl.graph(edges, idtype=th.int32) # create a int32 graph
|
||||
>>> g32.idtype
|
||||
torch.int32
|
||||
>>> g64_2 = g32.long() # convert to int64
|
||||
>>> g64_2.idtype
|
||||
torch.int64
|
||||
>>> g32_2 = g64.int() # convert to int32
|
||||
>>> g32_2.idtype
|
||||
torch.int32
|
||||
|
||||
참고할 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,256 @@
|
||||
.. _guide_ko-graph-heterogeneous:
|
||||
|
||||
1.5 이종 그래프 (Heterogeneous Graph)
|
||||
----------------------------------
|
||||
|
||||
:ref:`(English Version)<guide-graph-heterogeneous>`
|
||||
|
||||
이종 그래프는 다른 타입의 노드와 에지를 갖는다. 다른 타입의 노드/에지는 독립적인 ID 공간과 피처 저장소를 갖는다. 아래 그램의 예를 보면, user와 game 노드 ID는 모두 0부터 시작하고, 서로 다른 피처들을 갖고 있다.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/user_guide_graphch_2.png
|
||||
|
||||
두 타입의 노드(user와 game)와 두 타입의 에지(follows와 plays)를 갖는 이종 그래프 예
|
||||
|
||||
이종 그래프 생성하기
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
DGL에서 이종 그래프(짧게 heterograph)는 관계당 하나의 그래프들의 시리즈로 표현된다. 각 관계는 문자열 트리플 ``(source node type, edge type, destination node type)`` 이다. 관계가 에지 타입을 명확하게 하기 때문에, DGL은 이것들을 캐노니컬(canonical) 에지 타입이라고 한다.
|
||||
|
||||
아래 코드는 DGL에서 이종 그래프를 만드는 예제이다.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> import dgl
|
||||
>>> import torch as th
|
||||
|
||||
>>> # Create a heterograph with 3 node types and 3 edges types.
|
||||
>>> graph_data = {
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
|
||||
... ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
|
||||
... }
|
||||
>>> g = dgl.heterograph(graph_data)
|
||||
>>> g.ntypes
|
||||
['disease', 'drug', 'gene']
|
||||
>>> g.etypes
|
||||
['interacts', 'interacts', 'treats']
|
||||
>>> g.canonical_etypes
|
||||
[('drug', 'interacts', 'drug'),
|
||||
('drug', 'interacts', 'gene'),
|
||||
('drug', 'treats', 'disease')]
|
||||
|
||||
동종(homogeneous) 및 이분(bipartite) 그래프는 하나의 관계를 갖는 특별한 이종 그래프일 뿐임을 알아두자.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # A homogeneous graph
|
||||
>>> dgl.heterograph({('node_type', 'edge_type', 'node_type'): (u, v)})
|
||||
>>> # A bipartite graph
|
||||
>>> dgl.heterograph({('source_type', 'edge_type', 'destination_type'): (u, v)})
|
||||
|
||||
이종 그래프와 연관된 *메타그래프(metagraph)* 는 그래프의 스키마이다. 이것은 노드들과 노드간의 에지들의 집합에 대한 타입 제약 조건을 지정한다. 메타그래프의 노드 :math:`u` 는 연관된 이종 그래프의 노드 타입에 해당한다. 메타그래프의 에지 :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::
|
||||
|
||||
>>> # Get the number of all nodes in the graph
|
||||
>>> g.num_nodes()
|
||||
10
|
||||
>>> # Get the number of drug nodes
|
||||
>>> g.num_nodes('drug')
|
||||
3
|
||||
>>> # Nodes of different types have separate IDs,
|
||||
>>> # hence not well-defined without a type specified
|
||||
>>> g.nodes()
|
||||
DGLError: Node type name must be specified if there are more than one node types.
|
||||
>>> g.nodes('drug')
|
||||
tensor([0, 1, 2])
|
||||
|
||||
특정 노드/에지 타입에 대한 피쳐를 설정하고 얻을 때, DGL은 두가지 새로운 형태의 문법을 제공한다 -- `g.nodes['node_type'].data['feat_name']`와 `g.edges['edge_type'].data['feat_name']`.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # Set/get feature 'hv' for nodes of type 'drug'
|
||||
>>> g.nodes['drug'].data['hv'] = th.ones(3, 1)
|
||||
>>> g.nodes['drug'].data['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.]])
|
||||
>>> # Set/get feature 'he' for edge of type 'treats'
|
||||
>>> g.edges['treats'].data['he'] = th.zeros(1, 1)
|
||||
>>> g.edges['treats'].data['he']
|
||||
tensor([[0.]])
|
||||
|
||||
만약 그래프가 오직 한개의 노드/에지 타입을 갖는다면, 노드/에지 타입을 명시할 필요가 없다.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> g = dgl.heterograph({
|
||||
... ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
|
||||
... ('drug', 'is similar', 'drug'): (th.tensor([0, 1]), th.tensor([2, 3]))
|
||||
... })
|
||||
>>> g.nodes()
|
||||
tensor([0, 1, 2, 3])
|
||||
>>> # To set/get feature with a single type, no need to use the new syntax
|
||||
>>> g.ndata['hv'] = th.ones(4, 1)
|
||||
|
||||
.. note::
|
||||
|
||||
에지 타입이 목적지와 도착지 노드의 타입을 고유하게 결정할 수 있다면, 에지 타입을 명시할 때 문자 트리플 대신 한 문자만들 사용할 수 있다. 예를 듬녀, 두 관계 ``('user', 'plays', 'game')`` and ``('user', 'likes', 'game')``를 갖는 이종 그래프가 있을 때, 두 관계를 지정하기 위해서 단지 ``'plays'`` 또는 ``'likes'`` 를 사용해도 된다.
|
||||
|
||||
디스크에서 이종 그래프 로딩하기
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Comma Separated Values (CSV)
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
이종 그래프를 저장하는 일반적인 방법은 다른 타입의 노드와 에지를 서로 다른 CSV 파일에 저장하는 것이다. 예를들면 다음과 같다.
|
||||
|
||||
.. code::
|
||||
|
||||
# data folder
|
||||
data/
|
||||
|-- drug.csv # drug nodes
|
||||
|-- gene.csv # gene nodes
|
||||
|-- disease.csv # disease nodes
|
||||
|-- drug-interact-drug.csv # drug-drug interaction edges
|
||||
|-- drug-interact-gene.csv # drug-gene interaction edges
|
||||
|-- drug-treat-disease.csv # drug-treat-disease edges
|
||||
|
||||
동종 그래프의 경우와 동일하게, 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)
|
||||
|
||||
>>> # Retain relations ('drug', 'interacts', 'drug') and ('drug', 'treats', 'disease')
|
||||
>>> # All nodes for 'drug' and 'disease' will be retained
|
||||
>>> eg = dgl.edge_type_subgraph(g, [('drug', 'interacts', 'drug'),
|
||||
... ('drug', 'treats', 'disease')])
|
||||
>>> eg
|
||||
Graph(num_nodes={'disease': 3, 'drug': 3},
|
||||
num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'treats', 'disease'): 1},
|
||||
metagraph=[('drug', 'drug', 'interacts'), ('drug', 'disease', 'treats')])
|
||||
>>> # The associated features will be copied as well
|
||||
>>> eg.nodes['drug'].data['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.]])
|
||||
|
||||
이종 그래프를 동종 그래프로 변환하기
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
이종 그래프는 다른 타입의 노드/에지와 그것들에 연관된 피쳐들을 관리하는데 깔끔한 인터페이스를 제공한다. 이것을 아래의 경우 특히 유용하다.
|
||||
|
||||
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)
|
||||
|
||||
>>> # By default, it does not merge any features
|
||||
>>> hg = dgl.to_homogeneous(g)
|
||||
>>> 'hv' in hg.ndata
|
||||
False
|
||||
|
||||
>>> # Copy edge features
|
||||
>>> # For feature copy, it expects features to have
|
||||
>>> # the same size and dtype across node/edge types
|
||||
>>> hg = dgl.to_homogeneous(g, edata=['he'])
|
||||
DGLError: Cannot concatenate column ‘he’ with shape Scheme(shape=(2,), dtype=torch.float32) and shape Scheme(shape=(1,), dtype=torch.float32)
|
||||
|
||||
>>> # Copy node features
|
||||
>>> hg = dgl.to_homogeneous(g, ndata=['hv'])
|
||||
>>> hg.ndata['hv']
|
||||
tensor([[1.],
|
||||
[1.],
|
||||
[1.],
|
||||
[0.],
|
||||
[0.],
|
||||
[0.]])
|
||||
|
||||
원래의 노드/에지 타입과 타입별 ID들은 :py:attr:`~dgl.DGLGraph.ndata` 와 :py:attr:`~dgl.DGLGraph.edata` 에 저장된다.
|
||||
|
||||
.. code::
|
||||
|
||||
>>> # Order of node types in the heterograph
|
||||
>>> g.ntypes
|
||||
['disease', 'drug']
|
||||
>>> # Original node types
|
||||
>>> hg.ndata[dgl.NTYPE]
|
||||
tensor([0, 0, 0, 1, 1, 1])
|
||||
>>> # Original type-specific node IDs
|
||||
>>> hg.ndata[dgl.NID]
|
||||
tensor([0, 1, 2, 0, 1, 2])
|
||||
|
||||
>>> # Order of edge types in the heterograph
|
||||
>>> g.etypes
|
||||
['interacts', 'treats']
|
||||
>>> # Original edge types
|
||||
>>> hg.edata[dgl.ETYPE]
|
||||
tensor([0, 0, 1])
|
||||
>>> # Original type-specific edge IDs
|
||||
>>> hg.edata[dgl.EID]
|
||||
tensor([0, 1, 0])
|
||||
|
||||
모델링 목적으로, 특정 관계들을 모아서 그룹으로 만들고, 그것들에 같은 연산을 적용하고 싶은 경우가 있다. 이를 위해서, 우선 이종 그래프의 에지 타입 서브그래프를 추출하고, 그리고 그 서브그래프를 동종 그래프로 변환한다.
|
||||
|
||||
.. 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,32 @@
|
||||
.. _guide_ko-graph:
|
||||
|
||||
1장: 그래프
|
||||
=========
|
||||
|
||||
:ref:`(English version)<guide-graph>`
|
||||
|
||||
그래프는 앤티티들(entity 또는 노드들)과 노드들간의 관계(에지)로 표현되며, 노드와 에지들을 타입을 갖을 수 있다. (예를 들어, ``"user"`` 와 ``"item"`` 은 서로 다른 타입의 노드들이다.) DGL은 :class:`~dgl.DGLGraph` 를 핵심 자료 구조로 갖는 그래프-중심의 프로그래밍 추상화를 제공한다. :class:`~dgl.DGLGraph` 그래프의 구조, 그 그래프의 노드 및 에지 피처들과 이 컴포넌트들을 사용해서 수행된 연산 결과를 다루는데 필요한 인터페이스를 제공한다.
|
||||
|
||||
로드맵
|
||||
-------
|
||||
|
||||
이 장은 1.1절의 그래프 정의에 대한 간단한 소개를 시작으로 :class:`~dgl.DGLGraph`: 의 몇가지 핵심 개념을 소개한다.
|
||||
|
||||
* :ref:`guide_ko-graph-basic`
|
||||
* :ref:`guide_ko-graph-graphs-nodes-edges`
|
||||
* :ref:`guide_ko-graph-feature`
|
||||
* :ref:`guide_ko-graph-external`
|
||||
* :ref:`guide_ko-graph-heterogeneous`
|
||||
* :ref:`guide_ko-graph-gpu`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
graph-basic
|
||||
graph-graphs-nodes-edges
|
||||
graph-feature
|
||||
graph-external
|
||||
graph-heterogeneous
|
||||
graph-gpu
|
||||
@@ -0,0 +1,18 @@
|
||||
사용자 가이드[시대에 뒤쳐진]
|
||||
=====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:titlesonly:
|
||||
|
||||
graph
|
||||
message
|
||||
nn
|
||||
data
|
||||
training
|
||||
minibatch
|
||||
distributed
|
||||
mixed_precision
|
||||
|
||||
|
||||
이 한글 버전 DGL 사용자 가이드 2021년 11월 기준의 영문 :ref:`(User Guide) <guide-index>` 을 Amazon Machine Learning Solutions Lab의 김무현 Principal Data Scientist가 번역한 것입니다. 오류 및 질문은 `muhyun@amazon.com` 으로 보내주세요.
|
||||
@@ -0,0 +1,62 @@
|
||||
.. _guide_ko-message-passing-api:
|
||||
|
||||
2.1 빌트인 함수 및 메시지 전달 API들
|
||||
-----------------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-api>`
|
||||
|
||||
DGL에서 **메시지 함수** 는 한개의 인자 ``edges`` 를 갖는데, 이는 :class:`~dgl.udf.EdgeBatch` 의 객체이다. 메시지 전달이 실행되는 동안 DGL은 에지 배치를 표현하기 위해서 이 객체를 내부적으로 생성한다. 이것은 3개의 맴버, ``src`` , ``dst`` , 그리고 ``data`` 를 갖고, 이는 각각 소스 노드, 목적지 노드, 그리고 에지의 피쳐를 의미한다.
|
||||
|
||||
**축약 함수(reduce function)** 는 한개의 인자 ``nodes`` 를 갖는데, 이는 :class:`~dgl.udf.NodeBatch` 의 객체이다. 메시지 전달이 실행되는 동안 DGL은 노드 배치를 표현하기 위해서 이 객체를 내부적으로 생성한다. 이 객체는 ``mailbox`` 라는 맴버를 갖는데, 이는 배치에 속한 노드들에게 전달된 메시지들을 접근 방법을 제공한다. 가장 흔한 축약 함수로는 ``sum`` , ``max`` , ``min`` 등이 있다.
|
||||
|
||||
**업데이트 함수** 는 위에서 언급한 ``nodes`` 를 한개의 인자로 갖는다. 이 함수는 ``축약 함수`` 의 집계 결과에 적용되는데, 보통은 마지막 스탭에서 노드의 원래 피처와 이 결과와 결합하고, 그 결과를 노드의 피처로 저장한다.
|
||||
|
||||
DGL은 일반적으로 사용되는 메시지 전달 함수들과 축약 함수들을 ``dgl.function`` 네임스패이스에 **빌트인** 으로 구현하고 있다. 일반적으로, **가능한 경우라면 항상** DLG의 빌트인 함수를 사용하는 것을 권장하는데, 그 이유는 이 함수들은 가장 최적화된 형태로 구현되어 있고, 차원 브로드캐스팅을 자동으로 해주기 때문이다.
|
||||
|
||||
만약 여러분의 메시지 전달 함수가 빌트인 함수로 구현이 불가능하다면, 사용자 정의 메시지/축소 함수를 직접 구현할 수 있다. 이를 **UDF** 라고 한다.
|
||||
|
||||
빌트인 메시지 함수들은 단항(unary) 또는 이상(binary)이다. 단항의 경우 DGL은 ``copy`` 를 지원한다. 이항 함수로 DGL은 ``add`` , ``sub`` , ``mul`` , ``div`` , 그리고 ``dot`` 를 지원한다. 빌트인 메시지 함수의 이름 규칙은 다음과 같다. ``u`` 는 ``src`` 노드를, ``v`` 는 ``dst`` 노드를 그리고 ``e`` 는 ``edges`` 를 의미한다. 이 함수들에 대한 파라미터들은 관련된 노드와 에지의 입력과 출력 필드 이름을 지칭하는 문자열이다. 지원되는 빌트인 함수의 목록은 :ref:`api-built-in` 을 참고하자. 한가지 예를 들면, 소스 노드의 ``hu`` 피처와 목적지 노드의 ``hv`` 피처를 더해서 그 결과를 에지의 ``he`` 필드에 저장하는 것을 빌트인 함수 ``dgl.function.u_add_v('hu', 'hv', 'he')`` 를 사용해서 구현할 수 있다. 이와 동일한 기능을 하는 메시지 UDF는 다음과 같다.
|
||||
|
||||
.. code::
|
||||
|
||||
def message_func(edges):
|
||||
return {'he': edges.src['hu'] + edges.dst['hv']}
|
||||
|
||||
빌트인 축약 함수는 ``sum``, ``max``, ``min`` 그리고 ``mean`` 연산을 지원한다. 보통 축약 함수는 두개의 파라메터를 갖는데, 하나는 ``mailbox`` 의 필드 이름이고, 다른 하나는 노드 피처의 필드 이름이다. 이는 모두 문자열이다. 예를 들어, ``dgl.function.sum('m', 'h')`` 는 메시지 ``m`` 을 합하는 아래 축약 UDF와 같다.
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
def reduce_func(nodes):
|
||||
return {'h': torch.sum(nodes.mailbox['m'], dim=1)}
|
||||
|
||||
UDF의 고급 사용법을 더 알고 싶으면 :ref:`apiudf` 를 참고하자.
|
||||
|
||||
: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로 메시지 생성, 메시지 병합 그리고 노드 업데이트를 단일 호출로 합쳤는데, 전반적으로 최적화할 여지가 남아있다.
|
||||
|
||||
:meth:`~dgl.DGLGraph.update_all` 의 파라메터들은 메시지 함수, 축약 함수, 그리고 업데이트 함수이다. :meth:`~dgl.DGLGraph.update_all` 를 호출할 때 업데이트 함수를 지정하지 않는 경우, 업데이트 함수는 ``update_all`` 밖에서 수행될 수 있다. DGL은 이 방법을 권장하는데, 업데이트 함수는 코드를 간결하게 만들기 위해서 보통은 순수 텐서 연산으로 구현되어 있기 때문이다. 예를 들면, 다음과 같다.
|
||||
|
||||
.. code::
|
||||
|
||||
def update_all_example(graph):
|
||||
# store the result in graph.ndata['ft']
|
||||
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
|
||||
fn.sum('m', 'ft'))
|
||||
# Call update function outside of update_all
|
||||
final_ft = graph.ndata['ft'] * 2
|
||||
return final_ft
|
||||
|
||||
이 함수는 소스 노드의 피처 ``ft`` 와 에지 피처 ``a`` 를 곱해서 메시지 ``m`` 을 생성하고, 메시지 ``m`` 들을 더해서 노드 피처 ``ft`` 를 업데이트하고, 마지막으로 ``final_ft`` 결과를 구하기 위해서 ``ft`` 에 2를 곱하고 있다. 호출이 완료되면 DGL은 중간에 사용된 메시지들 ``m`` 을 제거한다. 위 함수를 수학 공식으로 표현하면 다음과 같다.
|
||||
|
||||
.. math:: {final\_ft}_i = 2 * \sum_{j\in\mathcal{N}(i)} ({ft}_j * a_{ij})
|
||||
|
||||
DGL의 빌트인 함수는 부동소수점 데이터 타입을 지원한다. 즉, 피쳐들은 반드시 ``half`` (``float16``), ``float``, 또는 ``double`` 텐서여야만 한다. ``float16`` 데이터 타입에 대한 지원은 기본 설정에서는 비활성화되어 있다. 그 이유는 이를 지원하기 위해서는 ``sm_53`` (Pascal, Volta, Turing, 그리고 Ampere 아키텍타)와 같은 최소한의 GPU 컴퓨팅 능력이 요구되기 때문이다.
|
||||
|
||||
사용자는 DGL 소스 컴파일을 통해서 mixed precision training을 위해서 float16을 활성화시킬 수 있다. (자세한 내용은 :doc:`Mixed Precision Training <mixed_precision>` 튜토리얼 참고)
|
||||
@@ -0,0 +1,24 @@
|
||||
.. _guide_ko-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>`__ 에서 사용되는 것처럼 메시지 병합이전에 에지의 가중치를 적용하는 것은 GNN 모델링에서 흔하게 사용되는 기법이다. DGL은 이를 다음과 같은 밥벙으로 지원하고 있다.
|
||||
|
||||
- 가중치를 에지 피쳐로 저장
|
||||
- 메시지 함수에서 에지 피쳐를 소스 노드의 피쳐와 곱하기
|
||||
|
||||
예를 들면,
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
|
||||
# Suppose eweight is a tensor of shape (E, *), where E is the number of edges.
|
||||
graph.edata['a'] = eweight
|
||||
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),
|
||||
fn.sum('m', 'ft'))
|
||||
|
||||
이 예제는 eweight를 이제 가중치고 사용하고 있다. 에지 가중치는 보통은 스칼라 값을 갖는다.
|
||||
@@ -0,0 +1,39 @@
|
||||
.. _guide_ko-message-passing-efficient:
|
||||
|
||||
2.2 효율적인 메시지 전달 코드 작성 방법
|
||||
------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-efficient>`
|
||||
|
||||
DGL은 메시지 전달에 대한 메모리 사용과 연산 속드를 최적화하고 있다. 이 최적화들을 활용하는 일반적으로 사용되는 방법은 직접 메시지 전달 함수를 만들어서 이를 :meth:`~dgl.DGLGraph.update_all` 호출시 빌트인 함수와 함께 파라메터로 사용하는 것이다.
|
||||
|
||||
만약 그래프의 에지들의 수가 노드들의 수보다 훨씬 많은 경우에는 노드에서 에지로의 불필요한 메모리 복사를 피하는 것이 도움이 된다. 에지에 메시지를 저장할 필요가 있는 :class:`~dgl.nn.pytorch.conv.GATConv` 와 같은 경우에는 빌트인 함수를 사용해서 :meth:`~dgl.DGLGraph.apply_edges` 를 호출해야 한다. 때로는 에지에 저장할 메시지의 차원이 너무 커서 메모리를 많이 차지하기도 한다. DGL에서는 가능한 에지 피쳐의 차원을 낮추는 것을 권장한다.
|
||||
|
||||
에지에 대한 연산을 노드로 분할하여 이를 달성하는 방법에 대한 예제이다. 이 방법은 다음과 같다. ``src`` 피쳐와 ``dst`` 피쳐를 연결하고, 선형 레이어 :math:`W\times (u || v)`를 적용하는 경우를 들어보자. ``src``와 ``dst`` 피처 차원은 매우 높은 반면에 선형 레이어의 결과 차원은 낮다고 가정하자. 이 예제를 직관적으로 구현하면 다음과 같다.
|
||||
|
||||
.. 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
|
||||
|
||||
제안하는 구현은 이 선형 연산을 두개로 나누는 것이다. 하나는 ``src`` 피처에 적용하고, 다른 하나는 ``dst`` 피쳐에 적용한다. 그 후, 에지에 대한 두 선형 연산의 결과를 마지막 단계에서 더한다. 즉, :math:`W_l\times u + W_r \times v` 를 실행하는 것이다. :math:`W` 행렬의 왼쪽 반과 오른쪽 반이 각각 :math:`W_l` 와 :math:`W_r` 일 때, :math:`W \times (u||v) = W_l \times u + W_r \times v` 가 성립하기 때문에 가능하다.
|
||||
|
||||
.. 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,33 @@
|
||||
.. _guide_ko-message-passing-heterograph:
|
||||
|
||||
2.5 이종 그래프에서의 메시지 전달
|
||||
--------------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-heterograph>`
|
||||
|
||||
이종 그래프 ( :ref:`guide_ko-graph-heterogeneous` ) 또는 헤테로그래프는 여러 타입의 노드와 에지를 갖는 그래프이다. 각 노드와 에지의 특징을 표현하기 위해서 다른 타입의 속성을 갖기 위해서 노드와 에지들이 다른 타입을 갖을 수 있다. 복잡한 그래프 뉴럴 네트워크들에서 어떤 노드나 에지 타입들은 다른 차원들을 갖게 모델링 되기도 한다.
|
||||
|
||||
이종 그래프에서 메시지 전달은 두 파트로 나뉜다:
|
||||
|
||||
1. 각 관계(relation) r에 대한, 메지시 연산과 집계(aggregation)
|
||||
2. 가 노트 타입에 대한 모든 관계의 집계 결과를 합치는 축약(reduction)
|
||||
|
||||
이종 그래프에서 메시지 전달을 담당하는 DGL 인터페이스는 :meth:`~dgl.DGLGraph.multi_update_all` 이다. :meth:`~dgl.DGLGraph.multi_update_all` 는 :meth:`~dgl.DGLGraph.update_all` 에 대한 파라메터들을 갖는 사전(dictionary)을 인자로 받는다. 이 사전의 각 키값는 관계이고, 그에 대한 값은 크로스 타입 리듀셔(cross type reducer)에 대한 문자열이다. Reducer는 ``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])
|
||||
# Save it in graph for message passing
|
||||
G.nodes[srctype].data['Wh_%s' % etype] = Wh
|
||||
# Specify per-relation message passing functions: (message_func, reduce_func).
|
||||
# Note that the results are saved to the same destination feature 'h', which
|
||||
# hints the type wise reducer for aggregation.
|
||||
funcs[etype] = (fn.copy_u('Wh_%s' % etype, 'm'), fn.mean('m', 'h'))
|
||||
# Trigger message passing of multiple types.
|
||||
G.multi_update_all(funcs, 'sum')
|
||||
# return the updated node feature dictionary
|
||||
return {ntype : G.nodes[ntype].data['h'] for ntype in G.ntypes}
|
||||
@@ -0,0 +1,16 @@
|
||||
.. _guide_ko-message-passing-part:
|
||||
|
||||
2.3 그래프 일부에 메지시 전달 적용하기
|
||||
------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-message-passing-part>`
|
||||
|
||||
그래프 노드의 일부만 업데이트를 하기 원하는 경우, 업데이트를 하고 싶은 노드들의 ID를 사용해서 서브그래프를 만든 후, 그 서브그래프에 :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_ko-minibatch` 참고하자.
|
||||
@@ -0,0 +1,39 @@
|
||||
.. _guide_ko-message-passing:
|
||||
|
||||
2장: 메지시 전달(Message Passing)
|
||||
=============================
|
||||
|
||||
:ref:`(English Version) <guide-message-passing>`
|
||||
|
||||
메지시 전달 패러다임(Message Passing Paradigm)
|
||||
-----------------------------------------
|
||||
|
||||
:math:`x_v\in\mathbb{R}^{d_1}` 이 노드 :math:`v` 의 피처이고, :math:`w_{e}\in\mathbb{R}^{d_2}` 가 에지 :math:`({u}, {v})` 의 피처라고 하자. **메시지 전달 패러다임** 은 :math:`t+1` 단계에서 노드별(node-wise) 그리고 에지별(edge-wise)의 연산을 다음과 같이 정의한다:
|
||||
|
||||
.. 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` 는 각 에지에 대한 **메시지 함수** 로서 에지의 부속 노드(incident node)들의 피처를 그 에지 피처와 합쳐서 메시지를 만드는 역할을 수행한다. :math:`\psi` 는 각 노드에 대한 **업데이트 함수** 로, **축소 함수(reduce function)** :math:`\rho` 를 사용해서 전달된 메시지들을 통합하는 방식으로 노드의 피처를 업데이트한다.
|
||||
|
||||
로드맵
|
||||
----
|
||||
|
||||
이 장는 DGL의 메시지 전달 API들과, 노드와 에지에 효율적으로 적용하는 방법을 소개한다. 마지막 절에서는 이종 그래프에 메시지 전달을 어떻게 구현하는지 설명한다.
|
||||
|
||||
* :ref:`guide_ko-message-passing-api`
|
||||
* :ref:`guide_ko-message-passing-efficient`
|
||||
* :ref:`guide_ko-message-passing-part`
|
||||
* :ref:`guide_ko-message-passing-edge`
|
||||
* :ref:`guide_ko-message-passing-heterograph`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
message-api
|
||||
message-efficient
|
||||
message-part
|
||||
message-edge
|
||||
message-heterograph
|
||||
@@ -0,0 +1,348 @@
|
||||
.. _guide_ko-minibatch-customizing-neighborhood-sampler:
|
||||
|
||||
6.4 이웃 샘플러 커스터마이징하기
|
||||
-------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-customizing-neighborhood-sampler>`
|
||||
|
||||
DGL이 여러 이웃 샘플링 방법들을 제공하지만, 샘플링 방법을 직접 만들어야할 경우도 있다. 이 절에서는 샘플링 방법을 직접 만드는 방법과 stochastic 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` 의 선행 노드(predecessor)들 (또는 방향성 그래프의 경우 *이웃 노드들*)의 집합을 의미한다.
|
||||
|
||||
아래 그래프의 빨간색 노드를 업데이트하는 메시지 전달을 수행하기 위해서는,
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_0.png
|
||||
:alt: Imgur
|
||||
|
||||
아래 그림의 녹색으로 표시된 이웃 노드들의 노드 피쳐들을 합쳐야한다(aggregate).
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_1.png
|
||||
:alt: Imgur
|
||||
|
||||
이웃 샘플링 직접 해보기
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
우선 위 그림의 그래프를 DGL 그래프로 정의한다.
|
||||
|
||||
.. 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))
|
||||
|
||||
그리고 노드 한개에 대한 결과를 계산하기 위해서 멀티-레이어 메시지 전달을 어떻게 수행할지를 고려하자.
|
||||
|
||||
메시지 전달 의존성 찾기
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
아래 그래프에서 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
|
||||
|
||||
이 그래프는 원본 그래프의 모든 노드들을 포함하고 있지만, 특정 출력 노드들에 메시지를 전달할 에지들만을 포함하고 있다. 이런 그래프를 빨간색 노드 8에 대한 두번째 GNN 레이어에 대한 *프론티어(frontier)* 라고 부른다.
|
||||
|
||||
프론티어들을 생성하는데 여러 함수들이 사용된다. 예를 들어, :func:`dgl.in_subgraph()` 는 원본 그래프의 모든 노드를 포함하지만, 특정 노드의 진입 에지(incoming edge)들만 포함하는 서브 그래프를 유도하는 함수이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
frontier = dgl.in_subgraph(g, [8])
|
||||
print(frontier.all_edges())
|
||||
|
||||
전체 구현은 :ref:`api-subgraph-extraction` 와 :ref:`api-sampling` 를 참고하자.
|
||||
|
||||
기술적으로는 원본 그래프와 같은 노들들 집합을 잡는 어떤 그래프도 프로티어가 될 수 있다. 이는 :ref:`guide_ko-minibatch-customizing-neighborhood-sampler-impl` 에 대한 기반이다.
|
||||
|
||||
멀티-레이어 미니배치 메시지 전달을 위한 이분 구조(Bipartite Structure)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
하지만, :math:`\boldsymbol{h}_\cdot^{(1)}` 로부터 단순히 :math:`\boldsymbol{h}_8^{(2)}` 를 계산하는 것은 프론티어에서 메시지 전달을 계산하는 방식으로 할 수 없다. 그 이유는, 여전히 프론티어가 원본 그래프의 모든 노드를 포함하고 있기 때문이다. 이 그래프의 경우, (녹색과 빨간색 노드들) 4, 5, 7, 8, 11 노드들만이 입력으로 필요하고, 출력으로는 (빨간색 노드) 노드 8번이 필요하다. 입력과 출력의 노드 개수가 다르기 때문에, 작은 이분-구조(bipartite-structured) 그래프에서 메시지 전달을 수행할 필요가 있다.
|
||||
|
||||
아래 그림은 노드 8에 대해서 2번째 GNN 레이어의 MFG를 보여준다.
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_4_4.png
|
||||
:alt: Imgur
|
||||
|
||||
.. note::
|
||||
|
||||
Message Flow Graph에 대한 개념은 :doc:`Stochastic Training Tutorial
|
||||
<tutorials/large/L0_neighbor_sampling_overview>` 참고하자.
|
||||
|
||||
목적지 노드들이 소스 노드에도 등장한다는 점을 유의하자. 그 이유는 메시지 전달(예를 들어, :math:`\phi^{(2)}` )이 수행된 후에 이전 레이어의 목적지 노드들의 representation들이 피처를 합치는데 사용되기 때문이다.
|
||||
|
||||
DGL은 임의의 프론티어를 MFG로 변환하는 :func:`dgl.to_block` 함수를 제공한다. 이 함수의 첫번째 인자는 프론티어이고, 두번째 인자는 목적지 노드들이다. 예를 들어, 위 프론티어는 목적지 노드 8에 대한 MFG로 전환하는 코드는 다음과 같다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
dst_nodes = torch.LongTensor([8])
|
||||
block = dgl.to_block(frontier, dst_nodes)
|
||||
|
||||
:meth:`dgl.DGLGraph.number_of_src_nodes` 와
|
||||
:meth:`dgl.DGLGraph.number_of_dst_nodes` 메소스들 사용해서 특정 노트 타입의 소스 노드 및 목적지 노드의 수를 알아낼 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
num_src_nodes, num_dst_nodes = block.number_of_src_nodes(), block.number_of_dst_nodes()
|
||||
print(num_src_nodes, num_dst_nodes)
|
||||
|
||||
:attr:`dgl.DGLGraph.srcdata` 와 :attr:`dgl.DGLGraph.srcnodes` 같은 멤머를 통해서 MFG의 소스 노드 피쳐들을 접근할 수 있고, :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_src_nodes, 5)
|
||||
block.dstdata['h'] = torch.randn(num_dst_nodes, 5)
|
||||
|
||||
만약 MFG가 프론티어에서 만들어졌다면, 즉 프래프에서 만들어졌다면, MFG의 소스 및 목적지 노드의 피쳐는 다음과 같이 직접 읽을 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
print(block.srcdata['x'])
|
||||
print(block.dstdata['y'])
|
||||
|
||||
.. note::
|
||||
|
||||
MFG에서의 소스 노드와 목적지 노드의 원본의 노드 ID는 ``dgl.NID`` 피쳐에 저장되어 있고, MFG의 에지 ID들와 프론티어의 에지 ID 사이의 매핑은 ``dgl.EID`` 에 있다.
|
||||
|
||||
DGL에서는 MFG의 목적지 노드들이 항상 소스 노드에도 있도록 하고 있다. 다음 코드에서 알수 있듯이, 목적지 노드들은 소스 노드들에서 늘 먼저 위치한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
src_nodes = block.srcdata[dgl.NID]
|
||||
dst_nodes = block.dstdata[dgl.NID]
|
||||
assert torch.equal(src_nodes[:len(dst_nodes)], dst_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
|
||||
|
||||
하지만, 목적지 노드들은 위 보다 더 많은 노드들을 포함할 수 있다. 이 예제의 경우, 어떤 에지도 연결되지 않은 고립된 노드들(isolated node)이 있고, 이 고립 노드들은 소스 노드와 목적지 노드 모두에 포함될 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Node 3 is an isolated node that do not have any edge pointing to it.
|
||||
block3 = dgl.to_block(frontier2, torch.LongTensor([4, 5, 7, 8, 11, 3]))
|
||||
print(block3.srcdata[dgl.NID])
|
||||
print(block3.dstdata[dgl.NID])
|
||||
|
||||
Heterogeneous 그래프들
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
MFG들은 heterogeneous 그래프에도 적용됩니다. 다음 프론티어를 예로 들어보자.
|
||||
|
||||
.. 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, #4, #8 그리고 Game #2, #6을 포함한 MFG를 생성한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
hetero_block = dgl.to_block(hetero_frontier, {'user': [3, 6, 8], 'game': [2, 6]})
|
||||
|
||||
소스 노드들과 목적지 노드들의 타입별로 얻을 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# source users and games
|
||||
print(hetero_block.srcnodes['user'].data[dgl.NID], hetero_block.srcnodes['game'].data[dgl.NID])
|
||||
# destination users and games
|
||||
print(hetero_block.dstnodes['user'].data[dgl.NID], hetero_block.dstnodes['game'].data[dgl.NID])
|
||||
|
||||
|
||||
.. _guide_ko-minibatch-customizing-neighborhood-sampler-impl:
|
||||
|
||||
커스텀 이웃 샘플러 구현하기
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
아래 코드는 노드 분류를 위한 이웃 샘플링을 수행한다는 것을 떠올려 보자.
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
|
||||
이웃 샘플링 전략을 직접 구현하기 위해서는 ``sampler`` 를 직접 구현한 내용으로 바꾸기만 하면 된다. 이를 살펴보기 위해서, 우선 :class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler` 를 상속한 클래스인 :class:`~dgl.dataloading.dataloader.BlockSampler` 를 살펴보자.
|
||||
|
||||
:class:`~dgl.dataloading.dataloader.BlockSampler` 클래스는 :meth:`~dgl.dataloading.dataloader.BlockSampler.sample_blocks` 메소드를 통해서 마지막 레이어로부터 시작하는 MFG들의 리스트를 만들어내는 역할을 한다. ``sample_blocks`` 의 기본 구현은 프론티어들과 그것들을 MFG들로 변환하면서 backwards를 iterate한다.
|
||||
|
||||
따라서, 이웃 샘플링을 하기 위해서 단지 :meth:`~dgl.dataloading.dataloader.BlockSampler.sample_frontier` **메소드** 를 **구현하기만 하면된다**. 어떤 레이어를 위한 프론티어를 생성할 것인지, 원본 그래프, representation들을 계산할 노드들이 주어지면, 이 메소드는 그것들을 위한 프론티어를 생성하는것을 담당한다.
|
||||
|
||||
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):
|
||||
# Get all inbound edges to `seed_nodes`
|
||||
src, dst = dgl.in_subgraph(g, seed_nodes).all_edges()
|
||||
# Randomly select edges with a probability of p
|
||||
mask = torch.zeros_like(src).bernoulli_(self.p)
|
||||
src = src[mask]
|
||||
dst = dst[mask]
|
||||
# Return a new graph with the same nodes as the original graph as a
|
||||
# frontier
|
||||
frontier = dgl.graph((src, dst), num_nodes=g.num_nodes())
|
||||
return frontier
|
||||
|
||||
def __len__(self):
|
||||
return self.num_layers
|
||||
|
||||
샘플러를 직접 구현한 다음에는, 그 샘플러를 사용하는 데이터 로더를 생성하고, 예전과 같이 시드 노드들을 iterate하면서 MFG들의 리스트를 만들게 한다.
|
||||
|
||||
.. 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 # 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()
|
||||
|
||||
Heterogeneous 그래프들
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Heterogeneous 그래프에 대한 프론티어를 생성하는 것은 homogeneous 그래프의 경우와 동일하다. 리턴된 그래프가 원본 그래프와 같은 노드들을 갖도록 하면, 나머지는 그대로 동작할 것이다. 예를 들어, 위 ``MultiLayerDropoutSampler`` 를 재작성해서 모든 에지 타입들을 iterate 해서, heterogeneous 그래프에도 작동하게 만들 수 있다.
|
||||
|
||||
.. 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):
|
||||
# Get all inbound edges to `seed_nodes`
|
||||
sg = dgl.in_subgraph(g, seed_nodes)
|
||||
|
||||
new_edges_masks = {}
|
||||
# Iterate over all edge types
|
||||
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()
|
||||
|
||||
# Return a new graph with the same nodes as the original graph as a
|
||||
# frontier
|
||||
frontier = dgl.edge_subgraph(new_edges_masks, relabel_nodes=False)
|
||||
return frontier
|
||||
|
||||
def __len__(self):
|
||||
return self.num_layers
|
||||
@@ -0,0 +1,260 @@
|
||||
.. _guide_ko-minibatch-edge-classification-sampler:
|
||||
|
||||
6.2 이웃 샘플링을 사용한 에지 분류 GNN 모델 학습하기
|
||||
-----------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-edge-classification-sampler>`
|
||||
|
||||
에지 분류/리그레션 모델을 학습하는 것은 몇 가지 눈에 띄는 차이점이 있지만 노드 분류/리그레션과 어느정도 비슷하다.
|
||||
|
||||
이웃 샘플러 및 데이터 로더 정의하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:ref:`노드 분류에서 사용한 것과 같은 이웃 샘플러<guide_ko-minibatch-node-classification-sampler>` 를 사용할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
|
||||
에지 분류에 DGL이 제공하는 이웃 샘플러를 사용하려면, 미니-배치의 에지들의 집합을 iterate 하는 :class:`~dgl.dataloading.pytorch.EdgeDataLoader` 와 함께 사용해야한다. 이것은 아래 모듈에서 사용될 에지 미니-배치로부터 만들어질 서브 그래프와 *message flow graph* (MFG)들을 리턴한다.
|
||||
|
||||
다음 코드 예제는 PyTorch DataLoader를 만든다. 이는 베치들에 있는 학습 에지 ID 배열 :math:`train_eids` 들을 iterate 하고, 생성된 MFG들의 리스트를 GPU로 옮겨놓는다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
batch_size=1024,
|
||||
shuffle=True,
|
||||
drop_last=False,
|
||||
num_workers=4)
|
||||
|
||||
.. note::
|
||||
|
||||
Message flow graph의 개념은 :doc:`Stochastic Training Tutorial <tutorials/large/L0_neighbor_sampling_overview>` 를 참고하자.
|
||||
|
||||
빌트인으로 지원되는 샘플러들에 대한 전체 목록은 :ref:`neighborhood sampler API reference <api-dataloading-neighbor-sampling>` 에 있다.
|
||||
|
||||
:ref:`guide_ko-minibatch-customizing-neighborhood-sampler` 에는 여러분만의 이웃 샘플러 만드는 방법과 MFG 개념에 대한 보다 상세한 설명을 담고 있다.
|
||||
|
||||
이웃 샘플링을 위해서 원본 그래프에서 미니 배치의 에지들 제거하기
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
에지 분류 모델을 학습할 때, 때로는 computation dependency에서 학습 데이터에 있는 에지들을 존재하지 않았던 것처럼 만들기 위해 제거하는 것이 필요하다. 그렇지 않으면, 모델은 두 노드들 사이에 에지가 존재한다는 사실을 *인지* 할 것이고, 이 정보를 학습에 잠재적으로 이용할 수 있기 때문이다.
|
||||
|
||||
따라서, 에지 분류의 경우 때로는 이웃 샘플링은 미니-배치안에 샘플된 에지들 및 undirected 그래프인 경우 샘플된 에지의 역방향 에지들도 원본 그래프에서 삭제하기도 한다. :class:`~dgl.dataloading.pytorch.EdgeDataLoader` 객체를 만들 때, ``exclude='reverse_id'`` 를 에지 ID와 그와 연관된 reverse 에지 ID들의 매핑 정보와 함께 지정할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
n_edges = g.num_edges()
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
|
||||
# The following two arguments are specifically for excluding the minibatch
|
||||
# edges and their reverse edges from the original graph for neighborhood
|
||||
# sampling.
|
||||
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)
|
||||
|
||||
모델을 미니-배치 학습에 맞게 만들기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
에지 분류 모델은 보통은 다음과 같이 두 부분으로 구성된다:
|
||||
|
||||
- 첫번째는 부속 노드(incident node)들의 representation을 얻는 부분
|
||||
- 두번째는 부속 노드의 representation들로부터 에지 점수를 계산하는 부분
|
||||
|
||||
첫번째 부분은 :ref:`노드 분류<guide_ko-minibatch-node-classification-model>` 와 완전히 동일하기에, 단순하게 이를 재사용할 수 있다. 입력 DGL에서 제공하는 데이터 로더가 만들어 낸 MFG들의 리스트와 입력 피쳐들이 된다.
|
||||
|
||||
.. 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` 를 사용해서 에지 서브 그래프를 사용해서 에지들의 점수를 계산한다.
|
||||
|
||||
다음 코드는 부속 노드 피처들을 연결하고, 이를 dense 레이어에 입력해서 얻은 결과로 에지들의 점수를 예측하는 예를 보여준다.
|
||||
|
||||
.. 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']
|
||||
|
||||
전체 모델은 아래와 같이 데이터 로더로부터 얻은 MFG들의 리스트와 에지 서브 그래프, 그리고 입력 노드 피쳐들을 사용한다.
|
||||
|
||||
.. 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에서는 에지 서브 그래프의 노드들이 MFG들의 리스트에서 마지막 MFG의 출력 노드들과 동일하도록 확인한다.
|
||||
|
||||
학습 룹
|
||||
~~~~~
|
||||
|
||||
학습 룹은 노드 분류의 학습 룹과 비슷하다. 데이터 로더를 iterate해서, 미니배치의 에지들에 의해서 유도된 서브 그래프와 에지들의 부속 노드(incident node)들의 representation들을 계산하기 위한 MFG들의 목록을 얻는다.
|
||||
|
||||
.. 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()
|
||||
|
||||
Heterogeneous 그래프의 경우
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프들의 노드 representation들을 계산하는 모델은 에지 분류/리그레션을 위한 부속 노드 representation들을 구하는데 사용될 수 있다.
|
||||
|
||||
.. 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
|
||||
|
||||
점수를 예측하기 위한 homogeneous 그래프와 heterogeneous 그래프간의 유일한 구현상의 차이점은 :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.NodeDataLoader` 대신에 :class:`~dgl.dataloading.pytorch.EdgeDataLoader` 를 사용하고, 노드 타입과 노드 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)
|
||||
|
||||
만약 heterogeneous 그래프에서 역방향의 에지를 배제하고자 한다면 약간 달라진다. Heterogeneous 그래프에서 역방향 에지들은 에지와는 다른 에지 타입을 갖는 것이 보통이다. 이는 “forward”와 “backward” 관계들을 구분직기 위해서이다. (즉, ``follow`` 와 ``followed by`` 는 서로 역 관계이고, ``purchase`` 와 ``purchased by`` 는 서로 역 관계인 것 처럼)
|
||||
|
||||
만약 어떤 타입의 에지들이 다른 타입의 같은 ID를 갖는 역방향 에지를 갖는다면, 에지 타입들과
|
||||
그것들의 반대 타입간의 매핑을 명시할 수 있다. 미니배치에서 에지들과 그것들의 역방향 에지를 배제하는 것은
|
||||
다음과 같다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
dataloader = dgl.dataloading.EdgeDataLoader(
|
||||
g, train_eid_dict, sampler,
|
||||
|
||||
# The following two arguments are specifically for excluding the minibatch
|
||||
# edges and their reverse edges from the original graph for neighborhood
|
||||
# sampling.
|
||||
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`` 의 구현이 노드 타입들과 예측 값에 대한 두 사전들을 인자로 받는다는 점을 제외하면,
|
||||
homogeneous 그래프의 학습 룹 구현과 거의 같다.
|
||||
|
||||
.. 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>`__ 은 이분 그래프(bipartite graph)에 대한 에지 분류 예제이다.
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.. _guide_ko-minibatch-gpu-sampling:
|
||||
|
||||
6.7 이웃 샘플링에 GPU 사용하기
|
||||
------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-gpu-sampling>`
|
||||
|
||||
DGL 0.7부터 GPU 기반의 이웃 샘플링을 지원하는데, 이는 CPU 기반의 이웃 샘플링에 비해서 상당한 속도 향상을 가져다 준다. 만약 다루는 그래프와 피쳐들이 GPU에 들어갈 수 있는 크기이고, 모델이 너무 많은 GPU 메모리를 차지하지 않는다면, GPU 메모리에 올려서 GPU 기반의 이웃 샘플링을 하는 것이 최선의 방법이다.
|
||||
|
||||
예를 들어, `OGB Products <https://ogb.stanford.edu/docs/nodeprop/#ogbn-products>`__ 는 2.4M 노드들과 61M 에지들을 갖고, 각 노드는 100 차원의 피쳐를 갖는다. 노트 피쳐들을 모두 합해서 1GB 미만의 메모리를 차지하고, 그래프는 약 1GB 보다 적은 메모리를 사용한다. 그래프의 메모리 요구량은 에지의 개수에 관련이 있다. 따라서, 전체 그래프를 GPU에 로딩하는 것이 가능하다.
|
||||
|
||||
.. note::
|
||||
|
||||
이 기능은 실험적인 것으로 개발이 진행 중이다. 추가 업데이트를 지켜보자.
|
||||
|
||||
DGL 데이터 로더에서 GPU 기반의 이웃 샘플링 사용하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
DGL 데이터 로더에서 GPU 기반의 이웃 샘플링은 다음 방법으로 사용할 수 있다.
|
||||
|
||||
* 그래프를 GPU에 넣기
|
||||
* ``num_workers`` 인자를 0으로 설정하기. CUDA는 같은 context를 사용하는 멀티 프로세스를 지원하지 않기 때문이다.
|
||||
* ``device`` 인자를 GPU 디바이스로 설정하기
|
||||
|
||||
:class:`~dgl.dataloading.pytorch.NodeDataLoader` 의 다른 모든 인자들은 다른 가이드와 튜토리얼에서 사용한 것돠 같다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
g = g.to('cuda:0')
|
||||
dataloader = dgl.dataloading.NodeDataLoader(
|
||||
g, # The graph must be on GPU.
|
||||
train_nid,
|
||||
sampler,
|
||||
device=torch.device('cuda:0'), # The device argument must be GPU.
|
||||
num_workers=0, # Number of workers must be 0.
|
||||
batch_size=1000,
|
||||
drop_last=False,
|
||||
shuffle=True)
|
||||
|
||||
GPU 기반의 이웃 샘플링은 커스텀 이웃 샘플러가 두가지 조건을 충족하면 동작한다. (1) 커스텀 샘플러가 :class:`~dgl.dataloading.BlockSampler` 의 서브 클래스이고, (2) 샘플러가 GPU에서 완전하게 동작한다.
|
||||
|
||||
.. note::
|
||||
|
||||
현재는 :class:`~dgl.dataloading.pytorch.EdgeDataLoader` 와 heterogeneous 그래프는 지원하지 않는다.
|
||||
|
||||
GPU 기반의 이웃 샘플러를 DGL 함수와 함께 사용하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
다음 함수들은 GPU에서 작동을 지원한다.
|
||||
|
||||
* :func:`dgl.sampling.sample_neighbors`
|
||||
|
||||
* 균일 샘플링(uniform sampling)만 지원함. non-uniform샘플링은 CPU에서만 동작함.
|
||||
|
||||
위 함수들 이외의 GPU에서 동작하는 함수들은 :func:`dgl.to_block` 를 참고하자.
|
||||
@@ -0,0 +1,77 @@
|
||||
.. _guide_ko-minibatch-inference:
|
||||
|
||||
6.6 큰 그래프들에 대핸 정확한 오프라인 추론
|
||||
---------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-inference>`
|
||||
|
||||
GPU를 사용해서 GNN을 학습하는데 메모리와 걸리는 시간을 줄이기 위해서 서브 샘플링과 이웃 샘플링이 모두 사용된다. 추론을 수행할 때 보통은 샘플링으로 발생할 수 있는 임의성을 제거하기 위해서 전체 이웃들에 대해서 aggretate하는 것이 더 좋다. 하지만, GPU 메모리 제약이나, CPU의 느린 속도 때문에 전체 그래프에 대한 forward propagagtion을 수행하는 것은 쉽지 않다. 이 절은 미니배치와 이웃 샘플링을 통해서 제한적인 GPU를 사용한 전체 그래프 forward propagation의 방법을 소개한다.
|
||||
|
||||
추론 알고리즘은 학습 알고리즘과는 다른데, 추론 알고리즘은 첫번째 레이어부터 시작해서 각 레이이별로 모든 노드의 representation들을 계산해야하기 때문이다. 특히, 특정 레이어의 경우에 우리는 미니배치의 모든 노드들에 대해서 이 레이어의 출력 representation을 계산해야한다. 그 결과, 추론 알고리즘은 모든 레이어들 iterate하는 outer 룹과 노들들의 미니배치를 iterate하는 inner 룹을 갖는다. 반면, 학습 알고리즘은 노드들의 미니배치를 iterate하는 outer 룹과, 이웃 샘플링과 메시지 전달을 위한 레이어들을 iterate하는 inner 룹을 갖는다.
|
||||
|
||||
아래 애니매이션은 이 연산이 어떻게 일어나는지를 보여주고 있다 (각 레이어에 대해서 첫 3개의 미니배치만 표현되고 있음을 주의하자)
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_6_0.gif
|
||||
:alt: Imgur
|
||||
|
||||
오프라인 추론 구현하기
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
6.1 :ref:`guide_ko-minibatch-node-classification-model` 에서 다룬 2-레이어 GCN을 생각해 보자. 오프라인 추론을 구현하는 방법은 여전히 :class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler` 를 사용하지만, 한번에 하나의 레이어에 대한 샘플링을 수행한다. 하나의 레이어에 대한 계산은 메시지들어 어떻게 aggregate되고 합쳐지는지에 의존하기 때문에 오프라인 추론은 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):
|
||||
"""
|
||||
Offline inference with this module
|
||||
"""
|
||||
# Compute representations layer by layer
|
||||
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)
|
||||
|
||||
# Within a layer, iterate over nodes in batches
|
||||
for input_nodes, output_nodes, blocks in dataloader:
|
||||
block = blocks[0]
|
||||
|
||||
# Copy the features of necessary input nodes to GPU
|
||||
h = x[input_nodes].to(device)
|
||||
# Compute output. Note that this computation is the same
|
||||
# but only for a single layer.
|
||||
h_dst = h[:block.number_of_dst_nodes()]
|
||||
h = F.relu(layer(block, (h, h_dst)))
|
||||
# Copy to output back to CPU.
|
||||
y[output_nodes] = h.cpu()
|
||||
|
||||
x = y
|
||||
|
||||
return y
|
||||
|
||||
모델 선택을 위해서 검증 데이터셋에 평가 metric을 계산하는 목적으로 정확한 오프라인 추론을 계산할 필요가 없다는 점을 주목하자. 모든 레이어에 대해서 모든 노드들의 representation을 계산하는 것이 필요한데, 이것은 레이블이 없는 데이터가 많은 semi-supervised 영역에서는 아주 많은 리소스를 필요로하기 때문이다. 이웃 샘플링은 모델 선택 및 평가 목적으로는 충분하다.
|
||||
|
||||
오프라인 추론의 예들로 `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,266 @@
|
||||
.. _guide_ko-minibatch-link-classification-sampler:
|
||||
|
||||
6.3 이웃 샘플링을 사용한 링크 예측 GNN 모델 학습하기
|
||||
-----------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-link-classification-sampler>`
|
||||
|
||||
Negative 샘플링을 사용한 이웃 샘플러 및 데이터 로더 정의하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
노드/에지 분류에서 사용한 이웃 샘플러를 그대로 사용하는 것이 가능하다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
sampler = dgl.dataloading.MultiLayerFullNeighborSampler(2)
|
||||
|
||||
DGL의 :class:`~dgl.dataloading.pytorch.EdgeDataLoader` 는 링크 예측를 위한 negative 샘플 생성을
|
||||
지원한다. 이를 사용하기 위해서는, negative 샘플링 함수를 제공해야한다. :class:`~dgl.dataloading.negative_sampler.Uniform` 은 uniform 샘플링을 해주는 함수이다. 에지의 각 소스 노드에 대해서,이 함수는 ``k`` 개의 negative 목적지 노드들을 샘플링한다.
|
||||
|
||||
아래 코드는 에지의 각 소스 노드에 대해서 5개의 negative 목적지 노드를 균등하게 선택한다.
|
||||
|
||||
.. 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)
|
||||
|
||||
빌드인 negative 샘플러들은 :ref:`api-dataloading-negative-sampling` 에서 확인하자.
|
||||
|
||||
직접 만든 negative 샘플러 함수를 사용할 수도 있다. 이 함수는 원본 그래프 ``g`` 와, 미니배치 에지 ID 배열 ``eid`` 를 받아서
|
||||
소스 ID 배열과 목적지 ID 배열의 쌍을 리턴해야 한다.
|
||||
|
||||
아래 코드 예제는 degree의 거듭제곱에 비례하는 확률 분포에 따라서 negative 목적지 노드들을 샘플링하는 custom negative 샘플러다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class NegativeSampler(object):
|
||||
def __init__(self, g, k):
|
||||
# caches the probability distribution
|
||||
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_ko-training-link-prediction` 에서 설명한 것처럼, 링크 예측은 (positive 예제인) 에지의 점수와 존재하지 않는 에지(즉, negative 예제)의 점수를 비교하는 것을 통해서 학습될 수 있다. 에지들의 점수를 계산하기 위해서, 에지 분류/리그레션에서 사용했던 노드 representation 계산 모델을 재사용한다.
|
||||
|
||||
.. 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
|
||||
|
||||
점수 예측을 위해서 확률 분포 대신 각 에지의 scalar 점수를 예측하기만 하면되기 때문에, 이 예제는 부속 노드 representation들의 dot product로 점수를 계산하는 방법을 사용한다.
|
||||
|
||||
.. 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']
|
||||
|
||||
Negative 샘플러가 지정되면, DGL의 데이터 로더는 미니배치 마다 다음 3가지 아이템들을 만들어낸다.
|
||||
|
||||
- 샘플된 미니배치에 있는 모든 에지를 포함한 postive 그래프
|
||||
- Negative 샘플러가 생성한 존재하지 않는 에지 모두를 포함한 negative 그래프
|
||||
- 이웃 샘플러가 생성한 *message flow graph* (MFG)들의 리스트
|
||||
|
||||
이제 3가지 아이템와 입력 피쳐들을 받는 링크 예측 모델을 다음과 같이 정의할 수 있다.
|
||||
|
||||
.. 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
|
||||
|
||||
학습 룹
|
||||
~~~~~
|
||||
|
||||
학습 룹은 데이터 로더를 iterate하고, 그래프들과 입력 피쳐들을 위해서 정의한 모델에 입력하는 것일 뿐이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# an example hinge loss
|
||||
n = pos_score.shape[0]
|
||||
return (neg_score.view(n, -1) - pos_score.view(n, -1) + 1).clamp(min=0).mean()
|
||||
|
||||
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에서는 homogeneous 그래프들에 대한 링크 예측의 예제로 `unsupervised learning GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_sampling_unsupervised.py>`__ 를 제공한다.
|
||||
|
||||
Heterogeneous 그래프의 경우
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프들의 노드 representation들을 계산하는 모델은 에지 분류/리그레션을 위한 부속 노드
|
||||
representation들을 구하는데 사용될 수 있다.
|
||||
|
||||
.. 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
|
||||
|
||||
점수를 예측하기 위한 homogeneous 그래프와 heterogeneous 그래프간의 유일한 구현상의 차이점은
|
||||
: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
|
||||
|
||||
데이터 로더 구현도 노드 분류을 위한 것과 아주 비슷하다. 유일한 차이점은 negative 샘플러를 사용하며, 노드 타입과 노드 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)
|
||||
|
||||
만약 직접 만든 negative 샘플링 함수를 사용하기를 원한다면, 그 함수는 원본 그래프, 에지 타입과 에지 ID 텐서들의 dictionary를 인자로 받아야하고, 에지 타입들과 소스-목적지 배열 쌍의 dictionary를 리턴해야한다. 다음은 예제 함수이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class NegativeSampler(object):
|
||||
def __init__(self, g, k):
|
||||
# caches the probability distribution
|
||||
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들의 dictionary와 negative 샘플러를 데이터 로더에 전달한다. 예를 들면, 아래 코드는 heterogeneous 그래프의 모든 에지들을 iterate하는 예이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
train_eid_dict = {
|
||||
etype: g.edges(etype=etype, form='eid')
|
||||
for etype in g.canonical_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`` 의 구현이 노드 타입들과 예측 값에 대한 두 사전들을 인자로 받는다는 점을 제외하면, homogeneous 그래프의 학습 룹 구현과 거의 같다.
|
||||
|
||||
.. 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,158 @@
|
||||
.. _guide_ko-minibatch-custom-gnn-module:
|
||||
|
||||
6.5 미니-배치 학습을 위한 커스텀 GNN 모듈 구현하기
|
||||
----------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-custom-gnn-module>`
|
||||
|
||||
Homogeneous 그래프나 heterogeneous 그래프를 대상으로 전체 그래프를 업데이트하는 커스텀 GNN 모듈을 만드는 것에 익숙하다면, MFG에 대한 연산을 구현하는 코드도 비슷하다는 것을 알 수 있다. 차이점은 노드들이 입력 노드와 출력 노드로 나뉜다는 것 뿐이다.
|
||||
|
||||
커스텀 graph convolution 모듈을 예로 들자. 이 코드는 단지 커스텀 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))
|
||||
|
||||
전체 그래프에 대한 커스텀 메시지 전달 NN 모듈이 있고, 이를 MFG에서 작동하도록 만들고 싶다면, 다음과 같이 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 is now a pair of feature tensors for input and output nodes, instead of
|
||||
# a single feature tensor.
|
||||
# def forward(self, g, h):
|
||||
def forward(self, block, h):
|
||||
# with g.local_scope():
|
||||
with block.local_scope():
|
||||
# g.ndata['h'] = h
|
||||
h_src = h
|
||||
h_dst = h[:block.number_of_dst_nodes()]
|
||||
block.srcdata['h'] = h_src
|
||||
block.dstdata['h'] = h_dst
|
||||
|
||||
# g.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))
|
||||
block.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'h_neigh'))
|
||||
|
||||
# return self.W(torch.cat([g.ndata['h'], g.ndata['h_neigh']], 1))
|
||||
return self.W(torch.cat(
|
||||
[block.dstdata['h'], block.dstdata['h_neigh']], 1))
|
||||
|
||||
일반적으로, 직접 구현한 NN 모듈이 MFG에서 동작하게 만들기 위해서는 다음과 같은 것을 해야한다.
|
||||
|
||||
- 첫 몇 행들(row)을 잘라서 입력 피쳐들로부터 출력 노드의 피처를 얻는다. 행의 개수는 :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:`block.dstdata <dgl.DGLGraph.dstdata>` 로 교체한다.
|
||||
- 원본 그래프가 여러 종류의 노드 타입을 갖는 경우, :attr:`g.nodes <dgl.DGLGraph.nodes>` 를 입력 노드의 피쳐의 경우 :attr:`block.srcnodes <dgl.DGLGraph.srcnodes>` 로 또는 출력 노드의 피처의 경우 :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:`block.number_of_dst_nodes <dgl.DGLGraph.number_of_dst_nodes>` 로 각각 교체한다.
|
||||
|
||||
Heterogeneous 그래프들
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프의 경우도 커스텀 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}
|
||||
|
||||
Homogeneous 그래프, 이분 그래프(bipartite graph), 그리고 MFG를 위한 모듈 작성하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL의 모든 메시지 전달 모듈들은 homogeneous 그래프, 단방향 이분 그래프 (unidirectional bipartite graphs, 두개 노드 타입을 갖고, 하나의 에지 타입을 갖음), 그리고 하나의 에지 타입을 갖는 MFG에서 동작한다. 기본적으로 DGL 빌트인 뉴럴 네트워크 모듈의 입력 그래프와 피쳐는 아래 경우들 중에 하나를 만족해야 한다.
|
||||
|
||||
- 입력 피쳐가 텐서들의 쌍인 경우, 입력 그래프는 단방향 이분(unidirectional bipartite) 그래프이어야 한다.
|
||||
- 입력 피쳐가 단일 텐서이고 입력 그래프가 MFG인 경우, DGL은 자동으로 출력 노드의 피쳐를 입력 노드 피처의 첫 몇개의 행으로 정의한다.
|
||||
- 입력 피쳐가 단일 텐서이고 입력 그래프가 MGF가 아닌 경우, 입력 그래프는 반드시 homogeneous여야 한다.
|
||||
|
||||
다음 코드는 :class:`dgl.nn.pytorch.SAGEConv` 을 PyTorch로 단순하게 구현한 것이다. (MXNet이나 TensorFlow 버전도 제공함. (이 코드는 normalization이 제거되어 있고, mean aggregation만 사용한다.)
|
||||
|
||||
.. 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_ko-nn` 은 단방향 이분 그래프, homogeneous 그래프와 MFG에 적용되는 :class:`dgl.nn.pytorch.SAGEConv` 를 자세히 다루고 있다.
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
.. _guide_ko-minibatch-node-classification-sampler:
|
||||
|
||||
6.1 이웃 샘플링을 사용한 노드 분류 GNN 모델 학습하기
|
||||
-----------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-minibatch-node-classification-sampler>`
|
||||
|
||||
Stochastic 학습이 되도록 모델을 만들기 위해서는, 다음과 같은 것이 필요하다.
|
||||
|
||||
- 이웃 샘플러 정의하기
|
||||
- 미니 배치 학습이 되도록 모델을 변경하기
|
||||
- 학습 룹 고치기
|
||||
|
||||
이제, 이 단계를 어떻게 구현하는 하나씩 살펴보자.
|
||||
|
||||
이웃 샘플러 및 데이터 로더 정의하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL는 계산하기를 원하는 노드들에 대해서 각 레이어에서 필요한 computation dependency들을 생성하는 몇 가지 이웃 샘플러 클래스들을 가지고 있다.
|
||||
|
||||
가장 단순한 이웃 샘플러는 :class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler` 로, 노드가 그 노드의 모든 이웃들로부터 메시지를 수집하도록 해준다.
|
||||
|
||||
DGL의 샘플러를 사용하기 위해서는 이를 미니배치에 있는 노드들의 집한은 iterate하는 :class:`~dgl.dataloading.pytorch.NodeDataLoader` 와 합쳐야한다.
|
||||
|
||||
다음 예제 코드는 배치들의 학습 노드 ID 배열 ``train_nids`` 를 iterate하고, 생성된 MFG(Message Flow Graph)들의 목록을 GPU로 옮기는 PyTorch DataLoader를 만든다.
|
||||
|
||||
.. 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를 iterate 하면서 각 레이어에 대한 computation dependency들을 대표하도록 특별하게 생성된 그래프들의 리스트를 얻을 수 있다. DGL에서 이것들을 *message flow graph* (MFG) 라고 부른다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
input_nodes, output_nodes, blocks = next(iter(dataloader))
|
||||
print(blocks)
|
||||
|
||||
Iterator는 매번 세개의 아이템을 생성한다. ``input_nodes`` 는 ``output_nodes`` 의 representation을 계산하는데 필요한 노드들을 담고 있다. ``block`` 은 그것의 노드가 출력으로 계산되어야 할 각 GNN 레이어에 대해 어떤 노드 representation들이 입력으로 필요한지, 입력 노드들의 representation들이 출력 노드로 어떻게 전파되어야 하는지를 설명한다.
|
||||
|
||||
.. note::
|
||||
|
||||
Message flow graph의 개념은 :doc:`Stochastic Training Tutorial <tutorials/large/L0_neighbor_sampling_overview>` 을 참고하자.
|
||||
|
||||
지원되는 빌드인 샘플러들의 전체 목록은 :ref:`neighborhood sampler API reference <api-dataloading-neighbor-sampling>` 에서 찾아볼 수 있다.
|
||||
|
||||
:ref:`guide_ko-minibatch-customizing-neighborhood-sampler` 에는 여러분만의 이웃 샘플러 만드는 방법과 MFG 개념에 대한 보다 상세한 설명을 담고 있다.
|
||||
|
||||
|
||||
.. _guide_ko-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
|
||||
|
||||
이 때, 변경해야할 것은 ``g`` 를 앞에서 생성된 ``block`` 로 교체하는 것이 전부이다.
|
||||
|
||||
.. 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`` 모듈들은 데이터 로더가 생성한 ``block`` 의 원소를 argument로 받는다.
|
||||
|
||||
:ref:`The API reference of each NN module <apinn>` 는 모듈이 MFG를 argument로 받을 수 있는지 없는지를 알려주고 있다.
|
||||
|
||||
만약 여러분 자신의 메시지 전달 모듈을 사용하고 싶다면, :ref:`guide_ko-minibatch-custom-gnn-module` 를 참고하자.
|
||||
|
||||
학습 룹
|
||||
~~~~~
|
||||
|
||||
단순하게 학습 룹은 커스터마이징된 배치 iterator를 사용해서 데이터셋을 iterating하는 것으로 구성된다. MFG들의 리스트를 반환하는 매 iteration마다, 다음과 같은 일을 한다.
|
||||
|
||||
1. 입력 노드들의 노드 피처들을 GPU로 로딩한다. 노드 피쳐들은 메모리나 외부 저장소에 저장되어 있을 수 있다. 그래프 전체 학습에서 모든 노드들의 피처를 로드하는 것과는 다르게, 입력 노드들의 피처만 로드하면 된다는 점을 유의하자.
|
||||
|
||||
|
||||
만약 피쳐들이 ``g.ndata`` 에 저장되어 있다면, 그 피쳐들은 ``blocks[0].srcdata`` 에 저장된 피쳐들, 즉 첫번째 MFG의 소스 노드들의 피처들을 접근해서 로드될 수 있다. 여기서 노드들은 최종 representation을 계산하는데 필요한 모든 노드들을 의미한다.
|
||||
|
||||
2. MFG들의 리스트 및 입력 노드 피쳐들을 멀티-레이어 GNN에 입력해서 결과를
|
||||
얻는다.
|
||||
|
||||
3. 출력 노드에 해당하는 노드 레이블을 GPU에 로드한다. 비슷하게, 노드 레이블은 메모리나 외부 저장소에 저장되어 있을 수 있다. 역시, 그래프 전체 학습에서 모든 노드들의 레이블을 로드하는 것과는 다르게, 출력 노드들의 레이블만 로드한다는 점을 알아두자.
|
||||
|
||||
피처가 ``g.ndata`` 에 저장되어 있다면, 레이블은 ``blocks[-1].dstdata`` 의 피쳐들 즉, 마지막 MFG의 목적지 노드들의 피쳐들을 접근해서 로드될 수 있다. 이것들은 최종 representation을 계산할 노드들과 같다.
|
||||
|
||||
4. loss를 계산한 후, backpropagate를 수행한다.
|
||||
|
||||
.. 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에서는 end-to-end stochastic 학습 예제인 `GraphSAGE
|
||||
implementation <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/node_classification.py>`__ 를 제공한다.
|
||||
|
||||
Heterogeneous 그래프의 경우
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프에 대한 노드 분류 그래프 뉴럴 네트워크를 학습하는 것은 간단하다.
|
||||
|
||||
:ref:`how to train a 2-layer RGCN on full graph <guide_ko-training-rgcn-node-classification>` 를 예로 들어보자. 미니-배치 학습을 하는 RGCN 구현 코드는 이 예제와 매우 비슷하다. (간단하게 하기 위해서 self-loop, non-linearity와 기본적인 decomposition은 제거했다.)
|
||||
|
||||
.. 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이 제공하는 일부 샘플러들은 heterogeneous 그래프를 지원한다. 예를 들어, 제공되는 :class:`~dgl.dataloading.neighbor.MultiLayerFullNeighborSampler` 클래스 및 :class:`~dgl.dataloading.pytorch.NodeDataLoader` 클래스를 stochastic 학습에도 여전히 사용할 수 있다. 전체 이웃 샘플링에서 다른 점은 학습 셋에 노드 타입들과 노드 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)
|
||||
|
||||
학습 룹은 homogeneous 그래프에 대한 학습 룹이랑 거의 유사하다. 다른 점은 ``compute_loss`` 의 구현에서 노드 타입들와 예측 결과라는 두개의 dictionary들을 인자로 받는다는 것이다.
|
||||
|
||||
.. 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()
|
||||
|
||||
End-to-end stochastic 학습 예제는 `RGCN
|
||||
implementation <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/entity_classify_mb.py>`__ 를 참고하자.
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.. _guide_ko-minibatch:
|
||||
|
||||
6장: 큰 그래프에 대한 stochastic 학습
|
||||
===============================
|
||||
|
||||
:ref:`(English Version) <guide-minibatch>`
|
||||
|
||||
만약 수백만, 수십억개의 노드들 또는 에지들을 갖는 큰 그래프인 경우에는 :ref:`guide_ko-training` 에서 소개한 그래프 전체를 사용한 학습을 적용하기 어려울 것이다. Hidden state 크기가 :math:`H` 인 노드가 :math:`N` 개인 그래프에 :math:`L` -레이어의 graph convolutional network를 생각해보자. 중간 hidden 상태를 저장하는데 :math:`(NLH)` 메모리가 필요하고, :math:`N` 이 큰 경우 GPU 하나의 용량을 훨씬 넘을 것이다.
|
||||
|
||||
이 절에서 모든 노드들의 피쳐를 GPU에 올려야할 필요가 없는 stochastic 미니-배치 학습을 수행하는 법을 알아본다.
|
||||
|
||||
이웃 샘플링(Neighborhood Sampling) 방법 개요
|
||||
---------------------------------------
|
||||
|
||||
이웃 샘플링 방법은 일반적으로 다음과 같다. 각 gradient descent 단계마다, :math:`L-1` 레이어의 최종 representation을 계산되어야 할 노드들의 미니 배치를 선택한다. 그 다음으로 :math:`L-1` 레이어에서 그것들의 이웃 전체 또는 일부를 선택한다. 이 절차는 모델의 입력에 이를 때까지 반복된다. 이 반복 프로세스는 출력시작해서 거꾸로 입력까지의 의존성 그래프(dependency graph)를 생성하며, 이를 시각화하면 다음과 같다:
|
||||
|
||||
.. figure:: https://data.dgl.ai/asset/image/guide_6_0_0.png
|
||||
:alt: Imgur
|
||||
|
||||
이를 사용하면, 큰 그래프에 대한 GNN 모델을 학습하는데 필요한 워크로드 및 연산 자원을 절약할 수 있다.
|
||||
|
||||
DGL은 이웃 샘플링을 사용한 GNN 학습을 위한 몇 가지 이웃 샘플러들과 파이프라인을 제공한다. 또한, 샘플링 전략을 커스터마이징하는 방법도 지원한다.
|
||||
|
||||
로드맵
|
||||
----
|
||||
|
||||
이 장은 GNN은 stochastical하게 학습하는 여러 시나리오들로 시작한다.
|
||||
|
||||
* :ref:`guide_ko-minibatch-node-classification-sampler`
|
||||
* :ref:`guide_ko-minibatch-edge-classification-sampler`
|
||||
* :ref:`guide_ko-minibatch-link-classification-sampler`
|
||||
|
||||
이 후 절들에서는 새로운 샘플링 알고리즘들, 미니-배치 학습과 호환되는 새로운 GNN 모듈을 만들고자 하거나, 검증과 추론이 미니-배치에서 어떻게 수행되는지 이해하고 싶은 분들을 위한 보다 고급 토픽들을 다룬다.
|
||||
|
||||
* :ref:`guide_ko-minibatch-customizing-neighborhood-sampler`
|
||||
* :ref:`guide_ko-minibatch-custom-gnn-module`
|
||||
* :ref:`guide_ko-minibatch-inference`
|
||||
|
||||
마지막으로 이웃 샘플링을 구현하고 사용하는데 대한 성능 팁을 알아본다.
|
||||
|
||||
* :ref:`guide_ko-minibatch-gpu-sampling`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
minibatch-node
|
||||
minibatch-edge
|
||||
minibatch-link
|
||||
minibatch-custom-sampler
|
||||
minibatch-nn
|
||||
minibatch-inference
|
||||
minibatch-gpu-sampling
|
||||
@@ -0,0 +1,143 @@
|
||||
.. _guide_ko-mixed_precision:
|
||||
|
||||
8장: Mixed Precision 학습
|
||||
=======================
|
||||
|
||||
:ref:`(English Version) <guide-mixed_precision>`
|
||||
|
||||
DGL은 mixed precision 학습을 위해서 `PyTorch's automatic mixed precision package <https://pytorch.org/docs/stable/amp.html>`_ 와 호환된다. 따라서, 학습 시간 및 GPU 메모리 사용량을 절약할 수 있다.
|
||||
|
||||
Half precision을 사용한 메시지 전달
|
||||
------------------------------
|
||||
|
||||
fp16을 지원하는 DGL은 UDF(User Defined Function)이나 빌트인 함수(예, ``dgl.function.sum``,
|
||||
``dgl.function.copy_u``)를 사용해서 ``float16`` 피쳐에 대한 메시지 전달을 허용한다.
|
||||
|
||||
|
||||
다음 예제는 DGL 메시지 전달 API를 half-precision 피쳐들에 사용하는 방법을 보여준다.
|
||||
|
||||
>>> import torch
|
||||
>>> import dgl
|
||||
>>> import dgl.function as fn
|
||||
>>> g = dgl.rand_graph(30, 100).to(0) # Create a graph on GPU w/ 30 nodes and 100 edges.
|
||||
>>> g.ndata['h'] = torch.rand(30, 16).to(0).half() # Create fp16 node features.
|
||||
>>> g.edata['w'] = torch.rand(100, 1).to(0).half() # Create fp16 edge features.
|
||||
>>> # Use DGL's built-in functions for message passing on fp16 features.
|
||||
>>> g.update_all(fn.u_mul_e('h', 'w', 'm'), fn.sum('m', 'x'))
|
||||
>>> g.ndata['x'][0]
|
||||
tensor([0.3391, 0.2208, 0.7163, 0.6655, 0.7031, 0.5854, 0.9404, 0.7720, 0.6562,
|
||||
0.4028, 0.6943, 0.5908, 0.9307, 0.5962, 0.7827, 0.5034],
|
||||
device='cuda:0', dtype=torch.float16)
|
||||
>>> g.apply_edges(fn.u_dot_v('h', 'x', 'hx'))
|
||||
>>> g.edata['hx'][0]
|
||||
tensor([5.4570], device='cuda:0', dtype=torch.float16)
|
||||
>>> # Use UDF(User Defined Functions) for message passing on fp16 features.
|
||||
>>> def message(edges):
|
||||
... return {'m': edges.src['h'] * edges.data['w']}
|
||||
...
|
||||
>>> def reduce(nodes):
|
||||
... return {'y': torch.sum(nodes.mailbox['m'], 1)}
|
||||
...
|
||||
>>> def dot(edges):
|
||||
... return {'hy': (edges.src['h'] * edges.dst['y']).sum(-1, keepdims=True)}
|
||||
...
|
||||
>>> g.update_all(message, reduce)
|
||||
>>> g.ndata['y'][0]
|
||||
tensor([0.3394, 0.2209, 0.7168, 0.6655, 0.7026, 0.5854, 0.9404, 0.7720, 0.6562,
|
||||
0.4028, 0.6943, 0.5908, 0.9307, 0.5967, 0.7827, 0.5039],
|
||||
device='cuda:0', dtype=torch.float16)
|
||||
>>> g.apply_edges(dot)
|
||||
>>> g.edata['hy'][0]
|
||||
tensor([5.4609], device='cuda:0', dtype=torch.float16)
|
||||
|
||||
|
||||
End-to-End Mixed Precision 학습
|
||||
------------------------------
|
||||
|
||||
DGL은 PyTorch의 AMP package를 사용해서 mixed precision 학습을 구현하고 있어서, 사용 방법은 `PyTorch의 것 <https://pytorch.org/docs/stable/notes/amp_examples.html>`_ 과 동일하다.
|
||||
|
||||
GNN 모델의 forward 패스(loss 계산 포함)를 ``torch.cuda.amp.autocast()`` 로 래핑하면 PyTorch는 각 op 및 텐서에 대해서 적절한 데이터 타입을 자동으로 선택한다. Half precision 텐서는 메모리 효율적이고, half precision 텐서에 대한 대부분 연산들은 GPU tensorcore들을 활용하기 때문에 더 빠르다.
|
||||
|
||||
``float16`` 포멧의 작은 graident들은 언더플로우(underflow) 문제를 갖는데 (0이 되버림), PyTorch는 이를 해결하기 위해서 ``GradScaler`` 모듈을 제공한다. ``GradScaler`` 는 loss 값에 factor를 곱하고, 이 scaled loss에 backward pass를 수행한다. 그리고 파라메터들을 업데이트하는 optimizer를 수행하기 전에 unscale 한다.
|
||||
|
||||
다음은 3-레이어 GAT를 Reddit 데이터셋(1140억개의 에지를 갖는)에 학습을 하는 스크립트이다. ``use_fp16`` 가 활성화/비활성화되었을 때의 코드 차이를 살펴보자.
|
||||
|
||||
.. code::
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.cuda.amp import autocast, GradScaler
|
||||
import dgl
|
||||
from dgl.data import RedditDataset
|
||||
from dgl.nn import GATConv
|
||||
|
||||
use_fp16 = True
|
||||
|
||||
|
||||
class GAT(nn.Module):
|
||||
def __init__(self,
|
||||
in_feats,
|
||||
n_hidden,
|
||||
n_classes,
|
||||
heads):
|
||||
super().__init__()
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.append(GATConv(in_feats, n_hidden, heads[0], activation=F.elu))
|
||||
self.layers.append(GATConv(n_hidden * heads[0], n_hidden, heads[1], activation=F.elu))
|
||||
self.layers.append(GATConv(n_hidden * heads[1], n_classes, heads[2], activation=F.elu))
|
||||
|
||||
def forward(self, g, h):
|
||||
for l, layer in enumerate(self.layers):
|
||||
h = layer(g, h)
|
||||
if l != len(self.layers) - 1:
|
||||
h = h.flatten(1)
|
||||
else:
|
||||
h = h.mean(1)
|
||||
return h
|
||||
|
||||
# Data loading
|
||||
data = RedditDataset()
|
||||
device = torch.device(0)
|
||||
g = data[0]
|
||||
g = dgl.add_self_loop(g)
|
||||
g = g.int().to(device)
|
||||
train_mask = g.ndata['train_mask']
|
||||
features = g.ndata['feat']
|
||||
labels = g.ndata['label']
|
||||
in_feats = features.shape[1]
|
||||
n_hidden = 256
|
||||
n_classes = data.num_classes
|
||||
n_edges = g.num_edges()
|
||||
heads = [1, 1, 1]
|
||||
model = GAT(in_feats, n_hidden, n_classes, heads)
|
||||
model = model.to(device)
|
||||
|
||||
# Create optimizer
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=5e-4)
|
||||
# Create gradient scaler
|
||||
scaler = GradScaler()
|
||||
|
||||
for epoch in range(100):
|
||||
model.train()
|
||||
optimizer.zero_grad()
|
||||
|
||||
# Wrap forward pass with autocast
|
||||
with autocast(enabled=use_fp16):
|
||||
logits = model(g, features)
|
||||
loss = F.cross_entropy(logits[train_mask], labels[train_mask])
|
||||
|
||||
if use_fp16:
|
||||
# Backprop w/ gradient scaling
|
||||
scaler.scale(loss).backward()
|
||||
scaler.step(optimizer)
|
||||
scaler.update()
|
||||
else:
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
print('Epoch {} | Loss {}'.format(epoch, loss.item()))
|
||||
|
||||
NVIDIA V100 (16GB) 한개를 갖는 컴퓨터에서, 이 모델을 fp16을 사용하지 않고 학습할 때는 15.2GB GPU 메모리가 사용되는데, fp16을 활성화하면, 학습에 12.8G GPU 메모리가 사용된며, 두 경우 loss가 비슷한 값으로 수렴한다. 만약 head의 갯수를 ``[2, 2, 2]`` 로 바꾸면, fp16를 사용하지 않는 학습은 GPU OOM(out-of-memory) 이슈가 생길 것이지만, fp16를 사용한 학습은 15.7G GPU 메모리를 사용하면서 수행된다.
|
||||
|
||||
DGL은 half-precision 지원을 계속 향상하고 있고, 연산 커널의 성능은 아직 최적은 아니다. 앞으로의 업데이트를 계속 지켜보자.
|
||||
@@ -0,0 +1,69 @@
|
||||
.. _guide_ko-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 모듈의 경우에는 차원이란 보통은 입력 차원, 출력 차원, 그리고 은닉(hidden) 치원을 의미하는데, 그래프 뉴럴 네트워크의 경우 입력 차원은 소스 노드의 차원과 목적지 노드의 차원으로 나뉜다.
|
||||
|
||||
데이터 차원들 이외의 전형적인 그래프 뉴럴 네트워크의 옵션으로 aggregation 타입( ``self._aggre_type`` )이 있다. Aggregation 타입은 특정 목적지 노드에 대해서 관련된 여러 에지의 메시지들이 어떻게 집합되어야 하는지를 결정한다. 흔히 사용되는 aggregation 타입으로는 ``mean`` , ``sum`` , ``max`` , ``min`` 이 있으며, 어떤 모듈은 ``lstm`` 과 같이 좀더 복잡한 aggregation을 적용하기도 한다.
|
||||
|
||||
여기서 ``norm`` 은 피처 normalization을 위해서 호출될 수 있는 함수이다. SAGEConv 페이퍼에서는 l2 normlization, :math:`h_v = h_v / \lVert h_v \rVert_2` 이 normalization으로 사용되고 있다.
|
||||
|
||||
.. code::
|
||||
|
||||
# aggregator type: mean, pool, lstm, gcn
|
||||
if aggregator_type not in ['mean', 'pool', 'lstm', 'gcn']:
|
||||
raise KeyError('Aggregator type {} not supported.'.format(aggregator_type))
|
||||
if aggregator_type == 'pool':
|
||||
self.fc_pool = nn.Linear(self._in_src_feats, self._in_src_feats)
|
||||
if aggregator_type == 'lstm':
|
||||
self.lstm = nn.LSTM(self._in_src_feats, self._in_src_feats, batch_first=True)
|
||||
if aggregator_type in ['mean', 'pool', 'lstm']:
|
||||
self.fc_self = nn.Linear(self._in_dst_feats, out_feats, bias=bias)
|
||||
self.fc_neigh = nn.Linear(self._in_src_feats, out_feats, bias=bias)
|
||||
self.reset_parameters()
|
||||
|
||||
다음으로는 파라메터들과 서브모듈들을 등록한다. SAGEConv의 경우에는 서브모듈은 aggregation 타입에 따라 달라진다. 그 모듈들은 ``nn.Linear`` , ``nn.LSTM`` 등과 같은 순수한 PyTorch nn 모듈이다. 생성 함수의 마지막에는 ``reset_parameters()`` 호출로 가중치들을 초기화한다.
|
||||
|
||||
.. code::
|
||||
|
||||
def reset_parameters(self):
|
||||
"""Reinitialize learnable parameters."""
|
||||
gain = nn.init.calculate_gain('relu')
|
||||
if self._aggre_type == 'pool':
|
||||
nn.init.xavier_uniform_(self.fc_pool.weight, gain=gain)
|
||||
if self._aggre_type == 'lstm':
|
||||
self.lstm.reset_parameters()
|
||||
if self._aggre_type != 'gcn':
|
||||
nn.init.xavier_uniform_(self.fc_self.weight, gain=gain)
|
||||
nn.init.xavier_uniform_(self.fc_neigh.weight, gain=gain)
|
||||
@@ -0,0 +1,125 @@
|
||||
.. _guide_ko-nn-forward:
|
||||
|
||||
3.2 DGL NN 모듈의 Forward 함수
|
||||
---------------------------
|
||||
|
||||
:ref:`(English Versin) <guide-nn-forward>`
|
||||
|
||||
NN 모듈에서 ``forward()`` 함수는 실제 메시지 전달과 연산을 수행한다. 일반적으로 텐서들을 파라메터로 받는 PyTorch의 NN 모듈과 비교하면, DGL NN 모듈은 :class:`dgl.DGLGraph` 를 추가 파라메터로 받는다. ``forward()`` 함수는 3단계로 수행된다.
|
||||
|
||||
- 그래프 체크 및 그래프 타입 명세화
|
||||
- 메시지 전달
|
||||
- 피쳐 업데이트
|
||||
|
||||
이 절에서는 SAGEConv에서 사용되는 ``forward()`` 함수를 자세하게 살펴보겠다.
|
||||
|
||||
그래프 체크와 그래프 타입 명세화(graph type specification)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
def forward(self, graph, feat):
|
||||
with graph.local_scope():
|
||||
# Specify graph type then expand input feature according to graph type
|
||||
feat_src, feat_dst = expand_as_pair(feat, graph)
|
||||
|
||||
``forward()`` 는 계산 및 메시지 전달 과정에서 유효하지 않은 값을 만들 수 있는 여러 특별한 케이스들을 다룰 수 있어야 한다. :class:`~dgl.nn.pytorch.conv.GraphConv` 와 같은 그래프 conv 모듈에서 수행하는 가장 전형적인 점검은 입력 그래프가 in-degree가 0인 노드를 갖지 않는지 확인하는 것이다. in-degree가 0인 경우에, ``mailbox`` 에 아무것도 없게 되고, 축약 함수는 모두 0인 값을 만들어낼 것이다. 이는 잠재적인 모델 성능 문제를 일이킬 수도 있다. 하지만, :class:`~dgl.nn.pytorch.conv.SAGEConv` 모듈의 경우, aggregated representation은 원래의 노드 피쳐와 연결(concatenated)되기 때문에, ``forward()`` 의 결과는 항상 0이 아니기 때문에, 이런 체크가 필요 없다.
|
||||
|
||||
DGL NN 모듈은 여러 종류의 그래프, 단종 그래프, 이종 그래프(:ref:`guide_ko-graph-heterogeneous`), 서브그래프 블록(:ref:`guide_ko-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``)를 명시해야 한다. :meth:`~dgl.utils.expand_as_pair` 는 명시된 그래프 타입에 따라 ``feat`` 를 ``feat_src`` 와 ``feat_dst`` 로 확장하는 함수이다. 이 함수의 동작은 다음과 같다.
|
||||
|
||||
.. code::
|
||||
|
||||
def expand_as_pair(input_, g=None):
|
||||
if isinstance(input_, tuple):
|
||||
# Bipartite graph case
|
||||
return input_
|
||||
elif g is not None and g.is_block:
|
||||
# Subgraph block case
|
||||
if isinstance(input_, Mapping):
|
||||
input_dst = {
|
||||
k: F.narrow_row(v, 0, g.number_of_dst_nodes(k))
|
||||
for k, v in input_.items()}
|
||||
else:
|
||||
input_dst = F.narrow_row(input_, 0, g.number_of_dst_nodes())
|
||||
return input_, input_dst
|
||||
else:
|
||||
# Homogeneous graph case
|
||||
return input_, input_
|
||||
|
||||
homogeneous 그래프 전체를 학습시키는 경우, 소스 노드와 목적지 노드들의 타입이 같다. 이것들은 그래프의 전체 노드들이다.
|
||||
|
||||
Heterogeneous 그래프의 경우, 그래프는 여러 이분 그래프로 나뉠 수 있다. 즉, 각 관계당 하나의 그래프로. 관계는 ``(src_type, edge_type, dst_dtype)`` 로 표현된다. 입력 피쳐 ``feat`` 가 tuple 이라고 확인되면, 이 함수는 그 그래프는 이분 그래프로 취급한다. Tuple의 첫번째 요소는 소스 노드 피처이고, 두번째는 목적지 노드의 피처이다.
|
||||
|
||||
미니-배치 학습의 경우, 연산이 여러 목적지 노드들을 기반으로 샘플된 서브 그래프에 적용된다. DGL에서 서브 그래프는 ``block`` 이라고 한다. 블록이 생성되는 단계에서, ``dst_nodes`` 가 노드 리스트의 앞에 놓이게 된다. ``[0:g.number_of_dst_nodes()]`` 인덱스를 이용해서 ``feat_dst`` 를 찾아낼 수 있다.
|
||||
|
||||
``feat_src`` 와 ``feat_dst`` 가 정해진 후에는, 세가지 그래프 타입들에 대한 연산은 모두 동일하다.
|
||||
|
||||
메시지 전달과 축약
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
import dgl.function as fn
|
||||
import torch.nn.functional as F
|
||||
from dgl.utils import check_eq_shape
|
||||
|
||||
if self._aggre_type == 'mean':
|
||||
graph.srcdata['h'] = feat_src
|
||||
graph.update_all(fn.copy_u('h', 'm'), fn.mean('m', 'neigh'))
|
||||
h_neigh = graph.dstdata['neigh']
|
||||
elif self._aggre_type == 'gcn':
|
||||
check_eq_shape(feat)
|
||||
graph.srcdata['h'] = feat_src
|
||||
graph.dstdata['h'] = feat_dst
|
||||
graph.update_all(fn.copy_u('h', 'm'), fn.sum('m', 'neigh'))
|
||||
# divide in_degrees
|
||||
degs = graph.in_degrees().to(feat_dst)
|
||||
h_neigh = (graph.dstdata['neigh'] + graph.dstdata['h']) / (degs.unsqueeze(-1) + 1)
|
||||
elif self._aggre_type == 'pool':
|
||||
graph.srcdata['h'] = F.relu(self.fc_pool(feat_src))
|
||||
graph.update_all(fn.copy_u('h', 'm'), fn.max('m', 'neigh'))
|
||||
h_neigh = graph.dstdata['neigh']
|
||||
else:
|
||||
raise KeyError('Aggregator type {} not recognized.'.format(self._aggre_type))
|
||||
|
||||
# GraphSAGE GCN does not require fc_self.
|
||||
if self._aggre_type == 'gcn':
|
||||
rst = self.fc_neigh(h_neigh)
|
||||
else:
|
||||
rst = self.fc_self(h_self) + self.fc_neigh(h_neigh)
|
||||
|
||||
이 코드는 실제로 메시지 전달과 축약 연산을 실행하고 있다. 이 부분의 코드는 모듈에 따라 다르게 구현된다. 이 코드의 모든 메시지 전달은 :meth:`~dgl.DGLGraph.update_all` API와 ``built-in`` 메시지/축약 함수들로 구현되어 있는데, 이는 :ref:`guide_ko-message-passing-efficient` 에서 설명된 DGL의 성능 최적화를 모두 활용하기 위해서이다.
|
||||
|
||||
출력값을 위한 축약 후 피쳐 업데이트
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
# activation
|
||||
if self.activation is not None:
|
||||
rst = self.activation(rst)
|
||||
# normalization
|
||||
if self.norm is not None:
|
||||
rst = self.norm(rst)
|
||||
return rst
|
||||
|
||||
``forward()`` 함수의 마지막 부분은 ``reduce function`` 다음에 피쳐를 업데이트하는 것이다. 일반적인 업데이트 연산들은 활성화 함수를 적용하고, 객체 생성 단계에서 설정된 옵션에 따라 normalization을 수행한다.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.. _guide_ko-nn-heterograph:
|
||||
|
||||
3.3 Heterogeneous GraphConv 모듈
|
||||
-------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-nn-heterograph>`
|
||||
|
||||
:class:`~dgl.nn.pytorch.HeteroGraphConv` 는 heterogeneous 그래프들에 DGL NN 모듈을 적용하기 위한 모듈 수준의 인캡슐레이션이다. 메시지 전달 API :meth:`~dgl.DGLGraph.multi_update_all` 와 같은 로직으로 구현되어 있고, 이는 다음을 포함한다.
|
||||
|
||||
- :math:`r` 관계에 대한 DGL NN 모듈
|
||||
- 한 노드에 연결된 여러 관계로부터 얻은 결과를 통합하는 축약(reduction)
|
||||
|
||||
이는 다음과 같이 공식으로 표현된다:
|
||||
|
||||
.. 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` 는 aggregation 함수이다.
|
||||
|
||||
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):
|
||||
# An internal function to get common aggregation functions
|
||||
self.agg_fn = get_aggregate_fn(aggregate)
|
||||
else:
|
||||
self.agg_fn = aggregate
|
||||
|
||||
Heterograph convolution은 각 관계를 NN 모듈에 매핑하는 ``mods`` 사전을 인자로 받고, 한 노드에 대한 여러 관계들의 결과를 집계하는 함수를 설정한다.
|
||||
|
||||
.. 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()`` 함수는 두가지 추가적인 파라메터들, ``mod_args`` 와 ``mod_kwargs`` 을 받는다. 이것들은 ``self.mods`` 안에서, 다른 종류의 관계에 연관된 NN 모듈을 수행할 때, 커스터마이즈된 파라메터들로써 사용된다.
|
||||
|
||||
각 목적지 타입 ``nty`` 에 대한 결과 텐서를 저장하기 위해서 결과 사전(output dictionary)가 생성된다. 각 ``nty`` 에 대한 값은 리스트이다. 이는 ``nty`` 를 목적 타입으로 갖을 관계가 여러개가 있는 경우, 단일 노드 타입이 여러 아웃풋들을 갖을 수 있음을 의미한다. ``HeteroGraphConv`` 는 이 리스트들에 대해서 추가적인 aggregation을 수행할 것이다.
|
||||
|
||||
.. 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`` 는 heterogeneous 그래프 또는 heterogeneous 그래프의 서브그래프 블록일 수 있다. 보통의 NN 모듈처럼, ``forward()`` 함수는 다양한 입력 그래프 타입들을 별로도 다룰 수 있어야 한다.
|
||||
|
||||
각 관계는 ``(stype, etype, dtype)`` 인 ``canonical_etype`` 으로 표현된다. ``canonical_etype`` 을 키로 사용해서, 이분 그래프(bipartite graph)인 ``rel_graph`` 를 추출할 수 있다. 이분 그래프에서 입력 피쳐는 ``(src_inputs[stype], dst_inputs[dtype])`` 로 구성된다. 각 관계에 대한 NN 모듈이 호출되고, 결과는 저장된다.
|
||||
|
||||
.. code::
|
||||
|
||||
rsts = {}
|
||||
for nty, alist in outputs.items():
|
||||
if len(alist) != 0:
|
||||
rsts[nty] = self.agg_fn(alist, nty)
|
||||
|
||||
마지막으로 한 목적 노드 타입에 대해 여러 관계로 부터 얻어진 결과들은 ``self.agg_fn`` 를 통해서 집계된다. :class:`~dgl.nn.pytorch.HeteroGraphConv` 의 API DOC에서 관련 예제들이 있다.
|
||||
@@ -0,0 +1,28 @@
|
||||
.. _guide_ko-nn:
|
||||
|
||||
3장: GNN 모듈 만들기
|
||||
=================
|
||||
|
||||
:ref:`(English Version) <guide-nn>`
|
||||
|
||||
DGL NN 모듈은 GNN 모델을 만드는데 필요한 빌딩 블록들로 구성되어 있다. NN 모듈은 백엔드로 사용되는 DNN 프레임워크에 따라 `Pytorch’s NN Module <https://pytorch.org/docs/1.2.0/_modules/torch/nn/modules/module.html>`__ , `MXNet Gluon’s NN Block <http://mxnet.incubator.apache.org/versions/1.6/api/python/docs/api/gluon/nn/index.html>`__ 그리고 `TensorFlow’s Keras Layer <https://www.tensorflow.org/api_docs/python/tf/keras/layers>`__ 를 상속한다. DGL NN 모듈에서, 생성 함수에서의 파라메터 등록과 forward 함수에서 텐서 연산은 백엔드 프레임워크의 것과 동일하다. 이런 방식의 구현덕에 DGL 코드는 백엔드 프레임워크 코드와 원활하게 통합될 수 있다. 주요 차이점은 DGL 고유의 메시지 전달 연산에 존재한다.
|
||||
|
||||
DGL은 일반적으로 많이 사용되는 :ref:`apinn-pytorch-conv` , :ref:`apinn-pytorch-dense-conv` , :ref:`apinn-pytorch-pooling` 와 :ref:`apinn-pytorch-util` 를 포함하고 있고. 여러분의 기여를 환영한다.
|
||||
|
||||
이 장에서는 PyTorch 백엔드를 사용한 :class:`~dgl.nn.pytorch.conv.SAGEConv` 를 예제로 커스텀 DGL NN 모듈을 만드는 방법을 소개한다.
|
||||
|
||||
로드맵
|
||||
----
|
||||
|
||||
* :ref:`guide_ko-nn-construction`
|
||||
* :ref:`guide_ko-nn-forward`
|
||||
* :ref:`guide_ko-nn-heterograph`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
nn-construction
|
||||
nn-forward
|
||||
nn-heterograph
|
||||
@@ -0,0 +1,273 @@
|
||||
.. _guide_ko-training-edge-classification:
|
||||
|
||||
5.2 에지 분류 및 리그레션(Regression)
|
||||
--------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-edge-classification>`
|
||||
|
||||
때론 그래프의 에지들의 속성을 예측을 원하는 경우가 있다. 이를 위해서 *에지 분류/리그레션* 모델을 만들고자 한다.
|
||||
|
||||
우선, 예제로 사용할 에지 예측을 위한 임의의 그래프를 만든다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
src = np.random.randint(0, 100, 500)
|
||||
dst = np.random.randint(0, 100, 500)
|
||||
# make it symmetric
|
||||
edge_pred_graph = dgl.graph((np.concatenate([src, dst]), np.concatenate([dst, src])))
|
||||
# synthetic node and edge features, as well as edge labels
|
||||
edge_pred_graph.ndata['feature'] = torch.randn(100, 10)
|
||||
edge_pred_graph.edata['feature'] = torch.randn(1000, 10)
|
||||
edge_pred_graph.edata['label'] = torch.randn(1000)
|
||||
# synthetic train-validation-test splits
|
||||
edge_pred_graph.edata['train_mask'] = torch.zeros(1000, dtype=torch.bool).bernoulli(0.6)
|
||||
|
||||
개요
|
||||
~~~~~~~~~
|
||||
|
||||
앞 절에서 우리는 멀티 레이어 GNN을 사용해서 노드 분류하는 방법을 알아봤다. 임의의 노드에 대한 hidden representation을 계산하기 위해서 같은 기법을 적용한다. 그러면 에지들에 대한 예측은 그것들의 부속 노드들의 representation들로 부터 도출할 수 있다.
|
||||
|
||||
에지에 대한 예측을 계산하는 가장 일반적인 방법은 그 에지의 부속 노드들의 representation들과 부수적으로 그 에지에 대한 피쳐들의 parameterized 함수로 표현하는 것이다.
|
||||
|
||||
노드 분류 모델과 구현상의 차이점
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
이전 절에서 만든 모델을 사용해서 노드 representation을 계산한다고 가정하면, :meth:`~dgl.DGLGraph.apply_edges` 메소드로 에지 예측을 계산하는 컴포넌트만 작성하면 된다.
|
||||
|
||||
예를 들어, 에지 리그레션을 위해서 각 에지에 대한 점수를 계산하고자 한다면, 아래 코드와 같이 각 에지에 대한 부속 노드의 representation들의 dot product를 계산하면 된다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.function as fn
|
||||
class DotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations computed from the GNN defined
|
||||
# in the node classification section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
|
||||
return graph.edata['score']
|
||||
|
||||
또한 MLP를 사용해서 각 에지에 대한 벡터 값을 예측하는 예측하는 함수를 작성할 수도 있다. 이 벡터 값은 미래의 다운스트림 테스크들에 사용될 수 있다. 즉, 범주형 분류의 logit으로 사용.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class MLPPredictor(nn.Module):
|
||||
def __init__(self, in_features, out_classes):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_features * 2, out_classes)
|
||||
|
||||
def apply_edges(self, edges):
|
||||
h_u = edges.src['h']
|
||||
h_v = edges.dst['h']
|
||||
score = self.W(torch.cat([h_u, h_v], 1))
|
||||
return {'score': score}
|
||||
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations computed from the GNN defined
|
||||
# in the node classification section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(self.apply_edges)
|
||||
return graph.edata['score']
|
||||
|
||||
학습 룹(loop)
|
||||
~~~~~~~~~~~
|
||||
|
||||
노드 representation 계산 모델과 에지 예측 모델을 만들었다면, 모든 에지들에 대한 예측값을 계산하는 전체 그래프를 이용한 학습 룹을 작성할 수 있다.
|
||||
|
||||
노드 representation 계산 모델로 ``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)
|
||||
|
||||
이 예제에서 학습/검증/테스트 에지 셋이 에지의 이진 마스크로 구분된다고 가정한다. 또한 early stopping이나 모델 저장은 포함하지 않는다.
|
||||
|
||||
.. 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_ko-training-edge-classification-heterogeneous-graph:
|
||||
|
||||
Heterogeneous 그래프
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프들에 대한 에지 분류는 homogeneous 그래프와 크게 다르지 않다. 하나의 에지 타입에 대해서 에지 분류를 수행하자 한다면, 모든 노드 티압에 대한 노드 representation을 구하고, :meth:`~dgl.DGLGraph.apply_edges` 메소드를 사용해서 에지 타입을 예측하면 된다.
|
||||
|
||||
예를 들면, heterogeneous 그래프의 하나의 에지 타입에 대한 동작하는 ``DotProductPredictor`` 를 작성하고자 한다면, ``apply_edges`` 메소드에 해당 에지 타입을 명시하기만 하면 된다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroDotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h, etype):
|
||||
# h contains the node representations for each edge type computed from
|
||||
# the GNN for heterogeneous graphs defined in the node classification
|
||||
# section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h # assigns 'h' of all node types in one shot
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
비슷하게 ``HeteroMLPPredictor`` 를 작성할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroMLPPredictor(nn.Module):
|
||||
def __init__(self, in_features, out_classes):
|
||||
super().__init__()
|
||||
self.W = nn.Linear(in_features * 2, out_classes)
|
||||
|
||||
def apply_edges(self, edges):
|
||||
h_u = edges.src['h']
|
||||
h_v = edges.dst['h']
|
||||
score = self.W(torch.cat([h_u, h_v], 1))
|
||||
return {'score': score}
|
||||
|
||||
def forward(self, graph, h, etype):
|
||||
# h contains the node representations for each edge type computed from
|
||||
# the GNN for heterogeneous graphs defined in the node classification
|
||||
# section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h # assigns 'h' of all node types in one shot
|
||||
graph.apply_edges(self.apply_edges, etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
특정 타입의 에지에 대해서, 각 에지의 점수를 예측하는 end-to-end 모델을 다음과 같다:
|
||||
|
||||
.. 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}
|
||||
|
||||
학습 룹은 homogeneous 그래프의 것과 거의 유사하다. 예를 들어, 에지 타입 ``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())
|
||||
|
||||
|
||||
Heterogeneous 그래프의 에지들에 대한 에지 타입 예측하기
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
주어진 에지의 타입을 예측하는 일도 종종 하게된다.
|
||||
|
||||
:ref:`heterogeneous 그래프 예제 <guide_ko-training-heterogeneous-graph-example>` 에서는 user와 item을 연결하는 에지가 주어졌을 때, user가 ``click`` 을 선택할지, ``dislike`` 를 선택할지를 예측하고 있다.
|
||||
|
||||
이는 추천에서 흔히 쓰이는 평가 예측의 간략한 버전이다.
|
||||
|
||||
노드 representation을 얻기 위해서 heterogeneous graph convolution 네트워크를 사용할 수 있다. 이를 위해서 :ref:`이전에 정의한 RGCN <guide_ko-training-rgcn-node-classification>` 를 사용하는 것도 가능하다.
|
||||
|
||||
에지 타입을 예측하기 위해서 ``HeteroDotProductPredictor`` 의 용도를 간단히 변경해서 예측할 모든 에지 타입을 “병합“하고 모든 에지들의 각 타입에 대한 점수를 내보내는 하나의 에지 타입만 있는 다른 그래프를 취하게하면 된다.
|
||||
|
||||
이 예제에 적용해보면, ``user`` 와 ``item`` 두 노트 타입을 갖으며 ``user`` 와 ``item`` 에 대한 ``click`` 이나 ``dislike`` 같은 모든 에지 타입을 병합하는 단일 에지 타입을 갖는 그래프가 필요하다. 다음 문장으로 간단하게 생성할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
dec_graph = hetero_graph['user', :, 'item']
|
||||
|
||||
이 함수는 ``user`` 와 ``item`` 을 노드 타입으로 갖고, 두 노드 타입을 연결하고 있는 모든 에지 타입(예, ``click`` 와 ``dislike`` )을 합친 단일 에지 타입을 갖는 heterogeneous 그래프를 리턴한다.
|
||||
|
||||
위 코드는 원래의 에지 타입을 ``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 contains the node representations for each edge type computed from
|
||||
# the GNN for heterogeneous graphs defined in the node classification
|
||||
# section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h # assigns 'h' of all node types in one shot
|
||||
graph.apply_edges(self.apply_edges)
|
||||
return graph.edata['score']
|
||||
|
||||
노드 representation 모듈과 에지 타입 예측 모듈을 합친 모델은 다음과 같다.
|
||||
|
||||
.. 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())
|
||||
|
||||
DGL은 heterogeneous 그래프의 에지들에 대한 타입을 예측하는 문제인 평가 예측 예제로 `Graph Convolutional Matrix Completion <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__ 를 제공한다. `모델 구현 파일 <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gcmc>`__ 에 있는 노드 representation 모듈은 ``GCMCLayer`` 라고 불린다. 이 둘은 여기서 설명하기에는 너무 복잡하니 자세한 설명은 생략한다.
|
||||
@@ -0,0 +1,250 @@
|
||||
.. _guide_ko-training-graph-classification:
|
||||
|
||||
5.4 그래프 분류
|
||||
------------
|
||||
|
||||
:ref:`(English Version) <guide-training-graph-classification>`
|
||||
|
||||
데이터가 커다란 하나의 그래프가 아닌 여러 그래프로 구성된 경우도 종종 있다. 예를 들면, 사람들의 커뮤니티의 여러 종류 목록 같은 것을 들 수 있다. 같은 커뮤니티에 있는 사람들의 친목 관계를 그래프로 특징을 지어본다면, 분류할 수 있는 그래프들의 리스트를 만들 수 있다. 이 상황에서 그래프 분류 모델을 이용해서 커뮤니티의 종류를 구별해볼 수 있다.
|
||||
|
||||
개요
|
||||
~~~~~~~~~
|
||||
|
||||
그래프 분류가 노드 분류나 링크 예측 문제와 주요 차이점은 예측 결과가 전체 입력 그래프의 특성을 나타낸다는 것이다. 이전 문제들과 똑같이 노드들이나 에지들에 대해서 메시지 전달을 수행하지만, 그래프 수준의 representation을 찾아내야한다.
|
||||
|
||||
그래프 분류 파이프라인은 다음과 같다:
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/batch/graph_classifier.png
|
||||
:alt: Graph Classification Process
|
||||
|
||||
그래프 분류 프로세스
|
||||
|
||||
|
||||
일반적인 방법은 (왼쪽부터 오른쪽으로 진행):
|
||||
|
||||
- 그래프들의 배치를 준비한다
|
||||
- 그래프들의 배치에 메시지 전달을 수행해서 노드/에지 피쳐를 업데이트한다
|
||||
- 노드/에지 피쳐들을 모두 합쳐서 그래프 수준의 representation들을 만든다
|
||||
- 그래프 수준의 representation들을 사용해서 그래프들을 분류한다
|
||||
|
||||
그래프들의 배치(batch)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
보통의 경우 그래프 분류 문제는 많은 수의 그래프를 사용해서 학습하기 때문에, 모델을 학습할 때 그래프를 한개씩 사용하는 것은 굉장히 비효율적이다. 일반적 딥러닝에서 사용되는 미니-배치 학습의 아이디어를 발려와서, 그래프들의 배치를 만들어서 한번의 학습 이터레이션에 사용하는 것이 가능하다.
|
||||
|
||||
DGL는 그래프들의 리스트로부터 하나의 배치 그래프(batched graph)를 생성할 수 있다. 단순하게, 이 배치 그래프는 원래의 작은 그래프들을 연결하는 컴포넌트를 가지고 있는 하나의 큰 그래프로 사용된다.
|
||||
|
||||
.. figure:: https://data.dgl.ai/tutorial/batch/batch.png
|
||||
:alt: Batched Graph
|
||||
|
||||
배치 그래프(Batched Graph)
|
||||
|
||||
다음 코드 예제는 그래프들의 목록에 :func:`dgl.batch` 를 호출한다. 배치 그래프는 하나의 그래프이자, 그 리스트에 대한 정보를 담고 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
import torch as th
|
||||
|
||||
g1 = dgl.graph((th.tensor([0, 1, 2]), th.tensor([1, 2, 3])))
|
||||
g2 = dgl.graph((th.tensor([0, 0, 0, 1]), th.tensor([0, 1, 2, 0])))
|
||||
|
||||
bg = dgl.batch([g1, g2])
|
||||
bg
|
||||
# Graph(num_nodes=7, num_edges=7,
|
||||
# ndata_schemes={}
|
||||
# edata_schemes={})
|
||||
bg.batch_size
|
||||
# 2
|
||||
bg.batch_num_nodes()
|
||||
# tensor([4, 3])
|
||||
bg.batch_num_edges()
|
||||
# tensor([3, 4])
|
||||
bg.edges()
|
||||
# (tensor([0, 1, 2, 4, 4, 4, 5], tensor([1, 2, 3, 4, 5, 6, 4]))
|
||||
|
||||
대부분의 DGL 변환 함수들은 배치 정보를 버린다는 점을 주의하자. 이 정보를 유지하기 위해서, 변환된 그래프에 :func:`dgl.DGLGraph.set_batch_num_nodes` 와 :func:`dgl.DGLGraph.set_batch_num_edges` 를 사용한다.
|
||||
|
||||
그래프 리드아웃(readout)
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
모든 그래프는 노드와 에지의 피쳐들과 더불어 유일한 구조를 지니고 있다. 하나의 예측을 만들어내기 위해서, 보통은 아마도 풍부한 정보들을 합치고 요약한다. 이런 종류의 연산을 *리드아웃(readout)* 이라고 부른다. 흔히 쓰이는 리드아웃 연산들은 모든 노드 또는 에지 피쳐들에 대한 합(summation), 평균, 최대 또는 최소들이 있다.
|
||||
|
||||
그래프 :math:`g` 에 대해서, 평균 노드 피처 리드아웃은 아래와 같이 정의된다.
|
||||
|
||||
.. math:: h_g = \frac{1}{|\mathcal{V}|}\sum_{v\in \mathcal{V}}h_v
|
||||
|
||||
여기서 :math:`h_g` 는 :math:`g` 에 대한 representation이고, :math:`\mathcal{V}` 는 :math:`g` 의 노드들의 집합, 그리고 :math:`h_v` 는 노드 :math:`v` 의 피쳐이다.
|
||||
|
||||
DGL은 많이 쓰이는 리드아웃 연산들을 빌드인 함수로 지원한다. 예를 들어, :func:`dgl.mean_nodes` 는 위의 리드아웃 연산을 구현하고 있다.
|
||||
|
||||
:math:`h_g` 가 구해진 후, 이를 MLP 레이어에 전달해서 분류 결과를 얻는다.
|
||||
|
||||
뉴럴 네트워크 모델 작성하기
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
모델에 대한 입력은 노드와 에지의 피쳐들 갖는 배치 그래프이다.
|
||||
|
||||
배치 그래프에 연산하기
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
첫째로, 배치 그래프에 있는 그래프들을 완전히 분리되어 있다. 즉, 두 그래들 사이에 에지가 존재하지 않는다. 이런 멋진 성질 덕에, 모든 메시지 전달 함수는 같은 결과를 만들어낸다. (즉 그래프 간의 간섭이 없다)
|
||||
|
||||
두번째로, 배치 그래프에 대한 리드아웃 함수는 각 그래프에 별도록 수행된다. 배치 크기가 :math:`B` 이고 협쳐진 피쳐(aggregated feature)의 차원이 :math:`D` 인 경우, 리드아웃 결과의 shape은 :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):
|
||||
# Apply graph convolution and activation.
|
||||
h = F.relu(self.conv1(g, h))
|
||||
h = F.relu(self.conv2(g, h))
|
||||
with g.local_scope():
|
||||
g.ndata['h'] = h
|
||||
# Calculate graph representation by average readout.
|
||||
hg = dgl.mean_nodes(g, 'h')
|
||||
return self.classify(hg)
|
||||
|
||||
학습 룹
|
||||
~~~~~
|
||||
|
||||
데이터 로딩
|
||||
^^^^^^^^
|
||||
|
||||
모델이 정의되었다면, 학습을 시작할 수 있다. 그래프 분류는 커다란 그래프 한개가 아니라 상대적으로 작은 그래프를 많이 다루기 때문에, 복잡한 그래프 샘플링 알고리즘을 사용하지 않고 그래프들의 stochastic 미니-배치를 사용해서 효과적으로 학습을 수행할 수 있다.
|
||||
|
||||
:ref:`guide_ko-data-pipeline` 에서 소개한 그래프 분류 데이터셋을 사용하자.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl.data
|
||||
dataset = dgl.data.GINDataset('MUTAG', False)
|
||||
|
||||
그래프 분류 데이터셋의 각 아이템은 한개의 그래프와 그 그래프의 레이블 쌍이다. 데이터 로딩 프로세스를 빠르게 하기 위해서 GraphDataLoader의 장점을 사용해 그래프들의 데이터셋을 미니-배치 단위로 iterate한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from dgl.dataloading import GraphDataLoader
|
||||
dataloader = GraphDataLoader(
|
||||
dataset,
|
||||
batch_size=1024,
|
||||
drop_last=False,
|
||||
shuffle=True)
|
||||
|
||||
학습 룹은 데이터로더를 iterate하면서 모델을 업데이트하는 것일 뿐이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import torch.nn.functional as F
|
||||
|
||||
# Only an example, 7 is the input feature size
|
||||
model = Classifier(7, 20, 5)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(20):
|
||||
for batched_graph, labels in dataloader:
|
||||
feats = batched_graph.ndata['attr']
|
||||
logits = model(batched_graph, feats)
|
||||
loss = F.cross_entropy(logits, labels)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
|
||||
`DGL's GIN example <https://github.com/dmlc/dgl/tree/master/examples/pytorch/gin>`__ 의 end-to-end 그래프 분류 예를 참고하자. 이 학습 룹은 `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` (MXNet 및 Tensorflow 버전도 있음)와 같은 컴포넌트들과 graph convolution layer와 배치 normalization 등이 적용되어 있다.
|
||||
|
||||
Heterogeneous 그래프
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프들에 대한 그래프 분류는 homogeneous 그래프의 경우와는 약간 차이가 있다. Heterogeneous 그래프와 호환되는 graph convolution 모듈에 더해서, 리드아웃 함수에서 다른 종류의 노드들에 대한 aggregate를 해야한다.
|
||||
|
||||
다음 코드는 각 노트 타입에 대해서 노드 representation을 평균을 합산하는 예제이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class RGCN(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats, rel_names):
|
||||
super().__init__()
|
||||
|
||||
self.conv1 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(in_feats, hid_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
self.conv2 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(hid_feats, out_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
|
||||
def forward(self, graph, inputs):
|
||||
# inputs is features of nodes
|
||||
h = self.conv1(graph, inputs)
|
||||
h = {k: F.relu(v) for k, v in h.items()}
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
class HeteroClassifier(nn.Module):
|
||||
def __init__(self, in_dim, hidden_dim, n_classes, rel_names):
|
||||
super().__init__()
|
||||
|
||||
self.rgcn = RGCN(in_dim, hidden_dim, hidden_dim, rel_names)
|
||||
self.classify = nn.Linear(hidden_dim, n_classes)
|
||||
|
||||
def forward(self, g):
|
||||
h = g.ndata['feat']
|
||||
h = self.rgcn(g, h)
|
||||
with g.local_scope():
|
||||
g.ndata['h'] = h
|
||||
# Calculate graph representation by average readout.
|
||||
hg = 0
|
||||
for ntype in g.ntypes:
|
||||
hg = hg + dgl.mean_nodes(g, 'h', ntype=ntype)
|
||||
return self.classify(hg)
|
||||
|
||||
나머지 코드는 homegeneous 그래프의 경우와 다르지 않다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# etypes is the list of edge types as strings.
|
||||
model = HeteroClassifier(10, 20, 5, etypes)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(20):
|
||||
for batched_graph, labels in dataloader:
|
||||
logits = model(batched_graph)
|
||||
loss = F.cross_entropy(logits, labels)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
@@ -0,0 +1,179 @@
|
||||
.. _guide_ko-training-link-prediction:
|
||||
|
||||
5.3 링크 예측
|
||||
-----------
|
||||
|
||||
:ref:`(English Version) <guide-training-link-prediction>`
|
||||
|
||||
어떤 두 노드들 사이에 에지가 존재하는지 아닌지를 예측하고 싶은 경우가 있고, 이를 *링크 예측 과제* 라고 한다.
|
||||
|
||||
개요
|
||||
~~~~~~~~~
|
||||
|
||||
GNN 기반의 링크 예측 모델은 두 노드 :math:`u` 와 :math:`v` 간의 연결 가능도(likelihood)를 :math:`\boldsymbol{h}_u^{(L)}` 의 함수로 표현하는데, 여기서 :math:`\boldsymbol{h}_v^{(L)}` 는 멀티-레이어 GNN을 통해서 계단된 노드 representation이다.
|
||||
|
||||
.. math::
|
||||
|
||||
|
||||
y_{u,v} = \phi(\boldsymbol{h}_u^{(L)}, \boldsymbol{h}_v^{(L)})
|
||||
|
||||
:math:`y_{u,v}` 는 노드 :math:`u` 와 :math:`v` 사이의 점수를 뜻 한다.
|
||||
|
||||
링크 예측 모델을 학습시키는 것은 에지로 연결된 두 노드들에 대한 점수와 임의의 두 노드 쌍에 대한 점수를 비교하면서 이뤄진다. 예를 들어, 노드 :math:`u` 와 :math:`v` 사이에 에지가 존재하는 경우 노드 :math:`u` 와 :math:`v` 사이의 점수가 노드 :math:`u` 와 임의의 *노이즈* 분표 :math:`v' \sim P_n(v)`에 따라 샘플링된 노드 :math:`v'` 간의 점수보다 높도록 하는 학습이다.
|
||||
|
||||
위를 달성하기 위한 다양한 loss 함수가 있다. 몇 가지 예는 다음과 같다:
|
||||
|
||||
- Cross-entropy loss:
|
||||
:math:`\mathcal{L} = - \log \sigma (y_{u,v}) - \sum_{v_i \sim P_n(v), i=1,\dots,k}\log \left[ 1 - \sigma (y_{u,v_i})\right]`
|
||||
- BPR loss:
|
||||
:math:`\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} - \log \sigma (y_{u,v} - y_{u,v_i})`
|
||||
- Margin loss:
|
||||
:math:`\mathcal{L} = \sum_{v_i \sim P_n(v), i=1,\dots,k} \max(0, M - y_{u, v} + y_{u, v_i})`, 여기서 :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_ko-training-edge-classification>` 에지 리그레션 모델과 동일하다.
|
||||
|
||||
다음은 dot product를 사용해서 에지들의 점수를 계산하는 예제이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class DotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h):
|
||||
# h contains the node representations computed from the GNN defined
|
||||
# in the node classification section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
|
||||
return graph.edata['score']
|
||||
|
||||
학습 룹
|
||||
~~~~~
|
||||
|
||||
점수를 예측하는 모델은 그래프들에 적용되기 때문에, 네가티브 샘들은 별도의 그래프로 표현되어야 한다. 즉, 그것은 에지들이 모두 네가티브 노드들의 쌍들로만 구성된 그래프이다.
|
||||
|
||||
아래 코드는 네가티브 샘들로 구성된 그래프를 만드는 예제이다. 각 에지 :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)
|
||||
|
||||
그런 다음, 학습 룹은 반복적으로 네가티브 그래프를 만들고 loss를 계산한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# Margin loss
|
||||
n_edges = pos_score.shape[0]
|
||||
return (1 - pos_score + neg_score.view(n_edges, -1)).clamp(min=0).mean()
|
||||
|
||||
node_features = graph.ndata['feat']
|
||||
n_features = node_features.shape[1]
|
||||
k = 5
|
||||
model = Model(n_features, 100, 100)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
negative_graph = construct_negative_graph(graph, k)
|
||||
pos_score, neg_score = model(graph, negative_graph, node_features)
|
||||
loss = compute_loss(pos_score, neg_score)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
학습이 종료되면, 노드 representation은 다음과 같이 얻을 수 있다:
|
||||
|
||||
.. code:: python
|
||||
|
||||
node_embeddings = model.sage(graph, node_features)
|
||||
|
||||
노드 임베딩을 사용하는 방법은 여러가지가 있다. 몇가지 예를 들면, 다운스트림 분류기 학습, 관련된 엔터리 추천을 위한 nearest neighbor search 또는 maximum inner product search와 같은 것이 있다.
|
||||
|
||||
Heterogeneous 그래프들
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Heterogeneous 그래프에서의 링크 예측은 homogeneous 그래프에서의 링크 예측과 많이 다르지 않다. 다음 예제는 하나의 에지 타입에 대해서 예측을 수행한다고 가정하고 있는데, 이를 여러 에지 타입으로 확장하는 것은 쉽다.
|
||||
|
||||
링크 예측을 위해서 :ref:`앞에서 <guide_ko-training-edge-classification-heterogeneous-graph>` 의 ``HeteroDotProductPredictor`` 를 재활용해서 한 에지 타입에 대한 에지의 점수를 계산할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
class HeteroDotProductPredictor(nn.Module):
|
||||
def forward(self, graph, h, etype):
|
||||
# h contains the node representations for each node type computed from
|
||||
# the GNN defined in the previous section (Section 5.1).
|
||||
with graph.local_scope():
|
||||
graph.ndata['h'] = h
|
||||
graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
|
||||
return graph.edges[etype].data['score']
|
||||
|
||||
네가티브 샘플링을 수행하기 위해서, 링크 예측을 수행할 에지 타입에 대한 네가티브 그램프를 생성하면 된다.
|
||||
|
||||
.. 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})
|
||||
|
||||
모델을 heterogeneous 그래프들에서 에지 분류하는 모델과는 약간 다른데, 그 이유는 링크 예측을 할 때 에지 타입을 지정해야하기 때문이다.
|
||||
|
||||
.. 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)
|
||||
|
||||
학습 룹은 homogeneous 그래프에 대한 학습 룹과 비슷하다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def compute_loss(pos_score, neg_score):
|
||||
# Margin loss
|
||||
n_edges = pos_score.shape[0]
|
||||
return (1 - pos_score + neg_score.view(n_edges, -1)).clamp(min=0).mean()
|
||||
|
||||
k = 5
|
||||
model = Model(10, 20, 5, hetero_graph.etypes)
|
||||
user_feats = hetero_graph.nodes['user'].data['feature']
|
||||
item_feats = hetero_graph.nodes['item'].data['feature']
|
||||
node_features = {'user': user_feats, 'item': item_feats}
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
for epoch in range(10):
|
||||
negative_graph = construct_negative_graph(hetero_graph, k, ('user', 'click', 'item'))
|
||||
pos_score, neg_score = model(hetero_graph, negative_graph, node_features, ('user', 'click', 'item'))
|
||||
loss = compute_loss(pos_score, neg_score)
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
.. _guide_ko-training-node-classification:
|
||||
|
||||
5.1 노드 분류/리그래션(Regression)
|
||||
--------------------------------------------------
|
||||
|
||||
:ref:`(English Version) <guide-training-node-classification>`
|
||||
|
||||
가장 유명하고 널리 적용되고 있는 그래프 뉴럴 네트워크 중에 하나가 노드 분류이다. 학습/검증/테스트 셋의 각 노드는 미리 정해진 카테로기들로 중에 하나를 ground truth 카테고리로 분류되어 있다. 노드 regression도 비슷하다. 학습/검증/테스트 셋의 각 노드에 ground truth 수가 할당되어 있다.
|
||||
|
||||
개요
|
||||
~~~~~~
|
||||
|
||||
노드를 분류하기 위해서 그래프 뉴럴 네트워크는 :ref:`guide_ko-message-passing` 에서 소개한 메시지 전달 방법을 수행해서 노드 자신의 피쳐 뿐만 아니라 그 노드의 이웃 노드 및 에지의 피쳐도 함께 활용한다. 메시지 전달은 여러 회 반복해서 더 큰 범위의 이웃들에 대한 정보를 활용할 수 있다.
|
||||
|
||||
뉴럴 네트워크 모델 작성하기
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DGL은 한 차례 메시지 전달을 수행하는 몇 가지 빌트인 graph convolution 모듈을 제공한다. 여기서 우리는 GraphSAGE에서 사용되는 graph convolution 모듈인 :class:`dgl.nn.pytorch.SAGEConv` (MXNet과 TensorFlow에서도 사용 가능)를 사용한다.
|
||||
|
||||
보통 그래프에 대한 딥러닝 모델에서는 메시지 전달이 여러 번 수행되는 멀티-레이어 그래프 뉴럴 네트워크가 필요하다. 이는 다음 코드처럼 graph convolution 모듈들을 쌓아서 구현할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Contruct a two-layer GNN model
|
||||
import dgl.nn as dglnn
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
class SAGE(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats):
|
||||
super().__init__()
|
||||
self.conv1 = dglnn.SAGEConv(
|
||||
in_feats=in_feats, out_feats=hid_feats, aggregator_type='mean')
|
||||
self.conv2 = dglnn.SAGEConv(
|
||||
in_feats=hid_feats, out_feats=out_feats, aggregator_type='mean')
|
||||
|
||||
def forward(self, graph, inputs):
|
||||
# inputs are features of nodes
|
||||
h = self.conv1(graph, inputs)
|
||||
h = F.relu(h)
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
위 모델은 노드 분류 뿐만 아니라, :ref:`guide_ko-training-edge-classification` , :ref:`guide_ko-training-link-prediction` , 또는 :ref:`guide_ko-training-graph-classification` 와 같은 다른 다운스트림 테스크들을 위한 히든 노드 표현을 구하기 위해서 사용될 수 있음을 알아두자.
|
||||
|
||||
빌트인 graph convolution 모듈의 전체 목록은 :ref:`apinn` 를 참고하자.
|
||||
|
||||
DGL 뉴럴 네트워크 모듈이 어떻게 동작하는지 그리고 메시지 전달을 활용한 커스텀 뉴럴 네트워크 모듈을 작성하는 방법은 :ref:`guide_ko-nn` 에 있는 예제들을 참고하자.
|
||||
|
||||
학습 룹(loop)
|
||||
~~~~~~~~~~~
|
||||
|
||||
전체 그래프를 이용한 학습은 단지 위에서 정의된 모델에 forward propagation 그리고 학습 노드들의 groud truth 레이블과 예측을 비교해서 loss를 계산하는 것으로 구성된다.
|
||||
|
||||
이 절은 빌드인 데이터셋 :class:`dgl.data.CiteseerGraphDataset` 을 사용해서 학습 룹을 설명한다. 노드 피처 및 레이블은 각 그래프 인스턴스에 저장되어 있고, 학습-검증-테스트 분할 또한 그래프에 이진 마스크로서 저장되어 있다. 이는 :ref:`guide_ko-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)
|
||||
|
||||
다음은 정확도(accuracy)로 모델을 평가하는 예제 코드이다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def evaluate(model, graph, features, labels, mask):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
logits = model(graph, features)
|
||||
logits = logits[mask]
|
||||
labels = labels[mask]
|
||||
_, indices = torch.max(logits, dim=1)
|
||||
correct = torch.sum(indices == labels)
|
||||
return correct.item() * 1.0 / len(labels)
|
||||
|
||||
그리고, 학습 룹은 다음과 같이 작성할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = SAGE(in_feats=n_features, hid_feats=100, out_feats=n_labels)
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for epoch in range(10):
|
||||
model.train()
|
||||
# forward propagation by using all nodes
|
||||
logits = model(graph, node_features)
|
||||
# compute loss
|
||||
loss = F.cross_entropy(logits[train_mask], node_labels[train_mask])
|
||||
# compute validation accuracy
|
||||
acc = evaluate(model, graph, node_features, node_labels, valid_mask)
|
||||
# backward propagation
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
# Save model if necessary. Omitted in this example.
|
||||
|
||||
`GraphSAGE <https://github.com/dmlc/dgl/blob/master/examples/pytorch/graphsage/train_full.py>`__ 는 end-to-end homogeneous 그래프 노드 분류 예제를 제공한다. 해당 모델은 ``GraphSAGE`` 클래스에 구현되어 있고, 조정가능 한 레이어 수, dropout 확률들, 그리고 커스터마이징이 가능한 aggregation 함수 및 비선형성 등의 예제가 포함되어 있다.
|
||||
|
||||
.. _guide_ko-training-rgcn-node-classification:
|
||||
|
||||
Heterogeneous 그래프
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
만약 그래프가 heterogeneous(이종)이라면, 여러분은 노드의 모든 에지 타입에 대한 이웃들로부터 메시지를 수집하기를 원할 것이다. 모든 에지 종류에 대해서 각 에지 타입별로 서로 다른 graph convolution 모듈을 사용한 메시지 전달을 수행하는 것은, :class:`dgl.nn.pytorch.HeteroGraphConv` (MXNet과 Tensorflow에서도 제공함) 모듈을 사용해서 가능하다.
|
||||
|
||||
아래 코드는 heterogeneous graph convolution을 정의하는데, 이는 각 에지 타입에 따라 별도의 graph convolution을 수행하고, 모든 노드 타입들에 대한 결과로서 각 에지 타입에 대한 메시지 aggregation 값들을 합하는 일을 수행한다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Define a Heterograph Conv model
|
||||
|
||||
class RGCN(nn.Module):
|
||||
def __init__(self, in_feats, hid_feats, out_feats, rel_names):
|
||||
super().__init__()
|
||||
|
||||
self.conv1 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(in_feats, hid_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
self.conv2 = dglnn.HeteroGraphConv({
|
||||
rel: dglnn.GraphConv(hid_feats, out_feats)
|
||||
for rel in rel_names}, aggregate='sum')
|
||||
|
||||
def forward(self, graph, inputs):
|
||||
# inputs are features of nodes
|
||||
h = self.conv1(graph, inputs)
|
||||
h = {k: F.relu(v) for k, v in h.items()}
|
||||
h = self.conv2(graph, h)
|
||||
return h
|
||||
|
||||
``dgl.nn.HeteroGraphConv`` 는 노드 타입들과 노드 피쳐 텐서들의 사전을 입력으로 받고, 노드 타입과 노드 피쳐의 다른 사전을 리턴한다.
|
||||
|
||||
여기서 사용되는 데이터셋은 이미 user 및 item 피쳐를 가지고 있고, 이는 :ref:`heterogeneous graph example <guide_ko-training-heterogeneous-graph-example>` 에서 확인할 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
model = RGCN(n_hetero_features, 20, n_user_classes, hetero_graph.etypes)
|
||||
user_feats = hetero_graph.nodes['user'].data['feature']
|
||||
item_feats = hetero_graph.nodes['item'].data['feature']
|
||||
labels = hetero_graph.nodes['user'].data['label']
|
||||
train_mask = hetero_graph.nodes['user'].data['train_mask']
|
||||
|
||||
Forward propagation을 다음과 같이 단순하게 실행된다.
|
||||
|
||||
.. 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']
|
||||
|
||||
학습 룹은 예측을 계산할 노드 representation들의 사전을 사용하는 것을 제외하고는 homogeneous graph의 학습 룹과 동일하다. 예를 들어, ``user`` 노드 만을 예측하고 싶다면, 단지 리턴된 사전에서 ``user`` 노드 임베딩을 추출하면 된다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
opt = torch.optim.Adam(model.parameters())
|
||||
|
||||
for epoch in range(5):
|
||||
model.train()
|
||||
# forward propagation by using all nodes and extracting the user embeddings
|
||||
logits = model(hetero_graph, node_features)['user']
|
||||
# compute loss
|
||||
loss = F.cross_entropy(logits[train_mask], labels[train_mask])
|
||||
# Compute validation accuracy. Omitted in this example.
|
||||
# backward propagation
|
||||
opt.zero_grad()
|
||||
loss.backward()
|
||||
opt.step()
|
||||
print(loss.item())
|
||||
|
||||
# Save model if necessary. Omitted in the example.
|
||||
|
||||
DGL은 `RGCN <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/entity_classify.py>`__ 의 end-to-end 예제를 제공한다. Heterogeneous graph convolution의 정의는 `모델 구현 파일 <https://github.com/dmlc/dgl/blob/master/examples/pytorch/rgcn-hetero/model.py>`__ ``RelGraphConvLayer`` 에서 확인할 수 있다.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
.. _guide_ko-training:
|
||||
|
||||
5장: 그래프 뉴럴 네트워크 학습하기
|
||||
==========================
|
||||
|
||||
:ref:`(English Version) <guide-training>`
|
||||
|
||||
개요
|
||||
----------------
|
||||
|
||||
이 장에서는 :ref:`guide_ko-message-passing` 에서 소개한 메시지 전달 방법과 :ref:`guide_ko-nn` 에서 소개한 뉴럴 네트워크 모듈을 사용해서 작은 그래프들에 대한 노드 분류, 에지 분류, 링크 예측, 그리고 그래프 분류를 위한 그래프 뉴럴 네트워크를 학습하는 방법에 대해서 알아본다.
|
||||
|
||||
여기서는 그래프 및 노드 및 에지 피쳐들이 GPU 메모리에 들어갈 수 있는 크기라고 가정한다. 만약 그렇지 않다면, :ref:`guide_ko-minibatch` 를 참고하자.
|
||||
|
||||
그리고, 그래프와 노드/에지 피쳐들은 이미 프로세싱되어 있다고 가정한다. 만약 DGL에서 제공되는 데이터셋 또는 :ref:`guide_ko-data-pipeline` 에서 소개한 ``DGLDataset`` 과 호환되는 다른 데이터셋을 사용할 계획이라면, 다음과 같이 단일-그래프 데이터셋을 위한 그래프를 얻을 수 있다.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import dgl
|
||||
|
||||
dataset = dgl.data.CiteseerGraphDataset()
|
||||
graph = dataset[0]
|
||||
|
||||
주의: 이 장의 예제들은 PyTorch를 백엔드로 사용한다.
|
||||
|
||||
.. _guide_ko-training-heterogeneous-graph-example:
|
||||
|
||||
Heterogeneous 그래프
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
때로는 heterogeneous 그래프를 사용할 경우도 있다. 노드 분류, 에지 분류, 그리고 링크 예측 과제들의 예제를 위해서 임의로 만든 heterogeneous 그래프를 사용하겠다.
|
||||
|
||||
임의로 생성한 heterogeneous 그래프 ``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()
|
||||
# randomly generate training masks on user nodes and click edges
|
||||
hetero_graph.nodes['user'].data['train_mask'] = torch.zeros(n_users, dtype=torch.bool).bernoulli(0.6)
|
||||
hetero_graph.edges['click'].data['train_mask'] = torch.zeros(n_clicks, dtype=torch.bool).bernoulli(0.6)
|
||||
|
||||
|
||||
로드맵
|
||||
----
|
||||
|
||||
이 장은 그래프 학습 테스크를 설명하기 위해서 4개의 절로 구성되어 있다.
|
||||
|
||||
* :ref:`guide_ko-training-node-classification`
|
||||
* :ref:`guide_ko-training-edge-classification`
|
||||
* :ref:`guide_ko-training-link-prediction`
|
||||
* :ref:`guide_ko-training-graph-classification`
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
:glob:
|
||||
|
||||
training-node
|
||||
training-edge
|
||||
training-link
|
||||
training-graph
|
||||
Reference in New Issue
Block a user