word2vec 이란? - 분포 가설부터 학습 알고리즘까지

2025. 8. 4. 15:49·CS 기초부터 한 걸음씩

들어가며: 아이디어에서 알고리즘으로

이전 포스트에서 우리는 벡터 임베딩의 본질과 분포 가설의 철학적 배경을 살펴봤다. 이제 그 아이디어를 구체적인 알고리즘으로 구현한 word2vec에 대해 깊이 탐구해보자.

word2vec은 2013년 구글의 토마스 미콜로프(Tomas Mikolov)가 발표한 단어 임베딩 학습 알고리즘이다. 단순해 보이는 이 알고리즘이 자연어처리 분야에 미친 영향은 엄청났다. 왜 word2vec이 그토록 효과적인지, 그 내부 작동 원리를 상세히 분석해보자.

분포 가설의 구체적 구현: 문맥 예측 문제

언어 모델링의 핵심 아이디어

word2vec의 핵심은 문맥을 통한 단어 예측이다. 분포 가설 "단어는 함께 나타나는 단어들로 알 수 있다"를 기계학습 문제로 바꾸면 다음과 같다:

문제 정의:

  • 주어진 단어의 문맥을 보고 중심 단어를 예측하거나
  • 중심 단어를 보고 주변 문맥을 예측한다

이 예측 문제를 학습하는 과정에서 신경망의 가중치가 자연스럽게 의미적 관계를 포함한 벡터 표현으로 수렴하게 된다.

문맥 윈도우(Context Window)의 개념

문맥 윈도우는 중심 단어 주변의 단어들을 정의하는 범위다.

문장: "The quick brown fox jumps over the lazy dog"
중심 단어: "fox" (윈도우 크기 = 2)
문맥: ["quick", "brown", "jumps", "over"]

윈도우 크기가 클수록:

  • 장점: 더 넓은 문맥 정보 활용
  • 단점: 계산 복잡성 증가, 노이즈 증가

일반적으로 윈도우 크기는 5-10 사이에서 설정한다.

Skip-gram 모델: 중심 단어로 문맥 예측

Skip-gram의 기본 아이디어

Skip-gram Word2Vec는 CBow Word2Vec와 달리 주변 단어들을 사용해 중심 단어를 예측하는 대신, 중심 단어를 사용해 주변 단어들을 예측한다.

 

Skip-gram의 학습 목표:

주어진 중심 단어 w_c로부터
문맥 단어들 {w_o1, w_o2, ..., w_ok}를 예측

Skip-gram 아키텍처: 신경망이 단어를 학습하는 방법

word2vec은 신경망이라는 일종의 "단어 학습기"이다. 마치 사람이 단어를 배우듯이, 컴퓨터도 단어들 사이의 관계를 천천히 학습한다.

신경망의 3층 구조:

입력층: "강아지"라는 단어 입력 (one-hot 벡터)
    ↓
은닉층: "강아지"의 특성을 숫자로 표현 (임베딩 벡터)
    ↓  
출력층: 주변에 나올 단어들의 가능성 점수들

각 층의 역할을 비유로 설명:

1. 입력층 - 단어를 컴퓨터 언어로 번역

  • 사람: "강아지"라고 말함
  • 컴퓨터: [0,0,0,1,0,0,0] 같은 숫자 배열로 이해
  • 이를 one-hot 벡터라고 함 (해당 단어 위치만 1, 나머지는 0)

2. 은닉층 - 단어의 "특성 카드" 생성 여기가 바로 임베딩 벡터가 만들어지는 곳

"강아지"라는 단어의 특성을 숫자로 표현한다면?

  • 크기: 0.3 (중간 크기)
  • 친근함: 0.9 (매우 친근함)
  • 털복숭이: 0.8 (털이 많음)
  • 실내동물: 0.7 (집에서 키움)

이런 특성들을 임베딩 벡터 : [0.3, 0.9, 0.8, 0.7]

