by museonghwang

Skip-Gram with Negative Sampling

|

  1. Skip-Gram
  2. Skip-Gram with Negative Sampling(SGNS)
  3. Skip-Gram with Negative Sampling(SGNS) example
  4. 네거티브 샘플링을 이용한 Word2Vec 구현
    • 4.1 20뉴스그룹 데이터 전처리하기
    • 4.2 네거티브 샘플링을 통한 데이터셋 구성하기
    • 4.3 Skip-Gram with Negative Sampling(SGNS) 구현하기
    • 4.4 결과 확인하기
  5. SGNS Summaray


Negative Sampling 방법은 Word2Vec의 CBOW와 Skip-gram 모두 단어 개수가 많아질수록 계산 복잡도가 증가하여 연산 속도가 저하된다는 한계점을 보완하기 위해 제안되었습니다. CBOW와 Skip-gram는 역전파 과정에서 단어 집합의 크기만큼 연산이 필요합니다. 따라서 단어 개수가 많아질수록 계산 복잡도 역시 높아지고, 이는 모델 학습 속도 저하를 유발합니다. 즉, 기존 Word2Vec 방식은 모든 단어의 임베딩 벡터값을 업데이트합니다. Negative Sampling은 이러한 계산 복잡도를 줄이기 위해 고안된 효율적인 기술입니다.



1. Skip-Gram

Word2Vec 의 학습방법 중 CBOW 에서는 주변 단어를 통해 중심 단어를 예측했다면, Skip-gram중심 단어로부터 주변 단어를 예측 합니다. 인공 신경망을 도식화해보면 아래와 같습니다.

image


윈도우 크기가 2일 때, Skip-gram은 다음과 같이 데이터셋을 구성합니다.

image

image


Skip-gram중심 단어로부터 주변 단어를 예측 합니다.

image


image


Skip-gram은 입력층, 투사층, 출력층 3개의 층으로 구성된 신경망이며, 소프트맥스 함수를 지난 예측값(Prediction)과 실제값으로부터 오차(error)를 구합니다.

image


소프트맥스 함수를 지난 예측값(Prediction)과 실제값으로부터 오차(error)를 구하고, 이로부터 embedding table을 update 합니다.

image


실제로는 CBoWSkip-gram 을 방금 설명한 바와 같이 구현하지는 않습니다. 그 이유는 아래와 같이 구현한다면 속도가 너무 느리기 때문 입니다.

image


2. Skip-Gram with Negative Sampling(SGNS)

Word2Vec 의 출력층에서는 소프트맥스 함수를 지난 단어 집합 크기의 벡터와 실제값인 원-핫 벡터와의 오차를 구하고 이로부터 임베딩 테이블에 있는 모든 단어에 대한 임베딩 벡터 값을 업데이트합니다. 만약 단어 집합의 크기가 수만 이상에 달한다면 이 작업은 굉장히 무거운 작업 이므로, Word2Vec 은 꽤나 학습하기에 무거운 모델이 됩니다. 즉, 단어 집합의 크기에 대해서 softmax + cross entropy 연산은 너무 Heavy 합니다.

image


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

네거티브 샘플링(Negative Sampling)Word2Vec가 학습 과정에서 전체 단어 집합이 아니라 일부 단어 집합에만 집중할 수 있도록 하는 방법 으로, 다중 클래스 분류를 이진 분류 문제로 바꾸므로서 연산량을 획기적으로 줄입니다.

image


즉, Skip-gram은 주변 단어로부터 중심 단어를 예측하는 모델이었다면, 이제 중심 단어와 주변 단어의 내적으로부터 어떤 값을 예측하는 모델로 변경하는 것 입니다.

우선, 기존의 Skip-gram 의 데이터셋부터 변경할 필요가 있습니다. 중심 단어를 입력, 주변 단어를 레이블로 하는 데이터셋의 형식을 변경해줍니다. 주변 단어와 중심 단어 데이터셋에 True를 의미하는 레이블 1을 할당해줍니다.

