"RAG 핵심 학습 (10/26) — Dense Retrieval 깊이 다이브"

임베딩과 벡터DB가 준비됐다. 이제 검색 그 자체 — Dense Retrieval의 원리를 본다.

Dense Retrieval은 키워드 일치가 아니라 벡터 거리로 답을 찾는다. 단순해 보이지만 Bi-encoder의 비대칭, Negative Sampling, Top-K 거리 분포를 모르면 왜 어떤 답이 나왔는지 설명할 수 없다. 본 편은 DPR(Karpukhin 2020)에서 시작해 query·document 임베딩의 비대칭 학습, top-K 거리의 의미, 그리고 Dense의 한계를 식과 코드로 푼다.


0. Prerequisites

  • 8편 임베딩 모델 — 정규화, 유사도 함수.
  • 9편 벡터DB — HNSW의 top-K 검색.
  • 7편 메타데이터 — pre-filter와 결합 흐름.

1. 학습 목표

  1. Bi-encoder 구조와 query·doc 비대칭을 식으로 안다.
  2. DPR의 in-batch negative sampling 원리를 안다.
  3. top-K 거리 분포로 검색 품질을 진단한다.
  4. Dense의 5가지 한계언제 BM25·Hybrid·Reranker로 보완할지 안다.

2. 핵심 요약

Dense Retrieval은 query와 document를 같은 벡터 공간에 매핑하고 거리/유사도로 top-K를 뽑는다. Bi-encoder(query encoder + doc encoder, 같거나 다를 수 있음)로 인덱싱과 query를 분리해 문서 임베딩은 미리 계산, query는 런타임에 계산한다. DPR(Karpukhin 2020)이 표준화한 학습 방식은 in-batch negatives로 정답 쌍을 가까이, 다른 batch 쌍을 멀리. 검색 시 코사인 또는 내적으로 top-K. Dense의 강점은 동의어·의미 유사성에 강함이고, 약점은 희귀 어휘·정확 매칭에 약함 — 11편 BM25와 결합(Hybrid, 12편)이 표준 해법.


3. 직관 — Dense가 잡고, BM25가 놓치는 것

질문: "분기 매출 보고서의 통화 환산 기준?"

같은 코퍼스에서:

  • BM25: "통화 환산"이 그대로 들어간 청크를 강하게 매칭. 본문에 "환율 적용"으로만 쓰여 있으면 놓침.
  • Dense: "통화 환산"과 "환율 적용"을 유사한 의미로 묶어 둘 다 top-K.
diagram-1

반대 경우: "SKU-2024-04 재고" 같이 고유명사·코드가 핵심인 query는 BM25가 강함. Dense는 희귀 토큰의 임베딩이 흐림.


4. 정의 — Bi-encoder와 핵심 용어

용어 정의
Bi-encoder query encoder \(E_q\)와 doc encoder \(E_d\)가 각자 독립으로 벡터를 만들고, 사후에 유사도만 계산하는 구조
Symmetric \(E_q = E_d\) (같은 모델). BGE-M3, OpenAI 기본
Asymmetric \(E_q \ne E_d\) (다른 가중치). DPR, Upstage Solar
Top-K 유사도 기준 상위 K개 candidate. 보통 K=5~50
In-batch negative 학습 시 같은 batch 내 다른 쌍을 negative로 사용
Hard negative 의미가 비슷하지만 정답 아님 — 가장 학습 효과 큼
Bi-encoder vs Cross-encoder Bi: 사전 계산 가능, 빠름 / Cross: query+doc 동시 입력, 느림. Cross는 13편 Reranker

5. 식 — DPR과 학습 손실

유사도 (정규화된 벡터 가정):

$$\text{sim}(q, d) = E_q(q) \cdot E_d(d)$$

InfoNCE 손실 (DPR의 표준 학습):

$$\mathcal{L} = -\log \frac{\exp(\text{sim}(q, d^+))}{\exp(\text{sim}(q, d^+)) + \sum_{d^- \in N} \exp(\text{sim}(q, d^-))}$$

  • \(d^+\) = 정답 문서
  • \(N\) = negative 집합 (in-batch + hard)
  • 학습은 정답 유사도를 키우고 negative 유사도를 줄이는 방향.

Top-K 검색:

$$\text{Top-K}(q) = \arg\max_{d \in \mathcal{D}}^{(K)} \text{sim}(q, d)$$

ANN(9편 HNSW)이 이 \(\arg\max\)를 근사. 정확 KNN은 \(\mathcal{O}(N)\), HNSW는 \(\mathcal{O}(\log N)\).

