이번 글에서는 Word2Vec을 이용한 워드 임베딩을 구현해 볼 것이다.
1. 영어 워드 임베딩 구축
우선 scikit-learn에서 제공하는 데이터 set을 사용할 예정이다.
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_20newsgroups
dataset = fetch_20newsgroups(shuffle=True, random_state=1, remove=('headers', 'footers', 'quotes'))
dataset = dataset.data
news_df = pd.DataFrame({'document':dataset})
news_df
위의 데이터셋에는 총 11314개의 데이터가 포함되어 있다.
1-1. 데이터 전처리
데이터의 전처리를 진행한 후 임베딩을 진행해야 성능이 향상될 것이기 때문에 불필요한 토큰 제거 및 소문자화 등 간단한 전처리를 진행하였다.
# 결측치 확인
news_df.replace("", float("NaN"), inplace=True)
news_df = news_df.dropna().reset_index(drop=True)
print(f"필터링된 데이터셋 총 개수 : {len(news_df)}")
# >> 11096
# 중복제거
processed_news_df = news_df.drop_duplicates(['document']).reset_index(drop=True)
processed_news_df
우선 공백을 nan으로 치환한 뒤, 결측값을 모두 drop 해줬고, 중복된 row들도 제거해 줬다.
다음으로 데이터 내의 특수문자를 정규표현식을 이용하여 제거해 주고,
이번 실습에서 사용하는 Word2Vec의 Skip-Gram는 각 데이터에 2개 이상의 단어가 있어야 한다. 그래야 중심단어와 타깃단어를 정의할 수 있어야 에러가 생기기 않는다. 따라서 미리 2개 이하의 단어를 가진 데이터를 제거해 줄 것이다.
마지막으로 문장의 전체 길이가 200 이하이거나, 전체 단어 개수가 5개 이하인 데이터를 필터링해주고 문장 전체를 소문자로 바꿔줬다.
processed_news_df['document'] = processed_news_df['document'].apply(lambda x: x.replace("[^a-zA-Z]", " "))
processed_news_df['document'] = processed_news_df['document'].apply(lambda x: ' '.join([token for token in x.split() if len(token) > 2]))
processed_news_df = processed_news_df[processed_news_df.document.apply(lambda x: len(str(x)) <= 200 and len(str(x).split()) > 5)].reset_index(drop=True)
processed_news_df['document'] = processed_news_df['document'].apply(lambda x: x.lower())
processed_news_df
다음으로 nltk 라이브러리를 이용하여 불용어를 제거해 줄 것이다.
우선 영어 불용어를 불러오고, 각 문서를 공백을 기준으로 끊어주고 불용어에 해당되지 않은 단어들만 `tokenized_doc`에 모아준다.
import nltk
from nltk.corpus import stopwords
nltk.download('stopwords')
stop_words = stopwords.words('english')
tokenized_doc = processed_news_df['document'].apply(lambda x: x.split())
tokenized_doc = tokenized_doc.apply(lambda x: [s_word for s_word in x if s_word not in stop_words])
tokenized_doc
1-2. 단어 토큰화
토큰화하기 이전에 이번 실습에서는 Word2Vec의 Skip-Gram을 사용할 건데, 이를 사용하기 위해서는 각 데이터에 2개 이상의 단어가 있어야 한다. 그래야 중심단어와 타겟단어를 정의할 수 있어 에러가 생기기 않는다.
따라서 미리 2개 이하의 단어를 가진 데이터를 한 번 더 제거해 줄 것이다.
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))
# >> 2235
다음으로 keras의 tokenizer을 이용해서 단어들을 토큰화할 것이다.
from tensorflow.keras.preprocessing.text import Tokenizer
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)
위에서 word2idx는 각 단어에 정수를 매핑해 주고, idx2word에 voca가 만들어진다.
마지막으로 encoded에는 각 문장이 어떤 벡터로 구성되었는지를 확인할 수 있다.
1-3. negative sampling
이렇게 정제(cleaning)와 정규화(normalization), 토큰화(tokenization)를 진행하고 Word2Vec 모델에 negative sampling을 적용하여 모델링을 해볼 것이다.
from tensorflow.keras.preprocessing.sequence import skipgrams
training_dataset = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded[:1000]]
위에서 어떤 단어를 넣을지(sample), vocabsize, 윈도 사이즈를 정해줘야 한다.
negative sampling 방법을 이용하였기 때문에 윈도우 크기 내에서 중심단어와 주변단어의 관계를 가지는 경우=positive sample에는 label이 1로 되어있고, 그렇지 않은 경우는 라벨이 0으로 되어있다.
이를 확인해 보면 다음과 같이 같은 라벨링이 되어있는 것을 볼 수 있다.
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])
)
1-4. Skip-gram with Negative Sampling
이제 negative sampling 된 데이터를 통해 학습하는 skip-gram 즉, 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
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)
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')
우선 임베딩 차원을 100으로 설정하고, 두 개의 임베딩 층을 추가하였다.
데이터 셋 내의 각각의 단어가 임베딩 행렬을 거쳐 나온 벡터를 활용하여 두 내적을 계산하고 그 내적값이 1, 0이 되도록 sigmoid함수를 활성화 함수로 거쳐 최종 예측값을 얻는다.
for epoch in range(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 + 1, 'Loss :',loss)
이후 모델을 이용하여 학습을 시켜준다.
1-5. 임베딩 품질 확인
이렇게 학습된 모델의 결과를 gensim의 함수를 이용하여 단어 벡터 간 유사도를 구할 수 있다.
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=['apple'])
우선 임베딩 된 벡터들을 임시 파일에 저장한 뒤, 해당 파일에서 벡터들을 불러와 유사도를 계산하게 된다.
사실 위의 실습에서는 epoch를 적게 학습하였기 때문에 유사한 결과가 나오지는 않았다.
그러나 이러한 방식으로 유사한 상위 벡터들을 뽑을 수 있다.
2. 한국어 워드 임베딩 구축 및 시각화
이번에는 한국어 데이터 셋을 이용하여 워드 임베딩과 시각화까지 이용하여 쉽게 확인하는 것을 시도해 보겠다.
우선 실습이전에 한국어 글꼴을 미리 설치해 준다.
!sudo apt-get install -y fonts-nanum
!sudo fc-cache -fv
!rm ~/.cache/matplotlib -rf
데이터 셋은 네이버의 영화 리뷰 데이터 셋을 이용할 것이며, 이 데이터 셋은 총 200,000개의 리뷰 데이터로 영화 리뷰를 긍/부정으로 분류하기 위하여 수집된 데이터이다.
2-1. 데이터 수집
우선 데이터 수집해야 하는데 urlib라이브러리를 이용하여 다운로드하도록 하겠다.
import urllib.request
import pandas as pd
urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt", filename="ratings_train.txt")
urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt", filename="ratings_test.txt")
그럼 아래와 같이 데이터가 생성된 것을 볼 수 있다.
2-2. 데이터 전처리
다음으로 데이터 전처리를 진행할 것이다.
# 결측치처리
train_dataset.replace("", float("NaN"), inplace=True)
train_dataset = train_dataset.dropna().reset_index(drop=True)
# 중복 제거
train_dataset = train_dataset.drop_duplicates(['document']).reset_index(drop=True)
# 한글이 아닌 문자 제거
train_dataset['document'] = train_dataset['document'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
#길이가 짧은 데이터 제거
train_dataset['document'] = train_dataset['document'].apply(lambda x: ' '.join([token for token in x.split() if len(token) > 2]))
# 전체 길이가 10 이하이거나 전체 단어 개수가 5개 이하인 데이터를 필터링합니다.
train_dataset = train_dataset[train_dataset.document.apply(lambda x: len(str(x)) > 10 and len(str(x).split()) > 5)].reset_index(drop=True)
이후 konlpy의 okt 분석기를 이용하여 한국어 불용어를 제거해 줄 것이다.
from konlpy.tag import Okt
# 불용어 정의
stopwords = ['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다']
train_dataset = list(train_dataset['document'])
# 형태소 분석기 OKT를 사용한 토큰화 작업
okt = Okt()
tokenized_data = []
for sentence in train_dataset:
tokenized_sentence = okt.morphs(sentence, stem=True) # 토큰화
stopwords_removed_sentence = [word for word in tokenized_sentence if not word in stopwords] # 불용어 제거
tokenized_data.append(stopwords_removed_sentence)
2-3. 데이터 분포 확인
이제 데이터의 분포를 확인해 볼 수 있다.
print('리뷰의 최대 길이 :',max(len(review) for review in tokenized_data))
print('리뷰의 평균 길이 :',sum(map(len, tokenized_data))/len(tokenized_data))
plt.hist([len(review) for review in tokenized_data], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()
2-4. 워드 임베딩 구축
gensim에서 제공되는 Word2Vec을 이용하여 토큰화된 네이버 영화 리뷰 데이터를 학습한다.
from gensim.models import Word2Vec
embedding_dim = 100
model = Word2Vec(
sentences = tokenized_data,
vector_size = embedding_dim,
window = 5,
min_count = 5,
workers = 4,
sg = 0
)
(word2vec의 버전에 따라 size와 vector_size로 매개변수의 이름이 다를 수 있다.)
word_vectors = model.wv
vocabs = list(word_vectors.key_to_index.keys())
(word2vec의 버전에 따라 vocab이라는 키워드가 key_to_index로 바뀌었다. )
이렇게 임베딩 된 단어들 간의 유사도를 아래와 같이 확인해 볼 수 있다.
for sim_word in model.wv.most_similar("마블"):
print(sim_word)
또는 개별적으로 확인하기 위해서는 `similarity`를 이용해 볼 수도 있다.(model에 바로 similarity를 적용하면 안 되고 model.wv에 적용해야 한다.)
print(model.wv.similarity('슬픔', '눈물'))
# >> 0.9931
2-5. PCA를 이용한 임베딩 벡 시각화
첫 번째 방법으로 PCA를 이용한 차원축소 방식을 이용하여 임베딩 벡터들의 차원을 확인해 볼 것이다.
그전에 시각화의 유용성을 위해 한글폰트를 세팅해 줬다.
from sklearn.decomposition import PCA
import matplotlib.font_manager
plt.rc('font', family='NanumBarunGothic')
word_vector_list = [word_vectors[word] for word in vocabs]
각각의 벡터들을 모두 담은 다음 PCA의 주성분을 2개로 설정하여 진행해 준다.
pca = PCA(n_components=2)
xys = pca.fit_transform(word_vector_list)
x_asix = xys[:, 0]
y_asix = xys[:, 1]
def plot_pca_graph(vocabs, x_asix, y_asix):
plt.figure(figsize=(25, 15))
plt.scatter(x_asix, y_asix, marker = 'o')
for i, v in enumerate(vocabs):
plt.annotate(v, xy=(x_asix[i], y_asix[i]))
plot_pca_graph(vocabs, x_asix, y_asix)
그런데 이러한 PCA는 많은 벡터들이 한 곳에 모여있어서 제대로 확인하기가 어렵다.
2-5. t-SNE를 이용한 임베딩 벡터 시각화
t-SNE라는 차원 축소 방식을 이용하여 동일하게 시각화를 해볼 것이다.
t-SNE는 numpy array 형식을 입력해야 하기 때문에 word_vector_list를 넘파이 배열로 바꿔주고 진행하였다.
from sklearn.manifold import TSNE
tsne = TSNE(learning_rate = 100)
word_vector_list = np.array(word_vector_list)
transformed = tsne.fit_transform(word_vector_list)
x_axis_tsne = transformed[:, 0]
y_axis_tsne = transformed[:, 1]
def plot_tsne_graph(vocabs, x_asix, y_asix):
plt.figure(figsize=(30, 30))
plt.scatter(x_asix, y_asix, marker = 'o')
for i, v in enumerate(vocabs):
plt.annotate(v, xy=(x_asix[i], y_asix[i]))
plot_tsne_graph(vocabs, x_axis_tsne, y_axis_tsne)
pca보다 분산되어 표시되어 있지만 저 차원으로 매핑되어 확인하기가 어렵다.
2-6. 임베딩 프로젝터를 이용한 시각화
임베딩된 벡터들을 좀 더 직관적으로 확인하기 위하여 구글에서 제공하는 임베딩 프로젝터를 이용하여 시각화해볼 것이다.
from gensim.models import KeyedVectors
model.wv.save_word2vec_format('sample_word2vec_embedding')
!python -m gensim.scripts.word2vec2tensor --input sample_word2vec_embedding --output sample_word2vec_embedding
우선 우리가 임베딩한 벡터들을 파일로 만들고 아래 링크에 들어가 생성된 2개의 tensor.tsv와 metadata.tsv를 업로드해준다. 그럼 제일 아래 그림과 같이 임베딩 된 벡터들을 직관적으로 시각화할 수 있으며 원하는 voca를 직접 검색하여 찾아볼 수도 있다. 자세한 사용법은 위키독스의 문서를 참고하길 바란다. https://wikidocs.net/50704
'ML & DL > NLP' 카테고리의 다른 글
[패캠/NLP] 문장 임베딩 ELMo (1) | 2023.12.22 |
---|---|
[패캠/NLP] 문장 임베딩 (1) | 2023.12.22 |
[패캠/NLP] 임베딩 기법(Word2Vec, FastText, GloVe) (1) | 2023.12.20 |
[패캠/NLP] 워드 임베딩 (1) | 2023.12.20 |
[패캠/NLP] 문장 임베딩 및 유사도 측정 실습 (1) | 2023.12.18 |