데이터 한 그릇

word2vec 개선 본문

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

word2vec 개선

장사이언스 2022. 1. 19. 15:23

word2vec 개선

앞에서 만든 word2vec 같은 경우 CBOW 모델이였다.

CBOW 모델은 말뭉치의 크기 커지면 계산 시간이 너무 많이 걸린다는 단점이 있다.

이번 장에서는 word2vec 의 속도 개선을 할 것.

첫 번째 개선으로는 `Embedding` 이라는 새로운 계층을 만들 것.

두 번째 개선으로는 `네거티브 샘플링`이라는 새로운 손실 함수를 도입한다.

 


word2vec 개선(1)

word2vec 이 시간이 많이 걸리는 두 가지 이유는 아래와 같다.

  1. 입력층의 원핫 표현과 가중치 행렬 W_in 의 곱 계산(4.1절에서 해결)
  2. 은닉층과 가중치 행렬 W_out 의 곱 및 Softmax 계층의 계산

**첫 번째**는 입력층 단어의 원핫 표현이다. 만일 문서의 전체 말뭉치 개수가 7개면 원핫 표현된 벡터는 길이가 7이다. 이 경우에는 계산이 빠르게 될 수 있다. 하지만 말뭉치의 개수가 10000개 라고 가정하면 벡터의 길이가 너무 길어지게 되고 메모리가 낭비되게 된다. 또한 길이가 긴 벡터와 w_in과의 곱도 상당한 시간이 소요된다.

**이 문제를 `Embedding` 계층을 세우면서 해결하려고 한다.**

**두 번째**는 은닉층 이후의 계산이다. 은닉층의 값들과 w_out 을 곱하는 것만 해도 시간이 많이 소모된다.

그리고 softmax 계층에서도 단어가 많기 때문에 계산량이 증가하여 계산이 느려지게 된다.

**이 문제는 `네거티브 샘플링` 이라는 새로운 손실 함수를 만들면서 해결하려고 한다.**

(크게 문제를 은닉층을 기준으로 나눠서 생각하면 이해하기 쉽다.)

 

Embedding 계층

단어의 개수가 10000개인데 원핫 인코딩을 하게 되면 단어는 10000차원의 벡터로 표현이 된다.

10000차원의 벡터와 w_in 행렬을 곱하게 되면 시간이 너무 오래 걸린다.

하지만 원핫 인코딩된 단어와 w_in 행렬을 곱한 결과는 w_in 행렬의 특정 행을 가져오는 것과 같다.

즉, 입력 단어ID 에 해당하는 w_in 행렬의 행이 도출되는 것과 같다.

따라서, 가중치 매개변수로부터 '단어ID 에 해당하는 행(벡터)을 추출하는 계층' 을 만들어야 한다.

이 계층을 `Embedding` 계층이라고 한다.

자연어 처리 분야에서 단어의 밀집 벡터 표현을 단어 임베딩이 혹은 단어 분산 표현이라고 부른다.

 

행렬에서 특정 행을 추출하는 코딩은 굉장히 단순하다.

파이썬 코드는 아래와 같다.

 

import numpy as np
W = np.arange(21).reshape(7,3)
print(W)

#3번째 행 뽑기

print(W[2])

#6번째 행 뽑기

print(W[5])

 

이제 Embedding 계층을 이루는 함수를 만들어 보자.

 

class Embedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.idx = None
        
    def forward(self,idx):
        W, = self.params
        self.idx = idx
        out = W[idx]
        return out
    
    def backward(self,dout):
        dW, = self.grads
        dW[...] = 0
        dw[self.idx] = dout #실은 나쁜 예
        
        return None

 

하지만 backward는 문제가 발생한다.

만일 구하려고 하는 idx가 동일할시 즉, idx = [0,2,0,4] 라면, w의 0번째 행에 두 개의 값이 할당이 된다. 따라서 할당이 아니라 '더하기' 를 해줘야 한다.

 

따라서 함수를 수정하면 다음과 같다.

 

def backward(self, dout):
    dW, = self.grads
    dW[...] = 0
    
    for i, word_id in enumerate(self.idx):
        dW[word_id] += dout[i]
        
        
    return None

 

word2vec 개선 (2)

남은 문제점은 은닉층 이후의 계산. (W_out 과의 행렬곱과 softmax 계산)

`네거티브 샘플링(부정적 샘플링)` 기법을 사용해서 이 문제를 해결.

softmax 함수 대신에 네거티브 샘플링 기법을 사용하면 어휘가 아무리 많아져도 계산량을 낮은 수준에서 일정하게 억제할 수 있다.

 

다중 분류에서 이진 분류로

softmax 는 다중 분류 문제에서 사용하는 활성화 함수다.

네거티브 샘플링의 핵심 아이디어는 "이진 분류" 이다.

즉, 다중 분류 문제를 이진 분류에 근사 시키는 것이 이 기법의 핵심이다.

cbow에서 다중 분류 문제를 이중 분류 문제로 바꿀 수 있는 질문을 생각해 내는 것.

