본문 바로가기

밑바닥부터 시작하는 딥러닝

[밑바닥부터 시작하는 딥러닝2: Chapter 3] word2vec

728x90


본 포스팅은 밑바닥부터 시작하는 딥러닝2을 토대로 공부한 내용을 정리하기 위한 포스팅입니다.
해당 도서에 나오는 Source Code 및 자료는 GitHub를 참조하여 진행하였습니다.
https://github.com/WegraLee/deep-learning-from-scratch-2

GitHub - WegraLee/deep-learning-from-scratch-2: 『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019)

『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019). Contribute to WegraLee/deep-learning-from-scratch-2 development by creating an account on GitHub.

github.com


이번 장에서는 ‘추론 기반 기법’으로 단어의 분산을 표현을 얻어내고, 단순한 word2vec를 구현하도록 한다.

3.1 추론 기반 기법과 신경망


통계 기반 기법은 단어의 동시발생 행렬을 만들고, 그 행렬에 SVD를 적용하여 밀집벡터를 얻는 기법이었다. 그러나 이 방식은 대규모 말뭉치를 다룰 때 문제가 발생한다. 예를 들어 어휘가 100만 개라면, 통계 기반 기법에서는 ‘100만x100만’이라는 거대한 행렬을 만들어야 한다.


추론 기반 기법은 [그림 3-2]처럼 주변 단어가 주어졌을 때 ‘?’에 무슨 단어가 들어가는지를 추측하는 기법이다. 즉, 타깃의 단어를 출력하는 것이 목적이다. 그러기 위해서는 추론 문제를 반복해서 풀면서 단어의 출현 패턴을 학습해야 한다.

3.1.3 신경망에서의 단어 처리


신경망은 단어를 있는 그대로 처리할 수 없으므로 단어를 ‘고정 길이의 벡터’로 변환해야 한다. 이때 사용하는 대표적인 방법이 단어를 원핫 표현(one-hot)으로 변환하는 것이다. 원핫 표현이란 벡터의 원소 중 하나만 1이고 나머지는 모두 0인 벡터를 말한다.


앞 장과 같이 “You say goodbye and I say hello."라는 한 문장의 말뭉치는 ('you', 'say', 'goodbye', 'and', 'I', 'say', 'hello', '.')의 7개 단어로 분리할 수 있다. 2장에서처럼 단어를 단어 ID로 표현하면 ‘you'와 ’goodbye‘는 각각 0, 2이고 이를 원핫 표현으로 변환하면 [그림 3-4]처럼 나타낼 수 있다.

이처럼 단어를 고정 길이 벡터로 변환하면 우리 신경망의 입력층은 [그림 3-5]처럼 뉴런의 수를 ‘고정’할 수 있다.

[그림 3-5]처럼 입력층의 뉴런은 총 7개이다. 그럼 단어를 벡터로 나타낼 수 있고, 신경망을 구성하는 ‘계층’들은 벡터를 처리할 수 있고, 따라서 단어를 신경망으로 처리할 수 있다. [그림 3-7]은 fully-connected layer에 의한 변환을 단순화한 그림이다.


3.2 단순한 word2vec


앞 절에서는 신경망으로 단어를 처리하는 방법에 대해 정리했다. 이번에는 word2vec에서 제안하는 CBOW(continuous bag-of-words)모델을 정리해볼 것이다.

CBOW 모델은 맥락으로부터 타깃(taget)을 추측하는 용도의 신경망이다. 현재 목표는 이 CBOW 모델이 가능한 한 정확하게 추론하도록 훈련시켜서 단어의 분산 표현을 얻어내는 것이다. CBOW 모델의 신경망은 [그림 3-9]처럼 표현할 수 있다.


입력층은 one-hot 변환한 단어 벡터이고, 두 입력층에서 은닉층으로의 변환은 똑같은 fully connected layer(W_in)가 처리한다. 그리고 은닉층에서 출력층 뉴런의 변환은 다른 fully connected layer(W_out)가 처리한다.
[그림 3-9]의 은닉층의 뉴런은 입력층의 fully connected layer에 의해 변환된 값이 되는데, 입력층이 여러 개이면 전체를 ‘평균’하면 된다. 예를 들어 첫 번째 입력층을 h1, 두 번째 입력층을 h2로 변환되었다고 하면, 은닉층 뉴런은 (h1+h2)/2가 된다.
마지막으로 출력층 7개의 뉴런 하나하나는 각각의 단어에 대응하고 있다. 그리고 출력층 뉴런은 각 단어의 ‘점수’를 뜻하며, 갚이 높을수록 대응 단어의 출현 확률이 높아진다. 여기서 점수란 확률로 해석되기 전의 값이고, 이 점수에 softmax 함수를 적용해서 ‘확률’을 얻을 수 있다.

