본문 바로가기
Computer/ML·DL·NLP

[이수안컴퓨터연구소] 임베딩 Embedding

by injeolmialmond 2022. 1. 8.

https://www.youtube.com/watch?v=hR8Rvp-YNGg&list=PL7ZVZgsnLwEEoHQAElEPg7l7T6nt25I3N&index=7 

https://colab.research.google.com/drive/1mBJgcpLrcyJeytlgarr2Gftff8JROPd8?usp=sharing 

 

_7 임베딩(Embedding).ipynb

Colaboratory notebook

colab.research.google.com

임베딩(Embedding)

  • 워드 임베딩은 단어를 컴퓨터가 이해하고, 효율적으로 처리할 수 있도록 단어를 벡터화하는 기술
  • 워드 임베딩은 단어의 의미를 잘 표현해야만 하며, 현재까지도 많은 표현 방법이 연구
  • 워드 임베딩을 거쳐 잘 표현된 단어 벡터들은 계산이 가능하며, 모델 투입도 가능

 

인코딩(Encoding)

 

  • 기계는 자연어(영어, 한국어 등)을 이해할 수 없음
  • 데이터를 기계가 이해할 수 있도록 숫자 등으로 변환해주는 작업이 필요
  • 이러한 작업을 인코딩이라고 함
  • 텍스트 처리에서는 주로 정수 인코딩, 원 핫 인코딩을 사용

 

정수 인코딩

 

dictionary를 이용한 정수 인코딩

  • 각 단어와 정수 인덱스를 연결하고, 토큰을 변환해주는 정수 인코딩
text = "평생 살 것처럼 꿈을 꾸어라. 그리고 내일 죽을 것처럼 오늘을 살아라."

tokens = [x for x in text.split(' ')]
unique = set(tokens) # 토큰이 중복되지 않도록
unique = list(unique)

token2idx = {}
for i in range(len(unique)):
    token2idx[unique[i]] = i
    
encode = [token2idx[x]for x in tokens]
encode # [5, 9, 7, 6, 3, 8, 1, 4, 7, 0, 2]

keras를 이용한 정수 인코딩

  • 정수 인코딩은 단어에 정수로 레이블을 부여
  • dictionary, nltk 패키지를 이용한 방법들도 있지만, keras에서는 텍스트 처리에 필요한 도구들을 지원
  • 해당 도구는 자동으로 단어 빈도가 높은 단어의 인덱스는 낮게끔 설정
from tensorflow.keras.preprocessing.text import Tokenizer

text = "평생 살 것처럼 꿈을 꾸어라. 그리고 내일 죽을 것처럼 오늘을 살아라."

t = Tokenizer()
t.fit_on_texts([text]) # Tokenizer().fit_on_texts()에는 텍스트로 이루어진 리스트를 넣어야 함
print(t.word_index) # 딕셔너리로 표현됨 : {'것처럼': 1, '평생': 2, '살': 3, '꿈을': 4, '꾸어라': 5, '그리고': 6, '내일': 7, '죽을': 8, '오늘을': 9, '살아라': 10}

encoded = t.texts_to_sequences([text])[0]
print(encoded) # [2, 3, 1, 4, 5, 6, 7, 8, 1, 9, 10]

 

원 핫 인코딩(One-Hot Encoding)

정수 인코딩보다는 원 핫 인코딩을 더 자주 씀.

조건문과 반복문을 이용한 원 핫 인코딩

  • 원 핫 인코딩은 정수 인코딩한 결과를 벡터로 변환한 인코딩
  • 원 핫 인코딩은 전체 단어 개수 만큼의 길이를 가진 배열에 해당 정수를 가진 위치는 1, 나머지는 0을 가진 벡터로 변환
import numpy as np

one_hot = []
for i in range(len(encoded)):
    temp = []
    for j in range(max(encoded)):
        if j == (encoded[i] - 1): # encoded의 값은 1부터 시작. for문은 0부터 시작
            temp.append(1)
        else :
            temp.append(0)
    one_hot.append(temp)
    
np.array(one_hot)

keras를 이용한 원 핫 인코딩

  • keras에서는 정수 인코딩을 원 핫 인코딩을 간단하게 변환해주는 to_categorical() 함수를 제공
from tensorflow.keras.utils import to_categorical

one_hot = to_categorical(encoded)
one_hot

 

 

