Graph-tool 10

Sylen·2025년 1월 4일

아래 튜토리얼은 (\texttt{graph_tool.generation.generate_knn}) 함수가 어떤 역할을 하고, 내부적으로 어떤 수학적 개념·원리를 사용해 네트워크를 생성하는지를 친절하고 자세하게 설명합니다. 또한, 실제 파이썬 예제 코드와 함께, 중요한 매개변수와 인자에 대한 상세한 주석을 포함합니다.


1. 개요: KNN(k-Nearest Neighbors) 그래프란?

(\textbf{k-Nearest Neighbors(최근접 k개 이웃)}) 그래프는, 주어진 (N)개의 데이터(또는 점들) 각각에 대해, “가장 가까운 k개의 점들과 연결”하는 방식으로 만들어진 그래프입니다. 이를 통해,

  • 각 점(노드)은 유클리디안 거리(또는 사용자가 정의한 거리 함수) 관점에서 가까운 이웃끼리 연결됨.
  • “공간상 유사한” 데이터들이 그래프 상에서도 연결되어 있게 되므로, 군집(Clustering)이나 근접 탐색을 위한 그래프 구조로 자주 활용됩니다.

(\texttt{generate_knn}) 함수는 이러한 k-최근접이웃 그래프를 자동으로 만들어줍니다. 더 구체적으로,
1. 입력: 점들의 좌표(또는 점들 사이의 거리함수), (k)값 등.
2. 출력:

  • (\textbf{Graph}) (k-최근접이웃 그래프)
  • (\textbf{EdgePropertyMap}) (간선별로 계산된 거리 값)

2. 함수 파라미터 개념 & 동작원리

(\texttt{graph_tool.generation.generate_knn(points, k, dist=None, ...)})

  1. points

    • (\texttt{iterable}) 혹은 (\texttt{numpy.ndarray}) : 크기가 ((N, d))인 행렬(즉, (N)개의 점, 각 점은 (d)차원 좌표).
    • 혹은 단순히 정수 (N) 하나만 주어질 수도 있음(이때는 (\texttt{dist}) 함수를 통해서 거리 계산).
  2. k

    • 각 점(노드)마다 “가장 가까운 k개 이웃”을 찾을 때의 (k).
    • pairs=True인 경우에는 “그래프” 대신 “(\text{k-closest pairs})”를 찾는다(자세한 내용은 아래).
  3. dist (기본값: None)

    • None이면 유클리디안 거리((L_2)-norm)로 간주.
    • 함수라면, (\texttt{dist}(i, j)) (\rightarrow) 점 i와 j 간의 거리 리턴. 이 경우 (\texttt{points})는 “점의 개수”만 주어야 하고, 실제 좌표는 dist 함수 내부에서 처리.
  4. pairs (기본값: False)

    • False: “각 노드별 k개 이웃” => k-최근접이웃 그래프
    • True: “가장 가까운 k 쌍(pair)” => 전역적으로 거리 가장 작은 k개의 점 쌍만 연결.
  5. exact (기본값: False)

    • False일 때, 빠른 근사 알고리즘(approximate) 사용.
    • True면, 정확하지만 느린 방식으로 모든 거리를 확인하여 kNN을 찾음.
    • 근사 알고리즘은 “(\mathrm{O}(N^{1+\epsilon})) 정도로 동작한다고 알려짐”([dong-efficient-2011]).
  6. r, max_rk, epsilon, c_stop, max_iter

    • approximate(KNN) 알고리즘에서 사용하는 파라미터들.
    • (r) : 임의로 선택된 일부 neighbor를 샘플링하는 비율.
    • max_rk: 반복 과정에서 고려할 임의 이웃의 최대 개수.
    • epsilon: 수렴 판정 기준(이웃 집합 변동이 일정 기준 미만이 되면 중단).
    • c_stop=True이면, global clustering coefficient(글로벌 클러스터링 계수)가 더 이상 증가하지 않을 때 중단.
    • max_iter: 최대 반복 횟수 (0이면 제한 없음).
  7. directed (기본값: False)

    • True면 방향 그래프를 생성(“노드 i가 j의 이웃이더라도, j가 i의 이웃은 아닐 수 있음”).
    • False면 undirected(보통 kNN 그래프는 무방향).
  8. 리턴:

    1. (\texttt{g}): (\texttt{Graph}) 객체 (kNN 그래프)
    2. (\texttt{w}): (\texttt{EdgePropertyMap}) (간선의 거리값).
      • e.g. w[e]로 해당 간선 e의 거리(distance) 얻을 수 있음.