그럼 이번엔 CBOW 모델을 ‘뉴런 관점’이 아닌, ‘계층 관점’으로 이해해보도록 한다.

[그림 3-11]에서 알 수 있듯이 CBOW 모델의 가장 앞단에는 2개의 MatMul 계층이 있고, 이어서 두 계층의 출력이 더해진다. 그리고 더해진 값의 평균값은 은닉층 뉴런이 된다. 마지막으로 은닉층 뉴런에 또 다른 MatMul 계층이 적용되어 ‘점수(score)'가 출력된다.

CBOW 모델은 활성화 함수를 사용하지 않는 간단한 구성의 신경망이다. 또한, 입력층은 가중치 W_in을 공유한다는 점을 주의해야 한다.

3.2.2 CBOW 모델의 학습


지금까지 정리한 CBOW 모델은 각 단어의 점수를 출력했다. 이 점수에 softmax 함수를 적용하면 ‘확률’을 얻을 수 있었는데, 이 확률은 맥락이 주어졌을 때 그 중앙에 어떤 단어가 출현하는지를 나타낸다.

CBOW 모델의 학습에서는 taget에 대해 올바른 예측을 할 수 있도록 가중치를 조정하는 일을 한다. 그 결과로 W_in과 W_out 모두에 단어의 출현 패턴을 파악한 벡터가 학습된다. 이번 절에서는 신경망을 학습시키기 위해 Softmax와 Cross Entropy Error(CEE)를 이용하도록 한다. 여기에서는 Softmax로 점수를 확률로 변환하고, 그 확률과 정답 레이블로부터 CEE를 구한 후, 그 값을 손실로 사용해 학습을 진행한다. 계층 그림으로 표현하면 [그림 3-13]과 같다.


  [그림 3-13]은 CBOW 모델의 손실을 구하는 계산의 흐름이자, 이 신경망의 순방향 전파이다.

3.3 학습 데이터 준비


앞서 말했듯 지금 목표는 신경망에 ‘contexts’을 입력했을때 ‘target'이 출현할 확률을 높이는 것이다. 그럼 말뭉치에서 ‘contexts'와 ’target‘을 만드는 작업을 알아보도록 하자.

우선 말뭉치 텍스트를 단어 ID로 변환해야 한다. 그런 다음 단어의 id의 배열인 corpus로부터 contexts와 target을 만들어낸다. 구체적으로는 [그림 3-17]처럼 corpus를 입력하면 contexts와 target을 반환하는 함수를 구현한다.


[그림 3-17]과 같이 contexts는 2차원 배열이다. 이때 contexts의 0번째 차원에는 각 context 데이터가 저장된다. 마찬가지로 target에서도 target[0]에는 0번째 target, target[1]에는 1번째 target이 저장된다. [그림 3-17]을 create_contexts_target(corpus, window_size)로 구현한다.

def preprocess(text):
    text = text.lower()
    text = text.replace('.', ' .')
    words = text.split()
    
    word_to_id = {}
    id_to_word = {}
    
    for word in words:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word
            
    corpus = np.array([word_to_id[w] for w in words])
    
    return corpus, word_to_id, id_to_word
def create_contexts_target(corpus, window_size = 1):
    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)
text = "You say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text)
contexts, target = create_contexts_target(corpus, window_size=1)
print(contexts)
print(target)


이것으로 corpus로부터 contexts와 target을 만들어냈다. 이를 CBOW 모델에 넘겨주면 되는데, 아직 contexts와 target이 단어 ID이므로 one-hot 표현으로 변환해야한다.

3.3.2 원핫 표현으로 변환

contexts와 target을 one-hot 표현으로 변환하면 형상이 각각 (6,2), (6,)에서 (6, 2, 7), (6, 7)이 된다. one-hot 표현으로의 변환은 책에서 제공하는 convert_one_hot() 함수를 사용한다. 코드는 아래와 같다.

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 모델 구현


구현해야 할 CBOW 모델의 신경망은 [그림 3-19]와 같다. 여기서는 SimpleCBOW 클래스로 구현해보도록 한다.

