Deep Learning

Pre-trained Word Vector를 Tensorflow에서 사용하기

둔진 2020. 5. 20. 08:36

  Tensorflow에서 제공하는 Embedding Layer는 간편하게 사용하기에 좋습니다. 하지만 아무래도 대량의 데이터를 사용해서 별도로 훈련시킨 Word Embedding을 사용하는 것보다는 성능이 떨어집니다. (Tensorflow의 Embedding Layer vs fastText)

  fastText 같은 Library는 Python 인터페이스를 제공하기 때문에 pre-trained word vector를 Tensorflow에서 사용하기 쉬운 편입니다 (한국어 토큰의 단위는 뭐가 좋을까?). 하지만 이런 인터페이스가 없이 순수하게 단어와 그 단어의 Embedding 데이터만 있다면 어떻게 할까요? 결론부터 말하면 Keras Embedding Layer의 weights를 수동으로 지정해주고, trainable을 False로 해서 training에서 제외시키면 됩니다.

  아래 실제 예를 들어서 한번 보겠습니다. 한국어 토큰의 단위는 뭐가 좋을까?와 후속 글들에서 사용한 Naver Movie 태스크를 계속 이용하겠습니다.

1. fastText vec 파일

  fastText로 훈련을 시키고 나면, fastText 고유의 모델도 나오지만 *.vec 파일도 같이 생성이 됩니다. 이 파일은 아래와 같이 각 단어와 그 단어의 embedding이 텍스트 파일로 저장되어있습니다.

254551 100
. -0.10487 -0.099523 0.2545 0.11628 -0.29301 -0.10785 0.099124 -0.44543 -0.073618 0.18674 -0.2006 0.12969 -0.091671 -0.22936 -0.18388 -0.11691 0.090781 0.017952 0.10287 0.19753 -0.051613 0.17163 0.27302 -0.18497 0.10391 -0.21119 0.11373 0.42941 -0.071135 0.084024 -0.027976 0.066976 -0.18009 -0.084219 -0.1672 -0.18928 -0.08257 -0.29677 0.0030799 -0.16048 -0.19918 0.29201 0.12742 -0.26709 -0.26075 0.20923 0.18204 0.13541 0.5421 0.37331 0.14902 -0.060302 0.065207 -0.17524 0.030769 -0.25293 -0.15077 0.064334 0.03622 -0.36372 -0.029672 0.049806 0.32565 -0.067889 -0.24594 0.069863 0.20587 0.049916 0.32064 0.2362 0.014902 0.17059 0.13242 0.21424 -0.086264 -0.10459 -0.27568 0.059371 -0.054577 0.11543 -0.039111 -0.0088316 -0.060751 -0.0064676 0.10415 0.18845 0.12263 -0.075766 -0.36565 0.048481 0.053475 0.054344 0.46624 -0.072684 -0.18491 0.12709 -0.15638 -0.062447 -0.27093 -0.31987
의 -0.22594 -0.11791 -0.011636 0.35154 -0.084411 -0.051374 0.046974 -0.1842 0.029462 -0.033034 -0.23228 -0.038859 -0.0323 -0.26425 -0.16366 -0.43753 0.14605 -0.11451 -0.080288 -0.037785 -0.060549 0.046365 0.10907 -0.027873 0.264 -0.047122 0.14328 0.21897 -0.11781 -0.074791 -0.21582 -0.055199 -0.090352 -0.057479 -0.015602 -0.39332 -0.25813 -0.16662 0.12239 -0.26666 0.20609 0.032119 -0.091906 -0.27625 -0.024875 0.077713 0.16151 -0.029307 0.064283 0.20459 0.16951 0.012216 0.44919 -0.069572 -0.040275 -0.12614 -0.095869 -0.057071 -0.11612 0.022829 0.20002 -0.11876 0.063324 -0.079731 0.035882 0.032231 0.21276 -0.12063 0.20824 0.32541 -0.1487 0.27904 0.068206 0.025219 0.054075 -0.043147 -0.27615 0.26829 -0.10263 -0.032489 -0.1403 -0.053289 0.071746 -0.093683 0.024979 -0.10631 0.026899 -0.0076882 -0.32998 0.048958 0.03548 0.051677 0.10494 -0.049428 -0.070927 0.19328 -0.23637 0.015759 -0.081132 -0.36007
다 -0.13201 -0.12739 0.31773 0.19274 -0.30226 0.047135 0.12848 -0.37849 -0.054117 0.13855 -0.30475 0.094579 -0.1663 -0.18998 -0.18453 -0.10494 0.1167 0.01068 0.1796 0.13492 -0.042974 0.25082 0.29651 -0.17384 0.15385 -0.21189 0.1087 0.38853 -0.092791 -0.024528 -0.10277 0.035023 -0.15358 0.0036208 -0.16856 -0.13752 -0.22457 -0.26079 0.033856 -0.15561 -0.09292 0.2295 0.08664 -0.30634 -0.24136 0.18878 0.10285 0.12372 0.50428 0.3055 0.18137 0.11601 0.21064 -0.21421 0.032266 -0.28413 -0.13529 0.049654 0.028216 -0.37579 -0.017815 0.015287 0.25666 -0.045606 -0.19478 0.047534 0.15701 0.088331 0.25283 0.28349 0.043468 0.12504 0.11816 0.22245 -0.062467 -0.025681 -0.2895 0.071713 0.012591 0.083128 -0.053313 0.041973 -0.054066 0.01356 0.11986 0.089138 0.11949 -0.08493 -0.34909 -0.09263 0.076634 0.044987 0.32116 -0.025656 -0.18832 0.13472 -0.2525 -0.017052 -0.23064 -0.34662

  첫번째 줄은 실제 데이터가 아니고 헤더입니다. 첫 번째 254551은 현재 모델에 254551개의 단어가 있다는 뜻이고, 100은 Embedding dimension이 100이라는 뜻입니다. 이번 포스트에서는 이 파일을 사용해서 진행을 해보겠습니다.

  * 앞에 말씀드린대로 fastText는 Python 인터페이스를 제공하기 때문에, fastText를 사용하신다면 fastText의 Python 인터페이스를 사용하는 것이 좋습니다. 편리하기도 하고, fastText의 character n-gram 기반의 알고리즘도 활용할 수 있기 때문입니다. 이 경우에 out-of-vocaburary에 대한 성능이 더 좋습니다.

