그래프 신경망이란 무엇일까 (기본)

그래프 신경망이란 무엇일까? (기본)


📌 출력으로 임베딩 자체를 얻는 변환식 임베딩 방법은 여러 한계를 갖는다.

  1. 학습이 진행된 이후에 추가된 정점에 대해서는 임베딩을 얻을 수 없다.
  2. 모든 정점에 대한 임베딩을 미리 계산하여 저장해두어야 한다.
  3. 정점이 속성(Attribute) 정보를 가진 경우에 이를 활용할 수 없다.

📌 출력으로 인코더를 얻는 귀납식 임베딩 방법은 여러 장점을 갖는다.

  1. 학습이 진행된 이후에 추가된 정점에 대해서도 임베딩을 얻을 수 있다.
  2. 모든 정점에 대한 임베딩을 미리 계산하여 저장해둘 필요가 없다.
  3. 정점이 속성(Attribute) 정보를 가진 경우에 이를 활용할 수 있다.

image.png


✅ 그래프 신경망 기본

1️⃣ 그래프 신경망의 구조

📌 그래프 신경망은 그래프정점의 속성 정보를 입력으로 받는다.

  • 그래프의 인접 행렬을 A라고 하자.
  • 인접 행렬 A는 $|V|$X$|V|$의 이진 행렬이다.
  • 각 정점 $u$의 속성(Attrivute) 벡터를 $X_{u}$라고 하자.
  • 정점 속성 벡터 $X_{u}$는 m차원 벡터이고, m은 속성의 수를 의미한다.

정점의 속성의 예시는 다음과 같다.

  • 온라인 소셜 네트워크에서 사용자의 지역, 성별, 연령, 프로필 사진 등
  • 논문 인용 그래프에서 논문에 사용된 키워드에 대한 원-핫 벡터
  • PageRank 등의 정점 중심성, 군집 계수(Clustering Coefficient) 등


📌 그래프 신경망은 이웃 정점들의 정보를 집계하는 과정을 반복하여 임베딩을 얻는다.

  • 아래 예시를 보면 대상 정점의 임베딩을 얻기 위해 이웃들 그리고 이웃의 이웃들의 정보를 집계한다.

image.png


📌 각 집계 단계를 층(Layer)이라고 부르고, 각 층마다 임베딩을 얻습니다

  • 각 층에서는 이웃들의 이전 층 임베딩을 집계하여 새로운 임베딩을 얻는다.
  • 0번 층, 즉 입력 층의 임베딩으로는 정점의 속성 벡터를 사용한다.

image.png


📌 대상 정점 마다 집계되는 정보가 상이하다.

  • 대상 정점 별 집계되는 구조를 계산 그래프(Computation Graph)라고 부른다.

image.png



서로 다른 대상 정점간에도 층 별 집계 함수는 공유한다.

image.png


📌 서로 다른 구조의 계산 그래프를 처리하기 위해서는 어떤 형태의 집계 함수가 필요할까?

image.png


집계 함수는 (1) 이웃들 정보의 평균을 계산하고 (2) 신경망에 적용하는 단계를 거친다.

image.png

image.png


마지막 층에서의 정점 별 임베딩이 해당 정점의 출력 임베딩이다.

image.png



2️⃣ 그래프 신경망의 학습

📌 그래프 신경망의 학습 변수(Trainable Parameter)층 별 신경망의 가중치이다.

image.png


📌 먼저 손실함수를 결정한다. 정점간 거리를 "보존"하는 것을 목표로 할 수 있다.

  • 변환식 정점 임베딩에서처럼 그래프에서의 정점간 거리를 "보존"하는 것을 목표로 할 수 있다.
  • 만약, 인접성을 기반으로 유사도를 정의한다면, 손실 함수는 다음과 같다.

image.png


📌후속 과제(Downstream Task)의 손실함수를 이용한 종단종(End-to-End) 학습도 가능하다.

🔎 정점 분류가 최종 목표인 경우를 생각해 보자.

예를 들어,

  1. 그래프 신경망을 이용하여 정점의 임베딩을 얻고
  2. 이를 분류기(Classifier)의 입력으로 사용하여
  3. 각 정점의 유형을 분류하려고 한다.

이 경우 분류기의 손실함수, 예를 들어 교차 엔트로피(Cross Entropy)를 전체 프로세스의 손실함수로 사용하여 종단종(End-to-End) 학습을 할 수 있다.

