본문 바로가기

딥러닝/딥러닝을 이용한 자연어처리 입문

[딥러닝 NLP] 09. 워드 임베딩(Word2Vec, SGNS, GloVe, FastText, ELMo, Doc2Vec)

# 09-01 Word Embedding

-희소표현(Sparse Representation): 원-핫 벡터나 DTM과 같이 벡터 또는 행렬의 대부분의 값이 0으로 표현되는 방법, 따라서 공간적 낭비가 크며 단어의 의미를 고려하지 못함

-분산표현(Distributed Representation): 단어의 의미를 공간에 벡터화하는 표현 방법. 비슷한 문맥에서 등장하는 단어들은 비슷한 의미를 가진다는 분포 가설(Distributional hypothesis)의 가정 하에 만들어진 표현 방법.

-밀집표현(Dense Representation): 사용자가 설정한 값에 맞춰 모든 단어의 벡터 차원이 같아지며 모든 값이 실수가 되는 표현 방법

-워드 임베딩(Word Embedding): 단어를 밀집 벡터 형태로 표현하는 방법

-임베딩 벡터(Embedding Vector): 워드 임베딩의 결과로 나온 벡터

 

# 09-02 Word2Vec

Word2Vec은 은닉층이 1개인 얕은 신경망(shallow neural netword)

CBOW, Skip-gram 의 2가지가 있으며 전반적으로 Skip-gram이 성능이 좋음

 

(1) CBOW(Continuous Bag of Words)

-주변 단어(context word)를 입력으로 중심 단어(center word)를 예측하는 방법

-윈도우 크기 n: 중심 단어를 예측하기 위해 앞, 뒤에서 각각 사용할 주변 단어의 개수

-슬라이딩 윈도우(sliding window): 윈도우를 옆으로 움직여가며 학습을 위한 데이터셋을 만드는 방법

  • 예문 : "The fat cat sat on the mat", n=2
  • 중심 단어: sat 일 때 주변 단어: fat, cat, on, the

슬라이딩 윈도우
CBOW 도식화

-입력층: 주변 단어들의 원핫벡터

-출력층: 중심 단어의 원핫벡터

-은닉층: 일반적인 은닉층과는 달리 활성화 함수 없이 룩업 테이블 연산을 수행하므로 투사층이라고도 부름

1) 입력층->투사층: 입력으로 주어진 각 주변단어 원핫벡터들에 가중치 W(VXM) 를 곱하고 그 벡터들의 평균 벡터를 구함

2) 투사층->출력층: 평균벡터에 가중치 W'(MXV) 를 곱하고 소프트맥스 함수를 거쳐 스코어 벡터가 됨

3) 실제 정답 원핫벡터와 스코어벡터 사이의 오차를 줄이기 위해 크로스 엔트로피 함수를 사용해 역전파 수행

 

(2) Skip-gram

중심 단어를 입력으로 주변 단어를 예측하는 방법

윈도우 크기 n=2 일 때 데이터셋
Skip-gram 도식화

 

3. NNLM 과 Word2Vec 비교

-예측대상: NNLM은 다음 단어를, Word2Vec(CBOW)는 중심 단어를 예측

-참고단어: NNLM은 이전 단어만을, Word2Vec(CBOW)는 전후단어 모두 참고

-구조: Word2Vec은 NNLM에 존재하던 활성화함수가 있는 은닉층을 제거해 투사층 다음에 바로 출력층으로 연결

-Word2Vec이 NNLM보다 학습속도가 빠른 이유: 은닉층 제거, 소프트맥스, 네거티브 샘플링, 연산량 감소(V가 log(V)로 바뀜)

 

# 09-03 Word2Vec 실습

1. 영어 Word2Vec

(1) 데이터 전처리

(2) Word2Vec 훈련

from gensim.models import Word2Vec
from gensim.models import KeyedVectors

model = Word2Vec(sentences=result, vector_size=100, window=5, min_count=5, workers=4, sg=0) # CBOW
model_result = model.wv.most_similar("man") # 단어의 유사도 계산

vector_size = 워드 벡터의 특징 값. 즉, 임베딩 된 벡터의 차원.
window = 컨텍스트 윈도우 크기
min_count = 단어 최소 빈도수 제한 (빈도가 적은 단어들은 학습하지 않는다.)
workers = 학습을 위한 프로세스 수
sg = 0은 CBOW, 1은 Skip-gram.

(3) 모델 저장 및 로드