image


이 모델의 목적은 중심 단어와 주변 단어를 입력으로 하였을 때, 실제로 이 두 단어의 관계가 이웃(neighbors)의 관계 인지를 예측하는 것 입니다.

image


주변 단어와 중심 단어 데이터셋에 True를 의미하는 레이블 1을 할당해주었는데, 실제로 이웃 관계이므로 이들의 레이블은 1을 할당해주는 것이 맞습니다.

이제 거짓을 의미하는 샘플들도 추가해주어야 한다. 이를 Negative Sample 이라고 한다. Negative Sample 은 전체 데이터셋에서 랜덤으로 추가해줍니다.

image


이제 모델 구조도 이전과 달라집니다. 다음과 같이 두 개의 Embedding table을 준비하는데, 한 테이블은 중심 단어, 한 테이블은 주변 단어를 위한 테이블입니다.

image


image


중심 단어와 주변 단어의 내적으로부터 실제값인 1 또는 0을 예측하고, 실제값과의 오차(error)를 계산하여, 역전파를 통해 두 개의 테이블을 업데이트합니다.

image


3. Skip-Gram with Negative Sampling(SGNS) example

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

실습 전 한번 더 친숙한 예제로 개념을 살펴보겠습니다. 다음과 같은 예문이 있다고 가정하겠습니다.

\["The\ \ fat\ \ cat\ \ sat\ \ on\ \ the\ \ mat"\]


위 예문에 대해서 동일하게 윈도우 크기가 2일 때, 데이터셋은 다음과 같이 구성됩니다.

image


Skip-gram중심 단어로부터 주변 단어를 예측하는 모델 이었습니다. 위와 같은 문장이 있다고 한다면, Skip-gram 은 중심 단어 cat으로부터 주변 단어 The, fat, sat, on을 예측합니다. 기존의 Skip-gram 모델을 일종의 주황 박스로 생각해본다면, 아래의 그림과 같이 입력은 중심 단어, 모델의 예측은 주변 단어인 구조입니다.

image


하지만 네거티브 샘플링을 사용하는 Skip-gram(Skip-Gram with Negative Sampling, SGNS)은 이와는 다른 접근 방식을 취합니다. SGNS다음과 같이 중심 단어와 주변 단어가 모두 입력이 되고, 이 두 단어가 실제로 윈도우 크기 내에 존재하는 이웃 관계인지 그 확률을 예측 합니다.

image


기존의 Skip-gram 데이터셋을 SGNS의 데이터셋으로 바꾸는 과정을 보겠습니다.

image


위의 그림에서 좌측의 테이블은 기존의 Skip-gram 을 학습하기 위한 데이터셋입니다. Skip-gram 은 기본적으로 중심 단어를 입력, 주변 단어를 레이블로 합니다. 하지만 SGNS 를 학습하고 싶다면, 이 데이터셋을 우측의 테이블과 같이 수정할 필요가 있습니다. 우선, 기존의 Skip-gram 데이터셋에서 중심 단어와 주변 단어를 각각 입력1, 입력2로 둡니다. 이 둘은 실제로 윈도우 크기 내에서 이웃 관계였므로 레이블은 1로 합니다. 이제 레이블이 0인 샘플들을 준비할 차례입니다.

image


실제로는 입력1(중심 단어)와 주변 단어 관계가 아닌 단어들을 입력2로 삼기 위해서 단어 집합에서 랜덤으로 선택한 단어들을 입력2로 하고, 레이블을 0으로 합니다. 이제 이 데이터셋은 입력1과 입력2가 실제로 윈도우 크기 내에서 이웃 관계인 경우에는 레이블이 1, 아닌 경우에는 레이블이 0인 데이터셋 이 됩니다.

그리고 이제 두 개의 임베딩 테이블을 준비합니다. 두 임베딩 테이블은 훈련 데이터의 단어 집합의 크기를 가지므로 크기가 같습니다.

