아래 튜토리얼은 graph-tool 라이브러리에서 제공하는 similarity() 함수를 이용해, 두 그래프의 인접관계(또는 가중치 포함) 유사도를 측정하는 방법을 자세히 설명합니다. 특히, Jaccard 유사도를 기반으로 하여 두 그래프가 얼마나 “비슷한 간선 구조”를 갖는지 정량화할 수 있습니다. 초보자를 위해 파이썬 코드 예시, 자세한 한글 주석, 추가 팁 등을 친절하고 정성스럽게 안내합니다.
similarity() 함수 개요similarity(
g1,
g2,
eweight1=None,
eweight2=None,
label1=None,
label2=None,
norm=True,
p=1.0,
distance=False,
asymmetric=False
)
인자 설명:
g1, g2: 비교하고자 하는 두 개의 Graph 객체label1, label2를 통해 매칭시킬 수 있음eweight1, eweight2: (옵션) 각 그래프의 간선 가중치 EdgePropertyMaplabel1, label2: (옵션) 정점 라벨(=인덱스)을 매핑하기 위한 VertexPropertyMapg1, g2의 내부 정점 인덱스를 그대로 사용norm=True: 결과값을 전체 간선(혹은 가중치 합)으로 정규화할지 여부p=1.0: (옵션-distance/유사도에서 p의 값distance=False: True면 유사도가 아니라, “거리”()를 반환(원문에서는 “complement”)asymmetric=False: True면 비대칭(asymmetric) 유사도 계산g1의 “간선 중 g2에도 있는” 부분만 카운팅 등반환: float 스칼라 값
아래는 두 개의 무방향 그래프를 생성하고, similarity()로 유사도를 계산해보는 예제입니다.
import graph_tool.all as gt
import numpy as np
def example_similarity_unweighted():
# 1) 그래프 g1, g2 생성: 무작위 100노드, 평균 차수 3
g1 = gt.random_graph(100, lambda: (3,3), directed=False)
g2 = gt.random_graph(100, lambda: (3,3), directed=False)
# 2) 기본 similarity(=Jaccard), norm=True, distance=False
sim_val = gt.similarity(g1, g2, norm=True, distance=False)
print("Similarity (Jaccard, unweighted) g1 vs g2:", sim_val)
# 3) distance=True 시
dist_val = gt.similarity(g1, g2, norm=True, distance=True)
print("Distance (1 - similarity):", dist_val)
if __name__=="__main__":
example_similarity_unweighted()
random_graph(100, (3,3), directed=False)similarity(g1, g2, norm=True, distance=False)distance=True 하면 1 - sim 형태Similarity (Jaccard, unweighted): 0.03
Distance (1 - similarity): 0.97아래 예시는 간단한 5개 정점의 방향성 그래프 두 개를 만들고, 임의 가중치로 similarity를 계산해 봅니다.
def example_similarity_weighted():
# 1) 첫 번째 그래프 g1
g1 = gt.Graph(directed=True)
v1 = [g1.add_vertex() for _ in range(5)]
e1a = g1.add_edge(v1[0], v1[1])
e1b = g1.add_edge(v1[1], v1[2])
e1c = g1.add_edge(v1[3], v1[4])
w1 = g1.new_edge_property("double")
w1[e1a] = 1.0
w1[e1b] = 2.5
w1[e1c] = 0.5
# 2) 두 번째 그래프 g2
g2 = gt.Graph(directed=True)
v2 = [g2.add_vertex() for _ in range(5)]
e2a = g2.add_edge(v2[0], v2[1])
e2b = g2.add_edge(v2[2], v2[4])
e2c = g2.add_edge(v2[3], v2[4])
w2 = g2.new_edge_property("double")
w2[e2a] = 1.5
w2[e2b] = 2.5
w2[e2c] = 0.5
# 3) similarity with weights
sim_val = gt.similarity(g1, g2, eweight1=w1, eweight2=w2,
norm=True, distance=False)
print("Weighted similarity(g1 vs g2, norm=True):", sim_val)
# distance = True -> 1 - sim
dist_val = gt.similarity(g1, g2, eweight1=w1, eweight2=w2,
norm=True, distance=True)
print("Weighted distance:", dist_val)
두 그래프의 노드 수, 인덱스 체계가 다를 때, 동일한 라벨을 갖는 노드를 서로 매칭하고 싶다면 label1, label2를 활용할 수 있습니다.
def example_similarity_with_labels():
# 예: g1, g2가 서로 다른 인덱스지만 "이름"이 같은 노드들을 매칭해보자
g1 = gt.Graph(directed=False)
g2 = gt.Graph(directed=False)
# g1: "A,B,C" 노드
v1A = g1.add_vertex(); g1.vertex_properties["name"] = g1.new_vertex_property("string")
g1.vp.name[v1A] = "NodeA"
v1B = g1.add_vertex(); g1.vp.name[v1B] = "NodeB"
v1C = g1.add_vertex(); g1.vp.name[v1C] = "NodeC"
g1.add_edge(v1A, v1B)
g1.add_edge(v1B, v1C)
# g2: "NodeA, NodeB, NodeC" - 순서 달라도 라벨만 일치하면 됨
v2X = g2.add_vertex(); g2.vertex_properties["lab"] = g2.new_vertex_property("string")
g2.vp.lab[v2X] = "NodeB" # index 0 => name=NodeB
v2Y = g2.add_vertex(); g2.vp.lab[v2Y] = "NodeA" # index 1 => name=NodeA
v2Z = g2.add_vertex(); g2.vp.lab[v2Z] = "NodeC" # index 2 => name=NodeC
g2.add_edge(v2X, v2Y) # B->A
g2.add_edge(v2X, v2Z) # B->C
# similarity(g1, g2) with label1=g1.vp.name, label2=g2.vp.lab
sim_val = gt.similarity(
g1, g2,
label1=g1.vp.name,
label2=g2.vp.lab,
norm=True
)
print("Similarity with vertex label matching:", sim_val)
위 예시에서 “NodeA”, “NodeB”, “NodeC”가 서로 매핑되며, 인덱스가 달라도 라벨이 같다면 동일 노드로 간주.
norm=True/False:p: -distance/유사도에서 p (기본 1)distance=False/True:asymmetric=False/True:label1, label2: 노드 라벨 매칭(없으면 인덱스로 대응)eweight1, eweight2: 간선 가중치 (없으면 무가중)아래는 가중치 포함, 라벨도 매핑하며, distance(거리) 모드로 비교하는 완성 예제:
import graph_tool.all as gt
def example_similarity_full():
# 그래프 g1 (5노드, 방향성, 라벨)
g1 = gt.Graph(directed=True)
g1.vp.label = g1.new_vertex_property("string")
v1 = [g1.add_vertex() for _ in range(5)]
# 라벨 설정
labels1 = ["A", "B", "C", "D", "E"]
for i, v in enumerate(v1):
g1.vp.label[v] = labels1[i]
# 간선
e1 = g1.add_edge(v1[0], v1[1]) # A->B
e2 = g1.add_edge(v1[1], v1[2]) # B->C
e3 = g1.add_edge(v1[2], v1[3]) # C->D
# 가중치
w1 = g1.new_edge_property("double")
w1[e1], w1[e2], w1[e3] = 1.2, 2.0, 0.5
# 그래프 g2 (5노드, 방향성, 라벨)
g2 = gt.Graph(directed=True)
g2.vp.lab = g2.new_vertex_property("string")
v2 = [g2.add_vertex() for _ in range(5)]
labels2 = ["E", "C", "A", "D", "B"] # 일부 순서 뒤섞음
for i, v in enumerate(v2):
g2.vp.lab[v] = labels2[i]
# 간선
e4 = g2.add_edge(v2[0], v2[3]) # E->D
e5 = g2.add_edge(v2[2], v2[1]) # A->C
e6 = g2.add_edge(v2[4], v2[1]) # B->C
# 가중치
w2 = g2.new_edge_property("double")
w2[e4], w2[e5], w2[e6] = 0.5, 2.0, 2.0
# similarity: distance=True => (1 - similarity)
dist_val = gt.similarity(
g1, g2,
eweight1=w1, eweight2=w2,
label1=g1.vp.label, label2=g2.vp.lab,
norm=True,
distance=True # => 거리(1 - sim)
)
print("Distance (1-sim) between g1,g2 with label & weight:", dist_val)
if __name__=="__main__":
example_similarity_full()
similarity() 함수를 통해, 두 그래프가 간선 구조(또는 가중치 포함) 관점에서 얼마나 유사한지 편리하게 계산할 수 있습니다.
따라서, 네트워크의 유사성 비교나, 리와이어링 전후 차이 또는 여러 버전의 그래프가 얼마나 변했는지 측정할 때 유용합니다.
아래 튜토리얼은 graph-tool 라이브러리의 random_graph() 함수를 이용해 무작위 그래프를 생성하는 과정을 보여줍니다. 특히, 오류 없이 제대로 동작하기 위해서 정점의 차수(degree)를 생성하는 방법(파라미터)과 주의사항을 자세히 설명합니다. 초보자를 위해 파이썬 코드 예시, 상세한 한글 주석 등을 포함했습니다.
random_graph() 함수 개요graph_tool.generation.random_graph(n, deg_sampler, ..., directed=False)는
n deg_sampler lambda: np.random.poisson(lambda_value) (in_degree, out_degree) 형태를 반환하는 함수를 인자로 받아, 무작위로 그래프를 생성합니다.
그러나 유향 그래프 생성 시에는 deg_sampler가 lambda: (in_deg, out_deg) 형태의 튜플을 반환해야 하고,
무향 그래프 생성 시에는 deg_sampler가 lambda: deg 형태의 단일 정수를 반환해야 합니다.
중요: 무향(
directed=False) 그래프에서는deg_sampler()가 단일int(or float로 변환가능)를 반환해야 하며,(3,3)와 같이 튜플을 반환하면 에러가 발생합니다.
lambda: (3,3)에서 오류가 나는가?lambda: (3,3)는 매번 (3,3)라는 튜플을 반환directed=False인 그래프에서 random_graph()는 단일 차수 값을 예상(3,3) 형태로 두 개의 값(=유향 그래프처럼 in/out degrees)을 반환 → 무향 그래프 코드에서는 예상치 못한 형식 → 에러 발생import graph_tool.all as gt
import numpy as np
def example_random_graph_undirected():
"""
무향 그래프를 random_graph()로 생성:
deg_sampler는 단일 정수를 반환해야 한다.
아래에서는 np.random.poisson(λ)를 사용.
"""
n = 50 # 정점 수
# deg_sampler: np.random.poisson(2)를 반환 (평균 차수가 2인 Poisson분포)
# --> 무향이므로 단일 int여야 함!
deg_sampler = lambda: np.random.poisson(2)
# 무작위 무향 그래프 생성
g = gt.random_graph(
n,
deg_sampler,
directed=False
)
print("Undirected random graph created with", g.num_vertices(), "vertices and", g.num_edges(), "edges.")
# 임의의 정보 출력
print("Degree of first vertex:", g.vertex(0).out_degree())
if __name__=="__main__":
example_random_graph_undirected()
deg_sampler = lambda: np.random.poisson(2) deg_sampler() → 3 (확률적으로 2 주변 정수)random_graph(50, deg_sampler, directed=False) Undirected random graph created with 50 vertices and 40 edges.
Degree of first vertex: 3(값은 무작위)(in,out) 형태의 정수 튜플만약 유향 그래프(directed=True)를 생성하려면, deg_sampler()는 두 개의 정수(또는 float) (in_deg, out_deg)를 반환해야 합니다.
def example_random_graph_directed():
"""
유향 그래프 random_graph() 예시:
deg_sampler -> (in_degree, out_degree) 형태 튜플.
"""
n = 30
# in_degree, out_degree 각각 Poisson(2)로
deg_sampler = lambda: (np.random.poisson(2), np.random.poisson(2))
# 유향 그래프
g = gt.random_graph(
n,
deg_sampler,
directed=True
)
print("Directed random graph created with", g.num_vertices(), "vertices and", g.num_edges(), "edges.")
# 임의 정보
v0 = g.vertex(0)
print("Vertex 0 in-degree:", v0.in_degree(), "out-degree:", v0.out_degree())
if __name__=="__main__":
example_random_graph_directed()
deg_sampler = lambda: (np.random.poisson(2), np.random.poisson(2))directed=True → 내부적으로 유향간선 구성(3,3)로 하면 왜 안 되나?lambda: (3,3)는 매번 (3,3)라는 고정 튜플을 반환directed=False인 무향 그래프 모드에서 (3,3)은 "단일 정수"가 아니므로 에러lambda: 3 처럼 단일 값lambda: (3,3) 가능(단, directed=True로 해야 함)random_graph()로 생성할 때: g = gt.random_graph(n, lambda: np.random.poisson(λ), directed=False)여기서 deg_sampler()는 단일 정수(혹은 float) 반환해야 함(in_deg, out_deg) 형태의 튜플 반환 가능:g = gt.random_graph(n, lambda: (in_deg, out_deg), directed=True)(3,3)처럼 튜플을 고정으로 반환할 경우, 반드시 directed=True인지 확인해야 하며, 무향 그래프에서는 오류 발생def bad_example():
# 의도: 무향 그래프(50개 노드) & 차수 ~3
# 근데 deg_sampler가 (3,3) 튜플을 반환
g = gt.random_graph(
50,
lambda: (3,3), # WRONG for undirected => error
directed=False
)
# => 에러 발생!
def good_example():
# 1) 무향
g_undirected = gt.random_graph(
50,
lambda: 3, # 단일 int => 문제 없음
directed=False
)
print("Undirected Graph:", g_undirected.num_vertices(), "vertices,", g_undirected.num_edges(), "edges")
# 2) 유향, (in,out)
g_directed = gt.random_graph(
50,
lambda: (3,3), # (in=3, out=3)
directed=True
)
print("Directed Graph:", g_directed.num_vertices(), "vertices,", g_directed.num_edges(), "edges")
아래는 무작위 그래프를 만드는 올바른 예시 튜토리얼입니다.
(in_degree, out_degree) 튜플import graph_tool.all as gt
import numpy as np
def tutorial_random_graph():
"""
graph_tool의 random_graph() 함수를 이용해 무작위 그래프를 생성하는 예시.
1) 무향 그래프 (단일 정수 반환)
2) 유향 그래프 ((in,out) 튜플 반환)
"""
# 1) 무향 그래프 -> deg_sampler(): 단일 정수
def undirected_deg_sampler():
# Poisson(λ=2)로 무작위 차수
return np.random.poisson(2)
g_undirected = gt.random_graph(
50,
undirected_deg_sampler,
directed=False # 무향
)
print("[Undirected] #V:", g_undirected.num_vertices(), "#E:", g_undirected.num_edges())
# 2) 유향 그래프 -> deg_sampler(): (in,out) 형태
def directed_deg_sampler():
# in_deg, out_deg를 각각 2 평균의 포아송
return (np.random.poisson(2), np.random.poisson(2))
g_directed = gt.random_graph(
50,
directed_deg_sampler,
directed=True # 유향
)
print("[Directed] #V:", g_directed.num_vertices(), "#E:", g_directed.num_edges())
if __name__=="__main__":
tutorial_random_graph()
- 만약
(3,3)처럼 고정된 튜플을 반환하려면 반드시directed=True를 사용해야 함.lambda: (3,3)+directed=False→ 오류 발생 (무향에서는 단일 차수를 요구).
이렇게 하면 오류 없이 무작위 그래프를 생성할 수 있습니다.
아래 튜토리얼은 graph-tool 라이브러리의 vertex_similarity() 함수를 이용하여 정점 간 유사도(similarity) 값을 계산하고, 이를 활용하는 방법을 다룹니다. 초보자를 위해 파이썬 코드 예시, 친절한 한글 주석, 추가 활용 팁 등을 포함해 자세히 설명합니다.
similarities = graph_tool.topology.vertex_similarity(
g,
sim_type="jaccard", # "dice", "salton", "hub-promoted", ...
vertex_pairs=None, # 특정 정점 쌍에 대해서만 유사도 계산 (iterable)
eweight=None, # (옵션) 간선 가중치
sim_map=None # (옵션) 결과 저장할 vertex property map
)
입력
g: 사용될 그래프 (Graph 객체).sim_type: 유사도 종류 (기본값: "jaccard"). 아래 표 참고.vertex_pairs: 특정 정점 쌍만 대상. 생략 시 그래프의 모든 정점 쌍에 대해 계산.eweight: (선택) EdgePropertyMap 형태의 간선 가중치.sim_map: (선택) 만약 vertex_pairs=None일 때(즉 모든 쌍) 계산 결과를 저장할 vertex_property_map(vector type). 출력
similarities:vertex_pairs가 주어졌다면, 해당 쌍들에 대한 유사도를 담은 numpy.ndarray.vertex_pairs=None(모든 정점 쌍)일 경우, 유사도 행렬(혹은 2D정보)을 가진 vector-valued vertex_property_map.| sim_type | 설명 | 공식 (G=무가중/단순그래프 기준) |
|---|---|---|
| "jaccard" | Jaccard 유사도 | $\displaystyle \frac{ |
| "dice" | Sørensen–Dice 계수 | $\displaystyle \frac{2\, |
| "salton" | Salton (cosine) 유사도 | $\displaystyle \frac{ |
| "hub-promoted" | Hub-promoted similarity | $\displaystyle \frac{ |
| "hub-suppressed" | Hub-suppressed similarity | $\displaystyle \frac{ |
| "inv-log-weight" | Inverse log weighted similarity (Adamic-Adar 등) | $\displaystyle \sum_{x\in N(u)\cap N(v)} \frac{1}{\ln |
| "resource-allocation" | Resource allocation similarity | $\displaystyle \sum_{x\in N(u)\cap N(v)} \frac{1}{ |
| "leicht-holme-newman" | Leicht-Holme-Newman similarity | $\displaystyle \frac{ |
leicht-holme-newman의 공식은 원문 참조.무가중 그래프 기준 공식만 제시했으며, 가중/멀티그래프 시
A[u,v](가중치/중복) 합산에 맞추어 수정 적용.
import graph_tool.all as gt
import numpy as np
import matplotlib.cm as cm
def example_vertex_similarity_jaccard():
# 1) 무작위 그래프 생성(무방향)
g = gt.random_graph(
30,
lambda: np.random.poisson(3),
directed=False
)
# 2) "jaccard" 유사도로 모든 정점 쌍(sim_map에 저장)
sim_map = gt.vertex_similarity(
g,
sim_type="jaccard",
vertex_pairs=None, # => 모든 정점 쌍
eweight=None, # 가중치 없다고 가정
sim_map=None # 새 property map 자동 생성
)
# sim_map: vector-valued VertexPropertyMap
# sim_map[v][u] => v와 u 사이 jaccard 유사도 (float)
# 3) 예: vertex 0과 나머지들의 유사도
similarities_to_0 = sim_map[g.vertex(0)].a # numpy array
print("Jaccard similarity to vertex 0:", similarities_to_0)
# 4) 시각화 or 분석
# 예) 정점 컬러를 "vertex 0과의 jaccard 유사도"로 설정
colorprop = g.new_vp("double")
colorprop.a = similarities_to_0
# graph_draw 등으로 시각화 가능 (생략)
if __name__=="__main__":
example_vertex_similarity_jaccard()
Jaccard similarity to vertex 0: [0. 0.33333333 0.2 0.25 ... ]
(길이는 30, 값은 무작위 그래프 구조에 따라 달라짐)
vertex_pairs에 (u, v) 쌍 목록을 주면, 해당 쌍들만 계산하여 numpy 배열을 반환한다.
def example_vertex_similarity_pairs():
g = gt.random_graph(20, lambda: np.random.poisson(2), directed=False)
pairs = [
(0,1), (0,5), (2,6), (3,7)
]
# Salton similarity (cosine)
result = gt.vertex_similarity(
g,
sim_type="salton",
vertex_pairs=pairs
)
# => numpy.ndarray, shape=(len(pairs), )
for p, val in zip(pairs, result):
print(f"Salton sim between {p} = {val:.4f}")
실행 결과 예시:
Salton sim between (0,1) = 0.5164
Salton sim between (0,5) = 0.0000
Salton sim between (2,6) = 0.2582
Salton sim between (3,7) = 0.7746
가중치(EdgePropertyMap)가 있으면, 유사도 계산 시 A[u,v] (가중치) 기반으로 수정된 공식을 사용.
예시:
def example_vertex_similarity_weighted():
# 유향 그래프
g = gt.Graph(directed=True)
vlist = [g.add_vertex() for _ in range(5)]
# 간선
e1 = g.add_edge(vlist[0], vlist[1])
e2 = g.add_edge(vlist[0], vlist[2])
e3 = g.add_edge(vlist[1], vlist[3])
e4 = g.add_edge(vlist[2], vlist[3])
e5 = g.add_edge(vlist[3], vlist[4])
# 가중치 property
wprop = g.new_edge_property("double")
wprop[e1] = 1.5
wprop[e2] = 2.0
wprop[e3] = 0.5
wprop[e4] = 0.8
wprop[e5] = 2.5
# "hub-promoted" similarity를 계산, 가중치 사용
sim_map = gt.vertex_similarity(
g,
sim_type="hub-promoted",
eweight=wprop,
vertex_pairs=None
)
# vertex 0과의 similarity
sim0 = sim_map[vlist[0]].a
print("Hub-promoted similarity to v0:", sim0)
유향 그래프인 경우, "이웃"은 out-neighbors를 가정. in-neighbor 유사도를 원하면
GraphView(g, reversed=True)로 바꿔서 사용.
distance=True → 유사도 대신 "그래프 간 거리" (1 - similarity 등)를 반환asymmetric=True → 비대칭 유사도( 예: set difference ) 적용distance=False, asymmetric=False가 기본.vertex_pairs=None(전체 정점쌍) → 시간vertex_pairs 길이가 → vertex_similarity()는 그래프 내 각 정점쌍 사이의 이웃관계 (또는 가중치) 기반으로 여러 가지 well-known local similarity 지표( Jaccard, Dice, Salton 등 )를 간단히 계산할 수 있는 강력한 함수입니다.
결과물인 similarity(혹은 distance)를 통해 링크 예측이나 그래프 분석 등에 활용 가능합니다.
아래 튜토리얼은 그래프 서브그래프 동형성(Subgraph Isomorphism) 문제를 다루고, 특히 Boost.Graph의 vf2_subgraph_iso(C++ API)에 대응하거나, graph-tool 등 파이썬 라이브러리를 통해 서브그래프 동형성을 판별하는 과정을 자세히 설명합니다.
이 튜토리얼은 서브그래프 동형성이 무엇인지, 왜 NP-완전인지, 어떤 알고리즘(예: VF2)들이 있는지, 그리고 파이썬 예제 코드(graph-tool 또는 유사)를 통해 실용적으로 문제에 접근하는 방안을 구체적으로 설명합니다.
(도시·교통 빅데이터, 네트워크 분석 전문가 시점에서, 초보자가 이해하기 쉽도록 한글 주석도 풍부히 담았습니다.)
서브그래프 동형성이란, 두 그래프 와 가 주어졌을 때, 내부에 와 ‘정확히 같은 구조’를 가진 부분 그래프가 존재하는지 판별하는 문제입니다.
서브그래프 동형성은 대표적으로 백트래킹(Backtracking) 기반 접근:
1. DFS/백트래킹으로 의 각 정점을 내부 정점에 매핑 시도
2. 매핑 중간에 정점/간선 호환성(Equivalence predicate) 어긋나면 백트래킹
3. 모든 정점 매핑 완료 → 성공 시 subgraph isomorphism 성립
vf2_subgraph_iso 함수가 이를 구현.graph-tool에서 현재(2024년) 직접적인 subgraph_isomorphism() 함수는 공식 문서에 없을 수 있습니다. 그러나 유사하게, 탐색 알고리즘이나 topology 하위 모듈에서 subgraph_isomorphism(Beta/테스트 버전) 시도할 수 있습니다. (또는 Boost.Python 바인딩으로 직접 VF2를 호출하는 방식.)
아래 예시는 “개념적” 예제 코드로, 실제 graph_tool.topology 내 (가칭) subgraph_isomorphism 함수를 가정:
import graph_tool.all as gt
def subgraph_iso_example():
# 1) 작은 그래프 g_small
g_small = gt.Graph(directed=False)
vs_small = [g_small.add_vertex() for _ in range(4)]
# 간선 (사각형)
g_small.add_edge(vs_small[0], vs_small[1])
g_small.add_edge(vs_small[1], vs_small[2])
g_small.add_edge(vs_small[2], vs_small[3])
g_small.add_edge(vs_small[3], vs_small[0])
# 2) 큰 그래프 g_large
g_large = gt.Graph(directed=False)
vs_large = [g_large.add_vertex() for _ in range(6)]
# 간선: (0--1--2--3), (4--5) + (2--4), (1--5) ...
g_large.add_edge(vs_large[0], vs_large[1])
g_large.add_edge(vs_large[1], vs_large[2])
g_large.add_edge(vs_large[2], vs_large[3])
g_large.add_edge(vs_large[2], vs_large[4])
g_large.add_edge(vs_large[4], vs_large[5])
g_large.add_edge(vs_large[1], vs_large[5]) # 임의로 추가
# 3) (가정) subgraph_isomorphism 호출
# return 형식: iterator or list of dict-like mapping
# !! 실제 graph-tool에는 정식 API가 아닐 수 있으니 가상의 예시임.
iso_mappings = subgraph_isomorphism(g_small, g_large)
if not iso_mappings:
print("No subgraph isomorphic to g_small found in g_large.")
return
print("Found subgraph isomorphisms:")
for mapping in iso_mappings:
# mapping: g_small 정점 -> g_large 정점
# 예) {0:2, 1:1, 2:5, 3:4}
print(mapping)
if __name__=="__main__":
subgraph_iso_example()
주의: 이 코드는 실제로
graph_tool최신 버전에서 동작하지 않을 수 있습니다(공식subgraph_isomorphism가 없거나, 별도 브랜치에 있을 가능성).
그러나 개념적으로 VF2 또는 유사 알고리즘을 Python 레벨에서 호출해, “g_small가g_large내부 어느 부분과 구조 일치?”를 찾을 수 있음을 보여주는 예시입니다.
vf2_subgraph_isoBoost C++ 라이브러리에서는:
bool vf2_subgraph_iso(
const GraphSmall& graph_small,
const GraphLarge& graph_large,
SubGraphIsoMapCallback user_callback
/* 기타 파라미터... */
);
user_callback: 서브그래프 동형성을 만족하는 매핑이 발견될 때마다 콜백으로 호출예)
// pseudo-code for callback:
struct my_callback {
template <typename MapSmallToLarge, typename MapLargeToSmall>
bool operator()(MapSmallToLarge f, MapLargeToSmall g) {
// f(v_s) = v_l
// ... do something with mapping ...
// return false => stop searching
return true;
}
};
graph_small)을 pickgraph_large의 어떤 정점이 이와 호환되는지(간선 eq/ 정점 eq) 체크시간복잡도: 최악에선 , 하지만 실제로는 많은 pruning 덕에 빨라질 수 있음.
subgraph_isomorphism API가 완비되지 않았을 수 있음, 그러나 원리상 C++ Boost Graph vf2_subgraph_iso에 대응 가능.정리:
이로써, 서브그래프 동형성 문제와 관련된 개념, NP-완전성, 대표 알고리즘(VF2) 구조, 간단한 Python 예시 등을 모두 살펴보았습니다.
아래 튜토리얼은 “(Sub)Graph Isomorphism Algorithm for Matching Large Graphs” (Luigi P. Cordella, Pasquale Foggia, Carlo Sansone, and Mario Vento) 논문의 주요 내용을 중심으로, 서브그래프 동형성(Subgraph Isomorphism) 문제와 해당 알고리즘(VF, VF2)을 도시·교통 빅데이터(분산처리), 네트워크 분석에 적용할 수 있는 관점에서, 개념, 수식, 원리, 구현 순으로 최대한 자세히 설명합니다.
본 튜토리얼은 초보자가 이해하기 쉽게 한글 주석과 파이썬 예시 등을 포함합니다.
정의
서브그래프 동형성 문제는 두 그래프 $ G_1 = (V_1, E_1와 $ G_2 = (V_2, E_2가 주어졌을 때, $ G_1 $ 내부에 $ G_2 $와 ‘정확히 동일한 구조’를 가진 부분 그래프가 존재하는지를 판별하는 문제입니다.
왜 중요한가?
Cordella 등은 2001년, 2004년에 걸쳐 VF(VF2) 라 불리는 (Sub)Graph Isomorphism 알고리즘을 제안했습니다.
이 논문의 기여
1. VF 알고리즘을 개선(VF2) → 메모리 사용량이 줄고, 대형 그래프에서 더욱 효율적.
2. 랜덤 그래프·실제 어플리케이션(기술 도면 등)에서 Ullmann(1976)보다 상당히 빠른 성능 시연.
논문에서는 5가지 룰(Rpred, Rsucc, Rin, Rout, Rnew)을 제안하여, 부분 매핑이 더 확장될 수 있는지 판단:
Rpred, Rsucc : 매핑된 정점 간 선행/후행(Predecessor/Successor) 관계가 양 그래프에서 호환되는지 검사
Rin, Rout : 1-단계 look-ahead.
Rnew : 2-단계 look-ahead(좀더 넓은 정점 후보군).
만약 이들 규칙 중 하나라도 만족 안 하면, 가지치기(백트래킹)하여 탐색 중단.
![Pseudocode]
VF2_Match(G1, G2, state):
if 모든 정점이 매핑되었다면:
return SUCCESS (혹은 매핑을 callback)
P = 후보 정점 쌍 집합 (SSR기반)
for (n, m) in P:
if F(s, n, m) == True: # feasibility check
state.push(n, m)
VF2_Match(G1, G2, state)
state.pop(n, m)
return
graph-tool에는 공식 subgraph_isomorphism()가 없지만, 논문 방식(VF2)을 Python으로 예시해볼 수 있습니다.
아래 코드는 개념적이며, 실제 대규모 그래프 시 최적화·C++ 연동 등 필요.
import sys
class VF2State:
def __init__(self, G1, G2):
self.G1 = G1
self.G2 = G2
# core_1[v] = 매핑된 G2 정점 (or None)
self.core_1 = [None]*G1.num_vertices()
# core_2[u] = 매핑된 G1 정점 (or None)
self.core_2 = [None]*G2.num_vertices()
# in_1, out_1, in_2, out_2 → terminal set membership
self.in_1 = [0]*G1.num_vertices()
self.out_1= [0]*G1.num_vertices()
self.in_2 = [0]*G2.num_vertices()
self.out_2= [0]*G2.num_vertices()
self.depth = 0 # 현재 매핑된 정점 수(혹은 탐색 depth)
def match(self, solution_callback):
# 만약 모든 정점(혹은 subgraph 조건)이 만족되면 성공
if self.depth == self.G1.num_vertices():
# => G1 전체가 G2의 부분그래프로 매칭됨(=이건 Isomorphism case)
# Subgraph Isomorphism시, 원하는 개수만큼 매핑할 수도.
solution_callback(self.core_1) # or pass mapping
return
# 1) 후보 P(s) 계산
P = self.compute_candidate_pairs()
for (n, m) in P:
if self.feasibility(n, m):
# 매핑 확장
self.core_1[n] = m
self.core_2[m] = n
self.depth += 1
# in/out update, skip
# 재귀
self.match(solution_callback)
# backtrack
self.core_1[n] = None
self.core_2[m] = None
self.depth -= 1
def compute_candidate_pairs(self):
# SSR기반으로 Tin, Tout 집합 등 계산
# 간단히: 아직 매핑 안된 정점들 중
# 한쪽 그래프 정점 n, 다른쪽 그래프 정점 m
# => yield (n, m)
candidates = []
# (실제론 in_1/out_1/in_2/out_2를 사용)
# 여기선 간단하게 모든 미매핑 정점 쌍
for n in range(self.G1.num_vertices()):
if self.core_1[n] is None:
for m in range(self.G2.num_vertices()):
if self.core_2[m] is None:
candidates.append((n,m))
return candidates
def feasibility(self, n, m):
# 1) 구조적(Rpred, Rsucc, etc)
# 2) 속성(semantic)
# (실제로는 check edges, in/out등)
# 여기서는 간단히 pass
return True
def subgraph_isomorphism(G1, G2, callback=print):
"""
G1, G2: 그래프(임의)
callback: 매핑 발견 시 호출
"""
state = VF2State(G1, G2)
state.match(callback)
시간 복잡도
메모리
실험 결과(논문):
논문에 따르면, 기술 도면(라인 드로잉)을 그래프로 변환하여 부품 서브그래프를 찾는 문제에서 VF2가 강력한 성능을 보임:
(끝)
아래 설명은 Tomita, Tanaka, Takahashi (2006) 논문("The worst-case time complexity for generating all maximal cliques and computational experiments" / Theoretical Computer Science 363, pp. 28–42)을 기반으로, 무방향 그래프에서 모든 최대 클리크(Maximal Clique) 목록을 생성하는 알고리즘과 해당 알고리즘의 최악 사례 시간 복잡도를 자세히 살펴봅니다. 특히 수학적 개념과 원리에 중점을 두어 최대한 정성스럽게 정리합니다.
전역 변수 $ Q $: 현재까지 확장된 클리크(아직 최대인지 불분명).
SUBG $ = \bigcap_{p_i \in Q} \Gamma(p_i$또는 $ V $에서 $ Q $ 모든 정점과 공통으로 인접한 부분).
재귀함수 $ \text{EXPAND}(SUBG, CAND$:
아래 설명은 Cazals, Karande (2008)의 논문("A note on the problem of reporting maximal cliques") 내용을 토대로, 모든 최대 클리크를 찾는 문제(Maximal Clique Enumeration)와 관련한 알고리즘들의 핵심 개념과 원리를 정리한 것입니다. 특히, Bron–Kerbosch (1973) 알고리즘과 그 변형들, Koch (2001), 그리고 Tomita et al. (2006) 알고리즘 간의 연관성과 차이점을 수식과 함께 상세히 다룹니다.
본 논문(Cazals & Karande, 2008)은 위 세 접근을 서로 비교·정리하여, 결국 (1) Koch가 언급하였으나 활용하지 않았던 “$P \cup X\에서 피벗을 고르는” 아이디어가, (2) Tomita 등 알고리즘과 사실상 동일함을 보임.
다음과 같은 세 집합을 사용:
Bron–Kerbosch(Version 1) 의 핵심 아이디어(그림 1 참조):
1. $ R 을 클리크 후보로 유지하며, $ P $에 있는 정점을 하나씩 추가(R \cup {v})해 재귀적으로 탐색. 2. 만약 $ P = \emptyset $이고 $ X = \emptyset $이면, $ R $은 최대 클리크로 출력(왜냐하면 더 이상 확장할 정점 없음 + 이미 처리된 정점도 없음). 3. 한편, $X에 속한 정점들은 이미 처리되었으므로, 중복 생성을 막기 위해 이들을 다시 선택하지 않음.
이 알고리즘은 최악의 경우 시간 복잡도가 매우 크며, 특히 “비최대 클리크”를 모두 한 번씩 탐색(완성된 뒤에야 비최대임을 알게 됨)한다는 점에서 지수적 중복이 발생할 수 있다.
Version 2 (Bron–Kerbosch 원 논문 내)에서는 “피벗(pivot)” 개념을 도입해 중복/불필요 탐색을 줄인다. 아이디어:
이유: 내 정점들을 단독으로 에 추가해서는, 로 더 확장 가능한 여지가 있기 때문에 그것만으로 최대 클리크가 되지 않는다. 따라서 maximal clique를 만들려면 반드시 $P^+\ 중 적어도 하나를 골라야 한다. (이걸 “건너뛰기(pruning)”로 활용)
Koch(2001)은 피벗 선택 전략을 다양히 실험:
Koch의 문제점:
여기서 Koch 자신이 “피벗을 에서 고르면 된다”고 언급하였으나, 실제로 활용하지는 않았다고 본 논문은 지적. (이 방법을 쓰면 위 문제 사례가 으로 해결)
Tomita 등은 Koch가 “가능”이라 언급한 “피벗을 에서 고른다”를 실제로 구현(알고리즘 Cliques).
EXPAND(SUBG, CAND)에서, $ \text{pivot } u \in (P \cup X을 고른 뒤, $P - \Gamma(u) 정점들만 재귀적으로 확장.결론: Tomita 알고리즘은 Bron–Kerbosch Version 2의 자연스러운 확장판으로서, Koch가 제안했으나 구현하지 않은 “피벗을 에서 선택” 방식을 사용한다.
수식이나 코드로 표현하면, Tomita 알고리즘(Cliques)의 핵심은 다음과 같이 요약할 수 있다:
이 알고리즘이 최악 사례 임을 Tomita 등(2006)이 증명하였고, 이는 Moon–Moser 그래프에서 최대 클리크가 개 나타날 수 있다는 사실과 일치한다.
끝으로, Cazals & Karande(2008)은 다음과 같이 결론짓는다:
“Tomita 등 (2006)은 Koch(2001)가 언급한 '피벗을 $P \cup X\에서 고르는' 아이디어를 완전히 구현하여, 실제로도 여러 그래프에서 뛰어난 성능을 보여준다. 이로써 Bron–Kerbosch 알고리즘의 변종들이 서로 밀접하게 연결되어 있음을 알 수 있다.”
以上.