3. “가까운 이웃” 찾기의 수학적/알고리즘적 원리

(1) 유클리디안 거리 (기본)

  • 기본적으로 (\texttt{euclidean distance}):
    [
    | xi - x_j |_2 = \sqrt{ \sum{p=1}^{d} (x{i,p} - x{j,p})^2 }.
    ]
  • 만약 (N)이 크고 (d)가 크면, 모든 쌍을 계산((\mathrm{O}(N^2)))하는 것은 부담이 큼 → 근사 알고리즘.

(2) 근사 KNN 알고리즘

  • [dong-efficient-2011]에서 제안된 방식으로, 랜덤 이웃 샘플을 통해 점진적으로 k-최근접 이웃 집합을 개선해나가는 기법.
  • 대략적인 개념:
    1. 각 점마다 임의의 neighbor들을 시작집합으로 잡음.
    2. 이웃 집합을 서로 비교/갱신하여, 실제 근처에 있는 점을 더 많이 포함하도록 반복.
    3. 변화 폭이 작으면(변동 비율이 epsilon 이하 등) 스톱.

(3) pairs = True 일 때

  • “노드 개별 k개 이웃”이 아니라 “전체 노드 쌍 중에서 가장 가까운 k개 쌍” 찾기([lenhof-k-closest] 관련).
  • 즉, 전역적으로 거리 상위 k개를 찾음 → 그래프 형태라기보다, (i,j) k쌍만 연결한 특별한 그래프가 됨.

4. 코드 예시: 간단한 kNN 그래프 만들기

(A) 무작위 2D 점으로 시연

import numpy as np
import graph_tool.all as gt
import matplotlib.pyplot as plt

def demo_generate_knn():
    # 1) 임의로 N=50, d=2차원 데이터를 생성
    points = np.random.random((50, 2))
    
    # 2) k-최근접 이웃 그래프 생성
    k = 5
    g, w = gt.generate_knn(
        points, 
        k=k, 
        dist=None,      # None => 유클리디안 거리
        pairs=False,    # False => 각 노드마다 k=5개 이웃
        exact=False,    # 근사 방식; True면 정확하지만 느림
        r=0.5, 
        epsilon=1e-3,
        directed=False
    )
    
    # g: kNN 그래프, w[e]: 간선 e의 거리값
    print("[INFO] # of vertices:", g.num_vertices())
    print("[INFO] # of edges:",    g.num_edges())
    
    # 3) 시각화
    # (a) 점의 좌표 => vertex_positions : (vertex->(x,y)) PropertyMap
    pos_map = g.new_vertex_property("vector<double>")
    for i, v in enumerate(g.vertices()):
        pos_map[v] = (points[i,0], points[i,1])
    
    # (b) graph_draw로 그려보기
    #   - kNN 특성상, 각 점은 최대 k개 선이 연결됨
    gt.graph_draw(g, pos=pos_map, edge_pen_width=1.0,
                  vertex_size=5, output_size=(400,400),
                  vertex_fill_color="blue", 
                  output=None)  # output="knn_demo.png" 하면 파일로 출력
    
    # 4) 간선 거리값 살펴보기
    distances = [w[e] for e in g.edges()]
    print(f"  Min distance: {min(distances):.4f}")
    print(f"  Max distance: {max(distances):.4f}")
    print(f"  Avg distance: {np.mean(distances):.4f}")

if __name__ == "__main__":
    demo_generate_knn()
  • 동작:
    1. 2D 평면 위 임의 점(50개)을 생성.
    2. 각 점마다 가장 가까운 이웃 k=5개씩 연결(근사).
    3. 그래프를 그려서 확인.
    4. (\texttt{w[e]}) 에 저장된 간선 거리도 확인.

