본문 바로가기

7. ML | DL | NLP

7/4(화) IT K-DT(84일차) / 4. 워드 임베딩(word embedding) (2)

 

 

4. 워드 임베딩(Word Embedding)

 

4-4. 워드 임베딩 구축하기

 

import pandas as pd
import numpy as np
from sklearn.datasets import fetch_20newsgroups # 20newsgroups라는 데이터셋을 활용할 예정

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

# header, footer, quote를 삭제하여 정제한 데이터셋을 쓸 예정
dataset

 

 

# dataset 내부의 data(말뭉치)만 가져올 예정
dataset.data

 

 

# dataset에 data만 저장할 예정
dataset = dataset.data

# 수정한 dataset을 확인 # 너무 길기 때문에 맨 앞의 요소만 확인
dataset[0]

 

 

# dataset의 내부에 총 data의 갯수
len(dataset) # 11314개

 

 

# document 필드를 가진 데이터프레임으로 변환
news_df = pd.DataFrame({'document':dataset}) # key:value값을 가진 dictionary형태로 저장해야 함
news_df

 

 

# 간단한 전처리과정 진행 예정
# dataset(=dataframe)에 결측값을 확인
news_df.info()

 

 

# dataset의 결측값을 제거 후 데이터셋 총 개수 확인
news_df.replace('', float('NaN'), inplace=True)
print(news_df.isnull().values.any()) # NaN이 있으면 True, 없으면 False를 출력함.



 

news_df = news_df.dropna().reset_index(drop=True)
print(f'필터링된 데이터셋 총 개수: {len(news_df)}')

 

 

news_df

 

 

# 열을 기준으로 중복된 데이터를 제거
processed_news_df = news_df.drop_duplicates(['document']).reset_index(drop=True)
processed_news_df

 

 

# 맨 앞의 글자만 파악
len(processed_news_df.iloc[0][0]) # 총 343개 있음.

 

 

# dataset에 특수문자를 제거 # 영문만 뽑아오는 쪽으로 방향을 잡음
processed_news_df['document'] = processed_news_df['document'].str.replace('[^a-zA-Z]',' ')
processed_news_df

 

 

# dataset에서 길이가 너무 짧은 단어(길이가 2이하)를 제거 # 띄어쓰기를 기준으로 모두 split할 예정
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

 

 

# 전체 길이가 200이하이거나 전체 단어 개수가 5개 이하인 data를 filtering
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

 

 

# 전체 단어에 대한 소문자 변환
processed_news_df['document'] = processed_news_df['document'].apply(lambda x: x.lower())
processed_news_df

 

 

# 불용어 제거
import nltk
from nltk.corpus import stopwords

# stopwords 다운로드
nltk.download('stopwords')

 

 

# 현재 영어로 등록되어있는 stopwords의 갯수
stop_words = stopwords.words('english')
print(len(stop_words))
# 10개만 확인해볼 예정
print(stop_words[:10])

 

 

# dataset에 불용어를 제외하여 띄어쓰기 단위로 문장을 분리
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]) 

# stop_words에 포함되지 않은 단어만 필터링하여 재지정
tokenized_doc



 

# list로 변경한 후, 갯수를 파악
tokenized_doc =tokenized_doc.to_list()
print(len(tokenized_doc)) # 총 8199개 존재

 

 

from tensorflow.keras.preprocessing.text import Tokenizer

# 토큰화
tokenizer = Tokenizer()
tokenizer.fit_on_texts(tokenized_doc)

# 토큰에 대한 인덱스 지정
word2idx = tokenizer.word_index
# word2idx를 입력해서 출력시 인덱스를 지정해서 출력, 

idx2word는 반대로 출력, encoded는 인덱스번호만 출력
idx2word = {value:key for key, value in word2idx.items()}
encoded = tokenizer.texts_to_sequences(tokenized_doc)

# 생성된 단어사전의 크기 확인
vocab_size = len(word2idx) + 1 # 인덱스이므로 1을 추가해줌
print(f'단어 사전의 크기 : {vocab_size}')

 

 

# negative sampling을 쓰기위해 keras에서 제공하는 전처리 도구인 skipgram을 이용할 예정
from tensorflow.keras.preprocessing.sequence import skipgrams

 # 전체 샘플에서 5개를 가져다 negative sampling 진행 예정
skip_grams = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded[:5]] # window_size 기본값: 4
print(f'전체 샘플 수 : {len(skip_grams)}')

 

 

# skip_grams[0]에 skipgrams로 형성된 데이터셋의 확인.
pairs, labels = skip_grams[0][0], skip_grams[0][1]
print(f'3 pairs: {pairs[:3]}')
print(f'3 labels: {labels[:3]}')
# [37008, 107]는 positive(관계있음),  [37008, 18296], [458, 26465]은 negative(관계없음)

 

 

# 첫번째 뉴스 그룹 sample에 대해 생긴 pairs와 labels의 개수
print(len(pairs))
print(len(labels))
# 각각 2100개씩



 

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

 

 

# 5000개의 단어로 training dataset을 생성
training_dataset = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded[:5000]]

len(training_dataset)

 

 

# word embedding 구축 예정. # keras 모델 사용 예정
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Embedding, Reshape, Activation, Input, Dot
from tensorflow.keras.utils import plot_model

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) # 0.5 이상이면 1, 미만이면 0인 시그모이드함수

model = Model(inputs=[w_inputs, c_inputs], outputs=output) # 모델 제작 완료

 

# 모델의 요약
model.summary()

 

 

model.compile(loss='binary_crossentropy', optimizer='adam') 

# 단항분류인 이중분류문제이므로 binary_crossentropy 함수를 사용.

 

# 시각화
plot_model(model, to_file='model.png', show_shapes=True, show_layer_names=True)

 

 

for epoch in range(50):
  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) # Loss값은 계속 줄어듦
  # 학습을 시킨 후, 품질확인이 필요

 

 

# 모델을 text로 만들어주는 라이브러리
import gensim

 

f = open('vectors.txt', 'w')
f.write('{} {}\n'.format(vocab_size-1, embedding_dim))
vectors = model.get_weights()[0] # index 0번의 가중치를 저장.
# 가중치 확인
# print(vectors)
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) 

# vectors.txt파일을 불러와서 가중치를 그대로 사용할 수 있게 해줌.

w2v.most_similar(positive=['apple']) # apple과 가장 유사한 단어들이 확률과 함께 출력됨

 

 

w2v.most_similar(positive=['engine']) # engine과 가장 유사한 단어들이 확률과 함께 출력됨