model_result = model.wv.most_similar("man") # 모델 저장
loaded_model = KeyedVectors.load_word2vec_format("eng_w2v") # 모델 로드

 

2. 한국어 Word2Vec

(1) 데이터 전처리

import pandas as pd
import matplotlib.pyplot as plt
import urllib.request
from gensim.models.word2vec import Word2Vec
from konlpy.tag import Okt

# 데이터 로드
urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings.txt", filename="ratings.txt")
train_data = pd.read_table('ratings.txt')

# 불용어제거
train_data['document'] = train_data['document'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
stopwords = ['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다']
okt = Okt()
tokenized_data = []
for sentence in tqdm(train_data['document']):
    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) Word2Vec 훈련

from gensim.models import Word2Vec
model = Word2Vec(sentences = tokenized_data, vector_size = 100, window = 5, min_count = 5, workers = 4, sg = 0)
model.wv.vectors.shape # (16477, 100)

 

3. 사전 훈련된 Word2Vec

-사전 훈련된 워드 임베딩벡터(pre-trained word embedding vector): 방대한 데이터를 Word2Vecc이나 GloVe 등으로 사전에 학슶켜 높은 임베딩 벡터들

-구글이 제공하는 사전훈련된 3백만개의 Word2Vec 단어벡터(임베딩벡터 차원 300)

https://drive.google.com/file/d/0B7XkCwpI5KDYNlNUTTlSS21pQmM/edit

import gensim
import urllib.request

# 구글의 사전 훈련된 Word2Vec 모델 로드
urllib.request.urlretrieve("https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz", \
                           filename="GoogleNews-vectors-negative300.bin.gz")
word2vec_model = gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin.gz', binary=True)
print(word2vec_model.vectors.shape) # (3000000, 300)

# 단어 유사도 계산
print(word2vec_model.similarity('this', 'is'))
print(word2vec_model.similarity('post', 'book'))

# 특정 단어의 벡터 출력
print(word2vec_model['book'])

 

# 09-04 Negative Sampling

1. Negative Sampling

-Word2Vec이 학습 과정에서 전체 단어집합이 아니라 일부 단어집합에만 집중하도록 하는 방법

-현재 집중하고 있는 중심 단어에 대해 전체 단어집합보다 훨씬 작은 단어집합을 만들어놓고, 마지막 단계에서 다중클래스 분류가 아닌 이진분류 문제로 바꾸어 만들어둔 작은 단어집합이 긍정, 랜덤으로 샘플링한 단어들이 부정으로 레이블링되도록 

 

2. Skip-Gram with Negative Sampling(SGNS)

-Skip-gram: 중심 단어로부터 주변 단어를 예측

Skip-gram: 중심단어로 주변단어를 예측

-SGNS: 중심 단어와 주변 단어를 모두 입력하고, 그 두 단어가 실제로 윈도우크기 내에 존재할 확률을 예측

중심단어와 주변단어가 이웃관계일 확률을 예측

 

-중심단어는 입력1, 주변단어 및 랜덤단어는 입력2로 저장되며, 입력1과 입력2의 이웃여부에 따라 레이블 0 또는 1

입력1(중심단어)와 입력2(주변단어)가 이웃관계면 레이블 1
입력1(중심단어)와 입력2(랜덤단어)가 이웃관계가 아니면 레이블 0

-최종 입력데이터는 아래 왼쪽 표와 같다. 아래 오른쪽 그림처럼 입력1과 입력2에 대한 임베딩 테이블은 따로 존재한다.

최종 입력데이터 및 레이블(왼쪽), 입력1과 입력2에 대한 각각의 임베딩 테이블(오른쪽)

-입력1과 입력2는 각각 임베딩테이블(W)을 룩업하여 임베딩벡터로 변환된다.

각 임베딩테이블을 통해 룩업

-입력1의 임베딩벡터와 입력2의 임베딩벡터의 내적값을 예측값으로, 실제 레이블과의 오차로부터 역전파 수행하여 임베딩테이블(W)을 업데이트

-학습 후에는 좌측의 임베딩테이블(W)만 쓰거나 두 임베딩테이블을 연결하여 사용할 수도 있다.

 

3. 네거티브 샘플링 실습

(1) 네거티브 샘플링으로 데이터셋 구성

케라스에서 제공하는 skipgrams 사용

토큰화, 정제, 정규화, 불용어제거, 정수인코딩까지 마친 데이터가 encoded 라고 하자

from tensorflow.keras.preprocessing.sequence import skipgrams

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

 

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

# 모델 학습(epoch=5)
for epoch in range(1, 6):
    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) # 각 에포크의 손실 출력

 

