ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [밑시딥2] word2vec 속도 개선
    A.I/Study 2023. 7. 2. 02:49

    word2vec 개선

    기존 CBOW 모델에서의 계산 병목 발생 지점

    • 입력층의 원핫표현과 가중치 행렬 W_in의 곱 계산 -> Embedding 레이어로 해결
    • 은닉층과 가중치 행렬 W_out의 곱 및 Softmax 계층의 계산 -> 네거티브 샘플링으로 해결

    Embedding 계층

    가중치 매개변수로부터 '단어 ID에 해당하는 행(벡터)'을 추출하는 계층

    class Embedding:
      def __init__(self, W):
        	self.params = [W]
            self.grads = [np.zeros_like(W)]
            self.idx = None
            
      def forward(self, idx):
        	W, = params
            self.idx = idx
            out = W[idx]
            return out
            
      def backward(self, dout):
        	dW, = self.grads
            dW[...] = 0
            for i, word_id in enumerate(self.idx):
            	dW[word_id] += dout[i]
            
            return None

    Embedding 레이어의 forward와 backward

    위 다이어그램과 마찬가지로 특정 행을 가져오거나 업데이트 한다.

    네거티브 샘플링

    핵심 아이디어는 다중 분류 문제를 이진 분류로 바꾸는데 있다.

    즉, 단어를 통해 단어를 예측하는 것이 아닌, 두 단어를 넣고 Yes 또는 No를 이끌어 내도록 유도하는 것이다.

    이진 분류를 사용하는 word2vec (CBOW 모델)의 전체 다이어그램

    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

    현재의 신경망은 긍정적인 답만 학습했다. 
    하지만, 네거티브 샘플링을 완전히 구현하기 위해서는 오답에 대한 출력을 0에 가깝게 하는 것이 필요하다.

     네거티브 샘플링은 다음의 방법으로 수행한다.

    기본적으로 코퍼스의 통계데이터를 이용한다.

    1. 코퍼스에서 각 단어의 출현 횟수를 구해 '확률 분포'로 나타낸다.
    2. 그 확률분포대로 단어를 샘플링한다.
    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 = [], []
            for layer in self.embed_dot_layers:
                self.params += layer.params
                self.grads += layer.grads
    
        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)
            loss = self.loss_layers[0].forward(score, correct_label)
    
            # 부정적 예 순전파
            negative_label = np.zeros(batch_size, dtype=np.int32)
            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)
    
            return loss
    
        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

    위 코드에서 사용된 power 변수와 0.75는 출현 확률이 낮은 단어를 완전히 버리지 않기 위해서 추가한 보정값이다.
    0.75라는 수치에는 구체적인 이유가 없음으로 변경해도 무방하다.

    아래는 네거티브 샘플링이 적용된 CBOW 모델이다.

    class CBOW:
        def __init__(self, vocab_size, hidden_size, window_size, corpus):
            V, H = vocab_size, hidden_size
    
            # 가중치 초기화
            W_in = 0.01 * np.random.randn(V, H).astype('f')
            W_out = 0.01 * np.random.randn(V, H).astype('f')
    
            # 계층 생성
            self.in_layers = []
            for i in range(2 * window_size):
                layer = Embedding(W_in)  # Embedding 계층 사용
                self.in_layers.append(layer)
            self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)
    
            # 모든 가중치와 기울기를 배열에 모은다.
            layers = self.in_layers + [self.ns_loss]
            self.params, self.grads = [], []
            for layer in layers:
                self.params += layer.params
                self.grads += layer.grads
    
            # 인스턴스 변수에 단어의 분산 표현을 저장한다.
            self.word_vecs = W_in
    
        def forward(self, contexts, target):
            h = 0
            for i, layer in enumerate(self.in_layers):
                h += layer.forward(contexts[:, i])
            h *= 1 / len(self.in_layers)
            loss = self.ns_loss.forward(h, target)
            return loss
    
        def backward(self, dout=1):
            dout = self.ns_loss.backward(dout)
            dout *= 1 / len(self.in_layers)
            for layer in self.in_layers:
                layer.backward(dout)
            return None

    이로서 아무리 큰 코퍼스가 주어져도 효율적으로 수행할 수 있는 word2vec 모델이 완성됬다.

    레퍼런스

    밑바닥부터 시작하는 딥러닝 2 (사이토 고키 지음, 개앞맵시 옮김)

    개앞맵시님의 밑시딥2 저장소: WegraLee/deep-learning-from-scratch-2: 『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019) (github.com)

    'A.I > Study' 카테고리의 다른 글

    [밑시딥2] 순환 신경망 (RNN)  (0) 2023.07.13
    [Math for Deeplearning] Linear Algebra  (0) 2023.07.08
    [Math for Deeplearning] Probability  (0) 2023.07.01
    [밑시딥2] word2vec  (0) 2023.06.26
    [밑시딥2] 자연어와 단어의 분산 표현  (0) 2023.06.17
Designed by Tistory.