단어 임베딩(Word Embeddings) — Word2Vec 직접 구현

단어는 함께 등장하는 이웃으로 알 수 있습니다. 그 생각을 얕은 신경망(shallow network)으로 학습시키면 기하 구조(geometry)가 자연스럽게 떨어져 나옵니다.

유형: Build 언어: Python 선수 조건: Phase 5 · 02(BoW + TF-IDF), Phase 3 · 03(Backpropagation from Scratch) 소요 시간: 약 75분

학습 목표

  • 분포 가설(Distributional Hypothesis)을 설명하고 Word2Vec이 이를 어떻게 학습 문제로 바꾸는지 이해합니다.
  • Skip-gram과 CBOW(Continuous Bag-of-Words)를 구분합니다.
  • Skip-gram 학습 쌍(training pair)과 임베딩 표(embedding table)를 만들고 네거티브 샘플링(negative sampling) 목적 함수를 구현합니다.
  • 학습된 임베딩에서 최근접 이웃(nearest neighbor)과 유추(analogy)를 계산합니다.
  • 정적 임베딩(static embedding)의 장점과 다의어(polysemy), OOV(Out-Of-Vocabulary) 한계를 설명합니다.

문제

TF-IDF는 dogpuppy가 서로 다른 단어라는 사실은 알지만, 두 단어가 거의 같은 의미라는 점은 알지 못합니다. dog로 학습한 분류기(classifier)는 puppy가 등장하는 리뷰에 일반화하지 못합니다. 동의어 목록(synonym list)으로 덮어 보려 해도, 드문 용어(rare term), 특정 도메인의 전문 용어(domain jargon), 그리고 예상치 못한 모든 언어에서 실패합니다.

우리에게 필요한 것은 dogpuppy가 벡터 공간(vector space)에서 가까이 놓이는 표현(representation)입니다. king - man + womanqueen 근처에 도달하는 공간이 필요하고, dog로 학습한 신호(signal)가 자연스럽게 puppy에도 어느 정도 전달되는 공간이 필요합니다.

Word2Vec이 바로 그 공간을 제공했습니다. 2013년에 공개된 두 층짜리 신경망(two-layer neural network)이며, 수조 개 토큰(trillion-token) 규모의 학습으로 만들어졌습니다. 아키텍처는 민망할 만큼 단순하지만, 그 결과는 NLP의 다음 10년을 바꿔 놓았습니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.TF-IDF는 'dog'와 'puppy'를 완전히 별개의 차원에 배치합니다. 'dog'가 포함된 리뷰로 학습한 분류기(classifier)가 'puppy'를 사용한 리뷰를 만나면 어떤 문제가 생기나요?

2.분포 가설(Distributional Hypothesis)은 '단어는 함께 등장하는 이웃으로 알 수 있다'고 말합니다. 이 원칙의 직접적인 결과는 무엇인가요?

0/2 답변 완료

개념

Skip-gram window and embedding space

분포 가설(Distributional Hypothesis) (Firth, 1957): "You shall know a word by the company it keeps." 두 단어가 비슷한 문맥(context)에 등장한다면 비슷한 의미를 가질 가능성이 높다는 가정입니다.

Word2Vec은 이 생각을 두 가지 방식으로 활용합니다.

  • Skip-gram: 중심 단어(center word)가 주어졌을 때 주변 단어(surrounding words)를 예측합니다. 윈도우 크기(window size)가 2라면 cat -> (the, sat, on)이 됩니다.
  • CBOW(Continuous Bag-of-Words): 주변 단어가 주어졌을 때 중심 단어를 예측합니다. (the, sat, on) -> cat이 됩니다.

Skip-gram은 학습 속도가 느리지만 드문 단어(rare words)를 더 잘 다루기 때문에 기본값(default)으로 자리 잡았습니다.

이 신경망은 비선형성(nonlinearity)이 없는 은닉층(hidden layer) 하나만 가집니다. 입력은 어휘집(vocabulary)에 대한 원-핫 벡터(one-hot vector)이고, 출력은 어휘집 전체에 대한 소프트맥스(softmax)입니다. 학습이 끝나면 출력층(output layer)은 버리고, 은닉층의 가중치(weight)가 곧 임베딩(embedding)이 됩니다.

one-hot(center) ── W ──▶ hidden (d-dim) ── W' ──▶ softmax(vocab)
                          ^
                          이것이 임베딩입니다

여기서 핵심 문제는 10만(100k) 단어 규모의 어휘집에 대한 소프트맥스 계산이 지나치게 비싸다는 점입니다. Word2Vec은 이를 네거티브 샘플링(negative sampling) 으로 우회하여, 다중 클래스 문제를 이진 분류(binary classification) 과제로 바꿉니다. "이 문맥 단어가 중심 단어 근처에 실제로 등장했는가, 그렇지 않은가(yes or no)"를 맞추는 문제로 환원하는 것입니다. 학습 쌍(training pair)마다 어휘집 전체에 대한 소프트맥스를 계산하는 대신, 함께 등장하지 않은(non-co-occurring) 단어 몇 개만 음성 표본(negative)으로 뽑아 함께 학습합니다.