중요: 이 벡터는 미리 만들어진 게 아니라, 신경망이 학습하면서 점진적으로 생산

3. 출력층 - "다음에 올 단어 맞히기 게임"

  • 신경망: "강아지 다음에 올 단어는 '산책'일 확률 70%, '먹이'일 확률 20%..."
  • 이런 확률들을 계산하는 게 softmax 함수

softmax 함수: 점수를 확률로 바꾸는 마법

softmax가 하는 일을 카페 메뉴 선택으로 비유:

당신이 카페에 가서 메뉴를 고민한다고 생각해보자.

  • 아메리카노: 관심도 8점
  • 라떼: 관심도 5점
  • 프라푸치노: 관심도 2점
  • 차: 관심도 1점

softmax는 이 점수들을 확률(%)로 바꿔줍니다:

# 1단계: 점수들을 지수함수로 변환 (큰 점수를 더 크게 만듦)
exp_scores = [e^8, e^5, e^2, e^1] = [2981, 148, 7, 3]

# 2단계: 전체 합으로 나누어 확률로 변환
total = 2981 + 148 + 7 + 3 = 3139
probabilities = [2981/3139, 148/3139, 7/3139, 3/3139] 
              = [0.95, 0.047, 0.002, 0.001]
              = [95%, 4.7%, 0.2%, 0.1%]

word2vec에서의 softmax:

  • 신경망이 각 단어에 대해 "다음에 나올 가능성 점수" 계산
  • softmax가 이를 "확률"로 변환
  • 모든 확률의 합은 반드시 100%가 됨 (총합에서 나눴음)

Skip-gram의 장점과 특성

장점:

  1. 희소한 단어에 효과적: 각 단어가 여러 문맥을 생성하므로 학습 기회 증가
  2. 큰 데이터셋에서 우수한 성능: 더 많은 학습 샘플 생성
  3. 구문적/의미적 관계 포착: 단어 간 미묘한 관계 학습

단점:

  1. 계산 복잡도 높음: Skip-gram 모델의 전체 복잡도는 N×D+N×D×log2(V)이다. CBOW와 달리 단일 클래스 분류 문제가 아닌 N개 클래스 분류 문제이므로, Skip-gram 모델의 전체 복잡도는 CBOW 모델보다 크다.
  2. 메모리 사용량 많음: 더 많은 학습 샘플 저장 필요

CBOW (Continuous Bag-of-Words) 모델: 문맥으로 중심 단어 예측

CBOW의 기본 아이디어

CBOW는 Skip-gram과 반대 방향으로 접근한다. 여러 문맥 단어들을 입력받아 중심 단어를 예측한다.

CBOW의 학습 목표:

주어진 문맥 단어들 {w_c1, w_c2, ..., w_ck}로부터
중심 단어 w_o를 예측

CBOW 아키텍처 상세 분석

신경망 구조:

입력층: 문맥 단어들의 one-hot 벡터들 (각각 V차원)
    ↓
은닉층: 평균화된 임베딩 벡터 (D차원)
    ↓
출력층: 모든 단어에 대한 확률 분포 (V차원)

수학적 정의:

  1. 입력: 문맥 단어들의 one-hot 벡터 {x_c1, x_c2, ..., x_ck}
  2. 은닉층: h = (1/k) × Σ(W^T × x_ci) (임베딩 벡터들의 평균)
  3. 출력: y = W'^T × h
  4. 확률: P(w_o|context) = softmax(y)

CBOW의 핵심: 벡터 평균화

CBOW의 가장 중요한 특징은 문맥 단어들의 임베딩 벡터를 평균화한다는 점이다.

# 예시: 문맥 ["The", "quick", "brown", "jumps"]
context_vectors = [embed("The"), embed("quick"), 
                  embed("brown"), embed("jumps")]
hidden = average(context_vectors)  # 벡터들의 평균