거리 분포 진단:

  • top-1 유사도 \(s_1\)과 top-10 유사도 \(s_{10}\)의 gap \(s_1 - s_{10}\)이 클수록 검색이 확신.
  • gap이 작으면 덤덤한 분포 — 5편 8.3절(청크 큼)이나 본 편 8.3절(쿼리 모호).

6. 원리 워크스루 — Dense Retrieval을 처음부터

6.1 임베딩 후 in-memory top-K (개념 확인)

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-m3")

docs = ["분기 매출 보고는 환율 적용 후 USD로 통합한다.",
        "재고 SKU-2024-04는 인천 창고에 30개 보관 중.",
        "예외 신청은 부서장 승인 후 SEC-EX-04 양식을 제출."]

doc_embs = model.encode(docs, normalize_embeddings=True)   # (3, 1024)
query_emb = model.encode("통화 환산 기준?", normalize_embeddings=True)  # (1024,)

scores = doc_embs @ query_emb     # 내적 = 정규화 시 코사인
top_k = np.argsort(-scores)[:2]
for i in top_k:
    print(f"  {scores[i]:.3f} | {docs[i]}")

출력 예시:

  0.612 | 분기 매출 보고는 환율 적용 후 USD로 통합한다.
  0.184 | 예외 신청은 부서장 승인 후 SEC-EX-04 양식을 제출.

top-1 (0.612)과 top-2 (0.184)의 gap이 크다 → 확신 있는 검색. 첫 청크가 "환율 적용"만 가지고 있는데 "통화 환산" query에 매칭됐다. 이게 Dense의 핵심 능력.

6.2 LangChain으로 벡터DB 통합 top-K

from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma

embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-m3", encode_kwargs={"normalize_embeddings": True})
vectordb = Chroma(persist_directory="./chroma_db", embedding_function=embeddings, collection_name="rag")

vectordb.add_texts(docs, metadatas=[{"version": "3.2"}] * len(docs))

results = vectordb.similarity_search_with_score(
    "통화 환산 기준?",
    k=5,
    filter={"version": "3.2"},   # 7편 pre-filter
)
for doc, score in results:
    print(f"  {score:.3f} | {doc.page_content[:50]}...")

LangChain은 embed_queryembed_documents자동 분리 — 8편 8.2절(비대칭) 사고를 줄임.

6.3 DPR style — 비대칭 학습된 두 encoder

from transformers import DPRQuestionEncoder, DPRContextEncoder, DPRQuestionEncoderTokenizer, DPRContextEncoderTokenizer

q_enc   = DPRQuestionEncoder.from_pretrained("facebook/dpr-question_encoder-single-nq-base")
d_enc   = DPRContextEncoder.from_pretrained("facebook/dpr-ctx_encoder-single-nq-base")
q_tok   = DPRQuestionEncoderTokenizer.from_pretrained("facebook/dpr-question_encoder-single-nq-base")
d_tok   = DPRContextEncoderTokenizer.from_pretrained("facebook/dpr-ctx_encoder-single-nq-base")

q_inputs = q_tok("통화 환산 기준?", return_tensors="pt")
q_emb = q_enc(**q_inputs).pooler_output     # (1, 768)

d_inputs = d_tok("분기 매출 보고는 환율 적용 후 USD로 통합한다.", return_tensors="pt")
d_emb = d_enc(**d_inputs).pooler_output     # (1, 768)

sim = (q_emb * d_emb).sum(dim=-1).item()

핵심: 두 encoder의 가중치가 다르다. 같은 텍스트를 둘에 넣어도 다른 벡터. 8편 8.2절 비대칭 사고를 막으려면 query는 q_enc, doc은 d_enc반드시 분리 호출.


7. 변형과 사례

7.1 Symmetric vs Asymmetric — 모델 카드 확인

  • 무엇이 바뀌나: \(E_q\)와 \(E_d\)가 같은가 다른가.
  • 왜 쓰나: Asymmetric은 query와 doc의 입력 분포가 다른 경우 더 효율(짧은 query vs 긴 doc).
  • 무엇이 가능해졌나: query 길이 \(\ne\) doc 길이라도 정밀한 매칭.
  • 어디에 적합한가: 사용자 질문(짧고 구어) vs 문서(길고 격식)의 간극이 큰 도메인.
  • 한계: query·doc 호출 함수 분리 운영 필요. 누락 시 8.2절 사고.

