안녕, 세상!

(2)-4-2 개선된 word2vec 학습 본문

It공부/Deep learning

(2)-4-2 개선된 word2vec 학습

dev_Lumin 2021. 1. 20. 00:23

직전 설명에서 기존 word2vec을 개선하였습니다.

 

(1) CBOW 모델 구현

앞 장에서 단순한 SimpleCBOW 클래스를 개선할 것입니다.

개선점은 Embedding 계층과 Negative Sampling Loss 계층을 적용하는 것입니다.

 

개선된 CBOW 모델 클래스 코드는 다음과 같습니다.

#import sys
#sys.path.append('..')
#from common.np import *  # import numpy as np
#from common.layers import Embedding
#from ch04.negative_sampling_layer import NegativeSamplingLoss

class CBOW:
    def __init__(self, vocab_size, hidden_size, window_size, corpus):
        V, H = vocab_size, hidden_size

        # 가중치 초기화
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(V, H).astype('f')

        # 계층 생성
        self.in_layers = []
        for i in range(2 * window_size):
            layer = Embedding(W_in)  # Embedding 계층 사용
            self.in_layers.append(layer)
        self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)

        # 모든 가중치와 기울기를 배열에 모은다.
        layers = self.in_layers + [self.ns_loss]
        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 = 0
        for i, layer in enumerate(self.in_layers):
            h += layer.forward(contexts[:, i])
        h *= 1 / len(self.in_layers)
        loss = self.ns_loss.forward(h, target)
        return loss

    def backward(self, dout=1):
        dout = self.ns_loss.backward(dout)
        dout *= 1 / len(self.in_layers)
        for layer in self.in_layers:
            layer.backward(dout)
        return None

생성자에서는 4개의 인수 (어휘수, 은닉층 뉴런수, 문맥 수, id목록) 이렇게 구성되어 있고,

가중치 초기화가 완료되면, 이어서 계층을 생성합니다.

 

Embedding 계층을 2*window_size개 작성하여 in_layers[ ]에 배열로 보관합니다.

(타깃 중심으로 window size가 각각 있는 것이므로 2를 곱함)

 

이 후 신경망에서 사용하는 모든 가중치 매개변수와 기울기를 params와 grads에 저장

분산 표현에 접근하기 위해 word_vecs에 W_in을 할당

 

순전파는

각 계층을 적절한 순서로 호출합니다.

전의 SimpleCBOW 클래스는 contexts와 target이 one-hot encoding 형태였지만,

개선된 CBOW 클래스에서는 contexts와 target이 ID 형태로 되어 있습니다.

 

SimpleCBOW 클래스에서는 입력 측의 가중치와 출력 측의 가중치의 형상이 달라서 

출력 측의 가중치에는 단어 벡터가 열 방향으로 배치되었습니다.

한편 CBOW 클래스의 출력 측 가중치는

입력 측 가중치와 같은 형상으로, 단어 벡터가 행 방향에 배치됩니다.

이유는 NegativeSamplingLoss 에서 Embedding 계층을 사용했기 때문입니다.

 


이제 CBOW 모델의 학습을 구현하는 코드는 다음과 같습니다.

import sys
sys.path.append('..')
import numpy as np
from common import config
# GPU에서 실행하려면 아래 주석을 해제하세요(CuPy 필요).
# ===============================================
# config.GPU = True
# ===============================================
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
from cbow import CBOW
from skip_gram import SkipGram
from common.util import create_contexts_target, to_cpu, to_gpu
from dataset import ptb


# 하이퍼파라미터 설정
window_size = 5
hidden_size = 100
batch_size = 100
max_epoch = 10

# 데이터 읽기
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)

contexts, target = create_contexts_target(corpus, window_size)
if config.GPU:
    contexts, target = to_gpu(contexts), to_gpu(target)

# 모델 등 생성
model = CBOW(vocab_size, hidden_size, window_size, corpus)
# model = SkipGram(vocab_size, hidden_size, window_size, corpus)
optimizer = Adam()
trainer = Trainer(model, optimizer)

# 학습 시작
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

# 나중에 사용할 수 있도록 필요한 데이터 저장
word_vecs = model.word_vecs
if config.GPU:
    word_vecs = to_cpu(word_vecs)
params = {}
params['word_vecs'] = word_vecs.astype(np.float16)
params['word_to_id'] = word_to_id
params['id_to_word'] = id_to_word
pkl_file = 'cbow_params.pkl'  # or 'skipgram_params.pkl'
with open(pkl_file, 'wb') as f:
    pickle.dump(params, f, -1)

