해당 포스팅은 '밑바닥부터 시작하는 딥러닝2'를 공부하고 정리, 요악한 글입니다. 모든 내용은 해당 도서를 기준으로 합니다.
◼️ 3.1 추론 기반 기법과 신경망
단어를 벡터로 표현하는 방법은 통계 기반 기법과 추론 기반 기법이 있다.
◾ 3.1.1 통계 기반 기법의 문제점
통계 기반 기법에서는 주변 단어의 빈도를 기초로 단어를 표현해왔으며 단어의 동시발생 행렬을 만들고 그 행렬에 SVD를 적용하여 밀집벡터를 얻었다.
하지만 이 방식은 대규모 말뭉치를 다룰 때 문제가 발생한다.
어휘가 100만 개라면, 통계 기반 기법에서는 '100만x100만'이라는 거대한 행렬을 만드는데 이러한 행렬에 SVD를 적용하는 일은 현실적이지 않다.
통계 기반 기법은 단 1회의 처리(SVD 등)만에 단어의 분산 표현을 얻는다.
추론 기반 기법에서는 신경망을 이용하는 경우 미니배치로 학습하는 것이 일반적이다. 미니배치 학습에서는 신경망이 한번에 소량의 학습 샘플씩 반복해서 학습하며 가중치를 갱신해간다.
위 그림처럼 통계 기반 기법은 학습 데이터를 한꺼번에 처리하지만 추론 기반 기법은 학습 데이터의 일부를 사용하여 순차적으로 학습한다.
◾ 3.1.2 추론 기반 기법 개요
추론이란 아래 그림처럼 주변 단어가 주어졌을 때 "?"에 어떤 단어가 들어가는지를 추측하는 작업이다.
위 그림처럼 추론 문제를 풀고 학습하는 것이 추론 기반 기법이 다루는 문제이다. 이러한 추론 문제를 반복해서 풀면서 단어의 출현 패턴을 학습하는 것이다.
위 그림처럼 추론 기반 기법에는 어떠한 모델이 등장하는데, 모델은 맥락 정보를 입력받아 각 단어의 출현 확률을 출력한다. 이러한 틀 안에서 말뭉치를 사용해 모델이 올바른 추측을 내놓도록 학습시킨다. 그 결과로 단어의 분산 표현을 얻는 것이 추론 기반 기법의 전체 그림이다.
◾ 3.1.3 신경망에서의 단어 처리
신경망은 'you'와 'say' 같은 단어를 있는 그대로 처리할 수 없으니 단어를 고정 길이의 벡터로 변환해야 한다. 이때 사용하는 대표적인 방법이 단어를 원핫(one-hot)표현으로 변환하는 것이다.
총 오휘 수만큼의 원소를 갖는 벡터를 준비하고 인덱스가 단어 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) # 은닉층 노드
print(h)
>> [[ 1.08641249 -0.82780485 -0.29980432]]
c는 원핫 표현이므로 단어 ID에 대응하는 원소만 1이고 그 외에는 0인 벡터이다. 위 코드의 c와 W의 행렬 곱은 결국 아래 그림처럼 가중치의 행벡터 하나를 뽑아낸 것과 같다.
앞선 코드로 수행한 작업은 MatMul 계층으로도 수행할 수 있다.
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul
c = np.array([[1, 0, 0, 0, 0, 0, 0]])
W = np.random.randn(7, 3)
layer = MatMul(W)
h = layer.forward(c)
print(h)
>> [[-0.38687194 0.82963359 0.87623966]]
이 코드는 common 디렉터리에 있는 MatMul 계층을 import 해서 사용한다.
◼️ 3.2 단순한 word2vec
◾ 3.2.1 CBOW 모델의 추론 처리
CBOW 모델은 맥락으로부터 타깃(target)을 추측하는 용도의 신경망이다. 타깃은 중앙 단어이고 그 주변 단어들이 '맥락'이다.
CBOW 모델의 입력은 맥락이다. 이 맥락을 원핫 표현으로 변환하여 CBOW 모델이 처리할 수 있도록 준비한다.
CBOW 모델의 신경망을 아래처럼 그릴 수 있다.
입력층이 2개 있고 은닉층을 거쳐 출력층에 도달한다. 두 입력층에서 은닉층으로의 변환은 똑같은 완전연결계층(가중치는 Win)이 처리한다.
은닉층의 뉴런은 입력층의 완전연결계츠엥 의해 변환된 값이 되는데, 입력층이 여러 개이면 전체를 '평균'하면 된다.
위 그림을 보면 출력층의 뉴런은 총 7개인데 이 뉴런 하나하나가 각각의 단어에 대응한다. 출력층은 뉴런은 각 단어의 '점수'를 뜻하며 값이 높을수록 대응 단어의 출현 확률도 높아진다. 여기서 점수란 확률로 해석되기 전의 값이고 이 점수에 소프트맥스 함수를 적용해서 '확률'을 얻을 수 있다.
이때 완전연결계층의 가중치 Win은 7x3 행렬이며 이 가중치가 바로 단어의 분산 표현의 정체이다. 이를 그림으로 표현하면 아래처럼 된다.
따라서 학습을 진행할수록 맥락에서 출현하는 단어를 잘 추측하는 방향으로 이 분산 표현들이 갱신될 것이다.
이렇게 해서 얻은 벡터에는 단어의 의미도 잘 녹아들어 있다. 이것이 word2vec의 전체 그림이다.
위 그림에서 알 수 있듯이 CBOW 모델의 가장 앞단에는 2개의 MatMul 계층이 있고 이어서 두 계층의 출력이 더해진다. 더해진 값에 0.5를 곱하면 평균이 되며 이 평균이 은닉층 뉴런이 된다.
은닉층 뉴런에 또 다른 MatMul 계층이 적용되어 점수가 출력된다.
# chap03/cbow_predict.py
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul
# 샘플 맥락 데이터
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) # average
s = out_layer.forward(h) # score
print(s)
◾ 3.2.2 CBOW 모델의 학습
CBOW 모델은 출력층에서 각 단어의 점수를 출력했고 이 점수에 소프트맥스 함수를 적용하면 확률을 얻을 수 있다.
이 확률은 맥락(전후 단어)이 주어졌을 때 그 중앙에 어떤 단어가 출현하는지를 나타낸다.
CBOW 모델의 학습에서는 올바른 예측을 할 수 있도록 가중치를 조정하는 일을 한다. 그 결과로 가중치 Win에 단어의 출현 패턴을 파악한 벡터가 학습된다.
◾ 3.2.3 word2vec의 가중치와 분산 표현
word2vec에서 사용되는 신경망에는 두 가지 가중치가 있다. 입력 측 완전연결계층의 가중치(Win)와 출력 측 완전연결계층의 가중치(Wout)이다.
입력 측 가중치 Win의 각 행이 각 단어의 분산 표현에 해당한다. 출력 측 가중치 Wout에도 단어의 의미가 인코딩된 벡터가 저장되고 있다고 생각할 수 있다. 출력 측 가중치는 아래 그림에서 보듯 각 단어의 분산 표현이 열 방향(수직 방향)으로 저장된다.
최종적으로 이용하는 단어의 분산 표현으로는 word2vec(특히 skip-gram 모델)에서는 입력 측의 가중치만 이용하는 표현이 대중적인 선택이다.
◼️ 3.3 학습 데이터 준비
◾ 3.3.1 맥락과 타깃
word2vec에서 이용하는 신경망의 입력은 '맥락'이다. 정답 레이블은 맥락에 둘러 싸인 중앙의 단어, 즉 '타깃'이다.
위 그림에서는 말뭉치로부터 목표로 하는 단어를 '타깃'으로 그 주변 단어를 '맥락'으로 뽑아냈다.
import sys
sys.path.append('..')
from common.util import preprocess
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)
>> [0 1 2 3 4 1 5 6]
print(id_to_word)
>> {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
# common/util.py
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:
cs.append(corpus[idx + t])
contexts.append(cs)
return np.array(contexts), np.array(target)
from common.util import create_contexts_target
contexts, target = create_contexts_target(corpus, window_size=1)
print(contexts)
>> [[0 2]
[1 3]
[2 4]
[3 1]
[4 5]
[1 6]]
print(target)
>> [1 2 3 4 1 5]
◾ 3.3.2 원핫 표현으로 변환
위 그림처럼 맥락과 타깃을 단어 ID에서 원핫 표현으로 변환하면 된다.
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, vocabsize), 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 common.util import preprocess, create_contexts_target, convert_one_hot
text = 'You say goodby 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)
print(target)
>> [[0 1 0 0 0 0 0]
[0 0 1 0 0 0 0]
[0 0 0 1 0 0 0]
[0 0 0 0 1 0 0]
[0 1 0 0 0 0 0]
[0 0 0 0 0 1 0]]
print(contexts)
>> [[[1 0 0 0 0 0 0]
[0 0 1 0 0 0 0]]
[[0 1 0 0 0 0 0]
[0 0 0 1 0 0 0]]
[[0 0 1 0 0 0 0]
[0 0 0 0 1 0 0]]
[[0 0 0 1 0 0 0]
[0 1 0 0 0 0 0]]
[[0 0 0 0 1 0 0]
[0 0 0 0 0 1 0]]
[[0 1 0 0 0 0 0]
[0 0 0 0 0 0 1]]]
◼️ 3.4 CBOW 모델 구현
# chap03/simple_cbow.py
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 += layers.params
self.grads += layer.grads
# 인스턴스 변수에 단어의 분산 표현을 저장한다.
self.word_vecs1 = W_in
self.word_vecs2 = W_out.T
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
◾ 3.4.1 학습 코드 구현
# chap03/train.py
import sys
sys.path.append('..')
from common.trainer import Trainer
from common.optimizer import Adam
from simple_cbow import SimpleCBOW
from common.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)
# cbow 학습 데이터셋 생성
contexts, target = create_contexts_target(corpus, window_size)
# Input에 맞는 one-hot 표현 변환
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)
word_vecs1 = model.word_vecs1
for word_id, word in id_to_word.items():
print(word, word_vecs1[word_id])
>> you [-1.3807591 -1.0351917 -1.1238494 -1.0471877 1.0710732]
say [-0.24706812 1.125094 1.1692127 1.155186 -1.1067501 ]
goodbye [-0.56422764 -0.8567286 -0.70119053 -0.8194563 0.9058622 ]
and [-1.9357495 0.7339323 0.7825899 0.7863268 -0.90669537]
i [-0.5696927 -0.82052916 -0.6930974 -0.81896836 0.91717815]
hello [-1.3897839 -1.0456055 -1.1249272 -1.0641869 1.0661185]
. [ 1.6761091 1.2073398 1.221956 1.2177749 -1.0616693]
이렇게 단어를 밀집벡터로 나타낼 수 있게 된다.
이 밀집벡터가 바로 단어의 분산 표현이다.
◼️ 3.5 word2vec 보충
CBOW 모델을 확률 관점에서 다시 살펴보면 다음과 같다.
◾ 3.5.1 CBOW 모델과 확률
동시확률이란 A와 B가 동시에 일어날 확률이며 P(A,B)로 쓴다.
사후 확률은 P(A|B)로 쓴다. 이는 말 그대로 사건이 일어난 후의 확률이다. B(라는 정보)가 주어졌을 때 A가 일어날 확률로 해석할 수도 있다.
CBOW 모델을 확률 표기법으로 기술하면 다음과 같다.
맥락으로 wt-1과 wt+1이 주어졌을 때 타깃이 wt가 될 확률을 수식으로 쓰면 다음 식 3.1과 같이 된다.
이를 이용하면 CBOW 모델의 손실 함수도 간결하게 표현할 수 있다.
CBOW 모델의 손실 함수는 단순히 식 3.1의 확률에 log를 취한 다음 마이너스를 붙이면 된다. 이를 음의 로그 기능도라고 한다.
이를 말뭉치 전체로 확장하면 다음 식이 된다.
CBOW 모델의 학습이 수행하는 일은 이 손실함수의 값을 가능한 작게 만드는 것이다.
◾ 3.5.2 skip-gram 모델
word2vec은 2개의 모델을 제안하고 있다 하나는 CBOW 모델이고 다른 하나는 skip-gram모델 이다.
skip-gram 모델은 CBOW에서 다루는 맥락과 타깃을 역전시킨 모델이다.
위 그림과 같이 CBOW 모델은 맥락이 여러 개 있고 그 여러 맥락으로부터 중앙의 단어를 추측한다.
skip-gram 모델은 중앙의 단어로부터 주변의 여러 단어를 추측한다.
skip-gram 모델의 입력층은 하나이고 출력층은 맥락의 수만큼 존재한다. 각 출력층에서는 개별적으로 손실을 구하고 이 개별 손실들을 모두 던한 값을 최종 손실로 한다.
skip-gram 모델을 확률로 표기하면 다음과 같다.
이 식은 wt가 주어졌을 때 wt-1과 wt+1이 동시에 일어날 확률을 뜻한다.
여기서 skip-gram 모델에서는 맥락의 단어들 사이에 관련성이 없다고 가정하고 다음과 같이 분해한다. 이를 조건부 독립이라고 가정한다.
위 식을 교차 엔트로피 오차에 적용하여 skip-gram 모델의 손실 함수를 유도할 수 있다.
이를 말뭉치 전체로 확장하면 skip-gram 모델의 손실 함수는 다음과 같아 진다.
skip-gram 모델은 맥락의 수만큼 추측하기 때문에 그 손실 함수는 각 맥락에서 구한 손실의 총합이어야 한다.
반면, CBOW 모델은 타깃 하나의 손실을 구한다.
단어 분산 표현의 정밀도 면에서 skip-gram 모델의 결과가 더 좋은 경우가 많기 때문에 CBOW 모델과 skip-gram 모델 중 skip-gram 모델을 사용하는 것이 더 좋다.
말뭉치가 커질수록 저빈도 단어나 유추 문제의 성능 면에서 skip-gram 모델이 더 뛰어난 경향이 있다.
학습 속도 면에서는 skip-gram 모델은 손실을 맥락의 수만큼 구해야 해서 계산 비용이 그만큼 커지기 때문에 CBOW 모델이 더 빠르다.
◾ 3.5.3 통계 기반 vs 추론 기반
통계 기반 기법은 말뭉치의 전체 통계로부터 1회 학습하여 단어의 분산 표현을 얻는다.
추론 기반 기법에서는 말뭉치를 일부분씩 여러 번 보면서 학습한다.
어휘에 추가할 새 단어가 생겨서 단어의 분산 표현을 갱신해야 하는 상황에서는 통계 기반 기법에서는 계산을 처음부터 다시 해야 한다. 단어의 분산 표현을 조금만 수정하고 싶어도 동시발생 행렬을 다시 만들고 SVD를 수행하는 일련의 작업을 다시 해야 한다.
그에 반해 추론 기반 기법(word2vec)은 매개변수를 다시 학습할 수 있다.
학습한 가중치를 초깃값으로 사용해 다시 학습하면 되지만 이런 특성 덕분에 기존에 학습한 경험을 해치지 않으면서 단어의 분산 표현을 효율적으로 갱신할 수 있다.
통계 기반 기법에서는 주로 단어의 유사성이 인코딩 된다. word2vec(특히 skip-gram)에서는 단어의 유사엉은 물론 한층 복잡한 단어 사이의 패턴까지도 파악되어 인코딩 된다.
이런 이유로 추론 기반 기법이 통계 기반 기법보다 정확하다고 흔히들 오해한다.
하지만 실제로 단어의 유사성을 정량 평가하면 의외로 추론 기반 통계 기반 기법의 우열을 가릴 수 없다고 한다.
skip-gram과 네거티브 샘플링을 이용한 모델은 모두 말뭉치 전체의 동시발생 행렬에 특수한 행렬 분해를 적용한 것돠 같다.
word2vec 이후 추론 기반 기법과 통계 기반 기법을 융합한 GloVe 기법이 등장했다.
GloVe의 기본 아이디어는 말뭉치 전체의 통계 정보를 손실 함수에 도입해 미니배치 학습을 하는 것이다.
◼️ 3.6 정리
- 추론 기반 기법은 추측하는 것이 목적이며 그 부산물로 단어의 분산 표현을 얻을 수 있다.
- wrod2vec은 추론 기반 기법이며 단순한 2층 신경망이다.
- word2vec은 skip-gram 모델과 CBOW 모델을 제공한다.
- CBOW 모델은 여러 단어로부터 하나의 단어를 추측한다.
- 반대로 skip-gram 모델은 하나의 단어로부터 다수의 단어를 추측한다.
- word2vec은 가중치를 다시 학습할 수 있으므로 단어의 분산 표현 갱신이나 새로운 단어 추가를 효율적으로 수행할 수 있다.