본문 바로가기
Drawing (AI)/DeepLearning

Word Embedding(2)

by 생각하는 이상훈 2023. 5. 14.
728x90

Skip-Gram with Negative Sampling (SGNS)

네거티브 샘플링(Negative Sampling)을 사용하는 Word2Vec을 직접 케라스(Keras)를 통해 구현해보고자 한다.

 

네거티브 샘플링 (Negative Sampling)

Word2Vec의 출력층에서는 소프트맥스 함수를 통과한 단어 집합 크기의 벡터와 실제값인 one-hot 벡터와의 오차를 구하고 이로부터 임베딩 테이블에 있는 모든 단어에 대한 임베딩 벡터 값을 업데이트합니다.

Word2Vec은 역전파 과정에서 모든 단어의 임베딩 벡터값의 업데이트를 수행하지만, 만약 현재 집중하고 있는 중심 단어와 주변 단어가 '강아지'와 '고양이', '귀여운'과 같은 단어라면, 이 단어들과 별 연관 관계가 없는 '돈가스'나 '컴퓨터'와 같은 수많은 단어의 임베딩 벡터값까지 업데이트하는 것은 비효율적다.

네거티브 샘플링은 Word2Vec이 학습 과정에서 전체 단어 집합이 아니라 일부 단어 집합에만 집중할 수 있도록 하는 방법이다. 가령, 현재 집중하고 있는 주변 단어가 '고양이', '귀여운'이라고 해보면 여기에 '돈가스', '컴퓨터', '회의실'과 같은 단어 집합에서 무작위로 선택된 주변 단어가 아닌 단어들을 일부 가져온다. 이렇게 하나의 중심 단어에 대해서 전체 단어 집합보다 훨씬 작은 단어 집합을 만들어놓고 마지막 단계를 이진 분류 문제로 변환한다. 주변 단어들을 긍정(positive), 랜덤으로 샘플링 된 단어들을 부정(negative)으로 레이블링한다면 이진 분류 문제를 위한 데이터셋이 된다. 이는 기존의 단어 집합의 크기만큼의 선택지를 두고 다중 클래스 분류 문제를 풀던 Word2Vec보다 훨씬 연산량에서 효율적이다.

 

기존의 Skip-gram과 그 데이터셋을 SGNS의 데이터 셋으로 바꾸는 과정을 보자.

위 그림에서 좌측은 기존의 Skip-gram 데이터셋이고 우측은 SGNS 학습을 위해 변형을 시킨 데이터셋이다.

위 예시를 보면 cat에 근접한 글자들만 데이터셋에 넣는 것이 아니라 상관없는 값들도 넣고 해당 두 단어는 관계가 없다는 것을 레이블을 0으로 설정함으로써 표현한다.

두 테이블 중 하나는 입력 1인 중심 단어의 테이블 룩업을 위한 임베딩 테이블이고, 하나는 입력 2인 주변 단어의 테이블 룩업을 위한 임베딩 테이블이다. 각 단어는 각 임베딩 테이블을 테이블 룩업하여 임베딩 벡터로 변환된다.

각 임베딩 테이블을 통해 테이블 룩업하여 임베딩 벡터로 변환되었다면 그 후의 연산은 다음과 같이 진행한다.

중심 단어와 주변 단어의 내적값을 이 모델의 예측값으로 하고, 레이블과의 오차로부터 역전파하여 중심 단어와 주변 단어의 임베딩 벡터값을 업데이트한다. 학습 후에는 좌측의 임베딩 행렬을 임베딩 벡터로 사용할 수도 있고, 두 행렬을 더한 후 사용하거나 두 행렬을 연결(concatenate)해서 사용할 수도 있다. 


실습 코드

데이터 전처리 과정

import pandas as pd
import numpy as np
import nltk
from nltk.corpus import stopwords
from sklearn.datasets import fetch_20newsgroups
from tensorflow.keras.preprocessing.text import Tokenizer

dataset = fetch_20newsgroups(shuffle=True, random_state=1, remove=('headers', 'footers', 'quotes'))
documents = dataset.data
print('총 샘플 수 :',len(documents))
# 총 샘플 수 : 11314

# 전처리
news_df = pd.DataFrame({'document':documents})
# 특수 문자 제거
news_df['clean_doc'] = news_df['document'].str.replace("[^a-zA-Z]", " ")
# 길이가 3이하인 단어는 제거 (길이가 짧은 단어 제거)
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: ' '.join([w for w in x.split() if len(w)>3]))
# 전체 단어에 대한 소문자 변환
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: x.lower())

