딥러닝독학! (5) simple word2vec- 밑바닥부터시작하는딥러닝 2

 

 

앞선 게시물까지는 밑바닥부터 시작하는 딥러닝 1권이었다. 

필요한 부분만 빨리 훑었고, 이제는 자연어처리 기반인 밑바닥부터시작하는 딥러닝 2권으로 시작한다.

Word2vec부터 어텐션장까지 이어서 할 것이다.


 

1.  추론 기반 기법과 신경망

단어를 벡터로 표현하는 방법 중 성공적인 두 가지는 '통계 기반 기법''추론 기반 기법'이 있다.

 

1.1 통계 기반 기법의 문제점

통계기반 기법에서는 주변 단어의 빈도를 기초로 단어를 표현했다. 

(단어의 동시발생행렬을 만들고 SVD를 적용하여 밀집벡터를 얻음)

 

현업에서 다루는 말뭉치의 어휘 수는 어마어마하다. 어휘가 100만개라면 100만*100만 행렬을 만들어야 하는데 현실적이지 않음.

 

통계기반 기법은 말뭉치 전체의 통계를 이용해 단 1회의 처리만에 단어의 분산 표현을 얻음.

추론기반 기법은 미니배치로 학습하는 것이 일반적이다. 신경망이 한번에 소량의 학습 샘플씩 반복해서 학습하며 가중치 개선.

 

1.2 추론 기반 기법 개요

추론이란 주변 단어가 주어졌을 때 무슨 단어가 들어가는지를 추측하는 작업

이런 추론문제를 반복해서 풀면서 단어의 출현 패턴을 학습한다.

이를 모델 관점에서 보면 모델은 맥락 정보를 입력받아 (출현할 수 있는) 각 단어의 출현 확률을 출력한다.

1.3 신경망에서의 단어 처리

신경망에서는 'you'와 'say'등의 단어를 있는 그대로 처리할 수 없으니 고정 길이의 벡터로 변환한다. 

대표적인 방법으로는 원핫표현(원핫벡터)으로 변환하는 것이 있다. 

원핫 표현
벡터의 원소 중 하나만 1이고 나머지는 0인 벡터

총 어휘 수만큼의 원소를 갖는 벡터 준비
인덱스가 단어 ID와 같은 원소를 1로 나머지는 0으로 설정

위 그림에선 입력층의 뉴런은 총 7개. 7개의 뉴런은 각각 7개의 단어들에 대응한다.

이제 단어를 신경망으로 처리할 수 있다. 아래는 원핫표현으로 된 단어 하나를 완전계층연결을 통해 변환하는 모습이다.

여기서는 편향을 생략한다. 단순화하면 아래 그림과 같다.

c = np.array([[1,0,0,0,0,0,0]])
W = np.random.randn(7,3)
#편향이 없는 완전연결계층은 행렬곱 계산에 해당한다.
h = np.matmul(c,W)

c는 원핫표현이므로 행렬곱을 하면 c에서 1인부분의 인덱스에 위치한 가중치의 행벡터 하나를 뽑아낸 것과 같다.

행벡터를 표현할 뿐인데 행렬곱연산을 하는것은 비효율적일 수 있다. 후에 개선할 것이다.

 

2. 단순한 word2vec

이때까지 신경망으로 단어를 처리하는 방법을 보았다면, 이제는 '모델'을 신경망으로 구축하는 것이다.

이번 절에서 사용할 신경망은 word2vec에서 제안하는 CBOW모델이다.

 

2.1 CBOW 모델의 추론 처리

CBOW모델은 맥락으로부터 타깃을 추측하는 용도의 신경망이다.

타깃 중앙단어
맥락 주변단어

CBOW 모델의 입력은 맥락이다. 먼저, 이 맥락을 원핫표현으로 변환하여 모델이 처리할 수 있도록 준비한다. 

입력층이 2개인 이유는 맥락으로 고려할 단어를 2개로 정했기 때문
즉, 맥락에 포함시킬 단어가 N개라면 입력층도 N개가 된다.

은닉층의 뉴런은 입력층의 완전연결계층에 의해 변환된 값이 된다. 입력층이 여러개라면 전체를 평균한다.

출력층의 뉴런은 총 7개 인데, 각 뉴런은 각 단어에 대응한다. 

출력층 뉴런은 각 단어에 대한 '점수'를 뜻하며 값이 높을수록 출현 확률도 높아진다.

(점수에 소프트맥스 함수를 적용해서 확률을 얻음)

 

가중치 $\mathbf{W_in}$은 7*3 행렬이며, 단어의 분산 표현이다. 

학습을 진행할수록 맥락에서 출현하는 단어를 잘 추측하는 방향으로 갱신된다.

 

