핵심 질문: 정규화된 벡터가 왜 “단위 구 표면”에만 박히는가

임베딩(Embedding, 텍스트를 숫자 배열로 바꾸는 과정) 결과물이 어떻게 코사인 유사도 검색과 맞물리는지 이해하려면, 벡터의 기하학부터 짚어야 함.

벡터(Vector)의 두 성분

  • 벡터는 원점 (0,0)에서 출발하는 화살표임
  • 성분은 딱 두 개: 방향(각도)길이(크기, L2 norm)
  • 예시: “사과”가 [3, 4]로 임베딩되면 원점에서 (3,4)로 뻗는 화살표, 길이는 $\sqrt{3^2 + 4^2} = 5$

정규화(Normalization)의 정체

  • 정규화란 방향은 건드리지 않고 길이만 강제로 1로 맞추는 연산임 (v / ‖v‖)
  • [3, 4][0.6, 0.8] 로 깎임 (새 길이 $\sqrt{0.36 + 0.64} = 1$)
  • OpenAI text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002모델 레벨에서 이미 L2 정규화된 벡터를 반환함 (공식 문서 명시)

단위 구(Unit Hypersphere) 표면

  • 모든 벡터의 길이가 정확히 1이 되면, 각 벡터의 끝점은 원점에서 거리 1인 점들의 집합이 됨
  • 이 집합이 바로 단위 구의 표면임 (2차원이면 원의 테두리, 3차원이면 공 껍질, 1536차원이면 초구(Hypersphere, 고차원 구)의 표면)

한마디 요약: OpenAI 임베딩 결과는 “1536차원 공의 껍질 위에 뿌려진 별자리”로 봐야 함.


유클리디안 vs 코사인: 표면 위에서는 순위가 같음

단위 구 표면에 박혀 있으면 두 점을 직선으로 재도, 원점에서 바라본 각도로 재도 순위가 동일하게 나옴. 이건 수학적으로 증명되는 성질임.

수식으로 확인

두 단위 벡터 a, b (‖a‖=‖b‖=1)가 있을 때:

유클리디안 거리²: ‖a - b‖² = ‖a‖² - 2a·b + ‖b‖² = 2 - 2(a·b)
코사인 거리      : 1 - (a·b)/(‖a‖‖b‖) = 1 - a·b

둘 다 a·b(내적)에 대해 단조 감소 관계임. 내적이 클수록(= 유사할수록) 두 거리 값이 같이 작아지니, 순위(ranking)가 뒤집히는 일이 수학적으로 불가능함.

그럼 왜 코사인(<=>)을 쓰나

합산(Hybrid Scoring) 공식에 꽂기 좋아서.

연산자거리 타입출력 범위합산 친화성
<->L2 (유클리디안)[0, ∞)나쁨 (차원 올라가면 최대값이 $\sqrt{d}$까지 팽창)
<#>내적 부호 반전(-∞, ∞)최악 (부호도 바뀜, 음수 처리 번거로움)
<=>코사인[0, 2]양호 (양수 고정, 정규화 벡터면 [0, 1] 근방)
<+>L1 (택시캡)[0, ∞)나쁨

한마디 요약: 순위 품질이 같으면, 나머지 점수(최신성·중요도·키워드)와 같은 자릿수(0~1) 로 떨어지는 <=>가 이긴 것임.


pgvector <=>의 실제 출력 범위: [0 ~ 2]

pgvector의 <=>는 “유사도”가 아니라 “거리”임. 혼동하면 안 됨.

수학적 코사인 값 vs pgvector 코사인 거리

두 벡터 각도수학 Cosine Similaritypgvector <=> (Cosine Distance)
0도 (동일)cos(0°) = 1.01 - 1.0 = 0.0 (최소 거리)
90도 (무관)cos(90°) = 0.01 - 0.0 = 1.0
180도 (정반대)cos(180°) = -1.01 - (-1.0) = 2.0 (최대 거리)

복원 공식

pgvector가 뱉는 거리를 다시 “유사도 점수”로 되돌리려면:

1 - (embedding <=> :query_vec) AS similarity

이 식을 전개하면 1 - (1 - cos) = cos, 즉 수학적 코사인 유사도가 그대로 돌아옴. 범위는 원복되어 [-1 ~ 1].


실제 OpenAI 임베딩은 왜 항상 [0, 1] 사이에서 노는가

이론상 유사도가 -1까지 떨어질 수 있는데, 실제 운영 환경에서 음수가 거의 안 나오는 이유가 두 개 있음.

원인 1: 고차원 공간의 “차원의 저주(Curse of Dimensionality)”

  • 1536차원 같은 초고차원에서 무작위로 뽑은 두 단위 벡터의 코사인 값은 확률적으로 0 근처에 몰림
  • 이론적 증명: 랜덤 단위 벡터의 내적 분포는 평균 0, 분산 1/d인 정규분포에 수렴 (d=차원)
  • 1536차원에서 무작위 두 벡터의 코사인이 ±0.05 벗어날 확률이 이미 매우 작음

원인 2: 학습 데이터 편향 (“꼬깔콘 분포”)

  • 실제 자연어 임베딩을 2D로 사영해 보면, 점들이 좁은 원뿔(cone) 영역에 뭉쳐 있음 (anisotropy, 이방성 현상)
  • 아무리 관련 없는 단어 쌍(“사랑” vs “쓰레기통”)도 코사인 유사도가 대략 0.05 ~ 0.3 수준이지, 음수로 떨어지는 경우는 사실상 관측이 안 됨

