이번엔, 논문의 내용을 기반으로 데이터를 이용해 코드 구현한 거에 대해 공부를 해보았다. 느낀 점은 데이터를 구축하는 과정이 많이 어려웠지만 모델을 생성하는 부분은 다른 딥러닝 모델을 형성하는 방식과 똑같았다. 다음은 GCN을 text classification에 적용한 코드를 공부해 정리해 볼 것이다. 앞으로 GCN에 대한 나의 최종 목표는 Spatio Temporal Graph Convolutional Networks에 대해 개념 및 코드를 완벽하게 이해해서 처음으로 맡게 된 프로젝트(행동분야 -> STGCN 적용 / 실제 데이터를 통해 내가 직접 코드를 구현하는 프로젝트)를 뿌듯하게 마무리 하는 것이다.
코드는 다음 자료를 참고하였다.
https://github.com/zhulf0804/GCN.PyTorch
3개의 Dataset : citeseer, cora, depbum
'Pubmed' dataset을 그림으로 표현하면, 다음과 같다.
1. Load Data
- dataset 종류에는 x, y, allx, ally, tx, ty, graph가 있었다. 이 dataset을 가지고 데이터를 구축해 나아갔다.
[dataset에 대한 설명]
- ind.dataset_str.x => the feature vectors of the training instances as scipy.sparse.csr.csr_matrix object
- ind.dataset_str.tx => the feature vectors of the test instances as scipy.sparse.csr.csr_matrix.object
- ind.dataset_str.allx => thes feature vectors of both labeled and unlabeled training instances
- (a superset of ind.dataset_str.x) as scipy.sparse.csr.csr_matrix object
- ind.dataset_str.y => the one-hot labels of the labeled training instances as numpy.ndarray object
- ind.dataset_str.ty => the one-hot labels of the test instances as numpy.ndarray object
- int.dataset_str.ally => the labels for instances in ind.dataset_str.allx as numpy.ndarray object
- ind.dataset_str.graph => a dict in the format {index: [index_of_neighbor_node]} as collections.defaultdict object
- ind.dataset_str.test.index => the indices of test instances in graph, for the inductive setting as list obje
- load_data 함수를 통해 최종적으로 data에서 adjacency matrix, features, y_train, y_val, y_test, train_mask, val_mask, test_mask를 구축하였다.
## datasets.py
def process_features(features):
row_sum_diag = np.sum(features, axis=1) #행 기준 합 # n*1
row_sum_diag_inv = np.power(row_sum_diag, -1) #제곱함수 #n*1
row_sum_diag_inv[np.isinf(row_sum_diag_inv)] = 0. #(만약, 값이 양의 무한대 혹은 음의 무한대로 간다면 0으로 처리)
row_sum_inv = np.diag(row_sum_diag_inv) # 대각행렬 생성 -> n*n matrix
return np.dot(row_sum_inv, features) # 행렬곱 -> n*n matrix
def sample_mask(idx, l):
mask = np.zeros(l)
mask[idx] = 1
return np.array(mask, dtype=np.bool)
def load_data(dataset): #dataset: citeseer, cora, pubmed
# get data
data_path = 'data' # 경로 설정
suffixs = ['x','y','allx','ally','tx','ty','graph'] # 데이터의 종류: x, y, allx, ally, tx, ty, graph
objects = []
for suffix in suffixs:
file = os.path.join(data_path, 'ind.%s.%s' % (dataset, suffix)) # 데이터 이름이 'ind.citeseer.allx', 'ind.citeseer.x' ...
objects.append(pickle.load(open(file,'rb'), encoding='latin1')) ##pickle은 binary 형태로 저장됨. 따라서, binary 형태로 read
x, y, allx, ally, tx, ty, graph = objects
x, allx, tx = x.toarray(), allx.toarray(), tx.toarray() # 배열 형태로 변환
# test indices
test_index_file = os.path.join(data_path, 'ind.%s.test.index'%dataset) # 데이터 이름이 'ind.cora.test.index'..
with open(test_index_file, 'r') as f:
lines = f.readlines()
indices = [int(line.strip()) for line in lines] # test index 추출
min_index, max_index = min(indices), max(indices)
# preprocess test indices and combine all data
tx_extend = np.zeros((max_index-min_index+1, tx.shape[1]))
features = np.vstack([allx, tx_extend]) # allx와 tx_extend 배열을 세로를 기준으로 결합할 때 사용.
features[indices] = tx
ty_extend = np.zeros((max_index-min_index+1, ty.shape[1]))
labels = np.vstack([ally, ty_extend])
labels[indices] = ty
# get adjacency matrix
# adjacency matrix는 networks library를 통해 형성.
adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph)).toarray() # returns a graph from a dictionary of lists
idx_train = range(len(y)) # train set 개수
idx_val = range(len(y), len(y)+500) # validation set 개수
idx_test = indices # test set 개수
train_mask = sample_mask(idx_train, labels.shape[0])
val_mask = sample_mask(idx_val, labels.shape[0])
test_mask = sample_mask(idx_test, labels.shape[0])
zeros = np.zeros(labels.shape)
y_train = zeros.copy()
y_val = zeros.copy()
y_test = zeros.copy()
y_train[train_mask, :] = labels[train_mask, :]
y_val[val_mask, :] = labels[val_mask, :]
y_test[test_mask, :] = labels[test_mask, :]
features = torch.from_numpy(process_features(features)) # feature 형성
# type을 torch로 변환
y_train, y_val, y_test, train_mask, val_mask, test_mask = \
torch.from_numpy(y_train), torch.from_numpy(y_val), torch.from_numpy(y_test), \
torch.from_numpy(train_mask), torch.from_numpy(val_mask), torch.from_numpy(test_mask)
return adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask
2. Dataset 형성
- 위에서 만든 함수인 process_features(features), load_data(dataset)를 이용해 3개의 dataset(citeseer, cora, depbum)을 구축하였다.
## data_stat.py
import numpy as np
import os
import sys
from datasets import load_data # 앞서 만들었던 load_data 가져오기
# load_data를 통해 각 dataset의 adjacency matrix, features, train set, validation set, test set 형성
def get_citeseer():
adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask = load_data('citeseer')
y_train, y_val, y_test = y_train.numpy(), y_val.numpy(), y_test.numpy()
train_num = np.sum(y_train) ## 1의 값만 존재 따라서 1의 합은 train set의 개수
val_num = np.sum(y_val)
test_num = np.sum(y_test)
print("="*20, "citeseer", "="*20)
print("train set num is %d, val set num is %d, test set num is %d"%(train_num, val_num, test_num))
classes = [[]for _ in range(3)] # 빈 리스트 3개 생성 # [[] [] []]
for i in range(6):
classes[0].append(np.sum(y_train[:, i]))
classes[1].append(np.sum(y_val[:, i]))
classes[2].append(np.sum(y_test[:, i]))
types = ['train set', 'val_set', 'test set']
classes = np.array(classes, dtype=int)
for i in range(3):
print("each class num in %s" % types[i], classes[i])
def get_cora():
adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask = load_data('cora')
y_train, y_val, y_test = y_train.numpy(), y_val.numpy(), y_test.numpy()
train_num = np.sum(y_train) ## 1의 값만 존재
val_num = np.sum(y_val)
test_num = np.sum(y_test)
print("="*20, "cora", "="*20)
print("train set num is %d, val set num is %d, test set num is %d"%(train_num, val_num, test_num))
classes = [[]for _ in range(3)] # 빈 리스트 3개 생성
for i in range(6):
classes[0].append(np.sum(y_train[:, i]))
classes[1].append(np.sum(y_val[:, i]))
classes[2].append(np.sum(y_test[:, i]))
types = ['train set', 'val_set', 'test set']
classes = np.array(classes, dtype=int)
for i in range(3):
print("each class num in %s" % types[i], classes[i])
def get_pubmed():
adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask = load_data("pubmed")
y_train, y_val, y_test = y_train.numpy(), y_val.numpy(), y_test.numpy()
train_num = np.sum(y_train)
val_num = np.sum(y_val)
test_num = np.sum(y_test)
print("="*20, "pubmed", "="*20)
print("train set num is %d, val set num is %d, test set num is %d" % (train_num, val_num, test_num))
classes = [[] for _ in range(3)]
for i in range(3):
classes[0].append(np.sum(y_train[:,i]))
classes[1].append(np.sum(y_val[:,i]))
classes[2].append(np.sum(y_test[:,i]))
types = ['train set', 'val set', 'test set']
classes = np.array(classes, dtype=int)
for i in range(3):
print("each class num in %s" % types[i], classes[i])
father_path = os.path.dirname(sys.path[0])
os.chdir(father_path)
get_pubmed()
get_cora()
get_citeseer()
결과값을 확인해보면, dataset이 잘 형성된 것을 알 수 있다. 맨 처음에 제시한 dataset 자료와 비교해 보면 알 수 있다.
3. Model 생성
논문에서 제시한 모델 구조는 다음과 같다. (input layer -> hidden layer -> output layer)
- hidden layer를 여러 개 쌓으면 쌓을수록 연결된 node간의 더 많은 정보(풍부한 정보)를 담을 수 있다.
## gcn.py
# 모델 형성
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as Func
## Adjacency Matrix 형성
## node간의 연결여부 행렬
## 자기 특성도 반영해 주기 위해 대각성분이 모두 1인 대각행렬 추가 --> 즉, self-loop 추가
## return 값: normalzied symmetric adjacency matrix
def preprocess_adj(A):
I = np.eye(A.shape[0]) ## n*n 행렬 (n=node의 수)
A_hat = A + I ## adjacency matrix에 self-loop 추가 ## n*n 행렬
D_hat_diag = np.sum(A_hat, axis=1) ## D=degree matrix -> node에 연결된 node의 갯수(혹은 edge의 갯수) ## n*1 행렬
D_hat_diag_inv_sqrt = np.power(D_hat_diag, -0.5) ## Degree Matrix의 역행렬을 통해 normalization 효과 ## n*1 행렬
D_hat_diag_inv_sqrt[np.isinf(D_hat_diag_inv_sqrt)] = 0. # 값이 양의 무한대 혹은 음의 무한대로 갈 경우, 0으로 변환
D_hat_inv_sqrt = np.diag(D_hat_diag_inv_sqrt) ## 대각행렬 값만 추출
return np.dot(np.dot(D_hat_inv_sqrt, A_hat), D_hat_inv_sqrt) # D^-0.5 * (A+I) * D^-0.5 : normalized symmetric adjacency matrix
class GCNLayer(nn.Module):
def __init__(self, in_dim, out_dim, acti=True):
super(GCNLayer, self).__init__()
self.linear = nn.Linear(in_dim, out_dim)
if acti:
self.acti = nn.ReLU(inplace=True)
else:
self.acti = None
def forward(self, F):
output = self.linear(F)
if not self.acti:
return output
return self.acti(output)
## 논문에서는 input layer -> hidden layer -> output layer 생성
## 식을 확인해 보면, W(0), W(1) 두 개 존재
class GCN(nn.Module):
def __init__(self, input_dim, hidden_dim, num_classes, p):
super(GCN, self).__init__()
self.gcn_layer1 = GCNLayer(input_dim, hidden_dim)
self.gcn_layer2 = GCNLayer(hidden_dim, num_classes, acti=False) ## output layer 전에는 activation function 사용 안 함.
self.dropout = nn.Dropout(p) ## overfitting 방지
## softmax(A*relu(A*X*W(0)*W(1)
def forward(self, A, X):
A = torch.from_numpy(preprocess_adj(A)).float() # normalized symmetric adjacency matrix 형성
X = self.dropout(X.float()) # overffing을 방지하기 위해 dropout
F = torch.mm(A,X) ## matrix multiplication -> matrix A ** matrix X # (A*X*W(0)) # input data
F = self.gcn_layer1(F)
F = self.dropout(F)
F = torch.mm(A,F) # A*relu(A*X*W(0))*W(1)
output = self.gcn_layer2(F)
return output
4. Loss Function & Opitmizer
classification data로 loss function은 CrossEntropyLoss Function 사용 그리고 Adam Optimizer를 사용하였다.
## utils.py
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
class Loss(nn.Module):
def __init__(self):
super(Loss, self).__init__()
self.loss = nn.CrossEntropyLoss(reduction='None')
def forward(self, output, labels, mask):
labels = torch.argmax(labels, dim=1)
loss = self.loss(output, labels)
mask = mask.float()
mask /= torch.mean(mask)
loss *= mask
return torch.mean(loss)
def build_optimizer(model, lr, weight_decay):
t = model.state_dict()
gcn1 = (t['gcn_layer1.linear.weight']).numpy()
gcn1 = torch.Tensor(np.append(gcn1, t['gcn_layer1.linear.bias'].numpy()))
gcn2 = (t['gcn_layer2.linear.weight']).numpy()
gcn2 = torch.Tensor(np.append(gcn2, t['gcn_layer2.linear.bias'].numpy()))
opt = torch.optim.Adam([{'params': gcn1, 'weight_decay': weight_decay},
{'params': gcn2}
], lr=lr)
return opt
def get_lr():
pass
def get_loss(output, labels, mask):
loss = Loss()
return loss(output, labels, mask)
def get_accuracy(outputs, labels, mask):
outputs = torch.argmax(outputs, dim=1)
labels = torch.argmax(labels, dim=1)
outputs = outputs.numpy()
labels = labels.numpy()
correct = outputs==labels
mask = mask.float().numpy()
tp = np.sum(correct*mask)
tp = np.sum(correct*mask)
return tp/np.sum(mask)
5. Train
import argparse
import os
os.chdir('/content/drive/MyDrive/Colab Notebooks/Deep_Learning/Graph Convolutional Network/Semi Supervised Graph Convolutional Network')
import torch
from datasets import load_data
from gcn import GCN
from utils import build_optimizer, get_loss, get_accuracy
from tensorboardX import SummaryWriter
parser = argparse.ArgumentParser()
parser.add_argument('--dataset', type=str, default='citeseer', help='Dataset to train')
parser.add_argument('--init_lr', type=float, default=0.01, help='Initial learing rate')
parser.add_argument('--epoches', type=int, default=200, help='Number of traing epoches')
parser.add_argument('--hidden_dim', type=list, default=16, help='Dimensions of hidden layers')
parser.add_argument('--dropout', type=float, default=0.5, help='Dropout rate (1 - keep probability)')
parser.add_argument('--weight_decay', type=float, default=5e-4, help='Weight for l2 loss on embedding matrix')
parser.add_argument('--log_interval', type=int, default=10, help='Print iterval')
parser.add_argument('--log_dir', type=str, default='experiments', help='Train/val loss and accuracy logs')
parser.add_argument('--checkpoint_interval', type=int, default=20, help='Checkpoint saved interval')
parser.add_argument('--checkpoint_dir', type=str, default='checkpoints', help='Directory to save checkpoints')
args = parser.parse_args()
colab이나 jupyter로 code를 돌리는 경우, 다음과 같이 사용해주면 된다.
import argparse
import os
os.chdir('/content/drive/MyDrive/Colab Notebooks/Deep_Learning/Graph Convolutional Network/Semi Supervised Graph Convolutional Network')
import torch
from datasets import load_data
from gcn import GCN
from utils import build_optimizer, get_loss, get_accuracy
from tensorboardX import SummaryWriter
import numpy as np
import easydict
args = easydict.EasyDict({
"dataset" : 'citeseer',
"init_lr" : 0.01,
"epoches" : 200,
"hidden_dim" : 16,
"dropout" : 0.5,
"weight_decay" : 5e-4,
"log_interval" : 10,
"log_dir" : 'experiments',
"checkpoint_interval" : 20,
"checkpoint_dir" : 'checkpoints'
})
adj, features, y_train, y_val, y_test, train_mask, val_mask, test_mask = load_data(args.dataset)
model = GCN(features.shape[1], args.hidden_dim, y_train.shape[1], args.dropout) ## input_dim, hidden_dim, num_classes, p
optimizer = build_optimizer(model, args.init_lr, args.weight_decay)
def train():
log_dir = os.path.join(args.log_dir, args.dataset)
if not os.path.exists(log_dir):
os.makedirs(log_dir)
writer = SummaryWriter(log_dir)
saved_checkpoint_dir = os.path.join(args.checkpoint_dir, args.dataset)
if not os.path.exists(saved_checkpoint_dir):
os.makedirs(saved_checkpoint_dir)
for epoch in range(args.epoches + 1):
outputs = model(adj, features)
loss = get_loss(outputs, y_train, train_mask)
val_loss = get_loss(outputs, y_val, val_mask).detach().numpy()
model.eval()
outputs = model(adj, features)
train_accuracy = get_accuracy(outputs, y_train, train_mask)
val_accuracy = get_accuracy(outputs, y_val, val_mask)
model.train()
writer.add_scalars('loss', {'train_loss': loss.detach().numpy(), 'val_loss': val_loss}, epoch)
writer.add_scalars('accuracy', {'train_ac': train_accuracy, 'val_ac': val_accuracy}, epoch)
if epoch % args.log_interval == 0:
print("Epoch: %d, train loss: %f, val loss: %f, train ac: %f, val ac: %f"
%(epoch, loss.detach().numpy(), val_loss, train_accuracy, val_accuracy))
if epoch % args.checkpoint_interval == 0:
torch.save(model.state_dict(), os.path.join(saved_checkpoint_dir, "gcn_%d.pth"%epoch))
optimizer.zero_grad() # Important
loss.backward()
optimizer.step()
writer.close()
if __name__ == '__main__':
train()
6. Accuracy & Loss
Train/Validation Accuracy & Loss
- 총 200번의 epoch동안 accuracy의 경우, 한 시점의 epoch 이후로는 거의 상승하지 않지만, loss 값은 계속해서 하락하는 것을 볼 수 있다.
- 또한, accuracy와 loss 값을 통해 학습이 잘 된 것을 알 수 있다.
Citeseer - 6개의 Class
Cora - 7개의 Class
Pubmed - 3개의 Class
'Graph(Graph Neural Network)' 카테고리의 다른 글
GCN - Image Classification (0) | 2022.11.03 |
---|---|
Bayesian graph convolutional neural networks for semi-supervised classification (0) | 2022.10.09 |
Graph Convolutional Networks for Text Classification (0) | 2022.09.18 |
Semi-Supervised Classification With Graph Convolutional Neworks (0) | 2022.09.18 |
Spatio-Temporal Graph Convolutional Networks: A Deep Learning Framework for Traffic Forecasting (0) | 2022.09.09 |