[CS300 #273] 임베딩 — 의미를 좌표로 옮기는 법
컴퓨터공학 300 주제 시리즈의 273번째 글이다. 전체 지도는 여기.
한 줄 요약
임베딩은 단어·문장·이미지·사용자 같은 이산적인 대상을 의미가 비슷하면 가까운 실수 벡터로 바꾼 것이고, 그 거리(주로 코사인 유사도)로 검색·추천·군집화를 한다.
왜 필요한가
컴퓨터에게 “고양이”와 “강아지”는 그냥 다른 문자열이다. 원-핫 인코딩으로 바꿔도 마찬가지다. 어휘가 5만 개면 각 단어는 5만 차원 벡터에서 한 칸만 1 이고, 모든 단어 쌍의 거리가 똑같다. “고양이와 강아지는 고양이와 주식보다 가깝다”는 사실을 표현할 방법이 없다.
임베딩은 이 문제를 푼다. 수백~수천 차원의 촘촘한 벡터로, 비슷한 것을 가깝게 놓는다. 그러면 의미 검색, 중복 탐지, 추천, 그리고 다음 글들의 RAG 까지 “가까운 것 찾기”라는 하나의 연산으로 해결된다. 신경망 입장에서도 임베딩은 첫 번째 층이다. 언어 모델은 토큰 ID 를 받자마자 임베딩 표에서 벡터를 꺼내 계산을 시작한다.
핵심 개념
분포 가설
“단어의 의미는 그 단어가 함께 쓰이는 이웃으로 알 수 있다.” 언어학자 해리스와 퍼스로 거슬러 올라가는 이 생각이 거의 모든 단어 임베딩의 바탕이다. “우유를 ___ 가 마신다”의 빈칸에 들어갈 수 있는 단어들은 의미도 비슷하다.
만드는 방법의 계보
| 방법 | 아이디어 |
|---|---|
| 동시 출현 행렬 + SVD | 단어×문맥 빈도표를 저차원으로 압축(LSA 계열) |
| word2vec (Mikolov 외, 2013) | 주변 단어 예측(skip-gram) 또는 주변으로 가운데 예측(CBOW)을 얕은 신경망으로 학습 |
| GloVe (Pennington 외, 2014) | 전역 동시 출현 통계를 직접 맞추는 목적 함수 |
| 문맥 임베딩 (BERT 등) | 같은 단어도 문장에 따라 다른 벡터. “배”(과일/선박/신체) 구분 |
| 문장 임베딩 (Sentence-BERT 등) | 문장 전체를 벡터 하나로. 의미 검색에 특화 학습 |
word2vec 은 “왕 − 남자 + 여자 ≈ 여왕” 같은 벡터 산술로 유명해졌다. 의미 관계가 일정한 방향으로 나타난다는 관찰이다. 다만 이런 유추가 항상 성립하는 것은 아니고, 평가 방식에 따라 결과가 부풀려 보일 수 있다는 지적도 있다.
거리 재기
| 척도 | 식 | 특징 |
|---|---|---|
| 코사인 유사도 | a·b / (‖a‖‖b‖) | 방향만 봄. −1~1. 텍스트 임베딩의 표준 |
| 내적 | a·b | 벡터를 정규화했다면 코사인과 같은 순서 |
| 유클리드 거리 | ‖a − b‖ | 크기까지 봄 |
많은 임베딩 모델은 출력 벡터를 길이 1 로 정규화한다. 그러면 세 척도의 순위가 같아져 가장 빠른 내적을 쓰면 된다.
대규모 근접 이웃 검색
벡터 수백만 개에서 가장 가까운 것을 매번 전수 비교하면 느리다. 그래서 근사 최근접 이웃(ANN) 색인을 쓴다. 그래프 기반(HNSW), 군집 기반(IVF), 벡터 양자화(PQ)가 대표다. 정확도를 조금 내주고 속도를 크게 얻는다. 벡터 데이터베이스나 PostgreSQL 의 벡터 확장이 하는 일이 이것이다.
임베딩은 단어만이 아니다
추천 시스템은 사용자와 상품을 같은 공간에 놓는다. 사용자 벡터와 가까운 상품을 추천한다. 이미지와 텍스트를 같은 공간에 놓으면 “빨간 자동차” 라는 문장으로 사진을 검색할 수 있다. 범주형 특징(우편번호, 상품 ID)도 표 데이터 신경망에서는 임베딩으로 넣는다.
직접 해 보기
신경망 없이 “동시 출현 행렬 + SVD” 로 아주 작은 임베딩을 만든다. 문장 10개, 같은 문장 안 앞뒤 두 칸을 문맥으로 센다.
import numpy as np
from itertools import combinations
sents = ["고양이 가 우유 를 마신다", "강아지 가 우유 를 마신다",
"고양이 가 쥐 를 쫓는다", "강아지 가 공 을 쫓는다",
"고양이 가 생선 을 먹는다", "강아지 가 뼈 를 먹는다",
"주식 이 오른다", "환율 이 오른다", "주식 이 내린다", "환율 이 내린다"]
toks = [s.split() for s in sents]
vocab = sorted({w for t in toks for w in t}); idx = {w: i for i, w in enumerate(vocab)}
C = np.zeros((len(vocab), len(vocab)))
for t in toks: # 같은 문장 안 = 문맥(창 크기 2)
for i, w in enumerate(t):
for j in range(max(0, i-2), min(len(t), i+3)):
if i != j: C[idx[w], idx[t[j]]] += 1
P = np.log1p(C) # 빈도 완화
U, S, Vt = np.linalg.svd(P)
E = U[:, :5] * S[:5] # 5차원 임베딩
def cos(a, b): return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
for a, b in [("고양이","강아지"), ("주식","환율"), ("고양이","주식"), ("마신다","쫓는다")]:
print(f"cos({a}, {b}) = {cos(E[idx[a]], E[idx[b]]):.3f}")
실행 결과:
cos(고양이, 강아지) = 1.000
cos(주식, 환율) = 1.000
cos(고양이, 주식) = -0.000
cos(마신다, 쫓는다) = 0.547
아무도 “고양이와 강아지는 비슷하다”고 알려 주지 않았다. 둘 다 “가 우유 를 마신다”, “가 … 을 쫓는다” 같은 자리에 나타났다는 통계만으로 거의 같은 방향의 벡터가 되었다. 주식과 환율도 마찬가지다. 반면 고양이와 주식은 함께 쓰이는 이웃이 전혀 겹치지 않아 직교(0)한다.
고양이와 강아지의 유사도가 1.000 으로 찍힌 것은 둘의 문맥이 거의 같고, 5차원으로 압축하면서 작은 차이(생선/뼈, 쥐/공)가 버려졌기 때문이다. 같은 코드에서 차원을 12 로 늘려 보면 0.842 로 떨어진다. 차원 수는 “얼마나 세밀한 차이를 남길지”를 정하는 손잡이다. 실제 모델은 수십억 문장에서 수백 차원 이상을 학습한다.
현업에서는
- 의미 검색: 사내 문서, 장애 보고서, FAQ 를 문장 임베딩으로 색인하면 “디스크 꽉 참” 으로 검색해도 “스토리지 용량 부족” 문서를 찾는다. 키워드 검색과 함께 쓰는 하이브리드 검색이 흔하다.
- 모델을 바꾸면 전부 다시 만든다. 임베딩 공간은 모델마다 다르다. 모델 버전을 바꾸면 기존 벡터와 새 쿼리 벡터를 비교할 수 없으므로 전체 재색인이 필요하다. 색인에 모델 이름과 버전을 함께 저장해 두는 것이 좋다.
- 편향이 그대로 들어온다. 학습 데이터의 사회적 편향(직업과 성별의 연관 등)이 임베딩 공간에 방향으로 남는다. 채용·대출 같은 민감한 곳에 쓸 때 점검해야 한다.
- 비용 감각: 1,024 차원 float32 벡터 하나는 4KB 다. 문서 조각 100만 개면 벡터만 약 4GB 다. 작은 서버에서는 차원 축소나 양자화로 줄인다.
확인 문제
- 원-핫 벡터로는 단어 간 유사도를 표현할 수 없는 이유는?
- 분포 가설을 한 문장으로 말하라.
- 정적 임베딩(word2vec)과 문맥 임베딩(BERT)의 가장 큰 차이는?
- 벡터를 길이 1 로 정규화하면 코사인 유사도와 내적의 관계는?
- 임베딩 모델을 새 버전으로 바꿀 때 기존 벡터 색인을 그대로 쓰면 안 되는 이유는?
풀이
- 서로 다른 두 원-핫 벡터의 내적은 항상 0 이고 거리도 모두 같아, 어떤 쌍이 더 가깝다는 정보가 없다.
- 비슷한 문맥에 나타나는 단어는 비슷한 의미를 가진다.
- 정적 임베딩은 단어마다 벡터가 하나로 고정되고, 문맥 임베딩은 같은 단어라도 문장에 따라 다른 벡터가 나온다.
- 같아진다. 분모가 1 이 되기 때문이다.
- 모델마다 벡터 공간(축의 의미)이 달라, 새 모델로 만든 쿼리 벡터와 옛 모델의 문서 벡터를 비교하는 것은 의미가 없다.
더 읽을거리 (References)
- T. Mikolov 외, “Efficient Estimation of Word Representations in Vector Space”, 2013. arXiv:1301.3781
- J. Pennington, R. Socher, C. Manning, “GloVe: Global Vectors for Word Representation”, 2014. 프로젝트 페이지
- N. Reimers, I. Gurevych, “Sentence-BERT”, 2019. arXiv:1908.10084, Sentence Transformers 문서