LLM 엔지니어링11장 · 폐쇄망 문서 파이프라인과 폐쇄형 RAG11 / 23← 이전목차다음 →MetaContents

11장. 폐쇄망 문서 파이프라인과 폐쇄형 RAG

인덱스의 속도와 정확도

책 한 권을 찾을 때 서가를 처음부터 끝까지 훑으면 반드시 찾지만 느립니다. 분류 번호가 같은 서가 몇 개만 보면 빠르지만 다른 서가에 꽂힌 책은 놓칩니다. 사서에게 물어 "그 책 옆에 이런 책이 있어요"를 따라가면 빠르고 대개 맞습니다. 벡터의 인덱스도 이 셋입니다.

모든 벡터와 견주는 Flat, 칸을 나눠 몇 칸만 보는 IVF, 이웃을 따라 건너가는 HNSW 의 세 카드

왼쪽 Flat 은 모든 벡터와 견주어 정확하지만, 벡터가 많아지면 느립니다. 가운데 IVF 는 벡터를 칸(nlist)으로 나눠 두고 질문과 가까운 몇 칸(nprobe)만 봅니다. 오른쪽 HNSW 는 이웃끼리 이은 그물을 따라 건너가며, 한 번에 살펴볼 후보 수(efSearch)로 속도와 정확도를 고릅니다. 여기서 서가 훑기가 Flat, 분류 서가만 보기가 IVF, 사서에게 물어 옮겨 가기가 HNSW 입니다.

손잡이 견주기

인덱스미리 하는 일손잡이올리면
Flat없음없음늘 정확하다(재현율 1)
IVF칸 나누기 학습(train)nprobe: 볼 칸의 수정확해지고 느려진다
HNSW그물 만들기(넣을 때 오래 걸린다)efSearch: 살펴볼 후보 수정확해지고 느려진다

IVF 의 생김새

Python
import faiss
import numpy as np

rng = np.random.default_rng(0)
X = rng.normal(size=(2000, 32)).astype("float32")          # 연습용 벡터 2,000개
ivf = faiss.IndexIVFFlat(faiss.IndexFlatL2(32), 32, 20)    # 32차원, 칸 20개
ivf.train(X)                                               # 칸을 나누는 기준을 배운다
ivf.add(X)
ivf.nprobe = 4                                             # 가까운 칸 4개만 본다
D, I = ivf.search(X[:1], 3)
print(I)

코드에 나오는 함수

코드쉬운 뜻
faissFacebook AI Similarity Search(페이스북 AI 유사도 검색)의 머리글자다. 벡터 검색 라이브러리다
np.random.default_rng(0)씨앗 0 으로 난수 생성기를 만든다. rng 는 random number generator(난수 생성기)다
rng.normal(size=(2000, 32)).astype("float32")정규분포 난수로 32차원 벡터 2,000개를 만들고, FAISS 가 받는 float32 꼴로 바꾼다
faiss.IndexFlatL2(32)32차원 벡터를 모두와 견주는 인덱스다. L2 는 보통의 직선 거리(유클리드 거리)다. 여기서는 칸의 중심을 찾는 데 쓴다
faiss.IndexIVFFlat(…, 32, 20)IVF(Inverted File, 역색인 파일) 인덱스다. 벡터 32차원, 칸 20개(nlist)로 나눈다
ivf.train(X)벡터를 보고 칸 20개의 중심을 배운다
ivf.add(X)벡터를 넣는다. 벡터마다 가장 가까운 칸에 들어간다
ivf.nprobe = 4number of probes(살펴볼 수)의 줄임이다. 질문과 가까운 칸 4개만 본다
D, I = ivf.search(X[:1], 3)첫 벡터와 가까운 3개를 찾는다. D 는 distance(거리), I 는 index(번호)다

이 코드가 뜻하는 것

도서관 서가로 읽어 봅니다. 책을 꽂기 전에 분류 번호대로 서가 20개를 정해 두고(train), 책을 맞는 서가에 꽂습니다(add). 찾을 때는 찾는 책과 가까운 서가 4개만 둘러봅니다(nprobe = 4). 여기서 서가가 칸, 책이 벡터, 둘러볼 서가의 수가 nprobe 입니다.