IMDB 데이터

  • 인터넷 영화 데이터베이스(Internet Movie Database)
  • 양극단의 리뷰 5만개로 이루어진 데이터셋
    • 훈련데이터: 25,000개
    • 테스트데이터 : 25,000개
  • https://www.imdb.com/interfaces/

1. module import

from tensorflow.keras.datasets import imdb
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Dense, Flatten

 

2. 데이터 로드

num_words = 1000
max_len = 20 # 조금만 끊어서 연습해보장

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words = num_words)

print(x_train.shape) # (25000,)
print(y_train.shape) # (25000,)
print(x_test.shape) # (25000,)
print(y_test.shape) # (25000,)

3. 데이터 확인

  • 긍정: 1
  • 부정: 0
print(x_train[0]) # 이렇게 임베딩된 데이터는
print(y_train[0]) # 긍정이당!
for i in range(10):
    if y_train[i] == 0:
        label = '부정'
    else:
        label = '긍정'
    print('{}\n{}'.format(x_train[i], label)) # 첫 10개 리뷰에 대해서, 임베딩된 정수 리스트와 함께 긍정인지 부정인지 출력

4. 데이터 전처리

  • 모든 데이터를 같은 길이로 맞추기
    • pad_sequence()
      • 데이터가 maxlen보다 길면 데이터를 자름
      • 데이터가 길면 padding 설정
        • pre: 데이터 앞에 0으로 채움
        • post: 데이터 뒤에 0으로 채움
  • 모든 데이터(문장 하나하나)가 같은 길이로 맞추어야 Embedding 레이어를 사용할 수 있음
from tensorflow.keras.preprocessing.sequence import pad_sequences

max_len = 100

pad_x_train = pad_sequences(x_train, maxlen=max_len, padding='pre')
pad_x_test = pad_sequences(x_test, maxlen=max_len, padding='pre')


# 전처리 확인하기
print(len(x_train[5])) # 43
print(len(pad_x_train[5])) # 100
# 길이가 잘 맞춰진 것을 알 수 있음 (나머지 57개는 맨 앞에 0 57개로 채워짐)

print(len(x_train[1])) # 189
print(len(pad_x_train[1])) # 100
# 이 경우에는 데이터가 잘림

 

 

5. 모델 구성

model = Sequential()
model.add(Embedding(input_dim=num_words, output_dim=32,input_length=max_len))

model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))

model.summary()

 

 

6. 모델 컴파일 및 학습

model.compile(optimizer='rmsprop',
             loss='binary_crossentropy',
             metrics=['acc'])

history=model.fit(pad_x_train, y_train,
                 epochs=10,
                 batch_size=32,
                 validation_split=0.2) # 오버피팅됨. 모델 안에 일반화를 시킬 수 있는 장치가 없음..ㅜ

 

 

7. 시각화

import matplotlib.pyplot as plt
plt.style.use('seaborn-white')

hist_dict = history.history
hist_dict.keys() # dict_keys(['loss', 'acc', 'val_loss', 'val_acc'])


plt.plot(hist_dict['loss'], 'b-', label = 'Train Loss')
plt.plot(hist_dict['val_loss'], 'r:', label = "Validation Loss")
plt.legend()
plt.grid()

plt.figure()
plt.plot(hist_dict['acc'], 'b-', label = 'Train Accuracy')
plt.plot(hist_dict['val_acc'], 'r:', label = "Validation Accuracy")
plt.legend()
plt.grid()

plt.show()

오버피팅이 되었다는 것을 알 수 있음

 

 

8. 모델 평가

model.evaluate(pad_x_test, y_test) # [0.486984521150589, 0.8037199974060059] 각각 loss, 정확도

 

 

9. 단어의 수를 늘린 후 재학습

# 조건 다르게 다시 가볼게요
num_words = 2000
max_len=400

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words = num_words)

# 전처리
pad_x_train2 = pad_sequences(x_train, maxlen=max_len, padding='pre')
pad_x_test2 = pad_sequences(x_test, maxlen=max_len, padding='pre')



# 모델 설계
model = Sequential()
model.add(Embedding(input_dim=num_words, output_dim=32,input_length=max_len))

model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))



# 모델 컴파일
model.compile(optimizer='rmsprop',
             loss='binary_crossentropy',
             metrics=['acc'])

# 모델 학습
history=model.fit(pad_x_train2, y_train,
                 epochs=10,
                 batch_size=32,
                 validation_split=0.2)