이 평균화 과정에서:

  • 공통된 의미 요소들이 강화됨
  • 노이즈나 무관한 정보는 상쇄됨
  • 문맥의 전체적인 의미가 압축됨

CBOW vs Skip-gram: 두 가지 학습 방식

P(A|B): 조건부 확률 - "B가 주어졌을 때 A가 일어날 확률"

P(문맥단어|중심단어)의 의미:

일상 예시로 설명하면:

  • P(비|구름) = "구름이 있을 때 비가 올 확률"
  • P(산책|강아지) = "강아지가 있을 때 '산책'이라는 단어가 나올 확률"

word2vec에서 사용하는 조건부 확률들:

Skip-gram: P(주변단어|중심단어)

"강아지가 산책을 한다"라는 문장에서
P(산책|강아지) = "강아지"라는 단어 주변에 "산책"이 나타날 확률

CBOW: P(중심단어|주변단어들)

P(강아지|귀여운, 작은, 산책) = "귀여운, 작은, 산책" 주변에 "강아지"가 나타날 확률

 

요리 레시피로 비유하면:

CBOW (Continuous Bag-of-Words): "재료를 보고 요리 이름 맞히기"

재료: [토마토, 양파, 마늘, 올리브오일]
문제: 이 재료들로 무슨 요리를 만들까?
정답: 토마토 파스타

Skip-gram: "요리 이름을 보고 재료 맞히기"

요리: 토마토 파스타
문제: 이 요리에 어떤 재료들이 들어갈까?
정답: [토마토, 양파, 마늘, 올리브오일]

 

word2vec에서 적용하면:

CBOW: 문맥 단어들 → 중심 단어 예측

문맥: ["귀여운", "작은", "산책을", "한다"]
문제: 중간에 들어갈 단어는?
정답: "강아지"

학습 과정:
1. 문맥 단어들의 벡터를 평균내기
   avg = (귀여운벡터 + 작은벡터 + 산책을벡터 + 한다벡터) ÷ 4
2. 이 평균 벡터로 중심 단어 예측
3. "강아지"를 맞히면 성공!

Skip-gram: 중심 단어 → 문맥 단어들 예측

중심: "강아지"  
문제: 주변에 어떤 단어들이 나올까?
정답: ["귀여운", "작은", "산책을", "한다"]

학습 과정:
1. "강아지" 벡터 하나로
2. 주변 단어 4개를 모두 예측해야 함
3. 4번의 예측이 모두 맞아야 성공!

 

언제 어떤 방식을 쓸까?

CBOW가 좋은 경우:

  • 빈번한 단어 학습 (자주 나오는 단어는 문맥이 풍부)
  • 빠른 학습이 필요할 때
  • 컴퓨터 성능이 제한적일 때

Skip-gram이 좋은 경우:

  • 희소한 단어 학습 (가끔 나오는 단어도 여러 번 학습 기회)
  • 고품질 임베딩이 필요할 때
  • 대규모 데이터가 있을 때

계산 효율성 문제와 해결책

원래 방법의 문제점: "모든 학생과 비교하기"

학교에서 "가장 키 큰 학생 찾기" 대회를 한다고 가정

word2vec의 기본적인 소프트맥스 방식에서는:

  • 어휘 사전에 100만 개 단어 있음
  • "강아지" 다음에 올 단어를 예측할 때마다
  • 100만 개 단어 모두에 대해 확률 계산
  • 컴퓨터가 과부하로 너무 느려짐
# 매번 이런 계산을 해야 함 (너무 느림!)
for 모든_단어 in 사전_100만개:
    점수 = calculate_score(강아지벡터, 단어벡터)
    
probabilities = softmax(모든_점수들)  # 100만 개 확률 계산

문제의 핵심:

  • 시간 복잡도: O(V) (V = 어휘 크기)
  • 100만 단어라면 매번 100만 번 계산
  • 현실적으로 불가능한 속도

Hierarchical Softmax: "토너먼트 방식"으로 효율화