(3) 모델 결과 확인

import gensim

# 학습된 임베딩벡터를 vertor.txt에 저장
f = open('vectors.txt' ,'w') 
f.write('{} {}\n'.format(vocab_size-1, embed_size))
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=['soldiers'])

 

# 09-05 GloVe

1. 기존 방법론 비판

-카운트 기반의 LSA(Latent Semantic Analysis)는 DTM, TF-IDF와 같이 전체 문서에 대한 각 단어의 빈도수를 입력으로 차원을 축소(Truncated SVD)하는 방법론으로, 전체적인 통계 정보는 고려하지만 단어간 의미 유추 불가

-예측 기반의 Word2Vec은 단어간 의미 유추는 가능하지만 윈도우 크기 내의 주변 단어만 고려하기 때문에 전체적인 통계 정보 반영하지 못함

-GloVe는 카운트 기반과 예측 기반의 방법론 두 가지를 모두 사용함

 

2. 윈도우 기반 동시 등장 행렬(Window based Co-occurrence Matrix)

-동시 등장 행렬(Co-occurrence matrix): (전체 단어) X (전체 단어) 로 구성된 행렬에 대해, 윈도우 크기 N 내에서 i 단어에 대하여 k 단어가 등장한 횟수를 i 행 k 열에 기재한 행렬

 Ex) 아래와 같은 세 개의 문장이 있다.

  • I like deep learning
  • I like NLP
  • I enjoy flying

윈도우 크기 N=1 일 때, co-occurrence matrix는 다음과 같다.

 

위 행렬을 전치(Transpose)해도 동일한 행렬이 된다는 특징이 있다.

 

3. 동시 등장 확률(Co-occurrence Probability)

-동시 등장 확률 P(k|i) : 동시 등장 행렬에서 단어 i 가 등장했을 때 단어 k 가 등장하는 조건부 확률

-즉 중심단어가 i, 주변단어가 k일 때, i행의 모든 값을 더한 것이 분모, i행 k열의 값이 분자

동시 등장 확률 예시

위 예시에서 ice가 등장했을 때 solid가 등장할 확률은 steam이 등장했을 때 solid가 등장할 확률의 8.9배이다.

반대로 ice가 등장했을 때 gas가 등장할 확률은 steam이 등장했을 때 gas가 등장할 확률의 0.085배이다.

 

4. 손실 함수

-GloVe의 아이디어를 한 줄로 요약하면 '임베딩 된 중심 단어와 주변 단어 벡터의 내적이 전체 코퍼스에서의 동시 등장 확률이 되도록 만드는 것' 이며, 손실함수를 정의하는 과정은 생략한다. 최종 식은 다음과 같다.

-위 식에서 f(Xmn)은 Xik의 값에 영향을 받는 가중치 함수로, 동시 등장 행렬 X 가 희소 행렬일 가능성이 크기 때문에 도입된다. 동시 출현 빈도가 높은 단어쌍에는 낮은 가중치를, 빈도가 낮은 단어쌍에는 높은 가중치를 부여한다.

ex) 지나친 고빈도 단어쌍이 모델을 지배하는 것을 방지하고(the) 희소한 단어쌍의 정보를 보존(astronaut, space)

 

5. Glove 실습

pip install glove_python_binary

# 모든 전처리를 마친 데이터가 result에 저장되어 있다고 가정한다.
from glove import Corpus, Glove

corpus = Corpus() 

# 훈련 데이터로부터 GloVe에서 사용할 동시 등장 행렬 생성
corpus.fit(result, window=5)
glove = Glove(no_components=100, learning_rate=0.05)

# 학습에 이용할 쓰레드의 개수=4, 에포크=20
glove.fit(corpus.matrix, epochs=20, no_threads=4, verbose=True)
glove.add_dictionary(corpus.dictionary)

# 입력 단어와 유사한 단어 출력
print(glove.most_similar("man"))

 

# 09-06 FastText

-페이스북에서 개발한 모델로, Word2Vec과는 달리 내부 단어(subword)를 고려하여 학습

-모르는 단어(OOV, Out Of Vocabulary)에 대해서도 다른 단어와의 유사도를 계산할 수 있게 됨

-Word2Vec에 비해 빈도수가 적은 단어(Rare Word)에도 비교적 높은 임베딩 벡터값을 얻을 수 있으며, 오타가 섞인 단어에 대해서도 일정 수준의 성능을 보임

 

1. 내부단어(subword)

