-
[밑시딥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
위 다이어그램과 마찬가지로 특정 행을 가져오거나 업데이트 한다.
네거티브 샘플링
핵심 아이디어는 다중 분류 문제를 이진 분류로 바꾸는데 있다.
즉, 단어를 통해 단어를 예측하는 것이 아닌, 두 단어를 넣고 Yes 또는 No를 이끌어 내도록 유도하는 것이다.
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에 가깝게 하는 것이 필요하다.네거티브 샘플링은 다음의 방법으로 수행한다.
기본적으로 코퍼스의 통계데이터를 이용한다.
- 코퍼스에서 각 단어의 출현 횟수를 구해 '확률 분포'로 나타낸다.
- 그 확률분포대로 단어를 샘플링한다.
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