image.png

image.png


📌 그래프 신경망과 변환적 정점 임베딩을 이용한 정점 분류 비교

  • 그래프 신경망의 종단종(End-to-End) 학습을 통한 분류변환적 정점 임베딩 이후에 별도의 분류기를 학습하는 것보다 정확도가 대체로 높다.

아래 표는 다양한 데이터에서의 정점 분류의 정확도(Accuracy)를 보여준다.

image.png


📌학습에 사용할 대상 정점을 결정하여 학습 데이터를 구성

  • 우리는 모든 정점을 넣고 계산할 필요는 없다.
  • 선택한 대상 정점들만을 가지고 계산 그래프를 구성 가능하다.

image.png


📌 마지막으로 오차역전파(Backpropagation)을 통해 손실 함수를 최소화 한다.

구체적으로, 오차역전파를 통해 신경망의 학습 변수들을 학습한다.

image.png



3️⃣ 그래프 신경망의 활용

  • 또한 일부 정점들을 선택하여 계산 그래프를 계산하였고 학습된 신경망을 적용하여, 학습에 사용되지 않은 정점의 임베딩을 얻을 수 있다.

image.png


📌 마찬가지로, 학습 이후에 추가된 정점의 임베딩도 얻을 수 있다.

  • 온라인 소셜네트워크 등 많은 실제 그래프들은 시간에 따라서 변화한다.

image.png


📌 학습된 그래프 신경망을, 새로운 그래프에 적용할 수도 있다.

  • 예를 들어, A종의 단백질 상호 작용 그래프에서 학습한 그래프 신경망을 B종의 단백질 상호작용 그래프에 적용할 수 있다.

image.png



✅ 그래프 신경망 변형

1️⃣ 그래프 합성곱 신경망

📌 그래프 합성곱 신경망(Graph Convolutional Network, GCN)집계 함수이다.

image.png


차이를 보기 위해 기존의 집계 함수와 비교해 보자. 작은 차이지만 큰 성능의 향상으로 이어지기도 한다.

image.png


2️⃣ GraphSAGE

📌 GraphSAGE집계 함수이다.

  • 이웃들의 임베딩을 AGG함수를 이용해 합친 후, 자신의 임베딩과 연결(Concatenation)하는 점이 독특하다.

image.png


📌 AGG 함수로는 평균, 풀링, LSTM 등이 사용될 수 있다.

image.png

  • 여기서 $\pi$는 이웃들의 임베딩을 가지고 와서 순서를 섞은 다음에 LSTM에 넣는다는 의미로 이해하면 된다.



✅ 합성곱 신견망(CNN)과의 비교

1️⃣ 합성곱 신경망과 그래프 신경망의 유사성

📌 합성곱 신경망그래프 신경망은 모두 이웃의 정보를 집계하는 과정을 반복한다.

  • 구체적으로, 합성곱 신경망은 이웃 픽셀의 정보를 집계하는 과정을 반복한다.

image.png


2️⃣ 합성곱 신경망과 그래프 신경망의 차이

❗❗ 합성곱 신경망에서는 이웃의 수가 균일하지만, 그래프 신경망에서는 아니다.

  • 그래프 신경망에서는 정점 별로 집계하는 이웃의 수가 다르다.

image.png


그래프의 인접 행렬합성곱 신경망을 적용하면 효과적일까요?

그래프에는 합성곱 신경망이 아닌 그래프 신경망을 적용해야 한다 ❗❗

  • 합성곱 신경망이 주로 쓰이는 이미지에서는 인접 픽셀이 유용한 정보를 담고 있을 가능성이 높다.
  • 하지만, 그래프의 인접 행렬에서의 인접 원소는 제한된 정보를 가진다.
  • 특히나, 인접 행렬의 행과 열의 순서는 임의로 결정되는 경우가 많다.



✅ DGL라이브러리와 GraphSAGE를 이용한 정점 분류

라이브러리 로드

In [1]:
import numpy as np
import time
import torch
import torch.nn as nn
import torch.nn.functional as F
# Deep Graph Library
import dgl
from dgl.data import CoraGraphDataset
from sklearn.metrics import f1_score
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 정의의 첫 단계로 각 층을 정의

