아래 인용된 논문 (\textit{"Efficient K-Nearest Neighbor Graph Construction for Generic Similarity Measures"})는 임의(Arbitrary) 유사도 함수를 사용하는 대규모 데이터셋에서 (k)-최근접 이웃 그래프((k)-NNG)를 효율적으로 구성하기 위한 방법을 다루고 있습니다.
본 답변에서는 중학생도 이해할 수 있도록 최대한 평이한 표현으로, 동시에 수식과 원리에 집중해서 정성껏 설명하겠습니다. 답변 순서는 대략 아래와 같습니다:
“(\textbf{NN-Descent})라는 간단하지만 강력한 로컬 탐색 기반 알고리즘을 통해, 임의 유사도 함수에 대해 (\textbf{근사}) (k)-NNG를 빠르게 구성할 수 있다.”
이 과정을 반복하면, 점점더 “진짜 (k)-최근접 이웃”에 근접한 해를 얻을 수 있음.
(\textbf{작동 원리})는 직관적으로, “(\textbf{내 친구(이웃)의 친구(이웃)})도 내 친구가 될 가능성이 높다”는 현상을 이용.
[
\boxed{
\begin{aligned}
&\text{(핵심 포인트)}\
&\quad \bullet\; \text{임의의 유사도 함수(메트릭 등)에 적용 가능.}\
&\quad \bullet\; \text{이웃-이웃 탐색 + 반복적(Iterative)으로 근사 k-NN 갱신.}\
&\quad \bullet\; \text{다양한 최적화(Incremental, Sampling, Local Join)로 연산량 대폭 절감.}\
&\quad \bullet\; \text{크게 복잡한 자료구조 필요없이, 분산환경에서도 스케일업 쉬움.}
\end{aligned}
}
]
아래는 간단한(매우 축소된) NN-Descent 알고리즘을 파이썬으로 흉내 내는 예시입니다. (실제 paper에서처럼 효율 최적화는 많이 생략했음을 유의하세요.)
import numpy as np
import random
def similarity(x, y):
""" 예시: 유클리드 거리 기반 유사도(작을수록 유사한 것으로) """
# 여기서는 간단히 음(-) 거리로 유사도를 정의
dist = np.linalg.norm(x - y) # 유클리드 거리
return -dist # 음수를 취해 거리 작을수록 값이 커지도록
def nn_descent(data, K=10, max_iter=10):
"""
data: shape (n, dim) numpy array
K: number of neighbors
"""
n = data.shape[0]
# (1) 초기화: 무작위로 K개의 후보 이웃 선정
# neighbors[i] = [(idx, sim), ... ] (길이 K)
neighbors = []
for i in range(n):
# K개의 다른 점을 랜덤 선택
rand_idx = random.sample(range(n), K+1) # 혹시 i가 포함되면 빼줄 예정
rand_idx = [x for x in rand_idx if x != i]
rand_idx = rand_idx[:K] # K개만
temp_list = []
for j in rand_idx:
sim_val = similarity(data[i], data[j])
temp_list.append((j, sim_val))
# 유사도 큰 순으로 정렬(파이썬은 정렬시 기본 오름차순 -> sim_val이 큰게 앞으로)
temp_list.sort(key=lambda x: x[1], reverse=True)
neighbors.append(temp_list)
# (2) Iteration
for iteration in range(max_iter):
changed = 0
# R[v] 계산은 구현상 skip, 대신 "이웃의 이웃"을 직접 탐색
for i in range(n):
# (a) i의 이웃 후보들
neigh_i = neighbors[i]
# (b) "이웃의 이웃" 후보들을 모아서
candidates = set()
for (u_idx, sim_val) in neigh_i:
# 이웃 u의 이웃목록에 있는 노드들을 candidates에 추가
for (z_idx, _) in neighbors[u_idx]:
if z_idx != i:
candidates.add(z_idx)
# (c) i와 각 candidate 간 유사도 비교 -> 만약 top-K 갱신가능하면 갱신
# neighbors[i]는 항상 유사도 내림차순 정렬 상태를 유지(가장 뒤가 최소)
min_sim_in_list = neighbors[i][-1][1] # 리스트 내 최소유사도
for c_idx in candidates:
sim_c = similarity(data[i], data[c_idx])
if sim_c > min_sim_in_list:
# 대체
neighbors[i].pop() # 꼴찌 제거
neighbors[i].append((c_idx, sim_c))
neighbors[i].sort(key=lambda x: x[1], reverse=True)
min_sim_in_list = neighbors[i][-1][1]
changed += 1
if changed == 0:
print(f"Iteration {iteration}, no changes -> stop.")
break
return neighbors
# 예시 실행
if __name__=="__main__":
np.random.seed(0)
random.seed(0)
data = np.random.random((100, 5)) # 100개, 차원=5
result = nn_descent(data, K=5, max_iter=10)
# result[i] => i번 데이터의 근사 이웃 5개
# ...
위 코드는 개념 증명용으로, 실제 논문 구현에서 제시한 Sampling, Incremental, Local Join 최적화는 반영되어 있지 않습니다. 그럼에도, 전체적인 작동 흐름은 비슷합니다:
실제로 대규모 데이터(수십만~수백만)엔 C++/병렬/분산 구현이 필수겠지만, 기본 아이디어는 위와 유사합니다.
끝!