[번역] RAG 세상을 헤엄치는 사람들을 위한 가이드북

Guidebook to the State-of-the-Art Embeddings and Information Retrieval

Sigrid Jin
45 min readDec 30, 2024

역자 주) 해당 아티클은 아래 링크드인 글을 번역한 것입니다.

정보 검색은 역사가 깊은 학문 분야입니다. 예를 들어 1945년에 출간된 Vannevar Bush의 As We May Think을 읽어보시면 흥미로운 통찰을 얻을 수 있을 것입니다. 저 또한 수십 년의 경험이 있는 것은 아니지만, 지난 해(2023년)부터 본격화된 RAG(Retrieval Augmented Generation)와 텍스트 임베딩 붐이 종종 저조차도 “조금 올드하면서도 까칠한 사람이 되어버린 건가?” 하는 생각이 들 정도였습니다.

다행히도 2023년 초부터 Colin Harman의 Beware Tunnel Vision in AI Retrieval 같은 뛰어난 블로그 글들이 이미 여러 중요한 문제들을 더 잘 짚어주었죠. 그래서 이번에는 하이프에 대해 투덜거리는 대신, 제게도 흥미롭고 여러분께도 흥미로울 만한 최근 실험들을 공유하려 합니다.

저는 ColBERT (Khattab & Zaharia, 2020) 관련 연구·개발을 몇 년간 지켜보고 드디어 실제로 적용해볼 기회를 얻었습니다. ColBERT가 왜 흥미로운지는 이 가이드북 후반부에서 자세히 다루겠습니다. Vespa라는 오픈소스 검색엔진 겸 벡터 데이터베이스 역시 마찬가지인데, 제 생각에 여전히 이 ColBERT 스타일 검색을 네이티브로 지원하는 유일한 툴이라고 봅니다. 그래서 최근 실험의 주요 목표는 다음과 같았습니다:

  1. ColBERT를 사용해보고, 성능을 평가하기
  2. Vespa를 사용해보고, 어떻게 작동하는지 배워보기

물론 진행하다 보니 살짝 과하게 파고들게 되었고, 이 가이드북도 좀 긴 편입니다. 따라서 한 잔 이상의 커피를 준비하시는 것이 좋을 것 같습니다.

추가적으로 다룰 주제들은 대략 아래와 같습니다.

  • 임베딩과 그 일반화 가능성(Generalizability)에 대한 논의
  • 인간과 + LLM을 활용한 데이터셋 구축 및 라벨링
  • 17가지 검색 모델 평가
  • 긴 컨텍스트 임베딩 모델을 사용할 때, ‘청크(chunk)를 나눌 것인가 말 것인가’에 대한 결정
  • 하이브리드 검색 및 리-랭킹(Re-ranking)
  • 상용 SaaS 검색 서비스 평가
  • 임베딩 모델 파인튜닝
  • 임베딩 모델 및 벡터 검색 최적화
  • 해석 가능한(Interpretable) 신경망 검색 구현

결국 임베딩과 정보 검색 분야에 대한 이론과 실증을 결합한, 약간은 주관적이지만 경험적·이론적으로 뒷받침된 가이드북 이라고 보시면 됩니다.

임베딩(Embedding)과 그 일반화 가능성(Generalizability)

먼저 검색 실험으로 넘어가기 전에, 임베딩이 무엇인지 간단히 되짚어보겠습니다. 짧게 요약하자면, Roy Keyes의 정의를 인용하겠습니다:

“임베딩(Embeddings)이란 데이터를 더 유용하게 만들기 위한 학습된 변환(Transformation)이다.”
— Roy Keyes (임베딩에 대한 가장 짧은 정의?)

즉, 텍스트, 이미지, 음성 등 다양한 형태의 데이터를 검색, 군집화, 분류 등에 유용하게 만들기 위해, 원하는 핵심 정보를 압축적으로 담을 수 있는 수치 벡터 표현으로 변환하는 과정을 학습하는 것입니다. 그리고 Jo Kristian Bergum 또한 이렇게 말합니다:

“이 표현(Representation)이 얼마나 유용한지는 우리가 변환(Transformation)을 어떻게 학습했느냐, 그리고 새 데이터로 일반화가 얼마나 잘 되느냐에 달려 있다.”
— Jo Kristian Bergum (Three mistakes when introducing embeddings and vector search)

이는 통계학 입문 수준에서나 다룰 법한 기본 개념처럼 들리지만, 회사의 특정 도메인 데이터에 임베딩을 활용한 검색 시스템을 구축할 때 매우 중요한 부분입니다. 사실 트랜스포머 기반 단일 임베딩 모델은, 그 학습 데이터 도메인 범위를 벗어나면 성능이 상당히 저하됩니다. 예를 들어 BEIR 벤치마크와 논문(Thakur et al. 2021)에서 다음과 같은 결론을 얻었습니다:

“인도메인(In-domain) 성능은 도메인을 벗어나는(out-of-domain) 일반화 성능의 지표가 되지 못한다. … BEIR 테스트 결과에 따르면 BM25가 강력한 베이스라인으로, 많은 복잡한 접근법(단일 임베딩 모델들)보다 일반화 성능이 더 우수하다. … 예를 들어, (단일 임베딩 모델 기반) 밀집(dense) 검색 기법은 훈련 당시와 전혀 다른 도메인을 가진 데이터셋에서 상당히 부진한 모습을 보였다.”

지난해 RAG 붐이 일어났을 때, 꽤 많은 사람들이 “우리 도메인용 임베딩 학습을 다른 회사 API(예: OpenAI Embedding API)에 외주로 맡기면 문제가 없을까?”라는 고민을 충분히 했을지는 잘 모르겠습니다. BM25 같은 전통적 검색 기법은 여전히 강력한 베이스라인이 되고, 임베딩 기반 검색과 결합해 시너지를 내는 형태로 사용하기 좋습니다. 여러 공용 벤치마크에서도 BM25가 우수한 성능을 보여주며, 이 글에서 소개할 실험에서도 비슷한 결과를 확인할 수 있습니다.

단일 임베딩 모델의 낮은 일반화 성능과 하이브리드 검색 시스템의 이점은 2021년에 읽었던 Gao et al. (2021) 의 논문에도 잘 나와 있습니다. 당시에 제가 고객사 프로젝트에서 BM25와 단일 임베딩 모델을 혼합한 하이브리드 검색 방식을 구현한 이유 중 하나였죠. 또 다른 이유로는, 종종 특정 제품명, 약어, 줄임말, 코드베이스 함수명, 키워드 등을 정확히 검색해야 하는 상황 때문입니다. 임베딩 모델의 토크나이저(tokenizer)에 해당 토큰이 없을 수도 있고, 임베딩 검색은 늘 어느 정도 부정확성이 섞여 있기 때문에, 이런 케이스에는 오히려 전통적인 BM25가 정확도나 연산 효율 면에서 훨씬 낫습니다.

“검색창에 쿼리를 입력할 때, 사용자는 사실 그 쿼리 그대로를 ‘정확히’ 문서 안에서 찾아내기를 원할 수도 있습니다. 단순히 그와 관련된 비슷한 맥락을 찾아주는 게 아니라 ‘정확한’ 텍스트를 찾고 싶은 것이죠.”
— Doug Turnbull (The other hard retrieval problems)