image


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

image


각 임베딩 테이블을 통해 테이블 룩업하여 임베딩 벡터로 변환되었다면 그 후의 연산은 매우 간단합니다.

image


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

아래의 실습에서는 좌측의 행렬을 사용하는 방식을 택했습니다.


4. 네거티브 샘플링을 이용한 Word2Vec 구현

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


4.1 20뉴스그룹 데이터 전처리하기

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


20뉴스그룹 데이터를 사용하겠습니다. 이번 실습에서는 하나의 샘플에 최소 단어 2개는 있어야 합니다. 그래야만 중심 단어, 주변 단어의 관계가 성립하며 그렇지 않으면 샘플을 구성할 수 없어 에러가 발생합니다. 전처리 과정에서 지속적으로 이를 만족하지 않는 샘플들을 제거하겠습니다.

dataset = fetch_20newsgroups(
    shuffle=True,
    random_state=1,
    remove=('headers', 'footers', 'quotes')
)

documents = dataset.data
print('총 샘플 수 :',len(documents))
[output]
총 샘플 수 : 11314


총 샘플 수는 11,314개입니다. 전처리를 진행하겠습니다. 불필요한 토큰을 제거하고, 소문자화를 통해 정규화를 진행합니다.

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())

news_df.head()

image


현재 데이터프레임에 Null 값이 있는지 확인하겠습니다.

news_df.isnull().values.any()
[output]
False


Null 값이 없지만, 빈 값(empy) 유무도 확인해야 합니다. 모든 빈 값을 Null 값으로 변환하고, 다시 Null 값이 있는지 확인합니다.

news_df.replace("", float("NaN"), inplace=True)
news_df.isnull().values.any()
[output]
True


Null 값이 있음을 확인했습니다. Null 값을 제거합니다.

news_df.dropna(inplace=True)
print('총 샘플 수 :', len(news_df))
[output]
총 샘플 수 : 10995


샘플 수가 일부 줄어든 것을 확인할 수 있습니다. NLTK에서 정의한 불용어 리스트를 사용하여 불용어를 제거합니다.

stop_words = stopwords.words('english') # NLTK로부터 불용어를 받아옵니다.
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개 이하인 경우를 모두 찾아 제거하겠습니다.

# 단어가 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))
[output]
총 샘플 수 : 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[:5])
[output]
[[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],
 [262, 1036, 2223, 7839, 387, 1, 36, 3, 4, 69, 345, 901, 944, 20, 709, 6, 1662, 24704, 20295, 223, 40, 409, 52, 170, 585, 345, 189, 901, 944, 9, 1036, 1, 24, 901, 944, 1188, 222, 42, 125, 3279, 20295, 223, 1, 1037, 66, 3, 3278, 641, 295, 116, 8994, 1027, 258, 604, 218, 135, 3280, 71, 12465, 11492, 223], [8530, 1430, 11493, 1241, 13, 185, 42, 605, 271, 4627, 958, 340, 1921, 191, 3517, 2071, 33571, 51, 1514, 363, 1674, 3050, 20296, 33572, 8165, 340, 92, 113, 1328, 277, 1308, 62, 279, 6067, 3135, 3462, 548, 722, 35, 1420, 1269, 1128, 381, 75, 310, 1155, 25, 109, 69, 30, 4121, 718, 410, 255, 85, 512, 5892, 9472, 4523, 11, 2581, 1751, 61, 33573, 5112, 20297], [9, 185, 1531, 2204, 2517, 729, 7, 18, 303, 121, 1531, 479, 2413, 260, 1593, 310, 10, 2134, 6489, 1261, 6490, 6733, 55, 4296, 397, 5534]]


단어 집합의 크기를 확인하겠습니다.

vocab_size = len(word2idx) + 1 
print('단어 집합의 크기 :', vocab_size)
[output]
단어 집합의 크기 : 64277


총 64,277개의 단어가 존재합니다.


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

