word2vec

8 minute read

word2vec

word2vec단어를 추론 기법을 활용하여 나타내는 방법이다.
이전 Post에서 언급한 SVD를 활용하게 되면 n x n행렬에 적용하는 비용이 $O(n^3)$이다. 어휘의 수가 100만 넘어가도 적용하기 힘든 기법이라고 할 수 있다.
이런 통계기법의 한계때문에 사용하는 것이 추론 기법이고 이러한 추론기법에 대표적인 것이 word2vec이다.
이러한 SVD vs word2vec의 관한 비교는 이전 Post에서 언급한 Normal Equation vs Gradient Descent와 비슷한 맥락이다.
Normal Equation vs Gradient Descent
위와 같은 추론기법은 아래 이미지를 참조하면 간단하다.

즉 이전까지의 통계 기반과 다르게 빈칸에 관한 정답을 맞출 수 있도록 Model을 계속하여 Trainning하여 최적의 결과를 뽑는것이 목표이다.

즉, Word2vec이란 비슷한 위치에서 등장하는 단어들은 비슷한 의미를 가진다라는 가정으로서 단어를 표현하는 것 입니다.
분포 가설에 따라서 텍스트를 벡터화한다면 단어들이 의미적으로 가까운 단어는 거리가 짧게 나타나고 단어들이 의미적으로 먼 단어는 거리가 멀게 나타납니다.
이렇게 표현된 벡터들은 벡터의 차원이 단어집합의 크기일 필요도 없고 벡터의 차원이 저차원으로 줄어든다는 장점이 있다.

이러한 word2vec은 CBOW와 skip-gram이라는 2가지 방식이 존재한다.


CBOW

CBOW 모델은 맥락으로 부터 타깃을 추축하는 용도의 신경망 이다.
아래 그림은 CBOW에 대한 목적이다.

위의 그림과 같이 주변단어를 통하여 해당 단어를 맞추는 Model이다.


CBOW DataSet

이전 Post와 같이 Sliding Window를 통하여 전체 Text를 통한 Data Set을 만들때
Target Data를 기준으로 Window Size 안에 있는 단어는 Input Data가 되는 Model이다.
위와 같은 DataSet을 만드는 예시는 아래와 같다.
Text = “You say goodbye and i say hello”

맥락 타깃
you, goodbye say
say, and goodbye
goodbye, i and
and, say i
i, hello say
say, . hello

위와 같은 Data를 실질적으로 적용시키기 위하여 전 Post에서 언급한 Corpus로 변환 후 적용시키면 다음과 같다.
Corpus = “[0 1 2 3 4 1 5 6]”

맥락 타깃
[[0 2] [1
[1 3] 2
[2 4] 3
[3 1] 4
[4 5] 1
[1 6]] 5]

위와 같은 과정은 아래의 Code로서 나타낼 수 있다.

Dataset 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from common.util import preprocess
import numpy as np

# Input, Target Data 나누기
def create_contexts_target(corpus, window_size=1):
    #Target Data는 앞 뒤로 하나씩 버림
    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):
            #-1, 0, 1중에서 0은 Target Data이고 0을 제외한 나머지 쌍은
            #CBOW Model에서 Input Data로서 사용된다.
            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(contexts)
print('target')
print(target)
contexts
[[0 2]
 [1 3]
 [2 4]
 [3 1]
 [4 5]
 [1 6]]
target
[1 2 3 4 1 5]


최종 적인 Data는 위와같이 Corpus된 Input, Target Data를 One-Hot-Encoding으로서 표현하여 사용한다.
Input, Target Data를 CBOW Model에 넣기 위하여 One-Hot-Encodeing을 하는과정이다.

  • corpus: 말뭉치 (1차원 - Target Data, 2차원 - Contexts)
  • vocab_size: 어휘 수 (한 행의 차원)

즉 아래 Code
Context Data (Text 단어 수 x window_size * 2) => One-Hot-Context(Text 단어 수 x window_size * 2 x vocab_size)
Target Data (Text 단어 수) => One-Hot-Target Data(Text 단어 수 x vocab_size)
로서 바꾸는 과정이다 각 원소의 값은 One-Hot-Target 이므로 0 아니면 1 이다.
One-Hot-Encodeing 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def convert_one_hot(corpus, vocab_size):
    N = corpus.shape[0]

    #Target Data인 경우
    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

    #Input Data인 경우
    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