BM25는 여전히 훌륭합니다. 그렇다면 단일 임베딩 모델을 더 개선할 수 있는 방법은 무엇일까요? 우선, 벤더(OpenAI 등)에 전적으로 외주하지 말고, 오픈소스 단일 임베딩 모델을 직접 파인튜닝(fine-tuning)하여 우리 도메인에 맞추는 방법이 있습니다. Sentence-Transformers(초기 SBERT)를 비롯해 파인튜닝 라이브러리도 최근 몇 년간 크게 발전하여, 지금은 파인튜닝이 훨씬 수월해졌습니다. 임베딩 모델은 방대한(수십~수백 억 개) 파라미터가 필요한 거대 언어 모델(LLM)과 달리 비교적 가벼워서, 파인튜닝 비용도 저렴합니다. 저도 여러 고객사 프로젝트에서 단일 임베딩 모델을 파인튜닝했고, 도메인 특화성과 유용성이 크게 올라가는 걸 여러 차례 직접 확인했습니다.

다만 최근 들어선 모델 자체가 점차 거대해지고, 다양한 도메인의 방대한 데이터로 사전 학습 및 미세 조정이 이루어지는 추세여서, 오픈소스 모델 그대로도 옛날보다 훨씬 높은 성능을 발휘하는 편입니다. 예를 들어 E5나 BGE 모델들은 “수십~수백 억 규모의 텍스트 쌍을 여러 다양한 도메인에서 약하게(weak) 정답 표시된(학습) 데이터로 먼저 ‘프리트레인(pre-train)’하고, 그 후 여러 정답 라벨이 있는 데이터셋들로 파인튜닝한다”(Wang et al. 2023)는 식입니다. 그럼에도 “완전히 새로운 특정 도메인”으로 이동하면 여전히 문제가 생길 수 있으므로, 도메인 평가와 검증은 반드시 해보셔야 합니다.

ColBERT란 무엇인가?

앞서 이야기했듯이, 단일 임베딩 모델은 도메인을 벗어나면 일반화가 쉽지 않습니다. 그런데 ColBERT는 다른 접근법입니다. Santhanam et al.(2021) 연구에 따르면, 단일 임베딩 모델보다 도메인 내·외부 모두에서 더 우수한 성능을 보였습니다. 핵심은, ColBERT가 긴 텍스트를 하나의 벡터로 압축하는 것이 아니라, 텍스트의 각 토큰마다 임베딩을 생성한다는 점에 있습니다. 쿼리-문서 간 유사도는 문서 내 토큰 임베딩들을 쿼리 내 각 토큰 임베딩과 비교해 가장 높은 토큰 유사도들을 합산(MaxSim 연산)하여 계산합니다.

이른바 “컨텍스트 기반 지연 상호작용(contextual late interaction)” 방식이라 불리는 이 토큰 단위 임베딩 기법은 도메인 외부 검색에서도 더 우수한 표현력을 제공합니다. 또한 향후 자세히 다룰 해석 가능한 신경망 검색(Interpretable Neural Retrieval) 구현에도 중요한 기반이 됩니다. ColBERT와 MaxSim 연산을 직관적으로 설명한 시각 자료는 Jo Kristian Bergum의 블로그나 오리지널 ColBERT 논문들을 참고하시면 좋습니다.

단점은 ColBERT가 토큰별 벡터를 생성한다는 점으로, 저장해야 할 벡터 양이 기하급수적으로 늘어난다는 것입니다. 이런 경우, 저장 공간이나 계산 비용(MaxSim 계산)이 매우 커져서 대규모 실환경 시스템에서는 부담이 됩니다. ColBERTv2(Santhanam et al. 2021)나 PLAID 검색 엔진(Santhanam et al. 2022)은 ColBERT 임베딩 압축과 군집화를 통한 지연 상호작용(Pruned Late Interaction)을 적용해 ColBERT를 빠르고 효율적으로 만들었습니다. 하지만 실제 대규모 서비스에서는 문서가 실시간으로 계속 바뀔 수 있기 때문에, 매번 대량 일괄 클러스터링을 적용하기 쉽지 않습니다. 그래서 ColBERT를 이용할 때는, PLAID 같은 복잡한 방식을 쓰기보다는 보통 이미 1차로 검색된 소수의 후보 문서를 재랭킹(Re-rank)하는 용도로 더 많이 쓰기도 합니다.

또 하나의 단점은 ColBERT를 프로덕션 용도로 다루는 생태계가 아직 많지 않다는 점입니다. Vespa가 ColBERT 스타일 검색을 네이티브로 지원하는 사실상 유일한 검색 엔진·벡터 DB라고 하는데, 문서 내 토큰 임베딩 행렬을 저장하고 이를 텐서(tensor) 연산으로 MaxSim에 활용할 수 있는 기능을 갖추고 있기 때문입니다. 2021년부터 이미 ColBERT 관련 내용을 공개적으로 다루고 있을 정도로, 해당 분야에서 선구적인 리더십을 보여주고 있습니다.

벡터 데이터베이스

제가 맡았던 고객사 프로젝트들은 Elasticsearch, OpenSearch, PostgreSQL+pgvector 확장 등을 활용했으며, 때로는 NumPy 배열에 벡터를 직접 저장하여 NumPy의 dot product로 검색하는 방식만으로 충분할 때도 있었습니다.

일반적으로 규모가 큰 고객들은 이미 Elasticsearch같은 제품에 투자를 많이 해놓았고, 대부분 꽤 기업 환경에 성숙하게 적용되어 있습니다. 그런데 단지 “벡터만 다루는” 목적 하나 때문에, 새로 유행하는 벡터 DB를 추가로 들여오는 것은 아키텍처적으로 불필요한 복잡도를 야기합니다.

무엇보다 “고성능 벡터 검색” 자체는 이미 많은 도구로 해결 가능하고, 실제 ‘현실 세계의 검색 문제’는 훨씬 복합적입니다. 예컨대 여러 검색 기법을 하이브리드로 결합하거나, 다양한 메타데이터 필터링·정렬, 다단계 랭킹, 문서 당 여러 벡터(예: 청크별) 인덱싱(메타데이터를 중복 인덱싱하지 않고 함께 관리) 등이 필요해집니다. 작년에 한창 핫했던 ‘AI 네이티브’ 벡터 DB(예: Pinecone, Chroma DB 등)는 이러한 면에서 아직 기능이 턱없이 제한적이라, 개인적으로 큰 흥미가 없습니다.

“그리고 벡터 검색 자체가 점점 큰 문제가 아니게 되어 가고 있습니다. 현실의 어려운 검색 문제는 단순히 ‘벡터를 어떻게 불러오느냐’가 아니라 그 주위 전반적인 로직입니다.”
— Doug Turnbull (Are we at peak vector database?)

Vespa는 어떨까요? 검색 및 추천 시스템에 특화된 오픈소스 솔루션인데, Yahoo로부터 분사된 회사에서 계속 개발하고 있습니다. Elasticsearch나 OpenSearch와 유사해 보이지만, 벡터나 텐서(다차원 행렬) 연산에 훨씬 더 강력하고 확장성 있게 대응하도록 설계되어 있습니다. 예를 들어 ColBERT에서 필요한 토큰 임베딩 행렬을 저장하고, 이를 효율적으로 랭킹 계산에 활용할 수 있죠.

