728x90
In [1]:
import numpy as np
import collections
- CBOW의 가장큰 문제는 말뭉치에 포함된 어휘의 수가 많아지면 계산량도 커짐
$\rightarrow$ Embedding계층추가
$\rightarrow$ 네거티브 샘플링 (새로운 손실함수)도입
Embedding계층¶
- 가중치 매개변수로 부터 단어ID에 해당하는 행(벡터)를 추출하는 계층, Embedding계층에 단어 임베딩을 저장
- word embedding(단어 임베딩)은 자연어 처리 분야에서 단어의 밀집벡터 표현을 말함 (단어의 분산표현, distributed representation)
- distributional representation: 통계기반기법으로 얻은 단어벡터
- distributed representation: 추론기반기법으로 얻은 단어벡터
Embedding 계층 구현¶
In [2]:
W = np.arange(21).reshape(7,3)
print('=====W전체======')
print(W)
print('=====W[2]행=====')
print(W[2])
=====W전체====== [[ 0 1 2] [ 3 4 5] [ 6 7 8] [ 9 10 11] [12 13 14] [15 16 17] [18 19 20]] =====W[2]행===== [6 7 8]
In [3]:
idx = np.array([1, 0, 3, 0])
W[idx]
Out[3]:
array([[ 3, 4, 5], [ 0, 1, 2], [ 9, 10, 11], [ 0, 1, 2]])
In [4]:
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
def backward(self, dout):
dW, = self.grads
dW[...] = 0 # dW의 형상을 유지한채 그 원소들을 0으로 덮어씀
np.add.at(dW, self.idx, dout)
return None
- Embedding은 계층의 순전파는 가중치 W의 특정행을 추출하여 특정 행의 뉴런만을 다음 층으로 흘려보냄
- 따라서, Embedding역전파는 출력층으로 부터 전해진 기울기를 다음층으로 그대로 보내주면 됨(앞 층으로부터 전해진 기울기를 가중치 기울기 dW의 특정행에 설정)
np.add.at(A, idx, B): B를 A의 idx번째 행에 더해줌
In [5]:
# # np.add.at 동일한 표현
# for i, word_id in enumerate(self.idx):
# dW[word_id] += dout[i]
네거티브 샘플링¶
- 은닉층 뉴런과 가중치 행렬 곱의 처리, softmax계층 계산을 개선
- softmax대신 네거티브 샘플링을 이용하면 어휘가 많아져도 계산량을 낮은 수준에서 일정하게 억제할 수 있음
- 다중 분류(multi-class classification) $\rightarrow$ 이진분류(binomial classification)로 근사하는 것
- 출력측 가중치에서 단어 하나에 주목하여 그 점수만을 계산하고 시그모이드 함수를 이용해 그 해당 점수를 확률로 변환
- 다중분류: 출력층 $\rightarrow$ softmax함수, 손실함수$\rightarrow$ 교차 엔트로피 오차를 이용
- 이진분류: 출력층 $\rightarrow$ 시그모이드 함수, 손실함수$\rightarrow$ 교차 엔트로피 오차를 이용
- Sigmoid $$ y = \frac{1}{1+exp(-x)}$$
- 교차 엔트로피 오차 $$ L = -(tlogy +(1-t)log(1-y))$$
- 시그모이드와 교차엔트로피 오차를 조합하여 역전파의 값이 $y-t$임
In [6]:
class EmbeddingDot:
def __init__(self, W):
self.embed = Embedding(W)
self.params = self.embed.params # 매개변수 저장
self.grads = self.embed.grads # grad저장
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
네거티브 샘플링¶
- 긍정적인 예에 대해서는 sigmoid계층의 출력을 1에 가깝게 만들고 부정적인 예에 대해서는 sigmoid출력을 0에 가깝게 만드는 것
- ex) You say goodbye and I say hello. 예에서 맥락이 you, goodbye일때, target이 say인 긍정적인 경우 출력이 1이고 hello의 경우 출력을 0으로 해주는 결과를 만들어내는 가중치가 필요
- 모든 부정적인 어휘에 대해서 할수 없으므로 근사적으로 (적은 수의 부정적인 예를 샘플링하여) 사용
- 긍정적인 예를 타깃으로 한 경우 손실과 부정적인 예를 몇개 샘플링 하여 손실을 구하고 각 데이터의 손실을 더한값을 최종 손실로 함
- 네거티브 부정적인 예를 샘플링 하는 방법
- 말뭉치 통계데이터를 기초로 말뭉치에서 자중 등장하는 단어를 많이 추출하고 드물게 등장하는 단어를 적게 추출
- 말뭉치에서 단어별 출현횟수를 바탕으로 확률분포를 구한다음 그 확률분포에서 샘플링
- 확률분포대로 샘플링 하므로 말뭉치에서 자주 등장하는 단어는 선택될 가능성이 높고 희소단어는 선택되기 어려움
In [7]:
words = ['you', 'say', 'goodbye', 'I', 'hello', '.']
print(np.random.choice(words)) # 무작위로 하나만 샘플링
print(np.random.choice(words, size=5)) # 중복가능하며 5개를 샘플링
print(np.random.choice(words, size=5, replace=False)) #중복 불가능 하고 5개를 샘플링
hello ['I' 'say' '.' 'you' 'goodbye'] ['goodbye' 'hello' '.' 'you' 'I']
In [8]:
p = [0.5, 0.1, 0.05, 0.2, 0.05, 0.1 ] #word에 대한 확률분포
np.random.choice(words, p=p) # 확률분포에 따라 샘플링
Out[8]:
'I'
word2vec 수정 권고사항
- 기본확률에 0.75를 제곱하는 것
$$P'(w_i) = \frac{P(w_i)^{0.75}}{\sum_j^nP(w_j)^{0.75}}$$
- 확률분포의 각 요서를 0.75제곱 $\rightarrow$ 출현 확률이 낮은 단어를 버리지 않기 위해
In [9]:
p = [0.7, 0.29, 0.01]
new_p = np.power(p, 0.75)
new_p/= np.sum(new_p)
new_p
Out[9]:
array([0.64196878, 0.33150408, 0.02652714])
- 낮은 확률 (0.01)이 수정 후에는 0.0265 (2.65%)로 증가 하였다. 즉 낮은 확률의 단어가 조금 더 쉽게 샘플링 되도록 하기 위한 구제조치임.
- 0.75제곱의 수치는 이론적 의미는 없으므로 다른 값을 설정해도 됨
네거티브 샘플¶
In [15]:
## Unigram: 하나의 연속된 단어
# 말뭉치에서 단어의 확률분포를 만들고 0.75를 제곱한 후 random sampling으로 부정적인 예를 샘플링
class UnigramSampler:
def __init__(self, corpus, power, sample_size):
self.sample_size = sample_size
self.vocab_size = None
self.word_p = None
counts = collections.Counter()
for word_id in corpus:
counts[word_id] +=1
vocab_size = len(counts)
self.vocab_size = vocab_size
self.word_p = np.zeros(vocab_size)
for i in range(vocab_size):
self.word_p[i] = counts[i]
self.word_p = np.power(self.word_p, power)
self.word_p /= np.sum(self.word_p)
def get_negative_sample(self, target):
batch_size = target.shape[0]
# not GPU
negative_sample = np.zeros((batch_size, self.sample_size), dtype = np.int32)
for i in range(batch_size):
p = self.word_p.copy()
target_idx = target[i]
p[target_idx] = 0
p /= np.sum()
negative_sample[i, :] =np.random,choice(self.vocab_size, size = self.sample_size,\
replace=False, p=p)
return negative_sample
네거티브 샘플링¶
In [16]:
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 = [Embedding(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] #긍정적인 예의 target
negative_sample = self.sampler.get_negative_sample(target) #부정적인 예 샘플링하여 저장
# 긍정적예 순전파 loss[0]
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) # sigmoid with loss
# 부정적예 순전파
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
Sum Node forward and backward 이해¶
In [17]:
d, n = 8,7
x = np.random.randn(n,d)
y = np.sum(x , axis=0, keepdims=True)
print(x)
print(y)
[[ 0.24453787 -0.44849193 2.37522443 0.81504681 -1.88463415 0.85739727 2.05532778 -0.55448243] [ 1.56730253 -0.40965908 0.92791671 -0.47426427 0.95591712 -0.38568626 1.77193157 -0.78366005] [ 0.71945694 -1.0606031 1.25095535 -0.11832978 -0.13193915 -1.62376702 0.85949455 0.72669345] [ 0.41205819 -0.73944381 0.51007843 -1.22023915 0.6461321 2.09379106 -0.45638718 1.16144043] [-0.28008634 0.13319037 -0.59215464 0.09939126 -1.41104754 -2.65212158 -1.57646526 0.32816065] [ 0.20450405 1.20728376 0.10172982 0.3677373 1.02890643 1.02949509 0.47006649 -0.15169109] [ 0.18547259 0.11150395 1.46767237 -0.64790412 -0.91240906 -0.21748425 0.23730154 0.82145199]] [[ 3.05324583 -1.20621984 6.04142247 -1.17856196 -1.70907424 -0.8983757 3.3612695 1.54791295]]
In [18]:
dy = np.random.randn(1, d)
dx = np.repeat(dy, n, axis=0)
print(dy)
print(dx)
[[-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409]] [[-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409] [-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409] [-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409] [-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409] [-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409] [-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409] [-2.17736513 -0.27087786 0.40784092 -0.52614058 -2.29841372 -0.10212383 0.22914595 1.12607409]]
Summary
- Embedding계층은 단어의 분산표현을 담고 있고, 순전파 시 지정한 단어ID의 벡터를 추출한다
- word2vec은 어휘 수의 증가에 비례하여 계산량도 증가하므로 근사치로 계산하는 빠른 기법을 사용하면 좋다.
- 네거티브 샘픓링은 부정적 에를 몇개 샘플링 하는 기법으로 이를 이용하면 다중 불류를 이진 분류처럼 취급할 수 있다.
- word2vec으로 얻은 단어의 분산 표혀냐에는 단어의 의미가 녹아들어 있으며 비슷한 맬락에서 사용되는 단어는 벡터 공간에서 가까이 위치한다.
- word2vec의 단어의 분산표현을 이용하면 유추 문제를 벡터의 덧셈과 뺄셈으로 풀수 있다.
728x90
'Data Science > Deep Learning' 카테고리의 다른 글
[Deep Learning from Scratch 2] chapter 6.1. 게이트가 추가된 RNN (0) | 2023.04.26 |
---|---|
[Deep Learning from Scratch 2] chapter 5. 순환신경망(RNN) (0) | 2023.04.26 |
[Deep Learning from Scratch 2] chapter 3. word2vec (0) | 2023.04.26 |
[Deep Learning from Scratch 2] chapter 2. 자연어와 단어의 분산 표현 (0) | 2023.04.26 |
[Deep Learning from Scratch] chapter 8-4. 딥러닝 NIC, DCGAN (0) | 2023.04.25 |