줄하는 일쉽게 말하면
X = rng.normal(...)연습용 벡터 2,000개를 만든다책 2,000권
ivf = faiss.IndexIVFFlat(..., 32, 20)칸 20개짜리 인덱스를 만든다서가 20개를 둔다
ivf.train(X)칸을 나누는 기준을 배운다서가마다 분류 번호를 정한다
ivf.add(X)벡터를 칸에 넣는다책을 서가에 꽂는다
ivf.nprobe = 4볼 칸의 수를 정한다가까운 서가 4개만 둘러보기로 한다
D, I = ivf.search(X[:1], 3)가까운 3개를 찾는다비슷한 책 세 권을 꺼낸다

교안을 만든 PC 에서는 [[ 0 581 1119]] 가 나왔습니다. 맨 앞의 0 은 질문으로 쓴 첫 벡터 자기 자신입니다. 넣어 둔 벡터로 물었으니 자기 자신이 가장 가깝습니다. 나머지 둘이 정말 가장 가까운 이웃인지는 이 출력만으로는 알 수 없고, 모두 견주는 Flat 의 답과 대 봐야 압니다.

작게 줄여서 눈으로 보기

Flat 의 답을 정답으로 두고, nprobe 를 바꿔 가며 IVF 가 정답을 얼마나 찾는지(재현율) 봅니다. 질문 50개는 넣어 둔 벡터를 조금 흔들어 만든 연습용입니다.

Python
import faiss
import numpy as np

rng = np.random.default_rng(0)
X = rng.normal(size=(2000, 32)).astype("float32")
Q = X[:50] + rng.normal(scale=0.5, size=(50, 32)).astype("float32")   # 조금 흔든 질문 50개

flat = faiss.IndexFlatL2(32)
flat.add(X)
_, truth = flat.search(Q, 5)                     # 모두 견준 정답 5개씩

ivf = faiss.IndexIVFFlat(faiss.IndexFlatL2(32), 32, 20)
ivf.train(X)
ivf.add(X)
for nprobe in [1, 4, 20]:
    ivf.nprobe = nprobe
    _, found = ivf.search(Q, 5)
    recall = np.mean([len(set(a) & set(b)) / 5 for a, b in zip(truth, found)])
    print(f"nprobe {nprobe:2d} → 재현율 {recall:.3f}")
출력
nprobe  1 → 재현율 0.396
nprobe  4 → 재현율 0.708
nprobe 20 → 재현율 1.000
결과읽는 법
nprobe 1 → 재현율 0.396가장 가까운 칸 하나만 보면 정답 5개 가운데 평균 2개쯤만 찾는다
nprobe 4 → 재현율 0.708칸 4개를 보면 정답의 70% 쯤을 찾는다
nprobe 20 → 재현율 1.000칸 20개를 모두 보면 Flat 과 똑같다. 대신 빠르게 하려던 뜻이 없어진다

set(a) & set(b) 는 두 번호 묶음에 함께 있는 것, 곧 맞힌 정답입니다. 이 연습용 벡터는 뭉친 데 없이 고르게 흩어진 무작위 벡터라서, 정답 이웃이 이웃 칸에 나뉘어 있는 일이 잦습니다. 실제 문서의 임베딩은 비슷한 것끼리 뭉쳐 있어 같은 nprobe 에서도 재현율이 다를 수 있으니, 실습 5처럼 우리 데이터로 재서 고릅니다.

주의할 점

  • 벡터가 몇만 개 이하면 Flat 이 충분히 빠릅니다. 빠른 인덱스는 재현율을 조금 잃는 대신 속도를 얻는 것이라, 규모가 커질 때 꺼냅니다.
  • 재현율은 데이터의 생김새에 따라 다릅니다. 손잡이 값은 우리 데이터의 질문 묶음으로 재어 고릅니다(실습 5).
← 이전 절11장 · 벡터DB 고르기다음 절 →11장 · 폐쇄형 RAG 와 권한