본문 바로가기

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

3장 word2vec

통계 기반 기법의 문제점

대규모 말뭉치의 어휘 수는 엄청 많기 때문에 통계 기반 기법에서는 거대한 행렬(PPMI 같은)을 만들게 된다. 이러한 거대한 행렬에 SVD를 적용하는 것은 현실적이지 않다. 또한 미니배치 학습이 불가능하다.

 

반면 추론 기반 기법은 신경망을 통해 미니배치 학습이 가능하고 이것의 부산물로 단어의 분산 표현을 얻어낼 수 있다.

 

추론 기반 기법 개요

추론이란 위의 그림처럼 맥락이 주어졌을 때 "?"에 들어갈 단어를 추측하는 작업이다. 모델 관점에서 보면 다음과 같다.

 

 

신경망에서의 단어 처리

신경망에서 you, say 같은 단어를 처리할 수 있는 방법으로 고정 길이의 벡터로의 변환을 사용할 수 있다. 여기서는 one-hot을 사용한다.

 

(이 방법도 문장의 길이가 길어지면 문제점이 생길 것 같다는 생각이 들긴 한다.)

 

이러한 벡터를 입력으로 사용하는 FFNN은 다음과 같이 나타낼 수 있다.

 

가중치의 각 행벡터를 단어의 분산 표현으로 사용한다.

 

단순한 word2vec

CBOW 모델의 추론 처리

CBOW(continuous bag-of-words) 모델은 맥락으로부터 타겟을 추측하는 용도의 신경망이다. 구조는 다음과 같다.

 

입력이 두 개 이므로 은닉층의 출력을 평균내어 사용한다. 예를 들어 첫 번째 입력이 $h_0$로 변환되고 두 번째 입력이 $h_1$로 변환되면 은닉층 뉴런은 $({h_0}+{h_1})/2$이다. 그런 다음 소프트맥스 함수를 적용해서 확률을 얻어낸다.

 

은닉층의 뉴런 수를 입력층의 뉴런 수보다 적게 하는 것이 핵심이다. 이렇게 해야 단어 예측에 필요한 정보를 간결한 밀집벡터로 얻어낼 수 있다.

 

코드로 구현해보기 전에 layer 관점에서 본 신경망의 구조를 살펴보자.

 

import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul

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

# 가중치 초기화
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)

# forward
h0 = in_layer0.forward(c0)
h1 = in_layer1.forward(c1)
h = (h0 + h1)/2
s = out_layer.forward(h)
print(s)
#[[ 0.18872751 -0.23671186  0.23581016 -0.36307464  0.94125291 -0.19321523
#   0.19849204]]

입력층의 MatMul 계층은 가중치 W_in을 공유한다.

 

CBOW 모델의 학습

앞의 코드에서 s에 소프트맥스 함수를 적용하면 확률을 얻을 수 있다. 그런 다음 이를 정답 레이블과 함께 손실함수로 크로스 엔트로피 사용해 신경망을 학습시킬 수 있을 것이다.

 

학습의 부산물로 다음과 같이 2개의 가중치로부터 단어의 분산 표현을 얻을 수 있다.

많은 연구에서 출력 측 가중치는 버리고 입력 측 가중치만을 최종 단어의 분산 표현으로 이용한다고 한다.

 

 

학습 데이터 준비

맥락과 타겟

이를 다음과 같이 변환하여 사용한다.

 

CBOW 모델 구현

\

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

가중치는 astype('f') 메서드를 통해 32비트 부동소수점으로 초기화한다.

self.word_vecs에 단어의 분산 표현을 저장해둔다.

    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

contexts는 3차원 넘파이 배열이라고 가정한다. 그림 3-18을 예로 들면 shape이 (6, 2, 7)이다. 각 차원은 미니배치 크기, 맥락의 윈도우 크기, 원-핫 벡터를 의미한다.

이 입력 데이터에 대한 타겟의 shape은 (6, 7)이다.

    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

self.grads에 각 계층의 gradient를 보관해두었기 때문에 backward 메서드를 실행하는 것 만으로 grads가 갱신된다.

 

역전파의 계산 그래프는 다음과 같다.

 

학습을 수행한 뒤 다음과 같이 단어의 분산 표현을 얻을 수 있다.

word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
    print(word, word_vecs[word_id])
    
# you [-1.0970764  1.184899  -1.1113789  1.1572413 -1.2421755]
# say [ 1.2290171  -1.2552629   1.1583844  -1.2282614   0.92615485]
# goodbye [-0.7493066   0.6464459  -0.77691483  0.7031769  -0.8328685 ]
# and [ 1.391182  -1.2094862  1.4481384 -1.37329   -1.2865809]
# i [-0.7533861   0.63487667 -0.78025097  0.70588225 -0.8059587 ]
# hello [-1.1215996  1.1895287 -1.1145066  1.1500275 -1.2619911]
# . [ 0.34314007 -0.8159545   0.01803486 -0.3891676   1.9403551 ]

 

word2vec 보충

CBOW 모델과 확률

 

윈도우 크기가 1인 경우 맥락으로 $w_{t-1}$과 $w_{t+1}$이 주어졌을 때 타깃이 $w_t$가 될 확률의 수식은 다음과 같다.

이를 크로스 엔트로피를 적용하면 다음과 같다. 

이는 데이터에 하나에 대한 손실 함수이며, 전체는 다음과 같다.

 

skip-gram 모델

CBOW 모델이 맥락으로 부터 타겟을 추측하는 문제라면 skip-gram은 반대로 주어진 단어로 부터 주변의 맥락을 추측하는 문제이다.

 

skip-gram 모델의 신경망 구성은 다음과 같다.

 

입력층은 하나이고, 출력층은 맥락의 수 만큼 존재한다. 따라서 학습시에는 각각 개별 손실을 구하고 이를 모두 더한 것을 최종 손실로 한다.

 

윈도우 크기 1을 예시로 skip-gram 모델을 확률 표기로 나타내보자.

맥락이 조건부 독립이라고 가정하면 다음과 같이 분해 가능하다.

이에 크로스 엔트로피를 적용하고 전체 데이터에 대한 손실함수는 다음과 같다.

 

단어 분산 표현의 정밀도 면에서 skip-gram 모델의 결과가 더 좋은 경우가 많아서 이를 더 많이 사용한다고 한다. 단점으로는 손실을 맥락의 수 만큼 구해야해서 학습 속도 측면에서는 CBOW보다 느리다.

 

' > 밑바닥부터 시작하는 딥러닝 2' 카테고리의 다른 글

6장 게이트가 추가된 RNN (1)  (0) 2023.10.27
5장 RNN (2)  (0) 2023.09.27
5장 RNN (1)  (0) 2023.09.26
4장 word2vec 속도 개선  (0) 2023.09.26
2장 자연어와 단어의 분산 표현  (0) 2023.09.24