[패캠/NLP] Word2Vec 워드 임베딩 실습

2023. 12. 20. 16:12·ML & DL/NLP

이번 글에서는 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

 

Embedding projector - visualization of high-dimensional data

Visualize high dimensional data.

projector.tensorflow.org

728x90

'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
'ML & DL/NLP' 카테고리의 다른 글
  • [패캠/NLP] 문장 임베딩 ELMo
  • [패캠/NLP] 문장 임베딩
  • [패캠/NLP] 임베딩 기법(Word2Vec, FastText, GloVe)
  • [패캠/NLP] 워드 임베딩
창빵맨
창빵맨
  • 창빵맨
    Let's be Developers
    창빵맨
    로그인/로그아웃
  • 전체
    오늘
    어제
    • 분류 전체보기 (471)
      • 알쓸신잡 (79)
      • ML & DL (85)
        • Computer v.. (22)
        • NLP (22)
        • 파이썬 머신러닝 완.. (3)
        • 개념정리 (38)
      • 리눅스 (21)
      • 프로젝트 (29)
        • 산불 발생 예측 (6)
        • 음성비서 (12)
        • pdf 병합 프로그.. (0)
        • 수위 예측 (5)
        • 가짜 뉴스 분류 (5)
        • 전력사용량 예측 (1)
      • 코딩테스트 (217)
        • 프로그래머스[Pyt.. (17)
        • 프로그래머스[Fai.. (3)
        • 백준[Python] (160)
        • 이것이취업을위한코딩.. (18)
        • 파이썬 알고리즘 (19)
      • 데이터분석실습 (25)
        • 데이터 과학 기반의.. (18)
        • 헬로 데이터 과학 (7)
      • 메모장 (0)
      • 잡담 (4)
  • Personal

    GITHUB
    Instagram
  • 공지사항

  • 인기 글

  • 태그

    이것이취업을위한코딩테스트다
    이코테
    이분탐색
    dp
    그리디
    백준
    BFS
    나동빈
    파이썬
    DFS
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3

HOME

HOME

상단으로

티스토리툴바