아직 Vespa를 실제 대규모 프로덕션 환경에선 적용해보지 않았지만, 이미 Yahoo, Spotify R&D, OTTO.de 등에서 수십억 규모 데이터를 다루는 서비스로 운영 중이고, 싱가포르 정부나 Marqo.ai 같은 곳에서도 Vespa를 도입했다고 합니다. 다만 Vespa 역시 신기능(트랜스포머 임베딩 모델을 Vespa 노드 내에서 실행하는 등)에서 간혹 예기치 않은 문제 보고가 있는 것으로 알고 있고, 사용법도 초기 러닝 커브가 좀 있습니다.

제가 로컬 환경에서 사용해 본 바로는, 분명 배울 것이 많지만 강력한 기능을 갖춘 “야수(beast)” 같은 도구라고 느꼈습니다. 스키마(schema)에 새 텐서 필드를 손쉽게 추가하여 중복 없이 인덱싱된 문서에 결합할 수 있고, 새로운 랭킹 함수를 간단히 정의할 수도 있더군요. 이 덕에 원래보다 훨씬 더 많은 임베딩 모델을 실험하게 되었습니다.

실험용 데이터셋

일반적으로 공개된 일반 데이터셋을 사용해볼 수도 있었으나, 일상적인 업무 맥락에 맞춰 제 스스로 원하는 형태로 데이터를 구성하고 싶었습니다. 그래서 꽤 많은 분들이 이미 RAG 실험용으로 해왔듯, 저도 Thoughtworks.com의 블로그, 아티클, 고객 사례(이하 통칭 ‘아티클’)를 스크래이핑하여 데이터셋으로 만들었습니다. Python의 requests, beautifulsoup4 라이브러리, 그리고 datasets 라이브러리를 활용해, 효율적인 Apache Arrow 기반 데이터셋을 구성했습니다.

Thoughtworks.com에서 추출한 텍스트 본문 외에도, URL, 제목, 저자, 발행일, 1차/2차 주제, 콘텐츠 유형(블로그/아티클/고객 사례), 태그 등 검색 메타데이터로 쓸 만한 정보를 추가로 수집했습니다. 원본이 HTML 형식이므로, Python용 “html2md”를 써서 마크다운으로 변환하고, 비어 있는 헤더나 하이퍼링크, 이미지 등을 간단히 제거하는 전처리를 수행했습니다.

아래 그림(원문에 제시)에서 보이듯, Thoughtworks.com의 많은 글들은 길이가 상당히 깁니다. 트랜스포머 기반 임베딩 모델 대부분은 전통적으로 512 토큰 제한이 있었습니다. 그래서 보통 긴 본문을 여러 청크(chunk) 로 잘라야 합니다. 물론 일부 최신 모델은 훨씬 큰 컨텍스트 윈도우(수천~수만 토큰)를 지원하지만, 실험 결과를 보면 긴 문맥을 그냥 한 번에 넣으면 검색 성능이 상당히 떨어집니다. (자세한 내용은 뒤에 “긴 컨텍스트 단일 임베딩 모델을 사용할 때 청크를 나눌 것인가?” 섹션에서 실험으로 확인합니다.) 또한, 특정 부분만 검색 결과로 찾아야 하는 경우에도 청크 단위 인덱싱이 유용하기 때문에, 청크 작업은 그리 달갑지 않지만 아직은 피할 수 없는 현실입니다.

저는 마크다운 형태에서 적절한 헤더 구조를 유지하도록 청크를 나누는 방식으로 구현했습니다. 만약 헤더가 없는 텍스트라면, 단락(paragraph) 기반으로 구분하고, 각 청크를 최대 512 토큰 이내로 제한하는 식입니다. 완벽한 방법은 아니지만, 다양한 형식의 글이 섞여 있는 실험용 데이터셋에서는 충분하다고 판단했습니다.

그 결과, 아티클 2678개와 청크 12069개가 생성되었습니다. 실제 대규모 프로덕션 환경에서는 수백만~수십억 건을 다루는 사례가 많지만, 여기서는 방법론이 주안점이므로 이 정도면 충분합니다.

아래는 데이터셋과 Thoughtworks.com 아티클 관련 통계치입니다.

  • 가장 오래된 글: 2008년 6월 24일, Ross Pettit의 “Metric-Driven Management Versus Management-Driven Metrics”
  • 가장 최신 글: 2024년 1월 30일(4개 존재)
  • 가장 긴 글: “Introducing Agile Analytics” (단어 수 8283)
  • 가장 짧은 글: “Sustain with me” (단어 수 1, “Test”)
  • 저자 수: 총 1221명
  • 저자별 글 최다 집필 Top 5: Mike Mason(43개), Elena Yatzeck(37개), Dan McClure(26개), Jim Highsmith(23개), Sriram Narayan(23개)
  • 1차 주제 Top 5: Digital innovation(212개), Digital transformation(201개), Diversity, equity and inclusion(179개), Agile project management(178개), Careers at Thoughtworks(138개)
  • 콘텐츠 유형별 분포: Blogs(2209개), Client stories(217개), Articles(167개), Publications(35개)

실험용 데이터셋 라벨링

스크래이핑 및 전처리, 청크화까지 완료했지만, 검색 실험을 위해서는 검색 쿼리(query)가 필요합니다. 간단한 키워드 쿼리부터 자연어 질문(Natural language questions)형 쿼리까지 다양합니다. 저는 이번 실험에서 자연어 “질문형 쿼리”에 집중하기로 했습니다. 이는 RAG 시스템 등에서 많이 쓰이는 형태이기도 합니다.

데이터셋을 train, validation, test로 나눈 뒤(각각 2102, 526, 50개 아티클), 실제 Thoughtworks.com에서 입력된 검색 쿼리를 갖고 있지 않았으므로, 옵션은 다음 세 가지였죠:

  1. 수작업으로 질문 생성
  2. Amazon Mechanical Turk 같은 크라우드 소싱 서비스로 질문 생성
  3. LLM으로 자연스럽고 그럴듯한 질문 자동 생성

저는 1번과 3번을 섞었습니다. 즉, train/validation용은 LLM이 만든 합성(synthetic) 질문을, test용은 직접 사람이 만든 질문을 쓰기로 했습니다. 그 대신, test용에도 LLM이 만든 질문을 추가 생성해 두어, 인간 질문 vs. LLM 질문을 비교하는 미니 실험도 해볼 수 있도록 했습니다.

수작업 라벨링에는 오픈소스 툴 Argilla를 사용했습니다. Argilla는 텍스트-투-텍스트(T2T) 라벨링 UI를 지원합니다. 청크별 검색 성능을 평가하기 위해, 저는 각 청크마다 한 개씩 질문을 만들었고, 그 결과 50개 아티클, 204개 청크에 대한 질문 204개가 생성되었습니다. 청크당 여러 질문을 만들 수도 있었겠지만, 수작업 부담을 줄이기 위해 1:1 매핑으로 제한했습니다. 총 1~1.5일 정도 걸렸는데, 정확한 시간은 기록하지 않았습니다.