(B) custom distance 함수 적용

def custom_dist(i, j):
    """
    사용자 정의 거리 예시:
    - 만약 points가 전역 변수에 있다면, i,j 인덱스 바탕으로 
      좀 더 복잡한 거리 계산 가능(예: 맨해튼, 코사인 유사도 등).
    - 여기서는 단순 예시로, i+j의 절댓값 차...
    """
    return abs(i - j) * 0.5

def demo_generate_knn_custom():
    N = 10  # 총 10개 점(혹은 노드)이 있다고 치자
    # 'points' 인자를 int(N)으로 줌
    # -> dist(i,j)로 거리를 계산
    k = 3
    g, w = gt.generate_knn(
        points=N,
        k=k,
        dist=custom_dist,
        pairs=False,
        exact=True    # 정확 모드로
    )
    print("[INFO] # of vertices:", g.num_vertices())
    print("[INFO] # of edges:",    g.num_edges())
    # 간선 distance 출력
    for e in g.edges():
        dval = w[e]
        print(f"Edge {int(e.source())}-{int(e.target())}, dist={dval:.4f}")
  • 위처럼 (\texttt{points})에 단순 정수 (N)을 주고, dist(i,j) 함수를 정의해서 임의 거리 체계를 구성할 수 있음.

5. 수행시간 복잡도 & 참고문헌

  1. (\textbf{정확 알고리즘}) : (\mathcal{O}(N^2)) (모든 쌍 거리 계산)
  2. (\textbf{근사 알고리즘}) : [dong-efficient-2011] 논문에서, (\mathrm{O}(N^{1+\epsilon}))로 알려져 있음(실제론 데이터 분포에 따라 다를 수 있음).
  3. “k-Closest Pairs”를 찾을 경우([lenhof-k-closest]), (\mathrm{O}(N \log N + k)) 등 여러 결과가 존재.

문서에 적힌 참고문헌

  • [dong-efficient-2011]: Wei Dong, Charikar Moses, Kai Li, Efficient k-nearest neighbor graph construction for generic similarity measures, WWW ‘11.
  • [lenhof-k-closest]: HP Lenhof, M. Smid, “The k closest pairs problem”.

6. 요약

  • (\texttt{generate_knn}):
    • 점들의 좌표(또는 사용자 거리 함수)와 (k)를 입력 => kNN 그래프(혹은 k-closest pair) 생성.
    • 내부적으로 근사/정확 모드를 선택할 수 있어, 대규모 데이터에도 유연하게 적용 가능.
  • 활용 예:
    • 그래프 기반 클러스터링(community detection 전처리)
    • 밀도 기반 탐색(임베딩 후 KNN 그래프)
    • “비슷한 노드끼리 연결된” 그래프를 빠르게 구성

이와 같이 (\texttt{graph_tool})의 (\texttt{generate_knn}) 함수는, 대규모/고차원 데이터에서도 효율적으로 k-최근접이웃 그래프를 만들 수 있도록 다양한 옵션을 제공합니다. 실제로는 여러 파라미터(epsilon, r, c_stop) 등을 조정해가며, 정확도와 성능을 균형 있게 사용하면 좋습니다.

아래 내용은 graph-tool 라이브러리의 (triangulation) 함수를 사용해 2차원(또는 3차원) 점 집합에 대해 삼각분할(트라이앙귤레이션, Triangulation) 그래프를 생성하는 방법을 안내합니다. 또한 simpledelaunay의 차이, 코드 예시, betweenness centrality 계산 방법 등을 포괄하여, 초보자도 쉽게 접근할 수 있게 정리해보았습니다.


1. 삼각분할(Triangulation)이란?

  • 삼각분할이란, 주어진 점 집합이 만들어내는 볼록껍질(convex hull) 내부(및 표면)를 삼각형(2D) 혹은 테트라헤드론(3D) 형태로 분할하는 작업을 말합니다.
    • 2D에서는 “삼각형들의 모임”으로 구성.
    • 3D에서는 “사면체(테트라헤드론)의 모임”으로 구성.

