본문 바로가기

딥러닝/밑바닥부터 시작하는 딥러닝 2

3장 : word2vec

추론 기반 기법

3.1 추론 기반 기법과 신경망

단어를 벡터로 표현하는 방법 - 통계 기반 기법, 추론 기반 기법 -> 모두 분포 가설이 배경

3.1.1 통계 기반 기법의 문제점

  • 대규모 말뭉치를 다룰 때 문제 발생
  • SVD를 nxn 행렬에 적용하는 비용은 O(n^3) 
  • 통계 기반 기법은 1회의 처리만에 단어의 분산 표현 얻음
  • 추론 기반 기법에서는  신경망을 이용해 미니배치로 학습 -> 학습 샘플씩 반복해서 학습하며 가중치 갱신
    -> 계산량이 큰 작업 처리 가능, GPU 이용한 병렬 계산 가능

 

3.1.2 추론 기반 기법 개요

추론 : 주변 단어(맥락)가 주어졌을 때 ?에 무슨 단어가 들어가는지 추측하는 작업

  • 추론 문제를 반복해서 풀면서 단어의 출현 패턴 학습

  • 모델 - 맥락 정보를 입력받아 각 단어의 출현 확률을 출력
  • 말뭉치를 사용해 모델이 올바른 추측을 내놓도록 학습

 

3.1.3 신경망에서의 단어 처리

  • 단어를 고정길이의 벡터로 변환
  • 원핫 표현 (원핫 벡터) 사용
  • 원핫 표현 : 벡터의 원소 중 하나만 1이고 나머지는 모두 0인 벡터
  • 총 어휘 수만큼의 원소를 갖는 벡터 준비 -> 인덱서가 단어 ID와 같은 원소를 1로, 나머지는 모두 0으로 설정
  • 뉴런의 수 고정 가능 (그림에서 7개)

  • 완전연결계층이므로 각각의 노드가 이웃 층의 모든 노드와 화살표로 연결
  • 이번 장의 완전연결계층에서는 편향 생략

##완전연결계층에 의한 단어 벡터 변환
c = np.array([[1,0,0,0,0,0,0]])  #입력
W = np.random.randn(7,3)  #가중치
h = np.matmul(c, W)  #중간 노드
print(h)

##방법2 - Matmul 계층으로 수행
c = np.array([[1,0,0,0,0,0,0]])  
layer = MatMul(W)
h = layer.forward(c)  #순전파 수행 
print(h)

->

[[-1.43238709  0.23664779 -1.40858452]]
[[-1.43238709  0.23664779 -1.40858452]]
  • 단어ID가 0인 단어를 원핫표현으로 표현한 다음 완전연결계층 통과시켜 변환
  • c와 W의 행렬 곱 부분에서 c는 원핫 표현이기 때문에 행렬 곱은 결국 가중치의 행벡터 하나를 뽑아낸 것과 같음


3.2 단순한 word2vec

word2vec에서 사용하는 신경망

  • CBOW 모델 (continuous bag-of-words)
  • skip-gram 모델

 

3.2.1 CBOW 모델의 추론 처리

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

  • 입력은 맥락 
  • 맥락 : 단어들의 목록 ("you", "goodbye")
  • 맥락을 원핫 표현으로 변환

  • 입력층 2개 (맥락으로 고려할 단어를 2개로 정했기 때문, N개라면 입력층도 N개)
  • 은닉층을 거쳐 출력층에 도달 
  • 입력층에서 은닉층으로의 변환은 똑같은 완전연결계층 (가중치 Win)이 처리
  • 은닉층에서 출력층 뉴런으로의 변환은 다른 완전연결계층 (가중치 Wout)이 처리
  • 은닉층의 뉴런은 입력층의 완전연결계층에 의해 변환된 값
    입력층이 여러 개이면 전체를 평균  1/2(h1 +h2)
  • 출력층의 뉴런 하나하나는 각 단어에 대응
    -> 출력층 뉴런 : 각 단어의 점수 
    => 값이 높을수록 대응 단어의 출현 확률도 높아짐 (확률은 점수를 소프트맥스 함수를 적용해 얻음)
  • 은닉층의 뉴런 수를 입력층의 뉴런 수보다 적게 하는 것이 핵심 
    -> 은닉층에는 필요한 정보를 간결하게 담게 됨 -> 밀집벡터 표현 얻을 수 있음
  • 은닉층의 정보는 인간이 이해할 수 없는 코드로 쓰여 있음 : 인코딩
  • 은닉층의 정보로부터 원하는 결과를 얻는 작업 : 디코딩

 

 

 

 

 

 

  • 완전연결계층의 가중치 Win (7x3)
  • 단어의 분산 표현의 정체 
  • 각 행에는 해당 단어의 분산 표현이 담겨 있음
  • 학습을 진행할수록 분산 표현들이 갱신됨
  • 단어의 의미도 녹아들어 있음

 

 

 

