Skip to content
Snippets Groups Projects
Select Git revision
  • 1f8822057a39ec08699e7031d3bbb0108227124f
  • main default protected
2 results

introduction.rst

Blame
  • introduction.rst 18.07 KiB

    예제를 활용한 소개

    저희가 자체적으로 가지고 있는 예제를 통해 PyG 의 기본적인 개념을 간략히 소개하겠습니다.

    그래프 머신러닝에 대한 이해를 돕기 위해, 관심 있는 독자에게 Stanford CS224W: Machine Learning with Graphs 강의를 추천합니다. 직접 경험하면서 얻을 수 있는 PyG에 대한 이해를 돕기 위해, 관심 있는 독자에게 저희의 엄선된 Google Colab Notebooks 을 추천합니다.

    핵심으로써, PyG 는 다음과 같은 주요 기능을 제공합니다:

    그래프의 데이터 처리

    그래프는 객체(노드) 쌍의 관계(엣지)를 모델링하는 데 사용됩니다. PyG 의 단일 그래프는 기본적으로 torch_geometric.data.Data 클래스(class)의 인스턴스(instance)로 설명되며, 기본적으로 다음 속성(attribute)들을 가지고 있습니다:

    • data.x: 노드 특징 행렬(feature matrix)의 모양(shape)은 [num_nodes, num_node_features]
    • data.edge_index: COO format 로 나타내어진 그래프의 연결 상태의 모양(shape)은 [2, num_edges] 그리고 타입은 torch.long
    • data.edge_attr: 엣지 특징 행렬(feature matrix) 의 모양(shape)은 [num_edges, num_edge_features]
    • data.y: 학습하고자 하는 타겟 (다양한 모양(shape)을 가질 수 있음), e.g., 노드 수준의 타겟의 모양(shape)은 [num_nodes, *], 또는 그래프 수준의 타겟의 모양(shape)은 [1, *]
    • data.pos: 노드의 위치 행렬의 모양(shape)은 [num_nodes, num_dimensions]

    이러한 속성들이 전부 필수인 것은 아닙니다. 사실, torch_geometric.data.Data 객체는 이 속성들로만 제한된 것도 아닙니다. e.g. [3, num_faces] 모양(shape)과 torch.long 타입을 갖는 텐서인 3D 메쉬로부터, 삼각형의 연결 상태를 저장하기 위한 data.face 로 확장도 가능합니다.

    Note

    PyTorchtorchvision 에서는 이미지와 타겟의 튜플로써 예시를 표기합니다. 저희는 다양한 데이터 구조를 명확하고 이해하기 쉬운 방식으로 표현하기 위해 이러한 표기법을 생략했습니다.

    3개의 노드와 4개의 엣지를 가진 가중치와 방향성이 없는 그래프로 간단한 예시를 보여드리겠습니다. 각 노드는 하나의 특징(feature)만을 갖습니다:

    import torch
    from torch_geometric.data import Data
    
    edge_index = torch.tensor([[0, 1, 1, 2],
                               [1, 0, 2, 1]], dtype=torch.long)
    x = torch.tensor([[-1], [0], [1]], dtype=torch.float)
    
    data = Data(x=x, edge_index=edge_index)
    >>> Data(edge_index=[2, 4], x=[3, 1])

    모든 엣지의 소스 노드와 타겟 노드를 정의하는 텐서인 edge_index 가 인덱스 튜플의 리스트가 아니 라는 점에 유의하십시오. 이런 방식으로 인덱스를 작성하려면, 데이터 생성자에게 넘겨주기 전에 반드시 edge_index 를 전치하고 contiguous 를 적용해줘야 합니다:

    import torch
    from torch_geometric.data import Data
    
    edge_index = torch.tensor([[0, 1],
                               [1, 0],
                               [1, 2],
                               [2, 1]], dtype=torch.long)
    x = torch.tensor([[-1], [0], [1]], dtype=torch.float)
    
    data = Data(x=x, edge_index=edge_index.t().contiguous())
    >>> Data(edge_index=[2, 4], x=[3, 1])

    비록 그래프가 두 개의 엣지만을 갖지만, 엣지의 양 방향을 모두 고려하면 4개의 인덱스 튜플을 정의해야 합니다.

    Note

    언제든지 데이터 객체를 프린트 해보거나 그것의 특징(feature)과 모양(shape)에 대한 간단한 정보를 확인해 볼 수 있습니다.

    edge_index 의 요소는 반드시 {0, ..., num_nodes - 1} 범위 내의 인덱스를 가져야 한다는 점을 유의하십시오. 이는 최종 데이터의 표현을 최대한 간단하게 만들고자 할 때, 예를 들어, 첫 번째 엣지 (0, 1) 의 시작 노드와 도착 노드의 특징(feature)을 각각 x[0]x[1] 로 나타내기를 원할 때 필요합니다. ~torch_geometric.data.Data.validate 를 실행함으로써 최종적인 ~torch_geometric.data.Data 객체가 이러한 요건을 모두 충족하는지 언제든 확인할 수 있습니다:

    data.validate(raise_on_error=True)

    여러 노드 수준, 엣지 수준, 그래프 수준의 속성(attribute)들을 담는 것 외에도, ~torch_geometric.data.Data 는 다음과 같은 여러 기능의 유용한 함수들을 제공합니다:

    print(data.keys())
    >>> ['x', 'edge_index']
    
    print(data['x'])
    >>> tensor([[-1.0],
                [0.0],
                [1.0]])
    
    for key, item in data:
        print(f'{key} found in data')
    >>> x found in data
    >>> edge_index found in data
    
    'edge_attr' in data
    >>> False
    
    data.num_nodes
    >>> 3
    
    data.num_edges
    >>> 4
    
    data.num_node_features
    >>> 1
    
    data.has_isolated_nodes()
    >>> False
    
    data.has_self_loops()
    >>> False
    
    data.is_directed()
    >>> False
    
    # data 객체를 GPU로 옮김
    device = torch.device('cuda')
    data = data.to(device)

    torch_geometric.data.Data 에서 전체 메소드들의 리스트를 확인할 수 있습니다.

    일반적인 벤치마크 데이터셋

    PyG 는 상당히 많은 일반적인 벤치마크 데이터셋, 예를 들어, Planetoid 데이터셋 전부 (Cora, Citeseer, Pubmed), http://graphkernels.cs.tu-dortmund.de 과 그들의 정제된 버전 으로부터 얻을 수 있는 그래프 분류 데이터셋 전부, QM7 과 QM9 데이터셋, 그리고 FAUST, ModelNet10/40 과 ShapeNet 같은 3D 메쉬/포인트 클라우드 데이터셋 일부를 포함하고 있습니다.

    데이터셋을 초기화하는 것은 간단합니다. 데이터셋을 초기화하면 자동으로 해당하는 원본 파일을 다운로드되고 그것들을 앞서 설명한 ~torch_geometric.data.Data 형식으로 처리합니다.

    예를 들어 , (6개 클래스의 600개 그래프로 구성된) ENZYMES 데이터 집합을 로드하려면 다음과 같이 입력합니다:

    from torch_geometric.datasets import TUDataset
    
    dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')
    >>> ENZYMES(600)
    
    len(dataset)
    >>> 600
    
    dataset.num_classes
    >>> 6
    
    dataset.num_node_features
    >>> 3

    이제 데이터셋의 600개 그래프에 모두 접근할 수 있습니다:

    data = dataset[0]
    >>> Data(edge_index=[2, 168], x=[37, 3], y=[1])
    
    data.is_undirected()
    >>> True

    데이터셋의 첫 번째 그래프에는 37개의 노드가 있으며, 각 노드에는 3개의 피쳐가 있는 것을 확인할 수 있습니다. 168/2 = 84개의 방향성이 없는 엣지가 있고 그래프는 정확히 하나의 클래스에 할당되어 있습니다. 또한, 데이터 객체는 정확히 하나의 그래프 수준의 타겟을 가지고 있습니다.

    슬라이스(slices), 롱(long) 또는 부울(bool) 텐서를 사용하여 데이터 셋을 분할할 수도 있습니다. 예시, 90/10 학습/테스트 분할(train/test split)을 하려면, 다음을 입력하십시오:

    train_dataset = dataset[:540]
    >>> ENZYMES(540)
    
    test_dataset = dataset[540:]
    >>> ENZYMES(60)

    데이터를 분할(split)하기 전에 데이터의 순서가 이미 셔플(shuffle)되었는지 확실하지 않은 경우, 다음을 실행하여 순서를 무작위로 바꿀 수 있습니다:

    dataset = dataset.shuffle()
    >>> ENZYMES(600)

    이것은 다음의 코드와 동일합니다:

    perm = torch.randperm(len(dataset))
    dataset = dataset[perm]
    >> ENZYMES(600)

    다른 것도 시도해봅시다! 반지도학습(semi-supervised) 그래프 노드 분류를 위한 표준 벤치마크 데이터셋인 Cora를 다운로드 해보겠습니다:

    from torch_geometric.datasets import Planetoid
    
    dataset = Planetoid(root='/tmp/Cora', name='Cora')
    >>> Cora()
    
    len(dataset)
    >>> 1
    
    dataset.num_classes
    >>> 7
    
    dataset.num_node_features
    >>> 1433

    여기 이 데이터셋에는 방향성이 없는 인용 그래프(citation graph) 하나만 포함되어 있습니다:

    data = dataset[0]
    >>> Data(edge_index=[2, 10556], test_mask=[2708],
             train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])
    
    data.is_undirected()
    >>> True
    
    data.train_mask.sum().item()
    >>> 140
    
    data.val_mask.sum().item()
    >>> 500
    
    data.test_mask.sum().item()
    >>> 1000

    이번에는, ~torch_geometric.data.Data 클래스(class)의 객체에 각 노드의 레이블과 추가적인 노드 수준의 속성(attribute)인 train_mask , val_mask , test_mask 가 있습니다:

    • train_mask 는 훈련할 노드(140개 노드)를 나타냅니다,
    • val_mask 는 검증 단계(validation)에서, 예를 들어, 조기 종료(early stopping)시키기 위해 사용할 노드(500개 노드)를 나타냅니다,
    • test_mask 는 테스트할 노드(1000개 노드)를 나타냅니다.

    미니 배치(Mini-batches)

    신경망(Neural network)은 주로 배치 별 학습 방식을 사용합니다. PyG 는 희소 블록 대각 인접 행렬(sparse block diagonal adjacency matrix, `edge_index`로 정의됨)을 만들고 노드의 차원으로 특징(feature)과 타겟 행렬을 결합(concatenate)해서 미니 배치를 활용한 병렬화를 가능하게 합니다. 이 구성에 따라 예제마다 하나의 배치당 노드 개수와 엣지 개수를 다르게 가져갈 수 있습니다:

    \mathbf{A} = \begin{bmatrix} \mathbf{A}_1 & & \\ & \ddots & \\ & & \mathbf{A}_n \end{bmatrix}, \qquad \mathbf{X} = \begin{bmatrix} \mathbf{X}_1 \\ \vdots \\ \mathbf{X}_n \end{bmatrix}, \qquad \mathbf{Y} = \begin{bmatrix} \mathbf{Y}_1 \\ \vdots \\ \mathbf{Y}_n \end{bmatrix}
    

    PyG 는 이미 이러한 결합(concatenation) 프로세스를 처리하는 자체적인 torch_geometric.loader.DataLoader 클래스(class)를 가지고 있습니다. 예시를 통해 배워봅시다:

    from torch_geometric.datasets import TUDataset
    from torch_geometric.loader import DataLoader
    
    dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES', use_node_attr=True)
    loader = DataLoader(dataset, batch_size=32, shuffle=True)
    
    for batch in loader:
        batch
        >>> DataBatch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])
    
        batch.num_graphs
        >>> 32

    torch_geometric.data.Batch 클래스(class)는 torch_geometric.data.Data 클래스(class)로부터 상속을 받으며 batch 라는 추가적인 속성(attribute)을 갖습니다.

    batch 는 배치에서 각 노드를 그것이 속하는 그래프에 매핑하는 열 벡터입니다:

    \mathrm{batch} = {\begin{bmatrix} 0 & \cdots & 0 & 1 & \cdots & n - 2 & n -1 & \cdots & n - 1 \end{bmatrix}}^{\top}
    

    이것은, 예를 들어 각 그래프에 속한 노드들의 특징(feature)을 노드 차원에서 평균화하는 데 사용할 수 있습니다:

    from torch_geometric.utils import scatter
    from torch_geometric.datasets import TUDataset
    from torch_geometric.loader import DataLoader
    
    dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES', use_node_attr=True)
    loader = DataLoader(dataset, batch_size=32, shuffle=True)
    
    for data in loader:
        data
        >>> DataBatch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])
    
        data.num_graphs
        >>> 32
    
        x = scatter(data.x, data.batch, dim=0, reduce='mean')
        x.size()
        >>> torch.Size([32, 21])

    여기PyG 의 내부 배치 처리 기법(예: 배치의 동작을 수정하는 방법)에 대해 더 자세히 알아볼 수 있습니다. scatter 연산에 대해 관심 있는 독자분들은 torch_scatter documentation 를 참조하시기 바랍니다.

    Data Transforms

    Transforms are a common way in :obj:`torchvision` to transform images and perform augmentation. :pyg:`PyG` comes with its own transforms, which expect a :class:`~torch_geometric.data.Data` object as input and return a new transformed :class:`~torch_geometric.data.Data` object. Transforms can be chained together using :class:`torch_geometric.transforms.Compose` and are applied before saving a processed dataset on disk (:obj:`pre_transform`) or before accessing a graph in a dataset (:obj:`transform`).

    Let's look at an example, where we apply transforms on the ShapeNet dataset (containing 17,000 3D shape point clouds and per point labels from 16 shape categories).

    from torch_geometric.datasets import ShapeNet
    
    dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'])
    
    dataset[0]
    >>> Data(pos=[2518, 3], y=[2518])

    We can convert the point cloud dataset into a graph dataset by generating nearest neighbor graphs from the point clouds via transforms:

    import torch_geometric.transforms as T
    from torch_geometric.datasets import ShapeNet
    
    dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'],
                        pre_transform=T.KNNGraph(k=6))
    
    dataset[0]
    >>> Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])

    Note

    We use the :obj:`pre_transform` to convert the data before saving it to disk (leading to faster loading times). Note that the next time the dataset is initialized it will already contain graph edges, even if you do not pass any transform. If the :obj:`pre_transform` does not match with the one from the already processed dataset, you will be given a warning.

    In addition, we can use the :obj:`transform` argument to randomly augment a :class:`~torch_geometric.data.Data` object, e.g., translating each node position by a small number:

    import torch_geometric.transforms as T
    from torch_geometric.datasets import ShapeNet
    
    dataset = ShapeNet(root='/tmp/ShapeNet', categories=['Airplane'],
                        pre_transform=T.KNNGraph(k=6),
                        transform=T.RandomJitter(0.01))
    
    dataset[0]
    >>> Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])

    You can find a complete list of all implemented transforms at :mod:`torch_geometric.transforms`.

    Learning Methods on Graphs

    After learning about data handling, datasets, loader and transforms in :pyg:`PyG`, it's time to implement our first graph neural network!

    We will use a simple GCN layer and replicate the experiments on the Cora citation dataset. For a high-level explanation on GCN, have a look at its blog post.

    We first need to load the Cora dataset:

    from torch_geometric.datasets import Planetoid
    
    dataset = Planetoid(root='/tmp/Cora', name='Cora')
    >>> Cora()

    Note that we do not need to use transforms or a dataloader. Now let's implement a two-layer GCN:

    import torch
    import torch.nn.functional as F
    from torch_geometric.nn import GCNConv
    
    class GCN(torch.nn.Module):
        def __init__(self):
            super().__init__()
            self.conv1 = GCNConv(dataset.num_node_features, 16)
            self.conv2 = GCNConv(16, dataset.num_classes)
    
        def forward(self, data):
            x, edge_index = data.x, data.edge_index
    
            x = self.conv1(x, edge_index)
            x = F.relu(x)
            x = F.dropout(x, training=self.training)
            x = self.conv2(x, edge_index)
    
            return F.log_softmax(x, dim=1)

    The constructor defines two :class:`~torch_geometric.nn.conv.GCNConv` layers which get called in the forward pass of our network. Note that the non-linearity is not integrated in the :obj:`conv` calls and hence needs to be applied afterwards (something which is consistent across all operators in :pyg:`PyG`). Here, we chose to use ReLU as our intermediate non-linearity and finally output a softmax distribution over the number of classes. Let's train this model on the training nodes for 200 epochs:

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = GCN().to(device)
    data = dataset[0].to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
    
    model.train()
    for epoch in range(200):
        optimizer.zero_grad()
        out = model(data)
        loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
        loss.backward()
        optimizer.step()

    Finally, we can evaluate our model on the test nodes:

    model.eval()
    pred = model(data).argmax(dim=1)
    correct = (pred[data.test_mask] == data.y[data.test_mask]).sum()
    acc = int(correct) / int(data.test_mask.sum())
    print(f'Accuracy: {acc:.4f}')
    >>> Accuracy: 0.8150

    This is all it takes to implement your first graph neural network. The easiest way to learn more about Graph Neural Networks is to study the examples in the :obj:`examples/` directory and to browse :mod:`torch_geometric.nn`. Happy hacking!

    Exercises

    1. What does :obj:`edge_index.t().contiguous()` do?

    2. Load the :obj:`"IMDB-BINARY"` dataset from the :class:`~torch_geometric.datasets.TUDataset` benchmark suite and randomly split it into 80%/10%/10% training, validation and test graphs.

    3. What does each number of the following output mean?

      print(batch)
      >>> DataBatch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])