TransE, DistMult, ComplEx 등 관계 예측 모델과 Node2Vec, GraphSAGE 등 노드 임베딩 기법, PyTorch Geometric을 활용한 구현까지 지식 그래프 임베딩의 핵심을 다룹니다.
**Graph Embedding(그래프 임베딩)**은 그래프의 노드, 엣지, 또는 전체 구조를 연속적인 벡터 공간에 매핑하는 기법입니다. 이를 통해 그래프의 구조적 정보를 머신러닝 모델에서 직접 활용할 수 있습니다.
그래프에서 가까운 노드(A, B, C)는 벡터 공간에서도 가까이 위치하고, 멀리 떨어진 노드(D, E)는 벡터 공간에서도 다른 영역에 위치합니다.
Knowledge Graph Embedding(KGE) 모델은 (head, relation, tail) 트리플의 점수를 학습합니다. 존재하는 트리플은 높은 점수를, 존재하지 않는 트리플은 낮은 점수를 갖도록 학습됩니다.
TransE는 가장 기본적이면서 직관적인 KGE 모델입니다. 관계를 벡터 공간에서의 **이동(translation)**으로 해석합니다.
핵심 아이디어: head + relation ≈ tail
import numpy as np
# 개념적 예시
head = np.array([1.0, 2.0, 3.0]) # "Neo4j" 임베딩
relation = np.array([0.5, -1.0, 0.5]) # "DEPENDS_ON" 임베딩
tail = np.array([1.5, 1.0, 3.5]) # "Java" 임베딩
# TransE 점수: head + relation 과 tail의 거리가 가까울수록 좋음
score = np.linalg.norm(head + relation - tail)
# score가 작을수록 해당 트리플이 존재할 가능성이 높음TransE의 특징을 정리하면 다음과 같습니다.
(Neo4j, DEPENDS_ON, Java) -- Java에 의존하는 기술이 여러 개일 때 한계 발생TransR은 TransE의 한계를 극복하기 위해, 엔티티와 관계를 별도의 벡터 공간에서 표현합니다.
핵심 아이디어: 관계별 변환 행렬 M_r을 도입하여, M_r * head + relation ≈ M_r * tail
import numpy as np
# TransR: 관계별 변환 행렬을 통해 다른 공간으로 프로젝션
entity_dim = 50 # 엔티티 벡터 차원
relation_dim = 30 # 관계 벡터 차원
head = np.random.randn(entity_dim)
tail = np.random.randn(entity_dim)
relation = np.random.randn(relation_dim)
M_r = np.random.randn(relation_dim, entity_dim) # 변환 행렬
# 엔티티를 관계 공간으로 프로젝션
head_projected = M_r @ head
tail_projected = M_r @ tail
score = np.linalg.norm(head_projected + relation - tail_projected)DistMult는 관계를 대각 행렬로 표현하여 엔티티 간의 유사도를 측정합니다.
핵심 아이디어: score = head * relation * tail (요소별 곱의 합)
import numpy as np
head = np.array([0.5, 0.3, 0.8])
relation = np.array([1.2, 0.1, 0.9]) # 대각 행렬의 대각 원소
tail = np.array([0.4, 0.7, 0.6])
# DistMult 점수: 요소별 곱의 합
score = np.sum(head * relation * tail)
# 점수가 높을수록 해당 트리플이 존재할 가능성이 높음ComplEx는 DistMult를 복소수 공간으로 확장하여 비대칭 관계도 표현할 수 있습니다.
import numpy as np
# ComplEx: 복소수 벡터 사용
head = np.array([0.5+0.3j, 0.8+0.1j, 0.2+0.6j])
relation = np.array([1.0+0.5j, 0.3+0.7j, 0.9+0.2j])
tail = np.array([0.4+0.2j, 0.7+0.5j, 0.6+0.3j])
# ComplEx 점수: Re(head * relation * conj(tail))의 합
score = np.real(np.sum(head * relation * np.conj(tail)))| 모델 | 점수 함수 | 대칭 관계 | 비대칭 관계 | 1:N 관계 | 복잡도 |
|---|---|---|---|---|---|
| TransE | head + rel - tail 거리 | 불가 | 가능 | 약함 | 낮음 |
| TransR | 관계별 프로젝션 + 거리 | 가능 | 가능 | 가능 | 높음 |
| DistMult | 요소별 곱의 합 | 가능 | 불가 | 가능 | 낮음 |
| ComplEx | 복소수 곱의 실수부 합 | 가능 | 가능 | 가능 | 중간 |
실전에서는 ComplEx가 가장 균형 잡힌 선택입니다. 대칭/비대칭 관계를 모두 처리할 수 있으며, 계산 복잡도도 합리적입니다. 대규모 그래프에서는 TransE의 단순성이 장점이 될 수 있습니다.
KGE 모델이 트리플 단위의 관계를 학습하는 것과 달리, 노드 임베딩 기법은 그래프의 전체 구조를 반영하여 각 노드의 벡터 표현을 학습합니다.
Node2Vec는 그래프 위에서 **Random Walk(랜덤 워크)**를 수행하고, Word2Vec과 유사한 방식으로 노드 임베딩을 학습합니다.
from node2vec import Node2Vec
import networkx as nx
# 그래프 생성
G = nx.Graph()
G.add_edges_from([
("Neo4j", "Cypher"),
("Neo4j", "GDS"),
("Neo4j", "GraphRAG"),
("GraphRAG", "LangChain"),
("GraphRAG", "벡터검색"),
("Python", "Neo4j"),
("Python", "LangChain"),
])
# Node2Vec 모델 학습
node2vec = Node2Vec(
G,
dimensions=64, # 임베딩 차원
walk_length=30, # 랜덤 워크 길이
num_walks=200, # 노드당 워크 횟수
p=1, # 되돌아가기 확률 제어
q=1, # 탐색 깊이 제어
workers=4
)
model = node2vec.fit(window=10, min_count=1, batch_words=4)
# 유사 노드 검색
similar = model.wv.most_similar("Neo4j", topn=3)
for node, similarity in similar:
print(f" {node}: {similarity:.3f}")Node2Vec의 핵심 파라미터는 다음과 같습니다.
**GraphSAGE(Graph Sample and Aggregate)**는 이웃 노드의 특성을 집계하여 노드 임베딩을 생성하는 GNN(Graph Neural Network, 그래프 신경망) 모델입니다.
Node2Vec와 달리 GraphSAGE는 다음과 같은 장점이 있습니다.
Neo4j GDS에서 제공하는 **FastRP(Fast Random Projection)**는 빠른 노드 임베딩 생성 알고리즘입니다.
// Neo4j GDS에서 FastRP 실행
CALL gds.graph.project('techGraph', 'Technology', 'DEPENDS_ON')
CALL gds.fastRP.stream('techGraph', {
embeddingDimension: 128,
iterationWeights: [0.0, 1.0, 1.0, 0.5]
})
YIELD nodeId, embedding
WITH gds.util.asNode(nodeId) AS node, embedding
RETURN node.name, embedding
LIMIT 5**Link Prediction(링크 프레딕션)**은 그래프에서 아직 연결되지 않은 노드 쌍 사이에 관계가 존재할 가능성을 예측하는 작업입니다. 지식 그래프 완성(Knowledge Graph Completion)의 핵심 기법입니다.
import torch
from torch_geometric.data import Data
from torch_geometric.nn import SAGEConv
import torch.nn.functional as F
class LinkPredictor(torch.nn.Module):
"""GraphSAGE 기반 링크 프레딕션 모델입니다."""
def __init__(self, in_channels: int, hidden_channels: int, out_channels: int):
super().__init__()
self.conv1 = SAGEConv(in_channels, hidden_channels)
self.conv2 = SAGEConv(hidden_channels, out_channels)
def encode(self, x: torch.Tensor, edge_index: torch.Tensor) -> torch.Tensor:
"""노드 임베딩을 생성합니다."""
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return x
def decode(self, z: torch.Tensor, edge_label_index: torch.Tensor) -> torch.Tensor:
"""두 노드 간의 링크 확률을 계산합니다."""
src = z[edge_label_index[0]]
dst = z[edge_label_index[1]]
return (src * dst).sum(dim=-1)
def forward(self, x: torch.Tensor, edge_index: torch.Tensor,
edge_label_index: torch.Tensor) -> torch.Tensor:
z = self.encode(x, edge_index)
return self.decode(z, edge_label_index)from torch_geometric.utils import negative_sampling
def train_epoch(model: LinkPredictor, data: Data,
optimizer: torch.optim.Optimizer) -> float:
"""1 에포크 학습을 수행합니다."""
model.train()
optimizer.zero_grad()
# 노드 임베딩 생성
z = model.encode(data.x, data.edge_index)
# 포지티브 + 네거티브 샘플링
pos_edge_index = data.edge_label_index
neg_edge_index = negative_sampling(
edge_index=data.edge_index,
num_nodes=data.num_nodes,
num_neg_samples=pos_edge_index.size(1)
)
edge_label_index = torch.cat([pos_edge_index, neg_edge_index], dim=-1)
edge_label = torch.cat([
torch.ones(pos_edge_index.size(1)),
torch.zeros(neg_edge_index.size(1))
])
# 예측 및 손실 계산
pred = model.decode(z, edge_label_index).sigmoid()
loss = F.binary_cross_entropy(pred, edge_label)
loss.backward()
optimizer.step()
return loss.item()링크 프레딕션 모델을 학습할 때, 테스트 데이터의 엣지가 학습 그래프에 포함되지 않도록 주의해야 합니다. 시간 기반 분할(과거 관계로 학습, 미래 관계 예측)이 실전에서 가장 신뢰할 수 있는 평가 방법입니다.
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
def find_similar_entities(target_embedding: np.ndarray,
all_embeddings: dict[str, np.ndarray],
top_k: int = 5) -> list[tuple[str, float]]:
"""타겟 엔티티와 가장 유사한 엔티티를 찾습니다."""
names = list(all_embeddings.keys())
vectors = np.array(list(all_embeddings.values()))
similarities = cosine_similarity(
target_embedding.reshape(1, -1), vectors
)[0]
top_indices = np.argsort(similarities)[::-1][:top_k]
return [(names[i], float(similarities[i])) for i in top_indices]import numpy as np
def combine_embeddings(graph_emb: np.ndarray,
text_emb: np.ndarray,
alpha: float = 0.5) -> np.ndarray:
"""그래프 임베딩과 텍스트 임베딩을 가중 결합합니다."""
# 차원 정규화
graph_norm = graph_emb / (np.linalg.norm(graph_emb) + 1e-8)
text_norm = text_emb / (np.linalg.norm(text_emb) + 1e-8)
# 차원이 다른 경우 프로젝션 (실제로는 학습된 프로젝션 사용)
if graph_norm.shape != text_norm.shape:
# 간단한 제로 패딩 예시 (실전에서는 학습된 변환 사용)
max_dim = max(len(graph_norm), len(text_norm))
graph_padded = np.pad(graph_norm, (0, max_dim - len(graph_norm)))
text_padded = np.pad(text_norm, (0, max_dim - len(text_norm)))
return alpha * graph_padded + (1 - alpha) * text_padded
return alpha * graph_norm + (1 - alpha) * text_norm// GDS로 생성한 임베딩을 노드 속성으로 저장
CALL gds.fastRP.write('techGraph', {
embeddingDimension: 128,
writeProperty: 'graphEmbedding'
})
// 저장된 임베딩으로 유사 노드 검색
MATCH (target:Technology {name: "Neo4j"})
MATCH (other:Technology)
WHERE other.name <> target.name
WITH target, other,
gds.similarity.cosine(target.graphEmbedding, other.graphEmbedding) AS similarity
RETURN other.name, similarity
ORDER BY similarity DESC
LIMIT 5이번 장에서는 지식 그래프 임베딩의 이론과 실전을 다루었습니다.
다음 장 미리보기: 8장에서는 지식 그래프 쿼리와 추론을 다룹니다. Cypher 고급 패턴, 그래프 알고리즘의 실전 활용, 그리고 자연어를 Cypher로 변환하는 Text2Cypher 기법을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Cypher 고급 쿼리 패턴, PageRank/커뮤니티 감지/중심성 등 그래프 알고리즘의 실전 활용, LLM과 그래프 추론의 결합, Text2Cypher 자연어 변환까지 다룹니다.
Microsoft GraphRAG의 아키텍처, 커뮤니티 요약, 글로벌/로컬 검색 전략, Neo4j GraphRAG Python 라이브러리, 그리고 벡터+그래프+키워드 하이브리드 검색을 다룹니다.
지식 그래프의 증분 업데이트, 데이터 품질 검증, 스케일링 전략, 모니터링, 비용 최적화, 그리고 Graphiti를 활용한 실시간 KG 업데이트까지 프로덕션 운영의 핵심을 다룹니다.