만들어 보기

Step 1: 코퍼스(corpus)에서 학습 쌍(training pair) 만들기

def skipgram_pairs(docs, window=2):
    pairs = []
    for doc in docs:
        for i, center in enumerate(doc):
            for j in range(max(0, i - window), min(len(doc), i + window + 1)):
                if i == j:
                    continue
                pairs.append((center, doc[j]))
    return pairs

윈도우 안의 모든 (center, context) 쌍은 양성 학습 예시(positive training example)가 됩니다.

Step 2: 임베딩 표(embedding table)

행렬 두 개를 사용합니다. W는 중심 단어 임베딩 표(center-word embedding table)로, 최종적으로 우리가 보존하는 표입니다. W'는 문맥 단어 표(context-word table)로, 보통은 버리지만 때로는 W와 평균을 내어 함께 사용하기도 합니다.

import numpy as np


def init_embeddings(vocab_size, dim, seed=0):
    rng = np.random.default_rng(seed)
    W = rng.normal(0, 0.1, size=(vocab_size, dim))
    W_prime = rng.normal(0, 0.1, size=(vocab_size, dim))
    return W, W_prime

작은 값의 무작위 초기화(random initialization)를 사용합니다. 실무에서는 어휘 크기 1만 개, 차원 100 정도가 현실적이고, 교육용으로는 어휘 50개, 차원 16 정도만 되어도 기하 구조를 충분히 확인할 수 있습니다.

Step 3: 네거티브 샘플링(negative sampling) 목적 함수

양성 쌍 (center, context)마다 어휘집에서 무작위 단어 k개를 음성 표본(negative)으로 뽑습니다. 그런 다음 양성 쌍의 내적(dot product) W[center] · W'[context]는 커지도록, 음성 쌍의 내적은 작아지도록 학습합니다.

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-np.clip(x, -20, 20)))


def train_pair(W, W_prime, center_idx, context_idx, negative_indices, lr):
    v_c = W[center_idx]
    u_pos = W_prime[context_idx]
    u_negs = W_prime[negative_indices]

    pos_score = sigmoid(v_c @ u_pos)
    neg_scores = sigmoid(u_negs @ v_c)

    grad_center = (pos_score - 1) * u_pos
    for i, u in enumerate(u_negs):
        grad_center += neg_scores[i] * u

    W_prime[context_idx] -= lr * (pos_score - 1) * v_c
    for i, neg_idx in enumerate(negative_indices):
        W_prime[neg_idx] -= lr * neg_scores[i] * v_c
    W[center_idx] -= lr * grad_center

핵심 아이디어는 양성 쌍에서는 시그모이드(sigmoid) 값이 1에 가까워지도록, 음성 쌍에서는 0에 가까워지도록 로지스틱 손실(logistic loss)을 거는 것입니다. 기울기(gradient)는 두 표 모두로 흐릅니다. 자세한 유도 과정은 원 논문에 정리되어 있으며, 한 번쯤 손으로 직접 따라가며 풀어 보면 개념이 더 단단히 잡힙니다.

Step 4: 장난감 코퍼스(toy corpus)에서 학습

def train(docs, dim=16, window=2, k_neg=5, epochs=100, lr=0.05, seed=0):
    vocab = build_vocab(docs)
    vocab_size = len(vocab)
    rng = np.random.default_rng(seed)
    W, W_prime = init_embeddings(vocab_size, dim, seed=seed)
    pairs = skipgram_pairs(docs, window=window)

    for epoch in range(epochs):
        rng.shuffle(pairs)
        for center, context in pairs:
            c_idx = vocab[center]
            ctx_idx = vocab[context]
            negs = rng.integers(0, vocab_size, size=k_neg)
            negs = [n for n in negs if n != ctx_idx and n != c_idx]
            train_pair(W, W_prime, c_idx, ctx_idx, negs, lr)
    return vocab, W

규모가 큰 코퍼스에서 충분히 학습하면, 같은 문맥을 공유하는 단어들의 중심 임베딩이 서로 비슷해집니다. 장난감 코퍼스에서는 그 효과가 희미하게만 드러나지만, 수십억 개 토큰 규모에서는 효과가 극적으로 나타납니다.

Step 5: 유추(analogy) 트릭