더 좋은 거 있으면 그냥 더 좋은 걸 쓰는 걸로

2. Word Vector 데이터를 읽자

  아래와 같이 Word Vector 데이터를 읽습니다. 아래 함수는 각 단어(토큰)를 Index로 변환하는 dictionary, 그 Index에 맞춰서 Word Embedding이 저장된 list, 그리고 Embedding dimension을 반환합니다.

import numpy as np

def load_word_embedding(path):    
    with open(path) as f:
        n_lines, embedding_dim = f.readline().strip().split()
        n_lines = int(n_lines)
        embedding_dim = int(embedding_dim)        
        
        token_to_index = {}
        word_vectors = np.zeros((n_lines + 1, embedding_dim))
        
        token_to_index[''] = 0    
    
        for i in trange(1, n_lines, desc='Loading word vectors', unit=' words'):
            line = f.readline()            
            token, *values = line.rstrip().split(' ')
            
            values = np.array([float(value) for value in values])
            
            token_to_index[token] = i            
            word_vectors[i] = values                        
        
        return token_to_index, np.array(word_vectors), embedding_dim

3. 전처리 코드

  다음으로 Text 데이터를 읽은 후 전처리를 하는 코드입니다. 대부분 한국어 토큰의 단위는 뭐가 좋을까? 의 코드를 그대로 가져왔습니다.

import tensorflow as tf
import numpy as np
from tqdm import tqdm, trange

from konlpy.tag import Mecab

def parse_corpus_line(line):
    _, sentence, label = line.strip().split('\t')
    label = int(label)
    return sentence, label

def read_corpus(path):
    sentences = []
    labels = []

    for line in open(path):
        sentence, label = parse_corpus_line(line)
        sentences.append(sentence)
        labels.append(label)

    return np.array(sentences), np.array(labels)

