[NLP] word2vec 개선 - 임베딩, 네거티브 샘플링

2023. 2. 19. 14:42Natural Language Processing

저번 장에서 구현한 word2vec에는 문제점이 있다. 

 

원핫 인코딩된 단어 벡터와 가중치 행렬을 곱했을 때 결국은 가중치 행렬에서 특정 행만 추출된다. 

 

원핫 인코딩 벡터들의 모음은 결국에 대부분의 원소가 0으로 이루어진 희소행렬이기 때문에 

 

forward 과정에서 불필요한 행렬 계산이 이루어진다. 

 

따라서 우리는 Embedding이란 기법을 알아보고 구현에 적용해본다. 

 

위 그림은 원 핫 인코딩된 벡터와 가중치 행렬과의 행렬 곱 과정을 보여준다. 

 

원핫 벡터는 특정 열만 1을 가지고 있기 때문에 행렬 곱에서 결국 가중치 행렬의 특정 행만 추출하기 때문에 

 

굳이 행렬 곱을 하지 않더라도 행만 추출하면 행렬 곱을 한 것과 같은 효과를 얻을 수 있다. 

 

class Embedding:
    def __init__(self, W):
        self.params = W
        self.grads = [np.zeros_like(W)]
        # 역전파 시 그레디언트 저장
        self.idx = None

임베딩 클래스는 가중치 행렬을 입력받고, idx에 해당하는 가중치 행렬의 행을 추출한다. 

    def forward(self, idx):
        W = self.params
        #미니배치 처리를 고려(한듯)
        self.idx = idx
        out = W[idx]
        return out

 가중치 행렬 idx행만 추출하고 이를 반환한다.

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

역전파 시에는 뒤의 층에서 흘러들어온 그레디언트를 누적하여 역전파해준다. 

 

Embedding 계층을 활용하여 EmbeddingDot 계층을 만들어보자.

 

class EmbeddingDot:
    def __init__(self, W):
        self.embed = Embedding(W)
        self.params = self.embed.params
        self.grads = self.embed.grads
        self.cache = None

EmbeddingDot 계층은 Embedding 계층을 포함하여 은닉층 이후의 과정도 수행한다. 

 

    def forward(self, h, idx):
        target_W = self.embed.forward(idx)
        out = np.sum(target_W*h, axis=1)
        # 추출한 가중치 값과 은닉층 뉴런 h의 '원소곱 후 행마다 합'함(행렬 곱과 같은 효과)
        self.cache = (h, target_W) #은닉층 뉴런과 임베딩 데이터를 저장
        return out

forward에서는 h와 가중치 행렬의 특정행(target의 임베딩)을 내적한다. 

 

인자로 받는 h는 문맥 정보를 함축한 벡터 표현이며, target_W는 tartget의 임베딩 표현이다. 

 

이렇게 나온 결과(out)는 문맥 정보와 정답(target)과의 괴리 정도(score)라고 생각할 수 있다. 

 

원래 저번처럼 문맥 정보 벡터를 모든 단어 사전의 단어 벡터와 행렬곱하여 Softmax의 결과를 구하고,

 

가장 높은 값의 후보를 예측으로 출력할 수 있는데 왜 이렇게 해야하는 것일까?

 

위 방법은 단어 사전의 크기가 너무 클 경우, 차원에 저주에 빠질 수 있다는 단점이 있다. 

 

이에 대한 해결책으로 특정 몇몇 단어를 샘플링하고 그 특정 단어가 정답인지, 오답인지만을 출력하는 모델을 설계한다. 

class NegativeSamplingLoss:
    def __init__(self, W, corpus, power=0.75, sample_size=5):
        self.sample_size = sample_size
        self.sampler = UnigramSampler(corpus, power, sample_size)
        self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)]
        self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]

        self.params, self.grads = list(), list()
        for layer in self.embed_dot_layers:
            self.params.append(layer.params)
            self.grads.append(layer.grads)

NegativeSamplingLoss 계층은 샘플링할 단어의 수와 각 계층들을 멤버 변수로 포함한다. 

    def forward(self, h, target):
        batch_size = target.shape[0]
        negative_sample = self.sampler.get_negative_sample(target)

        # 긍정적 예 순전파
        score = self.embed_dot_layers[0].forward(h, target)
        correct_label = np.ones(batch_size, dtype=np.int32)
        # 긍정적 예이므로 np.ones
        loss = self.loss_layers[0].forward(score, correct_label)
        # Sigmoid 취한 뒤 크로스 엔트로피 loss 구함
        
                # 부정적 예 순전파
        negative_label = np.zeros(batch_size, dtype=np.int32)
        # 부정적 예이므로 np.zeros
        for i in range(self.sample_size):
            # 샘플링한 부정적 예 개수만큼 반복
            negative_target = negative_sample[:, i]  # 샘플링한 부정단어를 배치처리
            score = self.embed_dot_layers[1 + i].forward(h, negative_target)
            loss += self.loss_layers[1 + i].forward(score, negative_label)

target의 인덱스에 따라 정답과 동떨어진 맥락 벡터를 샘플링한다. 

 

예를 들어서 주어진 맥락이 'I'와 'hello'이고 target이 'say'일 때 네거티브 샘플링은 

 

say 이외의 carrot, tomato와 같은 단어들을 샘플링한다. 

 

모델은 위 맥락이 주어졌을 때 say와 같은 단어들의 출력은 1에 가깝게, carrot, tomato와 같은 단어들은 

 

0에 가까운 출력을 내도록 훈련된다. 

    def backward(self, dout=1):
        dh = 0
        for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):
            dscore = l0.backward(dout)
            dh += l1.backward(dscore)

        return dh

네거티브 샘플링의 역전파는 먼저 시그모이드-로스 계층을 거쳐서 임베딩 계층으로 전파된다. 

'Natural Language Processing' 카테고리의 다른 글

[NLP] LSTM(Long Short-Term Memory)  (0) 2023.02.25
[NLP] RNN(순환 신경망)  (0) 2023.02.22
[NLP] word2vec  (0) 2023.02.13
[NLP] ppmi과 SVD 차원축소  (0) 2023.02.12
[NLP] 동시발생행렬  (0) 2023.02.12