한마디 요약: 수학적으로 -1이 가능해도 OpenAI 모델 하에선 [0, 1] 구간으로 수렴하니, 프로젝트 코드가 “암묵적 양수 가정”으로 굴러감.


프로젝트 코드의 잠재적 결함: GREATEST 방어 부재

src/db/npc_npc_memory_manager.py:492 의 쿼리:

1 - (m.embedding <=> CAST(:query_embedding AS vector)) AS relevance

이 코드는 “코사인 유사도가 양수로만 나온다”는 가정 위에 돌아감. 가정이 깨지는 두 시나리오:

  1. 임베딩 모델 교체: OpenAI → 다른 모델로 갈아끼우면 이방성이 다른 분포가 튀어나옴
  2. 극단적 입력: 빈 문자열, 특수문자 덩어리(ㄴㅇㅊIOAerln!@#$) 같은 경우 이론상 음수 가능

방어 코드 (권장 패치)

GREATEST(1 - (m.embedding <=> :query_vec), 0) AS relevance

GREATEST(x, 0)은 “둘 중 큰 값 반환”, 즉 음수가 나오면 강제로 0으로 클램프(Clamp, 하한 고정) 하는 SQL 함수임.

점수 합산 흐름 비교

[현재 코드: 음수 방어 없음]
 "사과가 맛있다" vs "ㄴㅇㅊIOAerln!@#$"
   ├─ cosine_similarity = -0.3  (극단적 입력)
   ├─ relevance = -0.3
   └─ final_score = 0.15*최신성 + 0.15*중요도 + 0.50*(-0.3) + 0.20*키워드
                    → 마이너스 오염 발생

[GREATEST(..., 0) 적용 후]
   ├─ cosine_similarity = -0.3
   ├─ relevance = GREATEST(-0.3, 0) = 0
   └─ final_score 합산에서 음수 기여분 차단

왜 “잠재적 결함(Latent Defect)“인가

  • “통계적으로 발생 안 함” ≠ “절대 발생 안 함”
  • 현재 운영 중인 OpenAI 모델이 아닌 다른 모델로 교체하는 순간 터질 수 있음
  • 터지기 전에 GREATEST(..., 0) 한 줄로 미리 막는 게 비용 0에 가까운 헤징임

직접 확인 가이드

PostgreSQL에 pgvector 설치되어 있다면 실측으로 확인 가능:

-- 1. 두 벡터의 코사인 거리
SELECT '[1,0,0]'::vector <=> '[0,1,0]'::vector AS cos_dist;
-- 결과: 1.0 (90도)
 
-- 2. 정반대 방향
SELECT '[1,0,0]'::vector <=> '[-1,0,0]'::vector AS cos_dist;
-- 결과: 2.0 (180도)
 
-- 3. OpenAI 임베딩 샘플로 이방성 체감 (실제 두 단어 임베딩 두 개 뽑아서)
SELECT a.embedding <=> b.embedding AS dist
FROM memories a, memories b
WHERE a.id <> b.id
LIMIT 100;
-- 대부분 0.5 ~ 0.9 구간에 분포 (즉 유사도 환산 시 0.1 ~ 0.5)

일상 비유: 플라네타륨과 체감 온도

단위 구 표면 = 플라네타륨 천장

[플라네타륨 정중앙(원점)에 내가 서 있음]
1. OpenAI 임베딩 = 레이저 포인터로 반지름 1m 천장에 별(단어)을 쏨
2. 레이저는 무조건 천장 표면에만 맺힘 (공중에 떠 있거나 밖으로 나가지 못함)
3. 질문(A별)과 기억(B별) 사이 각도가 좁을수록 두 별의 표면 거리도 좁음

코사인 유사도 = 온도계, 코사인 거리 = 불쾌지수

[수학 세상: Cosine Similarity]
  영상 100도(1.0)  : 찰싹 붙음      (0도)
  0도(0.0)         : 무관           (90도)
  영하 100도(-1.0) : 극혐           (180도)

[pgvector 세상: <=> Cosine Distance]
  거리 0.0 : 찰싹 붙음 (최고)
  거리 1.0 : 1미터 거리 (무관)
  거리 2.0 : 지구 반대편 (정반대)

포함 관계 정리

거리 기반 유사도 검색
  └─ 벡터 공간 모델 (Vector Space Model)
       ├─ 정규화 벡터 공간 (단위 구 표면) ← OpenAI 임베딩 위치
       │    ├─ 코사인 거리 <=>  ← 이 노트의 주인공
       │    ├─ 유클리디안 거리 <-> (순위 동일)
       │    └─ 내적 <#>
       └─ 비정규화 벡터 공간 (길이 제각각)
            └─ 코사인/유클리디안 순위 다르게 나옴

<=>, <->, <#>, <+> 는 전부 pgvector의 거리 연산자임. 단위 구에 박힌 벡터끼리는 이 중 뭘 써도 순위는 같지만, 출력 범위와 합산 친화성이 달라서 <=>가 사실상 독주 중.


결론

  • 벡터가 정규화되어 있는가? → “네” 라면 코사인/유클리디안 순위 동일
  • 점수를 다른 항목과 합산할 건가? → “네” 라면 무조건 <=> 선택 (0~1 범위 친화)
  • 음수 방어 코드 있는가? → “아니오” 라면 GREATEST(..., 0) 추가 권장
  • 임베딩 모델 교체 계획 있는가? → “네” 라면 이방성 재측정 후 임계값 재튜닝 필수

관련: BM25와 PGroonga의 관계, retriever_추천