그래프 신경망이란 무엇일까 (심화)

그래프 신경망이란 무엇일까? (심화)


✅ 그래프 신경망 복습

✅ 그래프 신경망에서의 어텐션

1️⃣ 기본 그래프 신경망의 한계

📌 기본 그래프 신경망 vs 그래프 합성곱 신경망

  • 기본 그래프 신경망에서는 이웃들의 정보를 동일한 가중치로 평균을 낸다.
  • 그래프 합성곱 신경망 역시 단순히 연결성을 고려한 가중치로 평균을 낸다.

image.png


2️⃣ 그래프 어텐션 신경망

📌 그래프 어텐션 신경망(Graph Attention Network, GAT)에서는 가중치 자체도 학습한다.

  • 실제 그래프에서는 이웃 별로 미치는 영향이 다를 수 있기 때문이다.
  • 가중치를 학습하기 위해서 셀프-어텐션(Self-Attention)이 사용된다.

image.png


📌각 층에서 정점 $i$로부터 이웃$j$로의 가중치 $\alpha_{ij}$는 3단계를 통해 계산된다.

  1. 해당 층의 정점 $i$의 임베딩 $h_{i}$에 신경망 $W$를 곱해서 새로운 임베딩을 얻는다. image.png
  1. 정점 $i$와 정점 $j$의 새로운 임베딩을 연결한 후, 어텐션 계수 a를 내적한다. 어텐션 계수 a는 모든 정점이 공유하는 학습 변수이다. image.png
  1. 2번의 결과에 소프트맥스(Softmax)를 적용한다. image.png


image.png

  • 여기서 학습 변수들은 $W$$a$벡터이다.


📌 여러 개의 어텐션을 동시에 학습한 뒤, 결과를 연결하여 사용할 수도 있다.

  • 멀티헤드 어텐션(Multi-head Attention)이라고 부른다.

image.png


📌 어텐션의 결과 정점 분류의 정확도(Accuracy)가 향상되는 것을 확인할 수 있다.

image.png



✅ 그래프 표현 학습과 그래프 풀링

1️⃣ 그래프 표현 학습

📌 그래프 표현 학습, 혹은 그래프 임베딩이란 그래프 전체를 벡터의 형태로 표현하는 것이다.

  • 개별 정점을 벡터의 형태로 표현하는 정점 표현 학습과 구분된다.
  • 그래프 임베딩은 벡터의 형태로 표현된 그래프 자체를 의미하기도 한다.
  • 그래프 임베딩은 그래프 분류 등에 활용
  • 그래프 형태로 표현된 화합물의 분자 구조로부터 특성을 예측하는 것이 한가지 예시이다.


2️⃣ 그래프 풀링

📌 그래프 풀링(Graph Pooling)이란 정점 임베딩들로부터 그래프 임베딩을 얻는 과정이다.

  • 평균 등 단순한 방법보다 그래프의 구조를 고려한 방법을 사용할 경우 그래프 분류 등의 후속 과제에서 더 높은 성능을 얻는 것으로 알려져 있다.
  • 아래 그림의 미분가능한 풀링(Differentiable Pooling, DiffPool)은 군집 구조를 활용 임베딩을 계층적으로 집계한 그림이다.

image.png



✅ 지나친 획일화 문제

1️⃣ 지나친 획일화 문제

📌 지나친 획일화(Over-smoothing) 문제그래프 신경망의 층의 수가 증가하면서 정점의 임베딩이 서로 유사해지는 현상을 의미한다.

  • 지나친 획일화 문제는 작은 세상 효과와 관련이 있다.
  • 적은 수의 층으로도 다수의 정점에 의해 영향을 받게 된다.

image.png

5-layer정도면 5의 거리에 있는 정점을 확인하게되는데 이는 수 많은 정점으로 부터 정보를 합산하기 때문에 마치 지역적인 정보만 보는 것이 아니라 그래프의 전반을 보게되는 효과가 되어 정점들이 비슷비슷한 임베딩을 얻게 되고 분류의 성능이 떨어진다.


📌지나친 획일화의 결과로 그래프 신경망의 층의 수를 늘렸을 때, 후속 과제에서의 정확도가 감소하는 현상이 발견되었다.

  • 아래 그림에서 보듯이 그래프 신경망의 층이 2개 혹은 3개 일 떄 정확도가 가장 높다.

image.png

잔차항(Residual)을 넣는 것을 생각할 수 있는데, 즉 이전 층의 임베딩을 한 번 더 더해주는 것만으로는 효과가 제한적이다.

image.png


2️⃣ 지나친 획일화 문제에 대한 대응

📌 JK 네트워크(Jumping Knowledge Network)는 마지막 층의 임베딩 뿐 아니라, 모든 층의 임베딩을 함께 사용한다.

image.png


📌 APPNP는 0번째 층을 제외하고는 신경망 없이 집계 함수를 단순화하였습니다

image.png

APPNP의 경우, 층의 수 증가에 따른 정확도 감소 효과가 없는 것을 확인

  • 후속 과제로는 정점 분류가 사용되었다.

image.png



✅ 그래프 데이터의 증강