토너먼트 방식의 아이디어

기존 방식: 모든 학생과 일대일 비교 토너먼트 방식: 단계별 탈락전

           결승전
          /      \
      준결승A    준결승B  
      /   \      /   \
   강아지 고양이  자동차 비행기

토너먼트의 장점:

  • 1,000명 중 1등 찾기: 기존 999번 vs 토너먼트 ~10번
  • log₂(1000) ≈ 10번만 비교하면 됨

word2vec에서 Hierarchical Softmax

단어들을 이진 트리로 구성:

        Root
       /    \
    동물       사물
   /   \     /   \
 강아지 고양이 자동차 컴퓨터

확률 계산:

  • P(강아지) = P(동물|Root) × P(강아지|동물)
  • 2번의 이진 선택으로 4개 중 1개 선택

효과:

  • 100만 단어: 기존 100만 번 → 약 20번 (log₂ 1,000,000 ≈ 20)
  • 50,000배 빨라짐!

허프만 코딩: 똑똑한 트리 만들기

기본 아이디어: 자주 나오는 단어를 트리 위쪽에 배치

비유 - 도서관 책 배치:

  • 인기 도서: 1층 (빨리 찾을 수 있음)
  • 전문 서적: 지하 3층 (오래 걸려도 괜찮음)

word2vec에서:

  • "the", "and" 같은 빈번한 단어: 2-3번 비교
  • "antidisestablishmentarianism" 같은 희소 단어: 15-20번 비교
  • 전체 평균 계산 시간 최소화

Hierarchical Softmax의 장단점

장점:

  1. 대폭적인 속도 향상: O(V) → O(log V)
  2. 메모리 효율성: 출력 가중치 행렬 불필요
  3. 희소 단어에 유리: 계층적 소프트맥스는 빈도가 낮은 단어에 더 나은 성능을 보인다

단점:

  1. 트리 구조 의존성: 잘못된 트리는 성능 저하 야기
  2. 병렬화 어려움: 트리 탐색의 순차적 특성
  3. 빈번한 단어의 세밀함 부족: 상위 노드에서 조기 분류

Negative Sampling: "가짜 뉴스 구별하기"로 문제 단순화

Negative Sampling의 핵심 아이디어

  • 모든 단어에 대해 확률을 계산하는 대신
  • 소수의 "부정적" 단어들만 선택해서 비교
  • 이진 분류 문제로 변환

기존 문제: "100만 개 중에서 정답 찾기" (너무 어려움) vs 새로운 문제: "진짜 vs 가짜 구별하기" (훨씬 쉬움)

문제 변환: 다중 분류 → 이진 분류

기존 문제: "fox" 주변에 나타날 단어는?

  • 100만 개 단어 중 정답 선택 (다중 분류)

Negative Sampling: "fox" 주변에 "brown"이 나타날까?

  • YES/NO 대답 (이진 분류)
  • 추가로 몇 개의 "negative" 단어들도 NO로 학습

Negative Sampling 알고리즘

원본 텍스트: "귀여운 강아지가 공원에서 산책을 한다"

1단계: 진짜 문맥 쌍 만들기

Positive Sample (실제 나타난 조합):
("강아지", "귀여운") → 라벨: 1 (진짜)
("강아지", "공원에서") → 라벨: 1 (진짜)  
("강아지", "산책을") → 라벨: 1 (진짜)

2단계: 가짜 문맥 쌍 만들기

Negative Samples (실제로는 나타나지 않은 조합):
("강아지", "컴퓨터") → 라벨: 0 (가짜)
("강아지", "수학") → 라벨: 0 (가짜)
("강아지", "우주선") → 라벨: 0 (가짜)

3단계: 진짜/가짜 구별 학습

# 신경망에게 물어보기
"강아지와 귀여운이 함께 나타날까요?" → "네! 90% 확신"
"강아지와 컴퓨터가 함께 나타날까요?" → "아니요, 5% 확신"