1.1 simple vs. delaunay

  1. simple triangulation(type="simple")

    • 점을 하나씩 순차 삽입하면서, 해당 점이 속한 면(face)을 세 개 면으로 나누는 방식.
    • 점이 볼록껍질 바깥이면 “flip(플립)” 과정을 통해 볼록껍질을 복원함.
    • 삽입 순서에 따라 결과가 달라질 수 있음(비유니크).
  2. Delaunay triangulation(type="delaunay")

    • 델로네(Delaunay) 분할은 “빈 원(원 혹은 구) 속성(empty sphere property)”을 만족:
      즉, 각 삼각형(또는 사면체)을 둘러싸는 외접원(혹은 외접구)에 다른 어떤 점도 들어있지 않도록 구성.
    • 유니크(단, 특정 상태에서 예외적으로 동일 공-구상에 다섯 점이 있을 경우 등)
    • 그래프의 삼각형들이 보다 균등하게 분포되는 특성이 있어서, 보간/메시(mesh) 등 다양한 분야에서 자주 쓰임.

정리: Delaunay가 좀 더 ‘공간적으로 균형’있게 분할하지만, 계산 비용이 더 들 수 있음. Simple 모드는 삽입 순서에 따라 달라지며 상대적으로 구현이 간단.


2. 함수 개요: graph_tool.generation.triangulation

g, pos = gt.triangulation(points, type='simple', periodic=False)
  • 파라미터

    • points: numpy.ndarray, shape = (N, d)
      • (N)개의 점, 각 점은 (d)차원 좌표 (d=2 또는 3만 가능)
    • type: 문자열(기본값: 'simple')
      • 'simple' 또는 'delaunay'
    • periodic: bool (기본값: False)
      • periodic boundary를 적용할지 여부(단, 'delaunay'일 때만 유효)
  • 반환값

    • triangulation_graph(Graph 객체): 삼각분할 결과 그래프
    • pos(VertexPropertyMap): 각 정점의 (x,y) 혹은 (x,y,z) 좌표

2.1 작동 방식

  • 2차원이나 3차원 점을 입력하면, 해당 점들을 모두 연결하여 삼각형(혹은 사면체) 분할 그래프를 만듦.
  • simple:
    • 순차적으로 점을 삽입 → 한 점을 삽입하면, 그 점이 포함되는 면을 찾아 세 면으로 분할 → 바깥이면 flip으로 조정.
  • delaunay:
    • 델로네 조건을 만족하도록 정밀하게 분할.
    • periodic=True일 경우, 주기적 경계를 가정(2D이든 3D이든).

3. 예시 코드: 2D 점 집합의 Simple Triangulation

import graph_tool.all as gt
import numpy as np
import math

def simple_triangulation_example():
    # (1) 임의의 2D 점 생성 (N=500)
    points = np.random.rand(500, 2) * 4.0  # x,y 각각 [0,4) 범위

    # (2) Simple triangulation 그래프 생성
    g, pos = gt.triangulation(points, type='simple', periodic=False)

    print("== Simple Triangulation ==")
    print("Num. of vertices:", g.num_vertices())  # 대체로 500
    print("Num. of edges   :", g.num_edges())

    # (3) Edge에 유클리드 거리(weight) 부여
    weight = g.new_edge_property("double")
    for e in g.edges():
        s = e.source()
        t = e.target()
        # pos[s], pos[t]가 numpy array-like
        diff = pos[s] - pos[t]
        dist = math.sqrt(diff[0]**2 + diff[1]**2)
        weight[e] = dist

    # (4) 가중치가 있는 betweenness(중개중심성) 계산
    vb, eb = gt.betweenness(g, weight=weight)

    # eb: edge betweenness → 시각화 위해 적당히 스케일링
    eb.a *= 100

    # (5) 그래프 그리기
    gt.graph_draw(g, pos=pos,
                  vertex_fill_color=vb,     # 정점색: betweenness기반
                  edge_pen_width=eb,        # 에지 두께: betweenness기반
                  output="triang_simple.pdf")

    print(" - Drawn to triang_simple.pdf")
  • 위 코드 실행 시, (1) 2D 500개 점을 난수로 뽑고, (2) Simple triangulation 수행 → (3) edge마다 유클리드 거리를 할당, (4) betweenness 계산, (5) 시각화.
  • 결과로 생성된 PDF에는 삼각분할된 그래프가 그려지고, betweenness가 큰 노드(색이 짙어지거나), betweenness 높은 에지(두께 증가)로 표현됨.