##CBOW모델의 추론 처리

#샘플 맥락 데이터
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)  #가중치 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)

-> [[-1.8372924 -3.60577232 -3.03437831 -2.01436018 -2.0348912 -1.20465769 -0.26381478]]
=> 각 단어의 점수 

 

3.2.2 CBOW 모델의 학습

  • 다중 클래스 분류 수행하는 신경망
  • 소프트맥스 - 점수를 확률로 변환
  • 교차 엔트로피 오차 - 확률과 정답 레이블로부터 교차 엔트로피 오차 구함 (손실) -> 학습 진행

 

3.2.3 word2vec의 가중치와 분산 표현

  • 두 가지 가중치
    • 입력 측 완전연결계층의 가중치 (W_in) : 각 단어의 분산 표현
    • 출력 측 완전연결계층의 가중치 (W_out) : 단어의 의미가 인코딩된 벡터 (열 방향으로 저장)

  • word2vec에서는 입력 측의 가중치(W_in)을 최종 단어의 분산 표현으로 이용하는 것이 대중적인 선택

 

3.3 학습 데이터 준비 

3.3.1 맥락과 타깃

  • 맥락의 각 행이 신경망의 입력
  • 타깃의 각 행이 정답 레이블
  • 맥락의 수는 여러 개가 될 수 있지만 타깃은 오직 하나

from common.util import preprocess

text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)  #말뭉치 텍스트를 단어 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):  #-1, 0, 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)

-> 

[[0 2]
 [1 3]
 [2 4]
 [3 1]
 [4 5]
 [1 6]]
[1 2 3 4 1 5]

맥락과 타깃 - 단어ID로 구성됨 

 

3.3.2 원핫 표현으로 변환

  • 단어ID를 이용한 맥락의 형상 (6,2) -> 원핫 표현 (6,2,7)
  • 원핫 표현으로의 변환 : convert_one_hot()함수 사용
##원핫 표현으로 변환
from common.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)  #말뭉치를 단어 ID로 변환

contexts, target = create_contexts_target(corpus, window_size=1)  #맥락과 타깃 얻기

vocab_size = len(word_to_id)

#맥락과 타깃 원핫 표현으로 변환
target = convert_one_hot(target, vocab_size) #단어ID목록과 어휘수 인수로 받음
contexts = convert_one_hot(contexts, vocab_size)

3.4 CBOW 모델 구현

  • SimpleCBOW 구현

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계층 만들기(2개)
        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])  #맥락의 첫번째 단어 (6,7)
        h1 = self.in_layer0.forward(contexts[:, 1])  #맥락의 두번째 단어 (6,7)
        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 학습 코드 구현 

##학습 코드 구현
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)  #말뭉치를 단어 ID로 변환

contexts, target = create_contexts_target(corpus, window_size=1)  #맥락과 타깃 얻기
vocab_size = len(word_to_id)

