이커머스 검색을 위한 희소 임베딩 파인튜닝 | 1부: 왜 희소 임베딩이 BM25를 이기는가

이커머스 검색을 위한 희소 임베딩 파인튜닝 | 1부: 왜 희소 임베딩이 BM25를 이기는가 (Fine-Tuning Sparse Embeddings for E-Commerce Search | Part 1: Why Sparse Embeddings Beat BM25)

"iPhone 15 Pro Max 256GB"라고 검색했는데 밀집(dense) 임베딩 시스템이 128GB 모델을 거뜬히 반환해 버려요. 의미론적으로는 같은 폰이니까 유사도가 높게 나오거든요. 이커머스에서는 이런 세부 사항이 "노이즈"가 아니라 핵심이에요. 이 글은 희소(sparse) 임베딩이 이런 문제를 어떻게 해결하고, 파인튜닝하면 BM25를 28%나 이길 수 있는지를 다루는 연재의 첫 편입니다.

출처: 공식문서

이커머스 검색을 위한 희소 임베딩 파인튜닝에 관한 5부작 시리즈의 1부입니다. "왜 해야 하지?"부터 BM25를 28% 이기는 프로덕션 시스템까지 다룰 거예요.

시리즈:


밀집 임베딩 시스템에서 "iPhone 15 Pro Max 256GB"를 검색하면 흔쾌히 128GB 모델을 반환해요. 의미적 유사도가 높으니까요 — 같은 폰이잖아요! 그런데 고객이 256GB를 고른 데는 이유가 있어요. 이커머스에서 디테일은 노이즈가 아니라, 그게 전부예요.

이것이 희소 임베딩이 메워 주는 공백이에요. 그리고 파인튜닝을 통해 그 공백을 극적으로 잘 메워 줍니다 — 우리는 가장 큰 공개 이커머스 검색 벤치마크 중 하나인 Amazon의 ESCI 데이터셋에서 BM25 대비 28% 개선을 달성했어요.

이 시리즈에서 우리는 전체 시스템을 구축할 거예요: 데이터 로딩, Modal에서의 GPU 훈련, Qdrant를 이용한 평가, 그리고 하드 네거티브 마이닝까지요. 전체 코드는 GitHub에 있고, 파인튜닝된 모델은 HuggingFace에 있습니다. 워크스루를 건너뛰고 자신의 데이터로 바로 파인튜닝하고 싶다면, sparse-finetune CLI가 한 줄의 명령으로 전체 파이프라인을 실행해요. 하지만 먼저, 왜 희소 임베딩이 이커머스 검색의 올바른 도구인지 이해해 봅시다.

이커머스에서 밀집 임베딩의 문제

밀집 임베딩(OpenAI, Cohere, 또는 파인튜닝된 sentence transformer에서 얻는 그런 것)은 텍스트를 고정 크기 벡터로 압축하는데, 보통 384~1536 차원이고 전부 0이 아니에요. 의미를 포착하는 데는 탁월하죠. "Running shoes"와 "jogging sneakers"는 임베딩 공간에서 가까이 자리하게 됩니다.

하지만 이 강점은 이커머스에서는 약점이 돼요.

정확한 매칭이 흐려진다. 모든 차원이 0이 아닌 값을 가질 때, 모델은 정확한 용어 매칭보다 광범위한 의미적 유사성을 우선시해요. SKU 번호, 모델명, 특정 사이즈 같은 중요한 차별 요소들이 유사하지만 틀린 제품들과 같은 이웃으로 평균화되어 버립니다.

검색이 근사(approximate)다. 규모가 커지면 밀집 벡터는 HNSW 같은 ANN(Approximate Nearest Neighbor) 인덱스를 요구해요. "근사"라는 말은 재현율을 속도와 맞바꾼다는 뜻이에요. 관련 제품을 놓치면 매출 손실로 이어지는 검색에서 이 트레이드오프는 아픕니다.

결과가 불투명하다. 왜 제품 X가 제품 Y보다 위에 랭크됐을까요? 밀집 임베딩으로는 말할 수 없어요. 768차원 벡터는 해석 가능성이 전혀 없어요. 마케팅 팀이 왜 제품이 안 나오냐고 물으면 막막해지죠.

희소 임베딩의 등장