4. 예시 코드: 2D 점 집합의 Delaunay Triangulation

import graph_tool.all as gt
import numpy as np
import math

def delaunay_triangulation_example():
    # 1) 임의의 2D 점 생성
    points = np.random.rand(500, 2) * 4.0

    # 2) Delaunay triangulation
    #    periodic=True 로 하면 주기 경계에서의 델로네 분할
    g, pos = gt.triangulation(points, type='delaunay', periodic=False)

    print("== Delaunay Triangulation ==")
    print("Num. of vertices:", g.num_vertices())  # 일반적으로 500
    print("Num. of edges   :", g.num_edges())

    # 3) 엣지 가중치(유클리드 거리)
    weight = g.new_edge_property("double")
    for e in g.edges():
        s = e.source()
        t = e.target()
        diff = pos[s] - pos[t]
        dist = math.sqrt(diff[0]**2 + diff[1]**2)
        weight[e] = dist

    # 4) betweenness
    vb, eb = gt.betweenness(g, weight=weight)
    eb.a *= 120  # 에지 두께 스케일

    # 5) 그래프 시각화
    gt.graph_draw(g, pos=pos,
                  vertex_fill_color=vb,
                  edge_pen_width=eb,
                  output="triang_delaunay.pdf")

    print(" - Drawn to triang_delaunay.pdf")
  • Simple triangulation과 거의 동일하지만, type="delaunay"로만 바꿔주면 됨.
  • periodic=True도 가능. 이 경우, 점들이 경계를 넘어 wrap-around된다고 생각하고 델로네 분할.

5. 3D 점 집합에서의 Triangulation

  • 이 함수는 3차원(3D) 점 집합에 대해서도 지원.
  • 단, 그래프툴 자체의 3D 시각화는 기본적으로 2D 투영을 사용하므로, 별도의 3D 라이브러리나 pos 데이터를 활용한 시각화를 고려할 수 있음.
def delaunay_3d_example():
    points_3d = np.random.rand(200, 3) * 5.0  # (N=200, x,y,z in [0,5)

    g3d, pos3d = gt.triangulation(points_3d, type='delaunay', periodic=False)

    print("3D Delaunay Triangulation:")
    print("Vertices:", g3d.num_vertices(), "Edges:", g3d.num_edges())
    
    # edge 등은 동일하게 가중치 계산 가능 (단, 3차원 거리)
    # 시각화는 pos3d를 가지고 2D에 투영 or 다른 tool 써서 3D 시각 가능

6. Triangulation 그래프 특징 및 주의사항

  • 2D인 경우, 삼각형(face)들이 형성되며, graph-tool에서는 각 삼각형을 “3개의 edge가 형성하는 사이클”로 표현.
    • 3D인 경우, 사면체(4개의 점)이지만, graph-tool에서는 여전히 “edge + face” 관계로 표현(특별한 3D mesh 구조는 아님).
  • simple triangulation은 점 삽입 순서에 따라 결과가 달라질 수 있으니, 재현(reproducible)하려면 점을 일정한 순서로 고정해야 함.
  • Delaunay는 더욱 ‘균등’한 분할을 하지만, 계산비용이 조금 더 클 수 있음.
  • Edge 수: triangulation 그래프는 일반적으로 (3N - 3 - h) (2D) 정도( (h)는 볼록껍질의 점 수) 등 공식이 있으나, 구체적 계산은 분할 방식에 의존.