# 시각화
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')

hist_dict = history.history
hist_dict.keys() # dict_keys(['loss', 'acc', 'val_loss', 'val_acc'])


plt.plot(hist_dict['loss'], 'b-', label = 'Train Loss')
plt.plot(hist_dict['val_loss'], 'r:', label = "Validation Loss")
plt.legend()
plt.grid()

plt.figure()
plt.plot(hist_dict['acc'], 'b-', label = 'Train Accuracy')
plt.plot(hist_dict['val_acc'], 'r:', label = "Validation Accuracy")
plt.legend()
plt.grid()

plt.show()

model.evaluate(pad_x_test2, y_test) # [0.49533307552337646, 0.8497200012207031] 각각 loss, 정확도

 

 

위의 결과도 나쁘지 않으나 과적합이 되는 이유
  • 단어간 관계나 문장 구조 등 의미적 연결을 고려하지 않음
  • 시퀀스 전체를 고려한 특성을 학습하는 것은 Embedding층 위에 RNN층이나 1D 합성곱을 추가하는 것이 좋음

 

 

Word2Vec

  • 2013년, Mikolov 와 동료들이 제안한 모형
  • 분류 등과 같이 별도의 레이블이 없이 텍스트 자체만 있어도 학습이 가능
  • Word2Vec의 방식
    주변 단어의 관계를 이용
    • 방식1 : CBOW(continuous bag-of-words)
      • 주변단어의 임베딩을 더해서 대상단어를 예측
    • 방식2 : Skip-Gram
      • 대상 단어의 임베딩으로 주변단어를 예측
      • 일반적으로 CBOW보다 성능이 좋은 편
      • 한번에 여러 단어를 예측해야하기 때문에 비효율적
        -> 최근에는 negative sampling이라는 방법을 사용
    출처: https://www.researchgate.net/figure/CBOW-and-Skip-Gram-neural-architectures_fig14_328160770

 

T-SNE

  • T-SNE(t-Stochastic Neighbor Embedding)은 고차원의 벡터들의 구조를 보존하며 저차원으로 사상하는 차원 축소 알고리즘
  • 단어 임베딩에서도 생성된 고차원 벡터들을 시각화하기 위해 이 T-SNE 알고리즘을 많이 이용
  • t-sne는 가장 먼저 원 공간의 데이터 유사도 pij와 임베딩 공간의 데이터 유사도 qij를 정의
  • xi에서 xj 간의 유사도 pj|i 는 다음과 같이 정의

  • 설명하자면 pj|i xi xj 간의 거리에서 가중치 σ2i로 나눈 후, 이를 negative exponential을 취해 모든 점 간의 거리의 합과 나누어준 값으로 두 점 간의 거리가 가까울 수록 큰 값을 가짐
  • 또한 임베딩 공간에서의 yi에서 yj 간의 유사도 qj|i 는 다음과 같이 정의

  • qj|i xi xj 간의 거리에서 1을 더해준 후 역수를 취한 값과 전체 합산 값과 나누어 유사도를 정의
  • T-SNE의 학습은 pj|i와 비슷해지도록 qj|i의 위치를 조정하는 것이라고 할 수 있음

1. 데이터 준비

from sklearn.datasets import fetch_20newsgroups

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

print(len(documents))
documents[2]

2.  전처리

import re
import nltk

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

nltk.download('stopwords')
nltk.download('punkt')

def clean_text(d):
    pattern = r'[^a-zA-Z\s]'
    text = re.sub(pattern, '', d)
    return text

# 댓글 참고해서 함수 수정
def clean_stopword(d):
    stop_words = stopwords.words('english')
    return ' '.join(w.lower() for w in d.split() if w.lower() not in stop_words and len(w)>3)

def tokenize(d):
    return word_tokenize(d)
import pandas as pd

news_df = pd.DataFrame({'article':documents})
len(news_df) # 11314
news_df.replace("", float('NaN'), inplace=True)
news_df.dropna(inplace=True)
print(len(news_df)) # 11096
news_df['article'] = news_df['article'].apply(clean_text)
# news_df['article']

news_df['article'] = news_df['article'].apply(clean_stopword)
# news_df['article']

tokenized_news = news_df['article'].apply(tokenize)
tokenized_news = tokenized_news.to_list()



import numpy as np