-단어를 글자 단위 n-gram 구성으로 쪼개고 시작과 끝을 의미하는 < 와 > 를 도입

-최소값(디폴트 3)과 최대값(디폴트 6)을 설정해 벡터화 수행

-최종 벡터=쪼갠 벡터값들의 총 합

# n = 3 ~ 6인 경우
apple = <ap + app + ppl + ppl + le> + <app + appl + pple + ple> + <appl + pple> + , ..., +<apple>

 

2. Word2Vec Vs. FastText 실습

(1) Word2Vec

model.wv.most_similar("electrofishing")

# KeyError: "word 'electrofishing' not in vocabulary"

 

(2) FastText

from gensim.models import FastText

model = FastText(result, size=100, window=5, min_count=5, workers=4, sg=1)
model.wv.most_similar("electrofishing")

# [('electrolux', 0.7934642434120178), ('electrolyte', 0.78279709815979), ('electro', 0.779127836227417), ('electric', 0.7753111720085144), ('airbus', 0.7648627758026123), ('fukushima', 0.7612422704696655), ('electrochemical', 0.7611693143844604), ('gastric', 0.7483425140380859), ('electroshock', 0.7477173805236816), ('overfishing', 0.7435552477836609)]

 

3. 한국어 FastText

-음절 단위: <자연, 자연어, 연어처, 어처리, 처리>

-자모 단위(초성, 중성, 종성, 종성이 없으면 _ ): < ㅈ ㅏ, ㅈ ㅏ _, ㅏ _ ㅇ, ... 중략>

 

# 09-08 케라스 임베딩 층 Vs 사전훈련된 임베딩 실습

1. 케라스  임베딩 층

great은 정수 1918번으로 인코딩되었으므로 임베딩테이블의 1918번 행이 great의 임베딩벡터

# 임베딩 층 구현
vocab_size = 20000 # 전체 단어집합 크기
output_dim = 128 # 임베딩벡터 차원
input_length = 500 # 입력시퀀스 길이

v = Embedding(vocab_size, output_dim, input_length=input_length)
# 감성분류 모델 구현
import numpy as np
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

sentences = ['nice great best amazing', 'stop lies', 'pitiful nerd', 'excellent work', 'supreme quality', 'bad', 'highly respectable']
y_train = [1, 0, 0, 1, 1, 0, 1]

tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentences)
vocab_size = len(tokenizer.word_index) + 1 # 단어집합 크기

X_encoded = tokenizer.texts_to_sequences(sentences) # 정수 인코딩

max_len = max(len(l) for l in X_encoded) # 최대 길이(4)

X_train = pad_sequences(X_encoded, maxlen=max_len, padding='post') # 최대 길이로 패딩
y_train = np.array(y_train)

# 이진분류 모델 설계
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Embedding, Flatten

embedding_dim = 4

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim, input_length=max_len))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
model.fit(X_train, y_train, epochs=100, verbose=2)

 

2. 사전훈련된 GloVe

glove.6B.zip 안에 다수의 파일이 존재하는데 여기서는 glove.6B.100d.txt 사용

# glove.6B.100d.txt에 있는 모든 임베딩 벡터들 불러오기
from urllib.request import urlretrieve, urlopen
import gzip
import zipfile

urlretrieve("http://nlp.stanford.edu/data/glove.6B.zip", filename="glove.6B.zip")
zf = zipfile.ZipFile('glove.6B.zip')
zf.extractall() 
zf.close()
# 임베딩벡터들을 딕셔너리에 저장
embedding_dict = dict()

f = open('glove.6B.100d.txt', encoding="utf8")

for line in f:
    word_vector = line.split()
    word = word_vector[0]

    # 100개의 값을 가지는 array로 변환
    word_vector_arr = np.asarray(word_vector[1:], dtype='float32')
    embedding_dict[word] = word_vector_arr
f.close()

# 임베딩벡터 개수 확인
print(len(embedding_dict)) # 400000

