학습 데이터 준비
맥락: 각 행이 신경망의 입력으로 쓰임.
타겟: 각 행이 예측해야 하는 단어
맥락수는 여러개지만 타겟은 오직 하나, 그래서 맥락을 영어로 쓸 때는 's' 를 붙여 복수형임을 명시하는 것이 좋다.
말뭉치로부터 맥락과 타겟을 만드는 함수 구현
1.말뭉치 텍스트 ID 변환
2장에서 구현한 함수 사용
#util.py
import numpy as np
def preprocess(text):
text = text.lower()
text = text.replace('.', ' .')
words = text.split(' ')
word_to_id = {}
id_to_word = {}
for word in words:
if word not in word_to_id:
new_id = len(word_to_id)
word_to_id[word] = new_id
id_to_word[new_id] = word
corpus = np.array([word_to_id[w] for w in words])
return corpus, word_to_id, id_to_word
import sys
sys.path.append('...')
from util import preprocess
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)
print(id_to_word)
[0 1 2 3 4 1 5 6]
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
2. 맥락과 타깃작성
맥락은 2차원 배열로 contexts[0]에는 0번째 맥락, contexts[1]에는1번째 맥락이 저장된다.
타깃도 target[0]에는 0번째 타겟, target[1]에는 1번째 타겟이 저장된다.
def create_contexts_target(corpus, window_size=1):
# target 계산
target = corpus[window_size:-window_size] #중심단어가 되는 부분
contexts = [] #맥락
# contexts 계산
for idx in range(window_size, len(corpus)-window_size): #target 단어들 인덱스 반복
cs=[]
for t in range(-window_size, window_size + 1): #타겟단어의 주변단어 반복
if t == 0: #0인 경우 포함시키지 않음, target 단어 자체는 context에 포함시키지 않음
continue
cs.append(corpus[idx + t]) #context 단어 추가시킴
contexts.append(cs) #각 target 단어에 대한 context 단어들의 리스트
return np.array(contexts), np.array(target)
contexts, target = create_contexts_target(corpus)
print(contexts)
print(target)
[[0 2]
[1 3]
[2 4]
[3 1]
[4 5]
[1 6]]
[1 2 3 4 1 5]
3. 원핫 표현으로 변환
단어 ID를 이용했을 때 맥락의 형상은 (6,2) 인데 이를 원핫으로 변환하면 (6,2,7) 이 된다.
util.py의 convert_one_hot 함수를 가져왔다.
def convert_one_hot(corpus, vocab_size):
N = corpus.shape[0]
if corpus.ndim == 1:
one_hot = np.zeros((N, vocab_size), dtype=np.int32)
for idx, word_id in enumerate(corpus):
one_hot[idx, word_id] = 1
elif corpus.ndim == 2:
C = corpus.shape[1]
one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
for idx_0, word_ids in enumerate(corpus):
for idx_1, word_id in enumerate(word_ids):
one_hot[idx_0, idx_1, word_id] = 1
return one_hot
import sys
sys.path.append('..')
from util import preprocess,create_contexts_target,convert_one_hot
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
contexts, target = create_contexts_target(corpus, window_size=1)
vocab_size = len(word_to_id)
target = convert_one_hot(target, vocab_size)
contexts = convert_one_hot(contexts, vocab_size)
CBOW 모델 구현
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss
class SimpleCBOW:
def __init__(self, vocab_size, hidden_size):
V, H = vocab_size, hidden_size
# 가중치 초기화-작은 무작위값 초기화
W_in = 0.01 * np.random.randn(V, H).astype('f') #32비트 부동소수점수 초기화
W_out = 0.01 * np.random.randn(H, V).astype('f')
# 계층 생성
self.in_layer0 = MatMul(W_in) #matmul 계층은 단어의수(맥락수) 만큼 만들어야함
self.in_layer1 = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer = SoftmaxWithLoss()
# 모든 가중치와 기울기를 리스트에 모은다.
layers = [self.in_layer0, self.in_layer1, self.out_layer] #모두 같은 가중치를 쓰도록 초기화
self.params, self.grads = [], []
for layer in layers:
self.params += layer.params
self.grads += layer.grads
# 인스턴스 변수에 단어의 분산 표현을 저장한다.
self.word_vecs = W_in
이 코드는 여러계층에서 같은 가중치를 공유하고 있음.
forward 구현 -> 맥락과 타깃을 받아 손실 반환
def forward(self, contexts, target):
h0 = self.in_layer0.forward(contexts[:, 0]) #0번째 맥락
h1 = self.in_layer1.forward(contexts[:, 1]) #1번째 맥락
h = (h0 + h1) * 0.5
score = self.out_layer.forward(h)
loss = self.loss_layer.forward(score, target)
return loss
이 배열의 형상은 (6,2,7) 이다. 0번째 차원의 원소수는 미니배치의 수, 1번째 차원의 원소수는 맥락의 윈도우 크기, 2번째 차원은 원핫벡터, target의 형상은 2차원으로 (6,7)과 같은 형상이 된다.
backward 구현 -> 순전파때와는 반대로 전파
def backward(self, dout=1):
ds = self.loss_layer.backward(dout)
da = self.out_layer.backward(ds)
da *= 0.5 #두개의 맥락에 대한 평균 1/2(h1+h2)
self.in_layer1.backward(da)
self.in_layer0.backward(da)
return None
학습 코드 구현
import sys
sys.path.append('..')
from trainer import Trainer
from optimizer import Adam
from simple_cbow import SimpleCBOW
from util import preprocess, create_contexts_target, convert_one_hot
window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text) #전처리
vocab_size = len(word_to_id)
contexts, target = create_contexts_target(corpus, window_size)
target = convert_one_hot(target, vocab_size) #원핫벡터 변환
contexts = convert_one_hot(contexts, vocab_size)
model = SimpleCBOW(vocab_size, hidden_size)
optimizer = Adam() #최적화
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()
학습이 계속될수록 loss가 줄어드는 것을 알 수 있다. 학습이 끝난 후의 가중치 매개변수를 살펴보면,
word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
print(word, word_vecs[word_id])
you [-1.0189422 -0.91166073 1.6480061 -0.71211404 -0.8358288 ]
say [-0.01946388 1.1766013 0.35874593 1.1610314 1.1758682 ]
goodbye [-1.0458578 -0.9180885 -0.08102933 -1.1267978 -1.0944645 ]
and [1.5580194 0.96159405 1.5290062 0.95012426 0.9422874 ]
i [-1.0138284 -0.90713125 -0.06940629 -1.1075021 -1.0471977 ]
hello [-1.0080683 -0.9011932 1.6552026 -0.7422806 -0.8005005]
. [-1.4922124 1.0471749 -1.362185 1.057018 1.0624955]
단어를 밀집 벡터로 표현할 수 있게 되었다. 이는 단어의 분산표현이다.
CBOW 모델과 확률
사후 확률 : P(A|B) B라는 정보가 주어졌을 때 A가 일어날 확률
CBOW 모델에서는 맥락이 주어졌을 때 타겟단어가 출현할 확률을 출력하는 것이다.
P(Wt |Wt-1,W+1)
Wt-1과 W+1이 일어난 후 Wt가 일어날 확률, Wt-1과 W+1이 주어졌을 때 Wt가 일어날 확률 을 의미함.
CBOW의 손실함수
cross entropy error를 적용, yk는 k번째 해당하는 사건이 일어날 확률, tk는 정답레이블이며 원핫벡터로 표현
wt에 해당하는 원소만 1(wt가 발생)
음의 로그 가능도 : L = -log P(Wt |Wt-1,W+1)
샘플 데이터 하나에 대한 손실함수를 의미한다.
-말뭉치 전체로 확장시
skip-gram 모델
CBOW 모델 : you ___ goodbye and I say hello. (맥락으로 붙터 중앙의 단어 추측)
skip-gram 모델: ___ say _____ and I say hello. (중앙의 타겟으로부터 주변 맥락 추측)
skip gram 모델의 입력층은 하나이고, 출력층은 맥락의 수만큼 존재한다.
개별적으로 손실을 구하고, 개별 손실을 모두 더한값을 최종 손실로한다.
P( Wt-1,W+1 | Wt ) : Wt가 주어졌을 때 Wt-1와 W+1가 동시에 일어날 확률
맥락의 단어들 사이에 관련성이 없다고 가정하고 다음과 같이 분해함
P( Wt-1,W+1 | Wt ) = P( Wt-1| Wt ) P( Wt+1| Wt )
모델의 손실함수
logxy = logx+logy의 성질을 이용하였다. skip-gram 모델 손실함수는 맥락별 손실을 구한 다음 모두 더한다.
-말뭉치 전체 확장시
skip-gram 코드 구현
출처: https://github.com/sunny1c/deep-learning-from-scratch-2/blob/master/ch03/simple_skip_gram.py
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss
class SimpleSkipGram:
def __init__(self, vocab_size, hidden_size):
V, H = vocab_size, hidden_size
# 가중치 초기화
W_in = 0.01 * np.random.randn(V, H).astype('f')
W_out = 0.01 * np.random.randn(H, V).astype('f')
# 계층 생성
self.in_layer = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer1 = SoftmaxWithLoss()
self.loss_layer2 = SoftmaxWithLoss()
# 모든 가중치와 기울기를 리스트에 모은다.
layers = [self.in_layer, self.out_layer]
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 = self.in_layer.forward(target)
s = self.out_layer.forward(h)
l1 = self.loss_layer1.forward(s, contexts[:, 0])
l2 = self.loss_layer2.forward(s, contexts[:, 1])
loss = l1 + l2
return loss
def backward(self, dout=1):
dl1 = self.loss_layer1.backward(dout)
dl2 = self.loss_layer2.backward(dout)
ds = dl1 + dl2
dh = self.out_layer.backward(ds)
self.in_layer.backward(dh)
return None
말뭉치가 커질수록 skip-gram 모델이 더 뛰어난 경향이 있고, 학습 속도면에서는 CBOW 모델이 더 빠르다.
skip-gram모델은 손실을 맥락의 수만큼 구해야 하기 때문이다.
'Deep Learning > from scratch II' 카테고리의 다른 글
[밑바닥부터 시작하는 딥러닝 2] - 4. word2vec 속도 개선 II (2) | 2024.02.16 |
---|---|
[밑바닥부터 시작하는 딥러닝 2]- 4. word2vec 속도 개선 I (1) | 2024.02.15 |
[밑바닥부터 시작하는 딥러닝 2] - 3장 word2vec I (0) | 2024.02.14 |
[밑바닥부터 시작하는 딥러닝 2] - 자연어와 단어의 분산 표현 II (1) | 2024.02.13 |
[밑바닥부터 시작하는 딥러닝 2] - 2. 자연어와 단어의 분산 표현 I (1) | 2024.02.13 |