본문 바로가기

Deep Learning(강의 및 책)/밑바닥부터 시작하는 딥러닝 2

3. word2vec

728x90

밑바닥부터 시작하는 딥러닝 2를 바탕으로 제작하였습니다.

 

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

 ■ 통계 기반 기법의 문제점

통계 기반 기법에서는 주변 단어의 빈도를 기초로 단어를 표현했습니다. 구체적으로는 단어의 동시발생 행렬을 만들고 그 행렬에 SVD를 적용하여 밀집 벡터(단어의 분산 표현)를 얻었습니다. 추론 기반 기법에서는 일반적으로 미니배치로 학습하는데 미니배치 학습은 소량의 학습 샘플씩 반복해서 학습하며  가중치를 갱신시킵니다.

 

 

이와 같은 그림으로 차이를 볼 수 있습니다. 통계 기반 기법은 학습 데이터를 한꺼번에 처리합니다. 반대로 추론 기반 기법은 학습 데이터의 일부를 사용하여 순차적으로 학습합니다(미니배치 학습). 이 말은 말뭉치의 어휘 수가 많아 SVD 등 계산량이 큰 작업을 처리하기 어려운 경우에도 신경망을 학습시킬 수 있다는 뜻입니다.

 

 ■ 추론 기반 기법 개요

 

추론 기반 기법은 말 주변 단어(맥락)가 주어졌을 때 "?"에 무슨 단어가 들어가는지를 추측하는 작업입니다. 이러한 추론 문제를 반복해서 풀면서 단어의 출현 패턴을 학습합니다.

 

 

추론 기반 기법 모델은 위 그림과 같이 포현됩니다. 모델은 맥락 정보를 입력받아 각 단어의 출현 확률을 출력합니다. 이러한 틀 안에서 말뭉치를 사용해 모델이 올바른 추측을 하도록 학습시킵니다.

 

 ■ 신경망에서의 단어 처리

신경망은 'you'와 'say' 등 단어를 있는 그대로 처리할 수 없으니 고정 길이의 벡터로 변환해야 합니다. 이때 사용하는 대표적인 방법이 단어를 원핫 표현(또는 원핫 벡터)으로 변환하는 것입니다. 원핫 표현은 벡터의 원소 중 하나만 1이고 나머지는 모두 0인 벡터를 말합니다. 예를 들어

이와 같이 표현 할 수 있습니다. 총 어휘 수만큼의 원소를 갖는 벡터를 준비하고, 인덱스가 단어 ID와 같은 원소를 1, 나머지는 모두 0으로 설정합니다.

이를 통해 단어를 벡터로 표현할 수 있고, 신경망을 구성하는 '계층'들은 벡터를 처리할 수 있습니다. 즉 단어를 신경망으로 처리할 수 있게 되었습니다.

 

 

신경망은 완전연결계층이므로 각각의 노드가 이웃 층의 모든 노드와 화살표로 연결되어 있습니다. 이 화살표에는 가중치(매개변수)가 존재하여, 입력층 뉴런과의 가중합이 은닉층 뉴런이 됩니다. 이와 같은 완전연결계층에 의한 변환을 구현해보면

 

import numpy as np

c = np.array([1, 0, 0, 0, 0, 0, 0])
W = np.random.randn(7, 3)
h = np.matmul(c, W)

 

이와 같이 구현할 수 있습니다. c는 입력층이 되고 W는 가중치가 되며 h는 은닉층이 됩니다.

 

 

위 연산을 그림으로 표현하면 이와 같습니다. 결국 가중치의 행벡터를 뽑아내는 연산을 할 뿐인데 행렬 곱을 계산하는 건 비효율적입니다. 이 부분은 나중에 수정하겠습니다.

 

3.2. 단순한 word2vec

이제 모델을 신경망으로 구축을 할 건데 사용할 신경망은 CBOW(Continuous Bag-Of-Words) 모델입니다.

 

 ■ CBOW 모델의 추론 처리

CBOW 모델은 맥락으로부터 타깃(target)을 추측하는 용도의 신경망입니다(타깃은 중앙 단어이고 그 주변 단어들이 맥락입니다). CBOW 모델이 가능한 한 정확하게 추론하도록 훈련시켜서 단어의 분산 표현을 얻어냅니다.

CBOW 모델의 입력은 맥락입니다. 맥락은 단어들의 목록이 됩니다. 그러므로 맥락을 먼저 원핫 벡터로 변환하여 CBOW 모델이 처리할 수 있도록 전처리를 해줍니다.

 

 