여기서 핵심은 은닉층의 뉴런 수를 입력층의 뉴런 수보다 적게 하는 것!
그래야 단어 예측에 필요한 정보를 간결하게 담게 된다. 

 

이제 CBOW모델을 계층관점에서 보자.

앞 단의 두 MatMul계층이 더해짐
→더해진 값에 0.5 곱함 (평균이 됨)
→이 평균이 은닉층 뉴런임
→은닉층 뉴런에 또 다른 MatMul계층이 적용됨
→점수 출력

 

CBOW모델의 추론 처리코드 (추론 처리란 '점수'를 구하는 처리)

import numpy as np

# 샘플 맥락 데이터
c0 = np.array([[1, 0, 0, 0, 0, 0, 0]])
c1 = np.array([[0, 0, 1, 0, 0, 0, 0]])

# 가중치 초기화
W_in = np.random.randn(7, 3)
W_out = np.random.randn(3, 7)

# 계층 생성
in_layer0 = MatMul(W_in)
in_layer1 = MatMul(W_in)
out_layer = MatMul(W_out)

# 순전파
h0 = in_layer0.forward(c0)
h1 = in_layer1.forward(c1)
h = 0.5 * (h0 + h1)
s = out_layer.forward(h)
print(s)
입력층을 처리하는 MatMul계층을 맥락 수(여기선 2)만큼 생성 (이들은 가중치 W_in을 공유)
→출력층 측의 MatMul계층은 1개만 생성.
→입력층 측의 MatMul계층들의 forward()메서드를 호출해 중간 데이터 계산
→출력층 측의 MatMul계층을 통과시켜 각 단어의 점수를 구함

 

2.2 CBOW 모델의 학습

점수를 출력했으니 여기에 소프트맥스 함수를 적용하면 '확률'을 얻을 수 있음. 

확률맥락(전후단어)이 주어졌을 때 그 중앙에 어떤 단어가 출현하는지를 나타냄

 

위에서 맥락은 'you'와 'goodbye'이고 정답레이블(신경망이 예측해야 할 단어)은 'say'이다.

결과적으로는 가중치 $\mathbf{W_in}$에 단어의 출현 패턴을 파악한 벡터가 학습된다.

CBOW모델은 단어 출현패턴을 학습 시 사용한 말뭉치로부터 배운다.
따라서 말뭉치가 다르면 학습 후 얻게되는 단어의 분산 표현도 달라진다.

 

우리가 다루고 있는 모델은 다중 클래스 분류를 수행하는 신경망이므로 소프트맥스와 크로스 엔트로피 오차만 이용하면 된다.

소프트맥스 함수로는 점수를 확률로 변환하고, 그 확률과 정답 레이블로부터 크로스 엔트로피 오차를 구한 후, 그 값은 손실로 사용해 학습을 진행한다.

Softmax계층과 Cross Entropy Error 계층을 Softmax with Loss라는 하나의 계층으로 구현 할 것.

2.3 word2vec의 가중치와 분산 표현

word2vec에서 사용되는 신경망에는 두가지 가중치가 있다.

$^{(1)}$입력 측 완전연결계층의 가중치 $\mathbf{W_in}$ (각 행은 각 단어의 분산 표현)

$^{(2)}$출력 측 완전연결계층의 가중치 $\mathbf{W_out}$ (단어의 의미가 인코딩된 벡터 저장)

 

즉, 각 단어의 분산 표현은 입력측과 출력측 모두의 가중치에서 확인할 수 있다.

그러면, 최종적으로 이용하는 단어의 분산표현으로는 어느쪽 가중치를 선택하면 좋을까?

A. 입력 측의 가중치만 이용
B. 출력 측의 가중치만 이용
C. 양쪽 가중치를 모두 이용

word2vec에서는 A안이 가장 대중적인 선택임.

 

3. 학습 데이터 준비

간단한 예로 "You say goodbye and I say hello"의 한문장짜리 말뭉치를 이용

 

3.1 맥락과 타깃

word2vec에서 이용하는 신경망의 입력은 '맥락' 

그 정답레이블은 맥락에 둘러싸인 중앙의 단어 '타깃'

따라서, 맥락을 입력했을 때 타깃이 출현할 확률을 높인다.

이때, 각 샘플 데이터에서 맥락의 수는 여러개가 될 수 있지만 타깃은 하나이므로 복수형임은 꼭 명시하자.

 

이제 말뭉치로부터 맥락과 타깃을 만드는 함수를 구현한다.

먼저 말뭉치 텍스트를 단어 ID로 변환한다.

 

def preprocess(text):
    text = text.lower() #소문자로
    text = text.replace('.', ' .')  #특수문자 분리?
    words = text.split(' ') #띄어쓰기로 끊기

	#단어 id 변환
    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