토큰화, 정제, 정규화, 불용어 제거, 정수 인코딩까지 일반적인 전처리 과정을 거쳤습니다. 네거티브 샘플링을 통한 데이터셋을 구성할 차례입니다. 이를 위해서는 네거티브 샘플링을 위해서 케라스에서 제공하는 전처리 도구인 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]]


결과를 확인합니다. 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]))
[output]
(media (702), reputation (5533)) -> 1
(away (178), accelerates (31494)) -> 0
(lived (1148), media (702)) -> 1
(seem (207), disagree (1495)) -> 1
(soldiers (957), lineno (8526)) -> 0


윈도우 크기 내에서 중심 단어, 주변 단어의 관계를 가지는 경우에는 1의 레이블을 갖도록 하고, 그렇지 않은 경우는 0의 레이블을 가지도록 하여 데이터셋을 구성합니다. 이 과정은 각각의 뉴스그룹 샘플에 대해서 동일한 프로세스로 수행됩니다.

print(len(skip_grams))
[output]
10


encoded 중 상위 10개의 뉴스그룹 샘플에 대해서만 수행하였으므로 10이 출력됩니다. 그리고 10개의 뉴스그룹 샘플 각각은 수많은 중심 단어, 주변 단어의 쌍으로 된 샘플들을 갖고 있습니다. 첫번째 뉴스그룹 샘플이 가지고 있는 pairs와 labels의 개수를 출력해봅시다.

# 첫번째 샘플에 대해서 생긴 pairs와 labels
print(len(pairs))
print(len(labels))
[output]
2220
2220


이제 이 작업을 모든 뉴스그룹 샘플에 대해서 수행하겠습니다.

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


4.3 Skip-Gram with Negative Sampling(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='skip_gram.png', show_shapes=True, show_layer_names=True, rankdir='TB')
[output]
Model: "model"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_3 (InputLayer)           [(None, 1)]          0           []                               
                                                                                                  
 input_4 (InputLayer)           [(None, 1)]          0           []                               
                                                                                                  
 embedding_2 (Embedding)        (None, 1, 100)       6427700     ['input_3[0][0]']                
                                                                                                  
 embedding_3 (Embedding)        (None, 1, 100)       6427700     ['input_4[0][0]']                
                                                                                                  
 dot_2 (Dot)                    (None, 1, 1)         0           ['embedding_2[0][0]',            
                                                                  'embedding_3[0][0]']            
                                                                                                  
 reshape_1 (Reshape)            (None, 1)            0           ['dot_2[0][0]']                  
                                                                                                  
 activation_1 (Activation)      (None, 1)            0           ['reshape_1[0][0]']              
                                                                                                  
==================================================================================================
Total params: 12,855,400
Trainable params: 12,855,400
Non-trainable params: 0
__________________________________________________________________________________________________

image


모델의 학습은 9에포크 수행하겠습니다.

for epoch in range(1, 10):
    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)
[output]
Epoch : 1 Loss : 4626.981844887137
Epoch : 2 Loss : 3665.3362488895655
Epoch : 3 Loss : 3505.8999227024615
Epoch : 4 Loss : 3313.1815411988646
Epoch : 5 Loss : 3090.51806515269
Epoch : 6 Loss : 2854.1424025101587
Epoch : 7 Loss : 2647.802926401724
Epoch : 8 Loss : 2477.3305542107555
Epoch : 9 Loss : 2327.644738064613


4.4 결과 확인하기

학습된 모델의 결과를 확인해보겠습니다. 학습된 임베딩 벡터들을 vector.txt 에 저장합니다. 그 후 이를 gensimmodels.KeyedVectors.load_word2vec_format() 으로 로드하면 쉽게 단어 벡터 간 유사도를 구할 수 있습니다.

import gensim

