
비트 수가 아닌 충실도 예산으로 임베딩 저장하기 — Go를 위한 손실 벡터 코덱
요약
Go 언어를 위한 손실 벡터 코덱인 qdf를 소개합니다. 비트 수 대신 코사인 유사도와 같은 충실도 예산을 기준으로 임베딩 데이터를 압축하여 저장 공간을 최적화합니다.
핵심 포인트
- 비트 수가 아닌 목표 충실도(예: 코사인 유사도)를 기준으로 압축 수행
- Hadamard 회전, 양자화, 엔트로피 코딩 파이프라인 활용
- 임베딩 검색의 기하학적 구조를 유지하며 데이터 크기 최소화
- Go 환경에서 사용 가능한 스키마리스 이진 직렬화 도구
임베딩은 []float32입니다. 백만 개의 임베딩을 저장하면 512 MB의 float32 데이터가 되며, 여러분은 이 데이터의 대부분을 최근접 이웃 (nearest neighbours)을 검색하는 데 소비하게 됩니다. 하지만 이 512 MB에는 중요한 사실이 있습니다: 그중 거의 어느 것도 핵심적인 역할을 하지 않는다는 점입니다. 최근접 이웃 검색은 벡터의 정확한 비트가 아니라 벡터의 기하학적 구조 (geometry) — 즉, 상대적인 각도 — 에만 관심이 있습니다. 비트 단위로 정확하게 저장한다면, 검색 과정에서 전혀 사용되지 않을 정밀도에 대해 비용을 전액 지불하고 있는 셈입니다.
Go를 위한 스키마리스 (schemaless) 이진 직렬화 도구인 qdf는 이러한 관찰을 바탕으로 선택 가능한 손실 벡터 코덱 (lossy vector codec)을 구축했습니다. qdf는 사용자에게 몇 비트를 유지할지 묻는 대신, 어떤 충실도 (fidelity)가 필요한지 — 예를 들어 "코사인 유사도 (cosine similarity) ≥ 0.99 유지" — 를 묻고, 그 기준을 충족하는 데 필요한 최소한의 바이트를 사용합니다. 이 포스트에서는 이 조절 노브 (knob), 수치들, 그리고 언제 이 기능을 켜야 하는지(혹은 켜지 말아야 하는지)에 대해 다룹니다.
노브는 비트 폭이 아니라 충실도 예산입니다
제가 사용해 본 다른 모든 양자화기 (quantizer)들은 int8, 4-bit, 혹은 특정 개수의 중심점 (centroids)과 같이 표현 방식을 선택하도록 강요합니다. 이는 거꾸로 된 방식입니다. 여러분은 비트 수에는 관심이 없고, 검색이 여전히 잘 작동하는지에 관심이 있기 때문입니다. qdf는 이를 뒤집었습니다. 여러분이 출력 품질에 대한 **예산 (budget)**을 설정하면 코덱이 비트를 선택합니다:
enc := qdf.NewEncoderWith(qdf.OptBalanced | qdf.OptLossyVec)
enc.SetVectorBudget(qdf.MinCosine(0.99)) // 또는 MaxRelError / TargetSNR
예산을 설정하는 세 가지 방법:
MinCosine(0.99)— 원본 벡터와 재구성된 벡터 사이의 최소 코사인 유사도 (cosine similarity)를 제한합니다. 임베딩에 적합한 방식입니다. ANN 인덱스가 비교하는 것은 코사인 유사도이기 때문입니다.MaxRelError(1e-3)— 벡터당 상대적 L2 오차 (relative L2 error)를 제한합니다. 방향뿐만 아니라 크기 (magnitude)가 중요한 경우에 사용합니다.TargetSNR(40)— 신호와 유사한 float 컬럼의 경우, dB 단위의 신호 대 잡음비 (signal-to-noise ratio, SNR)를 목표로 합니다.
이 코덱은 []struct 내의 []float32 / []float64 컬럼만을 처리하며, 헤더 비용을 상쇄할 수 있을 만큼 충분히 긴 슬라이스에 대해서만 작동합니다 (짧은 벡터는 자동으로 무손실 경로로 전환됩니다). 구조체의 나머지 요소들 — ID, 메타데이터 등 — 은 평소와 같이 무손실로 인코딩됩니다.
바이트를 사용하는 방식
내부적인 파이프라인은 회전(rotate) → 양자화(quantize) → 엔트로피 코딩(entropy-code) 순으로 진행되며, 결과물이 결코 더 커지지 않도록 하는 폴백(fallback) 메커니즘을 갖추고 있습니다.
- **하다마르 회전 (Hadamard rotation)**은 각 벡터의 에너지를 모든 차원에 고르게 분산시킵니다. 이는 조용하지만 강력한 일꾼 역할을 합니다. 몇 개의 큰 크기를 가진 성분들을 많은 유사한 성분들로 변환하여, 양자화 오차를 등방성(isotropic)으로 만들며, 결정적으로 코덱이 "매끄러운" 벡터와 적대적인(adversarial) 벡터 모두에서 동일하게 동작하도록 만듭니다 (이에 대한 자세한 내용은 아래에서 다룹니다).
- 스칼라(scalar) 또는 E8-격자(E8-lattice) 양자화기는 회전된 성분들을 설정한 예산(budget)에 맞춘 그리드(grid)로 매핑합니다. 격자(lattice) 옵션은 회전된 성분들이 구(sphere) 근처에 군집을 이룬다는 점을 활용하여, 동일한 충실도를 더 적은 비트에 담아냅니다.
- **정적 rANS 엔트로피 패스 (static rANS entropy pass)**는 양자화된 심볼들로부터 잔여 중복성(residual redundancy)을 짜냅니다.
- 결코 더 커지지 않는 폴백 (Never-larger fallback): 손실 압축 본문 전체가 일반 무손실 인코딩보다 커지는 경우 — 이는 매우 작거나 이미 압축된 컬럼에서 발생할 수 있습니다 — qdf는 대신 무손실 바이트를 전송합니다. 코덱을 활성화한다고 해서 출력 데이터가 절대 늘어나지 않습니다. (이는 포맷의 나머지 부분에도 적용되는 동일한 원칙입니다. 코덱 선택 관련 기술 문서를 참조하세요.)
수치 데이터 — 직접 재현해 보세요
다음은 복사하여 실행할 수 있는 테스트 코드입니다. 이 코드는 코퍼스(corpus)를 구축하고, 코사인 예산(cosine budget)을 탐색하며, 모든 벡터에서 실제로 달성된 최악의 코사인 값을 측정합니다. 평균이 아니라 최악의 값을 측정하는 이유는, 아무것도 바닥 아래로 떨어지지 않아야만 그 바닥이 진정한 바닥(floor) 역할을 할 수 있기 때문입니다.
enc := qdf.NewEncoderWith(qdf.OptBalanced | qdf.OptLossyVec)
enc.SetVectorBudget(qdf.MinCosine(target))
_ = enc.EncodeValue(docs)
...
ubuntu-latest 환경에서 float32 타입의 벡터 2,000개를 대상으로 실행했습니다. 두 가지 코퍼스(Corpora)를 사용했습니다: 하나는 매끄러운(smooth) 코퍼스(정현파(sinusoids) — 모든 양자화(quantizer) 논문이 사용하는 유리한 사례)이고, 다른 하나는 무작위 단위(random-unit) 코퍼스(가우시안(Gaussian), L2-정규화(L2-normalized) — 본질적으로 압축이 불가능하며 실제 임베딩 모델이 생성하는 것과 훨씬 유사한, 정직한 최악의 사례)입니다.
128 차원 (무손실 기준선: 벡터당 520 B):
| 예산 (budget) | 무작위 단위 B/vec | 최악의 코사인 유사도 | 무손실 대비 |
|---|---|---|---|
cos≥0.99 | 72.3 | 0.9955 | −86% |
| ... | |||
| 768 차원 (무손실 기준선: 벡터당 3080 B): |
| 예산 (budget) | 무작위 단위 B/vec | 최악의 코사인 유사도 | 무손실 대비 |
|---|---|---|---|
cos≥0.99 | 407 | 0.9914 | −87% |
| ... | |||
![]() |
정직한 결과이기에 강조할 만한 두 가지 사항이 있습니다:
- 예산이 유지됩니다.
cos≥0.99설정 시, 2,000개 중 최악의 벡터가 0.9914–0.9955에 도달했습니다. 매번 설정한 하한선(floor) 위를 유지합니다. 조절 노브(knob)가 말하는 그대로 동작합니다. - 무작위 ≈ 매끄러움. 동일한 실행에서 매끄러운 코퍼스는 벡터당 72.1 B로 압축되었고, 무작위 단위는 72.3 B로 압축되었습니다 — 차이는 0.3%에 불과합니다. 대부분의 양자화기(quantizers)는 압축 불가능한 입력에서 무너지지만, 이 코덱이 무너지지 않는 이유는 Hadamard 회전(Hadamard rotation) 덕분입니다. 만약 어떤 벡터 코덱이 매끄러운 합성 데이터(synthetic data)에 대한 수치만 발표한다면, 의심해 보아야 합니다. 이 결과들은 서로 노이즈 범위 내에 있습니다.
cos≥0.99에서 128차원 벡터는 float32의 32비트 대비 차원당 약 4.5비트 수준으로 떨어집니다. 이는 대부분의 인덱스에서 recall@10으로 측정하기조차 힘든 코사인 유사도 손실을 감수하면서도 7배의 축소를 달성함을 의미합니다.
언제 사용할 것인가 — 그리고 언제 사용하지 말 것인가
| 상황 | 판결 |
|---|---|
| ANN 검색 (ANN retrieval)을 위한 임베딩 저장/전송 | Yes — 코사인 유사도 (cosine similarity)는 바로 이 예산이 방어하고자 하는 지표입니다. |
| ... |
경험 법칙(rule of thumb): 만약 당신의 파이프라인이 이미 임베딩을 근사치 (approximate)로 취급하고 있다면 — 그리고 ANN 검색이 그러하다면 — 임베딩을 비트 단위로 정확하게 (bit-exact) 저장하는 것은 비용만 지불하고 버려지는 정밀도입니다. 당신의 검색 시스템이 수용할 수 있는 코사인 하한선 (cosine floor)을 선택하고, 코덱이 비트를 찾도록 맡기세요.
직접 시도해보기
go get github.com/alex60217101990/qdf
go run github.com/alex60217101990/qdf/examples/embeddings
실행 가능한 examples/embeddings는 위에서 설명한 테스트 프레임워크 (harness)를 간소화한 것입니다. 당신의 벡터를 교체하고, 예산을 조절하며, 최악의 경우의 코사인 유사도를 관찰하세요. 만약 하한선이 유지되지 않는 코퍼스 (corpus)를 발견한다면, 그것은 버그이며 이를 위한 이슈 템플릿 (issue template)이 준비되어 있습니다. 경험담보다는 측정된 데이터가 더 강력합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기