Deep Learning/from scratch II

[밑바닥부터 시작하는 딥러닝 2] - 3장 word2vec II

해파리냉채무침 2024. 2. 15. 02:09

학습 데이터 준비

https://yerimoh.github.io/DL14/

맥락: 각 행이 신경망의 입력으로 쓰임.

타겟: 각 행이 예측해야 하는 단어 

맥락수는 여러개지만 타겟은 오직 하나, 그래서 맥락을 영어로 쓸 때는 '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. 맥락과 타깃작성

https://yerimoh.github.io/DL14/

맥락은 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. 원핫 표현으로 변환

https://yerimoh.github.io/DL14/

단어 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

학습 코드 구현

https://github.com/deeplearningfromscratch2/deep-learning-from-scratch-2/blob/master/Ch3_word2vec/ch3_word2vec.ipynb

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()

https://yerimoh.github.io/DL14/

학습이 계속될수록 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 모델에서는 맥락이 주어졌을 때 타겟단어가 출현할 확률을 출력하는 것이다.

https://yerimoh.github.io/DL14/

 

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)

샘플 데이터 하나에 대한 손실함수를 의미한다.

-말뭉치 전체로 확장시

https://yerimoh.github.io/DL14/

skip-gram 모델

CBOW 모델 : you ___ goodbye and I say hello. (맥락으로 붙터 중앙의 단어 추측)

skip-gram 모델:  ___ say _____ and I say hello. (중앙의 타겟으로부터 주변 맥락 추측)

https://yerimoh.github.io/DL14/

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 )

 

모델의 손실함수

 

https://yerimoh.github.io/DL14/

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모델은 손실을 맥락의 수만큼 구해야 하기 때문이다.