Processing math: 100%
그래프 신경망이란 무엇일까 (심화)

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


✅ 그래프 신경망 복습

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

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

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

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

image.png


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

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

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

image.png


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

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


image.png

  • 여기서 학습 변수들은 Wa벡터이다.


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

  • 멀티헤드 어텐션(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