딥러닝

밑바닥부터 시작하는 딥러닝 - word2vec 속도 개선(1)

Leesemo 2021. 10. 23. 20:56

지금까지 word2vec의 구조를 배우고 CBOW 모델을 구현해봤다.

CBOW 모델은 단순한 2층 신경망이라 간단하게 구현할 수 있었다.

그러나 말뭉치에 포함된 어휘 수가 많아지면 계산량도 커진다는 문제점이 있었다.


그래서 이번 글에서는 속도 개선에 대해 배울 것이다.

앞에서 배운 word2vec에 두 가지 개선을 추가할 것이다.

 

Embedding 계층

앞에서는 단어를 원핫 표현으로 바꿨다. 그리고 그것을 MatMul 계층에 입력하고, MatMul 계층에서 가중치 행렬을 곱해 사용했다. 그럼 어휘 수가 100만 개인 경우에는 어떨까?

은닉층 뉴런이 100개라면, MatMul 계층의 행렬 곱은 아래 그림처럼 된다.

만약 100만 개의 어휘를 담은 말뭉치가 있다면, 단어의 원핫 표현도 100만 차원이 된다. 그리고 벡터와 가중치 행렬을 곱해야 한다. 그러나 위의 그럼에서 결과적으로 수행하는 일은 단지 행렬의 특정 행을 추출하는 것이다. 따라서 원핫 표현으로의 변환과 MatMul 계층의 행렬 곱 계산은 필요가 없어진다.

 

그렇다면 가중치 매개변수로부터 '단어 ID에 해당하는 행(벡터)'을 추출하는 계층을 만들어보자.

이 계층을 Embedding 계층이라고 할 것이다.

 


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

인스턴스 변수 idx에는 추출하는 행의 인덱스를 배열로 저장한다.

역전파를 생각해보면 앞 층(출력층)으로부터 전해진 기울기를 다음 층(입력층)으로 흘려주면 된다.

앞 층으로부터 전해진 기울기를 가중치 기울기 dW의 특정 행에 설정한다. 그림으로는 아래 그림처럼 된다.

def backward(self, dout):
        dW, = self.grads
        dW[...] = 0
        dW[self.idx] = dout
        return None

가중치 기울기 dW를 꺼낸 다음, dW[...] = 0 문장에서 dW의 원소를 0으로 덮어쓴다. 그리고 앞 층에서 전해진 기울기를 idx번째 행에 할당한다.

하지만 앞의 backward() 구현에 문제가 하나 있다. idx의 원소가 중복될 때 발생한다. idx가 [0, 2, 0, 4] 일 경우 생각해보자. 그렇다면 아래 그림과 같은 문제가 발생한다.

dh의 각 행 값을 idx가 가리키는 장소에 할당해보면, dW의 0번째 행에 2개의 값이 할당된다. 먼저 쓰여진 값을 덮어쓴다는 뜻이다.

이 중복 문제를 해결하려면 '할당'이 아닌 '더하기'를 해야 한다. 즉, dh의 각 행의 값을 dW의 해당 행에 더해준다.

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

for문을 사용해 해당 인덱스에 기울기를 더했다. 이렇게 처리하면 idx에 중복 인덱스가 있더라도 올바르게 처리된다.


 

이것으로 Embedding 계층을 구현해봤다. 이제 word2vec의 구현은 입력 측 MatMul 계층을 Embedding 계층으로 전환할 수 있다. 그 효과로 메모리 사용량을 줄이고 쓸데없는 계산도 생략할 수 있게 되었다.