들어가며
검색 서비스를 운영하다 보면 한국어의 특성 때문에 고민이 깊어지는 순간이 있습니다. Dense + BM25 하이브리드 방식이 표준처럼 자리잡았지만, BM25의 근본적인 한계는 여전히 남아있습니다. 이 글에서는 BM25의 한계를 짚어보고, BGE-M3 Sparse가 어떻게 이를 보완할 수 있는지 MIRACL 한국어 벤치마크 실험 결과와 함께 공유합니다.
1. BM25 + 형태소 분석, 현재 우리의 선택
현재 많은 한국어 검색 시스템은 이런 구조를 가지고 있습니다:
사용자 쿼리 → (Kiwi) 형태소 분석 → BM25 스코어링 → 검색
왜 형태소 분석이 필수인가
한국어는 교착어입니다. "갤럭시를"에서 "갤럭시"를 찾으려면 조사 "를"을 분리해야 합니다. 형태소 분석 없이 BM25를 돌리면 조사에 따라 같은 단어가 앞에 있어도 찾을 수 없게 됩니다.
BM25 파이프라인
BM25 코드에서 핵심 로직을 살펴보면:
def tokenize_and_filter(self, text: str) -> str:
tokens = self.kiwi.tokenize(text)
# 유지할 품사: 명사, 고유명사, 한자 등
keep_tags = {"SH", "NNG", "NNP"}
# 불용어 제거 후 반환
return " ".join(out)
명사 중심으로 토큰을 추출하고 한국어 특성에 맞게 튜닝 및 불용어를 제거합니다.
2. BM25의 구조적 한계
BM25가 "단순히 단어 빈도만 체크한다"는 말은 정확하지 않습니다. 실제로는 TF(Term Frequency), IDF(Inverse Document Frequency), 문서 길이 정규화를 모두 고려합니다. 하지만 본질적인 한계는 명확합니다.
한계 1: 정확한 토큰 매칭만 가능
문서: "강남구 아파트 매매"
쿼리: "강남 부동산 구매"
BM25 결과: 매칭 실패 ❌
"강남구"와 "강남"은 다른 토큰입니다. "매매"와 "구매"도 마찬가지입니다. 형태소 분석기가 다르게 분리하면 검색이 안 됩니다.
한계 2: 형태소 분석 오류
문서: "SIO-AI8F 8ch 오디오 인터페이스"
Kiwi 분석: SIO | AI | 8 | F | 8 | ch | 오디오 | 인터페이스
문서: "갤럭시S24울트라"
Kiwi 분석: 갤럭시 | S | 24 | 울트라
상품명, 모델명 같은 복합어가 분리됩니다. "SIO-AI8F"는 하나의 제품 코드인데 여러 토큰으로 쪼개집니다. 이런 분리가 검색 품질을 떨어뜨립니다.
한계 3: 의미적 관계 이해 불가
문서: "우리나라 충주에서는 사과가 특산품입니다."
쿼리: "어느 지역의 apple이 맛있어?"
BM25 결과: 매칭 실패 ❌
"apple"과 "사과"는 같은 의미입니다. 하지만 BM25는 이를 전혀 알 수 없습니다. 영한 혼용 텍스트가 많은 서비스에서 이런 문제가 자주 발생합니다.
3. BGE-M3 Sparse
BM25의 한계를 인식하고 대안을 찾기 시작했습니다. 조건은 명확했습니다:
- 형태소 분석 의존성을 줄일 것
- 의미적 유사도를 어느 정도 파악할 것
- Dense 임베딩처럼 느리지 않을 것
여러 모델을 조사한 결과, BGE-M3의 Sparse가 눈에 들어왔습니다.
BGE-M3란?
BGE-M3는 BAAI에서 개발한 다목적 임베딩 모델입니다. 이름의 M3는 Multi-Linguality, Multi-Functionality, Multi-Granularity를 의미합니다. 특이한 점은 하나의 모델에서 Dense, Sparse, ColBERT 벡터를 동시에 추출할 수 있다는 것입니다.
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('BAAI/bge-m3')
output = model.encode(
texts,
return_dense=True, # Dense 벡터 (1024차원)
return_sparse=True, # Sparse 벡터 (학습된 term weight)
return_colbert_vecs=False
)
4. BGE-M3 Sparse의 장점
장점 1: 형태소 분석 불필요
BGE-M3는 자체 서브워드 토크나이저를 사용합니다. 한국어 형태소 분석이 필요 없습니다.
입력: "갤럭시S24울트라"
BGE-M3 내부: 서브워드 토크나이징 → 학습된 가중치 적용
→ 복합어가 그대로 의미 있는 벡터로 변환
상품명 "SIO-AI8F"도 통째로 처리됩니다. 형태소 분석기가 잘못 쪼개서 발생하는 오류가 원천 차단됩니다.
장점 2: 의미적 관계 인코딩
BGE-M3는 대규모 다국어 데이터로 학습되었습니다. 학습 과정에서 의미적 관계가 sparse 가중치에 반영됩니다.
"apple" 쿼리 → [사과, apple, 과일] 관련 토큰에 높은 가중치
"경기" 쿼리 → [대회, 시합, 게임] 관련 토큰에 높은 가중치
BM25로는 불가능한 동의어, 유사어 매칭이 어느 정도 가능해집니다.
장점 3: 다국어 지원
다국어가 지원되며, 혼용 사용시에도 의미적 유사도가 반영될 수도 있습니다.
문서: "Apple 아이폰 최신 모델"
쿼리: "애플 아이폰"
BM25: "Apple" ≠ "애플" → 매칭 실패 ❌
BGE-M3: 의미적 유사도 반영 → 매칭 성공 ✅
장점 4: 인덱스 증분 업데이트
BM25는 IDF 계산을 위해 전체 문서에 대해 fit()이 필요합니다. 새 문서가 추가되면 전체를 다시 계산해야 합니다.
BGE-M3는 사전 학습이 완료된 모델입니다.
5. 실험 설계
"정말 BGE-M3가 더 좋을까?"를 검증하기 위해 공정한 실험이 필요했습니다.
데이터셋: MIRACL Korean
MIRACL(Multilingual Information Retrieval Across a Continuum of Languages)은 18개 언어의 Wikipedia 기반 검색 벤치마크입니다. 한국어 데이터는 다음과 같이 구성됩니다:
구분 수량
| Corpus | 1,486,752 documents |
| Dev queries | 213 queries |
| 평가 지표 | nDCG@10 (primary), Recall@100, MRR |
비교 대상
- BM25 + Kiwi: 실제 서비스 코드의 tokenize_and_filter 로직 그대로 적용
- BGE-M3 Sparse: 형태소 분석 없이 원문 그대로 인코딩
중요한 실험 원칙
단순화된 BM25 구현으로 실험하면 의미가 없습니다. 실제 서비스와 동일한 전처리 파이프라인을 적용해야 합니다:
- 숫자+단위 결합 ("3" + "개" → "3개")
- 특정 품사(NNG, NNP, SH)만 유지
- 불용어 제거
이렇게 해야 "현재 시스템 대비 얼마나 개선되는가"를 정확히 측정할 수 있습니다.
6. 실험 결과
MIRACL Korean 전체 데이터(약 150만 문서)로 실험을 진행했습니다.
정량적 결과
지표 BM25 + Kiwi BGE-M3 Sparse 개선율
| nDCG@10 | 0.3825 | 0.6147 | +60.7% |
| Recall@100 | 0.7815 | 0.9006 | +15.2% |
| MRR | 0.4086 | 0.6742 | +65.0% |
모든 지표에서 BGE-M3 Sparse가 압도적으로 우수합니다.
결과 해석
nDCG@10 +60.7%: 상위 10개 결과의 순위 품질이 60% 이상 개선되었습니다. 사용자가 첫 페이지에서 원하는 문서를 찾을 확률이 크게 높아집니다.
Recall@100 +15.2%: Top-100에서 관련 문서를 찾아내는 비율이 78%에서 90%로 상승했습니다. 놓치는 문서가 줄어듭니다.
MRR +65.0%: 첫 번째 정답 문서의 평균 순위가 크게 개선되었습니다. 정답이 더 빨리 나타납니다.
기존 연구와의 비교
BGE-M3 논문에서 보고된 MIRACL 한국어 성능:
모델 nDCG@10
| BM25 (Lucene) | 37.1 |
| BGE-M3 Sparse | 61.5 |
| BGE-M3 Dense | 69.9 |
우리 실험의 BM25 + Kiwi(0.3825)는 Lucene BM25(37.1)와 유사한 수준입니다. BGE-M3 Sparse(0.6147)도 논문 결과(61.5)와 거의 일치합니다. 실험이 올바르게 수행되었음을 확인할 수 있습니다.
7. BGE-M3 Sparse의 한계
장점만 있는 것은 아닙니다.
속도
BM25 인코딩: ~400 docs/sec
BGE-M3 인코딩: ~150 docs/sec (GPU 사용 시)
BGE-M3가 약 2-3배 느립니다. 실시간 인코딩이 필요한 경우 고려가 필요합니다.
리소스
BGE-M3 모델 크기는 약 2GB입니다. GPU 메모리가 충분해야 합니다.
Sparse 벡터 크기
BGE-M3 Sparse 벡터는 문서당 200-800개 토큰을 포함합니다. BM25보다 저장 공간이 더 필요할 수 있습니다. 실험에서는 max_tokens=700으로 제한하여 사용했습니다.
8. 결론 및 다음 단계
결론
MIRACL 한국어 벤치마크에서 BGE-M3 Sparse는 BM25 + Kiwi 대비 nDCG@10 기준 60% 이상의 성능 향상을 보여주었습니다. 이 차이는 단순히 통계적 유의미성을 넘어, 사용자 경험에 실질적인 영향을 줄 수 있는 수준입니다.
특히 다음 상황에서 BGE-M3 Sparse가 효과적입니다:
- 상품명, 브랜드명 등 복합어가 많은 경우
- 동의어/유사어 매칭이 중요한 경우
- 영한 혼용 텍스트가 많은 경우
다음 단계
- Hybrid 실험: Dense + BGE-M3 Sparse 조합 테스트
- 도메인 특화 평가: 실제 서비스 데이터로 A/B 테스트
- dragonkue/BGE-m3-ko 테스트
BM25가 20년 넘게 검색의 표준이었습니다. 하지만 학습된 sparse 모델이 그 자리를 대체할 수 있다는 가능성이 보입니다. 특히 한국어처럼 형태소 분석이 필수인 언어에서 그 이점은 더욱 두드러집니다.
참고 자료
- BGE-M3 논문 - BGE-M3: Embedding Multi-Linguality, Multi-Functionality, Multi-Granularity in One
- MIRACL 데이터셋 - Multilingual Information Retrieval Across a Continuum of Languages
- FlagEmbedding GitHub - BGE-M3 공식 구현
'AI > NLP' 카테고리의 다른 글
| ModuleNotFoundError: No module named 'custom_st' (0) | 2024.11.29 |
|---|---|
| [NLP스터디] NLP에 필요한 한국어 문법 (0) | 2024.01.20 |
| [NLP스터디] 1주차 토큰화 (0) | 2024.01.19 |