#맥락과 타깃 원핫 표현으로 변환
target = convert_one_hot(target, vocab_size) #단어ID목록과 어휘수 인수로 받음
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])
you [ 1.1629325 -0.9632237  1.0220319 -0.9973298  1.7453   ]
say [-0.6323519   1.2298591   0.02795994  1.1977036   1.124968  ]
goodbye [ 0.76684    -0.95298827  1.010701   -0.98307246 -0.8858874 ]
and [-1.5050563   0.99208957  1.668874    0.9707058   0.74477434]
i [ 0.7717827  -0.9794516   1.0309044  -0.99582094 -0.89843786]
hello [ 1.151623   -0.95551836  1.0052384  -0.9810654   1.769742  ]
. [ 1.6542563  1.1570057 -1.4423819  1.1536727  1.1364125]
#가중치 초기화에서 random()함수를 사용하므로 실행할 때마다 결과 달라짐
  • 단어 ID의 분산 표현 출력 
  • 단어를 밀집벡터로 나타냄 
    -> 학습이 잘 이뤄졌기 때문에 단어의 의미를 잘 파악한 벡터 표현
  • 처리 효율 면에서 몇 가지 문제 존재

3.5 word2vec 보충

3.5.1 CBOW 모델과 확률

  • 동시 확률 : P(A, B) : A와 B가 동시에 일어날 확률
  • 사후 확률 : P(A|B) : B가 주어졌을 때 A가 일어날 확률

-> Wt-1과 Wt+1이 주어졌을 때 타깃이 Wt가 될 확률

-> 교차 엔트로피 오차 적용 

-> 말뭉치 전체로 확장

=> 음의 로그 가능도 :CBOW모델의 손실 함수

 

3.5.2 skip-gram 모델

: CBOW에서 다루는 맥락과 타깃을 역전시킨 모델 

-> 중앙의 단어(타깃)로부터 주변의 여러 단어 (맥락)을 추측

  • 입력층은 하나
  • 출력층은 맥락의 수만큼 존재 
  • 출력층에서는 개별적으로 손실을 구하고, 이 개별 손실들을 모두 더한 값이 최종 손실

-> Wt가 주어졌을 때 Wt-1과 Wt+1이 동시에 일어날 확률

-> 맥락 단어들 사이에 관련성이 없다고 가정하고 분해 (조건부 독립)

-> 교차 엔트로피 오차 적용 

-> 말뭉치 전체로 확장

=> skip-gram모델의 손실 함수 

 

  • skip-gram 모델
    • 손실 함수는 각 맥락에서 구한 손실의 총합
  • CBOW 모델
    • 타깃 하나의 손실

=> skip-gram 모델의 단어 분산 표현의 정밀도 면에서 결과가 더 좋음 

  • 더 어려운 상황에서 단련하기 때문 
  • 특히 말뭉치가 커질수록 성능이 더 뛰어남 
  • 학습 속도 면에서는 CBOW 모델이 더 빠름

 

3.5.3 통계 기반 vs. 추론 기반

  • 통계 기반 기법
    • 어휘 추가 -> 계산 처음부터 다시 
    • 주로 단어의 유사성이 인코딩
  • 추론 기반 기법
    • 매개변수 다시 학습 (기존 학습 가중치는 초깃값으로 사용) -> 효율적 갱신 가능
    • 단어의 유사성 + 복잡한 단어 사이의 패턴까지 인코딩

=> 우열을 가리기 힘듦

  • 서로 관련되어 있음
    • 추론 기반 기법도 말뭉치 전체의 동시발생 행렬에 특수한 행렬 분해를 적용한 것과 같음
    • 추론 기반 기법과 통계 기반 기법을 융합한 GloVe기법 등장 
      말뭉치 전체의 통계 정보를 손실 함수에 도입해 미니배치 학습을 하는 것

3.6 정리

  • 추론 기반 기법은 추측하는 것이 목적이며, 그 부산물로 단어의 분산 표현을 얻을 수 있다.
  • word2vec은 추론 기반 기법이며, 단순한 2층 신경망이다.
  • word2vec은 skip-gram, CBOW 모델을 제공한다.
  • CBOW 모델은 여러 단어(맥락)로부터 하나의 단어(타깃)을 추측한다.
  • 반대로 skip-gram모델은 하나의 단어(타깃)로부터 다수의 단어(맥락)을 추측한다.
  • word2vec은 가중치를 다시 학습할 수 있으므로, 단어의 분산 표현 갱신이나 새로운 단어 추가를 효율적으로 수행할 수 있다.

책 참고 : 밑바닥부터 시작하는 딥러닝 (한빛미디어)