지금은마라톤중

[GNN] 노드분류와 링크예측 실습(feat.DGL) 본문

STUDY/GNN

[GNN] 노드분류와 링크예측 실습(feat.DGL)

Ojungii 2024. 11. 29. 17:59

 

그래프 기계학습의 대표적인 예

- 노드 분류

- 링크 예측

 

노드 분류 (Node Classification)

  • 정의:그래프의 각 노드에 특정 레이블(클래스)을 예측하는 작업.
    • 예: 소셜 네트워크에서 사용자의 관심사 분류.
  • 입력: 노드 피처(특성)와 그래프 구조.
  • 출력: 각 노드의 클래스(레이블).
  • 활용 사례:
    • 연구 논문 데이터에서 논문의 주제 분류.
    • 소셜 네트워크에서 사용자의 성향 분류.

 

링크 예측 (Link Prediction)

  • 정의:그래프에서 두 노드 간의 연결 여부를 예측하는 작업.
    • 예: 추천 시스템에서 사용자와 상품 간 연결(구매 가능성) 예측.
  • 입력: 두 노드 쌍과 그래프 구조.
  • 출력: 두 노드 간 연결 확률(0~1).
  • 활용 사례:
    • 추천 시스템에서 친구 추천 또는 제품 추천.
    • 네트워크 복구를 위한 연결 가능성 분석.

 

 

GNN 모델 종류

- GCN, GAT, GraphSAGE 등등

GNN 단순 합 또는 평균 가중치 없음 기본적이고 직관적 모든 이웃을 동일하게 취급
GCN 모든 이웃의 평균/합산 정규화된 단일 가중치 적용 단순하면서 계산 효율적 모든 이웃을 동일하게 취급
GAT 모든 이웃 Attention 가중치 적용 이웃 간 중요도를 반영 가능 계산량 증가
GraphSAGE 일부 이웃 샘플링 샘플링된 이웃의 집계 방식 사용 대규모 그래프에 효율적 샘플링으로 정보 손실 가능

 

GraphSAGE

 

 


 

DGL을 활용한 실습

GPU 환경에서 하고 싶었지만, 코랩환경과 DGL 설치 간의 환경문제를 해결 못해서 결국 CPU에서 진행했습니다...

구글링으로 에러를 찾아봤는데 딱히 해결한 사람은 보이지 않고 DGL 개발자의 이 에러를 인지하고 있고 다음 버전 업데이트를 기다려달라는 코멘트를 보고 CPU로 진행하기로 했습니다.

 

 

환경 : 코랩(CPU) 

데이터셋 : Zachary의 카라테 클럽

 

 

노드 분류

이 클럽은 2개의 커뮤니티로 나뉘어, 지도자 (노드 0번)와 클럽 회장(노드 33번)가 각각 커뮤니티를 이끌게 됩니다.

 

 

 

 

데이터 로드

import dgl
import pandas as pd
import torch
import torch.nn.functional as F

def load_zachery():
    nodes_data = pd.read_csv('https://github.com/myeonghak/DGL-tutorial/raw/master/data/nodes.csv')
    edges_data = pd.read_csv('https://github.com/myeonghak/DGL-tutorial/raw/master/data/edges.csv')
    src = edges_data['Src'].to_numpy()
    dst = edges_data['Dst'].to_numpy()
    g = dgl.graph((src, dst))
    club = nodes_data['Club'].to_list()
    # Convert to categorical integer values with 0 for 'Mr. Hi', 1 for 'Officer'.
    club = torch.tensor([c == 'Officer' for c in club]).long()
    # We can also convert it to one-hot encoding.
    club_onehot = F.one_hot(club)
    g.ndata.update({'club' : club, 'club_onehot' : club_onehot})
    return g

 

그래프 임베딩 및 가중치 초기화

    
# ----------- 0. load graph -------------- #
g = load_zachery()
print(g)

# ----------- 1. node features -------------- #
node_embed = nn.Embedding(g.number_of_nodes(), 5)  # 각 노드는 5차원의 임베딩을 가지고 있습니다.
inputs = node_embed.weight                         # 노드 피처로써 이 임베딩 가중치를 사용합니다.


nn.init.xavier_uniform_(inputs) # 가중치 초기화
print(inputs)


labels = g.ndata['club']
labeled_nodes = [0, 33]
print('Labels', labels[labeled_nodes]) 	# Labels tensor([0, 1])

 

 

GraphSAGE 모델 구축

from dgl.nn import SAGEConv

# ----------- 2. create model -------------- #
# 2개의 레이어를 가진 GraphSAGE 모델 구축

