Skip-gram 코드 구현 : Word2Vec의 Skip-gram 모델 구현

2022. 1. 10. 00:01NLP

저번 포스트에서는 논문 Efficient Estimation Of Word Representations In Vector Space를 소개하고 Word2Vec의 아키테쳐에 대해 설명했습니다. 이번에는 Word2Vec 중 Skip-gram 모델을 구현해보겠습니다. 

 

skip-gram

Skip-gram은 Word2vec에서 제시한 모델 중 하나입니다. CBOW와 반대로 중심단어로 부터 주변단어를 예측합니다. 그림에서 볼 수 있듯이 Skip-gram의 모델의 imput layer는 하나이고 output layers는 주변단어의 수만큼 존재합니다. 따라서 각 output layer에서는 softmax with loss layer 등을 이용해 손실을 구하고 이 손실을 모두 더한 값이 최종 손실이 됩니다. 

 

주변단어 수 만큼 손실을 계산해야하기 때문에 CBOW보다 학습 속도가 느리지만 단어분산표현의 정밀도 면에서 Skip-gram의 모델의 결과가 더 좋은 경우가 많습니다. 

 

PyTorch를 사용하여 Skip-gram 알고리즘을 구현하는 과정입니다. kaggle의 text8.txt 데이터의 일부(1000개의 단어)로 학습했습니다.  중심단어를 입력했을 때, 그 주변단어를 도출될 확률을 높이도록 학습하는 것이 목표입니다. 

import sys
sys.path.append('..') 
from common.trainer import Trainer
from common.optimizer import Adam
from simple_cbow import SimpleCBOW
from common.util import preprocess, create_contexts_target, convert_one_hot

 

1. SkipGram 클래스 

class SimpleSkipGram:
    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_layer = MatMul(W_in) # 입력층
        self.out_layer = MatMul(W_out) # 출력층
        self.loss_layer1 = SoftmaxWithLoss() # Softmax 계층
        self.loss_layer2 = SoftmaxWithLoss() # Softmax 계층

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

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

- SimpleSkipGram 클래스의 인수

vocab_size : 어휘 수

hidden_size : hidden layer의 뉴런 수

 

- W_in , W_out 두개의 가중치

각각 작은 무작위 값으로 초기화 됩니다. 

 

- 계층 생성

입력층의 Matmul 계층 1개

출력층의 Matmul 계층 1개

Softmax with Loss 계층 주변단어 수(=윈도우 크기) 만큼 (여기서는 2개)

 

 

2. 순전파 forward

    def forward(self, contexts, target):
        h = self.in_layer.forward(target)
        s = self.out_layer.forward(h)
        l1 = self.loss_layer1.forward(s, contexts[:, 0])
        l2 = self.loss_layer2.forward(s, contexts[:, 1])
        loss = l1 + l2
        return loss

3. 역전파 backward

    def backward(self, dout=1):
        dl1 = self.loss_layer1.backward(dout)
        dl2 = self.loss_layer2.backward(dout)
        ds = dl1 + dl2
        dh = self.out_layer.backward(ds)
        self.in_layer.backward(dh)
        return None

 

4. 학습

## 학습데이터 준비과정
window_size = 1 
hidden_size = 5 # 은닉층의 뉴런수
batch_size = 3
max_epoch = 1000

text = x # 데이터
corpus, word_to_id, id_to_word = preprocess(text) # corpus를 단어 id로 반환

vocab_size = len(word_to_id) # 어휘 수
contexts, target = create_contexts_target(corpus, window_size) # 중심, 주변단어 반환

# one-hot encoding
target = convert_one_hot(target, vocab_size) 
contexts = convert_one_hot(contexts, vocab_size)
## 모델학습
model_2 = SimpleSkipGram(vocab_size, hidden_size)
optimizer = Adam()
trainer_2 = Trainer(model_2, optimizer)

trainer_2.fit(contexts, target, max_epoch, batch_size)

- 매개변수 갱신 방법

Adam

 

 

5. 결과

학습경과를 그래프로 나타내면 학습을 할 수록 손실이 감소하고 있는 것을 볼 수 있습니다. 

가로축은 학습 횟수, 세로축은 손실

각 단어의 분산 표현은 다음과 같습니다. 

ghosts [ -1.554753  -14.199934    5.4088864  -8.995648   -4.0544376]
mind [ 10.231861  -12.334701    2.9066288   2.9563556   5.0717473]
saying [  0.6028301  -16.115307     0.48380145  -6.917041     2.6495762 ]
reality [ 14.173826   -0.3625413 -10.682704    4.753849    8.219211 ]
advocated [13.001251   3.5767713 -6.613554   9.323543   7.023613 ]

# (중략)

단어를 벡터화시키는 것이 가능해졌습니다. 즉 분산표현으로 단어의 의미를 나타냈습니다. 

 

 

reference

Word2Vec  ⌜Efficient Estimation Of Word Representations In Vector Space⌟

밑바닥부터 시작하는 딥러닝2 (사이토 고키)

https://github.com/WegraLee/deep-learning-from-scratch-2/tree/master/ch03

https://www.kaggle.com/ashukr/implementation-of-word2vec-paper/data