PyTorch Geometric 1 - Intro
( 참고 : https://baeseongsu.github.io/posts/pytorch-geometric-introduction/ )
1. 그래프 데이터 다루기
(1) torch_geometric.data.Data
\(G = (V,E)\).
하나의 그래프는 torch_geometric.data.Data
클래스로 표현됨
Notation
- \(N_v\) : number of nodes
- \(d_v\) : number of node features
- \(N_e\) : number of edges
- \(d_e\) : number of edge features
(2) attribute
해당 클래스의 속성에는…
data.x
: node feature- shape : \((N_v, d_v)\)
data.edge_index
: connectivity info- shape : \((2, N_e)\)
data.edge_attr
: edge attribute- shape : \((N_e, d_e)\)
data.y
: label (target)- shape :
- node label : \((N_v, *)\)
- graph label : \((1,*)\)
- shape :
data.pos
: node position- shape : \((N_v, \text{num dimension})\)
Example
import torch
from torch_geometric.data import Data
# number of edges = 4
# number of nodes = 3
# node feature dimension : 1
#------------------------------------------------------#
# edge_index : (2,4)
edge_index = [[0, 1, 1, 2],
[1, 0, 2, 1]]
edge_index = torch.tensor(edge_index, dtype=torch.long) # 정수 형태
# x (node feature) : (3,1)
x = [[-1],[0],[1]]
x = torch.tensor(x, dtype = torch.float)
#------------------------------------------------------#
data = Data(x = x,
edge_index = edge_index)
(3) Method
data.keys
: name of featuredata.num_nodes
: \(N_v\)data.num_edges
: \(N_e\)data.contains_isolated_nodes()
: wheter there is isolated nodesdata.contains_self_loops()
: wheter there is self-loopdata.is_directed()
: wheter the graph is directed graph
2. 벤치마크 데이터셋 : torch_geometric.datasets
대표적인 데이터셋 : ENZYMES, Cora
(1) ENZYMES 데이터셋
특징
- (1) 600개의 그래프
- (2) 6개의 노드 class 종류
- (3) 3차원의 node feature
- Import Dataset
from torch_geometric.datasets import TUDataset
dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')
- Check attributes
# (1) Graph의 개수
len(dataset)
>>> 600
# (2) Node의 클래스 개수
dataset.num_classes
>>> 6
# (3) Node의 feature 차원
dataset.num_node_features
>>> 3
- Check specific graph
# (1) 1번째 그래프 가져오기
## edge 개수 : 168개
## node 개수 : 37개
## node 특징 차원 : 3개
## label : "graph" 단위
data = dataset[0]
>>> Data(edge_index=[2, 168], x=[37, 3], y=[1])
# (2) directed graph 여부 확인
data.is_undirected()
>>> True
# (3) 1~540번 그래프 가져오기
train_dataset = dataset[:540]
>>> ENZYMES(540)
# (4) 541~600번 그래프 가져오기
test_dataset = dataset[540:]
>>> ENZYMES(60)
# (5) 그래프 shuffle
dataset = dataset.shuffle()
>>> ENZYMES(600)
(2) Cora 데이터셋
특징
- (1) 1개의 그래프
- (2) 7개의 노드 class 종류
- (3) 1433차원의 node feature
상세 특징
- 2708개의 “scientific publications”로 구성
- edge : “인용 여부” ( = Citation Network )
- node : 논문
- node feature : 논문에서 자주 등장하는 1433개의 단어의 등장 여부 ( 1/ 0 )
- Import Dataset
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='/tmp/Cora', name='Cora')
- Check attributes
# (1) Graph의 개수
len(dataset)
>>> 1
# (2) Node의 클래스 개수
dataset.num_classes
>>> 7
# (3) Node의 feature 차원
dataset.num_node_features
>>> 1433
- Check specific graph
# (1) 1번째 그래프 가져오기
data = dataset[0]
>>> Data(edge_index=[2, 10556], test_mask=[2708],
train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])
# (2) directed graph 여부 확인
data.is_undirected()
>>> True
# (3) 학습(train)을 위한 마스크(1/0)
data.train_mask.sum().item()
>>> 140
# (4) 검증(validation)을 위한 마스크(1/0)
data.val_mask.sum().item()
>>> 500
# (5) 테스트(test)을 위한 마스크(1/0)
data.test_mask.sum().item()
>>> 1000
3. 미니 배치
(1) 미니 배치
Sparse block diagonal adjacency matrices를 통해 mini-batch 구성
ex) 1개의 batch
\(\mathbf{A}=\left[\begin{array}{ccc} \mathbf{A}_{1} & & \\ & \ddots & \\ & & \mathbf{A}_{n} \end{array}\right], \quad \mathbf{X}=\left[\begin{array}{c} \mathbf{X}_{1} \\ \vdots \\ \mathbf{X}_{n} \end{array}\right], \quad \mathbf{Y}=\left[\begin{array}{c} \mathbf{Y}_{1} \\ \vdots \\ \mathbf{Y}_{n} \end{array}\right]\).
- 하나의 배치 : \(\left(A_{i}, X_{i}, Y_{i}, i \in\{1, \ldots, n\}\right)\)
- 주의 : 만약 batch size가 64라면,
- node가 64개 (X)
- graph자체가 64개 (O)
(2) Data loader
- Import dataset & data loader
from torch_geometric.datasets import TUDataset
from torch_geometric.data import DataLoader
dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES', use_node_attr=True)
loader = DataLoader(dataset,
batch_size=32,
shuffle=True)
- Check data loader
for batch in loader:
batch
# batch size = 32 ( 32개의 그래프 )
# node 개수 = 1082
# edge 개수 = 4066
# node feature = 21 ( 모든 그래프 동일 )
# label : graph 단위
>>> Batch(batch=[1082], edge_index=[2, 4066], x=[1082, 21], y=[32])
batch.num_graphs
>>> 32
4. 데이터 변환
-
torchvision
:torchvision.transforms
=
torch_geometric
:torch_geometric.transforms
-
torch_geometric.transforms.Compose
를 통해, pipeline 식으로 변환 가능
(1) Example : ShapeNet
데이터셋
- Import Packages & Dataset
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.RandomTranslate(0.01))
데이터를 애초에 불러올 때 부터, 변환을 해서 불러올 수 있다.
pre_transform
:pre_transform = T.KNNGraph(k=6)
: KNN 통해, 그래프 생성 ( 6개 )
transform
: 그래프 형성 후에 transformtransform = T.RandomTranslate(0.01)
: node의 위치를 살짝 이동 ( perturbation )
- Check speific graph
dataset[0]
>>> Data(edge_index=[2, 15108], pos=[2518, 3], y=[2518])
- 1번째 그래프 확인
- edge 개수 : 15,108
- node 개수 : 2,518
- node feature 개수 : 3
5. 그래프로 학습하기 (GNN)
Task : Graph Node CLASSIFICATION
- (1) 특정 논문 내에 등장하는 단어 (node feature)
- (2) 논문들 사이의 인용 관계 (adjacency matrix)
를 통해, 어떠한 논문인지 맞추기
- Import dataset
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='/tmp/Cora', name='Cora')
- Build GNN
import torch
import torch.nn.functional as F
# Graph Convolution Network
from torch_geometric.nn import GCNConv
모델링
- 1번째 layer : GCN layer
- shape : \((d_v, \text{middle dim})\)
- 2번째 layer : GCN layer
- shape : \((\text{middle dim}, \text{class 개수})\)
class GNN(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = GCNConv(dataset.num_node_features, 16)
self.conv2 = GCNConv(16, dataset.num_classes)
def forward(self, data):
# [x] Node Feature (N_v, d_v)
# [edge_index] Adjacency Info (2, N_e)
x, edge_index = data.x, data.edge_index
# (GCN-relu-dropout)
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, training=self.training)
# (GCN-softmax)
x = self.conv2(x, edge_index)
out = F.log_softmax(x, dim=1)
return out
- Train Model
# 1) check device (cpu/gpu)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 2) send MODEL & DATA to device
model = GNN().to(device)
data = dataset[0].to(device)
# 3) build Optimizer
optimizer = torch.optim.Adam(model.parameters(),
lr=0.01,
weight_decay=5e-4)
Binary classification task!
-
loss function : “negative log likelihood”
(
F.nll_loss(y_pred, y_true)
)
# 4) Train model
model.train()
for epoch in range(200):
optimizer.zero_grad()
y_pred = model(data)
loss = F.nll_loss(y_pred[data.train_mask],
data.y[data.train_mask])
loss.backward()
optimizer.step()
- Evaluation
model.eval()
_, y_pred_class = model(data).max(dim=1) # (1) max value & (2) argmax
correct = float (y_pred_class[data.test_mask].eq(data.y[data.test_mask]).sum().item())
acc = correct / data.test_mask.sum().item()
print('Accuracy: {:.4f}'.format(acc))
>>> Accuracy: 0.8150