class GraphSAGE(nn.Module):
    def __init__(self, in_feats, h_feats, num_classes):
        super(GraphSAGE, self).__init__()
        self.conv1 = SAGEConv(in_feats, h_feats, 'mean')
        self.conv2 = SAGEConv(h_feats, num_classes, 'mean')

    def forward(self, g, in_feat):
        h = self.conv1(g, in_feat)
        h = F.relu(h)
        h = self.conv2(g, h)
        return h


# 주어진 차원의 모델 생성
# 인풋 레이어 차원: 5, 노드 임베딩
# 히든 레이어 차원: 16
# 아웃풋 레이어 차원: 2, 클래스가 2개 있기 때문, 0과 1

net = GraphSAGE(5, 16, 2)

 

 

GraphSAGE 모델 학습 및 평가

# ----------- 3. set up loss and optimizer -------------- #
# 이 경우, 학습 루프의 손실
optimizer = torch.optim.Adam(itertools.chain(net.parameters(), node_embed.parameters()), lr=0.01)

# ----------- 4. training -------------------------------- #
all_logits = []
for e in range(100):
    # forward
    logits = net(g, inputs)

    # 손실 계산
    logp = F.log_softmax(logits, 1)
    loss = F.nll_loss(logp[labeled_nodes], labels[labeled_nodes])

    # backward
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    all_logits.append(logits.detach())

    if e % 5 == 0:
        print('In epoch {}, loss: {}'.format(e, loss))
        
        
        

# In epoch 0, loss: 0.7915397882461548
# In epoch 5, loss: 0.3906407654285431
# In epoch 10, loss: 0.16861018538475037
# In epoch 15, loss: 0.05778202414512634
# In epoch 20, loss: 0.018602412194013596
# In epoch 25, loss: 0.006654476746916771
# In epoch 30, loss: 0.002896434161812067
# In epoch 35, loss: 0.0015504546463489532
# In epoch 40, loss: 0.0009892910020425916
# In epoch 45, loss: 0.0007208884926512837
# In epoch 50, loss: 0.0005760863423347473
# In epoch 55, loss: 0.0004905411042273045
# In epoch 60, loss: 0.0004358502337709069
# In epoch 65, loss: 0.00039837509393692017
# In epoch 70, loss: 0.00037084874929860234
# In epoch 75, loss: 0.0003493395051918924
# In epoch 80, loss: 0.0003315835783723742
# In epoch 85, loss: 0.00031627033604308963
# In epoch 90, loss: 0.0003026252379640937
# In epoch 95, loss: 0.00029029088909737766

# ----------- 5. check results ------------------------ #
pred = torch.argmax(logits, axis=1)
print('Accuracy', (pred == labels).sum().item() / len(pred))
# Accuracy 0.8235294117647058

 

 

결과 시각화

 

 


노드분류

가중치 초기화까지는 노드분류와 같습니다.

 

학습/테스트 셋 준비

일반적으로 링크 예측은 positive and negative 엣지라는 2가지 타입의 엣지를 만들어서 학습합니다.

positive 엣지는 보통 그래프 내에 이미 존재하는 엣지로부터 가져옵니다.

negative 엣지는 임의 생성합니다.

 

# 학습과 테스트를 위해 엣지 셋을 분할합니다.
# positive
u, v = g.edges()
eids = np.arange(g.number_of_edges())
eids = np.random.permutation(eids)
test_pos_u, test_pos_v = u[eids[:50]], v[eids[:50]]
train_pos_u, train_pos_v = u[eids[50:]], v[eids[50:]]

# negative
adj = sp.coo_matrix((np.ones(len(u)), (u.numpy(), v.numpy())))
adj_neg = 1 - adj.todense() - np.eye(34)  # 그래프에 없는 엣지를 정의
neg_u, neg_v = np.where(adj_neg != 0)
neg_eids = np.random.choice(len(neg_u), 200) # 너무 많기 때문에 200개 샘플링
test_neg_u, test_neg_v = neg_u[neg_eids[:50]], neg_v[neg_eids[:50]]
train_neg_u, train_neg_v = neg_u[neg_eids[50:]], neg_v[neg_eids[50:]]


# concat = pos + neg
# 학습 데이터셋
train_u = torch.cat([torch.as_tensor(train_pos_u), torch.as_tensor(train_neg_u)])
train_v = torch.cat([torch.as_tensor(train_pos_v), torch.as_tensor(train_neg_v)])
train_label = torch.cat([torch.zeros(len(train_pos_u)), torch.ones(len(train_neg_u))])