def nearest(vocab, W, target_vec, topk=5, exclude=None):
    exclude = exclude or set()
    inv_vocab = {i: w for w, i in vocab.items()}
    norms = np.linalg.norm(W, axis=1, keepdims=True) + 1e-9
    W_norm = W / norms
    target = target_vec / (np.linalg.norm(target_vec) + 1e-9)
    sims = W_norm @ target
    order = np.argsort(-sims)
    out = []
    for i in order:
        if i in exclude:
            continue
        out.append((inv_vocab[i], float(sims[i])))
        if len(out) == topk:
            break
    return out


def analogy(vocab, W, a, b, c, topk=5):
    v = W[vocab[b]] - W[vocab[a]] + W[vocab[c]]
    return nearest(vocab, W, v, topk=topk, exclude={vocab[a], vocab[b], vocab[c]})

사전 학습된 300차원(300d) Google News 벡터를 사용하면 다음과 같은 결과가 나옵니다.

>>> analogy(vocab, W, "man", "king", "woman")
[('queen', 0.71), ('monarch', 0.62), ('princess', 0.59), ...]

king - man + woman = queen이 성립하는 것은 모델이 왕족(royalty)이라는 개념을 이해해서가 아닙니다. (king - man)이라는 벡터가 "왕족스러움(royal)"에 가까운 어떤 방향을 담아내고, 그 방향을 woman에 더하면 왕족이면서 여성인 영역(royal-female region) 근처로 도달하기 때문에 그렇습니다.

사용하기

Word2Vec을 처음부터 직접 구현해 보는 것은 학습 목적입니다. 실무 NLP에서는 보통 gensim을 사용합니다.

from gensim.models import Word2Vec

sentences = [
    ["the", "cat", "sat", "on", "the", "mat"],
    ["the", "dog", "ran", "across", "the", "room"],
]

model = Word2Vec(
    sentences,
    vector_size=100,
    window=5,
    min_count=1,
    sg=1,
    negative=5,
    workers=4,
    epochs=30,
)

print(model.wv["cat"])
print(model.wv.most_similar("cat", topn=3))

실무에서 Word2Vec을 직접 학습하는 일은 드뭅니다. 보통은 사전 학습된 벡터(pre-trained vector)를 내려받아 사용합니다.

  • GloVe — Stanford에서 공개한, 동시 출현 행렬 분해(co-occurrence-matrix factorization) 기반 접근입니다. 50차원, 100차원, 200차원, 300차원 체크포인트(checkpoint)가 있으며, 일반 도메인 커버리지가 좋습니다. GloVe는 Lesson 04에서 자세히 다룹니다.
  • fastText — Facebook이 공개한 Word2Vec 확장(extension)입니다. 문자 n-그램(character n-gram)을 임베딩하기 때문에, 어휘 밖 단어(out-of-vocabulary)도 부분 단어(subword)를 합성해 표현할 수 있습니다. Lesson 04에서 다룹니다.
  • Pretrained Word2Vec on Google News — 300차원, 300만(3M) 단어 어휘로 2013년에 공개된 모델입니다. 지금도 매일 다운로드되고 있습니다.

2026년에도 Word2Vec이 여전히 강한 영역

  • 가벼운 도메인 특화 검색(lightweight domain-specific retrieval). 의료 초록을 노트북에서 한 시간 정도만 학습해도, 범용 모델이 잡지 못하는 도메인 특화 벡터를 얻을 수 있습니다.
  • 유추(analogy) 기반 특성 공학(feature engineering). 예를 들어 gender_vector = mean(man - woman pairs) 형태의 축을 만든 뒤, 다른 단어에서 이 축을 빼서 성별 중립 축(gender-neutral axis)을 만들 수 있습니다. 공정성(fairness) 연구에서도 여전히 사용됩니다.
  • 해석 가능성(interpretability). 100차원 정도면 PCA나 t-SNE로 시각화해서 군집(cluster)이 형성되는 모습을 직접 눈으로 확인할 수 있을 만큼 작습니다.
  • GPU 없이 기기 내(on-device)에서 추론(inference)을 돌려야 하는 모든 환경. Word2Vec의 조회는 행 하나를 가져오는 단순한 연산입니다.

Word2Vec이 실패하는 영역

가장 큰 한계는 다의어 장벽(polysemy wall)입니다. bank는 벡터를 하나만 가지므로, river bank(강둑)와 financial bank(은행)가 같은 벡터를 공유하게 됩니다. table(표 / 가구) 역시 마찬가지입니다. 그러면 다운스트림(downstream) 분류기는 이 벡터만 보고는 의미(sense)를 구분할 수 없습니다.

문맥적 임베딩(contextual embedding)인 ELMo, BERT, 그리고 그 이후의 모든 트랜스포머(transformer)는, 주변 문맥에 따라 같은 단어라도 등장할 때마다 다른 벡터를 만들어 내며 이 문제를 해결했습니다. Word2Vec에서 BERT로 넘어가는 도약은, 곧 정적(static)에서 문맥적(contextual)으로의 전환입니다. 트랜스포머 부분은 Phase 7에서 다룹니다.