import numpy as np 
from functions import softmax, cross_entropy_error


class MatMul:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.x = None

    def forward(self, x):
        W, = self.params
        out = np.dot(x, W)
        self.x = x
        return out

    def backward(self, dout):
        W, = self.params
        dx = np.dot(dout, W.T)
        dW = np.dot(self.x.T, dout)
        self.grads[0][...] = dW
        return dx


class Affine:
    def __init__(self, W, b):
        self.params = [W, b]
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None

    def forward(self, x):
        W, b = self.params
        out = np.dot(x, W) + b
        self.x = x
        return out

    def backward(self, dout):
        W, b = self.params
        dx = np.dot(dout, W.T)
        dW = np.dot(self.x.T, dout)
        db = np.sum(dout, axis=0)

        self.grads[0][...] = dW
        self.grads[1][...] = db
        return dx


class Softmax:
    def __init__(self):
        self.params, self.grads = [], []
        self.out = None

    def forward(self, x):
        self.out = softmax(x)
        return self.out

    def backward(self, dout):
        dx = self.out * dout
        sumdx = np.sum(dx, axis=1, keepdims=True)
        dx -= self.out * sumdx
        return dx


class SoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.y = None  # softmax의 출력
        self.t = None  # 정답 레이블

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)

        # 정답 레이블이 원핫 벡터일 경우 정답의 인덱스로 변환
        if self.t.size == self.y.size:
            self.t = self.t.argmax(axis=1)

        loss = cross_entropy_error(self.y, self.t)
        return loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]

        dx = self.y.copy()
        dx[np.arange(batch_size), self.t] -= 1
        dx *= dout
        dx = dx / batch_size

        return dx


class Sigmoid:
    def __init__(self):
        self.params, self.grads = [], []
        self.out = None

    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
        return dx


class SigmoidWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.loss = None
        self.y = None  # sigmoid의 출력
        self.t = None  # 정답 데이터

    def forward(self, x, t):
        self.t = t
        self.y = 1 / (1 + np.exp(-x))

        self.loss = cross_entropy_error(np.c_[1 - self.y, self.y], self.t)

        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]

        dx = (self.y - self.t) * dout / batch_size
        return dx
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 


여기서 입력 측의 맥락을 처리하는 MatMul 계층은 맥락에서 사용하는 단어의 수(윈도우 크기)만큼 만들어야 한다. 그리고 입력 측 MatMul 계층들은 모두 같은 가중치를 이용하도록 초기화한다. 마지막으로 이 신경망에서 사용되는 매개변수와 기울기를 인스턴스 변수인 params와 grads 리스트에 각각 모아둔다.

순전파인 forward() 메서드는 인수로 contexts와 target을 받아 loss를 반환한다. 여기서 인수 contexts는 3차원 넘파이 배열이라고 가정한다. [그림 3-18]에서 처럼 배열의 형상은 (6, 2, 7)이 된다. 그 0번째 차원의 원소 수는 미니배치의 수만큼이고, 1번째 차원의 원소 수는 맥락의 윈도우 크기, 2번째 차원은 one-hot 벡터이다. 또한 target의 형상은 2차원으로, 예컨대 (6, 7)과 같은 형상이 된다.

마지막으로 역전파인 backward()를 구현한다. 이 역전파의 계산 그래프는 [그림 3-20]과 같다.

신경망의 역전파는 기울기를 순전파 때와는 반대 방향으로 전파한다. 이 역전파는 ‘1’에서 시작하여 곧바로 Softmax With Loss 계층에 입력된다. 그리고 Softmax With Loss 계층의 역전파 출력이 ds이며, 이 ds를 출력 측 MatMul 계층으로 입력한다.
그런 다음 'x'와 ‘+’ 연산으로 역전파된다. ‘x’의 역전파는 순전파 시의 입력을 ‘서로 바꿔’ 기울기에 곱한다. ‘+’의 역전파는 기울기를 ‘그대로 통과’시킨다. 역전파를 구현하면 다음과 같다.

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


이것으로 역전파 구현을 마무리했다. 이미 각 매개변수의 기울기를 인스턴스 변수 grads에 모아두었다. 따라서 forward() 메서드를 호출한 다음 backward() 메서드를 실행하는 것만으로 grads 리스트의 기울기가 갱신될 것이다.

3.4.1 학습 코드 구현