1️⃣ 그래프 데이터 증강

📌 데이터 증강(Data Augmentation)은 다양한 기계학습 문제에서 효과적이다.

  • 그래프에도 누락되거나 부정확한 간선이 있을 수 있고, 데이터 증강을 통해 보완할 수 있다.
  • 임의 보행을 통해 정점간 유사도를 계산하고, 유사도가 높은 정점 간의 간선을 추가하는 방법이 제안되었다.

image.png


2️⃣ 그래프 데이터 증강에 따른 효과

📌 그래프 데이터 증강의 결과 정점 분류의 정확도가 개선되는 것을 확인할 수 있다.

  • 아래 그림의 HEAT과 PPR은 제안된 그래프 데이터 증강 기법을 의미한다.

image.png



✅ GraphSAGE의 집계 함수 구현

라이브러리 로드

In [1]:
import numpy as np                        
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
import dgl
from dgl.data import CoraGraphDataset
from sklearn.metrics import f1_score
import dgl.function as fn
Using backend: pytorch

Cora 인용 그래프 불러오기

In [2]:
'''
    Cora 데이터셋은 2708개의 논문(노드), 10556개의 인용관계(엣지)로 이루어졌습니다. 
    NumFeat은 각 노드를 나타내는 특성을 말합니다. 
    Cora 데이터셋은 각 노드가 1433개의 특성을 가지고, 개개의 특성은 '1'혹은 '0'으로 나타내어지며 특정 단어의 논문 등장 여부를 나타냅니다.
    즉, 2708개의 논문에서 특정 단어 1433개를 뽑아서, 1433개의 단어의 등장 여부를 통해 각 노드를 표현합니다.
    
    노드의 라벨은 총 7개가 존재하고, 각 라벨은 논문의 주제를 나타냅니다
    [Case_Based, Genetic_Algorithms, Neural_Networks, Probabilistic_Methods, Reinforcement_Learning, Rule_Learning, Theory]

    2708개의 노드 중, 학습에는 140개의 노드를 사용하고 모델을 테스트하는 데에는 1000개를 사용합니다.
    본 실습에서는 Validation을 진행하지않습니다.

    요약하자면, 앞서 학습시킬 모델은 Cora 데이터셋의 
    [논문 내 등장 단어들, 논문들 사이의 인용관계]를 활용하여 논문의 주제를 예측하는 모델입니다.
'''

# Cora Graph Dataset 불러오기
G = CoraGraphDataset()
numClasses = G.num_classes

G = G[0]
# 노드들의 feauture & feature의 차원
features = G.ndata['feat']
inputFeatureDim = features.shape[1]

# 각 노드들의 실제 라벨
labels = G.ndata['label']

# 학습/테스트에 사용할 노드들에 대한 표시
trainMask = G.ndata['train_mask']        
testMask = G.ndata['test_mask']
  NumNodes: 2708
  NumEdges: 10556
  NumFeats: 1433
  NumClasses: 7
  NumTrainingSamples: 140
  NumValidationSamples: 500
  NumTestSamples: 1000
Done loading data from cached files.

GraphSAGE 구현의 첫 단계로 GraphSAGE의 집계 함수를 직접 구현해본다.

  • AGG함수로는 평균을 사용한다.

image.png

In [3]:
class SAGEConv(nn.Module):
    """
    in_feats: 인풋 feature의 사이즈
    out_feats: 아웃풋 feature의 사이즈
    activation: None이 아니라면, 노드 피쳐의 업데이트를 위해서 해당 activation function을 적용한다.
    """
    '''
        https://arxiv.org/pdf/1706.02216.pdf 
        https://docs.dgl.ai/en/0.4.x/_modules/dgl/nn/pytorch/conv/sageconv.html
        위 두 페이지 참조
    '''
    
    def __init__(self, in_feats, out_feats, activation):
        super(SAGEConv, self).__init__()
        self._in_feats = in_feats
        self._out_feats = out_feats
        self.activation = activation
        
        # 신경망의 입력으로 input 임베딩 차원을 x2 해준다.
        # 위 수식을 보면 두개의 벡터를 concat하는 부분 때문이다.
        self.W = nn.Linear(in_feats+in_feats, out_feats, bias=True)


    def forward(self, graph, feature):
        graph.ndata['h'] = feature                                                      
        graph.update_all(fn.copy_src('h', 'm'), fn.sum('m', 'neigh'))                   

        # Aggregate & Noramlization
        degs = graph.in_degrees().to(feature)
        # 평균 계산
        hkNeigh = graph.ndata['neigh']/degs.unsqueeze(-1)
        # concat
        hk = self.W(torch.cat((graph.ndata['h'], hkNeigh), dim=-1))                   

        if self.activation != None:
            hk = self.activation(hk)

        return hk

위에서 구현한 집계 함수(SAGEConv)를 이용하여 각 층을 정의