7.2 In-batch negative + Hard negative

  • 무엇이 바뀌나: 학습 batch의 다른 쌍을 모두 negative로 사용 + 사전 검색한 hard negative를 추가.
  • 왜 쓰나: in-batch만으론 비슷한 doc 간 구분이 약함. Hard negative가 결정 경계를 날카롭게.
  • 무엇이 가능해졌나: 의미가 비슷한 두 문서를 정확히 구분하는 retrieval.
  • 어디에 적합한가: 도메인 특화 fine-tuning (법무·의료·사내).
  • 한계: hard negative 채굴 비용. self-training 루프 필요.

7.3 Multi-vector retrieval — ColBERT

  • 무엇이 바뀌나: 문서당 하나의 벡터가 아니라 토큰별 벡터. query의 각 토큰이 doc의 가장 가까운 토큰에 매칭(late interaction).
  • 왜 쓰나: 단일 벡터의 정보 압축 손실을 회피.
  • 무엇이 가능해졌나: 긴 문서의 부분 매칭 정확도 향상.
  • 어디에 적합한가: 긴 문서 코퍼스, 정확도 우선.
  • 한계: 인덱스 크기 수십 배. BGE-M3가 ColBERT 모드 내장.

7.4 query 확장 — Multi-query 회피용 단일 query 보강

  • 무엇이 바뀌나: 사용자 query를 paraphrase 3~5개로 확장해 각각 검색 후 통합.
  • 왜 쓰나: 짧은·모호한 query의 recall을 키움.
  • 무엇이 가능해졌나: query 재기술로 검색 안정성↑.
  • 어디에 적합한가: 사용자 query가 짧고 모호한 챗봇 RAG.
  • 한계: query당 호출 수↑ (LLM + retrieval 모두). 18편 Query Rewrite에서 자세히.

7.5 Embedding cache — 같은 query 반복 시

  • 무엇이 바뀌나: query 임베딩을 해시 키로 캐시.
  • 왜 쓰나: FAQ·반복 query에서 재계산 비용 0.
  • 무엇이 가능해졌나: latency·비용 절감.
  • 어디에 적합한가: 사용자 패턴이 Zipf-like(소수 query에 집중)인 모든 RAG.
  • 한계: 임베딩 모델 변경 시 캐시 전체 무효화 — TTL 관리.

8. 한계와 실패 양상

8.1 희귀 어휘·고유명사 약함

  • 왜 본질적인가: 임베딩 모델은 고빈도 단어에 강하고 희귀 토큰에 약함. SKU-2024-04 같은 코드는 임베딩이 흐림.
  • 진단: 고유명사·코드를 포함한 query의 recall이 유의미하게 낮음.
  • 완화: 11편 BM25와 Hybrid. 12편 RRF.
  • 다음 편: 11편(BM25), 12편(Hybrid).

8.2 덤덤한 top-K — 확신 부족

  • 왜 본질적인가: query가 모호하거나 코퍼스에 유사한 청크가 많을 때 top-K 점수가 비슷하게 줄지어 나옴.
  • 진단: top-1과 top-10 gap < 0.1 (정규화 코사인 기준).
  • 완화: query rewrite(18편), Reranker(13편), 답에 낮은 확신 신호.
  • 다음 편: 13편, 18편, 19편(Confidence).

8.3 부정문·반의어 처리 약함

  • 왜 본질적인가: "예외가 아닌 경우"와 "예외인 경우"는 임베딩이 유사. 의미 반전을 벡터 거리로 표현 어려움.
  • 진단: 부정 query에서 정반대 답이 top-K.
  • 완화: 답 생성 단계에서 명시적 의미 검증. 12편 Hybrid의 BM25 신호가 일부 보완(고빈도 부정 토큰).
  • 다음 편: 22편(답 검증).

8.4 언어 불일치 — 학습 분포 밖

  • 왜 본질적인가: 학습이 영어 중심인 모델에 한국어 query → 임베딩 흐림.
  • 진단: 같은 코퍼스를 다른 모델로 임베딩 후 retrieval 품질 비교.
  • 완화: 8편 결정 표 — 한국어 비중 30% 이상이면 BGE-M3 또는 Upstage.
  • 다음 편: 8편 cross-ref.

8.5 최근성 무시 — 시간 가중치 없음

  • 왜 본질적인가: Dense는 의미 유사도만 본다. 최신성이 중요한 query("이번 분기 정책")에 옛 청크가 top-K.
  • 진단: 답에 옛 버전 정책이 인용.
  • 완화: 7편 version=latest pre-filter, 또는 created_at 시간 boost(post-filter 단계의 sigmoid 가중).
  • 다음 편: 22편(답 검증), 7편 cross-ref.

