본문 바로가기
ML & DL/책 & 강의

[밑시딥2] CHAPTER 3 word2vec

by 공부하는 무니 2023. 7. 12.
반응형

3.1 추론 기반 기법과 신경망

3.1.1 통계 기반 기법의 문제점

2장에서 다뤘던 통계 기반 기법은 대규모 말뭉치를 다룰 때 문제가 발생한다. 현업에서 다루는 말뭉치의 어휘 수는 어마어마하다. 영어의 어휘 수는 100만이 넘는데, 이를 통계 기반 기법을 적용하면 100만x100만이라는 거대한 행렬이 만들어진다.

통계 기반 기법은 말뭉치 전체의 통계를 이용해 단 1회의 처리 만에 단어의 분산 표현을 얻는다. 한편 추론 기반 기법에서는 특히 신셩망을 이용하는 경우 미니배치로 학습하는 것이 일반적이다. 

추론 기반 기법은 여러 머신과 여러 GPU를 이용한 병렬 계산도 가능해져서 학습 속도를 높일 수 있다. 

3.1.2 추론 기반 기법 개요

우리는 이 모델로 신경망을 사용한다. 모델은 맥락 정보를 입력받아 각 단어의 출현 확률을 출력한다. 이러한 틀 안에서 말뭉치를 사용해 모델이 올바른 추측을 내놓도록 학습시킨다.

3.1.3 신경망에서의 단어 처리

신경망을 이용해 단어를 처리할 것인데, 원핫 벡터로 변환하여 단어를 표현한다.

3.2 단순한 word2vec

word2vec에서 사용하는 신경망은 CBOW와 skip-gram이 있다. 여기서는 CBOW를 구현해보자.

3.2.1 CBOW 모델의 추론 처리

CBOW 모델은 맥락으로부터 타깃을 추측하는 용도의 신경망이다. 

지금까지는 CBOW를 뉴런 관점에서 그려왔다. 이번에는 계층 관점에서 그려보자.

# coding: utf-8

import numpy as np
from layers import MatMul


# 샘플 맥락 데이터
c0 = np.array([[1, 0, 0, 0, 0, 0, 0]])
c1 = np.array([[0, 0, 1, 0, 0, 0, 0]])

# 가중치 초기화
W_in = np.random.randn(7, 3)
W_out = np.random.randn(3, 7)

# 계층 생성
in_layer0 = MatMul(W_in)
in_layer1 = MatMul(W_in)
out_layer = MatMul(W_out)

# 순전파
h0 = in_layer0.forward(c0)
h1 = in_layer1.forward(c1)
h = 0.5 * (h0 + h1)
s = out_layer.forward(h)
print(s)

3.2.2 CBOW 모델의 학습

지금까지 설명한 CBOW 모델은 출력층에서 각 단어의 점수를 출력했다. 이 점수에 소프트맥스 함수를 적용하면 '확률'을 얻을 수 있다. 이 확률은 맥락이 주어졌을 때, 그 중앙에 어떤 단어가 출현하는지를 나타낸다.

여기에 Softmax 계층과 Cross Entropy Error 계층을 추가하면 이것만으로 손실을 얻을 수 있다.

3.2.3 word2vec의 가중치와 분산 표현

word2vec에서 사용되는 신경망에는 두 가지 가중치가 있다. Win과 Wout.

출력 측 가중치는 각 단어의 분산표현이 열 방향으로 저장된다.

그렇다면 최종적으로 단어의 분산 표현으로는 어느 쪽 가중치를 선택하면 좋을까?

A. 입력 측의 가중치만 이용한다.

B. 출력 측의 가중치만 이용한다.

C. 양쪽 가중치를 모두 이용한다.

word2vec(skip-gram): A안

GloVe: C안 (두 가중치를 더한다)

3.3 학습 데이터 준비

3.3.1 맥락과 타깃

word2vec에서 이용하는 신경망의 인풋은 '맥락'이다. 그리고 정답 레이블은 맥락에 둘러싸인 중앙의 단어, 즉 '타깃'이다. 말뭉치에서 맥락과 타깃을 만드는 작업을 생각해보자.