그림과 같이 CBOW 모델 신경망을 그릴 수 있습니다. 입력층이 2개, 은닉층을 거쳐 출력층에 도달합니다. 은닉층의 뉴런은 입력층의 완전연결계층에 의해 변환된 값이 되는데, 입력층이 여러 개이면 전체를 '평균'하면 됩니다. 예를 들어 첫 번째 입력층이 h1으로 변환되고 두 번째 입력층이 h2로 변한 된다고 했을 때, 은닉층 뉴런은 (h1 + h2)/2가 됩니다. 마지막 출력층을 보면 뉴런은 총 7개인데, 여기서 중요한 것은 이 뉴런 하나하나가 각각의 단어에 대응한다는 점입니다. 그리고 출력층 뉴런은 각 단어의 점수를 뜻하며, 값이 높을수록 대응 단어의 출현 확률도 높아집니다.

 

 

이번에는 CBOW 모델을 '뉴런 관점'이 아닌 '계층 관점'에서 그렸습니다. 두 계층의 출력이 더해지고 0.5를 곱해 평균을 구하고 이 값이 은닉층의 뉴런이 됩니다. 마지막으로 은닉층 뉴런에 또 다른 MatMul 계층이 적용되어 점수가 출력됩니다. 이러한 CBOW 모델의 출력 처리를 구현해보겠습니다.

 

# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul
from common.util import preprocess
from common.util import create_contexts_target

# 샘플 맥락 데이터
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 계층을 맥락 수만큼 생성하고, 출력층의 MatMul 계층은 1개만 생성합니다. forward() 메서드를 호출해 중간 데이터를 계산하고 출력층 측의 MatMul 계층을 통과시켜 각 단어의 점수를 구합니다.

 

 ■ CBOW 모델의 학습

신경망 학습을 위해 softmax, cross entropy를 사용하겠습니다. softmax를 통해 확률을 구하고 그 확률과 정답 레이블로부터 오차를 구한 후, 그 값을 loss로 사용해 학습을 진행합니다. 그림으로 그리면

 

 

이와 같습니다. softmax와 loss를 한 층으로 합쳐서 표현했습니다.

 

 ■ word2vec의 가중치와 분산 표현

지금까지 본 word2vec에서 사용되는 신경망은 두 가지의 가중치가 있습니다. 입력 측 완전연결계층의 가중치와 출력 측 완전연결계층의 가중치가 존재합니다. 또한 출력 측 가중치에도 단어의 의미가 인코딩된 벡터가 저장되고 있다고 생각할 수 있습니다. 다만, 출력 측 가중치는 각 단어의 분산 표현이 열 방향으로 저장됩니다. 

 

 

이와 같이 표현됩니다.

 

3.3. 학습 데이터 준비

 ■ 맥락과 타깃

 

이와 같이 말뭉치로부터 목표로 하는 단어를 타깃으로, 그 주변 단어를 맥락으로 뽑아냈습니다. 이 작업을 말뭉치 안의 모든 단어에 대해 수행합니다. 이렇게 만들어진 것이 신경망의 입력으로 쓰이고, 타깃이 각 행의 정답 레이블이 됩니다. 이렇게 맥락과 타깃을 만드는 코드를 구현해보겠습니다.

 

def create_contexts_target(corpus, window_size=1):
    '''맥락과 타깃 생성

    :param corpus: 말뭉치(단어 ID 목록)
    :param window_size: 윈도우 크기(윈도우 크기가 1이면 타깃 단어 좌우 한 단어씩이 맥락에 포함)
    :return:
    '''
    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)

 

이와 같이 구현할 수 있습니다.  함수는 두 개의 인수를 받는데 하나는 ID배열, 나머지 하나는 맥락의 윈도우 크기입니다. 맥락과 타깃을 각각 넘파이 다차원 배열로 돌려줍니다.

 

 ■ 원핫 표현으로 변환

 

맥락과 타깃을 원핫 표현으로 변환합니다. 이때 맥락의 형상은 (6, 2)였지만 원핫 벡터로 변환하면 형상은 (6, 2, 7)이 됩니다.

 

3.4. CBOW 모델 구현

 

이와 같은 그림처럼 모델을 구현해보겠습니다.

 

# coding: utf-8
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')
        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

    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

    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

 

이와 같은 코드로 구현할 수 있습니다. SimpleCBOW라는 이름을 가진 class로 구현했습니다. 처음에 가중치들을 크기에 맞게 초기화를 한 후 각 계층을 생성해줍니다. 모든 가중치와 기울기를 리스트 형태로 저장을 한 후 변수에 단어의 분산을 저장합니다. forward() 메서드를 보면 은닉층으로 값이 전달되고 SoftmaxWithLoss 함수를 통해 점수를 구하고 오차를 구합니다. backward() 메서드는 구한 오차를 통해 기울기가 갱신됩니다. 이렇게 생성한 모델의 backward()를 그림으로 그리면

 

 

이와 같이 표현됩니다.