vocab_size = len(word_to_id)
target = convert_one_hot(target,vocab_size)
contexts = convert_one_hot(contexts,vocab_size)

print('contexts')
print(contexts)
print('target')
print(target)
contexts
[[[1 0 0 0 0 0 0]
  [0 0 1 0 0 0 0]]

 [[0 1 0 0 0 0 0]
  [0 0 0 1 0 0 0]]

 [[0 0 1 0 0 0 0]
  [0 0 0 0 1 0 0]]

 [[0 0 0 1 0 0 0]
  [0 1 0 0 0 0 0]]

 [[0 0 0 0 1 0 0]
  [0 0 0 0 0 1 0]]

 [[0 1 0 0 0 0 0]
  [0 0 0 0 0 0 1]]]
target
[[0 1 0 0 0 0 0]
 [0 0 1 0 0 0 0]
 [0 0 0 1 0 0 0]
 [0 0 0 0 1 0 0]
 [0 1 0 0 0 0 0]
 [0 0 0 0 0 1 0]]



CBOW Model

이러한 CBOW Model은 아래 그림처럼 나타 낼 수 있다.

위의 과정에서 모든 단어들을 one-hot-encoding 방식으로 벡터화 하였다 따라서 input 과 target은 다음과 같은 구조를 띄고 있다.

$$x_k = [0, ..., 0,1,0, ..., 0]$$

$$y_j = [0, ..., 0,1,0, ..., 0]$$

또한 몇몇 상수에 대해서 정리하고 넘어가자.
위에서 \(x_k, y_k\)행렬의 크기는 Text의 단어의 개수 이다. 이것은 위에서 V-dim이라고 정의하였다.
또한 Sliding Window를 통하여 Corpus를 통과시키기 때문에 C는 Window_size x 2 - 1(Target Data)이다.
각각의 가중치는 행렬 연산 matmul을 위하여 사용되므로 **Hidden Layer 의 \(h_i\)의 차원인 N을 기준으로 맞추워야 한다.**
따라서 Input Data에 곱해지는 가중치의 \(W_{V x N}\)은 V x N 차원이 되고 Hidden Layer에서 곱해지는 가중치의 \(W_{Nx V} \prime(U)\)은 N x V 차원이 된다.

위와 같은 Model에서 최종적인 목적은 주변 단어들이 주어졌을 때의 중심 단어의 조건부 확률을 최대화 하는 것 이다.
이러한 목적은 아래의 식으로서 나타낼 수 있다.

$$P(x_c|x_{c-m}, ...,x_{c-1}, x_{c+1}, ..., x_{c+m})$$


여기서 위와같은 Model의 진행과정을 살펴보게 되면
각각의 \(x_k = [0, ..., 0,1,0, ..., 0]\) 인 One-Hot-Encoder 방식이르모 아래와 같은 이미지처럼 W와 곱해진다는 것을 알 수 있다.


위와 같은 그림의 특성으로서 곱셈의 결과는 Embedded word vector가 되고 식은 아래와 같다.

$$v_{c-m} = Wx^{c-m}, ... ,v_{c+m} = Wx^{c+m}$$


이후 2m 개의 embedded vector들의 평균을 구하면 이 값이 Hidden Layer 가 된다.
\(h_i = \frac{v_{c-m} + v_{c-m +1} + ... + v_{c+m}}{2m}\) 최종적인 Output을 위하여 가중치인 U를 곱함으로써 결과를 출력한다.

$$z = Uh_i$$

위와 같은 CBOW Model의 최종적인 결과에 Softmax를 사용하여 가장 들어갈 만한 단어를 출력하게 된다.

$$\hat{y} = softmax(z)$$


위와 같은 과정에서 Pamameter Update를 위해 backpropagation을 생각해 보자.
Loss를 최소화 하기 위하여 아래와 같은 Object Function을 정의하자.

$$H(\hat{y},y) = - \sum_{j=1}^{|V|}y_jlog(\hat{y_j})$$

위와 같은 식에서 Target Data인 \(y_j\)또한 One-Hoe-Encoding이므로 최종적인 식은 아래와 같다는 것을 알 수 있다.

$$H(\hat{y},y) = -y_jlog(\hat{y_j})$$