def convert_to_index(tokens, token_to_index):
    result = []
    for token in tokens:
        index = token_to_index.get(token)
        if index != None:
            result.append(index)
            
    return np.array(result)

tagger = Mecab()

def tokenize_by_morpheme_char(s):
    return tagger.morphs(s)
           
def preprocess(sentences, token_to_index):
    tokenized_sentences = [tokenize_by_morpheme_char(sentence) for sentence in tqdm(sentences, desc='Tokenizing')]
    
    result = []
    max_tokens = 0
    for tokens in tqdm(tokenized_sentences, desc='Converting to index'):
        indices = convert_to_index(tokens, token_to_index)
        max_tokens = max(max_tokens, len(indices))
        result.append(indices)
            
    return np.array(result), max_tokens

  데이터 준비과정은 다음과 같습니다.

  1. Raw Corpus에서 데이터를 읽습니다.
  2. Corpus의 각 줄을 Tokenize합니다. 이 포스트에서는 형태소 단위를 사용합니다.
  3. Tokenize 된 문장을 Word Embedding에 맞는 Index 형태로 변환합니다.

4. 데이터 준비

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('-train', required=True)
    parser.add_argument('-test', required=True)    
    parser.add_argument('-fasttext', required=True)
    parser.add_argument('--batch-size', type=int, default=128)    
    parser.add_argument('--epochs', type=int, default=20)    
    args = parser.parse_args()
    
    token_to_index, word_vectors, embedding_dim = load_word_embedding(args.fasttext)    
    
    train_sentences, train_labels = read_corpus(args.train)    
    train_data, max_tokens = preprocess(train_sentences, token_to_index)
    train_data = tf.keras.preprocessing.sequence.pad_sequences(train_data, max_tokens, padding='post')
    train_dataset = tf.data.Dataset.from_tensor_slices((train_data, train_labels)).batch(args.batch_size)
    
    test_sentences, test_labels = read_corpus(args.test)
    test_data, _ = preprocess(test_sentences, token_to_index)
    test_data = tf.keras.preprocessing.sequence.pad_sequences(test_data, max_tokens, padding='post')
    test_dataset = tf.data.Dataset.from_tensor_slices((test_data, test_labels)).batch(args.batch_size)   

  Data는 전처리까지 마친 후 Tensorflow Dataset으로 변환합니다. 훈련 데이터의 가장 긴 문장을 기준으로 Padding도 합니다.

5. Model 을 만들자

  기본적인 모델은 지금까지 썼던 것과 똑같습니다. 다만 이번에는 Embedding Layer만 Tensorflow의 것을 그대로 썼습니다. 대신 weights는 fastText로 훈련된 값을 가져오고요.

    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Embedding(len(token_to_index) + 1, embedding_dim, weights=[word_vectors], input_length=max_tokens, trainable=False))
    model.add(tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(128, return_sequences=True)))
    model.add(tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(128)))
    model.add(tf.keras.layers.Dense(64))
    model.add(tf.keras.layers.Dense(1))
    
    model.summary()
    
    model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), optimizer='adam', metrics=['accuracy'])    
    model.fit(train_dataset, epochs=args.epochs)
    
    test_loss, test_accuracy = model.evaluate(test_dataset)    
    print('test_loss', test_loss)
    print('test_accuracy', test_accuracy)

  Embedding Layer를 보시면 평소와 똑같지만 두가지 인자가 추가됐습니다. weights 인자를 통해서 fastText 모델의 값을 넣어주고요. trainable을 False로 해서 훈련 과정에서 Embedding Layer는 제외하도록 합니다.

  이런 방식으로 Tensorflow의 Embedding Layer를 그대로 사용하면서, 값 자체는 미리 훈련시켜서 성능이 더 좋은 데이터를 사용할 수 있습니다.

 

  그럼 오늘도 즐거운(?) 개발 생활 즐기시고, 더 즐거운 휴식도 즐기시길 바랍니다 ㅎㅎ