# 현재 데이터프레임에 Null 값이 있는지 확인
news_df.isnull().values.any()
False
# Null 값이 없지만, 빈 값(empy) 유무도 확인해야함

# 모든 빈 값을 Null 값으로 변환하고, 다시 Null 값이 있는지 확인
news_df.replace("", float("NaN"), inplace=True)
news_df.isnull().values.any()
True

# Null 값 제거
news_df.dropna(inplace=True)
print('총 샘플 수 :',len(news_df))
#총 샘플 수 : 10995

# NLTK에서 정의한 불용어 리스트를 사용하여 불용어를 제거
stop_words = stopwords.words('english')
tokenized_doc = news_df['clean_doc'].apply(lambda x: x.split())
tokenized_doc = tokenized_doc.apply(lambda x: [item for item in x if item not in stop_words])
tokenized_doc = tokenized_doc.to_list()

# 단어가 1개 이하인 샘플의 인덱스를 찾아서 저장하고, 해당 샘플들은 제거.
drop_train = [index for index, sentence in enumerate(tokenized_doc) if len(sentence) <= 1]
tokenized_doc = np.delete(tokenized_doc, drop_train, axis=0)
print('총 샘플 수 :',len(tokenized_doc))
총 샘플 수 : 10940

# 샘플 수가 점차 줄었음 단어 집합 생성, 정수 인코딩 진행
tokenizer = Tokenizer()
tokenizer.fit_on_texts(tokenized_doc)

word2idx = tokenizer.word_index
idx2word = {value : key for key, value in word2idx.items()}
encoded = tokenizer.texts_to_sequences(tokenized_doc)

# 상위 2개의 샘플을 출력
print(encoded[:2])
[[9, 59, 603, 207, 3278, 1495, 474, 702, 9470, 13686, 5533, 15227, 702, 442, 702, 70, 1148, 1095, 1036, 20294, 984, 705, 4294, 702, 217, 207, 1979, 15228, 13686, 4865, 4520, 87, 1530, 6, 52, 149, 581, 661, 4406, 4988, 4866, 1920, 755, 10668, 1102, 7837, 442, 957, 10669, 634, 51, 228, 2669, 4989, 178, 66, 222, 4521, 6066, 68, 4295],
[1026, 532, 2, 60, 98, 582, 107, 800, 23, 79, 4522, 333, 7838, 864, 421, 3825, 458, 6488, 458, 2700, 4730, 333, 23, 9, 4731, 7262, 186, 310, 146, 170, 642, 1260, 107, 33568, 13, 985, 33569, 33570, 9471, 11491]]

# 단어 집합 size 확인
vocab_size = len(word2idx) + 1 
print('단어 집합의 크기 :', vocab_size)
# 단어 집합의 크기 : 64277

 

네거티브 샘플링을 통한 데이터셋 구성

# skipgrams를 사용하여 네거티브 샘플링을 통해 데이터셋을 구성하기 위한 코드
# 상위 10개의 뉴스그룹 샘플에 대해서만 수행
from tensorflow.keras.preprocessing.sequence import skipgrams
# 네거티브 샘플링
skip_grams = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded[:10]]

# 첫번째 샘플인 skip_grams[0] 내 skipgrams로 형성된 데이터셋 확인
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]))
'''          
(commited (7837), badar (34572)) -> 0
(whole (217), realize (1036)) -> 1
(reason (149), commited (7837)) -> 1
(letter (705), rediculous (15227)) -> 1
(reputation (5533), midonrnax (47527)) -> 0
'''

print('전체 샘플 수 :',len(skip_grams))
# 전체 샘플 수 : 10

# 첫번째 뉴스그룹 샘플에 대해서 생긴 pairs와 labels의 개수
print(len(pairs))
print(len(labels))
# 2220
# 2220

skip_grams = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded]

# SGNS 구현
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Embedding, Reshape, Activation, Input
from tensorflow.keras.layers import Dot
from tensorflow.keras.utils import plot_model
from IPython.display import SVG

# 하이퍼파라미터인 임베딩 벡터의 차원은 100으로 정하고, 두 개의 임베딩 층을 추가
embedding_dim = 100

# 중심 단어를 위한 임베딩 테이블
w_inputs = Input(shape=(1, ), dtype='int32')
word_embedding = Embedding(vocab_size, embedding_dim)(w_inputs)

