Multi-Vector (후기 상호작용) 임베딩 모델을 활용한 검색
요약
본 글은 ColBERT 스타일의 Multi-Vector 임베딩 모델을 활용한 검색 기술을 다룹니다. 일반적인 단일 벡터 방식과 달리, 멀티-벡터 모델은 토큰당 벡터를 유지하여 정보 손실 없이 강력한 매칭 정보를 보존합니다. 이를 통해 더 높은 검색 성능을 제공하며, 시각 문서 및 오디오/비디오 검색에도 적용 가능합니다.
핵심 포인트
- 멀티-벡터 모델은 토큰별 벡터를 유지하여 정보 손실을 최소화합니다.
- 단일 벡터 방식 대비 강력한 매칭 정보를 보존해 검색 성능이 우수합니다.
- ColBERT 스타일로, 쿼리와 문서 간의 상호작용(late interaction)에 중점을 둡니다.
- OCR 없이 페이지 이미지와 직접 일치하는 시각 문서 검색도 가능합니다.
MultiVectorEncoder는 ColBERT 스타일의 후기 상호작용(late interaction) 검색에 사용됩니다. 모든 PyLate 체크포인트와 Stanford-NLP ColBERT 체크포인트를 이 인코더에 바로 로드할 수 있으며, 심층(dense), 희소(sparse), 리랭커 모델을 위해 이미 사용하고 있는 익숙한 API를 통해 colpali-engine 모델도 시각 문서 검색에 사용할 수 있습니다. 일반적인 임베딩 모델이 전체 텍스트를 하나의 벡터로 압축하는 반면, 멀티-벡터 모델은 토큰당 하나의 벡터를 유지하며 MaxSim 연산자를 사용하여 쿼리와 문서를 점수화합니다. 이는 단일 벡터가 평균화해버리는 토큰 수준의 매칭 정보를 보존하므로, 일반적으로 더 큰 인덱스 비용을 감수하는 대신 더 강력한 검색 성능을 제공합니다. 또한, 텍스트 쿼리가 OCR(광학 문자 인식) 단계 없이 페이지 이미지와 직접 일치되는 시각 문서 검색 분야에서도 최신 기술입니다.
이 블로그 게시물에서는 이러한 모델들을 사용하는 방법을 보여드릴 것입니다: 다양한 체크포인트 형식 로드하기, 인코딩 및 점수화, 검색 스택에 연결하기, 페이지 이미지에서 실행하기, 그리고 인덱스를 저렴하게 유지하는 방법까지 다룹니다. 아래의 모든 내용은 간단히 pip install -U sentence-transformers를 설치하여 실행할 수 있습니다.
이 블로그는 멀티-벡터 모델을 사용하는 것에 관한 것입니다. 만약 이들을 학습시키는 방법을 알고 싶다면, 동반되는 'Training and Finetuning Multi-Vector Embedding Models with Sentence Transformers' 블로그 게시물을 참고하세요.
- 멀티-벡터 모델이란 무엇인가?
- 설치
- 모델 로드하기
- 쿼리 및 문서 인코딩하기
- MaxSim으로 점수화하기
- 의미론적 검색
- 검색 및 리랭킹
- 인덱싱
- 시각 문서 검색
- 오디오 검색
- 비디오 검색
- 해석 가능성(Interpretability)
- 토큰 풀링
- 추론 속도 높이기
- 모델 평가하기
- PyLate 또는 colpali-engine에서 온 경우
- 지원되는 모델
- 감사 표시
- 추가 자료
밀집 임베딩 모델(dense embedding model)은 텍스트를 읽고 단일의 고정 크기 벡터를 반환합니다. 모델이 감지한 모든 것이 그 384, 768, 또는 1024라는 숫자에 담겨야 하며, 유사성은 두 가지 요약본 사이의 내적(dot product) 하나로 결정됩니다. 이는 놀라울 정도로 잘 작동하지만, 압축 방식에 특정 손실이 있습니다. 즉, 희귀한 엔티티, 정확한 식별자, 또는 긴 구절 속의 한 가지 중요한 절 모두가 같은 벡터 공간에서 자리를 차지하기 위해 경쟁해야 합니다. 여러 요구 사항을 동시에 가진 쿼리 역시 이와 같은 벽에 부딪힙니다. 예를 들어, '나무 다리와 둥근 쿠션이 있는 초록색 소파'라는 쿼리의 경우, 단일 벡터는 네 가지 요소를 모두 하나의 점으로 혼합해야 하므로, 잘못된 다리를 가진 초록색 소파가 실제로 요청한 소파 근처에 놓이게 됩니다.
다중 벡터 모델(multi-vector model)은 (late-interaction 또는 ColBERT 스타일 모델이라고도 불리며, ColBERT 논문에서 유래함) 이러한 압축 과정을 건너뜁니다. 동일한 트랜스포머를 실행하지만, 토큰 임베딩을 단일 벡터로 풀링(pooling)하는 대신, 각 토큰 임베딩을 작은 차원(전통적으로 128)으로 투영하고 모두 보존합니다. 따라서 9개 토큰 문서가 1x128 벡터가 아닌 9x128 행렬이 됩니다.
쿼리와 문서 간의 상호작용은 점수 계산 시점까지 연기되는데, 여기서 'late interaction(후기 상호작용)'이라는 이름이 유래했습니다. 크로스 인코더(cross-encoder)는 두 텍스트를 모델을 통해 함께 통과시키며 초기에 상호작용합니다. 이는 정확하지만, 모든 문서가 새로운 쿼리에 대해 다시 인코딩되어야 하므로 사전 계산할 여지가 없습니다. 위에서 설명한 밀집 임베딩 모델 같은 바이 인코더(bi-encoder)는 거의 상호작용하지 않습니다(두 완성된 요약본 사이의 내적 하나). 그리고 이것이 바로 컬렉션을 한 번에 인코딩하고 빠르게 쿼리할 수 있게 하는 방식입니다. 후기 상호작용은 그 중간 지점에 위치합니다. 문서는 여전히 독립적으로 인코딩되어 오프라인으로 색인화될 수 있지만, 점수 계산 시에는 모든 쿼리 토큰을 모든 문서 토큰과 비교하므로 두 요소가 상호작용할 공간이 훨씬 더 많이 남게 됩니다.
점수 계산은 MaxSim을 사용합니다. 즉, 각 쿼리 토큰에 대해 임의의 문서 토큰과의 최고 유사성을 취한 다음, 이 최댓값들을 쿼리 전체에 걸쳐 합산하는 방식입니다.
토큰 임베딩이 L2-정규화(L2-normalized)되어 있기 때문에, 이 모든 내적(dot products)은 [-1, 1] 범위의 코사인 유사도(cosine similarity)가 됩니다.
따라서 전체 합산 값은 [-쿼리 토큰 수, 쿼리 토큰 수] 범위 안에 놓이게 됩니다.
이 연산자를 소프트 정렬(soft alignment)로 이해할 수 있습니다. 즉, 모든 쿼리 토큰이 자신을 가장 잘 설명하는 하나의 문서 토큰을 가리키고, 그 점수는 전반적으로 문서를 얼마나 잘 뒷받침하는지를 나타냅니다.
정렬이 반드시 어휘적일 필요는 없습니다. 왜냐하면 토큰 임베딩은 문맥화(contextualized)되어 있기 때문입니다. lightonai/mLateOn을 사용하여
비용은 인덱스 크기입니다. 문서당 하나의 벡터 대신 토큰당 하나의 벡터를 사용하면 차원은 작아졌음에도 불구하고 훨씬 더 많은 벡터가 생성됩니다. lightonai/LateOn을 사용하여 4,874개의 Natural Questions 패시지를 인코딩한 결과, 평균 124.8개 패시지당 608,414개의 토큰 벡터가 생성되었습니다:
| 표현 방식 | 벡터 수 | 차원 | float32 크기 |
|---|---|---|---|
Dense, all-MiniLM-L6-v2 | 4,874 | 384 | 7.5 MB |
Dense, gte-modernbert-base | 4,874 | 768 | 15.0 MB |
Multi-vector, LateOn | 608,414 | 128 | 311.5 MB |
이는 MiniLM 인덱스 저장 공간의 약 42배에 해당하며, 패시지당 62 KiB입니다. 하지만 인덱스는 종종 압축되는데, 예를 들어 동일한 608,414개 벡터가 PLAID 인덱스로는 92 MB를 차지합니다. 이는 PLAID가 벡터 자체 대신 중심점 ID(centroid id)와 양자화된 잔차값(quantized residual)을 저장하기 때문입니다. 규모로 보자면, Qwen3-Embedding-8B와 같은 4096차원 밀집 모델은 이 동일한 4,874개 패시지에 약 80 MB가 필요할 것이므로, 압축된 다중 벡터 인덱스는 이미 사용자들이 운영하는 밀집 인덱스와 비슷한 영역에 위치하게 됩니다. 토큰 풀링(Token Pooling)은 그 어떤 것보다 먼저 벡터 수를 줄이고, 검색(Retrieve) 및 재순위화(Rerank)는 아예 인덱스 구축을 피합니다.
PyLate이 이 게시물 전반에 걸쳐 언급되므로 간단히 요약하겠습니다. Sentence Transformers는 밀집(dense) 모델과 희소(sparse) 모델은 처리했지만, 후기 상호작용(late interaction)은 처리하지 못했습니다. 그래서 LightOn은 그 격차를 메우기 위해 PyLate을 구축했으며, 이 모델들이 필요로 하는 학습(training), 추론(inference), 검색(retrieval) 구성 요소를 추가했습니다. 아래에서 로드하게 될 내용의 상당 부분은 이를 통해 훈련되었으며, LightOn은 fast-plaid와 같은 생태계도 구축했는데, 이는 인덱싱(Indexing) 섹션에서 등장하는 후기 상호작용 인덱스입니다. v6.0 버전부터는 이러한 기능들이 Sentence Transformers 자체에 포함되어 있습니다.
이러한 트레이드오프를 염두에 두고 모델을 실행해 보겠습니다.
다중 벡터(Multi-vector) 모델은 일반 설치로 작동합니다:
pip install -U sentence-transformers
ColPali 스타일의 시각 문서 검색을 위해서는 이미지 종속성도 필요합니다 (모든 추가 기능에 대한 설치는 Installation 섹션을, 범용적인 다중 모드 임베딩 및 재순위화 모델은 Multimodal Embedding & Reranker Models 섹션을 참조하십시오):
pip install -U "sentence-transformers[image]"
Sentence Transformers v6.0은
transformers
v5.x,
torch
2.2+ 및
huggingface-hub
v1.x를 필요로 합니다. 이 중 어느 하나라도 버전을 낮게 고정하려면, 먼저 업그레이드를 계획하십시오. 모든 변경 사항 목록은 마이그레이션 가이드(Migration Guide)를 참조하십시오.
다중 벡터 모델을 로드하는 것은 다른 Sentence Transformers 모델을 로드하는 것과 정확히 동일하게 보입니다:
from sentence_transformers import MultiVectorEncoder
model = MultiVectorEncoder("lightonai/LateOn")
작동하는 모델을 찾으려면 Hub에서 multi-vector 및 sentence-transformers 태그를 찾아보십시오. 이 두 가지 태그가 붙은 모든 모델은 해당 모델이 PyLate 체크포인트, Stanford-NLP ColBERT 체크포인트, 또는 시각 문서 검색을 위한 ColPali 계열 모델 중 무엇으로 시작했는지에 관계없이 위의 코드로 로드됩니다. 저희는 이 기능을 가진 모든 모델에 해당 태그를 추가하기 위해 생태계를 작업하고 있으며, 따라서 목록은 계속 늘어나고 있습니다.
아래쪽의 MultiVectorEncoder는 시간이 지나면서 이러한 체크포인트들이 게시된 모든 형식을 읽어 들입니다. 따라서 PyLate 및 Stanford-NLP 체크포인트는 아직 태그가 추가되지 않은 곳에서도 직접 로드됩니다:
from sentence_transformers import MultiVectorEncoder
# 네이티브 Sentence Transformers 체크포인트. PyLate은 동일한 스키마를 기반으로 하므로,
# 모든 PyLate 체크포인트는 동일하게 로드됩니다
...
시각 문서 검색 모델은 예외입니다. ColPali 계열 체크포인트는 colpali-engine 자체 형식으로 제공되며, Sentence Transformers가 사용할 수 있는 정보를 담고 있지 않기 때문에, 각각이 로드되기 전에 저장소에 작은 설정(configuration)을 추가해야 합니다. 대부분의 작업은 완료되었으며 병합되기를 기다리고 있습니다. 현재 상태와 오늘 어떻게 로드할 수 있는지에 대해서는 지원되는 모델(Supported Models)을 참조하십시오.
다중 벡터 모델은 체크포인트마다 다른 몇 가지 레시피 노브(recipe knobs)를 가지고 있습니다: 쿼리와 문서에 대한 마커 접두사, 길이 제한(length caps), 쿼리가 [MASK] 토큰으로 패딩되는지 여부, 그리고 문서를 점수화할 때 건너뛰는 토큰이 무엇인지 등입니다. 이 모든 것은 모듈 설정(module configs)에 존재하므로, print(model)을 실행하면 됩니다.
어떤 토큰을 로드했는지 정확히 보여줍니다. 여기는 원래의 ColBERTv2 체크포인트로, 모든 쿼리를 정확히 32개의 토큰으로 패딩하고 문서를 180개에서 자릅니다:
from sentence_transformers import MultiVectorEncoder
model = MultiVectorEncoder("colbert-ir/colbertv2.0")
print(model)
...
이것이 전형적인 ColBERT 파이프라인입니다: 문맥화된 토큰 임베딩을 생성하는 Transformer, 각 토큰을 128차원으로 투영하는 토큰 레벨의 Dense, 점수 계산 중 어떤 토큰을 포함할지 결정하는 MultiVectorMask, 그리고 토큰 레벨의 Normalize입니다. 다른 체크포인트들은 서로 다른 값들로 채워져 있습니다. lightonai/GTE-ModernColBERT-v1은 쿼리 확장(query expansion) 없이, 동일한 네 가지 모듈을 사용하며 각각 [Q]와 [D] 프롬프트와 48 및 300의 최대 길이를 가집니다.
이 모든 것을 건드릴 필요는 거의 없습니다. 왜냐하면 출시되는 모든 체크포인트가 자체적으로 설정하기 때문입니다. 커스텀 모델을 베어백본(bare backbone)에서 구축할 때 중요하며, 이는 '커스텀 모델 생성(Creating Custom Models)'에서 다루고 있습니다.
하지만 하나의 값은 사용자 데이터와 비교해 볼 가치가 있습니다. 바로 document_length인데, 이것이 트렁케이션(truncates)을 수행하므로 그 이후의 내용은 인덱스에 절대 도달하지 못합니다. 예를 들어, LateOn의 300 최대 길이를 거치는 662개 토큰 분량의 구절은 273개의 벡터로 돌아오고, 나머지 구절은 단순히 사라집니다. 대부분의 이 체크포인트들은 짧은 구절을 기반으로 학습되었기 때문에, 만약 사용자의 청크(chunks)가 최대 길이보다 길다면, `encode_document(..., processing_kwargs={
정확한 임베딩을 얻으려면 다음이 필요합니다:
from sentence_transformers import MultiVectorEncoder
model = MultiVectorEncoder("lightonai/mLateOn")
queries = ["What is the capital of France?"]
...
돌려받는 것은 각 입력마다 하나씩, 각각 (num_tokens, embedding_dim) 형태를 가진 2D 텐서의 리스트입니다. 밀집 임베딩(dense embeddings)과는 달리, 모든 입력이 고유한 토큰 수를 가지고 있기 때문에 이들을 하나의 직사각형 텐서로 쌓을 수 없습니다. 두 번째 문서는 첫 번째 문서보다 길기 때문에 더 높은 행을 가진 행렬 형태로 반환됩니다.
각 호출은 모델 자체의 레시피를 적용합니다. encode_query는 쿼리 마커(query marker)를 접두사로 붙이고, 체크포인트가 요청하는 경우 쿼리를 고정된 길이로 확장하며, 쿼리 길이에 맞춰 잘라냅니다. encode_document는 문서 마커(document marker)를 접두사로 붙이고, 문서 길이에 맞춰 자르며, 점수 계산 마스크에서 스킵 리스트된 토큰(구두점 등, 대부분의 체크포인트에 해당)을 제거합니다.
일반적인 encode() 인자들은 모두 여전히 적용되므로, batch_size, show_progress_bar
AI 자동 생성 콘텐츠
본 콘텐츠는 Hugging Face Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기