자연어처리

word2vec, CBOW (Continuous Bags of Words)

coding art 2023. 1. 3. 16:53
728x90

참조: ”Deep Learning from Scratch ⓶: 밑바닥부터 시작하는 딥러닝2“, 3장 word2vec, pp.113 ~ 141., 사이토 고키 지음, 한빛미디어.

 

분산가설에 따른 통계적 기법과 달리 신경망 학습법을 사용하는 추론적 방법 즉 word2vec 알고리듬에 대해서 살펴보자.

word2vec 알고리듬에서는 인접한 맥락(문맥, context)이 주어져 있을 때 무슨 단어가 가장 적합할지 추론하는 알고리듬이다.

즉 맥락이 주어진 상태에서 ? 에 적합한 단어를 단어장에서 확률적으로 가장 높은 확률값을 가지는 단어를 찾아내는 작업이다.

 

맥락으로 주어지는 입력 단어들은 숫자 라벨 값을 가지고 있는바 이들을 one-hot 코드화 하여 사용하면 편리하지만 통계적 방법에서 동시발생 매트릭스처럼 그 길이가 커지기는 마찬가지이다.

{ you
goodbye }
0 (1,0,0,0,0,0,0)
2 (0,0,1,0,0,0,0)

하지만 word2vec 알고리듬에서는 미니배치(mini batch) 기법 적용이 가능하다. 전체 샘플 데이터를 분할 후 분할된 작은 샘플 데이터만을 램 메모리에 올린 후 연산 작업을 되풀이 실행하면서 업데이트 하는 것이다. 이러한 방법으로 대용량 메모리 필요성 문제를 일단 해결할 수 있게 된다.

 

word2vec 모델의 일종인 CBOW (Continuous Bag of Words) 모델 네트워크를 살펴보자. ‘Continous’ 의 의미는 은닉층의 변수인 웨이트들을 연속변수로 여기는데 있다.

 

단어장에서 ‘you’와 ‘goodbye’에 해당하는 on-hot 코드를 대상으로 7x3 웨이트 매트릭스로 도입한 은닉층(hidden layer) 연산 후 다시 3x7 웨이트 매트릭스를 도입 연산하여 최종적으로 7x1 출력층을 얻어낸다.

 

웨이트 매트릭스는 학습 초기에 무작위한 난수로 채워진다. 1차 학습 결과가 맥락에서 요구하는 ‘say’ 의 확률 값이 처음부터 1.0 이 될 수는 없으므로 오차역전파(back-propagation) 알고리듬에 의해 예를 들어 Gradient Descent 또는 Adam 옵티마이저에 의해 지속적으로 웨이트 값을 업데이트 해 나가다 보면 학습이 완료되어 맥락에 해당하는 웨이트 값이 결정된다. 즉 학습 계산 결과를 살펴보면 밀집(dense) 벡터가 얻어짐을 확인할 수 있다.

 

다음의 표는 현 예제의 모든 경우의 맥락들과 정답 레이블 사례를 표시하였다.

단어장 contexts 정답 레이블
you say goodbye and I say hello. you, goodbye say
you say goodbye and I say hello. say, and goodbye
you say goodbye and I say hello. goodbye, I and
you say goodbye and I say hello. and, say I
you say goodbye and I say hello. I, hello say
you say goodbye and I say hello. say hello

 

이 결과를 함수 create_contexts_target(corpus, window_size=1)을 사용하여 맥락(contexts)과 타겟(target, 정답레이블)을 작성해 보자.

 

1 def create_contexts_target(corpus, window_size=1):
target = corpus[window_size:-window_size] #[+1:-1]-> 1번~6번
contexts = [] # 6개 생성


for idx in range(window_size, len(corpus)-window_size):
cs = []
# -1, 0, +1->0은 제외
for t in range(-window_size, window_size + 1):
if t == 0:
continue
cs.append(corpus[idx + t]) # 1+(-1)->0, 1+(+1)->2
contexts.append(cs)
return np.array(contexts), np.array(target)

 