text = 'You say goodbye and I say hello'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)
print(id_to_word)

이제는 단어 ID의 배열인 corpus로부터 맥락과 타깃을 만든다.

def create_contexts_target(corpus, window_size=1):
    target = corpus[window_size:-window_size]
    contexts = []

    for idx in range(window_size, len(corpus)-window_size):
        cs = []
        for t in range(-window_size, window_size + 1):
            if t == 0:
                continue
            cs.append(corpus[idx + t])
        contexts.append(cs)

    return np.array(contexts), np.array(target)
contexts, target = create_contexts_target(corpus, window_size= 1)
print(contexts)
print(target)

이때, 인수는 두개이며 하나는 단어ID의 배열(corpus), 다른 하나는 맥락의 윈도우 크기(window_size)

이제 맥락과 타깃을 만들어냈으니 CBOW모델에 넘겨주면 된다. 

아직 맥락과 타깃의 각 원소는 단어 ID이기 때문에 원핫표현으로 변환한다.


3.2 원핫 표현으로 변환

단어 ID를 이용했을 때 맥락의 형상은 (6,2)인데 원핫표현으로 변환하면 (6,2,7)이 된다.

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

이때까지의 데이터 준비 과정은 다음과 같다.

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)

이제 학습 데이터 준비를 끝냈다.

 

4. CBOW 모델 구현

SimpleCBOW라는 이름으로 구현할 것이다. 초기화 메서드부터 시작하자!

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')
        W_out = 0.01 * np.random.randn(H, V).astype('f')

        # 계층 생성
        self.in_layer0 = MatMul(W_in)
        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
인수로는 어휘 수(vocab_size)와 은닉층의 뉴런 수(hidden_size)를 받는다.
가중치는 2개(W_in, W_out)를 생성하고 무작위 값으로 초기화한다.
필요한 계층(입력측2개, 출력측 1개)을 생성한다.
Softmax with Loss 계층을 생성한다. 
입력 측의 맥락을 처리하는 MatMul계층은 맥락에서 사용하는 단어의 수만큼 만들고 이들은 모두 같은 가중치를 이용.
신경망에서 사용되는 매개변수와 기울기를 params, grads리스트에 각각 모아둔다.

신경망의 순전파인 forward()메서드를 구현한다. 인수로 맥락과 타깃을 받아 손실을 반환한다.

    def forward(self, contexts, target):
        h0 = self.in_layer0.forward(contexts[:, 0])
        h1 = self.in_layer1.forward(contexts[:, 1])
        h = (h0 + h1) * 0.5
        score = self.out_layer.forward(h)
        loss = self.loss_layer.forward(score, target)
        return loss
contexts는 위 예에서 (6,2,7)이 되는데
0번째 차원의 원소 수는 미니배치의 수,
1번째 차원의 원소 수는 맥락 윈도우 크기
2번째 차원은 원 핫 벡터
target은 위 예에서 (6,7)과 같은 형상이 된다.

역전파인 backward()를 구현한다.

신경망의 역전파는 기울기를 순전파 때와는 반대로 전파한다.

1에서 시작하여 Softmax with Loss계층에 입력된다. 출력은 ds이며, 이를 출력 측 MatMul계층으로 입력.

그 다음 *, + 연산으로 역전파 된다. 

*의 역전파는 순전파 시의 입력을 서로 바꿔 기울기에 곱하고 + 역전파는 기울기를 그대로 통과시킨다.

    def backward(self, dout=1):
        ds = self.loss_layer.backward(dout)
        da = self.out_layer.backward(ds)
        da *= 0.5
        self.in_layer1.backward(da)
        self.in_layer0.backward(da)
        return None

forward()메서드를 호출한 다음 backward()메서드를 실행하는 것만으로 grads리스트의 기울기가 갱신된다.

 

4.1 학습 코드 구현

import matplotlib.pyplot as plt

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

word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
    print(word, word_vecs[word_id])

학습을 거듭할수록 손실이 줄어든다. 학습이 끝난 후 가중치 매개변수는?

word_vecs의 각 행에는 대응하는 단어 ID의 분산표현이 저장되어 있다. 

word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
  print(word, word_vecs[word_id])

단어를 밀집벡터로 표현할 수 있게 됨!

 

아쉬운 점은 작은 말뭉치로는 좋은 결과를 얻을 수 없음. 

현시점의 모델은 처리 효율면에서 몇가지 문제가 있다.

 

5. 정리

추론 기반 기법은 추측하는 것이 목적! 

word2vec은 추론기반기법, 단순한 2층 신경망

CBOW모델은 여러단어로부터 하나의 단어를 추측한다.