In [3]:
# 기존에 구현되어 있는 SAGEConv 모듈을 불러와서 GraphSAGE 모델을 구축한다.
from dgl.nn.pytorch.conv import SAGEConv
class GraphSAGE(nn.Module):
    '''
        graph               : 학습할 그래프
        inFeatDim           : 데이터의 feature의 차원
        numHiddenDim        : 모델의 hidden 차원
        numClasses          : 예측할 라벨의 경우의 수
        numLayers           : 인풋, 아웃풋 레이어를 제외하고 중간 레이어의 갯수
        activationFunction  : 활성화 함수의 종류
        dropoutProb         : 드롭아웃 할 확률
        aggregatorType      : [mean, gcn, pool (for max), lstm]
    '''
    def __init__(self,graph, inFeatDim, numHiddenDim, numClasses, numLayers, activationFunction, dropoutProb, aggregatorType):
        super(GraphSAGE, self).__init__()
        self.layers = nn.ModuleList()
        self.graph = graph

        # 인풋 레이어
        self.layers.append(SAGEConv(inFeatDim, numHiddenDim, aggregatorType, dropoutProb, activationFunction))
       
        # 히든 레이어
        for i in range(numLayers):
            self.layers.append(SAGEConv(numHiddenDim, numHiddenDim, aggregatorType, dropoutProb, activationFunction))
        
        # 출력 레이어 (마지막 층에는 activation함수가 없다.)
        # Output 차원은 class의 개수로 맞춰준다.
        self.layers.append(SAGEConv(numHiddenDim, numClasses, aggregatorType, dropoutProb, activation=None))

    def forward(self, features):
        x = features
        for layer in self.layers:
            x = layer(self.graph, x)
        return x

역전파을 통해 GraphSAGE를 학습한다.

In [4]:
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 [5]:
# 모델 학습 결과를 평가할 함수
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 [6]:
# 하이퍼파라미터 초기화
dropoutProb = 0.5
learningRate = 1e-2
numEpochs = 50
numHiddenDim = 128
numLayers = 2
weightDecay = 5e-4
aggregatorType = "gcn"

이제 전체 과정 진행

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

# 손실함수
lossFunction = torch.nn.CrossEntropyLoss()
# 옵티마이저 초기화
optimizer = torch.optim.Adam(model.parameters(), lr=learningRate, weight_decay=weightDecay)
GraphSAGE(
  (layers): ModuleList(
    (0): SAGEConv(
      (feat_drop): Dropout(p=0.5, inplace=False)
      (fc_neigh): Linear(in_features=1433, out_features=128, bias=True)
    )
    (1): SAGEConv(
      (feat_drop): Dropout(p=0.5, inplace=False)
      (fc_neigh): Linear(in_features=128, out_features=128, bias=True)
    )
    (2): SAGEConv(
      (feat_drop): Dropout(p=0.5, inplace=False)
      (fc_neigh): Linear(in_features=128, out_features=128, bias=True)
    )
    (3): SAGEConv(
      (feat_drop): Dropout(p=0.5, inplace=False)
      (fc_neigh): Linear(in_features=128, out_features=7, bias=True)
    )
  )
)
In [8]:
train(model, lossFunction, features, labels, trainMask, optimizer, numEpochs)
Epoch 00000 | Time(s) 0.6860 | Loss 1.9373 | Accuracy 0.1786
Epoch 00001 | Time(s) 0.4610 | Loss 1.9464 | Accuracy 0.4786
Epoch 00002 | Time(s) 0.3923 | Loss 1.7560 | Accuracy 0.4071
Epoch 00003 | Time(s) 0.3388 | Loss 1.6410 | Accuracy 0.7857
Epoch 00004 | Time(s) 0.3158 | Loss 1.3236 | Accuracy 0.9000
...
Epoch 00046 | Time(s) 0.2115 | Loss 0.0480 | Accuracy 0.9929
Epoch 00047 | Time(s) 0.2111 | Loss 0.0444 | Accuracy 1.0000
Epoch 00048 | Time(s) 0.2101 | Loss 0.0388 | Accuracy 1.0000
Epoch 00049 | Time(s) 0.2096 | Loss 0.0468 | Accuracy 1.0000
In [9]:
test(model, features, labels, testMask)
Test Accuracy 0.7830
Test macro-f1 0.7825

+ Recent posts