CBOW 모델의 학습은 일반적인 신경망 학습 과정과 동일하다. 학습 데이터를 준비해 신경망에 입력한 다음, 기울기를 구하고 가중치 매개변수를 순서대로 갱신해간다. 다음은 학습을 위한 소스 코드이다.

 coding: utf-8 
import sys
sys.path.append('..')
import numpy
import time
import matplotlib.pyplot as plt
import numpy as np  # import numpy as np
from util import clip_grads


class Trainer: # Trianer
    def __init__(self, model, optimizer):
        self.model = model
        self.optimizer = optimizer
        self.loss_list = []
        self.eval_interval = None
        self.current_epoch = 0

    def fit(self, x, t, max_epoch=10, batch_size=32, max_grad=None, eval_interval=20):
        data_size = len(x)
        max_iters = data_size // batch_size
        self.eval_interval = eval_interval
        model, optimizer = self.model, self.optimizer
        total_loss = 0
        loss_count = 0

        start_time = time.time()
        for epoch in range(max_epoch):
            # 뒤섞기
            idx = numpy.random.permutation(numpy.arange(data_size))
            x = x[idx]
            t = t[idx]

            for iters in range(max_iters):
                batch_x = x[iters*batch_size:(iters+1)*batch_size]
                batch_t = t[iters*batch_size:(iters+1)*batch_size]

                # 기울기 구해 매개변수 갱신
                loss = model.forward(batch_x, batch_t)
                model.backward()
                params, grads = remove_duplicate(model.params, model.grads)  # 공유된 가중치를 하나로 모음
                if max_grad is not None:
                    clip_grads(grads, max_grad)
                optimizer.update(params, grads)
                total_loss += loss
                loss_count += 1

                # 평가
                if (eval_interval is not None) and (iters % eval_interval) == 0:
                    avg_loss = total_loss / loss_count
                    elapsed_time = time.time() - start_time
                    print('| 에폭 %d |  반복 %d / %d | 시간 %d[s] | 손실 %.2f'
                          % (self.current_epoch + 1, iters + 1, max_iters, elapsed_time, avg_loss))
                    self.loss_list.append(float(avg_loss))
                    total_loss, loss_count = 0, 0

            self.current_epoch += 1

    def plot(self, ylim=None):
        x = numpy.arange(len(self.loss_list))
        if ylim is not None:
            plt.ylim(*ylim)
        plt.plot(x, self.loss_list, label='train')
        plt.xlabel('반복 (x' + str(self.eval_interval) + ')')
        plt.ylabel('손실')
        plt.show()
class Adam: # optimizer
    '''
    Adam (http://arxiv.org/abs/1412.6980v8)
    '''
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = [], []
            for param in params:
                self.m.append(np.zeros_like(param))
                self.v.append(np.zeros_like(param))
        
        self.iter += 1
        lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for i in range(len(params)):
            self.m[i] += (1 - self.beta1) * (grads[i] - self.m[i])
            self.v[i] += (1 - self.beta2) * (grads[i]**2 - self.v[i])
            
            params[i] -= lr_t * self.m[i] / (np.sqrt(self.v[i]) + 1e-7)
import numpy as np
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import ptb
from layers import MatMul, SoftmaxWithLoss
from trainer import Trainer
from optimizer import Adam

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()


매개변수 갱신 방법으로는 SGD와 AdaGrad 같은 유명 알고리즘이 있지만, 이 코드에서는 Adam을 선택했다. Trainer 클래스로 학습 데이터로부터 미니배치를 선택한 다음, 신경망에 입력해 기울기를 구하고, 그 기울기를 Optimizer에 넘겨 매개변수로 갱신하는 일련의 작업을 수행한다. 결과는 [그림 3-21]과 같이 나온다.

결과를 보면 학습을 거듭할수록 손실값이 줄어드는 것을 알 수 있다. 하지만 여기서 다룬 말뭉치는 워낙 작다. 큰 말뭉치로 바꾸면 결과가 좋아지겠지만, 처리 속도 면에서 문제가 발생한다. 지금 구현한 SimpleCBOW 모델은 처리 효율 면에서 몇 가지 문제가 있다. 따라서 4장에서 진정한 CBOW 모델을 구현하도록 한다.

3.5 word2vec 보충


