차원 수

문건희·2025년 8월 31일

임베딩 차원 수, 제대로 고르기 — 384 vs 768 vs 1024

“임베딩 차원이 높을수록 무조건 좋을까?”
RAG/검색/추천에서 자주 받는 질문을, 숫자·차원·메모리·속도 중심으로 정리했습니다.

TL;DR

  • 차원 수(=d) 는 한 문장을 표현하는 벡터 좌표의 길이. 예: SBERT 768차원 → 길이 768의 실수 벡터.

  • 차원이 높으면 표현력↑(미세한 의미 구분), 하지만 메모리·연산비용·“허브니스(hubness)” 리스크↑.

  • 차원이 낮으면 메모리·속도↑(빠름), 하지만 정보 손실로 성능↓ 가능.

  • 실무 기준:

    • 문서 수 적고 속도 여유 → 768~1024
    • 대규모(≥ 수백만)·지연 민감 → 384~512 + 2단계 검색(저차원 1차 후보 + 고차원 재랭크)
    • Elasticsearch 사용 시 dense_vector.dims == 모델 차원 꼭 맞추기.

1) 임베딩 차원이란?

  • 한 문장/청크를 d차원 실수 벡터로 매핑한 결과의 길이(d).
    예) snunlp/KR-SBERT-...768차원
  • 이 d는 모델이 학습 중 “의미를 담을 수 있는 용량(표현력)”과 연관.

직관:

  • d가 크면 더 많은 “축”으로 의미를 분해해 담을 수 있음 (세밀한 구분).
  • d가 작으면 압축이 심해져 서로 다른 의미가 섞일 수 있음(충돌/정보손실).

2) 차원 수가 클수록 생기는 일

장점

  • 표현력/세분화↑: 유사하지만 다른 개념을 더 잘 분리(특히 다국어, 도메인 다양, 문맥 복잡).
  • 충분한 학습 데이터가 있으면 일반화 성능↑(문장 의미의 다양한 측면을 담을 수 있음).

단점

  • 메모리·스토리지↑: N개 문서면 저장량이 ≈ N × d × 4byte(float32) 로 증가.
  • 거리 계산 비용↑: ANN/Brute-force 모두 연산량이 d에 선형으로 증가.
  • Hubness↑: 고차원에서 특정 벡터가 **과도하게 “가까운 이웃”**으로 자주 등장하는 현상 → 검색 랭킹이 왜곡될 수 있음.
  • 데이터가 적을수록 과적합 위험: d가 크면 모델/인덱스가 “잡음”까지 설명하려고 함.

3) 차원 수가 작을수록 생기는 일

장점

  • 빠름 & 가벼움: 도커/서버리스/모바일 등에서 유리.
  • 대규모 인덱스 운영 비용↓: 메모리/디스크·네트워크 전송량↓, 캐시 효율↑.

단점

  • 정보 손실: 문장 의미의 중요한 축이 사라져 정확도/재현율 저하 가능.
  • 도메인 다양성이 큰 경우 분리 한계.

4) 언젠가 들어본 그 법칙: “차원(d) vs 데이터 수(n)”

  • 이론적으로, 거리(유사도)를 보존하려면 필요한 차원 수가 대략 데이터 수의 로그에 비례(존슨–린덴스트라우스 류의 직관).
  • 하지만 학습된 임베딩은 단순 랜덤 투영이 아니기 때문에, 이 법칙은 엄밀한 규칙이라기보단 감각으로만 참고.

실무 감

  • n이 아주 크다(≥ 수백만) → d를 너무 크게 잡으면 비용이 눈덩이.
  • n이 중간(수만~수십만) → 384~768선에서 “성능 vs 비용”의 엘보(elbow) 지점을 찾아 실험.
  • n이 작다(≤ 수천~수만) → 768도 무난. 다만 과적합·허브니스는 체크.

5) 메모리 & 속도, 숫자로 감 잡기

저장량(대략)

  • 공식: N × d × 4 bytes (float32 가정)

예) N = 100,000 문서

  • d=384 → 100,000 × 384 × 4 = 153,600,000 bytes ≈ 146.5 MB
  • d=768 → 100,000 × 768 × 4 = 307,200,000 bytes ≈ 293.0 MB
  • d=1024 → 100,000 × 1024 × 4 = 409,600,000 bytes ≈ 390.6 MB

팁:

  • float16(2 bytes)나 **양자화(quantization, PQ/OPQ/INT8)**를 쓰면 절반 이하로 줄일 수 있음(검색 정확도 하락과 트레이드오프).
  • Elasticsearch dense_vector는 내부 구현 제약이 있어 저정밀도 저장/양자화는 보통 FAISS/HNSW 라이브러리가 더 유연함.

연산(대략)

  • 코사인/내적 1회 비용 ∝ d.
  • KNN(HNSW)도 후보 간 거리 계산량이 d에 비례 → d가 두 배면 대략 연산량도 두 배로 본다(상수항 제외).

6) 언제 차원을 키우고/줄일까 (의사결정 표)