각 샘플 데이터에서 맥락의 수는 여러 개가 될 수 있으나, 타깃은 오직 하나뿐이다. 

맥락과 타깃을 만드는 함수는 아래와 같다.

def create_contexts_target(corpus, window_size=1):
    '''맥락과 타깃 생성

    :param corpus: 말뭉치(단어 ID 목록)
    :param window_size: 윈도우 크기(윈도우 크기가 1이면 타깃 단어 좌우 한 단어씩이 맥락에 포함)
    :return:
    '''
    target = corpus[window_size:-window_size]
    contexts = []

    for idx in range(window_size, len(corpus)-window_size):
        cs = []
        for t in range(-window_size, window_size + 1):
            if t == 0:
                continue
            cs.append(corpus[idx + t])
        contexts.append(cs)

    return np.array(contexts), np.array(target)

3.3.2 원핫 표현으로 변환

맥락과 타깃을 원핫 표현으로 바꿔보자.

이때 다시 한번 각각의 다차원 배열의 형상에 주목해야 한다. 그림에서 보면 단어ID를 이용했을 떄의 맥락의 형상은 (6, 2)인데, 원핫 표현으로 변환하면 (6, 2, 7)이 된다.

def convert_one_hot(corpus, vocab_size):
    '''원핫 표현으로 변환

    :param corpus: 단어 ID 목록(1차원 또는 2차원 넘파이 배열)
    :param vocab_size: 어휘 수
    :return: 원핫 표현(2차원 또는 3차원 넘파이 배열)
    '''
    N = corpus.shape[0]

    if corpus.ndim == 1:
        one_hot = np.zeros((N, vocab_size), dtype=np.int32)
        for idx, word_id in enumerate(corpus):
            one_hot[idx, word_id] = 1

    elif corpus.ndim == 2:
        C = corpus.shape[1]
        one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
        for idx_0, word_ids in enumerate(corpus):
            for idx_1, word_id in enumerate(word_ids):
                one_hot[idx_0, idx_1, word_id] = 1

    return one_hot

3.4 CBOW 모델 구현

# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss


class SimpleCBOW:
    def __init__(self, vocab_size, hidden_size):
        V, H = vocab_size, hidden_size

        # 가중치 초기화
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(H, V).astype('f')

        # 계층 생성
        self.in_layer0 = MatMul(W_in)
        self.in_layer1 = MatMul(W_in)
        self.out_layer = MatMul(W_out)
        self.loss_layer = SoftmaxWithLoss()

        # 모든 가중치와 기울기를 리스트에 모은다.
        layers = [self.in_layer0, self.in_layer1, self.out_layer]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads

        # 인스턴스 변수에 단어의 분산 표현을 저장한다.
        self.word_vecs = W_in

    def forward(self, contexts, target):
        h0 = self.in_layer0.forward(contexts[:, 0])
        h1 = self.in_layer1.forward(contexts[:, 1])
        h = (h0 + h1) * 0.5
        score = self.out_layer.forward(h)
        loss = self.loss_layer.forward(score, target)
        return loss

    def backward(self, dout=1):
        ds = self.loss_layer.backward(dout)
        da = self.out_layer.backward(ds)
        da *= 0.5
        self.in_layer1.backward(da)
        self.in_layer0.backward(da)
        return None

3.4.1 학습 코드 구현

# coding: utf-8
from trainer import Trainer
from optimizer import Adam
from simple_cbow import SimpleCBOW
from util import preprocess, create_contexts_target, convert_one_hot


window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000

text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)

vocab_size = len(word_to_id)
contexts, target = create_contexts_target(corpus, window_size)
target = convert_one_hot(target, vocab_size)
contexts = convert_one_hot(contexts, vocab_size)

model = SimpleCBOW(vocab_size, hidden_size)
optimizer = Adam()
trainer = Trainer(model, optimizer)

trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
    print(word, word_vecs[word_id])

3.5 word2vec 보충

CBOW 모델을 '확률' 관점에서 다시 살펴보자.

3.5.1 CBOW 모델과 확률

