본문 바로가기
Computer/ML·DL·NLP

[이수안컴퓨터연구소] 케라스 Word2Vec Skipgram, CBOW 구현

by injeolmialmond 2022. 1. 31.

 

케라스 Word2Vec Skipgram, CBOW 구현 - YouTube

전처리

sklearn.datasets.fetch_20newsgroups — scikit-learn 1.0.2 documentation

 

sklearn.datasets.fetch_20newsgroups

Examples using sklearn.datasets.fetch_20newsgroups: Biclustering documents with the Spectral Co-clustering algorithm Biclustering documents with the Spectral Co-clustering algorithm, Topic extracti...

scikit-learn.org

참고! 뉴스에는 헤더 / 푸터 / 인용구가 들어가는데, 이런 부분을 제외하고자 한다면 remove에 튜플로 넣어서 제외 가능.

# 데이터 & 전처리
from sklearn.datasets import fetch_20newsgroups

dataset = fetch_20newsgroups(shuffle=True, random_state=0, remove=('headers', 'footers', 'quotes'))
documents = dataset.data

print(len(documents))
documents[0] # 이메일주소, 개행문자, 숫자가 있으니 전처리 해주자!
import re
import nltk

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

nltk.download('stopwords')
nltk.download('punkt')

def clean_text(d): # 문자열만 추출
    pattern = r'[^a-zA-Z\s]'
    text = re.sub(pattern, '', d)
    return text

def clean_stopword(d):
    stop_words = stopwords.words('english')
    return ' '.join([w.lower() for w in d.split() if w.lower() not in stop_words and len(w) > 3])

def tokenize(d):
    return word_tokenize(d)
import pandas as pd

news_df = pd.DataFrame({'article':documents})
len(news_df) # 11314
news_df.replace('', float("NaN"), inplace=True)
news_df.dropna(inplace=True)
len(news_df) # 11096
news_df['article'] = news_df['article'].apply(clean_text)
news_df['article'] = news_df['article'].apply(clean_stopword)
news_df

news_df = news_df.reset_index()
del news_df['index']
news_df

tokenized_news = news_df['article'].apply(tokenize)
tokenized_news =tokenized_news.to_list()

import numpy as np

drop_news = [index for index, sentence in enumerate(tokenized_news) if len(sentence) <= 1] # 짧은 문장 인덱스리스트
news_texts = np.delete(tokenized_news, drop_news, axis = 0) # 인덱스리스트에 있는 경우 지워라
print(len(news_texts)) # 10939
# 단어 사전 만들기
from tensorflow.keras.preprocessing.text import Tokenizer

news_2000 = news_texts[:2000]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(news_2000)

idx2word = {value:key for key, value in tokenizer.word_index.items()}
sequences = tokenizer.texts_to_sequences(news_2000)
vocab_size = len(tokenizer.word_index) + 1
print(vocab_size) # 31365

Skipgram

Skipgram 전처리

  • 네거티브 샘플링(Negative Sampling)
    • Word2Vec은 출력층이 내놓는 값에 소프트맥스 함수를 적용해 확률값으로 변환한 후 이를 정답과 비교해 역전파(backpropagation)
    • 소프트맥스를 적용하려면 분모에 해당하는 값, 즉 중심단어와 나머지 모든 단어의 내적을 한 뒤, 이를 다시 exp 계산을 하는데 전체 단어가 많을 경우 엄청난 계산량 발생
    • 네거티브 샘플링은 소프트맥스 확률을 구할 때 전체 단어를 대상으로 구하지 않고, 일부 단어만 뽑아서 계산을 하는 방식
    • 네거티브 샘플링 동작은 사용자가 지정한 윈도우 사이즈 내에 등장하지 않는 단어(negative sample)를 5~20개 정도 뽑고, 이를 정답단어와 합쳐 전체 단어처럼 소프트맥스 확률을 계산하여 파라미터 업데이트
from tensorflow.keras.preprocessing.sequence import skipgrams
skip_grams = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in sequences[:10]] # 10개 단어 샘플!
pairs, labels = skip_grams[0][0], skip_grams[0][1]
for i in range(5):
    print("{:s}({:d}), {:s}({:d}) -> {:d}".format(
    idx2word[pairs[i][0]], pairs[i][0],
    idx2word[pairs[i][1]], pairs[i][1],
    labels[i]))
    