# 테스트 데이터셋
test_u = torch.cat([torch.as_tensor(test_pos_u), torch.as_tensor(test_neg_u)])
test_v = torch.cat([torch.as_tensor(test_pos_v), torch.as_tensor(test_neg_v)])
test_label = torch.cat([torch.zeros(len(test_pos_u)), torch.ones(len(test_neg_u))])

 

GraphSAGE 모델 구축

마지막 차원이 2로 나오는 노드분류와 달리 16으로 끝납니다.

from dgl.nn import SAGEConv

# ----------- 2. create model -------------- #
# 2개의 레이어를 가진 GraphSAGE 모델 구축
class GraphSAGE(nn.Module):
    def __init__(self, in_feats, h_feats):
        super(GraphSAGE, self).__init__()
        self.conv1 = SAGEConv(in_feats, h_feats, 'mean')
        self.conv2 = SAGEConv(h_feats, h_feats, 'mean')

    def forward(self, g, in_feat):
        h = self.conv1(g, in_feat)
        h = F.relu(h)
        h = self.conv2(g, h)
        return h

# 주어진 차원의 모델 생성
# 인풋 레이어 차원: 5, 노드 임베딩
# 히든 레이어 차원: 16
net = GraphSAGE(5, 16)

 

GraphSAGE 모델 학습 및 평가

# ----------- 3. set up loss and optimizer -------------- #
# 이 경우, 학습 루프의 손실
optimizer = torch.optim.Adam(itertools.chain(net.parameters(), node_embed.parameters()), lr=0.01)

# ----------- 4. training -------------------------------- #
all_logits = []
for e in range(100):
    # forward
    logits = net(g, inputs)
    pred = torch.sigmoid((logits[train_u] * logits[train_v]).sum(dim=1))

    # 손실 계산
    loss = F.binary_cross_entropy(pred, train_label)

    # backward
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    all_logits.append(logits.detach())

    if e % 5 == 0:
        print('In epoch {}, loss: {}'.format(e, loss))


# In epoch 0, loss: 1.1155415773391724
# In epoch 5, loss: 0.6575013995170593
# In epoch 10, loss: 0.615079939365387
# In epoch 15, loss: 0.5571699142456055
# In epoch 20, loss: 0.4741498529911041
# In epoch 25, loss: 0.4196116626262665
# In epoch 30, loss: 0.3800223767757416
# In epoch 35, loss: 0.35212990641593933
# In epoch 40, loss: 0.31633520126342773
# In epoch 45, loss: 0.29031288623809814
# In epoch 50, loss: 0.26397034525871277
# In epoch 55, loss: 0.232723668217659
# In epoch 60, loss: 0.1946088671684265
# In epoch 65, loss: 0.1504456251859665
# In epoch 70, loss: 0.1053193062543869
# In epoch 75, loss: 0.06971405446529388
# In epoch 80, loss: 0.04405388981103897
# In epoch 85, loss: 0.027077697217464447
# In epoch 90, loss: 0.01675054244697094
# In epoch 95, loss: 0.010031377896666527

# ----------- 5. check results ------------------------ #
pred = torch.sigmoid((logits[test_u] * logits[test_v]).sum(dim=1))
print('Accuracy', ((pred >= 0.5) == test_label).sum().item() / len(pred))
# Accuracy 0.88

 

 


 

 

 

 

DGL을 활용하여 그래프 기계학습으로 할 수 있는 간단한 예제 실습을 해봤습니다.

각 GNN 모델의 Agg, Update 등을 수식으로 봤을 때는 어려웠는데, 코드구현에서는 메서드로 잘 되어 있어서 실습에는 오히려 어려움이 덜 했던 것 같습니다.

GNN은 연산은 복잡하지만 코드구현은 일반 딥러닝보다 층이 얕고 간단하다고 했는데 진짜 그런 것 같습니다...

물론 간단한 실습이었지만..ㅎㅎ

 

환경 문제도 빨리 해결해서 더 많은 공부를 해야겠습니다!!

 

 

 

 

 

 

 

 

출처 :

- 충남대학교 임성수 교수님 그래프 기계학습 강의

- github.com : myeonghak/DGL-tutorial

- SNAP

'STUDY > GNN' 카테고리의 다른 글

[GNN] Networkx 실습  (1) 2024.11.14
[GNN] Lecture 2_(2) : 전통 그래프  (2) 2024.09.29
[GNN] Lecture 2 _(1) : 전통 그래프  (1) 2024.09.29
[GNN] Lecture 1 : 그래프 소개  (0) 2024.09.29
[GNN] Overview  (2) 2024.09.28
Comments