예를 들어서 우리는 you say goodbye and I say hello 라는 문장이 있을 때, cbow 방식을 사용해서 맥락 단어 you, goodbye 가 들어갔을 때, 어떤 단어가 나올지 추축하는 과정을 거쳤다. 하지만 질문을 바꿔서 you와 goodbye 가 들어갔을 때, say 가 나오는가? 로 바꾸면 이진 분류 문제로 바꿀 수 있다.

신경망의 구조도 이러한 질문에 기반하여 바꾼다.

따라서, 은닉층과 w_out 행렬의 내적을 할 때, 우리가 구하려고 하는 target 단어에 해당하는 단어 벡터를 w_out 에서 꺼내서 은닉층과 곱해준다. (w_out 가중치 행렬에서는 열이 각각의 단어 벡터를 표현한다.)

즉 위의 예시대로면 say 에 해당하는 단어 벡터를 w_out 에서 꺼내서 은닉층 행렬과 곱해준다.

(은닉층 행렬이 1 x 100 이고 w_out 이 100 x 10000 인데 질문을 이진 분류로 바꾼 신경망에서는

은닉층 행렬이 1 x 100 이고 say 단어 벡터, 100 x 1 이다.)

결국 하나의 노드 값 (1 x 1 )이 도출이 되고 이 값을 sigmoid 에 넣어서 확률로 바꿔준다.

시그모이드 함수와 교차 엔트로피 함수

이진 분류 문제에서 가장 많이 사용하는 활성화 함수와 손실 함수는 sigmoid 함수와 교차 엔트로피 오차다.

값이 sigmoid 함수를 거쳐서 가공되고 이 값이 다시 교차 엔트로피 오차 식에 들어가서 가공되어 Loss 를 출력한다.

오차역전파를 통해서 나아가면 simoid 함수에 들어가기전 입력 x에 대해서 y-t 를 출력한다.

y는 신경망의 예측확률을 의미하고 t는 정답 레이블을 의미한다.

예를 들어서 정답 레이블이 1이면 y가 1에 가까워질수록 오차는 적어지고 1과 멀어질수록 오차는 커진다.

따라서 오차가 크면 크게 학습하고 오차가 작으면 작게 학습한다.

다중 분류에서 이진 분류로(구현)

먼저 지금까지의 과정을 살펴보자.

은닉층 이전의 문제점은 각 단어마다 원핫 인코딩을 하면 벡터가 너무 길어진다는 점이다.

따라서 Embedding 계층을 세웠는데, 이는 단어 벡터와 w_in 을 곱하면 해당 단어에 해당하는 index의 w_in 행이 도출된다는 점을 이용했다. 즉, w_in 의 해당 단어 분산 표현을 원핫인코딩 대신에 사용한다. (이 값은 w_in 과의 곱이 끝난걸 의미한다.) 따라서 각 단어마다의 embed 값이 도출되고 이 값을 더하고 개수로 나눠서 은닉층 행렬을 얻는다.

다음 문제는 은닉층 이후의 문제다.

모든 어휘에 대한 값을 출력해야 하기 때문에 w_out 과의 내적과 soft 함수를 적용한 계산이 오래걸린다.

따라서 이진 분류로의 문제로 근사시켜야 한다.

따라서 target 단어에 해당하는 벡터를 w_out 행렬에서 끄집어 낸다. w_out 의 열은 단어의 분산 분포를 나타낸다. 따라서 해당하는 target 단어의 열에 해당하는 w_out의 열을 끄집어 낸다.(단어 벡터를)

그리고 이 값을 은닉층(h) 행렬과 곱하고 sigmoid 함수에 넣고 loss 오차에 집어넣는다.

이때 타겟 데이터를 embedding 하고 h와 곱하는 걸 Embedding dot 계층을 두어서 한 번에 해결할 수 있다.

코드는 다음과 같다.

 

class EmbeddingDot:
    def __init__(self,W):
        self.embed = Embedding(W)
        self.params = self.embed.params
        self.grads = self.embed.grads
        self.cache = None
        
    def forward(self,h, idx):
        target_W = self.embed.forward(idx)
        out = np.sum(target_W * h, axis = 1)
        
        self.cache = (h, target_W)
        
        return out
    
    def backward(self,dout):
        h, target_W = self.cache
        
        dout = dout.reshape(dout.shape[0],1)
        
        dtarget_W = dout * h
        self.embed.backward(dtarget_W)
        dh = dout * target_W
        return dh

 

네거티브 샘플링

하지만 이진 분류로 근사시키기 위해서는 긍정 정답에 대한 가중치 설정 뿐만 아니라 부정 정답에 대한 가중치 설정도 해야만 한다.

즉, 정답에 가까우면 출력값을 1에 가깝게 출력하고 정답이 아니면 출력값을 0에 가깝게 추출하는 가중치 설정이 필요하다.

(지금까지 한 작업은 긍정 정답에 가까울수록 1에 가깝게 출력하는 가중치 설정이였다.)

 

하지만 타겟 데이터 이외의 모든 데이터를 학습 시키는 일은 우리가 word2vec 을 개선하려는 목적과 반대된다.