최종적인 식은 우리가 계속해서 사용해왔던 Cross Entropy 와 같다는 것을 알 수 있다.

위와 같은 과정에서 간단한 CBOW Network를 만들어보자.
위의 파라미터에서 Window_size = 1 이라고 가정하고 만든 Network이다.

Simple CBOW Network

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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):
        #Softmax_with_Loss이므로 바로 1의값을 backpropagation의 값으로 할당
        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

위의 과정을 Trainning하여 결과를 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from common.trainer import Trainer
from common.optimizer import Adam
from common.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])

you [-1.3394176 -1.0604719  1.0941241 -1.0375448 -1.1365515]
say [-0.12250558  1.1409756  -1.1509938   1.1322652   1.1484705 ]
goodbye [-0.61274445 -0.85734284  0.80242366 -0.8717004  -0.7234571 ]
and [-1.9480621   0.8366879  -0.8693571   0.82995474  0.8321035 ]
i [-0.6231824 -0.8477259  0.8073446 -0.8777823 -0.7197336]
hello [-1.3399466 -1.0698344  1.086817  -1.0340238 -1.1064119]
. [ 1.6405182  1.1792932 -1.1426598  1.1641334  1.1772958]


위처럼 얻어진 Model에서 우리가 알 수 있는 가중치는 2개이다.
입력 측 완전연결계층의 가중치 W와 출력 측 완전연결계층의 가중치 U 이다.
W는 각 행이 각 단어의 분산 표현에 해당한다.
U는 각 가중지 U에도 단어의 의미가 Encoding된 Vector가 저장되어 있다고 생각할 수 있다.
최종적으로 우리가 사용할 수 있는 단어 분산 표현은 다음과 같이 3가지 경우이다.

  1. W(입력 측의 가중치)만 이용한다.
  2. U(출력 측의 가중치)만 이용한다.
  3. 양쪽 가중치를 모두 이용한다.

위와 같은 상황에서 1번을 사용하는 것이 word2vec에서 대중적인 선택 방법이다.



Skip-gram

Skip-gram 모델은 타깃으로 부터 맥락을 추축하는 용도의 신경망 이다.
아래 그림은 Skip-gram에 대한 목적이다.

위의 그림과 같이 해당단어를 통하여 주변단어를 맞추는 Model이다.

Skip-gram Model

이러한 Skip-gram Model은 아래 그림처럼 나타 낼 수 있다.

위의 그림을 살펴보게 되면 CBOW Model과 Input 과 Output만이 바뀐 것을 알 수 있다.

위와 같은 Model에서 최종적인 목적은 중심 단어가 주어졌을 때의 주변 단어들의 조건부 확률을 최대화 하는 것 이다.
이러한 목적은 아래의 식으로서 나타낼 수 있다.

$$P(x_{c-m}, ...,x_{c-1}, x_{c+1}, ..., x_{c+m}|x_c)$$

$$ \begin{multline} P(x_{c-m}, ...,x_{c-1}, x_{c+1}, ..., x_{c+m}|x_c) \\ = P(x_{c-m}|x_c) P(x_{c-m+1}|x_c) ... P(x_{c+m}|x_c) \end{multline} $$

$$Loss = -logP(x_{c-m}, ...,x_{c-1}, x_{c+1}, ..., x_{c+m}|x_c)$$

$$= -logP(x_{c-m}|x_c) P(x_{c-m+1}|x_c) ... P(x_{c+m}|x_c)$$

$$ \begin{multline} = -\frac{1}{2m} \sum_{k=1}{2m} (logP(x_{c-m}|x_c) + \\ logP(x_{c-m+1}|x_c) + logP(x_{c+m-1}|x_c) + logP(x_{c+m}|x_c)) \end{multline} $$

최종적인 식을 살펴보게 되면 skip-gram 모델은 맥락의 수 만큼 추측하기 때문에 그손실 함수는 각 맥락에서 구한 손실의 총합이여야 한다.
반면 CBOW 모델은 타깃 하나의 Loss를 구한다.


참조: 원본코드
참조: dreamgonfly 블로그
참조: reniew 블로그
참조: 데이터 사이언스 스쿨
참조:dreamgonfly Blog
코드에 문제가 있거나 궁금한 점이 있으면 wjddyd66@naver.com으로 Mail을 남겨주세요.

Categories:

Updated:

Leave a comment