이 방식을 train/validation 세트에 그대로 적용하려면 50일 이상 걸릴 텐데, 현실적으로 어렵고 정신 건강에도 안 좋았을 겁니다. 그래서 LLM이 필요했죠.
LLM을 활용해 검색용 쿼리(질문)를 생성하는 것은 새롭지 않습니다. 사실 2019년 경부터도, encoder-decoder 모델을 써서 합성 쿼리를 만드는 시도(Nogueira et al. 2019)가 있었는데, 현재 LLM들은 훨씬 강력해졌습니다. Thoughtworks에서도 2023년에 한 고객 프로젝트에서 이런 LLM 생성 쿼리를 활용해 임베딩 모델 파인튜닝을 했습니다.

저는 가능한 오픈소스 LLM만 사용해보고 싶었고, 소규모 LLM(7B 파라미터급)이 이런 작업에서 얼마나 잘해줄지 궁금했습니다. 그래서 7B 파라미터 규모이면서 Apache 2.0 라이선스를 갖춘 Mistral-7B-Instruct-v0.2 모델을 활용했습니다. Google Gemma 7B-it, Qwen 1.5–7B-Chat 등도 시도해봤지만, 결과 품질이 크게 다르지 않았습니다. Mixtral-8x7B라는 더 큰 모델도 잠시 써봤지만, 질문 생성 품질이 크게 나아지진 않았고, 무엇보다 7B 모델이 GPU 메모리를 훨씬 덜 차지해 비용이 낮으므로 7B에 정착했습니다.

프롬프트 엔지니어링을 오래 하진 않았습니다. 어차피 이 부분이 이번 실험의 핵심이 아니었고, 자동화(DSPy)로도 가능해질 것 같아서요. 아래는 Mistral에 사용했던 프롬프트 템플릿 예시입니다. 최대 3개 질문을 만들도록 하고, 질문은 15단어 이하로 제약했으며, 주어진 컨텍스트 내용 안에서만 유의미한 질문을 만들어내도록 지시했습니다.

[INST] You are a natural language question generator. Generate up to three questions based only on the given context and not your prior knowledge. Generated questions should be diverse in nature across the context. Generated questions must be less than 15 words long, keep them rather concise! Do not add any question numbering or other artifacts, only generate the question itself! Generated questions should revolve around the core content and topics of the given context, and they can refer to the content with relevant synonyms, not only with exactly matching words. Do not invent new facts outside the given context!
Context: "{context}" [/INST]

또한, 자연어 ‘질문형 쿼리’뿐 아니라, 일반적인 검색엔진 스타일(구글 검색 형태)의 짧은 쿼리도 생성하도록 별도 프롬프트를 사용했습니다. 이렇게 하면 질문형뿐 아니라 키워드형 쿼리에도 모델을 파인튜닝할 수 있을 테니까요.

[INST] You are a natural language google search query generator. Generate up to three specific and diverse search queries based only on the given context and not your prior knowledge. Generated search queries must be less than 10 words long, keep them short! Generated search queries should revolve around the core content and topics of the given context, and they can refer to the content with relevant synonyms, not only with exactly matching words. Do not invent new facts outside the given context!
Context: "{context}" [/INST]

Outlines라는 오픈소스 라이브러리를 사용해 위 프롬프트 응답을 Pydantic 스키마 형태로 쉽게 파싱했습니다. Outlines가 ExLlamaV2 엔진과 연동되어 모델 추론 및 양자화(Quantization)도 쉽게 할 수 있었고, 이를 통해 Nvidia T4 GPU(16GB)에 8비트 양자화된 Mistral 7B 모델을 올려 약 22시간 돌렸습니다(비용 약 12달러).

이렇게 해서 최종 생성된 합성 질문 및 쿼리 개수는:

  • Train용: 질문 28505개, 쿼리 29374개
  • Validation용: 질문 7224개, 쿼리 7494개
  • Test용: 질문 626개, 쿼리 648개

총합 약 57,000개 이상의 질문약 37,500개 이상의 검색 쿼리가 생성된 셈이죠.

예시로, 아래는 Test용 청크 한 부분과 해당 청크에 대한 사람이 작성한 질문, 그리고 LLM이 생성한 질문/검색 쿼리입니다.

#### Key takeaways Low code adoption is growing, but it's still far better suited to some development scenarios than others- Low code has been discussed as an alternative to traditional development processes and practices. But decision-makers need to understand its constraints and build appropriate guardrails around application development.- Ask the right questions up-front to determine if low code is the right choice for a given applicationLow-code tools and platforms have enabled people to create useful software systems without having to write and maintain large custom codebases for many years now winning advocates and critics in near-equal numbers.But, with some forecasts predicting that by 2025, as much as 70% of new applications could be created using low-code tools and platforms; and ongoing developer shortages driving businesses to explore new ways of accelerating software delivery and reducing workloads, more organizations than ever are exploring what the technology could do for them.Low-code capabilities have matured significantly in recent years but skepticism persists and for good reason. While low-code tools have the potential to empower a new generation of so-called 'citizen developers' and take pressure off dev teams by streamlining the creation of simple capabilities, the fact remains that they simply aren't suitable for every development scenario.The first step towards determining if low code is right for you and ultimately gaining the value it could potentially deliver for your business is understanding what it's best suited to support."
  • 사람(인간)이 작성한 질문: “What needs to be taken into account when considering low-code?”
  • LLM이 생성한 질문:
  • “What development scenarios are low-code tools best suited for?”
  • “Why is it important to consider the constraints of low-code adoption?”
  • “How can organizations determine if low-code tools are the right choice for their application development?”
  • LLM이 생성한 검색 쿼리:
  • “Low code vs traditional development scenarios”
  • “Understanding low code constraints for application development”
  • “Determining suitable applications for low-code tools”

이런 식으로 생성된 합성 질문과 쿼리는 임베딩 모델 파인튜닝 외에도 유용합니다. 이를 문서(아티클) 스키마의 별도 필드로 함께 벡터화·인덱싱해두면, 문서 본문뿐 아니라 합성된 질문·쿼리 필드 에도 검색을 수행함으로써 검색 정확도를 높일 수 있습니다. 또한, 사용자가 검색할 때 이 질문들을 추천(Suggest) 질문 형태로 보여줄 수도 있죠.

지금까지 시중에 나와있는 (off-the-shelf) 검색 모델을 평가해보자

원래는 ColBERT vs. 제가 과거에 사용했던 몇몇 단일 임베딩 모델만 비교하려 했는데, Vespa가 워낙 간편하게 여러 모델을 실험할 수 있게 해주다 보니, 호기심에 다양한 모델을 추가로 검증하게 되었습니다. 모델 개발 속도가 워낙 빠른 시대이기도 해서, 실험하는 동안에도 흥미로운 새 모델이 여럿 나왔습니다.

그 중 주목할 만한 것은 BGE-M3 라는 모델인데, 한 번의 전파(Forward Pass)로 세 가지 출력을 모두 낼 수 있습니다.

  1. 단일(밀집) 임베딩
  2. ColBERT 형태의 멀티(토큰별) 임베딩
  3. 신경망 기반 희소(Neural Sparse) 임베딩