따라서 다른 방법을 사용해야만 하는데, 바로 **부정적 예를 몇 개만 선택해서 사용하는 것**이다. => `네거티브 샘플링`

 

**(네거티브 샘플링 정리)**

정리하면, 네거티브 샘플링은 긍정적 예를 타깃으로 한 경우의 가중치 설정과 손실 값을 구한다.

그리고 몇 개의 부정적 예를 선별해서 그 부정적 예들에 대해서도 학습을 하면서 가중치 설정을 하고 각각 손실 값을 구한다.

그리고 긍정적 예의 손실값과 부정적 예의 손실을 더해서 최종 손실로 정한다.

(이때 부정적 예시의 단어들의 개수는 5 ~ 10개로 한다.)

네거티브 샘플링의 샘플링 기법

그러면 부정적 단어의 샘플은 어떻게 구하는가?

무작정 랜덤한 단어를 뽑는 것보다 더 좋은 방법이 존재한다.

말뭉치에서 자주 등장하는 단어를 많이 추출하고 드물게 등장하는 단어를 적게 추출한다.

말뭉치에서 단어별 출현 횟수를 바탕으로 확률 분포를 구한 다음, 그 확률 분포에 따라서 샘플링을 시행하면 된다.

(본래, 샘플링의 개수가 크면 클수록 성능이 좋아지지만, 그러면 word2vec 수정의 이유가 없어지므로 많이 할 수가 없다. 근데 문서 내에서 자주 등장하지 않은 단어로 부정 단어를 학습시키게 되면 성능이 더 나쁠 수 밖에 없다.)

이때 확률분포를 구할 때 기본 확률 분포에 0.75를 제곱해야만 한다.

p(w_i) 는 i번째 단어가 나올 확률을 의미하는데, 이 확률에 0.75를 해준다.

제곱을 해주는 이유는 나타날 확률이 너무 적은 단어들에 대해서 구제해주기 위함이다.

제곱을 해주면 너무 낮은 확률을 기록하고 있는 단어들도 확률이 커지게 되어 뽑힐 확률이 어느 정도 높아진다.

 


앞서 개선한 word2vec 모델을 이용해서 데이터들을 학습하게 되면 각 단어에 대한 단어 벡터가 생성이 된다.

이 단어 벡터는 입력 기울기 행렬의 행에 해당하며 이는 단어의 분산 표현을 담고 있다고 할 수 있다.

이 단어 벡터를 이용해서 비슷한 단어를 유추할 수 있게 된다.

 


word2vec 남은 주제

word2vec 으로 얻은 단어의 분산 표현은 비슷한 단어를 찾는 용도로 사용될 수 있다.

하지만 여기서 끝나는 게 아니다. word2vec이 중요한 이유는 **전이 학습 (transfer learning)** 에 있다.

전이 학습은 한 분야에서 배운 지식을 다른 분야에도 적용하는 기법이다.

그러니, 미리 단어 분산 표현들을 획득하고 원하는 자연어 처리 분야에 적용할 수 있다.

 

또한 단어 분산 표현은 단어를 고정 길이 벡터로 변환해준다는 장점도 있다.

또한 `문장` 도 단어의 분산 표현을 사용해서 고정 길이 벡터로 변환할 수 있다.

문장을 고정 벡터 길이로 변환하는 방법은 활발히 연구되고 있으며 가장 간단한 방법은 문장의 각 단어를 분산 표현으로 변환하고 그 합을 구하는 것. 이를 bag-of-words 라 하여, 단어의 순서를 고려하지 않은 방식이다.

하지만 순환 신경망(RNN) 을 이용하면 한층 세련된 방법으로 문장을 고정 길이 벡터로 바꿀 수 있다. (단어 word2vec 을 이용해서)

word2vec 사용

단어를 word2vec 하고 머신러닝에 적용하여 실제 문제를 해결

단어 벡터 평가 방법

그렇다면 word2vec 의 성능은 어떻게 평가할 것인가?

word2vec 과 머신러닝 학습을 거쳐서 나온 성능으로 평가를 하기엔 시간이 너무 많이 걸린다.(최적 하이퍼 파라미터 찾아가는 과정도 있으니)

따라서 머신러닝 적용 이전의 word2vec에서 자체적으로 성능을 평가 해야만 한다.

평가 방법은 1. 단어의 유사성 2. 유추 문제를 통해서 할 수 있다.

  1. 단어의 유사성은 사람이 임의로 만들어 놓은 단어 간 유사도를 이용해서 word2vec의 각 단어 벡터의 코사인 유사도 수치와 비교하는 방식이다.
  2. 유추 문제를 활용한 평가는 king : queen = man : ? 과 같은 문제를 출제하고 그 정답률로 단어의 분산 표현의 우수성을 측정한다.

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

어텐션(Attention)  (0) 2022.01.25
게이트가 추가된 RNN  (0) 2022.01.21
순환신경망(RNN)  (0) 2022.01.20
word2vec  (0) 2022.01.18
자연어와 단어의 분산 표현  (0) 2022.01.13
Comments