또 다른 실패 지점은 어휘 밖 단어(Out-Of-Vocabulary; OOV) 문제입니다. Word2Vec은 학습 데이터에 없던 Zoomer-approved 같은 단어의 벡터를 만들어 낼 수 없고, 대체할 방법(fallback)도 없습니다. fastText가 부분 단어 합성(subword composition)으로 이 문제를 해결합니다(Lesson 04).

산출물 만들기

outputs/skill-embedding-probe.md로 저장합니다.

---
name: embedding-probe
description: Inspect a word2vec model. Run analogies, find neighbors, diagnose quality.
version: 1.0.0
phase: 5
lesson: 03
tags: [nlp, embeddings, debugging]
---

You probe trained word embeddings to verify they are working. Given a `gensim.models.KeyedVectors` object and a vocabulary, you run:

Guide the student in Korean.

1. Three canonical analogy tests. `king : man :: queen : woman`. `paris : france :: tokyo : japan`. `walking : walked :: swimming : ?`. Report the top-1 result and its cosine.
2. Five nearest-neighbor tests on domain-specific words the user supplies. Print top-5 neighbors with cosines.
3. One symmetry check. `similarity(a, b) == similarity(b, a)` to within float precision.
4. One degenerate check. If any embedding has a norm below 0.01 or above 100, the model has a training bug. Flag it.

Refuse to declare a model good on analogy accuracy alone. Analogy benchmarks are gameable and do not transfer to downstream tasks. Recommend intrinsic + downstream evaluation together.

연습문제

  1. 쉬움. 고양이와 강아지 관련 문장 20개로 구성한 장난감 코퍼스에서 학습 루프(training loop)를 돌립니다. 200 에폭(epoch) 학습 뒤, nearest(vocab, W, W[vocab["cat"]])이 상위 3개 안에 dog를 포함하는지 확인합니다. 그렇지 않다면 에폭 수나 어휘를 늘려 봅니다.
  2. 중간. 빈도가 높은 단어에 대한 서브샘플링(subsampling)을 추가합니다. 빈도가 10^-5를 넘는 단어는, 그 빈도에 비례하는 확률로 학습 쌍에서 누락(drop)시킵니다. 드문 단어 유사도(rare-word similarity)에 어떤 영향을 주는지 측정합니다.
  3. 어려움. 20 Newsgroups 코퍼스에서 모델을 학습합니다. he - shedoctor - nurse 두 가지 편향 축(bias axis)을 계산한 뒤, 직업(occupation) 단어들을 두 축에 사영(project)합니다. 어떤 직업이 가장 큰 편향 차이를 보이는지 보고합니다. 이는 공정성 연구자들이 실제로 사용하는 종류의 진단 방법입니다.

핵심 용어

용어흔한 설명실제 의미
단어 임베딩(Word embedding)단어를 벡터로 표현한 것문맥에서 학습된 밀집(dense)하고 저차원(typically 100–300)의 표현
Skip-gramWord2Vec의 트릭중심 단어에서 문맥 단어를 예측합니다. CBOW보다 느리지만 드문 단어에 강합니다
네거티브 샘플링(Negative sampling)학습 단축 기법전체 어휘에 대한 소프트맥스를 k개 무작위 단어에 대한 이진 분류로 대체합니다
정적 임베딩(Static embedding)단어당 벡터 한 개문맥과 무관하게 단어마다 같은 벡터를 사용합니다. 다의어에 실패합니다
문맥적 임베딩(Contextual embedding)문맥에 민감한 벡터주변 단어에 따라 등장할 때마다 다른 벡터를 사용합니다. 트랜스포머가 만들어 냅니다
OOV(Out-Of-Vocabulary)어휘 밖 단어학습 시 보지 못한 단어로, Word2Vec은 이 단어들의 벡터를 만들 수 없습니다

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

embedding-probe

Inspect a word2vec model. Run analogies, find neighbors, diagnose quality.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.Word2Vec은 어휘 전체에 대한 소프트맥스(softmax) 대신 네거티브 샘플링(negative sampling)을 사용합니다. 네거티브 샘플링이 해결하는 핵심 계산 문제는 무엇인가요?

2.'bank'라는 단어는 모든 문맥에서 공유되는 하나의 정적(static) Word2Vec 임베딩만 가집니다. 다운스트림 분류기가 'river bank'(강둑)에 관한 문장을 받았을 때 오분류할 수 있는 이유는 무엇인가요?

3.Word2Vec 유추(analogy) 'king - man + woman = queen'에서, 벡터 (king - man)은 기하학적으로 무엇을 나타내나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다