여기서 희소 임베딩이라 함은 모델이 “토큰(단어)에 가중을 주고, 경우에 따라 확장 토큰까지 추가”하는 SPLADE 류의 기법을 어느 정도 흉내 낸 것을 말합니다. 다만, BGE-M3의 희소 벡터는 SPLADE처럼 토큰 확장(Expansion)을 하진 않습니다. BGE-M3는 100개 넘는 언어를 다루는 멀티링구얼 모델이고, 최대 8192 토큰의 긴 텍스트까지 처리가 가능합니다. 파라미터가 5억 6천8백만(568M) 개로 임베딩 모델 치고는 제법 큰 편입니다. 그래서 저는 다른 비교적 큰 모델들과 함께, 소규모 모델(BGE-small-en-v1.5, 약 3천3백만(33.4M) 파라미터)도 추가로 실험하여, 작은 모델도 얼마나 성능을 낼 수 있는지 확인했습니다.

평가 지표로는 주로 MRR(Mean Reciprocal Rank) 을 썼습니다. 이는 검색 결과 내 첫 번째로 등장하는 정답의 순위만 반영하기 때문에, 정확도를 순위별로 확인하기에 좋아서입니다. 즉, 사람(혹은 라벨러)이 만든 질문마다 해당 질문에 맞는 ‘정답 청크(문서)’가 1개씩 있으니, 얼마나 빠르게(앞 순위에) 올바른 청크를 가져오는지를 보는 거죠.

MRR@K는 상위 K개 결과 중에서 Reciprocal Rank를 어떻게 측정하는지를 뜻합니다(K=1,3,5,10,20,50 등으로 측정). 멀티 정답 시나리오라면 nDCG 등이 더 좋을 수도 있습니다.

직접 MRR을 계산해보고 싶으시다면, 사실 몇 줄의 파이썬 코드면 됩니다.

from typing import Listdef calculate_reciprocal_rank(retrieved_ids: List[str], relevant_id: str) -> float:
if relevant_id not in retrieved_ids:
return 0.0
return 1 / (retrieved_ids.index(relevant_id) + 1)

모델별 MRR@K 결과 테이블

아래 표에는 다양한 모델들의 MRR@K(1,3,5,10,20,50) 점수를 모두 담았습니다. 각 모델 범주(단일 임베딩, 멀티(토큰) 임베딩, 희소, BM25, etc.) 내에서 가장 높은 점수는 굵게 표시했습니다. 오픈소스 모델들은 Apache 2.0 혹은 MIT 라이선스이고, 상업 모델은 OpenAI와 Cohere에서 제공하는 임베딩 API를 활용했습니다.

평가 시에는 아티클 본문 텍스트만 검색 필드로 사용했습니다(타이틀 등은 사용 X). 실제 프로덕션에선 여러 필드를 함께 사용하는 게 일반적입니다.

이제 이를 시각화한 그래프(MRR@K)를 봅시다.

결과적으로 MRR@K 성능이 전반적으로 꽤 좋습니다. Thoughtworks.com 글이 영어이며, 소프트웨어 관련 일상적 주제를 다루므로, 대부분의 모델이 학습 과정에서 유사 텍스트를 접했을 가능성이 높습니다.

단일 임베딩 모델 비교

  • BAAI/bge-m3(대형 모델), intfloat/multilingual-e5-large-instruct(대형 모델) 등이 대체로 좋은 편이지만, mixedbread-ai/mxbai-embed-large-v1 같은 비교적 작은 모델도 성능이 근소하게 비슷하거나 오히려 살짝 우세한 부분이 있습니다. (mixedbread.ai 모델에 대한 커뮤니티의 호평이 타당함을 확인.)
  • 단일 임베딩 중 가장 높은 점수는 OpenAI의 text-embedding-3-large 모델이지만, 차이는 크지 않습니다. 해당 모델의 파라미터 수는 공개되지 않았지만, 출력 벡터 차원 수(dimension)가 매우 크다는 점을 감안하면, 거기에 비해 성능 차이가 크지 않아 “의외로 효율성이 떨어진다”고도 볼 수 있습니다.
  • Cohere의 상용 모델은 OpenAI보다 벡터 차원(768차원)이 훨씬 작음에도, 거의 비슷한 MRR 성능을 냅니다. Cohere에는 SBERT 논문 저자인 Nils Reimers 등 관련 분야 전문가가 많습니다.

벡터 차원 대비 성능

출력 벡터 차원 수도 중요한 요소입니다. 차원이 클수록 저장 및 검색 시 메모리·시간 비용이 커집니다. 아래와 같이 “MRR@10 / 벡터 차원 수” 비율로 비교해보면 OpenAI 모델이 그다지 효율적이지 않음을 알 수 있습니다.

물론 이것만으로 모든 것을 평가할 순 없습니다. 상용 모델은 파라미터 수도 비공개고, OpenAI 모델은 Matryoshka Representation Learning(Kusupati et al. 2022) 기법으로 차원 축소가 가능하기도 합니다. (이번 실험에선 다루지 않았습니다.)

ColBERT와 희소 모델 비교

  • 멀티(토큰별) 임베딩인 BAAI/bge-m3 ColBERT 결과가 전체적으로 가장 높았습니다. 연구들도 ColBERT가 도메인 내외 성능 모두 우수하다고 보고하고 있으니 예상대로입니다.
  • 희소(Neural Sparse) 모델도 단일 임베딩보다 일반화에 장점을 가지며, 실제로 opensearch-project/opensearch-neural-sparse-encoding-v1이 단일 임베딩보다 더 높은 점수를 기록했습니다.
  • BAAI/bge-m3의 희소 표현은 상대적으로 낮았습니다. (SPLADE처럼 용어 확장(term expansion)을 하지 않기에 opensearch-project의 sparse 모델만큼은 못 미친 듯합니다.)

BM25

BM25처럼 전통적인 모델도, 라벨링된 테스트 쿼리들이 “동의어”를 많이 사용했다는 불리한 조건임에도 불구하고 괜찮은 편이었습니다. 특정 키워드나 약어가 질문에 등장하는 경우에는 단일 임베딩 모델보다 더 우수한 결과를 보이기도 했습니다. 실제 프로덕션에서는 제목(title) 필드도 BM25 인덱스에 함께 넣으면, MRR이 몇 포인트 더 상승하곤 합니다.

긴 컨텍스트 단일 임베딩 모델: 청크를 나눌 것인가?

초창기 BERT 기반 임베딩 모델들은 보통 512 토큰 제한이 있었습니다. 그러나 BGE-M3처럼 8192 토큰까지 가능하다고 광고하는 모델도 새롭게 등장했고, 이론적으로는 기사 하나(수천 단어)를 통으로 임베딩할 수 있는 시대가 되었습니다.

이 경우, 길고 복잡한 문서 전체를 하나의 임베딩으로 만들면 과연 괜찮을까요? 서로 다른 토픽이 뒤섞일 텐데, 과연 하나의 벡터에 잘 압축될지 의문입니다.

제 데이터셋 통계를 보면, 대다수 아티클이 8192 토큰 이내에 수렴합니다. (가장 긴 아티클도 8283 단어이니, 토큰화하면 대략 비슷하거나 조금 넘는 정도.)

그래서 BGE-M3 모델을 활용해, 청크 없이 문서 전체를 임베딩했을 때와, 기존 512 토큰 단위로 청크를 나눈 경우를 비교해보았습니다.

