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

이번 글에서는 word2vec의 두 번째 개선을 해볼 것이다.

은닉층 이후의 처리(행렬 곱과 Softmax 계층의 계산)이다. 네거티브 샘플링이라는 기법을 사용해 볼 것이다.

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


은닉층 이후 계산의 문제점

어휘가 100만 개, 은닉층 뉴런이 100개일 때의 word2vec을 예로 생각해보자.

입력층과 출력층에는 뉴런이 각 100만 개씩 존재한다.

이전에는 Embedding 계층을 도입하여 입력층 계산에서의 낭비를 줄였다. 남은 문제는 은닉층 이후의 처리이다.

첫 번째는 거대한 행렬을 곱하는 문제이다. 큰 행렬의 곱을 계산하려면 시간이 오래 걸린다. 또한 역전파 때도 같은 계산을 수행하기 때문에 행렬 곱을 가볍게 만들어야 한다.

두 번째로 Softmax에도 같은 문제가 발생한다. 즉 어휘가 많아지면 Softmax의 계산량도 증가한다.

k번째 원소를 타깃으로 했을 때의 Softmax 계산식이다. 이 식에서는 어휘 수를 100만 개로 가정했으므로 분모의 값을 얻으려면 exp 계산을 100만 번 수행해야 한다. 이 계산도 어휘 수에 비례해 증가하므로 Softmax를 대신할 가벼운 계산이 필요하다.


다중 분류에서 이진 분류로

네거티브 샘플링의 핵심은 '이진 분류'이다. '다중 분류'를 '이진 분류'로 근사하는 것이 네거티브 샘플링을 이해하는 데 중요 포인트이다.

 

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

이진 분류 문제를 신경망으로 푸려면 점수에 시그모이드 함수를 적용해 확률로 변환하고, 손실을 구할 때는 손실 함수로 '교차 엔트로피 오차'를 사용한다. 이 둘은 이진 분류 신경망에서 가장 흔하게 사용하는 조합이다.

 

구현

다중 분류에서는 출력층에 어휘 수만큼의 뉴런을 준비하고 이 뉴런들이 출력한 값을 Softmax 계층에 통과시켰다.

이때 이용되는 신경망을 '계층'과 '연산' 중심으로 그리면 아래 그림처럼 된다.

맥락이 'you'와 'goodbye'이고, 정답이 되는 타깃이 'say'인 경우의 예이다.

또한 입력층에서는 각각에 대응하는 단어 ID의 분산 표현을 추출하기 위해 Embedding 계층을 사용했다.

위의 신경망을 이진 분류 신경으로 변환하면 신경망 구성은 아래 그림과 같아진다.

은닉층 뉴런 h와 출력 측의 가중치 W(out)에서 단어 'say'에 해당하는 단어 벡터와의 내적을 계산한다. 그리고 그 출력을 Sigmoid with Loss 계층에 입력해 최종 손실을 얻어낸다.

 

위의 그림의 후반부를 더 단순하게 만들어 볼것이다.

이를 위해 Embedding Dot 계층을 도입한다. 이 계층은 Embedding 계층과 'dot 연산'의 처리를 합친 계층이다. 이 계층을 사용하면 위의 그림처럼 그릴 수 있게 된다.

그리고 은닉층 뉴런 h는 Embedding Dot 계층을 거쳐 Sigmoid with Loss 계층을 통과한다. Embedding Dot 계층을 사용하면서 은닉층 이후의 처리가 간단해졌다.

그럼 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

총 4개의 인스턴스 변수가 있다. params에는 매개변수를 저장하고, grads에는 기울기를 저장한다.

embed는 Embedding 계층을, cache는 순전파 시 계산 결과를 잠시 유지하기 위한 변수로 사용한다.

순전파를 담당하는 forward(h, idx) 메서드는 인수로 은닉층 뉴런(h)과 단어 ID의 넘파이 배열(idx)를 받는다.

forward() 메서드에서는 우선 Embedding 계층의 forward(idx)를 호출한 다음 내적을 계산한다.

위의 그림처럼 적당한 W와 h, 그리고 idx를 준비한다. 여기에서 idx가 [0, 3, 1]인데, 이는 3개의 데이터를 미니배치로 한 번에 처리하는 예이다. idx가 [0, 3, 1]이므로 target_W는 W의 0번, 3번, 1번째의 행을 추출한 결과이다.

그리고 target_W * h는 각 원소의 곱을 계산한다. 그리고 이 결과를 행마다(axis = 1) 전부 더해 최종 결과 out을 얻는다.

이상이 Embedding Dot 계층의 순전파이다. 역전파는 순전파의 반대 순서로 기울기를 전달해 구현한다.

 


이상으로 '다중 분류'에서 '이진 분류'로 변환하는 법을 배웠다. 지금까지는 긍정적인 예에 대해서만 학습했기 때문에 부정적인 예를 입력하면 어떤 결과가 나올지 확실하지 않다.

TAGS.

Comments