핵심 질문: 정규화된 벡터가 왜 “단위 구 표면”에만 박히는가
임베딩(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 Similarity | pgvector <=> (Cosine Distance) |
|---|---|---|
| 0도 (동일) | cos(0°) = 1.0 | 1 - 1.0 = 0.0 (최소 거리) |
| 90도 (무관) | cos(90°) = 0.0 | 1 - 0.0 = 1.0 |
| 180도 (정반대) | cos(180°) = -1.0 | 1 - (-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이 코드는 “코사인 유사도가 양수로만 나온다”는 가정 위에 돌아감. 가정이 깨지는 두 시나리오:
- 임베딩 모델 교체: OpenAI → 다른 모델로 갈아끼우면 이방성이 다른 분포가 튀어나옴
- 극단적 입력: 빈 문자열, 특수문자 덩어리(
ㄴㅇㅊIOAerln!@#$) 같은 경우 이론상 음수 가능
방어 코드 (권장 패치)
GREATEST(1 - (m.embedding <=> :query_vec), 0) AS relevanceGREATEST(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)추가 권장 - 임베딩 모델 교체 계획 있는가? → “네” 라면 이방성 재측정 후 임계값 재튜닝 필수