# held(912), quote(939) -> 1
# charismatic(8463), moteccinfops(22899) -> 0
# generally(913), fuer(15246) -> 0
# christ(416), opposed(1480) -> 1
# majority(1188), bosch(10313) -> 0
print(len(skip_grams), len(pairs), len(labels)) # 10 5020 5020
skip_grams = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in sequences]
# skipgram 모델 구성
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Embedding, Reshape, Activation, Input, Dot
from tensorflow.keras.utils import plot_model
embed_size = 50

def word2vec():
    target_inputs = Input(shape=(1,), dtype='int32')
    target_embedding = Embedding(vocab_size, embed_size)(target_inputs)
    
    context_inputs = Input(shape=(1,), dtype='int32')
    context_embedding = Embedding(vocab_size, embed_size)(context_inputs)
    
    dot_product = Dot(axes=2)([target_embedding, context_embedding])
    dot_product = Reshape((1,), input_shape=(1,1))(dot_product)
    output = Activation('sigmoid')(dot_product)
    
    model = Model(inputs=[target_inputs, context_inputs], outputs=output)
    model.compile(loss='binary_crossentropy', optimizer='adam')
    
    return model

skip-gram 모델 구성

model = word2vec()
model.summary()
plot_model(model, show_shapes=True, show_layer_names=True)

for epoch in range(1,11):
    loss = 0
    for _, elem in enumerate(skip_grams):
        first_elem = np.array(list(zip(*elem[0]))[0], dtype='int32')
        second_elem = np.array(list(zip(*elem[0]))[1], dtype='int32')
        labels = np.array(elem[1], dtype='int32')
        X = [first_elem, second_elem] # 학습데이터
        Y = labels # 정답
        loss += model.train_on_batch(X,Y) # 한번의 epoch에 여러 번 트레이닝
        
    print('Epoch : ', epoch, 'Loss : ', loss)

성능이.. 별로 안좋네용...

import gensim

f = open('skipgram.txt', 'w')
f.write('{} {}\n'.format(vocab_size-1, embed_size))
vectors = model.get_weights()[0]
for word, i in tokenizer.word_index.items():
    f.write('{} {}\n'.format(word, ' '.join(map(str, list(vectors[i,:])))))
f.close()

skipgram = gensim.models.KeyedVectors.load_word2vec_format('skipgram.txt', binary=False)

CBOW

CBOW 전처리

skipgram 전처리했던 것 cbow로 변환시켜서 사용할게용

# CBOW

def skipgram2cbow(skipgrams):
    cbows = []
    flag = 0
    for n in skip_grams:
        temp1 = []
        for t in n :
            if flag == 0:
                temp1.append(t)
            else:
                flag = 1
                tmp2 = []
                for x in t:
                    temp2.append([x[1], x[0]])
                temp1.append(temp2)
        cbows.append(temp1)
    return cbows
cbows = skipgram2cbow(skip_grams)
pairs, labels = cbows[0][0], cbows[0][1]
for i in range(5):
    print('{:s}({:d}), {:s}({:d}) -> {:d}'.format(
    idx2word[pairs[i][0]], pairs[i][0],
    idx2word[pairs[i][1]], pairs[i][1],
    labels[i]))

print(len(cbows), len(pairs), len(labels)) # 2000 5020 5020

skip-gram : 타겟 단어로 문맥 맞추기

cbow : 문맥 가지고 타겟 단어 맞추기

-> cbow가 skip-gram 보다 훨씬 간단한 문제. 따라서 skip-gram보다 훨씬 더 많이 학습시켜도 시간은 비슷할듯

 

CBOW 모델 구성

model = word2vec()
model.summary()
plot_model(model, show_shapes=True, show_layer_names=True)
for epoch in range(1,11):
    loss = 0
    for _, elem in enumerate(cbows):
        first_elem = np.array(list(zip(*elem[0]))[0], dtype='int32')
        second_elem = np.array(list(zip(*elem[0]))[1], dtype='int32')
        labels = np.array(elem[1], dtype='int32')
        X = [first_elem, second_elem] # 학습데이터
        Y = labels # 정답
        loss += model.train_on_batch(X,Y) # 한번의 epoch에 여러 번 트레이닝
        
    print('Epoch : ', epoch, 'Loss : ', loss)

import gensim

f = open('cbow.txt', 'w')
f.write('{} {}\n'.format(vocab_size-1, embed_size))
vectors = model.get_weights()[0]
for word, i in tokenizer.word_index.items():
    f.write('{} {}\n'.format(word, ' '.join(map(str, list(vectors[i,:])))))
f.close()

cbow = gensim.models.KeyedVectors.load_word2vec_format('cbow.txt', binary=False)

댓글