# 정답과 비교해서 점수 매기기  
정답: 예, 예상: 90% → 좋음!
정답: 아니오, 예상: 5% → 좋음!

Negative Sampling의 확률적 해석

목표 함수:

maximize P(D=1|w_o, w_c) × ∏(negative words) P(D=0|w_n, w_c)

여기서:

  • D=1: 실제 문맥 쌍
  • D=0: 가짜 문맥 쌍
  • σ(v_o^T × v_c): P(D=1|w_o, w_c)

Negative Word 선택 전략

단순하게 선택하면 안 되는 이유:

만약 단어를 랜덤하게 선택한다면?

  • "the", "and", "a" 같은 흔한 단어들만 계속 선택됨
  • 신경망이 "강아지는 the와 관련 없다"만 계속 학습
  • 정작 중요한 구별 (강아지 vs 컴퓨터)은 학습 못함

해결책: 서브샘플링된 확률 분포

P(w) = f(w)^(3/4) / Σf(w)^(3/4)

여기서:

  • f(w): 단어 w의 빈도
  • 3/4 지수: 빈번한 단어 확률 억제, 희소 단어 확률 증대

효과:

  • 빈번한 단어: 선택 확률 감소
  • 중간 빈도 단어: 선택 확률 증가
  • 더 균형잡힌 negative sampling

Negative Sampling의 장단점

장점:

  1. 극도로 빠른 계산: O(k) (k: negative sample 수, 보통 5-20)
  2. 병렬화 용이: 각 샘플 독립적 처리
  3. 빈번한 단어에 효과적: negative sampling은 빈도가 높은 단어에 더 나은 성능을 보인다
  4. 구현 단순성: 복잡한 트리 구조 불필요

단점:

  1. 이론적 근거 부족: 어떤 목적 함수를 최적화하는지 불분명
  2. 하이퍼파라미터 민감성: negative sample 수 선택이 중요
  3. 희소 단어 처리: hierarchical softmax보다 불리

학습 과정의 역전파와 가중치 업데이트

Skip-gram with Negative Sampling의 역전파

순전파:

# 1. 중심 단어 임베딩 추출
center_vec = W[center_word_idx]  # D차원 벡터

# 2. 내적 계산 및 시그모이드
positive_score = np.dot(center_vec, W_prime[positive_word_idx])
positive_prob = sigmoid(positive_score)

negative_scores = np.dot(center_vec, W_prime[negative_word_indices])
negative_probs = sigmoid(-negative_scores)

손실 함수:

loss = -np.log(positive_prob) - np.sum(np.log(negative_probs))

역전파:

# 그래디언트 계산
positive_grad = (positive_prob - 1) * W_prime[positive_word_idx]
negative_grads = (1 - negative_probs) * W_prime[negative_word_indices]

# 중심 단어 벡터 업데이트
center_gradient = positive_grad + np.sum(negative_grads, axis=0)
W[center_word_idx] -= learning_rate * center_gradient

# 출력 벡터들 업데이트  
W_prime[positive_word_idx] -= learning_rate * (positive_prob - 1) * center_vec
for i, neg_idx in enumerate(negative_word_indices):
    W_prime[neg_idx] -= learning_rate * (1 - negative_probs[i]) * center_vec

학습률 스케줄링과 수렴 전략

적응적 학습률:

initial_lr = 0.025
min_lr = 0.0001
current_lr = max(min_lr, initial_lr * (1 - progress))

SubSampling: 빈번한 단어 처리 빈번한 단어("the", "and" 등)는 학습에서 제외하거나 확률적으로 스킵:

keep_prob = (np.sqrt(word_freq / 0.001) + 1) * 0.001 / word_freq
if random.random() > keep_prob:
    skip_word()

word2vec 학습의 핵심 통찰

왜 word2vec이 의미적 관계를 학습하는가?

