우와한 개발자

[Spring AI] 텍스트 유사도란? — DTM, TF-IDF, 코사인 유사도, 자카드 유사도, HNSW

by 우와한개발자

1. 텍스트 기반 유사도

  • 임베딩 모델 없이 텍스트 자체를 비교하는 방식.
  • 임베딩 모델이 나오기 전에 검색에서 많이 쓰였다.

1) 자카드 유사도 (Jaccard Similarity)

  • 두 문장에서 공통으로 등장하는 단어 비율로 측정
  • 단어 집합 기반으로 순서나 빈도는 무시한다.
문장 A: {"강아지", "뛴다"}
문장 B: {"강아지", "달린다"}

자카드 유사도 = 공통 단어 수 / 전체 단어 수
             = 1 / 3 = 0.33
def jaccard_similarity(a, b):
    set_a = set(a.split())
    set_b = set(b.split())
    intersection = set_a & set_b
    union = set_a | set_b
    return len(intersection) / len(union)

a = "강아지 뛴다"
b = "강아지 달린다"
print(jaccard_similarity(a, b))  # 0.333...

 

2) DTM (Document-Term Matrix)

  • 문서에 단어가 몇 번 등장하는지 행렬로 표현
  • 단순 빈도 기반이라 흔한 단어(은, 는, 이, 가)도 중요하게 취급되는 한계가 있다.
문서1: "강아지 뛴다 강아지"
문서2: "고양이 달린다"

        강아지  뛴다  고양이  달린다
문서1  [  2,    1,    0,     0  ]
문서2  [  0,    0,    1,     1  ]
from sklearn.feature_extraction.text import CountVectorizer

docs = ["강아지 뛴다 강아지", "고양이 달린다"]
vectorizer = CountVectorizer()
dtm = vectorizer.fit_transform(docs)

print(vectorizer.get_feature_names_out())  # 단어 목록
print(dtm.toarray())                       # 행렬

 

3) TF-IDF

  • DTM에서 발전한 방식
  • 자주 등장하지만 흔한 단어는 중요도를 낮추고, 특정 문서에서만 자주 나오는 단어는 중요도를 높여 가중치를 부여한다.

(1) TF (Term Frequency)

  • 특정 문서에서 단어가 얼마나 자주 등장하는지

(2) IDF (Inverse Document Frequency)

  • 전체 문서에서 단어가 얼마나 희귀한지
  • 모든 문서에 등장하는 흔한 단어일수록 낮은 값

(3) TF-IDF = TF × IDF

  • 특정 문서에서 자주 나오면서 다른 문서에서는 잘 안 나오는 단어일수록 높은 값
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

docs = ["강아지가 뛴다", "개가 달린다", "오늘 날씨 맑음"]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(docs)

# 문서 간 코사인 유사도 계산
similarity = cosine_similarity(tfidf_matrix)
print(similarity)

 

2. 벡터 기반 유사도

  • 임베딩으로 변환된 벡터끼리 비교하는 방식.

 

1) 코사인 유사도 (Cosine Similarity)

  • 두 벡터 사이의 각도로 유사도 측정
  • 벡터의 크기는 무시하고 방향만 봄
  • 범위 0~1, 1에 가까울수록 유사하다.
  • 텍스트 의미 검색(RAG)에 가장 많이 쓰인다.
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

a = np.array([[0.12, 0.87, 0.34]])
b = np.array([[0.13, 0.85, 0.36]])
print(cosine_similarity(a, b))  # 1에 가까운 값

 

2) 유클리드 거리 (Euclidean Distance)

  • 두 벡터 사이의 직선 거리로 측정
  • 값이 0에 가까울수록 유사
  • 벡터 크기가 의미있을 때 적합하다.
  • 이미지/추천 시스템에 사용한다.
from scipy.spatial.distance import euclidean

a = [0.12, 0.87, 0.34]
b = [0.13, 0.85, 0.36]
print(euclidean(a, b))  # 0에 가까울수록 유사

 

3) 내적 (Dot Product)

  • 두 벡터를 곱해서 합산한 값으로 측정
  • 벡터 크기에 영향을 받기 때문에 단독으로 쓰면 유사도가 왜곡될 수 있다.
  • 벡터를 정규화(크기를 1로 맞춤)한 상태에서 사용하면 코사인 유사도와 동일한 결과를 얻는다.
  • 정규화된 벡터 환경에서 코사인 유사도를 더 빠르게 계산할 수 있어 대규모 고속 검색에 적합하다.
import numpy as np

a = np.array([0.12, 0.87, 0.34])
b = np.array([0.13, 0.85, 0.36])
print(np.dot(a, b))  # 값이 클수록 유사

 

3. 텍스트 기반 vs 벡터 기반 유사도 비교

방식 분류 임베딩 필요 적합한 상황
자카드 유사도 텍스트 기반 X 단순 키워드 매칭
DTM / TF-IDF 텍스트 기반 X 검색 엔진 전통 방식
코사인 유사도 벡터 기반 O RAG, 의미 검색 표준
유클리드 거리 벡터 기반 O 이미지, 추천 시스템
내적 벡터 기반 O 정규화된 환경에서 고속 검색

 

4. 인덱스 방식

  • 수백만 개의 벡터를 전부 순회하며 유사도를 계산하면 너무 느리기 때문에 인덱스를 사용해 탐색 범위를 줄인다.

 

1) HNSW (Hierarchical Navigable Small World)

  • 고차원 벡터 공간에서 쿼리 벡터와 가장 유사한 항목을 초고속으로 찾아내는 그래프 기반의 근사 최근접 이웃(ANN) 검색 알고리즘
  • 계층적 그래프 구조로 벡터를 연결해 가장 가까운 벡터를 빠르게 탐색한다.
  • 장점 : 검색 속도 빠름, 정확도 높음
  • 단점 : 메모리 사용량 많음, 인덱스 구축 시간 있음
  • pgvector 기본 설정값

 

2) IVFFlat (Inverted File Index)

  • 벡터를 클러스터로 나눠두고 가까운 클러스터만 탐색
  • 장점 : 메모리 효율적
  • 단점 : HNSW보다 정확도 낮음

 

5. 임베딩 / Vector DB / 텍스트 유사도의 관계

1) 파이프라인

  • 셋은 독립적인 개념이 아니라 하나의 파이프라인이다
텍스트
  → 임베딩 모델이 벡터로 변환
  → Vector DB에 저장
  → 검색 시 코사인 유사도로 비교
  → 의미적으로 유사한 문서 반환
  • 임베딩 : 텍스트를 벡터로 변환하는 과정/모델
  • Vector DB : 변환된 벡터를 저장하고 유사도 검색을 수행하는 저장소
  • 텍스트 유사도 : 임베딩 + Vector DB를 통해 얻는 최종 결과

 

2) RAG와의 연결

  • 임베딩이 없으면 텍스트를 벡터로 못 바꾸고, 벡터가 없으면 Vector DB에 저장도 유사도 비교도 못한다
  • RAG는 이 파이프라인을 이용해서 질문과 의미적으로 유사한 문서를 찾고 LLM에 context로 전달한다

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기