결과를 보면, 단일(전체) 임베딩이 훨씬 낮은 MRR@K를 냅니다. 왜냐하면 테스트 세트 질문은 청크 단위로 라벨링되어, 기사(문서) 하나에 여러 다른 부분(청크)이 있을 때 각각 다른 질문들이 들어올 수 있기 때문입니다. 문서 전체를 하나로 뭉뚱그려 벡터화하면, 세부적인 내용은 희석되어 잘 검색되지 않게 되는 거죠.

하이브리드 검색 & 리랭킹(Re-ranking)

글 서두에서 BM25+단일 임베딩 모델 식의 하이브리드 검색 장점을 언급했었습니다. Vespa에서 이를 구현하는 것이 매우 간단해, 이번에도 여러 형태로 시도해봤습니다.

특히 BGE-M3 모델은 앞서 설명했듯이 (1)단일(밀집) 임베딩, (2)희소 임베딩, (3)ColBERT 멀티 임베딩을 모두 뽑을 수 있으므로, 하나의 모델로 다양한 표현을 결합해 사용할 수도 있습니다. 보통 임베딩 모델 추론 자체가 아키텍처에서 가장 비용이 큰 부분 중 하나이기에, 한 번의 전파로 세 가지를 동시에 얻는 건 매력적입니다.

하이브리드 검색에서 문제는 점수 융합(Score Fusion) 방식인데, 대표적으로 순위 기반 융합(Rank-based fusion)과 점수 기반 융합(Score-based fusion)이 있습니다.

  • 순위 기반 융합: 예를 들어 Reciprocal Rank Fusion(Cormack et al. 2009)이 유명합니다. 각 모델이 제공한 순위(랭킹)만 참조하며, 개별 점수 값은 고려하지 않습니다.
  • 점수 기반 융합: 단순 선형 결합, 가중합(Weighted sum) 등 혹은 더 복잡한 학습 기반 러닝투랭크(Learning-to-rank) 모델도 쓸 수 있습니다. 이를 통해 추가적인 피처(사용자별 개인화 등)를 반영할 수도 있습니다.

BM25 점수는 일반적으로 무한 범위라서, 다른 모델 점수와 단순 가중합하기 전에 정규화(Normalization)를 해줘야 합니다. 예컨대 min-max 정규화나, Seo et al.(2022)에서 제안된 아크탄젠트(Arctan) 변환 2/pi * arctan(x) 등을 쓰기도 합니다. 저는 과거 프로젝트에서 이 아크탄젠트 정규화를 BM25 점수에 적용했을 때 결과가 매우 좋았습니다. Vespa에서 간단히 수학 함수를 적용하여 커스텀 랭킹을 만드는 것이 수월해서, 실험하기도 편리합니다.

Reranking은 대용량 문서 중 상위 N개를 신속히 뽑아낸 후, 더 무거운 모델로 그 N개를 정교하게 재정렬하는 방식을 말합니다. 실제 서비스에서는 여러 단계(funnel)를 적용하기도 합니다. Vespa의 단계별 랭킹(Phased ranking) 기능을 써서 간편하게 실험해봤습니다.

앞서의 평가에서 ColBERT (BGE-M3 멀티 벡터)이 가장 높았다는 걸 확인했는데, 대규모 문서에 ColBERT를 그대로 적용하면 토큰별 임베딩 연산이 너무 무거운 것이 문제입니다. 예컨대, 제가 로컬 Dell XPS 노트북(인텔 CPU) 에서 Vespa를 4 스레드로 설정해 2678개 아티클(각각 512 토큰 이하 청크 12069개)에 대해 ColBERT로 검색해보니, 쿼리당 6초 정도 걸렸습니다. 반면, ColBERT를 리랭커로만 사용해 상위 50개(약 200개 청크)만 재랭킹하니 쿼리당 약 0.2초로 줄었습니다. 더 적은 수(예: 상위 20개)만 재랭킹하면, 훨씬 빨라집니다.

하이브리드·리랭킹 실험 결과

아래 표는 BGE-M3의 (1)단일(밀집), (2)희소, (3)ColBERT 등 여러 조합과 BM25를 섞은 하이브리드 검색 및 ColBERT 리랭킹 방식들을 평가한 MRR@K 점수를 모아둔 것입니다.

  • BGE-M3 단일+희소 조합은 각각 단독으로 썼을 때보다 성능이 오름을 확인할 수 있습니다. BM25와 BGE-M3 단일의 조합도 마찬가지입니다.
  • 세 가지(BM25 + BGE-M3 단일 + BGE-M3 희소)를 모두 결합하면 현재까지 중 가장 높은 점수를 얻습니다. 이는 기존에 최고였던 BGE-M3 ColBERT 단독보다도 더 좋습니다.
  • BM25 점수 정규화 기법 비교에서, min-max보다 arctan 방식이 더 성능이 좋았습니다. 랭크 기반(Rank-based) 융합인 Reciprocal Rank Fusion은 점수 기반 융합보다 성능이 낮았습니다.
  • ColBERT 리랭킹은 이미 검색 정확도가 높은(하이브리드) 상위 결과 를 재정렬하는 것이므로, 추가 향상 폭이 크지 않고 오히려 미묘하게 성능이 떨어지는 경우도 있었습니다. 그러나 ColBERT가 제공하는 “해석 가능성” 장점을 얻으려면, 리랭킹 기법으로서 ColBERT를 활용하는 전략이 여전히 유효합니다.

상용 SaaS 검색 서비스 평가

만약 “검색 시스템을 직접 구축하기보다는, 어떤 클라우드 SaaS 서비스를 가져다 써볼까?”라면, 많은 옵션이 있습니다. 저는 큰 회사(클라우드 벤더)에서 제공하는 Azure AI SearchGCP Vertex AI Search를 간단히 평가해봤습니다. “내 문서를 한꺼번에 업로드만 하면, 자동으로 임베딩을 잘 만들어서 검색 성능을 내줄까?”를 가정해본 거죠.

Azure AI Search

  • “Integrated Vectorization”이라는 미리보기(preview) 기능이 있어서, 청크 쪼개기와 벡터화 과정을 Azure가 대신 처리해줍니다.
  • 내부적으로는 Azure OpenAI 임베딩(=OpenAI의 text-embedding-ada-002) 모델을 쓰므로, 해당 사용 권한이 있어야 합니다(승인까지 하루 정도 대기).
  • 설정 시, 전통적 텍스트 검색(BM25)만 할 수도 있고, 벡터 검색만 할 수도 있고, 둘을 결합할 수도 있습니다.

GCP Vertex AI Search

  • 더 “블랙박스”에 가깝고 세부 설정 옵션이 제한적입니다.
  • 구글 내부 임베딩 모델을 쓰는 것으로 추정되지만, “Bring Your Own Embedding” 옵션도 있다고 합니다.
  • “구글 검색 급의 정보 검색 품질”을 표방하며, 실제로 내부적으로 여러 기법(하이브리드, 리랭킹)을 적용하는 것으로 보입니다.

