"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. 학습 목표
- Bi-encoder 구조와 query·doc 비대칭을 식으로 안다.
- DPR의 in-batch negative sampling 원리를 안다.
- top-K 거리 분포로 검색 품질을 진단한다.
- 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.
반대 경우: "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_query와 embed_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=latestpre-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 통합 패턴도 함께.
시리즈 전체 안내: 시리즈 목차
댓글
댓글 쓰기