이 코드에서는 윈도우 크기를 5, 은닉층의 뉴런 수를 100개로 설정했습니다.

말뭉치에 따라 다르겠지만, 윈도우 크기는 2~10개, 은닉층의 뉴런 수는 50~500개 정도가 적당합니다.

 

PTB는 크기가 크므로 컴퓨터에 엔비디아 GPU가 있으며 cupy가 설치되어 있다면 cuda를 사용할 수 있습니다.

 

학습이 끝난 후 가중치를 꺼내어(여기서는 입력 측 가중치만), 나주에 이용할 수 있도록 파일에 보관합니다.

파일에 저장할 때는 'pickle' 기능을 이용합니다.

pickle은 파이썬 코드의 객체를 파일로 저장하는데 이용할 수 있습니다.

 

 

학습을 이미 시킨 파일을 이용하여,

github.com/WegraLee/deep-learning-from-scratch-2/blob/master/ch04/cbow_params.pkl

 

WegraLee/deep-learning-from-scratch-2

『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019). Contribute to WegraLee/deep-learning-from-scratch-2 development by creating an account on GitHub.

github.com

(git clone 해서 받기)

 

 

(2) 모델 평가

모델을 평가해보겠습니다.

모델 평가는 앞서 사용했던 most_similar() 메소드를 이용하여 단어의 연관성을 파악해보겠습니다.

most_similar() 코드는 다음과 같습니다.

def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
    '''유사 단어 검색
    :param query: 쿼리(텍스트)
    :param word_to_id: 단어에서 단어 ID로 변환하는 딕셔너리
    :param id_to_word: 단어 ID에서 단어로 변환하는 딕셔너리
    :param word_matrix: 단어 벡터를 정리한 행렬. 각 행에 해당 단어 벡터가 저장되어 있다고 가정한다.
    :param top: 상위 몇 개까지 출력할 지 지정
    '''
    if query not in word_to_id:
        print('%s(을)를 찾을 수 없습니다.' % query)
        return

    print('\n[query] ' + query)
    query_id = word_to_id[query]
    query_vec = word_matrix[query_id]

    # 코사인 유사도 계산
    vocab_size = len(id_to_word)

    similarity = np.zeros(vocab_size)
    for i in range(vocab_size):
        similarity[i] = cos_similarity(word_matrix[i], query_vec)

    # 코사인 유사도를 기준으로 내림차순으로 출력
    count = 0
    for i in (-1 * similarity).argsort():
        if id_to_word[i] == query:
            continue
        print(' %s: %s' % (id_to_word[i], similarity[i]))

        count += 1
        if count >= top:
            return

 

 

most_similar을 이용한 평가 코드는 다음과 같습니다.

# import sys
# sys.path.append('..')
# from common.util import most_similar
import pickle


pkl_file = 'cbow_params.pkl'

with open(pkl_file, 'rb') as f:
    params = pickle.load(f)
    word_vecs = params['word_vecs']
    word_to_id = params['word_to_id']
    id_to_word = params['id_to_word']

# 가장 비슷한(most similar) 단어 뽑기
querys = ['you', 'year', 'car', 'toyota']
for query in querys:
    most_similar(query, word_to_id, id_to_word, word_vecs, top=5)

결과들이 비교적 의미적으로 연관된 단어들로 잘 나온 것을 확인할 수 있습니다.

 

word2vec으로 얻은 단어의 분산 표현은 비슷한 단어를 가까이 모을 뿐만 아니라 더 복잡한 패턴을 파악하는 것으로 알려져 있습니다.

대표적으로 유추 문제(비유 문제)가 있습니다.

예로 'king - man + woman = queen'

즉 word2vec의 단어 분산 표현을 사용한다면 유추 문제를 벡터의 덧셈과 뺄셈으로 풀 수 있다는 뜻입니다.

 

위의 예시로 이어서 설명하자면

벡터 공간에서 'man -> woman' 벡터와 'king -> ? '벡터가 가능한 가까워지는 단어(?)를 찾는 것입니다.

특정 '단어'의 분산 표현(단어 벡터)을 'vec('단어')'라고 할 때

vec('king') + vec('woman') - vec('man') = vec(?) 식이 성립됩니다.

 

이를 구현하는 함수는 analogy() 함수로 다음과 같습니다.

 