7. 확장 아이디어

  1. face 목록: graph-tool에서는 각 face(삼각형)에 대한 정보도 얻을 수 있음 (g.faces() 등).
  2. 라플라시안 연산: 2D 메시에 대한 라플라시안 행렬 등을 구성할 수 있음(특정 PDE 해석에 응용).
  3. 알고리즘 속도: 많은 점(수십만~수백만)을 triangulation하려면 C++ CGAL 라이브러리 연동이므로 꽤 빠르지만, 여전히 큰 데이터에서는 메모리/시간 고려 필수.
  4. periodic = True와 함께 3D Delaunay 시도 → 물리 시뮬레이션(박스 내 입자 배치) 등에서 활용.

8. 종합 예시: Simple vs Delaunay 비교 및 betweenness 시각화

아래는 2D 점(500개)에 대해 simple/delaunay 두 방식 모두 실행 후, betweenness 시각화 파일을 각각 저장하는 종합 코드 예시입니다.

import graph_tool.all as gt
import numpy as np
import math

def compare_triangulations():
    # 난수로 2D 점 500개 생성
    points = np.random.rand(500, 2) * 4.0

    # 1) Simple Triangulation
    g_simp, pos_simp = gt.triangulation(points, type='simple', periodic=False)
    # 2) Delaunay Triangulation
    g_dela, pos_dela = gt.triangulation(points, type='delaunay', periodic=False)

    # 가중치(거리) 계산 함수
    def compute_edge_weight(g, pos):
        w = g.new_edge_property("double")
        for e in g.edges():
            s, t = e.source(), e.target()
            dx = pos[s][0] - pos[t][0]
            dy = pos[s][1] - pos[t][1]
            w[e] = math.sqrt(dx*dx + dy*dy)
        return w

    w_simp = compute_edge_weight(g_simp, pos_simp)
    w_dela = compute_edge_weight(g_dela, pos_dela)

    # Betweenness
    vb_s, eb_s = gt.betweenness(g_simp, weight=w_simp)
    vb_d, eb_d = gt.betweenness(g_dela, weight=w_dela)

    eb_s.a *= 100
    eb_d.a *= 100

    # 시각화
    gt.graph_draw(g_simp, pos=pos_simp, vertex_fill_color=vb_s, edge_pen_width=eb_s,
                  output="simple_vs.pdf")
    gt.graph_draw(g_dela, pos=pos_dela, vertex_fill_color=vb_d, edge_pen_width=eb_d,
                  output="delaunay_vs.pdf")

    print("Simple triangulation -> simple_vs.pdf")
    print("Delaunay triangulation -> delaunay_vs.pdf")


if __name__ == "__main__":
    compare_triangulations()
  • 결과로 simple_vs.pdf, delaunay_vs.pdf 두 파일이 생성됩니다.
    • simple vs. delaunay의 삼각분할 구조 차이, 및 betweenness 분포 차이를 확인 가능.

9. 마무리

  • triangulation() 함수는 2D/3D 점 집합에 대한 단순(Simple) 또는 델로네(Delaunay) 삼각분할을 자동으로 해주는 강력한 도구.
  • 결과 그래프를 사용하여 메시(mesh) 기반의 그래프 연산, 가중치 기반 betweenness 등 네트워크 중심성, 주기경계 처리, 2D/3D 시각화 등을 수행할 수 있음.
  • 학습/연구/시뮬레이션에서 유용하게 쓰이므로, 간단한 프로토타이핑부터 실제 규모의 문제에까지 활용 가능.
  • 델로네 분할 등은 CGAL 라이브러리가 내부적으로 쓰이므로, 정확하고 robust하지만, 대규모 데이터 시에는 시간/메모리 고려 필요.

이상으로 graph-tool의 삼각분할(triangulation) 함수에 대한 기초 튜토리얼을 마칩니다.
추가적으로, 3D 점이나 periodic 옵션을 다룰 때도 위 코드 구조를 응용하면 쉽게 적용 가능합니다. 즐거운 네트워크 분석 되세요!

profile
AI가 재밌는 걸

0개의 댓글