8.5 Common Pitfalls

  • "Dense면 BM25 안 써도 된다." — 8.1절. 고유명사·코드 약함. Hybrid가 표준.
  • "top-K 점수의 절대값을 임계로." — 모델·코퍼스마다 다름. gap분포를 봐야.
  • "query와 doc 같은 함수로 임베딩." — 8편 8.2절. 비대칭 모델 사고 단골.
  • "한 번 학습된 모델은 도메인 무관 잘 작동." — 도메인 fine-tuning이 큰 차이. 7.2절.
  • "top-K가 5면 충분." — Reranker(13편)를 쓰려면 candidate 30~100을 가져와야.

9. 정리된 결론

Q1. Bi-encoder의 비대칭이 무엇이고 왜 중요한가?

\(E_q \ne E_d\). query와 doc의 입력 분포 차이를 학습. 같은 함수로 임베딩하면 벡터 공간이 어긋남 — recall 급락. Chapter: 4장, 7.1절, 8편 8.2절.

Q2. DPR의 InfoNCE 손실을 한 줄로 말하라.

정답 쌍의 유사도를 분자, 정답+모든 negative의 합을 분모로 한 log-likelihood. 정답을 가깝게, negatives를 멀게. Chapter: 5장.

Q3. top-K gap이 의미하는 것은?

검색의 확신도. gap이 크면 명확, 작으면 덤덤(쿼리 모호 또는 청크 큼). Chapter: 5장, 8.2절.

Q4. Dense의 5가지 한계를 두 단어씩으로 요약하라.

희귀 어휘, 덤덤한 top-K, 부정문 약함, 언어 불일치, 최근성 무시. Chapter: 8장.

Q5. Dense + BM25 결합이 표준 해법인 이유는?

Dense는 의미·유사어 강함, BM25는 정확 매칭·고유명사 강함. 서로 상보적인 약점을 채움. Chapter: 8.1절, 12편 cross-ref.


10. 추가 학습

1차 자료

  • Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP 2020. arXiv:2004.04906.
  • Khattab, O., Zaharia, M. ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. SIGIR 2020. arXiv:2004.12832.
  • Reimers, N., Gurevych, I. Sentence-BERT. EMNLP 2019. arXiv:1908.10084. (Bi-encoder 표준화)
  • Xiong, L. et al. Approximate Nearest Neighbor Negative Contrastive Learning (ANCE). ICLR 2021. arXiv:2007.00808. (hard negative 채굴)
  • Lewis, P. et al. Retrieval-Augmented Generation. NeurIPS 2020. arXiv:2005.11401. (RAG 원논문, Dense의 RAG 통합)

공식 docs

  • DPR (HuggingFace): https://huggingface.co/docs/transformers/model_doc/dpr
  • LangChain Retrievers: https://python.langchain.com/docs/concepts/retrievers/
  • ColBERT v2: https://github.com/stanford-futuredata/ColBERT
  • Sentence-Transformers: https://sbert.net/

보조 자료

  • 사용자 노트 9장 — Dense Retrieval.
  • 사용자 노트 35장 2절 — Model-aware Ingestion (비대칭 임베딩의 ingestion 함의).

Cheat Sheet

항목 표준값 / 권장
Top-K 5~50 (Reranker 사용 시 30~100)
유사도 정규화 + 코사인 (또는 정규화 후 내적)
Symmetric / Asymmetric 모델 카드 확인 — 분리 함수 호출
Negative in-batch + hard (fine-tuning 시)
Gap 진단 top-1 \(-\) top-10 \(\ge\) 0.1 권장
캐시 query 해시 → 임베딩. 모델 변경 시 invalidate
BM25 결합 거의 모든 프로덕션 — Hybrid가 기본 (12편)

원칙 한 줄: 의미 유사도는 Dense, 정확 매칭은 BM25, 결정타는 Reranker — 셋의 분업이 RAG의 표준 검색.


Bridge — 다음 편

다음 — RAG 핵심 학습 (11/26) Sparse Retrieval과 BM25 깊이 다이브.

Dense가 놓치는 고유명사·정확 매칭을 채우는 검색이 Sparse — TF-IDF, BM25(Robertson 2009), Inverted Index, 한국어 형태소 분석기까지. Elasticsearch/OpenSearch의 RAG 통합 패턴도 함께.

시리즈 전체 안내: 시리즈 목차

댓글

이 블로그의 인기 게시물

"LLM 핵심 학습 (1/6) — 기본: 토큰화·임베딩·어텐션·위치 인코딩"

"LLM 핵심 학습 (2/6) — 파인튜닝: LoRA·QLoRA·증류·Adapter"

"ML 기초 학습 (1/9) — 머신러닝과 sklearn: 학습의 좌표계"