일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- graphrag
- paper review
- 파이썬
- 마이온컴퍼니
- GNN
- ux·ui디자인
- TiL
- tog
- 프로젝트
- DP
- 시각화
- DFS
- 멋재이사자처럼
- parklab
- intern10
- Join
- 인턴10
- SQL
- BFS
- Python
- likelion
- 마이온
- 그리디
- 멋사
- 멋쟁이사자처럼
- seaborn
- Rag
- folium
- likelionlikelion
- 알고리즘
- Today
- Total
지금은마라톤중
[GNN] 노드분류와 링크예측 실습(feat.DGL) 본문
그래프 기계학습의 대표적인 예
- 노드 분류
- 링크 예측
노드 분류 (Node Classification)
- 정의:그래프의 각 노드에 특정 레이블(클래스)을 예측하는 작업.
- 예: 소셜 네트워크에서 사용자의 관심사 분류.
- 입력: 노드 피처(특성)와 그래프 구조.
- 출력: 각 노드의 클래스(레이블).
- 활용 사례:
- 연구 논문 데이터에서 논문의 주제 분류.
- 소셜 네트워크에서 사용자의 성향 분류.
링크 예측 (Link Prediction)
- 정의:그래프에서 두 노드 간의 연결 여부를 예측하는 작업.
- 예: 추천 시스템에서 사용자와 상품 간 연결(구매 가능성) 예측.
- 입력: 두 노드 쌍과 그래프 구조.
- 출력: 두 노드 간 연결 확률(0~1).
- 활용 사례:
- 추천 시스템에서 친구 추천 또는 제품 추천.
- 네트워크 복구를 위한 연결 가능성 분석.
GNN 모델 종류
- GCN, GAT, GraphSAGE 등등
GNN | 단순 합 또는 평균 | 가중치 없음 | 기본적이고 직관적 | 모든 이웃을 동일하게 취급 |
GCN | 모든 이웃의 평균/합산 | 정규화된 단일 가중치 적용 | 단순하면서 계산 효율적 | 모든 이웃을 동일하게 취급 |
GAT | 모든 이웃 | Attention 가중치 적용 | 이웃 간 중요도를 반영 가능 | 계산량 증가 |
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 |