GNN(Graph Neural Network) - 12. Heterogeneous Graph Attention Network
이종 그래프(Heterogeneous graph)
지금 까지의 그래프는 노드(Node)의 Feature들과 차원이 모두 같았습니다.
이종 그래프(heterogeneous graph)란, 여러 유형의 노드(node type)와 여러 유형의 엣지(edge type)로 구성된 그래프입니다. 반면, 동종 그래프(homogeneous graph)는 모든 노드와 엣지가 같은 타입을 가지는 단일 유형의 그래프입니다.
Heterogeneous Graph Attention Network (HAN) 논문은 이종 그래프에서 Attention 기반 GNN을 적용한 최초의 모델입니다.
논문 제목: "Heterogeneous Graph Attention Network"
저자: Xiao Wang, Houye Ji, Chuan Shi, Bai Wang, Peng Cui, P. Yu, Yanfang Ye
출처: Published in WWW 2019 (The Web Conference 2019)
https://arxiv.org/pdf/1903.07293
이종 그래프에서 여러 용어들 부터 정의해보겠습니다.
메타패스(meta-path)
메타패스(meta-path)란 이종 그래프(heterogeneous graph)에서 노드 간의 관계를 특정한 경로(Path)로 정의하는 방법입니다. 즉, 여러 개의 엣지를 따라 이동할 수 있는 경로를 패턴화한 것을 의미합니다.
메타패스의 예
- 노드 유형: 논문(P), 저자(A), 컨퍼런스(C)
- 메타패스 경로
- PAP (Paper → Author → Paper): 같은 저자가 쓴 논문
- PCP (Paper → Conference → Paper): 같은 컨퍼런스에서 발표된 논문
- PAPCP (Paper → Author → Paper → Conference → Paper): 같은 저자가 썼고, 같은 컨퍼런스에서 발표된 논문
이러한 방법 저자와 저자간의 협력관계, 논문과 논문간의 인용관계를 나타내는 메타패스가 될 수 있습니다.
HeteroData
HeteroData는 PyG에서 이종 그래프(heterogeneous graph)를 표현하는 표준 데이터 구조입니다.
이종 그래프에서는 서로 다른 노드 타입(node type)과 서로 다른 엣지 타입(edge type)이 존재하는데,
이를 효율적으로 저장하고 처리하기 위해 HeteroData를 사용합니다.
import torch
from torch_geometric.data import HeteroData
# HeteroData 객체 생성
data = HeteroData()
# 노드 추가 (논문과 저자)
data['paper'].x = torch.randn(5, 32) # 5개의 논문, 32차원 특징
data['author'].x = torch.randn(3, 32) # 3명의 저자, 32차원 특징
# 엣지 추가 (저자가 논문을 쓴 관계)
data['author', 'writes', 'paper'].edge_index = torch.tensor([
[0, 1, 2], # 출발 노드 (author)
[0, 1, 3] # 도착 노드 (paper)
])
data
HeteroData(
paper={ x=[5, 32] },
author={ x=[3, 32] },
(author, writes, paper)={ edge_index=[2, 3] }
)
논문에서 이종그래프의 예시입니다.
(a) 3가지 유형의 노드 (배우, 영화, 감독)
(b) 이종그래프는 3가지 유형의 노드와 2가지 유형의 간산으로 연결됩니다.
(c) 2개의 메타경로 MAM(Movie-Actor-Movie) 와 MDM(Movie-Director-Movie)
(d) Movie m1의 이웃 메타경로(m1,m2,m3)
DBLP(Digital Bibliography & Library Project) 데이터셋
PyTorch Geometric(Pyg)에서 제공하는 이종(heterogeneous) 그래프 데이터셋으로,
논문(Paper), 저자(Author), 컨퍼런스(Conference), 용어(Term) 간의 관계를 포함하는 학술 네트워크입니다.
컴퓨터 과학 논문 데이터를 포함하는 오픈 액세스 학술 데이터베이스로 논문-저자 관계를 활용한 노드 분류 실험에 자주 사용됩니다.
4가지 노드 타입 (Node Types)
- paper (논문)
- author (저자)
- conference (컨퍼런스)
- term (논문에 포함된 키워드)
3가지 엣지 타입 (Edge Types)
- author → writes → paper (저자가 논문을 씀)
- paper → published_in → conference (논문이 컨퍼런스에서 발표됨)
- paper → has → term (논문이 특정 용어(키워드)를 포함)
HAN Framework
HAN은 두가지 다른 Self Attention으로 구성됩니다.
Node-Level Attention + Semantic-Level Attention을 활용하여 메타패스별 중요도를 학습하는 모델입니다.
(1) 노드 레벨 어텐션(node-level attention) : 노드와 이웃 노드간 관계를 학습하여 중요한 이웃 노드를 학습
각 meta-path에서 추출된 이웃 노드들이 중심 노드에 미치는 영향을 학습하는 attention 메터니즘으로, meta-path based neighbor를 고려하여 self-attention을 통해 각 이웃의 중요도를 학습합니다.
(2) 시맨틱 레벨 어텐션(semantic-level attention) : 메타패스간 관계를 학습하여 중요한 메타패스를 학습
각 meta-path들이 노드 표현에 미치는 형향을 학습하는 attention 메커니즘입니다. 예를 들어 PAP 저자관계보다 PCP 컨퍼런스가 더 유용한 정보일 수 있습니다.
Algorithm 1: The overall process of HAN. |
Input : 이종 그래프 G = (V, E) 노드 feature ${h_i, ∀_𝑖 ∈ V}$ 메타패스 ${Φ_0, Φ_1, . . . , Φ_𝑃}$ 어텐션 헤더 수 K Output : 최종 임베딩 벡터 Z 노드 레벨 어텐션 가중치 𝛼 시멘트릭 레벨 어텐션 가중치 𝛽 |
1 for $Φ_𝑖 ∈ {Φ_0, Φ_1, . . . , Φ_𝑃 }$ do 2 for 𝑘 = 1...𝐾 do 3 노드 유형별 특징 변환 $h_𝑖^′ ← M_{𝜙𝑖} · h_𝑖$ ; 4 for 𝑖 ∈ V do 5 meta-path 기반의 네이버 $N_𝑖^Φ$를 찾는다.; 6 for 𝑗 ∈ $N_𝑖^Φ$ do 7 Node-Level Attention을 적용 가중치 $𝛼_{𝑖𝑗}^Φ$계산 ; 8 end 9 Node-Level Attention을 적용하여 새로운 노드 임베딩 생성: ![]() 10 end 11 모든 attention head에서 학습된 임베딩을 Concatenate: ![]() 12 end 13 각 메타패스 $\Phi_i의 중요도를 나타내는 가중치 $\beta_{\Phi_i}$를 학습; 14 Semantic-Level Attention을 적용하여 최종 노드 임베딩 Z 생성: 15 end 16 Cross-Entropy 손실 함수 계산 ![]() 17 역전파(Backpropagation) 및 HAN 모델의 가중치 업데이트 18 return 𝑍, 𝛼, 𝛽 |
HANConv
class HANConv(in_channels: Union[int, Dict[str, int]], out_channels: int, metadata: Tuple[List[str], List[Tuple[str, str, str]]], heads: int = 1, negative_slope=0.2, dropout: float = 0.0, **kwargs)
pytorch-geometric 에서 HAN을 구현한 라이브러리 입니다.
하이퍼 파라미터
하이퍼파라미터 | 설명 | Default / Range |
num_layers | 모델의 계층(layer) 수 | 1-3 |
hidden_dim | 각 레이어의 임베딩 차원 | 64~256 |
num_heads | 각 Attention Layer에서 사용되는 Head 수 | 1~8 |
dropout | Dropout 비율 (과적합 방지) | 0.3~0.6 |
lr | 학습률 (Learning Rate) | 0.001 ~ 0.005 |
weight_decay | 정규화 파라미터 (L2 정규화) | 1e-5 ~ 1e-3 |
meta_paths | 메타패스 목록 | |
aggregation | 메타패스 간 Aggregation 방식 (mean, concat, sum) | mean |
activation | 활성화 함수 (ReLU, LeakyReLU, ELU) | ReLU |
batch_size | 32 ~ 512 |
코드작성
import torch
import torch.nn.functional as F
from torch import nn
import torch_geometric.transforms as T
from torch_geometric.datasets import DBLP
from torch_geometric.nn import HANConv
# 1️⃣ 데이터셋 로드 및 확인
dataset = DBLP(root='.', transform=T.ToUndirected()) # 무방향 그래프 변환
data = dataset[0]
print(data) # 데이터 확인
# 2️⃣ DBLP 데이터셋의 'conference' 노드 특징이 없으므로, 임의로 추가
data['conference'].x = torch.zeros(data['conference'].num_nodes, 1)
# 3️⃣ HAN 모델 정의
class HAN(nn.Module):
def __init__(self, dim_in, dim_h, dim_out, heads=8):
super().__init__()
self.han = HANConv(
dim_in, dim_h, heads=heads, dropout=0.6, metadata=data.metadata()
)
self.linear = nn.Linear(dim_h, dim_out)
def forward(self, x_dict, edge_index_dict):
out = self.han(x_dict, edge_index_dict) # HANConv 적용
out = self.linear(out['author']) # 'author' 노드의 임베딩을 활용하여 분류
return out
# 4️⃣ 모델 초기화 (입력 차원 자동 설정)
dim_in = {k: v.size(1) for k, v in data.x_dict.items()} # 노드별 입력 차원
model = HAN(dim_in=dim_in, dim_h=128, dim_out=4) # 학문 분야(4개) 분류
# 5️⃣ 옵티마이저 설정
optimizer = torch.optim.Adam(model.parameters(), lr=0.005, weight_decay=0.001)
# 6️⃣ 성능 평가 함수 정의
@torch.no_grad()
def test(mask):
model.eval()
pred = model(data.x_dict, data.edge_index_dict).argmax(dim=-1)
acc = (pred[mask] == data['author'].y[mask]).sum() / mask.sum()
return float(acc)
# 7️⃣ 모델 학습 루프
for epoch in range(101):
model.train()
optimizer.zero_grad()
out = model(data.x_dict, data.edge_index_dict) # 모델 예측값
mask = data['author'].train_mask # 학습용 마스크
loss = F.cross_entropy(out[mask], data['author'].y[mask]) # 손실 계산
loss.backward()
optimizer.step()
if epoch % 20 == 0:
train_acc = test(data['author'].train_mask)
val_acc = test(data['author'].val_mask)
print(f'Epoch: {epoch:>3} | Loss: {loss:.4f} | Train Acc: {train_acc*100:.2f}% | Val Acc: {val_acc*100:.2f}%')
# 8️⃣ 최종 테스트 정확도 확인
test_acc = test(data['author'].test_mask)
print(f'Test Accuracy: {test_acc*100:.2f}%')
HeteroData(
author={
x=[4057, 334],
y=[4057],
train_mask=[4057],
val_mask=[4057],
test_mask=[4057],
},
paper={ x=[14328, 4231] },
term={ x=[7723, 50] },
conference={ num_nodes=20 },
(author, to, paper)={ edge_index=[2, 19645] },
(paper, to, author)={ edge_index=[2, 19645] },
(paper, to, term)={ edge_index=[2, 85810] },
(paper, to, conference)={ edge_index=[2, 14328] },
(term, to, paper)={ edge_index=[2, 85810] },
(conference, to, paper)={ edge_index=[2, 14328] },
(paper, rev_to, author)={ edge_index=[2, 19645] },
(author, rev_to, paper)={ edge_index=[2, 19645] },
(term, rev_to, paper)={ edge_index=[2, 85810] },
(conference, rev_to, paper)={ edge_index=[2, 14328] },
(paper, rev_to, term)={ edge_index=[2, 85810] },
(paper, rev_to, conference)={ edge_index=[2, 14328] }
)
Epoch: 0 | Loss: 1.3862 | Train Acc: 47.25% | Val Acc: 35.75%
Epoch: 20 | Loss: 0.1741 | Train Acc: 99.25% | Val Acc: 79.25%
Epoch: 40 | Loss: 0.0643 | Train Acc: 100.00% | Val Acc: 79.00%
Epoch: 60 | Loss: 0.0504 | Train Acc: 100.00% | Val Acc: 78.00%
Epoch: 80 | Loss: 0.0480 | Train Acc: 100.00% | Val Acc: 77.75%
Epoch: 100 | Loss: 0.0393 | Train Acc: 100.00% | Val Acc: 76.00%
Test Accuracy: 79.98%