In [4]:
class GraphSAGE(nn.Module):
    '''
        graph               : 학습할 그래프
        inFeatDim           : 데이터의 feature의 차원
        numHiddenDim        : 모델의 hidden 차원
        numClasses          : 예측할 라벨의 경우의 수
        numLayers           : 인풋, 아웃풋 레이어를 제외하고 중간 레이어의 갯수
        activationFunction  : 활성화 함수의 종류
    '''
    def __init__(self, graph, inFeatDim, numHiddenDim, numClasses, numLayers, activationFunction):
        super(GraphSAGE, self).__init__()
        self.layers = nn.ModuleList()
        self.graph = graph

        # 인풋 레이어
        self.layers.append(SAGEConv(inFeatDim, numHiddenDim, activationFunction))
       
        # 히든 레이어
        for i in range(numLayers):
            self.layers.append(SAGEConv(numHiddenDim, numHiddenDim, activationFunction))
        
        # 출력 레이어
        self.layers.append(SAGEConv(numHiddenDim, numClasses, activation=None))
    
    # 순전파
    def forward(self, features):
        x = features
        for layer in self.layers:
            x = layer(self.graph, x)
        return x

역전파를 통해 GraphSAGE를 학습

In [5]:
def train(model, lossFunction, features, labels, trainMask, optimizer, numEpochs):
    executionTime=[]
    flag=True
    
    for epoch in range(numEpochs):
        model.train()

        startTime = time.time()
            
        logits = model(features)                                    # 포워딩
        loss = lossFunction(logits[trainMask], labels[trainMask])   # 모델의 예측값과 실제 라벨을 비교하여 loss 값 계산

        optimizer.zero_grad()                                       
        loss.backward()
        optimizer.step()

        executionTime.append(time.time() - startTime)

        acc = evaluateTrain(model, features, labels, trainMask)
        if epoch < 5 or epoch > numEpochs -5:
            print("Epoch {:05d} | Time(s) {:.4f} | Loss {:.4f} | Accuracy {:.4f}".format(epoch, np.mean(executionTime), loss.item(), acc))
        elif flag:
            print('...')
            flag=False

정점 분류의 성능 평가 및 테스트

In [6]:
# 모델 학습 결과를 평가할 함수
# def accuracy(logits, labels):
#     _, indices = torch.max(logits, dim=1)
#     correct = torch.sum(indices == labels)
#     return correct.item() * 1.0 / len(labels)

def evaluateTrain(model, features, labels, mask):
    model.eval()
    with torch.no_grad():
        logits = model(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)

def evaluateTest(model, features, labels, mask):
    model.eval()
    with torch.no_grad():
        logits = model(features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = torch.max(logits, dim=1)
        macro_f1 = f1_score(labels, indices, average = 'macro')
        correct = torch.sum(indices == labels)
        return correct.item() * 1.0 / len(labels), macro_f1
    
def test(model, feautures, labels, testMask):
    acc, macro_f1 = evaluateTest(model, features, labels, testMask)
    print("Test Accuracy {:.4f}".format(acc))
    print("Test macro-f1 {:.4f}".format(macro_f1))

하이퍼파라미터 설정

In [7]:
# 하이퍼파라미터 정의
learningRate = 1e-2
numEpochs = 50
numHiddenDim = 128
numLayers = 2
weightDecay = 5e-4

전체 과정 진행

In [8]:
# 모델 생성
model = GraphSAGE(G, inputFeatureDim, numHiddenDim, numClasses, numLayers, F.relu)
print(model)

lossFunction = torch.nn.CrossEntropyLoss()
# 옵티마이저 초기화
optimizer = torch.optim.Adam(model.parameters(), lr=learningRate, weight_decay=weightDecay)
GraphSAGE(
  (layers): ModuleList(
    (0): SAGEConv(
      (W): Linear(in_features=2866, out_features=128, bias=True)
    )
    (1): SAGEConv(
      (W): Linear(in_features=256, out_features=128, bias=True)
    )
    (2): SAGEConv(
      (W): Linear(in_features=256, out_features=128, bias=True)
    )
    (3): SAGEConv(
      (W): Linear(in_features=256, out_features=7, bias=True)
    )
  )
)
In [9]:
train(model, lossFunction, features, labels, trainMask, optimizer, numEpochs)
Epoch 00000 | Time(s) 0.7420 | Loss 1.9470 | Accuracy 0.1429
Epoch 00001 | Time(s) 0.4640 | Loss 1.9473 | Accuracy 0.2857
Epoch 00002 | Time(s) 0.4177 | Loss 1.9430 | Accuracy 0.2929
Epoch 00003 | Time(s) 0.3575 | Loss 1.9350 | Accuracy 0.4071
Epoch 00004 | Time(s) 0.3208 | Loss 1.9063 | Accuracy 0.2857
...
Epoch 00046 | Time(s) 0.2024 | Loss 0.0122 | Accuracy 1.0000
Epoch 00047 | Time(s) 0.2017 | Loss 0.0079 | Accuracy 1.0000
Epoch 00048 | Time(s) 0.2013 | Loss 0.0065 | Accuracy 1.0000
Epoch 00049 | Time(s) 0.2010 | Loss 0.0046 | Accuracy 1.0000
In [10]:
test(model, features, labels, testMask)
Test Accuracy 0.6410
Test macro-f1 0.6338

+ Recent posts