# 임의의 임베딩벡터 출력
print(embedding_dict['respectable']) # [-0.049773   0.19903    0.10585 ... 중략 ... -0.032502   0.38025  ]
print(len(embedding_dict['respectable'])) # 100
# 임베딩 테이블 생성
embedding_matrix = np.zeros((vocab_size, 100))
print(np.shape(embedding_matrix) # (16, 100)

print(tokenizer.word_index.items()) # 각 단어 및 매핑된 정수 확인
print(tokenizer.word_index['great']) # 단어 'great'에 매핑된 정수 확인, 2

# GloVe에서 'great'의 벡터값 확인
print(embedding_dict['great'])

# 단어집합의 모든 단어에 대해 GloVe의 임베딩벡터를 매핑
for word, index in tokenizer.word_index.items():
    # 단어와 맵핑되는 사전 훈련된 임베딩 벡터값
    vector_value = embedding_dict.get(word)
    if vector_value is not None:
        embedding_matrix[index] = vector_value

# 결과 확인
embedding_matrix[2] # 위에서 확인한 값과 동일

 

이제 매핑한 임베딩 테이블을 사용해 모델을 생성

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

output_dim = 100

model = Sequential()
e = Embedding(vocab_size, output_dim, weights=[embedding_matrix], input_length=max_len, trainable=False)
model.add(e)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
model.fit(X_train, y_train, epochs=100, verbose=2)

 

사전 훈련된 GloVe 예제는 링크 참고: https://blog.keras.io/using-pre-trained-word-embeddings-in-a-keras-model.html

 

3. 사전훈련된 Word2Vec

# 사전훈련된 구글 Wor2Vec 모델을 로드해 저장
import gensim
urlretrieve("https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz", \
                           filename="GoogleNews-vectors-negative300.bin.gz")
word2vec_model = gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin.gz', binary=True)

# 모델 크기 확인
print(word2vec_model.vectors.shape) # (3000000, 300)

 

300의 차원을 가진 Word2Vec 벡터가 3,000,000개 존재

# 임베딩테이블 생성
embedding_matrix = np.zeros((vocab_size, 300))
np.shape(embedding_matrix) # (16, 300)

# OOV에 대해 None을 리턴하는 함수 구현
def get_vector(word):
    if word in word2vec_model:
        return word2vec_model[word]
    else:
        return None

# 단어집합의 모든 단어에 대해 None이 아닐 경우 Word2Vec의 임베딩벡터를 매핑
for word, index in tokenizer.word_index.items():
    # 단어와 맵핑되는 사전 훈련된 임베딩 벡터값
    vector_value = get_vector(word)
    if vector_value is not None:
        embedding_matrix[index] = vector_value

# 결과 확인: 동일
print(word2vec_model['nice']) # 사전훈련된 모델의 임베딩벡터값
print(tokenizer.word_index['nice']) # 매핑한 임베딩벡터값

 

이제 매핑한 임베딩 테이블을 사용해 모델을 생성

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

model = Sequential()
model.add(Input(shape=(max_len,), dtype='int32'))
e = Embedding(vocab_size, 300, weights=[embedding_matrix], input_length=max_len, trainable=False)
model.add(e)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
model.fit(X_train, y_train, epochs=100, verbose=2)

 

사전훈련된 워드임베딩을 이용한 텍스트분류 실습: https://wikidocs.net/86083

 

# 09-08 ELMo(Embeddings from Language Model)

문맥을 반영한 워드 임베딩(Contextualized Word Embedding): 같은 표기의 단어여도 문맥에 따라 다르게 임베딩

논문 링크: https://aclweb.org/anthology/N18-1202

 

1. biLM(Bidirectional Language Model)이란?

-biLM(Bidirectional Language Model) : 순방향 RNN과 역방향 RNN 양쪽 방향을 다 학습하는 언어모델

-양방향 RNN과 biLM의 차이: 양방향 RNN은 순방향 RNN과 역방향RNN의 은닉상태를 연결(concatenate)하여 다음층의 입력으로 사용하는 반면, biLM은  순방향 LM과 역방향 LM을 별개의 모델로 학습

-ELMo의 biLM은 은닉층이 최소 2개 이상인 다층구조(Multi-layer) 를 전제로 함

-각 시점의 입력은 문자 임베딩(character embedding)을 통해 얻은 단어벡터

*문자 임베딩: 합성곱 신경망을 이용해 벡터화하는 방법으로, 문맥과 상관없이 dog와 doggy 등의 연관성을 찾아냄

 

2. biLM의 구조

"play"라는 시점에서 각 층의 출력값을 연결

요약: 특정 단어에 해당하는 시점에서, 순방향 LM과 역방향 LM의 "각 층의 출력값"을 연결, 가중합, 스칼라

이 때 "각 층의 출력값"이란? 첫 번째 층의 출력값은 임베딩층, 이후 층의 출력값은 은닉상태

이렇게 완성된 벡터를 ELMo 표현(representation) 이라고 함

이렇게 얻은 ELMo 표현과 기존의 임베딩벡터를 연결(concatenate)해서 입력으로 사용함

ELMo + GloVe

 

3. ELMo 로 스팸메일 분류 실습

텐서플로우 버전 1을 사용해야하므로 Colab 사용할 것

%tensorflow_version 1.x

 

아래 코드는 윈도우 명령프롬프트에서 수행할 것

pip install tensorflow-hub

 

패키지 임포트 및 ELMo 다운로드

import tensorflow_hub as hub
import tensorflow as tf
from keras import backend as K
import urllib.request
import pandas as pd
import numpy as np

# ELMo 다운로드
elmo = hub.Module("https://tfhub.dev/google/elmo/1", trainable=True)

sess = tf.Session()
K.set_session(sess)
sess.run(tf.global_variables_initializer())
sess.run(tf.tables_initializer())

 

스팸메일 데이터 준비: https://www.kaggle.com/uciml/sms-spam-collection-dataset

urllib.request.urlretrieve("https://raw.githubusercontent.com/mohitgupta-omg/Kaggle-SMS-Spam-Collection-Dataset-/master/spam.csv", filename="spam.csv")
data = pd.read_csv('spam.csv', encoding='latin-1')
data['v1'] = data['v1'].replace(['ham','spam'],[0,1])
y_data = list(data['v1'])
X_data = list(data['v2'])

# split train,test 8:2
n_of_train = int(len(X_data) * 0.8)
n_of_test = int(len(X_data) - n_of_train)
X_train = np.asarray(X_data[:n_of_train])
y_train = np.asarray(y_data[:n_of_train])
X_test = np.asarray(X_data[n_of_train:])
y_test = np.asarray(y_data[n_of_train:])

 

데이터 이동이 keras -> tensorflow -> keras 가 되도록 변환해주는 함수 정의

def ELMoEmbedding(x):
    return elmo(tf.squeeze(tf.cast(x, tf.string)), as_dict=True, signature="default")["default"]

 

모델 설계

from keras.models import Model
from keras.layers import Dense, Lambda, Input

input_text = Input(shape=(1,), dtype=tf.string)
embedding_layer = Lambda(ELMoEmbedding, output_shape=(1024, ))(input_text) # ELMo 임베딩층
hidden_layer = Dense(256, activation='relu')(embedding_layer) # 256개의 뉴런이 있는 은닉층
output_layer = Dense(1, activation='sigmoid')(hidden_layer) # 이진분류
model = Model(inputs=[input_text], outputs=output_layer)
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

 

훈련 데이터에 대해 모델 평가

history = model.fit(X_train, y_train, epochs=1, batch_size=60)

# 결과
Epoch 1/1
4457/4457 [==============================] - 1508s 338ms/step - loss: 0.1129 - acc: 0.9619

 

테스트 데이터에 대해 모델 평가

print("\n 테스트 정확도: %.4f" % (model.evaluate(X_test, y_test)[1]))

# 결과
1115/1115 [==============================] - 381s 342ms/step
테스트 정확도: 0.9803

 

# 09-10 Embedding Visualization

구글이 지원하는 임베딩 프로젝터(embedding projector)라는 데이터 시각화 도구 사용

임베딩 프로젝터 논문: https://arxiv.org/pdf/1611.05469v1.pdf

 

1. 모델로부터 파일 생성

학습을 마친 모델이 파일로 저장되어있다는 가정 하에, 아래 커맨드를 통해 시각화에 필요한 2개의 tsv 파일을 생성

!python -m gensim.scripts.word2vec2tensor --input 모델이름 --output 모델이름

 

결과로는 '모델 이름_metadata.tsv'와 '모델 이름_tensor.tsv'라는 파일이 생성됨

 

2. 임베딩 프로젝터를 이용해 시각화

아래 사이트에 접속해 좌측 상단의 Load 버튼 클릭, 2개의 Choose file 중 위에는 tensor.tsv를, 아래에는 metadata.tsc를 업로드

링크:  https://projector.tensorflow.org/

 

# 09-11 Document Embedding

문서 벡터를 얻는 방법: Doc2Vec, Set2Vec 등 패키지 사용 or 문서내에 존재하는 단어벡터들의 평균을 문서벡터로 사용

 

1. 문서벡터를 이용한 추천시스템 실습(Document Embedding)

(1) 데이터 크롤링 및 전처리 (코드생략)

책 이미지와 줄거리 링크:  https://drive.google.com/file/d/15Q7DZ7xrJsI2Hji-WbkU9j1mwnODBd5A/view?usp=sharing

 

(2) 사전훈련된 워드임베딩 사용하기

초기 단어 벡터값으로 사전훈련된 워드임베딩을 사용하면 성능을 높일 수 있음

urllib.request.urlretrieve("https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz", \
                           filename="GoogleNews-vectors-negative300.bin.gz")

word2vec_model = Word2Vec(size = 300, window=5, min_count = 2, workers = -1)
word2vec_model.build_vocab(corpus)
word2vec_model.intersect_word2vec_format('GoogleNews-vectors-negative300.bin.gz', lockf=1.0, binary=True)
word2vec_model.train(corpus, total_examples = word2vec_model.corpus_count, epochs = 15)

 

(3) 단어벡터 평균 구하기

def get_document_vectors(document_list):
    document_embedding_list = []

    # 각 문서에 대해서
    for line in document_list:
        doc2vec = None
        count = 0
        for word in line.split():
            if word in word2vec_model.wv.vocab:
                count += 1
                # 해당 문서에 있는 모든 단어들의 벡터값을 더한다.
                if doc2vec is None:
                    doc2vec = word2vec_model[word]
                else:
                    doc2vec = doc2vec + word2vec_model[word]

        if doc2vec is not None:
            # 단어 벡터를 모두 더한 벡터의 값을 문서 길이로 나눠준다.
            doc2vec = doc2vec / count
            document_embedding_list.append(doc2vec)

    # 각 문서에 대한 문서 벡터 리스트를 리턴
    return document_embedding_list
    
document_embedding_list = get_document_vectors(df['cleaned'])

 

(4) 추천시스템 구현

# 각 문서벡터 간 코사인 유사도 구하기
cosine_similarities = cosine_similarity(document_embedding_list, document_embedding_list)
print('코사인 유사도 매트릭스의 크기 :',cosine_similarities.shape) # (2381, 2381)

# 선택한 책에 대해 가장 줄거리가 유사한 5개의 책 리턴하는 함수
def recommendations(title):
    books = df[['title', 'image_link']]

    # 책의 제목을 입력하면 해당 제목의 인덱스를 리턴받아 idx에 저장.
    indices = pd.Series(df.index, index = df['title']).drop_duplicates()    
    idx = indices[title]

    # 입력된 책과 줄거리(document embedding)가 유사한 책 5개 선정.
    sim_scores = list(enumerate(cosine_similarities[idx]))
    sim_scores = sorted(sim_scores, key = lambda x: x[1], reverse = True)
    sim_scores = sim_scores[1:6]

    # 가장 유사한 책 5권의 인덱스
    book_indices = [i[0] for i in sim_scores]

    # 전체 데이터프레임에서 해당 인덱스의 행만 추출. 5개의 행을 가진다.
    recommend = books.iloc[book_indices].reset_index(drop=True)

    fig = plt.figure(figsize=(20, 30))

    # 데이터프레임으로부터 순차적으로 이미지를 출력
    for index, row in recommend.iterrows():
        response = requests.get(row['image_link'])
        img = Image.open(BytesIO(response.content))
        fig.add_subplot(1, 5, index + 1)
        plt.imshow(img)
        plt.title(row['title'])

# 좋아하는 책 제목 입력해 유사한 책 제목 및 표지 출력해보기
recommendations("The Da Vinci Code")

 

2. 워드 임베딩의 평균으로 문서 임베딩 구하기(Average Word Embedding)

(1) 데이터 로드 및 전처리(코드생략)

케라스 영화 리뷰 데이터

import numpy as np
from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing.sequence import pad_sequences

# 등장 빈도 순위가 20000 이상인 단어는 로드할 때 제거됨
vocab_size = 20000

(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=vocab_size)

 

(2) 모델 설계

Embedding() 다음에 GlobalAveragePooling1D()을 추가하면 해당 문장의 모든 단어 벡터들의 평균 벡터를 구함

from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.layers import Dense, Embedding, GlobalAveragePooling1D
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

embedding_dim = 64

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))