# 주변 단어를 위한 임베딩 테이블
c_inputs = Input(shape=(1, ), dtype='int32')
context_embedding  = Embedding(vocab_size, embedding_dim)(c_inputs)

# 각 임베딩 테이블은 중심 단어와 주변 단어 각각을 위한 임베딩 테이블이며
# 각 단어는 임베딩 테이블을 거쳐서 내적을 수행하고,
# 내적의 결과는 1 또는 0을 예측하기 위해서 시그모이드 함수를 활성화 함수로 거쳐 최종 예측값을 얻음

dot_product = Dot(axes=2)([word_embedding, context_embedding])
dot_product = Reshape((1,), input_shape=(1, 1))(dot_product)
output = Activation('sigmoid')(dot_product)

model = Model(inputs=[w_inputs, c_inputs], outputs=output)
model.summary()
model.compile(loss='binary_crossentropy', optimizer='adam')
plot_model(model, to_file='model3.png', show_shapes=True, show_layer_names=True, rankdir='TB')

# 5 epoches
for epoch in range(1, 6):
    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)  
    print('Epoch :',epoch, 'Loss :',loss)
    
Epoch: 1 Loss: 4339.997158139944
Epoch: 2 Loss: 3549.69356325455
Epoch: 3 Loss: 3295.072506020777
Epoch: 4 Loss: 3038.1063768607564
Epoch: 5 Loss: 2790.9479411702487

결과 확인

import gensim

f = open('vectors.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()

# 모델 로드
w2v = gensim.models.KeyedVectors.load_word2vec_format('./vectors.txt', binary=False)

w2v.most_similar(positive=['soldiers'])
 [('lebanese', 0.7539176940917969),
 ('troops', 0.7515299916267395),
 ('occupying', 0.7322258949279785),
 ('attacking', 0.7247686386108398),
 ('villagers', 0.7217503786087036),
 ('israeli', 0.7071422338485718),
 ('villages', 0.7000206708908081),
 ('wounded', 0.6976917386054993),
 ('lebanon', 0.6933401823043823),
 ('arab', 0.692956268787384)]
 
w2v.most_similar(positive=['doctor'])
[('nerve', 0.6576169729232788),
 ('migraine', 0.6502577066421509),
 ('patient', 0.6377599835395813),
 ('disease', 0.6300654411315918),
 ('quack', 0.6101700663566589),
 ('cardiac', 0.606243371963501),
 ('infection', 0.6030253171920776),
 ('medication', 0.6001783013343811),
 ('suffering', 0.593578040599823),
 ('hurt', 0.5818471908569336)]
 
w2v.most_similar(positive=['police'])
 [('prohibit', 0.6182408332824707),
 ('provisions', 0.5706381797790527),
 ('cops', 0.565453290939331),
 ('army', 0.563193142414093),
 ('possess', 0.5538119673728943),
 ('armed', 0.5535427331924438),
 ('rkba', 0.5533647537231445),
 ('ksanti', 0.5518242716789246),
 ('courts', 0.5495947599411011),
 ('officers', 0.5477950572967529)]
 
w2v.most_similar(positive=['knife'])
[('knives', 0.7748741507530212),
 ('caucasus', 0.7227305769920349),
 ('defence', 0.7217429280281067),
 ('males', 0.7207540273666382),
 ('heretics', 0.7145630717277527),
 ('azerbaijanis', 0.7136125564575195),
 ('advocate', 0.7055186629295349),
 ('officers', 0.7020978927612305),
 ('punished', 0.7012225389480591),
 ('taxation', 0.7001351118087769)]
 
w2v.most_similar(positive=['engine'])
[('brakes', 0.7013274431228638),
 ('cylinder', 0.6680346727371216),
 ('brake', 0.6459399461746216),
 ('seat', 0.6365581154823303),
 ('gasoline', 0.6263373494148254),
 ('honda', 0.611443281173706),
 ('mounted', 0.6093355417251587),
 ('ventilator', 0.5999234318733215),
 ('adjustable', 0.5938659310340881),
 ('propellants', 0.5935063362121582)]

 

728x90

'Drawing (AI) > DeepLearning' 카테고리의 다른 글

딥러닝 직접 구현하기 - (신경망)  (0) 2023.07.01
딥러닝 직접 구현하기 - (퍼셉트론)  (0) 2023.06.21
Word Embedding(1)  (2) 2023.05.11
Long Short-Term Memory (LSTM)  (0) 2023.05.05
Recurrent Neural Network  (0) 2023.05.02