def analogy(a, b, c, word_to_id, id_to_word, word_matrix, top=5, answer=None):
    for word in (a, b, c):
        if word not in word_to_id:
            print('%s(을)를 찾을 수 없습니다.' % word)
            return

    print('\n[analogy] ' + a + ':' + b + ' = ' + c + ':?')
    a_vec, b_vec, c_vec = word_matrix[word_to_id[a]], word_matrix[word_to_id[b]], word_matrix[word_to_id[c]]
    query_vec = b_vec - a_vec + c_vec
    query_vec = normalize(query_vec)

    similarity = np.dot(word_matrix, query_vec)

    if answer is not None:
        print("==>" + answer + ":" + str(np.dot(word_matrix[word_to_id[answer]], query_vec)))

    count = 0
    for i in (-1 * similarity).argsort():
        if np.isnan(similarity[i]):
            continue
        if id_to_word[i] in (a, b, c):
            continue
        print(' {0}: {1}'.format(id_to_word[i], similarity[i]))

        count += 1
        if count >= top:
            return

 

 

이 analogy() 함수를 이용해 다음 코드로 비유 문제를 해결할 수 있습니다.

# coding: utf-8
import sys
sys.path.append('..')
from common.util import analogy
import pickle

pkl_file = 'cbow_params.pkl'
# pkl_file = 'skipgram_params.pkl'

with open(pkl_file, 'rb') as f:
    params = pickle.load(f)
    word_vecs = params['word_vecs']
    word_to_id = params['word_to_id']
    id_to_word = params['id_to_word']

# 유추(analogy) 작업
print('-'*50)
analogy('king', 'man', 'queen',  word_to_id, id_to_word, word_vecs)
analogy('take', 'took', 'go',  word_to_id, id_to_word, word_vecs)
analogy('car', 'cars', 'child',  word_to_id, id_to_word, word_vecs)
analogy('good', 'better', 'bad',  word_to_id, id_to_word, word_vecs)

결과를 보면 성별, 현재/과거형, 단수/복수, 비교급 정도는 잘 파악하고 있는 것을 확인할 수 있습니다.

마지막 good : better = bad : ? 에서 worse가 아니라 more을 선택했지만 비교급이라는 성질은 파악한 것 같습니다.

 

이처럼 word2vec으로 얻은 다언의 분산 표현을 사용하면, 벡터의 덧셈과 뺄셈으로 유추 문제를 풀 수 있습니다.

 

해당 결과가 아주 뛰어나 보인 건 사실입니다.

잘 풀리는 문제 중심으로 보여줬기 때문입니다.

원하는 결과를 얻지 못하는 문제도 많을 것입니다.

그 이유은 PTB 데이터셋이 충분히 많은 데이터양을 가지고 있지 않기 때문일 것입니다.

만약 더 큰 말뭉치로 학습한다면 더 복잡하고 정밀한 관계를 파악하여 좋은 결과를 낼 것입니다.

 

 

 

(3) 분산 표현의 중요성

① 중요성

자연어 처리의 분산 표현은 비슷한 단어를 찾는 용도뿐만 아니라 

전이 학습(transfer learning)에 중요한 성질이 있습니다.

전이 학습은 한 분야에서 비운 지식을 다른 분야에도 적용하는 기법입니다.

 

자연어 문제를 풀 때 word2vec의 단어 분산 표현을 처음부터 학습하는 일은 거의 없습니다.

먼저 (위키백과나 구글 뉴스의 텍스트 데이터 등) 큰 말뭉치로 학습을 끝낸 후, 그 분산 표현을 각자의 작업에 이용합니다.

자연어 처리 작업이라면 가장 먼저 단어를 벡터로 변환하는 작업이 필요한데

이때 학습을 미리 끝낸 단어의 분산 표현을 이용하면 

자연어 처리 작업 대부분에 훌륭한 결과를 이끌어 냅니다.

 

단어의 분산 표현은 단어를 고정 길이 벡터로 변환해준다는 장점도 있습니다.

게다가 문장도 단어의 분산 표현을 사용하여 고정 길이 벡터로 변환할 수 있습니다.

가장 간단한 방법은 bag-of-words 기법으로

문장의 각 단어를 분산 표현으로 변환하고 그 합을 구합니다.

하지만 단순한 합은 순서를 고려하지 않게 됩니다.

 

다음 글에서 설명할 순환 신경망(RNN)을 사용하면 한층 세련된 방법으로 word2vec의 단어의 분산 표현을 이용하면서 문장을 고정 길이 벡터로 변환할 수 있습니다.

 