drop_news = [index for index, sentence in enumerate(tokenized_news) if len(sentence) <= 1] # 불필요한 데이터 (제거 대상)
news_texts = np.delete(tokenized_news, drop_news, axis = 0)
print(len(news_texts)) # 10939

 

Gensim을 이용한 Word2Vec

1. CBOW

from gensim.models import Word2Vec
# gensim 버전이 다른 것인지 유튜브와는 달리 size 대신 vector_size 사용함
model = Word2Vec(sentences=news_texts, 
                 window=4, # 고려할 앞 뒤 폭
                 vector_size=100, # 벡터 크기
                 min_count=5, # 사용할 단어의 최소 빈도 (5회 이하 단어 무시)
                 workers=4, # 동시에 처리할 작업의 수 
                 sg=0) # 0 : cbow, 1 : skip-gram
model.save('word2vec.model') # 저장하기
model = Word2Vec.load('word2vec.model') # 불러오기
# model.wv['film'] 
model.wv.similarity('movie', 'film') # 0.98277336

model.wv.most_similar(positive=['woman'])
# [('mother', 0.9907262921333313),
 ('brought', 0.9883763194084167),
 ('dead', 0.9873701930046082),
 ('wife', 0.9823874235153198),
 ('azerbaijanis', 0.9766591787338257),
 ('neighbors', 0.973651111125946),
 ('lived', 0.9733042120933533),
 ('parents', 0.9723166823387146),
 ('daughter', 0.9703890681266785),
 ('apartment', 0.9700297117233276)]
 
 model.wv.most_similar(positive=['woman', 'girl'], negative='soldier')
 # [('face', 0.9797027111053467),
 ('neighbors', 0.9784392714500427),
 ('coming', 0.9781953692436218),
 ('karina', 0.9771144390106201),
 ('shouting', 0.975655198097229),
 ('alive', 0.974787175655365),
 ('wife', 0.9734463691711426),
 ('mamma', 0.9732183218002319),
 ('apartment', 0.97276771068573),
 ('parents', 0.9713296294212341)]

 

2. Skip-gram

# skip-gram
from gensim.models import Word2Vec
# gensim 버전이 다른 것인지 유튜브와는 달리 size 대신 vector_size 사용함
model = Word2Vec(sentences=news_texts, 
                 window=4, # 고려할 앞 뒤 폭
                 vector_size=100, # 벡터 크기
                 min_count=5, # 사용할 단어의 최소 빈도 (5회 이하 단어 무시)
                 workers=4, # 동시에 처리할 작업의 수 
                 sg=1) # 0 : cbow, 1 : skip-gram

model.save('word2vec.model2') # 저장하기
model = Word2Vec.load('word2vec.model2') # 불러오기
# model.wv['film'] 
model.wv.similarity('movie', 'film') # 0.92668915

model.wv.most_similar(positive=['woman'])
# [('husband', 0.9329707026481628),
 ('marina', 0.9309569597244263),
 ('sister', 0.9188961386680603),
 ('igor', 0.9188531637191772),
 ('shagen', 0.914876401424408),
 ('naked', 0.913983166217804),
 ('corpse', 0.9133069515228271),
 ('lyuda', 0.9116402268409729),
 ('shouted', 0.9090188145637512),
 ('daughters', 0.9089158773422241)]
 
 model.wv.most_similar(positive=['woman', 'girl'], negative='soldier')
 # [('mamma', 0.8757262825965881),
 ('marina', 0.8475387096405029),
 ('karina', 0.843293309211731),
 ('papa', 0.8417982459068298),
 ('lyuda', 0.8405961990356445),
 ('drove', 0.839897871017456),
 ('igor', 0.8369525671005249),
 ('sister', 0.8334420919418335),
 ('shouting', 0.8330908417701721),
 ('threw', 0.8318761587142944)]

 

임베딩 벡터 시각화

  • metadata.tsv와 tensor.tsv 데이터 생성
from gensim.models import KeyedVectors

model.wv.save_word2vec_format('news_w2v')

!python -m gensim.scripts.word2vec2tensor -i news_w2v -o news_w2v
 

Embedding projector - visualization of high-dimensional data

Visualize high dimensional data.

projector.tensorflow.org

위 사이트에 들어가서 두 개 tsv 파일 업로드 ('Load' 버튼 누르고) 가능!

짜잔!

PCA (차원축소 된 버전)
tsne (차원축소X) :계속해서 움직임(=유사도 학습중)

 

댓글