f = open('vectors.txt' ,'w')
f.write('{} {}\n'.format(vocab_size-1, embedding_dim))
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=['disease'])
[output]
[('infection', 0.6975768804550171),
 ('intestinal', 0.6435999870300293),
 ('inflammation', 0.6353795528411865),
 ('diseases', 0.6294456124305725),
 ('patients', 0.6278392672538757),
 ('candida', 0.6135035157203674),
 ('systemic', 0.6041629910469055),
 ('chronic', 0.5998706221580505),
 ('migraine', 0.5968449711799622),
 ('symptoms', 0.5910621881484985)]


w2v.most_similar(positive=['soldiers'])
[output]
[('massacred', 0.7330883145332336),
 ('civilians', 0.726263701915741),
 ('shelling', 0.7134998440742493),
 ('refugees', 0.712714672088623),
 ('brutally', 0.7074486613273621),
 ('towns', 0.6952769160270691),
 ('massacre', 0.6930119395256042),
 ('snipers', 0.6848074793815613),
 ('azarbaijan', 0.6819743514060974),
 ('fighters', 0.6770718097686768)]


w2v.most_similar(positive=['police'])
[output]
[('damages', 0.5372452735900879),
 ('court', 0.5066943764686584),
 ('brutality', 0.49538081884384155),
 ('democratic', 0.4885564148426056),
 ('illegally', 0.4825240671634674),
 ('deprivation', 0.4776268005371094),
 ('authorize', 0.4710679054260254),
 ('enemy', 0.4705543518066406),
 ('refusal', 0.46288490295410156),
 ('rifles', 0.4595275819301605)]


w2v.most_similar(positive=['hero'])
[output]
[('betrayed', 0.6657158732414246),
 ('districts', 0.6575151085853577),
 ('nichols', 0.6299615502357483),
 ('harassed', 0.628355860710144),
 ('racked', 0.6282691359519958),
 ('tzeghagrons', 0.6266265511512756),
 ('hairenik', 0.6266023516654968),
 ('caucasus', 0.619964063167572),
 ('yerevan', 0.6187404990196228),
 ('laughter', 0.6154467463493347)]


w2v.most_similar(positive=['engine'])
[output]
[('sharpened', 0.5512747168540955),
 ('pathfinder', 0.5506775975227356),
 ('inline', 0.5288227200508118),
 ('shocks', 0.5259482264518738),
 ('rear', 0.4990272521972656),
 ('brake', 0.47791412472724915),
 ('rebuilt', 0.47202664613723755),
 ('slip', 0.4708540439605713),
 ('trunk', 0.47066107392311096),
 ('fairing', 0.4700755178928375)]


w2v.most_similar(positive=['doctor'])
[output]
[('patient', 0.5729305744171143),
 ('disease', 0.5609087347984314),
 ('intestinal', 0.532063901424408),
 ('infection', 0.5251743197441101),
 ('sumatriptin', 0.514649510383606),
 ('infections', 0.5072507262229919),
 ('seizures', 0.5061989426612854),
 ('antibiotics', 0.4987868666648865),
 ('patients', 0.497864693403244),
 ('migraine', 0.4913448691368103)]


5. SGNS Summaray

  • Embedding vector의 차원을 정하는 것은 결국 사용자의 몫입니다.
  • CBoW보다는 SGNS(Skipgram with Negative Sampling)이 가장 많이 선호됩니다.
  • 작은 윈도우 크기(2~7)를 가질수록, 상호 교환할 수 있을 정도의 높은 유사도를 가집니다.
    • 여기서 상호 교환이 가능하다는 것은 어쩌면 반의어도 포함될 수 있습니다.
    • 예를 들어, 친절한 ↔ 불친절한
  • 반면, 커다란 윈도우 크기(7~25)는 관련 있는 단어들을 군집하는 효과를 가집니다.

image


  • 또한 데이터셋을 위한 Negative Sampling의 비율 또한 성능에 영향을 주는 또 다른 결정요소입니다.
  • 논문에서는 5-20을 최적의 숫자로 정의하고 있습니다.
  • 데이터가 방대하다면 2-5로 충분합니다.

image