1. 분포 가설의 자동 학습

  • 비슷한 문맥에서 등장하는 단어들의 벡터가 유사해짐
  • "강아지"와 "고양이"는 "키우다", "먹이다" 등의 유사한 문맥에서 등장

2. 벡터 공간의 선형 구조

  • 일관된 관계가 벡터 차이로 표현됨
  • "남자-여자" 관계와 "왕-여왕" 관계의 유사성이 벡터 연산으로 포착

3. 차원 축소의 압축 효과

  • 고차원 희소 벡터 → 저차원 밀집 벡터
  • 노이즈 제거, 핵심 의미 요소만 보존

word2vec의 한계와 개선 방향

현재의 한계점

1. 다의성 문제

"Apple의 주가가 올랐다" vs "사과를 먹었다"
→ 하나의 "Apple" 벡터로는 두 의미 구분 불가

2. 문맥 독립성

  • 정적 임베딩: 문맥에 관계없이 동일한 벡터
  • 동적 문맥 고려 불가

3. OOV (Out-of-Vocabulary) 문제

  • 학습되지 않은 단어 처리 불가
  • 새로운 단어나 오타에 취약

4. 서브워드 정보 무시

  • "unhappiness" = "un" + "happy" + "ness" 구조 무시
  • 형태소 분석 정보 활용 안 함

후속 연구와 발전

FastText (Facebook, 2016):

  • 서브워드 정보 활용
  • Character n-gram 기반
  • OOV 문제 해결

ELMo (Allen Institute, 2018):

  • 문맥 의존적 임베딩
  • 양방향 LSTM 활용
  • 다의성 문제 부분 해결

BERT (Google, 2018):

  • Transformer 기반
  • 양방향 문맥 고려
  • Pre-training + Fine-tuning

word2vec은 현재도 널리 사용되는 기초 기술이며, 최신 언어 모델들의 토대가 되었다.

다음 포스트에서는 이러한 임베딩 기술을 효율적으로 저장하고 검색하는 벡터 데이터베이스의 핵심 기술인 유사도 척도와 KNN 검색에 대해 자세히 알아보자.

'CS 기초부터 한 걸음씩' 카테고리의 다른 글

[Android] Android Studio Logcat과 로그 레벨  (3) 2025.08.18
[Android] Activity와 Activity Lifecycle 기초 개념  (5) 2025.08.18
벡터 임베딩 - 비정형 데이터를 수치로 표현하는 방법  (3) 2025.08.04
함수형 프로그래밍 6편 - 함수형 사고 방식과 문제 해결 전략  (1) 2025.07.23
함수형 프로그래밍 5편 - 지연 계산과 함수형 최적화 전략  (4) 2025.07.23
'CS 기초부터 한 걸음씩' 카테고리의 다른 글
  • [Android] Android Studio Logcat과 로그 레벨
  • [Android] Activity와 Activity Lifecycle 기초 개념
  • 벡터 임베딩 - 비정형 데이터를 수치로 표현하는 방법
  • 함수형 프로그래밍 6편 - 함수형 사고 방식과 문제 해결 전략
Celion
Celion
오늘도 평소처럼 화이팅!
  • Celion
    Orion Log
    Celion
  • 전체
    오늘
    어제
    • 전체 글 (144)
      • Uncompiled Thoughts (8)
        • 네이버 부스트캠프 10기 (5)
      • CS 기초부터 한 걸음씩 (34)
      • Code Odyssey (22)
      • Algorithm (77)
        • Coding Test Records (63)
      • Git (3)
      • reference (0)
  • 블로그 메뉴

    • 태그
    • 방명록
  • 태그

    Level2
    boostcamp
    Kotlin
    greedy
    시뮬레이션
    java
    백준
    코테
    프로그래머스
    Level3
    알고리즘고득점kit
    문법정리
  • 최근 글

  • 인기 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.4
Celion
word2vec 이란? - 분포 가설부터 학습 알고리즘까지
상단으로

티스토리툴바