CBOW 모델을 확률 표기법으로 기술하면 아래와 같다.

맥락이 주어졌을 때 타깃이 될 확률은 수식으로 아래와 같이 쓸 수 있다.

이를 활용하면 교차 엔트로피 오차 식도 표현할 수 있다.

CBOW모델의 손실 함수는 단순히 사후 확률에 로그를 취한 다음 -를 붙인 값이 된다. 이를 음의 로그 가능도(negative log likelihood)라고 한다. 위 식은 샘플 데이터 하나에 대한 손실함수이며, 이를 말뭉치 전체로 확장하면 아래와 같다.

CBOW모델을 학습한다는 것은 위 식의 값을 가능한 한 작게 만드는 것이다. 그리고 이 때의 가중치 매개변수가 우리가 얻고자 하는 단어의 분산 표현이 된다. 

3.5.2 skip-gram 모델

skip-gram은 CBOW에서 다루는 맥락과 타깃을 역전시킨 모델이다.

skip-gram모델의 신경망 구성은 아래와 같다.

skip-gram의 입력층은 하나이고 출력층은 맥락의 수만큼 존재한다. 따라서 출력층에서 개별적으로 손실을 구하고 이 손실들을 모두 더한 값을 최종 손실로 한다. 

skip-gram모델을 확률 표기로 나타내면 아래와 같다.

여기서 skip-gram모델에서는 맥락의 단어들 사이에 관련성이 없다(조건부 독립)고 가정하고 다음과 같이 분해한다.

이를 교차 엔트로피 오차에 적용하면 skip-gram 모델의 손실 함수를 유도할 수 있다.

위 식은 샘플 데이터 하나짜리 skip-gram의 손실 함수 이다. 이를 말뭉치 전체로 확장하면 아래와 같다

CBOW와 skip-gram 중 어떤 것을 사용해야 할까? 단어 분산 표현의 정밀도 면에서 skip-gram이 더 좋다. 특히 말뭉치가 커질수록 저빈도 단어나 유추 문제의 성능 면에서 skip-gram이 더 뛰어나다. 반면 학습속도는 CBOW가 더 빠르다. 

3.5.3 통계 기반 vs. 추론 기반

  통계 기반 추론 기반
학습하는 틀 말뭉치의 전체 통계로부터 1회 학습하여 단어의 분산 표현을 얻었다 말뭉치를 일부분씩 여러 번 보면서 학습했다
단어의 분산 표현 갱신 계산을 처음부터 다시 해야 한다. 단어의 분산 표현을 조금만 수정하고 싶어도 동시발생 행렬을 다시 만들고 SVD를 수행하는 일련의 작업을 다시 해야 한다. 매개변수를 다시 학습할 수 있다. 지금까지 학습한 가중치를 초깃값으로 사용해 다시 학습하면 된다. 
분산 표현의 성격, 정밀도 주로 단어의 유사성이 인코딩된다.  단어의 유사성은 물론, 한층 복잡한 단어 사이의 패턴까지도 파악되어 인코딩 된다.

word2vec 이후 추론 기반 기법과 통계 기반 기법을 융합한 GloVe기법이 등장했다. 말뭉치 전체의 통계 정보를 손실 함수에 도입해 미니배치 학습을 하는 것이다. 이로 인해 두 세계를 명시적으로 융합하는 데 성공했다.

3.6 정리

  • 추론 기반 기법은 추측하는 것이 목적이며, 그 부산물로 단어의 분산 표현을 얻을 수 있다.
  • word2vec은 추론 기반 기법이며, 단순한 2층 신경망이다.
  • word2vec은 skip-gram 모델과 CBOW 모델을 제공한다.
  • CBOW 모델은 여러 단어(맥락)로부터 하나의 단어(타깃)를 추측한다.
  • 반대로 skip-gram 모델은 하나의 단어(타깃)로부터 다수의 단어(맥락)를 추측한다.
  • word2vec은 가중치를 다시 학습할 수 있으므로, 단어의 분산 표현 갱신이나 새로운 단어 추가를 효율적으로 수행할 수 있다.
반응형

댓글