상황추천 차원/전략이유
문서 수 적음(≤ 수만), 다국어·전문용어 많음768~1024의미 공간을 넉넉히, 표현 손실 방지
대규모(≥ 수백만), 응답시간 엄격384~512 + 2단계 검색1단계 저차원 ANN으로 후보 200개, 2단계 고차원/크로스인코더 재랭크
메모리 빡셈(EC2 t3.small 등)256~384 + 양자화비용 절감, 충분한 품질이면 승
짧은 질의·짧은 청크(FAQ, 타이틀 중심)384~512과도한 차원은 이득↓ 비용↑
길고 복잡한 설명/약관(한국어 문단 위주)768문맥 풍부, 한국어 SBERT 768과 궁합

7) Elasticsearch에서의 포인트

  • dense_vectordims임베딩 길이와 반드시 같아야 색인됨.

  • Cosine 쓰려면 L2 정규화 권장(정규화 후 dot = cosine).

  • 대규모·지연 민감이면 knn_vector + HNSW(근사 KNN) 고려.

  • 스코어링 예시(정규화되어 있다면):

    "script": {
      "source": "cosineSimilarity(params.q, 'embedding') + 1.0",
      "params": {"q": [ ... 768 floats ... ]}
    }
  • 두 단계 검색:

    1. knn_vector(저차원 or 양자화)로 Top-k 후보
    2. 후보에 대해 고차원 벡터 혹은 Cross-Encoder로 재랭크

8) 차원을 “바꿔보며” 성능-비용 곡선 보기 (실험 프로토콜)

  1. 기준 임베딩(예: 768) 만들기
  2. PCA로 차원 축소(예: 128/256/384/512)
  3. 동일 인덱싱 조건에서 Recall\@k / NDCG\@k / MRR 측정
  4. 엘보 지점(성능 크게 떨어지지 않으면서 비용 낮은 지점)을 채택
  5. 필요한 경우 두 단계 검색으로 타협
from sklearn.decomposition import PCA
import numpy as np

base_vecs = ...  # (N, 768)
for d in [128, 256, 384, 512]:
    pca = PCA(n_components=d, random_state=42)
    vecs_d = pca.fit_transform(base_vecs)  # (N, d)
    # 1) 이 vecs_d로 인덱싱
    # 2) 동일한 쿼리셋으로 Recall@10, NDCG@10 측정
    # 3) 비용/성능 비교해 선택

PCA는 “기존 768을 줄였을 때”의 실험용. 실제론 저차원 모델 자체를 쓰는 편이 관리가 쉬움(예: MiniLM 384).


9) 차원과 관련된 자주 하는 실수 체크리스트

  • 모델 바꿨는데 ES dims 안 바꿈 → 색인 에러

  • 임베딩 정규화 없이 cosine → 스코어 뒤틀림

  • 메타데이터 타입 충돌(문서마다 같은 키인데 숫자/문자 섞임) → 색인 실패

  • Hubness 무시 → 일부 문서가 과도하게 상위에 노출

    • ↳ 완화: 정규화, 차원 축소, 거리 보정(상호근접도/Mutual Proximity) 등

10) 코드 스니펫 모음

(A) 차원 자동 감지 → ES 매핑 자동 설정

from langchain_community.embeddings import HuggingFaceEmbeddings
from elasticsearch import Elasticsearch

model_name = "snunlp/KR-SBERT-V40K-klueNLI-augSTS"
emb = HuggingFaceEmbeddings(model_name=model_name)

probe = emb.embed_query("차원 확인")
dim = len(probe)  # 예: 768

es = Elasticsearch("http://localhost:9200")
index = "my_index"

if es.indices.exists(index=index):
    es.indices.delete(index=index)

es.indices.create(
    index=index,
    mappings={
        "properties": {
            "text": {"type": "text", "analyzer": "nori"},
            "embedding": {"type": "dense_vector", "dims": dim}
        }
    }
)
print("dims =", dim)

(B) L2 정규화 후 색인 (cosine 안정화)

import numpy as np

def l2norm(v):
    v = np.asarray(v, dtype=np.float32)
    n = np.linalg.norm(v)
    return (v / n).tolist() if n else v.tolist()

vecs = [l2norm(v) for v in emb.embed_documents(texts)]
# vecs를 embedding으로 색인하면, dot == cosine

(C) 2단계 검색(개념)

[낮은 차원 / 양자화 인덱스] --Top200--> [고차원 ES/FAISS] --Topk-->
[Cross-Encoder Rerank(Optional)] --Topk-->

11) 추천 선택지(요약)

  • 한국어 문단 중심 RAG + 문서 수: 수만~수십만768차원 무난

  • 문서 수: 수백만+ & 지연 엄격384~512 + 2단계 검색

  • 메모리/비용 제한 → 256~384 + 양자화 고려

  • 결과가 마음에 안 들면, 차원만 바꾸지 말고

    • 모델 변경(학습 데이터/도메인 적합성)
    • 청크 전략(분량/오버랩/레이아웃)
    • 리랭킹(크로스인코더)까지 함께 조정

마무리

임베딩 차원은 “성능 vs 비용”의 손잡이입니다. 데이터 크기, 지연 요구사항, 도메인 복잡성을 동시에 보고, 작은 실험으로 엘보 지점을 찾는 게 가장 빠른 길이에요.
필요하면, 현재 쓰는 인덱스 규모/응답 시간 목표 알려주면 권장 차원 + 인덱싱 전략까지 구체적으로 짜 드릴게!

0개의 댓글