희소 임베딩은 근본적으로 다른 접근 방식을 택해요. 텍스트를 작고 밀집한 벡터로 압축하는 대신, 큰 어휘 공간(보통 30,000+ 차원, 어휘의 토큰 하나당 하나)에 텍스트를 투영해요. 하지만 그중 100~300개만 0이 아닌 값을 가집니다.

Dense Sparse
Vector size 384-1536 dims ~30,000 dims
Non-zero values All dimensions carry a value 100-300 terms
Index type ANN (HNSW) Inverted index
Exact matching Weak Strong
Interpretability Black box Per-term weights

두 접근 모두 텍스트를 벡터로 인코딩하지만, 희소 임베딩은 밀집 모델이 압축해 버리는 개별 용어 시그널을 보존해요.

핵심 차이: 희소 벡터의 각 차원은 어휘의 실제 단어에 대응해요. 벡터를 들여다보면 모델이 어떤 용어를 중요하게 여기는지, 각각에 얼마나 가중치를 주는지 정확히 확인할 수 있어요.

SPLADE: 학습된 희소 표현

SPLADE(Sparse Lexical and Expansion)는 이것을 가능하게 하는 모델 아키텍처예요. 텍스트를 마스크 언어 모델(MLM) 헤드를 가진 트랜스포머에 통과시킨 뒤, 로그 포화(log saturation)와 맥스 풀링을 적용해 희소 가중치를 만들어 냅니다.

"noise canceling headphones" 같은 입력에 대해 SPLADE는 네 단계로 인코딩해요.

  1. 토큰화하고 인코딩 — 마스크 언어 모델(MLM) 헤드를 가진 DistilBERT로 입력을 통과시킴
  2. 로그 포화 적용log(1 + ReLU(x)), BM25의 포화 곡선의 학습된 버전으로 어떤 단일 용어도 지배하지 못하게 함
  3. 전체 토큰 위치에 걸쳐 맥스 풀링 — 어휘 용어별 단일 점수를 얻음
  4. 희소 벡터 출력 — 30,522개의 어휘 차원 중 약 200개의 0이 아닌 값
Token Weight
headphones 2.3
noise 1.9
canceling 1.7
audio 1.2
wireless 0.8
sound 0.6

로그 포화(log saturation) 단계는 중요해요. 없으면 단일 고신뢰 용어가 점수를 지배할 수 있어요. 로그 압축은 결과를 균형 있게 유지합니다 — "headphones"는 "audio"보다 중요하지만, 10배 더 중요하진 않아요.

모델은 세 가지를 동시에 학습합니다.

  1. 중요한 용어에 더 높은 가중치 부여 (제품명, 핵심 속성)
  2. 관련 용어로 질의 확장 (암묵적 동의어 확장)
  3. 노이즈 억제 (흔한 단어는 거의 0에 가까운 가중치)

질의 확장: 왜 SPLADE가 BM25를 이기는가

이 확장이 SPLADE를 전통적인 키워드 검색과 구분 짓는 부분이에요. BM25는 질의와 문서에 문자 그대로 둘 다 등장하는 용어만 매칭할 수 있어요. SPLADE는 훈련 데이터에서 모델이 학습한 관련 용어를 추가해요.

질의: "summer dress"

원래 용어: dress (2.5), summer (2.1)

SPLADE가 확장한 것: sundress (1.8), floral (0.9), lightweight (0.7), cotton (0.6)

모델은 "summer"가 없어도 제품 제목에 등장하는 "sundress", "floral", "cotton"을 추가해요. 이는 "summer"가 전혀 없어서 BM25가 완전히 놓치는 "Floral Sundress for Women - Lightweight Cotton" 같은 제품과 매칭됩니다.

수동 동의어 파일도 없고, 질의 재작성 규칙도 없어요. 모델은 수백만 개의 질의-제품 쌍을 보며 이 연관성들을 학습했어요.

왜 희소 벡터에 Qdrant인가?

모든 벡터 데이터베이스가 희소 벡터를 일급 시민으로 취급하진 않아요. Qdrant는 그렇게 하고, 그 차이는 실제로 중요합니다.

