GCN(Graph Convolutional Networks)는 각 노드의 특징뿐만 아니라, 인접 노드의 특징도 고려하여 새로운 특징을 생성합니다.
입력 데이터 X는 각 노드의 초기 특징을 나타냅니다.
인접 행렬 A는 그래프에서 노드 간의 연결 정보를 포함합니다.
A와 X의 행렬곱을 통해 이웃 노드의 정보를 집계할 수 있습니다.
단순히 A⋅X를 사용하면, 연결 수가 많은 노드가 지나치게 많은 영향을 미칠 수 있어 차수 행렬을 통해 인접행렬을 정규화 합니다.
이 과정을 통해 GCN은 노드 자체의 특징과 이웃 노드의 특징을 통합하고 그래프 구조를 반영한 새로운 노드 표현을 학습합니다.
GCN의 계산을 각 단계마다 확인해보겠습니다.
인접행렬(Ajacency matrix)
5개의 노드를 가진 그래프와, 그래프를 인접행렬로 나타낸 예입니다.
특징행렬(Feature maxrix)
이전까진 각 노드에 특징이 없었습니다.
기계학습에서는 각 노드는 성별, 국적, 나이등의 특징량을 가지고 있습니다.
행은 각 노드의 특징량, 열은 특징을 나타내는 일반적인 머신러닝의 테이블 정보입니다.
각 노드의 특징량을 나타내는행렬을 특징 행렬 X로 표현합니다.
GCN수식
GCN의 다층구조에 대한 일반적인 표현입니다.
X : 각 노드의 특징행렬
H(l) : 중간층의 특징량
A : 인접 행렬
f : 그래프 신경망의 변환 함수(Transformation Function).
초기 입력으로 H(0)=X를 사용하며, X는 각 노드의 초기 특징 행렬입니다.
와 A를 입력으로 받아 H(l+1)를 출력합니다.
H0에서 계산 되는 수식입니다.
f(H(0), A) = f(X, A) = φ(AX · W )
이 수식은 그래프 구조와 특징을 통합하는 GCN의 핵심 아이디어 입니다.
인접행렬과 특징행렬을 행렬 곱한 후 각 노드의 이웃 정보가 집계된 노드 특징을 나타냅니다.
행렬곱 이후에도 행렬의 크기는 동일합니다.
AX의 행렬 곱에서 문제점이 있습니다. 자신의 노드의 특징이 채워지지 않습니다.
이 문제를 해경하기 위해 인접행렬은 단위행렬을 더한 인접행령을 사용합니다.
AX행렬곱이 수행되고 이웃 노드의 특징량이 통합된 새로운 특징량이 추가됩니다.
다음은 AX · W 로 가중치 행렬곱을 수행합니다.
가중치는 입력차원 * 출력차원 의 초기값 random value로 할당합니다.
가중치 생성 예입니다. 입력차원이 Age, Nationality, Gender 3개이고, 출력차원은 4개로 생성합니다.
# W 행렬 생성 (입력 차원: 3, 출력 차원: 4)
#초기값은 랜덤
W = np.random.randn(3, 4) # 3x4 행렬
AX와 W의 행렬곱을 수행합니다. 출력차원을 4로 지정해서 최종 출력은 5*4 행렬이 출력됩니다.
φ(AX · W ) 마지막 단계로 활성화 함수를 적용합니다. 활성화 함수는 Relu를 지정한 예입니다.
AXW_relu = np.maximum(0, AXW)
AXW_relu
RELU 적용으로 양수는 값 그대로 출력되고, 음수는 0으로 비선형 변환되었습니다.
결과를 학습에 바로 사용하면 한가지 문제가 있습니다. 연결 수가 많은 노드가 값이 커져 지나치게 많은 영향을 미칠 수 있습니다. 최종 인접 행렬을 차수 행렬을 통해 정규화 합니다.
정규화된 인접 행렬
GCN Layer에 노드수가 항상 동일하지 않습니다.
위 과정을 통합 최종 인접행렬은 정규화된 인접행렬로 사용됩니다.
노드 개수에 따른 정규화를 위해 논문( Semi-Supervised Classification with Graph Convolutional Networks )에서는 정규화된 인접 행렬(hat A)를 다음과 같이 정의합니다.
(1) 자기자신의 특징량 추가
인접행렬에서 단위행렬을 더합니다.
adjacency += torch.eye(adjacency.size(0))
(2) 차수 행렬(D) 계산
인접행렬의 각 row의 합이 차수행렬이 됩니다.
degree = adjacency.sum(dim=1)
(3) 역제곱근 계산
degree_inv_sqrt = torch.pow(degree, -0.5)
degree_inv_sqrt[degree_inv_sqrt == float('inf')] = 0 # 무한대 방지
(4) 정규화된 인접 행렬 계산
최종적으로 정규화된 인접행렬을 생성합니다.
degree_matrix = torch.diag(degree_inv_sqrt)
A = degree_matrix @ adjacency @ degree_matrix
GCN 구현
GCN을 직접 사용하기 전, 동작을 이해하기 위해 GCN을 구현한 예입니다.
위 수식에서 설명한 절차가 단계적으로 적용되어 있습니다.
GCN동작 내용을 이해한 대로 구현한 내용으로 실제 GCN동작과는 차이가 있을것 같습니다.
마지막으로 PyTorch Geometric를 사용해 아래 코드의 결과와 비교해보겠습니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.utils import to_dense_adj
# 데이터셋 로드
dataset = Planetoid(root='.', name='Cora')
data = dataset[0]
# GCN 레이어 정의
class MyGCNLayer(nn.Module):
def __init__(self, dim_in, dim_out):
super().__init__()
self.linear = nn.Linear(dim_in, dim_out, bias=False)
def forward(self, x, adjacency):
x = self.linear(x)
x = torch.matmul(adjacency, x) # torch.matmul 사용
return x
# GCN 모델 정의
class MyGCN(nn.Module):
def __init__(self, dim_in, dim_h, dim_out):
super().__init__()
self.gnn1 = MyGCNLayer(dim_in, dim_h)
self.gnn2 = MyGCNLayer(dim_h, dim_out)
def forward(self, x, adjacency):
h = self.gnn1(x, adjacency)
h = F.relu(h)
h = self.gnn2(h, adjacency)
return F.log_softmax(h, dim=1)
@staticmethod
def normalize_adjacency(adjacency):
"""GCN 정규화 함수: D^(-1/2) * (A + I) * D^(-1/2)"""
# 자기 연결 추가
adjacency += torch.eye(adjacency.size(0))
# 차수 행렬 계산
degree = adjacency.sum(dim=1)
degree_inv_sqrt = torch.pow(degree, -0.5)
degree_inv_sqrt[degree_inv_sqrt == float('inf')] = 0 # 무한대 방지
# D^(-1/2) * A * D^(-1/2)
degree_matrix = torch.diag(degree_inv_sqrt)
return degree_matrix @ adjacency @ degree_matrix
def fit(self, data, adjacency, epochs=100):
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(self.parameters(), lr=0.01, weight_decay=5e-4)
self.train()
# 정규화된 인접 행렬 생성
normalized_adjacency = self.normalize_adjacency(adjacency)
for epoch in range(epochs+1):
optimizer.zero_grad()
out = self(data.x, normalized_adjacency)
loss = criterion(out[data.train_mask], data.y[data.train_mask])
acc = self.accuracy(out[data.train_mask].argmax(dim=1), data.y[data.train_mask])
loss.backward()
optimizer.step()
if epoch % 20 == 0:
val_loss = criterion(out[data.val_mask], data.y[data.val_mask])
val_acc = self.accuracy(out[data.val_mask].argmax(dim=1), data.y[data.val_mask])
print(f'Epoch {epoch:>3} | Train Loss: {loss:.3f} | Train Acc: {acc*100:>5.2f}% | Val Loss: {val_loss:.3f} | Val Acc: {val_acc*100:>5.2f}%')
@torch.no_grad()
def test(self, data, adjacency):
self.eval()
normalized_adjacency = self.normalize_adjacency(adjacency)
out = self(data.x, normalized_adjacency)
acc = self.accuracy(out.argmax(dim=1)[data.test_mask], data.y[data.test_mask])
return acc
@staticmethod
def accuracy(y_pred, y_true):
return (y_pred == y_true).sum().item() / len(y_true)
# 인접 행렬 밀집 변환
adjacency = to_dense_adj(data.edge_index)[0]
# 모델 학습 및 테스트
gcn = MyGCN(dataset.num_features, 16, dataset.num_classes)
gcn.fit(data, adjacency, epochs=100)
test_acc = gcn.test(data, adjacency)
print(f"Test Accuracy: {test_acc*100:.2f}%")
Epoch 0 | Train Loss: 1.947 | Train Acc: 11.43% | Val Loss: 1.945 | Val Acc: 10.80%
Epoch 20 | Train Loss: 0.238 | Train Acc: 99.29% | Val Loss: 0.872 | Val Acc: 77.40%
Epoch 40 | Train Loss: 0.024 | Train Acc: 100.00% | Val Loss: 0.723 | Val Acc: 78.60%
Epoch 60 | Train Loss: 0.021 | Train Acc: 100.00% | Val Loss: 0.711 | Val Acc: 78.60%
Epoch 80 | Train Loss: 0.023 | Train Acc: 100.00% | Val Loss: 0.703 | Val Acc: 79.00%
Epoch 100 | Train Loss: 0.020 | Train Acc: 100.00% | Val Loss: 0.704 | Val Acc: 78.00%
Test Accuracy: 79.60%
정확도가 이전보다 상승하였습니다.
GCNConv 로 구현
PyTorch Geometric은 PyTorch 기반의 그래프 신경망(Graph Neural Networks, GNN) 및 관련 작업을 수행하기 위한 오픈소스 라이브러리입니다. 그래프 구조 데이터를 효율적으로 처리하고, GNN 모델을 구현, 학습, 평가할 수 있는 기능을 제공합니다.
GCNConv는 PyTorch Geometric에서 제공하는 그래프 컨볼루션 네트워크(Graph Convolutional Network, GCN) 레이어 입니다. GCN 의 핵심 수식과 기능을 자동으로 처리하여, 그래프 신경망(Graph Neural Network, GNN)을 쉽게 구현할 수 있도록 설계되었습니다.
파라미터기본값 (Default)설명
파라미터 | 초기값 | 설명 |
in_channels | - | 입력 특징의 차원. 각 노드가 가진 초기 특징 수를 나타냄. 예: 노드 특징이 16차원일 경우 in_channels=16. |
out_channels | - | 출력 특징의 차원. 각 노드에 대해 학습할 새로운 표현의 크기를 나타냄. 예: 출력으로 32차원의 특징을 학습하고 싶다면 out_channels=32. |
improved | False | 논문의 기본 정규화 방식 대신 A+2I를 사용하는 개선된 정규화 방식을 사용할지 여부. |
add_self_loops | True | 자기 연결(self-loop)을 인접 행렬에 추가할지 여부. 자기 정보를 포함하려면 True. |
normalize | True | 인접 행렬 대칭 정규화 여부. False로 설정하면 정규화를 생략함. |
bias | True | 레이어에 학습 가능한 편향(bias) 항을 추가할지 여부. |
cached | False | 학습 시 인접 행렬의 정규화된 결과를 캐싱하여 반복 계산을 피할지 여부. (반복되는 그래프에서 유용) |
num_nodes | None | 그래프에서 노드의 수를 명시적으로 제공. 주로 그래프 구조가 고정되어 있거나 사전 계산된 데이터가 있을 때 사용. |
aggr |
"add" | 노드 특징을 집계(aggregation)하는 방식. 기본값은 합산("add")이며, 다른 옵션으로 평균("mean")과 최대값("max")이 있음. |
dtype | None | 가중치 행렬의 데이터 타입을 명시적으로 설정. 기본값은 None으로, 입력 데이터의 데이터 타입을 따름. |
구현
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv
# 데이터셋 로드
dataset = Planetoid(root='.', name='Cora')
data = dataset[0]
# PyTorch Geometric GCN 모델 정의
class MyGGCN(nn.Module):
def __init__(self, dim_in, dim_h, dim_out):
super().__init__()
self.conv1 = GCNConv(dim_in, dim_h)
self.conv2 = GCNConv(dim_h, dim_out)
def forward(self, x, edge_index):
h = self.conv1(x, edge_index)
h = F.relu(h)
h = self.conv2(h, edge_index)
return F.log_softmax(h, dim=1)
def fit(self, data, epochs=100):
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(self.parameters(), lr=0.01, weight_decay=5e-4)
self.train()
for epoch in range(epochs+1):
optimizer.zero_grad()
out = self(data.x, data.edge_index)
loss = criterion(out[data.train_mask], data.y[data.train_mask])
acc = self.accuracy(out[data.train_mask].argmax(dim=1), data.y[data.train_mask])
loss.backward()
optimizer.step()
if epoch % 20 == 0:
val_loss = criterion(out[data.val_mask], data.y[data.val_mask])
val_acc = self.accuracy(out[data.val_mask].argmax(dim=1), data.y[data.val_mask])
print(f'Epoch {epoch:>3} | Train Loss: {loss:.3f} | Train Acc: {acc*100:>5.2f}% | Val Loss: {val_loss:.3f} | Val Acc: {val_acc*100:>5.2f}%')
@torch.no_grad()
def test(self, data):
self.eval()
out = self(data.x, data.edge_index)
acc = self.accuracy(out.argmax(dim=1)[data.test_mask], data.y[data.test_mask])
return acc
@staticmethod
def accuracy(y_pred, y_true):
return (y_pred == y_true).sum().item() / len(y_true)
# GCN 모델 학습 및 테스트
model = MyGGCN(dataset.num_features, 16, dataset.num_classes)
model.fit(data, epochs=100)
test_acc_pyg = model.test(data)
print(f"Test Accuracy (PyTorch Geometric): {test_acc_pyg*100:.2f}%")
Epoch 0 | Train Loss: 1.954 | Train Acc: 15.71% | Val Loss: 1.946 | Val Acc: 20.60%
Epoch 20 | Train Loss: 0.101 | Train Acc: 100.00% | Val Loss: 0.771 | Val Acc: 78.20%
Epoch 40 | Train Loss: 0.014 | Train Acc: 100.00% | Val Loss: 0.731 | Val Acc: 78.20%
Epoch 60 | Train Loss: 0.014 | Train Acc: 100.00% | Val Loss: 0.718 | Val Acc: 78.20%
Epoch 80 | Train Loss: 0.017 | Train Acc: 100.00% | Val Loss: 0.717 | Val Acc: 77.80%
Epoch 100 | Train Loss: 0.015 | Train Acc: 100.00% | Val Loss: 0.719 | Val Acc: 77.60%
Test Accuracy (PyTorch Geometric): 81.00%
정확도가 81%로 코드를 , 역시 직접 구현한것보다 우수한 성능을 보입니다.
대칭정규화
대칭 정규화 개념이 조금 복잡해서 추가 정리해보았습니다.
인접행렬 A와 차수 행렬 D가 아래와 같이 있다고 가정합니다.
1) 단순정규화( $D^{-1} A$)
일반적으로 정규화는 차수를 단순 정규화 합니다.
각 행의 합이 1로 정규화됩니다. 하지만 열방향으로는 정규화가 되지 않습니다.
2) 대칭정규화($D^{-1/2}AD^{-1/2} $)
각 행과 열의 값을 정규화 합니다. 각 행과 열을 1로 만들지는 않지만 차수에 따른 편향을 완화 시킵니다.
대칭 정규화의 경우 차수가 높은 노드는 더 많은 정보량을 가지므로, 조금 더 큰값으로 표현됩니다.
이는 스펙트럼 정규화 방식으로 행렬의 고유값 분포를 조정하여 그래프의 smoothness를 증가시키는 역할을 합니다.
즉, 특정 노드가 차수에 의해 너무 많은 영향을 받거나, 너무 적은 영향을 받지 않도록 조정함.
대칭 정규화 결과 0.908, 1.149, 0.908 로 1로 정규화 되지 않습니다.
차수에 따라 정규화는 되지만, 차수가 높은 노드는 여전히 더 많은 정보를 가지게 됩니다.
'GNN' 카테고리의 다른 글
GNN(Graph Neural Network) - 7. GraphSAGE (0) | 2025.01.21 |
---|---|
GNN(Graph Neural Network) - 6. GAT(Graph Attention Network) (0) | 2025.01.09 |
GNN(Graph Neural Network) - 4. Node2Vect (0) | 2025.01.05 |
GNN(Graph Neural Network) - 3. 딥워크(DeepWalk) (1) | 2025.01.05 |
GNN(Graph Neural Network) - 2. 그래프와 행렬 (0) | 2025.01.04 |