# 모든 단어 벡터의 평균을 구한다.
model.add(GlobalAveragePooling1D())
model.add(Dense(1, activation='sigmoid')) # 이진분류

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('embedding_average_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['acc'])
model.fit(X_train, y_train, batch_size=32, epochs=10, callbacks=[es, mc], validation_split=0.2)

 

(3) 테스트 데이터에 대해 모델 평가

loaded_model = load_model('embedding_average_model.h5')
print("\n 테스트 정확도: %.4f" % (loaded_model.evaluate(X_test, y_test)[1])) # 0.8876

 

3. Doc2Vec 으로 사업보고서 유사도 계산 실습

Doc2Vec 논문 링크 : https://arxiv.org/abs/1405.4053

 

(1) 데이터 로드 및 전처리

형태소 분석기 Mecab을 원활하게 사용하려면 Colab에서 할 것

# dart.csv 파일 다운로드
!wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?export=download&id=1XS0UlE8gNNTRjnL6e64sMacOhtVERIqL' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=1XS0UlE8gNNTRjnL6e64sMacOhtVERIqL" -O dart.csv && rm -rf /tmp/cookies.txt

# 형태소 분석기 Mecab 설치
!pip install konlpy
!pip install mecab-python
!bash <(curl -s https://raw.githubusercontent.com/konlpy/konlpy/master/scripts/mecab.sh)

import pandas as pd
from konlpy.tag import Mecab
from gensim.models.doc2vec import TaggedDocument
from tqdm import tqdm
df = pd.read_csv('/content/dart.csv',  sep=',')
df = df.dropna()

# '제목'과 '본문'을 리스트로 저장
mecab = Mecab()

tagged_corpus_list = []

for index, row in tqdm(df.iterrows(), total=len(df)):
  text = row['business']
  tag = row['name']
  tagged_corpus_list.append(TaggedDocument(tags=[tag], words=mecab.morphs(text)))

TaggedDocument의 words에는 토큰화된 보고서 내용이, tags에는 보고서 제목이 저장되었다.

 

(2) Doc2Vec 학습 및 테스트(1시간 이상 소요)

from gensim.models import doc2vec

model = doc2vec.Doc2Vec(vector_size=300, alpha=0.025, min_alpha=0.025, workers=8, window=8)

# Vocabulary 빌드
model.build_vocab(tagged_corpus_list)
print(f"Tag Size: {len(model.docvecs.doctags.keys())}", end=' / ')

# Doc2Vec 학습
model.train(tagged_corpus_list, total_examples=model.corpus_count, epochs=50)

# 모델 저장
model.save('dart.doc2vec')

 

코드를 다 수행하고나면 3개의 파일이 생

  • dart.doc2vec
  • dart.doc2vec.trainables.syn1neg.npy
  • dart.doc2vec.wv.vectors.npy
# 테스트해보기
similar_doc = model.docvecs.most_similar('동화약품')
print(similar_doc)

 

# 09-12 한국어 위키피디아로 Word2Vec 실습

1. 위키피디아 데이터 다운로드 및 전처리

# 데이터 파싱 패키지
pip install wikiextractor

# 형태소 분석기
# Colab에 Mecab 설치
!git clone https://github.com/SOMJANG/Mecab-ko-for-Google-Colab.git
%cd Mecab-ko-for-Google-Colab
!bash install_mecab-ko_on_colab190912.sh

# 위키피디아 데이터 다운
!wget https://dumps.wikimedia.org/kowiki/latest/kowiki-latest-pages-articles.xml.bz2

# 데이터 파싱
!python -m wikiextractor.WikiExtractor kowiki-latest-pages-articles.xml.bz2

# AA ~ AF 디렉토리 안의 모든 파일들의 경로를 파이썬의 리스트 형태로 저장
def list_wiki(dirname):
    filepaths = []
    filenames = os.listdir(dirname)
    for filename in filenames:
        filepath = os.path.join(dirname, filename)

        if os.path.isdir(filepath):
            # 재귀 함수
            filepaths.extend(list_wiki(filepath))
        else:
            find = re.findall(r"wiki_[0-9][0-9]", filepath)
            if 0 < len(find):
                filepaths.append(filepath)
    return sorted(filepaths)
filepaths = list_wiki('text')

# 모든 파일을 하나로 합침
with open("output_file.txt", "w") as outfile:
    for filename in filepaths:
        with open(filename) as infile:
            contents = infile.read()
            outfile.write(contents)

 

2. 형태소 분석

from tqdm import tqdm
from konlpy.tag import Mecab 

mecab = Mecab() # 형태소 분석기

f = open('output_file.txt', encoding="utf8")
lines = f.read().splitlines()

result = []

for line in tqdm(lines):
  # 빈 문자열이 아닌 경우에만 수행
  if line:
    result.append(mecab.morphs(line))

 

3. Word2Vec 학습

from gensim.models import Word2Vec
model = Word2Vec(result, size=100, window=5, min_count=5, workers=4, sg=0)

# 결과 확인
model_result1 = model.wv.most_similar("대한민국")
print(model_result1)