가중치가 있는 희소 벡터. SPLADE는 임의의 학습된 가중치를 출력하고, Qdrant는 그것을 네이티브로 저장해요. BM25를 베이스라인으로 쓰고 있다면 질의 시점에 IDF를 추가할 수도 있어요.

sparse_vectors_config={
    "bm25": models.SparseVectorParams(
        modifier=models.Modifier.IDF,  # Apply IDF at query time
    )
}

하나의 요청으로 하이브리드. 네이티브 RRF/prefetch를 통해 희소의 정밀함과 밀집의 의미를 결합할 수 있어요 — 외부 재랭커도 필요 없어요.

client.query_points(
    collection_name="products",
    prefetch=[
        models.Prefetch(query=sparse_vector, using="sparse", limit=100),
        models.Prefetch(query=dense_vector, using="dense", limit=100),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    limit=10,
)

프로덕션 규모 확장. Rust + SIMD 최적화된 역인덱스(inverted index)에 온디스크 옵션이 있어, 수백만 개 제품에 걸쳐 문서당 200개 이상의 활성 용어에서도 RAM을 낮게 유지해요.

ANN 근사 없음. 희소 검색은 역인덱스를 사용하는데, BM25를 구동하는 것과 같은 데이터 구조예요. 결과는 정확합니다 — 근사 최근접 이웃 검색의 재현율 트레이드오프가 없어요.

스택

우리 훈련 파이프라인은 세 가지 구성 요소를 결합해요.

Modal (GPU 훈련)

  • 온디맨드 A100 GPU
  • 체크포인트용 영구 볼륨
  • 장기 훈련용 분리(detached) 실행

Sentence Transformers v5 (훈련 프레임워크)

  • SparseEncoder 아키텍처
  • 정규화를 포함한 SpladeLoss
  • 내장 훈련 유틸리티

Qdrant (희소 벡터 저장소)

  • 네이티브 희소 벡터 지원
  • 역인덱스
  • 하이브리드 검색 준비

Modal은 서버리스 A100 GPU를 제공해요 — 유휴 하드웨어도, 큐 관리도 없어요. Sentence Transformers v5는 SPLADE 훈련을 간단하게 만드는 SparseEncoder 클래스를 도입했고요. Qdrant는 네이티브 희소 벡터 지원으로 저장·인덱싱·검색을 처리해요.

우리가 만들 것

다음 네 편의 글에서 전체 파이프라인을 살펴볼 거예요.

  • Part 2: Modal에서 훈련하기 — Amazon ESCI 데이터셋 로딩, SPLADE 모델 생성, 희소성 정규화를 포함한 손실 함수 구성, 영구 체크포인트를 사용한 GPU 훈련 실행.

  • Part 3: 평가와 하드 네거티브 마이닝 — Qdrant에 제품 인덱싱, 검색 벤치마크 실행(nDCG, MRR, Recall), ANCE에서 착안한 하드 네거티브 마이닝 루프 구현, 파인튜닝이 모델에서 실제로 무엇을 바꾸는지 분석.

  • Part 4: 전문화 vs 일반화 — Wayfair과 Home Depot 데이터에 대한 교차 도메인 평가, 다중 도메인 훈련, 언제 전문화하고 언제 일반화할지, 프로덕션 배포 지침.

  • Part 5: 연구에서 제품으로 — 한 줄의 명령으로 전체 파인튜닝 파이프라인을 실행하는 오픈소스 CLI와 웹 대시보드.

최종 결과: Amazon ESCI에서 nDCG@10 0.389를 달성한 파인튜닝된 SPLADE 모델. BM25는 0.305, 바로 사용하는(off-the-shelf) SPLADE는 0.326이었어요. BM25 대비 그 28% 개선은 실제 이커머스 질의에서 의미 있게 더 나은 검색 결과로 이어져요. 모델을 HuggingFace에서 직접 써 볼 수 있어요: splade-ecommerce-esci (도메인 내 최고)와 splade-ecommerce-multidomain (더 나은 일반화).

참고: 이 지표들은 모든 관련 문서가 포함된 10만 개 제품과 1만 개 질의의 하위 표본에서 측정된 것이에요. 공식 Amazon ESCI 벤치마크와 직접 비교할 수 없으며, 비교적 신호(comparative signal)로만 취급해야 해요.

더 알아보기 (Learn more)