다음 표는 두 서비스별 MRR@K 점수와, 앞선 오픈소스 하이브리드 모델 중 가장 높은 스코어를 함께 비교한 것입니다.

  • Azure AI Search: BM25 + 벡터 검색을 지원하며, Reciprocal Rank Fusion만 제공합니다. 그 자체로는 나쁘지 않지만, 오픈소스 조합 대비 현저히 높은 점수를 내진 않습니다.
  • Azure는 여기에 “semantic ranking” 기능(“멀티링구얼 딥러닝 모델로 의미적 유사도를 계산해 상위 랭크에 올려준다”)도 있는데, 추가 비용이 들고 특정 지역(미국, 캐나다)에서만 지원합니다. 이걸 켜면 MRR이 개선되긴 하는데, 여전히 오픈소스 대비 낮습니다.
  • GCP Vertex AI Search: 처음엔 JSON 파일 한 개로 통째에 올려봤는데, 성능이 형편없었습니다. 라벨링된 테스트 세트가 있으니 이런 문제를 쉽게 파악할 수 있었습니다. 이후 “unstructured 모드”로 아티클을 개별 text 파일로 올리고, “자동 청크” 옵션을 켰더니 꽤 좋아졌습니다. 그래도 Azure + semantic ranking 정도 수준이었고, 오픈소스 대비 낮았습니다.

BGE-M3 모델 파인튜닝(Fine-tuning) — “효율” 개선

초반에 언급했듯이, 단일 임베딩 모델을 파인튜닝해 도메인 적합성을 높일 수 있습니다. 그런데 여기서는 이미 MRR 점수가 꽤 좋아, 남은 한두 점 퍼센트 상승을 위해서는 비용 대비 효과가 떨어질 수도 있습니다. 그래서 저는 성능 향상보다는 효율성(메모리, 속도) 개선을 목적으로 파인튜닝을 시도했습니다.

오리지널 ColBERT(2020)에서는 토큰 벡터를 128차원으로 썼습니다. 그런데 BGE-M3는 ColBERT에서 1024차원이나 됩니다. float32 기준이라면, 1024차원 토큰 임베딩 하나당 4096바이트(1024 × 4byte)가 들죠. 제 작은 실험 데이터(2678개 아티클, 총 청크 토큰 3828855개)만 해도 15GB나 됩니다. 오리지널 ColBERT(128차원)이면 이론상 2GB 정도였을 텐데, 1024차원은 너무 크죠.

BGE-M3의 단일(밀집) 임베딩도 1024차원이므로, 청크 12069개를 인덱싱하면 42MB 정도가 듭니다. 384차원 정도로 줄이면 18MB가 됩니다. ColBERT만큼 대단하진 않지만, 수백만~수십억 건 문서를 다룬다면 충분히 고민할 만한 절감 폭입니다.

즉, 임베딩 차원을 줄이면 저장 공간과 검색 속도가 향상됩니다. 그래서 BGE-M3를 파인튜닝하면서, ColBERT 표현은 128차원, 단일(밀집) 표현은 384차원으로 출력하도록 레이어를 수정했습니다. 원 저자가 제공한 BGE-M3 파인튜닝 코드를 참조해 조금 수정하여, 학습 과정에서 train·validation 손실을 모니터링할 수 있도록 했고, 모델링 부분에 추가 선형 레이어(1024→384)를 넣어 단일 임베딩 차원을 줄였습니다.

  • 학습 데이터: 앞서 LLM으로 생성한 합성 쿼리(질문+키워드) 총 57879개(train)
  • 검증 데이터: 14718개(validation)
  • 쿼리에 대응하는 양의 예시(Positive) 문서를 붙였고, **음의 예시(Negative)**는 랜덤 샘플링하여 삼중쌍(Triplet) (query, pos, neg)으로 구성했습니다. (더 세밀한 “Hard negative mining”은 이번엔 생략.)
  • GPU: Nvidia A100 40GB를 사용해 5에폭 학습 약 4시간(약 16달러 비용). Nvidia T4 16GB로도 가능했겠지만, 배치 크기를 키워서 속도를 높이기 위함이었습니다.

훈련 로스와 검증 로스는 전반적으로 부드럽게 잘 떨어지는 모습을 보였고, 오버피팅 징후는 없었습니다.

파인튜닝된 BGE-M3 모델 평가

목표는 ColBERT 표현을 128차원으로 대폭 줄이고, 단일 임베딩은 384차원으로 줄여도 원래 성능을 유지하거나 오히려 개선하는 것이었습니다.

아래 표는 오리지널 BGE-M3 vs. 파인튜닝 버전 MRR@K 점수 비교입니다.

  • Sparse 표현은 조금 더 점수가 개선되었고,
  • 단일(밀집) 384차원 표현은 원래 1024차원과 거의 동등한 수준을 유지했습니다. 게다가 새로 추가한 레이어를 제거하면(1024차원 출력) 더욱 높은 점수를 얻을 수도 있습니다. 이는 Matryoshka Representation Learning과 유사하며, 추후 실험해볼 만합니다.
  • ColBERT는 오히려 더 좋아졌습니다(MRR 상승). 이제 128차원이 되었는데, 성능은 이전보다 높아진 것이죠. 게다가 저장 공간도 훨씬 절감됩니다.
  • 당연히 리랭킹이나 하이브리드 검색 시에도 비슷한 긍정적 개선 효과를 볼 수 있었습니다.

속도 측면에서도,

  • 단일(밀집) 임베딩 검색은 약 1.3배 빨라졌고,
  • ColBERT 검색은 약 4.1배 빨라졌습니다.

이 모든 것은 결국 “더 도메인에 맞춘 모델”을 가지면서도, 동시에 효율을 높이는 결과로 이어집니다.

임베딩 모델 최적화 기법

1. 모델 추론 엔진(ONNX Runtime 등)

임베딩 모델은 보통 1B 이하 파라미터 정도로, CPU에서 추론해도 충분히 가능한 경우가 많습니다. 하지만 파이토치(PyTorch)로 그대로 실행하면 비효율적일 수 있으므로, ONNX Runtime 같은 엔진을 써서 최적화할 수 있습니다. ONNX Runtime은 마이크로소프트가 만든 오픈소스 라이브러리로, 다양한 하드웨어 및 운영체제에서 그래프 최적화와 하드웨어 가속을 이용해 속도를 높이고 비용을 절감할 수 있게 해줍니다.

저는 최근 4년간 ONNX Runtime을 자주 써 왔는데, 2~3배는 물론 10배 이상 빨라지는 경우도 경험했습니다. 아래는 제 로컬 노트북(CPU)에서 BGE-M3 모델에 대해, 입력 토큰 길이에 따른 PyTorch vs. ONNX Runtime 추론 속도 비교 표입니다.

  • 특히 짧은 입력(검색 쿼리) 일수록 2~3배 이상의 개선이 나타납니다.
  • Vespa 내부에서도 ONNX 모델을 돌릴 수 있는데, 아직 테스트는 안 해봤습니다. 모든 것을 Vespa에서 처리하면 네트워크 레이턴시가 줄어드는 장점이 있을 것입니다.

2. 양자화(Quantization)

ONNX Runtime에서 양자화(Quantization) 또한 큰 개선 효과가 있습니다. float32 대신 8비트 정수(int8)로 가중치·활성값을 근사함으로써 모델 크기를 4배 줄이고, 속도도 높입니다. 정확도 손실은 보통 미미합니다. 정적(Static)과 동적(Dynamic) 양자화가 있는데, 저는 주로 데이터셋이 필요 없는 동적 양자화를 써왔습니다. 살짝 더 느릴 수 있지만 정확도가 조금 더 낫다는 경험이 있습니다.

