2023. 2. 19. 14:42ㆍNatural 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 |