단어나 문장을 고정 길이 벡터로 변환할 수 있다는 것은 

변환된 고정 벡터를 일반적인 머신러닝 기법에 적용시킬 수 있기 때문에 

중요합니다.

 

(단어의 분산 표현 학습과 현재 직면한 문제를 해결하는 머신러닝 시스템 학습은 서로 다른 데이터셋을 사용해 개별적으로 수행하는 것이 일반적임

다만, 직면한 문제의 학습 데이터가 아주 많다면 단어의 분산 표현과 머신러닝 시스템 학습 모두를 처음부터 수행하는 방안도 고려해볼 수 있음)

 

 

② 단어 벡터 평가 방법

단어의 분산 표현(고정 길이 벡터)은 현실적으로 특정한 애플리케이션(특정 목적을 지닌 머신러닝)에서 사용되는 것이 대부분입니다.

궁극적으로 원하는 것은 정확도가 높은 시스템입니다.

시스템은 크게 단어의 분산 표현을 만드는 시스템과

특정 문제에 대해 분류를 수행하는 시스템(머신러닝)입니다.

 

분산 표현 시스템과 특정 목적을 지닌 시스템은 따로 수행할 수 있습니다.

보통 분산 표현 자체 평가, 즉 단어 벡터의 평가는 특정 목적을 지닌 시스템과 분리해서 평가하는 것이 일반적입니다. ( 일단 따로 평가하겠다는 것임)

 

이때 분산 표현 평가 사용되는 평가 척도는 단어의 '유사성'이나 '유추 문제'를 활용한 평가입니다.

단어의 유사성 평가에서는 사람이 작성한 단어 유사도 검증 세트를 사용해 평가하는 것이 일반적입니다.

예를 들어 유사도를 0부터 10 사이로 점수화한다면, 'cat'과 'animal'의 유사도는 8점,

'cat'과 'car'의 유사도는 2점과 같이,

사람이 단어 사이의 유사한 정도를 규정합니다.

그리고 사람이 부여한 점수와 word2vec에 의한 코사인 유사도 점수를 비교해 그 상관성을 보는 것입니다.

 

유추 문제를 활용한 평가는 'king : queen = man : ? ' 와 같은 유추 문제에 대한 정답률로 분산 표현 우수성을 평가합니다.

 

www.aclweb.org/anthology/D14-1162/

 

GloVe: Global Vectors for Word Representation

Jeffrey Pennington, Richard Socher, Christopher Manning. Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP). 2014.

www.aclweb.org

위 논문에서 유추 문제에 대한 평가 결과가 있는데 이를 간단히 그림으로 나타내면 다음과 같습니다.

모델 차수 말뭉치 크기 의미(semantics) 구문(syntax) 종합
CBOW 300 16억 16.1 52.6 36.1
skip-gram 300 10억 61 61 61
CBOW 300 60억 63.6 67.4 65.7
skip-gram 300 60억 73.0 66.0 69.1
CBOW 1000 60억 57.3 68.9 63.7
skip-gram 1000 60억 66.1 65.1 65.6

의미(sematics) : 단어의 의미를 유추하는 유추 문제의 정답률

    ex) 'king : queen = man : woman'

 

구문(syntax) : 단어의 형태 정보를 묻는 문제의 정답률

    ex) 'bad : worst = good : best'

 

 

이렇게 유추 문제를 정확하게 풀 수 있는 단어의 분산 표현이라면 자연어를 다루는 애플리케이션에 좋은 결과를 기대할 수 있습니다.

다만, 단어의 분산 표현의 우수함이 애플리케이션에 얼마나 기여하는지는 애플리케이션 종류나 말뭉치 내용 등, 다루는 문제 상황에 따라 다릅니다.

즉, 유추 문제에 대한 평가가 높다고 해서 반드시 좋은 결과가 나오리라는 보장은 없습니다.

 

 

 

(위의 내용은 밑바닥부터 시작하는 딥러닝 2 (한빛미디어)를 바탕으로 글을 작성하였습니다)

'It공부 > Deep learning' 카테고리의 다른 글

(2)-5-2 RNN 구현  (0) 2021.01.22
(2)-5-1 순환 신경망(RNN)  (3) 2021.01.21
(2)-4-1 word2vec 속도 개선  (0) 2021.01.19
(2)-3-2 word2vec 보충  (0) 2021.01.18
(2)-3-1. word2vec  (0) 2021.01.17
Comments