GCN과 GAT로 구현하는 그래프 러닝 노드 분류와 링크 예측
GCN·GAT의 메시지 패싱 방식과 노드 분류, 링크 예측 파이프라인을 데이터 품질·스케일링·보안 운영 관점에서 정리한다.
2026-08-14 · 최초 발행 2024-04-29
관계 구조를 학습에 넣는 그래프 러닝
소셜 네트워크, 지식 그래프, 추천 시스템, 바이오·화학 분자 구조처럼 관계 자체가 중요한 데이터에서는 벡터 피처만으로 패턴을 설명하기 어렵다. 그래프 러닝은 노드·엣지·속성으로 구성된 구조를 대상으로 관계성, 인접성, 구조 패턴을 함께 학습한다.
Graph Neural Networks(GNN)는 메시지 패싱을 통해 이웃 노드의 정보를 집계하고, 그 결과로 각 노드의 임베딩을 갱신한다. 레이어가 반복될수록 지역 이웃의 문맥이 더 넓은 범위로 확장된다.
GCN(Graph Convolutional Network)은 스펙트럴 또는 공간적 컨볼루션의 근사를 이용해 이웃 정보를 평균·정규화하는 모델이다. 구조가 단순하고 안정적이며 확장성 측면에서 기본 선택지가 된다. GAT(Graph Attention Network)은 어텐션으로 이웃별 가중치를 동적으로 학습한다. 이질적인 연결이나 허브 노드가 있는 그래프에서 표현력을 높일 수 있다.
노드 분류는 생성된 노드 임베딩으로 레이블을 예측하는 작업이다. 반지도 설정이 흔하며, 학습·검증·테스트 마스크를 분리해 사용한다. 링크 예측은 두 노드 사이에 엣지가 존재할 가능성을 추정하며 추천, 지식 그래프 완성, 이상 탐지에 활용된다.
메시지 패싱부터 평가 분리까지
입력은 노드 피처와 엣지 목록이며, 모델은 이웃 피처를 집계한 뒤 노드 상태를 갱신한다. 집계에는 합, 평균, 최대, 어텐션 방식을 사용할 수 있다.
정규화와 안정성도 함께 다뤄야 한다. 대칭 정규화 라플라시안과 self-loop를 적용하고, dropout이나 LayerNorm을 사용한다. 깊이가 과도해질 때 발생하는 over-smoothing은 레이어 깊이 제어와 residual로 완화할 수 있다.
그래프 규모가 커지면 전체 그래프를 한 번에 처리하기 어렵다. neighbor sampling, GraphSAGE 형태의 미니배치, 클러스터링 기반 파티셔닝을 적용하고, 그래프 파이프라인 캐싱 및 피처 서버 분리를 고려한다.
평가 방식은 과업별로 나뉜다. 노드 분류에서는 train/val/test 마스크와 정확도·F1을 사용한다. 링크 예측에서는 positive/negative 샘플링을 구성하고 AUC/AP를 평가 지표로 둔다. 시간 분리와 엄격한 엣지 스플릿은 데이터 누수를 막기 위한 전제다.
운영 환경에서는 동적 그래프 갱신과 증분 재학습 파이프라인이 필요하다. 관계 데이터의 민감도를 고려해 익명화, 엣지 가중 클리핑, 접근 제어도 적용한다.
학습과 배포가 이어지는 흐름
원천 로그·DB·파일에서 노드와 엣지 스키마를 정한 뒤, 피처 엔지니어링·정규화·샘플링·서브그래프 생성을 거쳐 학습 데이터를 만든다. GCN 또는 GAT 학습 뒤에는 검증을 수행하고, 임베딩과 추론 API를 배포한다. 이후 스케줄 재학습과 모니터링으로 상태를 추적한다.
결측 피처는 임퓨테이션하고 이상 엣지는 필터링한다. 클래스 불균형에는 가중 손실 또는 오버샘플링을 적용할 수 있다. 특히 시간 기준 분리와 엣지 스플릿은 학습 단계보다 먼저 확정해야 한다.
균일한 이웃에는 GCN, 차이가 큰 연결에는 GAT
| 관점 | GCN | GAT |
|---|---|---|
| 성능 | 균일 이웃에서 안정적 성능 | 이질적 이웃·허브에서 우수 성능 |
| 확장성 | 파라미터·연산량 경제성 우수 | 어텐션 계산 부담, 대규모 시 최적화 필요 |
| 일관성 | 정규화 집계로 변동성 낮음 | 데이터 질에 민감, 헤드/드롭아웃 튜닝 필요 |
| 안정성 | 과도한 깊이에서 over-smoothing 주의 | 헤드 수 증가 시 과적합·메모리 사용 증가 |
| 운영 편의 | 단순 구조로 재현성·배포 용이 | 성능-비용 트레이드오프 조정 필요 |
GCN은 정규화된 이웃 집계가 필요한 상황에서 먼저 적용하기 좋다. 이웃마다 정보 가치가 크게 다르거나 허브 노드의 영향을 구분해야 한다면 GAT을 검토할 수 있다. 다만 GAT은 어텐션 연산과 헤드·드롭아웃 설정에 따른 비용 및 튜닝 부담을 함께 감안해야 한다.
PyTorch Geometric으로 노드 분류와 링크 예측 실행하기
이 예시는 Cora 데이터셋의 노드 분류와 간단한 링크 예측을 다룬다. Python 3.10+, PyTorch 2.2+, PyTorch Geometric 2.4+를 전제로 하며, GPU는 선택 사항이고 CPU에서도 실행할 수 있다.
# env: python>=3.10, torch>=2.2, torch_geometric>=2.4
import torch
import torch.nn.functional as F
from torch import nn
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv, GATConv
from torch_geometric.transforms import RandomLinkSplit
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# ----- 모델 정의 -----
class GCN(nn.Module):
def __init__(self, in_dim, hid=64, out_dim=16, num_classes=None, dropout=0.5):
super().__init__()
self.conv1 = GCNConv(in_dim, hid, add_self_loops=True, normalize=True)
self.conv2 = GCNConv(hid, out_dim, add_self_loops=True, normalize=True)
self.classifier = nn.Linear(out_dim, num_classes) if num_classes else None
self.dropout = dropout
def forward(self, x, edge_index, return_logits=False):
x = F.relu(self.conv1(x, edge_index))
x = F.dropout(x, p=self.dropout, training=self.training)
z = self.conv2(x, edge_index)
if self.classifier and return_logits:
return self.classifier(z)
return z
class GAT(nn.Module):
def __init__(self, in_dim, hid=8, heads=8, out_dim=16, num_classes=None, dropout=0.6):
super().__init__()
self.gat1 = GATConv(in_dim, hid, heads=heads, dropout=dropout)
self.gat2 = GATConv(hid*heads, out_dim, heads=1, concat=False, dropout=dropout)
self.classifier = nn.Linear(out_dim, num_classes) if num_classes else None
self.dropout = dropout
def forward(self, x, edge_index, return_logits=False):
x = F.elu(self.gat1(x, edge_index))
x = F.dropout(x, p=self.dropout, training=self.training)
z = self.gat2(x, edge_index)
if self.classifier and return_logits:
return self.classifier(z)
return z
# ----- 1) 노드 분류 -----
dataset = Planetoid(root='data', name='Cora')
data = dataset[0].to(device)
model = GCN(dataset.num_features, hid=64, out_dim=32, num_classes=dataset.num_classes).to(device)
opt = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
def train_node_cls():
model.train()
opt.zero_grad()
logits = model(data.x, data.edge_index, return_logits=True)
loss = F.cross_entropy(logits[data.train_mask], data.y[data.train_mask])
loss.backward()
opt.step()
return float(loss)
@torch.no_grad()
def eval_node_cls():
model.eval()
logits = model(data.x, data.edge_index, return_logits=True)
preds = logits.argmax(dim=1)
accs = []
for mask in [data.train_mask, data.val_mask, data.test_mask]:
acc = (preds[mask] == data.y[mask]).float().mean().item()
accs.append(acc)
return accs # [train, val, test]
for epoch in range(1, 201):
loss = train_node_cls()
if epoch % 20 == 0:
tr, va, te = eval_node_cls()
print(f"[NodeCls] Epoch {epoch:03d} | Loss {loss:.3f} | Acc T/V/S {tr:.3f}/{va:.3f}/{te:.3f}")
# ----- 2) 링크 예측 -----
# 데이터 스플릿: 엣지 누수 방지
splitter = RandomLinkSplit(num_val=0.05, num_test=0.1, is_undirected=True, add_negative_train_samples=True)
train_data, val_data, test_data = splitter(dataset[0])
train_data, val_data, test_data = train_data.to(device), val_data.to(device), test_data.to(device)
lp_model = GAT(dataset.num_features, hid=8, heads=8, out_dim=32).to(device) # 임베딩만 사용
lp_opt = torch.optim.Adam(lp_model.parameters(), lr=0.005, weight_decay=1e-4)
bce = nn.BCEWithLogitsLoss()
def dot_decoder(z, edge_label_index):
src, dst = edge_label_index
return (z[src] * z[dst]).sum(dim=-1) # 점곱
def train_link_pred():
lp_model.train()
lp_opt.zero_grad()
z = lp_model(train_data.x, train_data.edge_index)
logits = dot_decoder(z, train_data.edge_label_index)
loss = bce(logits.view(-1), train_data.edge_label.float())
loss.backward()
lp_opt.step()
return float(loss)
@torch.no_grad()
def eval_link_pred(split):
lp_model.eval()
d = {'val': val_data, 'test': test_data}[split]
z = lp_model(d.x, d.edge_index)
logits = dot_decoder(z, d.edge_label_index)
probs = logits.sigmoid().view(-1)
labels = d.edge_label.float().view(-1)
# 간단 AUC 근사: 정밀 계산은 sklearn.metrics 사용 권장
# 여기서는 PR-AUC/AP 대신 ROC-AUC 계산에 대한 최신 정보 확인 필요
# 의존성 최소화를 위해 스코어 임계값 기반 정확도 산출
acc = ((probs > 0.5) == (labels > 0.5)).float().mean().item()
return acc
for epoch in range(1, 151):
loss = train_link_pred()
if epoch % 25 == 0:
vacc = eval_link_pred('val')
print(f"[LinkPred] Epoch {epoch:03d} | Loss {loss:.3f} | Val-Acc {vacc:.3f}")
tacc = eval_link_pred('test')
print(f"[LinkPred] Test-Acc {tacc:.3f}")
링크 예측 평가는 ROC-AUC/AP 사용이 권장되며, 실무에서는 sklearn.metrics 또는 torchmetrics 연계가 필요하다. 대규모 그래프에서는 NeighborSampler/pyg.loader를 사용하고, 배치 추론과 피처 서버 분리를 검토한다. 링크 스플릿 이후에는 학습 그래프만으로 임베딩을 계산해 누수를 차단해야 한다.
관계 데이터가 쓰이는 곳
추천 시스템에서는 사용자-아이템 이분 그래프의 링크 예측으로 추천을 생성하고, 시간 창 기반 스플릿과 신선도 가중을 적용한다.
이상 거래 탐지에서는 계정-디바이스-거래 그래프의 노드 분류로 사기 여부를 예측한다. 서브그래프 증거를 추출해 설명 가능성을 강화할 수 있다.
지식 그래프에서는 누락된 엔티티 관계를 링크 예측으로 보강한다. Relation-aware GNN 또는 TransE류 임베딩과 하이브리드 구성을 사용할 수 있다.
신약 탐색과 재료 과학에서는 분자 그래프의 노드·그래프 분류로 물성을 예측한다. 화학 결합과 링 구조 같은 도메인 피처를 멀티스케일 집계와 결합한다.
성능뿐 아니라 운영 비용까지 보는 기준
관계 정보를 활용하면 전통 MLP 대비 3~10%p 정확도 개선 가능성이 있으며, 극히 희소한 레이블 환경에서는 준지도 학습 효과를 기대할 수 있다. 구조 신호를 이용하면 라벨 효율성이 높아지고, 도메인이 이동해도 이웃 문맥을 바탕으로 견고성을 확보할 수 있다.
임베딩 재사용과 배치·온라인 혼합 추론은 인프라 비용 절감에 연결된다. 그래프 인덱싱과 캐싱은 지연 시간을 낮추는 데 쓰인다.
운영 단계에서는 GCN을 우선 적용하고, 성능 확장이 필요할 때 GAT을 선택한다. over-smoothing을 피하려면 깊이를 2~3으로 제한하고 residual/skip, PairNorm을 고려한다. 대규모 그래프에는 neighbor sampling, 파티셔닝 학습, 피처·엣지 캐시를 적용한다.
동적 그래프는 증분 재학습, warm-start, 임베딩 캐시 무효화 정책이 필요하다. 평가에서는 시간 기준 스플릿과 그룹 누수를 막고, 온라인 A/B와 드리프트 모니터링, 오류 사례 서브그래프의 재현성을 확보한다. PII 비식별화, 관계 민감도 마스킹, 엣지 가중 클리핑을 적용하며, 멤버십 추론 공격 대응을 위한 DP-SGD와 노이즈 주입의 트레이드오프도 검토한다.