출력된 맥락(contexts)과 타겟(target, 정답레이블)이 단어 ID 형태이므로 학습을 위해서 이들을 on hot 코드로 변환할 필요가 있다.

 

이런 방식으로 가능한 한 모든 경우의 맥락들에 대한 학습을 완료하게 되면 임의의 테스트용 맥락 입력에 대해서 확률적인 평가가 가능해질 것이다. 이때 사용되는 Cost 함수는 다음과 같은 형태를 취하게 된다.

word2vec 모델에 있어서 입력 데이터를 미니 배치(mini batch)화 하여 PC의 메모리 용량이 허용하는 만큼씩 학습을 시킬 수가 있다. 하지만 단어장의 규모가 백만 단위를 넘어서고 각 단어를 one hot 코드로 표현할 경우 은닉층 Win 의 크기를 (1,000,000, 100) 으로 설정하여 입력층과 은닉층을 곱하면 그 결과는 굳이 연산을 할 필요도 없이 은닉층 Win 가 있는 그대로 embedding 된 상태로 얻어지기 때문이다. 즉 은닉층 Win에 변동이 없다는 의미이다.

 

앞에서 설명된 알고리듬을 바탕으로 class SimpleCBOW로 작성해 보도록 하자.

class SimpleCBOW 코드작성을 위해서 우선 class MatMul과 class SoftmaxWithLoss를 미리 준비해 두자. Colab에서 코드를 실행하려면 미리 class MatMul과 class SoftmaxWithLoss를 class SimpleCBOW 이전 셀에서 실행시켜두면 된다. vocab_size는 corpus를 완성하면 얻어지는 파라메터이다. hidden_size는 word2vec 알고리듬에서 Embedding이 이루어지는 은닉층의 가중치 shape 값이다. 7X3 예제에서는 3이 되면 1,000,000X100 예제에서는 100이 된다. W_in과 W_out은 랜덤한 값으로 초기화 된다. 인위적으로 작은 수 0.01을 곱하면 SimpleCBOW의 효율이 좋아짐에 유의하자.

 

class SoftmaxWithLoss() 는 W_out과의 MatMul 처리된 결과에 대해서 softmax 로 처리 후 Cross Entropy 형의 Cost 함수를 구성 순전파 및 역전파 연산을 지원한다.

 

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

 

2개의 맥락에 대해 하나의 가중치 Win을 사용하여 구성한 레이어들과 출력 레이어를 합해 리스트 데이터 layers를 구성한다. self.params, self.grads를 리스트 데이터로 선언한다. 랜덤한 데이터로 초기화된 레이어별 params, grads를 instance변수인  self.params, self.grads에 저장한다.

 

한편 CBOW 알고리듬에서 단어들로부터 얻어지는 밀집벡터 Win을 instance 변수 self.word_vecs에 저장하였다가 학습이 끝난 후 불러내어 6-1절의 유사도(analogy) 연산에 활용한다.

 

2 # 모든 가중치와 기울기를 리스트에 모은다.
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

 

초기화 이 후 순전파 메소드를 사용해 학습을 시킨다. h0와 h1 연산에 사용된 forward는 class MatMul의 메서드를 사용하며 그때 필요한 가중치는 instance 변수인 self.params 에 저장된 값을 사용한다. 순전파 연산 완료 후 score와 target을 사용하여 Cost 함수 계산이 이루어진다.

 

3 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

 

순전파 연산이 완료 후 이어서 역전파 연산이 이루어진다. backward 연산은 class MatMul의 메서드를 사용하며 그때 필요한 가중치는 self.params 에 저장된 값을 사용한다.

 

4 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

 

class SimpleCBOW를 모델(model) 로 두고 옵티마이저와 함께 빌드 후 학습(fit)을 시키도록 한다.

 

5 ...
model = SimpleCBOW(vocab_size, hidden_size)

optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
...

 

첨부된 ipynb 파일을 구글 Colab에서 셀별로 실행해 보자.

SimpleCBOW.ipynb
0.34MB