아래는 BGE-M3 ONNX 모델에 대해, 비양자화 vs. 동적 양자화 각각의 입력 길이별 추론 속도 비교입니다.

  • 동적 int8 양자화 모델이 추가로 약 3배 더 빨랐고, PyTorch와 비교하면 최대 7~8배 차이를 보입니다.
  • 모델 파일 크기도 2272MB → 571MB로 1/4 수준.
  • 추론 시 메모리 사용량도 약 20% 감소.
  • 정확도(MRR) 관점에서 희소/단일 임베딩 표현은 0.15% ~ 0.65% 정도 손실, ColBERT 표현은 2% 정도 손실이 있었습니다.
  • ColBERT 양자화 손실이 조금 더 크지만, 충분히 수용 가능한 수준이라고 볼 수도 있고, 파인튜닝 과정에서 양자화 인지(QAT)로 진행하면 손실을 더 줄일 수도 있습니다.

결국 ONNX Runtime 도입과 양자화를 통해 대규모 프로덕션 환경에서 비용과 응답 속도를 크게 개선할 수 있습니다. 최근엔 Intel 등 다른 벤더의 추론·양자화 툴도 존재하니, 비교해보시는 것도 좋겠습니다.

벡터 검색 최적화 기법

앞서 BGE-M3를 파인튜닝하여 벡터 차수를 줄이는 방법도 설명했지만, 이 외에도 벡터 자체를 양자화하는 기법이 있습니다. 제품 양자화(Product Quantization), 스칼라 양자화(Scalar Quantization) 등 다양하지만, 요즘 주목받는 방식은 바이너리 양자화(Binary Quantization) 입니다.

간단히 말해, float32 벡터의 각 성분을 0보다 크면 1, 아니면 0으로 바꿉니다. 이를 8비트 단위로 묶으면(1바이트 = 8비트) float32 대비 32배 공간 절감 효과가 생기죠. 이런 바이너리 벡터끼리는 해밍 거리(Hamming distance) 로 매우 빠르게 유사도(혹은 비유사도)를 구할 수 있습니다. (45배 빠르다는 보고도 있습니다.) Vespa는 이런 int8/Hamming distance 방식도 지원합니다. Cohere나 mixedbread.ai 같은 모델 벤더들도 바이너리 양자화 기법을 홍보하고 있습니다.

물론 정확도 손실도 발생할 수 있어, 바이너리 양자화 전용으로 모델을 학습하거나 차원을 크게 설정하는 식의 추가 조치가 필요합니다. 혹은 바이너리 검색으로 상위 K개를 구한 뒤, float32 임베딩으로 다시 점수 계산(Re-scoring)한다든지, ColBERT 리랭커를 돌린다든지 여러 방식을 결합할 수 있습니다.

아래 표는 제가 파인튜닝된 BGE-M3 모델의 단일 임베딩을 바이너리 양자화했을 때의 결과입니다.

  • 1024차원을 바이너리화하면, 평균적으로 MRR 점수가 5.35% 정도 하락.
  • 384차원을 바이너리화하면, 평균 7.74% 정도 하락.
  • 하지만, ColBERT 리랭킹(하이브리드)에 결합하면, 실제 최종 점수 하락 폭은 1.16% 정도에 그칩니다. 그 대가로 벡터 저장 비용이 32배 줄어듭니다(384차원 → byte로 양자화 → 48바이트).
  • 만약 바이너리 양자화까지 고려하여 파인튜닝한다면(Cohere 사례처럼), 정확도를 더 높일 수도 있겠죠.

참고로, Vespa의 ColBERT용 바이너리 양자화는 문서 토큰 벡터를 저장할 때 binarized 형태로 유지하다가, 검색 시에 이를 float32로 약간 손실 복원(unpack)해서 MaxSim 연산을 수행합니다. 문서 쪽만 바이너리 형태로 관리하니, 32배 저장 공간 절약 효과를 누리면서 정확도 손실은 최소화합니다.

예를 들어, 제가 “파인튜닝된 BGE-M3 ColBERT 128차원 float32”라고 말할 때 이론상 2GB 공간이지만, Vespa에서 바이너리 양자화를 쓰면 실제 63MB만 차지하는 식입니다(약 240배 감소). 대규모 문서에선 그 차이가 어마어마해지겠죠.

ColBERT: 해석 가능한(Interpretable) 신경망 검색

BM25 같은 전통 검색 기법은 “어떤 토큰이 매칭되어 얼마나 점수가 올라갔는지”가 명확합니다. 반면, 단일 임베딩 모델은 내부 벡터 공간이 불투명해, 결과가 왜 그런지 해석하기 어렵습니다.

하지만 희소(sparse) 모델이나 ColBERT처럼 토큰 단위로 표현을 만들면, 어느 쿼리 토큰이 문서 토큰과 얼마나 유사한지 토큰별 점수를 직접 확인할 수 있습니다. Vespa에선 match-features 기능으로 ColBERT의 MaxSim 연산 결과를 중간 단계별로 반환해, 가장 기여도가 큰 문서 토큰들을 추출할 수 있습니다.

아래 예시는, 쿼리 토큰별로 문서에서 가장 높은 점수를 가진 토큰(그리고 그 다음으로 비슷한 토큰들)을 시각화해본 것입니다.

물론 항상 사람이 보기에 100% “아, 이건 완벽히 타당하네!”라고 느껴지진 않더라도,

  • 사용자가 검색 이유를 쉽게 확인하고,
  • 결과가 적절한지 빠르게 판단해 신뢰도를 높이고,
  • 사용자가 “아, 검색 쿼리를 이렇게 구성하는 게 좋겠다”는 학습으로 이어질 수 있습니다.

또, RAG 시나리오에서 대형 언어 모델에 넣을 “가장 중요한 문맥”을 찾는 데도 이 기법이 활용될 수 있습니다(문서 전체가 아닌, ColBERT가 가장 높은 점수를 준 문맥 부분만 넣는 식).

결론

이렇게 임베딩과 정보 검색 전 과정 — 임베딩 개념과 한계, 데이터셋 생성·라벨링, 각종 오프 더 셸프 모델 평가, 하이브리드·리랭킹, 임베딩 모델 파인튜닝 및 최적화, 해석 가능성까지 –을 주욱 살펴보았습니다. 작년(2023년) RAG 붐 때 전 세계적으로 “바로 임베딩 검색 쓰면 만사 해결” 식으로 성급한 프로젝트가 많았는데, 최신 스테이트 오브 더 아트 시스템을 정말 잘 구현하려면 이렇게나 많은 고려사항이 있다는 것을 보여주는 예시라 할 수 있습니다.

상용 솔루션도 쓸 만은 하지만, 오픈소스 기반으로 ML 전문성을 더하면 훨씬 좋은 성능과 유연성을 얻을 수 있습니다. 정보 검색은 LLM의 “생성(Generation)” 여부에 상관없이 대부분의 조직에 매우 가치가 큰 기능이며, 제대로 설계·구현된다면 많은 이점을 제공합니다. 이 가이드북이 그 길에 조금이나마 도움이 되길 바랍니다.

--

--

Responses (2)