이번 절에서는 CBOW 모델을 ‘확률’ 관점에서 다시 살펴보도록 한다. ‘확률’에서 A라는 현상이 일어날 확률은 P(A)라고 쓴다. 또, 동시 확률은 P(A, B)로 쓴다. 여기서 동시 확률이란 ‘A와 B가 동시에 일어날 확률’을 말한다.
사후 확률은 P(A|B)로 쓴다. 이는 말 그대로 ‘사건이 일어난 후의 확률’이다. ‘B가 주어졌을 때 A가 일어날 확률’이라고도 해석할 수 있다.

그럼 CBOW 모델을 확률 표기법으로 기술해보자. CBOW 모델은 window size 만큼의 context에서 target 단어를 추측하는 모델이었다. 그림으로 나타내면 아래와 같다.

그럼 window size가 1이라고 할 때, 맥락인 W_t-1과 W_t+1이 주어진 경우 target이 W_t가 될 확률을 수식으로 써보면 [식 3.1]처럼 쓸 수 있다.

[식 3.1]

[식 3.1]은 ‘W_t-1과 W_t+1이 일어난 후 W_t가 일어날 확률’을 뜻한다. 그리고 ‘W_t-1과 W_t+1이 주어졌을 때 W_t가 일어날 확률’로 해석할 수도 있다. [식 3.1]을 이용하면 CBOW 모델의 손실 함수도 간결하게 표현할 수 있다. Cross Entropy Error를 적용하고, 여기서 문제의 정답은 'W_t'가 발생‘이므로 W_t에 해당하는 원소만 1이고 나머지는 0이 된다는 점(one-hot)을 고려하면, 다음 식을 유도해낼 수 있다.

[식 3.2]

CBOW 모델의 손실 함수는 단순히 [식 3.1]의 확률에 log를 취한 다음 마이너스를 붙이면 된다. 이를 음의 로그 가능도(negative log likelihood)라 한다. 덧붙여 [식 3.2]는 data 하나에 대한 손실 함수이며, 이를 말뭉치 전체로 확장하기 위해 sigma를 취해주면 다음 식이 된다.

[식 3.3]

CBOW 모델의 학습이 수행하는 일은 이 손실 함수 [식 3.3]의 값을 가능한 한 작게 만드는 것이다. 그리고 이때의 가중치 매개변수가 우리가 얻고자 하는 단어의 분산 표현인 것이다.

3.5.2 skip-gram 모델


word2vec은 CBOW 모델과 skip-gram 모델 두 개를 제안하고 있다. skip-gram은 CBOW에서 다루는 contexts와 target을 역전시킨 모델이다. [그림 3-23]으로 보면 이 두 문제가 푸는 문제를 직관적으로 이해할 수 있다.

  [그림 3-23]과 같이 CBOW 모델은 여러 개의 맥락으로부터 타깃을 찾는 모델이다. 한편, skip-gram 모델은 타깃으로부터 여러 개의 맥락을 찾는 모델이다. 이 그림의 skip-gram 모델의 신경망 구성은 [그림 3-24]처럼 생겼다.

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

그러면 skip-gram 모델을 확률 표기로 나타내보자. target인 W_t로부터 contexts인 W_t-1, W_t+1을 추측하는 경우 [식 3.4]로 모델링 할 수 있다.

[식 3.4]

[식 3.4]는 ‘W_t'가 주어졌을 때 W_t-1과 W_t+1이 동시에 일어날 확률’을 뜻한다. 여기서 skip-gram은 contexts의 단어들 사이에 관련성이 없다고 가정한 후 다음과 같이 분해한다.(‘조건부 독립’)

[식 3.5]

이어서 [식 3.5]에 CBOW 모델의 손실 함수를 구할 때처럼 Cross Entropy Error를 적용하여 skip-gram 모델의 손실 함수를 유도한다.

[식 3.6]

[식 3.6]에서 알 수 있듯, skip-gram 모델의 손실 함수는 맥락별 손실을 구한 다음 모두 더한다. 그리고 이번에도, [식 3.6]은 하나의 data에 대한 skip-gram의 손실 함수이다. 이를 말뭉치 전체로 확장하기 위해 sigma를 적용하면 [식 3.7]이 된다.

[식 3.7]

skip-gram 모델은 contexts의 수만큼 추측하기 때문에 그 손실 함수는 각 맥락에서 구한 손실의 총합이어야 한다.

CBOW 모델과 skip-gram 모델을 비교하면, 단어 분산 표현의 정밀도 면에서 skip-gram 모델의 결과가 더 좋은 경우가 많다. 반면, 학습 속도 면에서는 CBOW 모델이 더 빠르다.

728x90