by museonghwang

FastText의 이해와 Word2Vec과의 비교

|

  1. FastText 개요
  2. Word2Vec의 대표적인 문제점
    • 2.1 OOV(Out-of-Vocabulary) 문제
    • 2.2 형태학적 특징을 반영할 수 없는 문제
  3. FastText 내부 단어(subword)의 이해
  4. FastText Pre-training
  5. FastText의 강점
    • 5.1 모르는 단어(Out Of Vocabulary, OOV)에 대한 대응
    • 5.2 단어 집합 내 빈도 수가 적었던 단어(Rare Word)에 대한 대응
    • 5.3 단어 집합 내 노이즈가 많은 코퍼스에 대한 대응
  6. Word2Vec Vs. FastText
    • 6.1 gensim 패키지 버전확인
    • 6.2 Libaray Import
    • 6.3 훈련 데이터 이해
    • 6.4 훈련 데이터 전처리
    • 6.5 Word2Vec 훈련
  7. FastText Summaray


단어를 벡터로 만드는 또 다른 방법으로는 페이스북에서 개발한 FastText가 있습니다. Word2Vec 이후에 나온 것이기 때문에, 메커니즘 자체는 Word2Vec의 확장이라고 볼 수 있습니다. Word2Vec와 FastText와의 가장 큰 차이점이라면 Word2Vec는 단어를 쪼개질 수 없는 단위로 생각한다면, FastText는 하나의 단어 안에도 여러 단어들이 존재하는 것으로 간주합니다. 내부 단어. 즉, 서브워드(subword)를 고려하여 학습합니다.



1. FastText 개요

FastTextWord2Vec의 개량 알고리즘으로 Subword를 고려한 알고리즘 입니다. Word2Vec 이후에 나온 것이기 때문에 메커니즘 자체는 Word2Vec 의 확장이라고 볼 수 있습니다.

예를 들어 eateating이 있다고 가정해보겠습니다. 훈련 데이터에서 eat 는 충분히 많이 등장해서, 학습이 충분히 잘 되었지만 eating은 잘 등장하지 않아서 제대로 된 임베딩 값을 얻지 못한다고 가정해보겠습니다.

image


즉, Word2Vec의 문제점 으로 OOV(Out-of-Vocabulary) 문제와, 하나의 단어에 고유한 벡터를 할당하므로 단어의 형태학적 특징을 반영할 수 없다는 문제 가 있습니다. 이때 FastText 의 아이디어는 eat 이라는 공통적인 내부 단어를 가지고 있는데 이를 활용할 수는 없을까 라는 의문에서 시작됩니다.

image


2. Word2Vec의 대표적인 문제점


2.1 OOV(Out-of-Vocabulary) 문제

image


Word2VecVocabulary“tensor”“flow” 가 있더라도, “tensorflow” 라는 단어가 Vocabulary 에 없다면, “tensorflow” 의 벡터값을 얻을 수 없습니다.


2.2 형태학적 특징을 반영할 수 없는 문제

image


위 단어들은 eat 이라는 동일한 어근을 가집니다. 하지만 Word2Vec 에서의 각 단어는 각 벡터의 값을 가질뿐입니다. 즉, 하나의 단어에 고유한 벡터를 할당하므로 단어의 형태학적 특징을 반영할 수 없다는 문제가 있습니다.


3. FastText 내부 단어(subword)의 이해

FastText에서는 각 단어는 글자 단위 n-gram의 구성으로 취급 합니다. $n$ 을 몇으로 결정하는지에 따라서 단어들이 얼마나 분리되는지 결정됩니다. 예를 들어서 $n$ 을 3으로 잡은 트라이그램(tri-gram)의 경우, appleapp, ppl, ple 로 분리하고 이들을 벡터로 만듭니다. 더 정확히는 시작과 끝을 의미하는 <, >를 도입하여 아래의 5개 내부 단어(subword) 토큰을 벡터로 만듭니다.

# n = 3인 경우
<ap, app, ppl, ple, le>


그리고 여기에 추가적으로 하나를 더 벡터화 하는데, 기존 단어에 <, 와 >를 붙인 토큰 입니다.

# 특별 토큰
<apple>


다시 말해 $n=3$ 인 경우, FastText 는 단어 apple 에 대해서 다음의 6개의 토큰을 벡터화하는 것입니다.

# n = 3인 경우
<ap, app, ppl, ple, le>, <apple>


그런데 실제 사용할 때는 $n$ 의 최소값과 최대값으로 범위를 설정 할 수 있는데, 기본값으로는 각각 3과 6으로 설정되어져 있습니다. 다시 말해 최소값 = 3, 최대값 = 6인 경우라면, 단어 apple에 대해서 FastText 는 아래 내부 단어들을 벡터화합니다.

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


여기서 내부 단어들을 벡터화한다는 의미저 단어들에 대해서 Word2Vec을 수행한다는 의미 입니다. 위와 같이 내부 단어들의 벡터값을 얻었다면, 단어 apple벡터값은 저 위 벡터값들의 총 합으로 구성 합니다.

apple = <ap + app + ppl + ppl + le> + <app + appl + pple + ple> + <appl + pple> + , ..., +<apple>


다른 예문으로 한번 더 살펴보고 확실하게 이해해보겠습니다. 위에서 FastText는 단어를 Character 단위의 n-gram으로 간주하며, $n$ 을 몇으로 하느냐에 따라서 단어가 얼마나 분리되는지가 결정 된다고 했습니다. 단어 ‘eating’을 예를 들어보겠습니다.

단어 eating에 시작과 끝을 의미하는 ‘<’와 ‘>’를 추가합니다.

image


n-gram을 기반으로 단어를 분리합니다. 이때 n = 3.

image


실제로는, 주로 n은 범위로 설정해줍니다. 이때 n = 3 ~ 6.

image


훈련 데이터를 N-gram의 셋으로 구성하였다면, 훈련 방법 자체는 SGNS(Skip-gram with Negative Sampleing)와 동일 합니다. 단, Word가 아니라 subwords들이 최종 학습 목표 이며, 이들의 합을 Word의 vector로 간주합니다.

image


의문점이 있을 수 있는데, 단어에 <, >를 해주는 이유 를 살펴보면 다음과 같습니다.

단어 양끝에 <, >를 해주지 않으면 실제로 독립적인 단어와 특정 단어의 n-gram인 경우를 구분하기 어렵습니다. 가령, where의 n-gram 중 하나인 her도 존재하지만 독립적인 단어 her 또한 Vocabulary에 존재할 수 있습니다. 이 때, 독립적인 단어 her는 ’<her>‘ 가 되므로서 where 내의 her와 구분할 수 있습니다.


4. FastText Pre-training

FastText의 훈련 과정을 이해해보겠습니다. 여기서는 SGNS(Skip-gram with Negative Sampleing)을 사용합니다. 현재의 목표는 중심 단어 eating으로부터 주변 단어 am과 food를 예측하는 것 입니다.

image


앞서 언급하였듯이 단어 eating은 아래와 같이 n-gram들의 합으로 나타냅니다.

image


우리가 해야하는 것은 eating으로부터 am과 food를 예측하는 것 입니다.

image


Negative Sampling이므로 실제 주변 단어가 아닌 단어들도 필요 합니다.

image


우리가 해야하는 것은 eating으로부터 am과 food를 예측하는 것이므로 eating과 am 그리고 eating과 food의 내적값에 시그모이드 함수를 지난 값은 1이 되도록 학습 하고, eating과 paris 그리고 eating과 earth의 내적값에 시그모이드 함수를 지난 값은 0이 되도록 학습 합니다.

image


이런 방법은 Word2Vec에서는 얻을 수 없었던 강점을 가집니다. 예를 들어 단어 Orange에 대해서 FastText를 학습했다고 해보겠습니다. n의 범위는 2-5로 하겠습니다.

image


그 후 Oranges라는 OOV 또는 희귀 단어가 등장했다고 해보겠습니다. Orange의 n-gram 벡터들을 이용하여 Oranges의 벡터값을 얻습니다.

image


또한 FastText는 오타에도 강건합니다.

image


5. FastText의 강점


5.1 모르는 단어(Out Of Vocabulary, OOV)에 대한 대응

FastText 의 인공 신경망을 학습한 후에는 데이터 셋의 모든 단어의 각 n-gram에 대해서 워드 임베딩이 됩니다. 이렇게 되면 장점은 데이터 셋만 충분한다면 위와 같은 내부 단어(Subword)를 통해 모르는 단어(Out Of Vocabulary, OOV)에 대해서도 다른 단어와의 유사도를 계산할 수 있다는 점 입니다.

가령, FastText에서 birthplace(출생지)란 단어를 학습하지 않은 상태라고 해봅시다. 하지만 다른 단어에서 birth와 place라는 내부 단어가 있었다면, FastText는 birthplace의 벡터를 얻을 수 있습니다. 이는 모르는 단어에 제대로 대처할 수 없는 Word2Vec, GloVe와는 다른 점입니다.


5.2 단어 집합 내 빈도 수가 적었던 단어(Rare Word)에 대한 대응

Word2Vec의 경우에는 등장 빈도 수가 적은 단어(rare word)에 대해서는 임베딩의 정확도가 높지 않다는 단점 이 있었습니다. 참고할 수 있는 경우의 수가 적다보니 정확하게 임베딩이 되지 않는 경우입니다.

하지만 FastText 의 경우, 만약 단어가 희귀 단어라도, 그 단어의 n-gram이 다른 단어의 n-gram과 겹치는 경우라면, Word2Vec과 비교하여 비교적 높은 임베딩 벡터값을 얻습니다.


5.3 단어 집합 내 노이즈가 많은 코퍼스에 대한 대응

FastText가 노이즈가 많은 코퍼스에서 강점을 가진 것 또한 이와 같은 이유입니다. 모든 훈련 코퍼스에 오타(Typo)나 맞춤법이 틀린 단어가 없으면 이상적이겠지만, 실제 많은 비정형 데이터에는 오타가 섞여있습니다. 그리고 오타가 섞인 단어는 당연히 등장 빈도수가 매우 적으므로 일종의 희귀 단어가 됩니다. 즉, Word2Vec에서는 오타가 섞인 단어는 임베딩이 제대로 되지 않지만, FastText는 이에 대해서도 일정 수준의 성능을 보입니다.

예를 들어 단어 apple과 오타로 p를 한 번 더 입력한 appple의 경우에는 실제로 많은 개수의 동일한 n-gram을 가질 것입니다.


6. Word2Vec Vs. FastText

간단한 실습을 통해 영어 Word2Vec와 FastText의 차이를 비교해보도록 하겠습니다.


6.1 gensim 패키지 버전확인

파이썬의 gensim 패키지에는 Word2Vec 을 지원하고 있어, gensim 패키지를 이용하면 손쉽게 단어를 임베딩 벡터로 변환시킬 수 있습니다. Word2Vec 을 학습하기 위한 gensim 패키지 버전을 확인합니다.

import gensim
gensim.__version__
[output]
'4.3.1'


6.2 Libaray Import

영어로 된 코퍼스를 다운받아 전처리를 수행하고, 전처리한 데이터를 바탕으로 Word2Vec 작업을 진행하겠습니다. 우선 필요 라이브러리를 불러옵니다.

import re
from lxml import etree
import urllib.request
import zipfile

import nltk
nltk.download('punkt')

from nltk.tokenize import word_tokenize, sent_tokenize
[output]
[nltk_data] Downloading package punkt to /root/nltk_data...
[nltk_data]   Unzipping tokenizers/punkt.zip.


6.3 훈련 데이터 이해

Word2Vec을 학습하기 위해서 데이터를 다운로드합니다. 사용할 훈련 데이터는, ted 영상들의 자막 데이터입니다. 파일의 형식은 xml 파일입니다. 해당 링크를 통해 내려받아 ted_en-20160408.xml 라는 이름의 파일을 설치할 수도 있고, 파이썬 코드를 통해 자동으로 설치할 수도 있습니다.

urllib.request.urlretrieve("https://raw.githubusercontent.com/GaoleMeng/RNN-and-FFNN-textClassification/master/ted_en-20160408.xml", filename="ted_en-20160408.xml")
[output]
('ted_en-20160408.xml', <http.client.HTTPMessage at 0x7fbaa9a92620>)


위의 코드를 통해 xml 파일을 내려받으면, 다음과 같은 파일을 볼 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<xml language="en"><file id="1">
  <head>
    <url>http://www.ted.com/talks/knut_haanaes_two_reasons_companies_fail_and_how_to_avoid_them</url>

    ...

    <content>Here are two reasons companies fail: they only do more of the same, or they only do what's new.

    ...

    So let me leave you with this. Whether you're an explorer by nature or whether you tend to exploit what you already know, don't forget: the beauty is in the balance.
    Thank you.
    (Applause)</content>
</file>
<file id="2">
  <head>
    <url>http://www.ted.com/talks/lisa_nip_how_humans_could_evolve_to_survive_in_space</url>
    
    ...

    (Applause)</content>
</file>
</xml>


훈련 데이터 파일은 xml 문법으로 작성되어 있어 자연어를 얻기 위해서는 전처리가 필요합니다. 얻고자 하는 실질적 데이터는 영어문장으로만 구성된 내용을 담고 있는 ’<content>‘’</content>‘ 사이의 내용입니다. 전처리 작업을 통해 xml 문법들은 제거하고, 해당 데이터만 가져와야 합니다. 뿐만 아니라, ’<content>‘’</content>‘ 사이의 내용 중에는 (Laughter)나 (Applause)와 같은 배경음을 나타내는 단어도 등장하는데 이 또한 제거해야 합니다.


6.4 훈련 데이터 전처리

targetXML = open('ted_en-20160408.xml', 'r', encoding='UTF8')
target_text = etree.parse(targetXML)

# xml 파일로부터 <content>와 </content> 사이의 내용만 가져온다.
parse_text = '\n'.join(target_text.xpath('//content/text()'))

# 정규 표현식의 sub 모듈을 통해 content 중간에 등장하는 (Audio), (Laughter) 등의 배경음 부분을 제거.
# 해당 코드는 괄호로 구성된 내용을 제거.
content_text = re.sub(r'\([^)]*\)', '', parse_text)


현재 영어 텍스트가 content_text 에 저장되어져 있습니다. 이에 대해서 NLTKsent_tokenize 를 통해서 문장을 구분해보겠습니다.

print('영어 텍스트의 개수 : {}'.format(len(content_text)))
[output]
영어 텍스트의 개수 : 24062319


# 입력 코퍼스에 대해서 NLTK를 이용하여 문장 토큰화를 수행.
sent_text = sent_tokenize(content_text)

# 각 문장에 대해서 구두점을 제거하고, 대문자를 소문자로 변환.
normalized_text = []
for string in sent_text:
    tokens = re.sub(r"[^a-z0-9]+", " ", string.lower())
    normalized_text.append(tokens)

# 각 문장에 대해서 NLTK를 이용하여 단어 토큰화를 수행.
result = [word_tokenize(sentence) for sentence in normalized_text]

print('총 샘플의 개수 : {}'.format(len(result)))
[output]
총 샘플의 개수 : 273424


총 문장 샘플의 개수는 273,424개입니다. 샘플 3개만 출력해보겠습니다.

for line in result[:3]: # 샘플 3개만 출력
    print(line)
[output]
['here', 'are', 'two', 'reasons', 'companies', 'fail', 'they', 'only', 'do', 'more', 'of', 'the', 'same', 'or', 'they', 'only', 'do', 'what', 's', 'new']
['to', 'me', 'the', 'real', 'real', 'solution', 'to', 'quality', 'growth', 'is', 'figuring', 'out', 'the', 'balance', 'between', 'two', 'activities', 'exploration', 'and', 'exploitation']
['both', 'are', 'necessary', 'but', 'it', 'can', 'be', 'too', 'much', 'of', 'a', 'good', 'thing']


상위 3개 문장만 출력해보았는데 토큰화가 잘 수행되었음을 볼 수 있습니다. 이제 Word2Vec 모델에 텍스트 데이터를 훈련시킵니다.


6.5 Word2Vec 훈련

여기서 Word2Vec 의 하이퍼파라미터값은 다음과 같습니다.

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


from gensim.models import Word2Vec

model = Word2Vec(
    sentences=result,
    vector_size=100,
    window=5,
    min_count=5,
    workers=4,
    sg=0
)


Word2Vec 에 대해서 학습을 진행하였습니다. Word2Vec 는 입력한 단어에 대해서 가장 유사한 단어들을 출력하는 model.wv.most_similar을 지원합니다. 특정 단어와 가장 유사한 단어들을 추출해보겠습니다. 이때 코사인 유사도라는 것을 유사도 메트릭으로 사용하며, 값의 범위는 -1 ~ 1 입니다.

model.wv.most_similar("drink")
[output]
[('drinking', 0.7223415374755859),
 ('milk', 0.7076201438903809),
 ('buy', 0.6865729093551636),
 ('eat', 0.6797118186950684),
 ('rent', 0.6692221760749817),
 ('coffee', 0.6465736627578735),
 ('burn', 0.6400552988052368),
 ('wash', 0.6392359137535095),
 ('wear', 0.6316686868667603),
 ('steal', 0.6246187686920166)]


입력 단어에 대해서 유사한 단어를 찾아내는 코드에 이번에는 electrofishing이라는 단어를 넣어보겠습니다. 해당 코드는 정상 작동하지 않고 에러를 발생시킵니다.

model.wv.most_similar("electrofishing")
[output]
KeyError: "Key 'electrofishing' not present in vocabulary"


에러 메시지는 단어 집합(Vocabulary)에 electrofishing이 존재하지 않는다고 합니다. 이처럼 Word2Vec는 학습 데이터에 존재하지 않는 단어. 즉, 모르는 단어에 대해서는 임베딩 벡터가 존재하지 않기 때문에 단어의 유사도를 계산할 수 없습니다.


6.6 FastText 훈련

이번에는 전처리 코드는 그대로 사용하고 Word2Vec 학습 코드만 FastText 학습 코드로 변경하여 실행해보겠습니다.

model = FastText(
    result,
    vector_size=100,
    window=5,
    min_count=5,
    workers=4,
    sg=1
)


model.wv.most_similar("drink")
[output]
[('drinks', 0.8812541961669922),
 ('drinkable', 0.7786468267440796),
 ('sweat', 0.7572851181030273),
 ('drinking', 0.7571462988853455),
 ('cigarette', 0.7453484535217285),
 ('cheat', 0.7437908053398132),
 ('alcohol', 0.739535927772522),
 ('grin', 0.738060474395752),
 ('burn', 0.735490083694458),
 ('sweater', 0.7331915497779846)]


이번에는 electrofishing에 대해서 유사 단어를 찾아보도록 하겠습니다.

model.wv.most_similar("electrofishing")
[output]
[('electrolyte', 0.859164297580719),
 ('electrolux', 0.8576809763908386),
 ('electroencephalogram', 0.8483548760414124),
 ('electroshock', 0.8433628082275391),
 ('electro', 0.8416800498962402),
 ('electrogram', 0.8294049501419067),
 ('electrochemical', 0.8202094435691833),
 ('electron', 0.8182265758514404),
 ('electric', 0.8150432705879211),
 ('airbus', 0.8121421933174133)]


Word2Vec는 학습하지 않은 단어에 대해서 유사한 단어를 찾아내지 못 했지만, FastText는 유사한 단어를 계산해서 출력하고 있음을 볼 수 있습니다.


7. FastText Summaray

충분히 잘 학습된 FastText는 전체 Word가 아니라 Subword들의 유사도를 반영함을 확인할 수 있습니다.

image

Read more

한국어 위키피디아로 Word2Vec 학습하기

|

한국어 위키피디아 데이터로 Word2Vec 학습을 진행해보겠습니다.


1. 위키피디아로부터 데이터 다운로드 및 통합

위키피디아로부터 데이터를 파싱하기 위한 파이썬 패키지인 wikiextractor 를 설치하겠습니다.

pip install wikiextractor


위키피디아 덤프(위키피디아 데이터)를 다운로드 하겠습니다.

wget https://dumps.wikimedia.org/kowiki/latest/kowiki-latest-pages-articles.xml.bz2


wikiextractor 를 사용하여 위키피디아 덤프를 파싱합니다.

python -m wikiextractor.WikiExtractor kowiki-latest-pages-articles.xml.bz2
[output]
INFO: Preprocessing 'kowiki-latest-pages-articles.xml.bz2' to collect template definitions: this may take some time.
INFO: Preprocessed 100000 pages
INFO: Preprocessed 200000 pages
INFO: Preprocessed 300000 pages
INFO: Preprocessed 400000 pages
INFO: Preprocessed 500000 pages
INFO: Preprocessed 600000 pages
INFO: Preprocessed 700000 pages
INFO: Preprocessed 800000 pages
INFO: Preprocessed 900000 pages
INFO: Preprocessed 1000000 pages
INFO: Preprocessed 1100000 pages
INFO: Preprocessed 1200000 pages
INFO: Preprocessed 1300000 pages
INFO: Preprocessed 1400000 pages
INFO: Preprocessed 1500000 pages
INFO: Preprocessed 1600000 pages
INFO: Preprocessed 1700000 pages
INFO: Preprocessed 1800000 pages
INFO: Loaded 63223 templates in 375.0s
INFO: Starting page extraction from kowiki-latest-pages-articles.xml.bz2.
INFO: Using 3 extract processes.
INFO: Extracted 100000 articles (1147.9 art/s)
INFO: Extracted 200000 articles (1685.0 art/s)
INFO: Extracted 300000 articles (1874.9 art/s)
INFO: Extracted 400000 articles (1980.8 art/s)
INFO: Extracted 500000 articles (2131.4 art/s)
INFO: Extracted 600000 articles (2112.3 art/s)
INFO: Extracted 700000 articles (2165.1 art/s)
INFO: Extracted 800000 articles (2406.7 art/s)
INFO: Extracted 900000 articles (5877.7 art/s)
INFO: Extracted 1000000 articles (3890.4 art/s)
INFO: Extracted 1100000 articles (2399.0 art/s)
INFO: Extracted 1200000 articles (2572.4 art/s)
INFO: Extracted 1300000 articles (2481.7 art/s)
INFO: Extracted 1400000 articles (2611.3 art/s)
INFO: Finished 3-process extraction of 1400056 articles in 634.2s (2207.6 art/s)


현재 경로에 있는 디렉토리와 파일들의 리스트를 받아오겠습니다.

%ls
kowiki-latest-pages-articles.xml.bz2  sample_data/  text/


text 라는 디렉토리 안에는 또 어떤 디렉토리들이 있는지 파이썬을 사용하여 확인해보겠습니다.

import os
import re

os.listdir('text')
[output]
['AG', 'AA', 'AI', 'AH', 'AD', 'AB', 'AE', 'AJ', 'AF', 'AC']


AA 라는 디렉토리의 파일들을 확인해보겠습니다.

%ls text/AA
[output]
wiki_00  wiki_12  wiki_24  wiki_36  wiki_48  wiki_60  wiki_72  wiki_84  wiki_96
wiki_01  wiki_13  wiki_25  wiki_37  wiki_49  wiki_61  wiki_73  wiki_85  wiki_97
wiki_02  wiki_14  wiki_26  wiki_38  wiki_50  wiki_62  wiki_74  wiki_86  wiki_98
wiki_03  wiki_15  wiki_27  wiki_39  wiki_51  wiki_63  wiki_75  wiki_87  wiki_99
wiki_04  wiki_16  wiki_28  wiki_40  wiki_52  wiki_64  wiki_76  wiki_88
wiki_05  wiki_17  wiki_29  wiki_41  wiki_53  wiki_65  wiki_77  wiki_89
wiki_06  wiki_18  wiki_30  wiki_42  wiki_54  wiki_66  wiki_78  wiki_90
wiki_07  wiki_19  wiki_31  wiki_43  wiki_55  wiki_67  wiki_79  wiki_91
wiki_08  wiki_20  wiki_32  wiki_44  wiki_56  wiki_68  wiki_80  wiki_92
wiki_09  wiki_21  wiki_33  wiki_45  wiki_57  wiki_69  wiki_81  wiki_93
wiki_10  wiki_22  wiki_34  wiki_46  wiki_58  wiki_70  wiki_82  wiki_94
wiki_11  wiki_23  wiki_35  wiki_47  wiki_59  wiki_71  wiki_83  wiki_95


텍스트 파일로 변환된 위키피디아 한국어 덤프는 총 10개의 디렉토리로 구성 되어져 있습니다. AA ~ AJ 의 디렉토리로 각 디렉토리 내에는 ‘wiki_00 ~ wiki_약 90내외의 숫자’ 의 파일들이 들어있습니다. 다시 말해 각 디렉토리에는 약 90여개의 파일들이 들어있습니다. 각 파일들을 열어보면 다음과 같은 구성이 반복되고 있습니다.

<doc id="문서 번호" url="실제 위키피디아 문서 주소" title="문서 제목">

내용

</doc>


예를 들어서 AA 디렉토리의 wiki_00 파일을 읽어보면, 지미 카터에 대한 내용이 나옵니다.

<doc id="5" url="https://ko.wikipedia.org/wiki?curid=5" title="지미 카터">
지미 카터
제임스 얼 "지미" 카터 주니어(, 1924년 10월 1일 ~ )는 민주당 출신 미국 39번째 대통령(1977년 ~ 1981년)이다.
지미 카터는 조지아 주 섬터 카운티 플레인스 마을에서 태어났다. 조지아 공과대학교를 졸업하였다. 그 후 해군에 들어가 전함·원자력·잠수함의 승무원으로 일하였다. 1953년 미국 해군 대
위로 예편하였고 이후 땅콩·면화 등을 가꿔 많은 돈을 벌었다.
... 이하 중략...
</doc>


이제 이 10개 AA ~ AJ 디렉토리 안의 wiki 숫자 형태의 수많은 파일들을 하나로 통합하는 과정을 진행해야 합니다. AA ~ AJ 디렉토리 안의 모든 파일들의 경로를 파이썬의 리스트 형태로 저장하겠습니다.

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


총 파일의 개수를 확인하겠습니다.

len(filepaths)
[output]
970


총 파일의 개수는 970개 입니다. 이제 output_file.txt 라는 파일에 970개의 파일을 전부 하나로 합치겠습니다.

with open("output_file.txt", "w") as outfile:
    for filename in filepaths:
        with open(filename) as infile:
            contents = infile.read()
            outfile.write(contents)


파일을 읽고 10줄만 출력해보겠습니다.

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

i = 0
while True:
    line = f.readline()
    if line != '\n':
        i = i + 1
        print("%d번째 줄 :"%i + line)
    if i==10:
        break 
f.close()
[output]
1번째 줄 :<doc id="5" url="https://ko.wikipedia.org/wiki?curid=5" title="지미 카터">

2번째 줄 :지미 카터

3번째 줄 :제임스 얼 카터 주니어(, 1924년 10월 1일~)는 민주당 출신 미국의 제39대 대통령(1977년~1981년)이다.

4번째 줄 :생애.

5번째 줄 :어린 시절.

6번째 줄 :지미 카터는 조지아주 섬터 카운티 플레인스 마을에서 태어났다.

7번째 줄 :조지아 공과대학교를 졸업하였다. 그 후 해군에 들어가 전함·원자력·잠수함의 승무원으로 일하였다. 1953년 미국 해군 대위로 예편하였고 이후 땅콩·면화 등을 가꿔 많은 돈을 벌었다. 그의 별명이 "땅콩 농부" (Peanut Farmer)로 알려졌다.

8번째 줄 :정계 입문.

9번째 줄 :1962년 조지아주 상원 의원 선거에서 낙선하였으나, 그 선거가 부정선거 였음을 입증하게 되어 당선되고, 1966년 조지아 주지사 선거에 낙선하지만, 1970년 조지아 주지사 선거에서 당선됐다. 대통령이 되기 전 조지아주 상원의원을 두번 연임했으며, 1971년부터 1975년까지 조지아 지사로 근무했다. 조지아 주지사로 지내면서, 미국에 사는 흑인 등용법을 내세웠다.

10번째 줄 :대통령 재임.


2. 형태소 분석

형태소 분석기 Mecab 을 사용하여 토큰화를 진행해보겠습니다.

from tqdm import tqdm
from konlpy.tag import Mecab 

mecab = Mecab()


우선 output_file 에는 총 몇 줄이 있는지 확인하겠습니다.

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

lines = f.read().splitlines()
print(len(lines))
[output]
11003273


11,003,273개의 줄이 존재합니다. 상위 10개만 출력해보겠습니다.

lines[:10]
[output]
['<doc id="5" url="https://ko.wikipedia.org/wiki?curid=5" title="지미 카터">',
 '지미 카터',
 '',
 '제임스 얼 카터 주니어(, 1924년 10월 1일~)는 민주당 출신 미국의 제39대 대통령(1977년~1981년)이다.',
 '생애.',
 '어린 시절.',
 '지미 카터는 조지아주 섬터 카운티 플레인스 마을에서 태어났다.',
 '조지아 공과대학교를 졸업하였다. 그 후 해군에 들어가 전함·원자력·잠수함의 승무원으로 일하였다. 1953년 미국 해군 대위로 예편하였고 이후 땅콩·면화 등을 가꿔 많은 돈을 벌었다. 그의 별명이 "땅콩 농부" (Peanut Farmer)로 알려졌다.',
 '정계 입문.',
 '1962년 조지아주 상원 의원 선거에서 낙선하였으나, 그 선거가 부정선거 였음을 입증하게 되어 당선되고, 1966년 조지아 주지사 선거에 낙선하지만, 1970년 조지아 주지사 선거에서 당선됐다. 대통령이 되기 전 조지아주 상원의원을 두번 연임했으며, 1971년부터 1975년까지 조지아 지사로 근무했다. 조지아 주지사로 지내면서, 미국에 사는 흑인 등용법을 내세웠다.']


두번째 줄을 보면 아무런 단어도 들어있지 않은 ’‘ 와 같은 줄도 존재합니다. 해당 문자열은 형태소 분석에서 제외하도록 하고 형태소 분석을 수행하겠습니다.

result = []

for line in tqdm(lines):
    # 빈 문자열이 아닌 경우에만 수행
    if line:
        result.append(mecab.morphs(line))
[output]
100%|██████████| 11003273/11003273 [14:52<00:00, 12328.56it/s]


빈 문자열은 제외하고 형태소 분석을 진행했습니다. 이제 몇 개의 줄. 즉, 몇 개의 문장이 존재하는지 확인해보겠습니다.

len(result)
[output]
7434242


7,434,242개로 문장의 수가 줄었습니다.


3. Word2Vec 학습

형태소 분석을 통해서 토큰화가 진행된 상태이므로 Word2Vec 을 학습합니다.

from gensim.models import Word2Vec

model = Word2Vec(
    result,
    vector_size=100,
    window=5,
    min_count=5,
    workers=4,
    sg=0
)


model_result1 = model.wv.most_similar("대한민국")
print(model_result1)
[output]
[('한국', 0.7304196357727051), ('미국', 0.6692186594009399), ('일본', 0.6360577344894409), ('부산', 0.5860438346862793), ('홍콩', 0.5758174061775208), ('태국', 0.5587795972824097), ('오스트레일리아', 0.5537664294242859), ('서울', 0.551203191280365), ('중화민국', 0.5402842164039612), ('대구', 0.5301536321640015)]


model_result2 = model.wv.most_similar("어벤져스")
print(model_result2)
[output]
[('어벤저스', 0.7947301864624023), ('엑스맨', 0.7730240821838379), ('아이언맨', 0.7690291404724121), ('스파이더맨', 0.7656212449073792), ('테일즈', 0.7625358700752258), ('에일리언', 0.7526578903198242), ('트랜스포머', 0.7525742053985596), ('솔저', 0.7348356246948242), ('헐크', 0.7259199619293213), ('스타트렉', 0.7257904410362244)]


model_result3 = model.wv.most_similar("반도체")
print(model_result3)
[output]
[('연료전지', 0.794660210609436), ('집적회로', 0.7878057956695557), ('전자', 0.7571590542793274), ('웨이퍼', 0.7522547841072083), ('실리콘', 0.7403302788734436), ('트랜지스터', 0.7359127402305603), ('그래핀', 0.7252676486968994), ('PCB', 0.703812837600708), ('가전제품', 0.6887932419776917), ('전기차', 0.6878519058227539)]


model_result4 = model.wv.most_similar("자연어")
print(model_result4)
[output]
[('구문', 0.7062239646911621), ('메타데이터', 0.7035123109817505), ('시각화', 0.6815322637557983), ('설명서', 0.6801173686981201), ('텍스트', 0.6770368814468384), ('말뭉치', 0.6747072339057922), ('매크로', 0.6737524271011353), ('데이터베이스', 0.6698500514030457), ('스키마', 0.6683913469314575), ('XML', 0.6677115559577942)]

Read more

Skip-Gram with Negative Sampling

|

  1. Skip-Gram
  2. Skip-Gram with Negative Sampling(SGNS)
  3. Skip-Gram with Negative Sampling(SGNS) example
  4. 네거티브 샘플링을 이용한 Word2Vec 구현
    • 4.1 20뉴스그룹 데이터 전처리하기
    • 4.2 네거티브 샘플링을 통한 데이터셋 구성하기
    • 4.3 Skip-Gram with Negative Sampling(SGNS) 구현하기
    • 4.4 결과 확인하기
  5. SGNS Summaray


Negative Sampling 방법은 Word2Vec의 CBOW와 Skip-gram 모두 단어 개수가 많아질수록 계산 복잡도가 증가하여 연산 속도가 저하된다는 한계점을 보완하기 위해 제안되었습니다. CBOW와 Skip-gram는 역전파 과정에서 단어 집합의 크기만큼 연산이 필요합니다. 따라서 단어 개수가 많아질수록 계산 복잡도 역시 높아지고, 이는 모델 학습 속도 저하를 유발합니다. 즉, 기존 Word2Vec 방식은 모든 단어의 임베딩 벡터값을 업데이트합니다. Negative Sampling은 이러한 계산 복잡도를 줄이기 위해 고안된 효율적인 기술입니다.



1. Skip-Gram

Word2Vec 의 학습방법 중 CBOW 에서는 주변 단어를 통해 중심 단어를 예측했다면, Skip-gram중심 단어로부터 주변 단어를 예측 합니다. 인공 신경망을 도식화해보면 아래와 같습니다.

image


윈도우 크기가 2일 때, Skip-gram은 다음과 같이 데이터셋을 구성합니다.

image

image


Skip-gram중심 단어로부터 주변 단어를 예측 합니다.

image


image


Skip-gram은 입력층, 투사층, 출력층 3개의 층으로 구성된 신경망이며, 소프트맥스 함수를 지난 예측값(Prediction)과 실제값으로부터 오차(error)를 구합니다.

image


소프트맥스 함수를 지난 예측값(Prediction)과 실제값으로부터 오차(error)를 구하고, 이로부터 embedding table을 update 합니다.

image


실제로는 CBoWSkip-gram 을 방금 설명한 바와 같이 구현하지는 않습니다. 그 이유는 아래와 같이 구현한다면 속도가 너무 느리기 때문 입니다.

image


2. Skip-Gram with Negative Sampling(SGNS)

Word2Vec 의 출력층에서는 소프트맥스 함수를 지난 단어 집합 크기의 벡터와 실제값인 원-핫 벡터와의 오차를 구하고 이로부터 임베딩 테이블에 있는 모든 단어에 대한 임베딩 벡터 값을 업데이트합니다. 만약 단어 집합의 크기가 수만 이상에 달한다면 이 작업은 굉장히 무거운 작업 이므로, Word2Vec 은 꽤나 학습하기에 무거운 모델이 됩니다. 즉, 단어 집합의 크기에 대해서 softmax + cross entropy 연산은 너무 Heavy 합니다.

image


Word2Vec역전파 과정에서 모든 단어의 임베딩 벡터값의 업데이트를 수행 하지만, 만약 현재 집중하고 있는 중심 단어와 주변 단어가 ‘강아지’와 ‘고양이’, ‘귀여운’과 같은 단어라면, 사실 이 단어들과 별 연관 관계가 없는 '돈가스'나 '컴퓨터'와 같은 수많은 단어의 임베딩 벡터값까지 업데이트하는 것은 비효율적 입니다.

네거티브 샘플링(Negative Sampling)Word2Vec가 학습 과정에서 전체 단어 집합이 아니라 일부 단어 집합에만 집중할 수 있도록 하는 방법 으로, 다중 클래스 분류를 이진 분류 문제로 바꾸므로서 연산량을 획기적으로 줄입니다.

image


즉, Skip-gram은 주변 단어로부터 중심 단어를 예측하는 모델이었다면, 이제 중심 단어와 주변 단어의 내적으로부터 어떤 값을 예측하는 모델로 변경하는 것 입니다.

우선, 기존의 Skip-gram 의 데이터셋부터 변경할 필요가 있습니다. 중심 단어를 입력, 주변 단어를 레이블로 하는 데이터셋의 형식을 변경해줍니다. 주변 단어와 중심 단어 데이터셋에 True를 의미하는 레이블 1을 할당해줍니다.

image


이 모델의 목적은 중심 단어와 주변 단어를 입력으로 하였을 때, 실제로 이 두 단어의 관계가 이웃(neighbors)의 관계 인지를 예측하는 것 입니다.

image


주변 단어와 중심 단어 데이터셋에 True를 의미하는 레이블 1을 할당해주었는데, 실제로 이웃 관계이므로 이들의 레이블은 1을 할당해주는 것이 맞습니다.

이제 거짓을 의미하는 샘플들도 추가해주어야 한다. 이를 Negative Sample 이라고 한다. Negative Sample 은 전체 데이터셋에서 랜덤으로 추가해줍니다.

image


이제 모델 구조도 이전과 달라집니다. 다음과 같이 두 개의 Embedding table을 준비하는데, 한 테이블은 중심 단어, 한 테이블은 주변 단어를 위한 테이블입니다.

image


image


중심 단어와 주변 단어의 내적으로부터 실제값인 1 또는 0을 예측하고, 실제값과의 오차(error)를 계산하여, 역전파를 통해 두 개의 테이블을 업데이트합니다.

image


3. Skip-Gram with Negative Sampling(SGNS) example

위 Negative Sampling 설명에 따르면 가령, 현재 집중하고 있는 주변 단어가 ‘고양이’, ‘귀여운’이라고 했을때, 여기에 ‘돈가스’, ‘컴퓨터’, ‘회의실’과 같은 단어 집합에서 무작위로 선택된 주변 단어가 아닌 단어들을 일부 가져옵니다. 이렇게 하나의 중심 단어에 대해서 전체 단어 집합보다 훨씬 작은 단어 집합을 만들어놓고 마지막 단계를 이진 분류 문제로 변환했을때, 주변 단어들을 긍정(positive), 랜덤으로 샘플링 된 단어들을 부정(negative)으로 레이블링한다면 이진 분류 문제를 위한 데이터셋 이 됩니다. 이는 기존의 단어 집합의 크기 만큼의 선택지를 두고 다중 클래스 분류 문제를 풀던 Word2Vec보다 훨씬 연산량에서 효율적 입니다.

실습 전 한번 더 친숙한 예제로 개념을 살펴보겠습니다. 다음과 같은 예문이 있다고 가정하겠습니다.

[“The\ \ fat\ \ cat\ \ sat\ \ on\ \ the\ \ mat”]


위 예문에 대해서 동일하게 윈도우 크기가 2일 때, 데이터셋은 다음과 같이 구성됩니다.

image


Skip-gram중심 단어로부터 주변 단어를 예측하는 모델 이었습니다. 위와 같은 문장이 있다고 한다면, Skip-gram 은 중심 단어 cat으로부터 주변 단어 The, fat, sat, on을 예측합니다. 기존의 Skip-gram 모델을 일종의 주황 박스로 생각해본다면, 아래의 그림과 같이 입력은 중심 단어, 모델의 예측은 주변 단어인 구조입니다.

image


하지만 네거티브 샘플링을 사용하는 Skip-gram(Skip-Gram with Negative Sampling, SGNS)은 이와는 다른 접근 방식을 취합니다. SGNS다음과 같이 중심 단어와 주변 단어가 모두 입력이 되고, 이 두 단어가 실제로 윈도우 크기 내에 존재하는 이웃 관계인지 그 확률을 예측 합니다.

image


기존의 Skip-gram 데이터셋을 SGNS의 데이터셋으로 바꾸는 과정을 보겠습니다.

image


위의 그림에서 좌측의 테이블은 기존의 Skip-gram 을 학습하기 위한 데이터셋입니다. Skip-gram 은 기본적으로 중심 단어를 입력, 주변 단어를 레이블로 합니다. 하지만 SGNS 를 학습하고 싶다면, 이 데이터셋을 우측의 테이블과 같이 수정할 필요가 있습니다. 우선, 기존의 Skip-gram 데이터셋에서 중심 단어와 주변 단어를 각각 입력1, 입력2로 둡니다. 이 둘은 실제로 윈도우 크기 내에서 이웃 관계였므로 레이블은 1로 합니다. 이제 레이블이 0인 샘플들을 준비할 차례입니다.

image


실제로는 입력1(중심 단어)와 주변 단어 관계가 아닌 단어들을 입력2로 삼기 위해서 단어 집합에서 랜덤으로 선택한 단어들을 입력2로 하고, 레이블을 0으로 합니다. 이제 이 데이터셋은 입력1과 입력2가 실제로 윈도우 크기 내에서 이웃 관계인 경우에는 레이블이 1, 아닌 경우에는 레이블이 0인 데이터셋 이 됩니다.

그리고 이제 두 개의 임베딩 테이블을 준비합니다. 두 임베딩 테이블은 훈련 데이터의 단어 집합의 크기를 가지므로 크기가 같습니다.

image


두 테이블 중 하나는 입력 1인 중심 단어의 테이블 룩업을 위한 임베딩 테이블이고, 하나는 입력 2인 주변 단어의 테이블 룩업을 위한 임베딩 테이블입니다. 각 단어는 각 임베딩 테이블을 테이블 룩업하여 임베딩 벡터로 변환됩니다.

image


각 임베딩 테이블을 통해 테이블 룩업하여 임베딩 벡터로 변환되었다면 그 후의 연산은 매우 간단합니다.

image


중심 단어와 주변 단어의 내적값을 이 모델의 예측값으로 하고, 레이블과의 오차로부터 역전파하여 중심 단어와 주변 단어의 임베딩 벡터값을 업데이트 합니다. 학습 후에는 좌측의 임베딩 행렬을 임베딩 벡터로 사용할 수도 있고, 두 행렬을 더한 후 사용하거나 두 행렬을 연결(concatenate)해서 사용할 수도 있습니다.

아래의 실습에서는 좌측의 행렬을 사용하는 방식을 택했습니다.


4. 네거티브 샘플링을 이용한 Word2Vec 구현

네거티브 샘플링(Negative Sampling)을 사용하는 Word2Vec을 직접 케라스(Keras)를 통해 구현해보겠습니다.


4.1 20뉴스그룹 데이터 전처리하기

import pandas as pd
import numpy as np
import nltk
from nltk.corpus import stopwords
from sklearn.datasets import fetch_20newsgroups
from tensorflow.keras.preprocessing.text import Tokenizer


20뉴스그룹 데이터를 사용하겠습니다. 이번 실습에서는 하나의 샘플에 최소 단어 2개는 있어야 합니다. 그래야만 중심 단어, 주변 단어의 관계가 성립하며 그렇지 않으면 샘플을 구성할 수 없어 에러가 발생합니다. 전처리 과정에서 지속적으로 이를 만족하지 않는 샘플들을 제거하겠습니다.

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

documents = dataset.data
print('총 샘플 수 :',len(documents))
[output]
총 샘플 수 : 11314


총 샘플 수는 11,314개입니다. 전처리를 진행하겠습니다. 불필요한 토큰을 제거하고, 소문자화를 통해 정규화를 진행합니다.

news_df = pd.DataFrame({'document':documents})
# 특수 문자 제거
news_df['clean_doc'] = news_df['document'].str.replace("[^a-zA-Z]", " ")
# 길이가 3이하인 단어는 제거 (길이가 짧은 단어 제거)
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: ' '.join([w for w in x.split() if len(w)>3]))
# 전체 단어에 대한 소문자 변환
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: x.lower())

news_df.head()

image


현재 데이터프레임에 Null 값이 있는지 확인하겠습니다.

news_df.isnull().values.any()
[output]
False


Null 값이 없지만, 빈 값(empy) 유무도 확인해야 합니다. 모든 빈 값을 Null 값으로 변환하고, 다시 Null 값이 있는지 확인합니다.

news_df.replace("", float("NaN"), inplace=True)
news_df.isnull().values.any()
[output]
True


Null 값이 있음을 확인했습니다. Null 값을 제거합니다.

news_df.dropna(inplace=True)
print('총 샘플 수 :', len(news_df))
[output]
총 샘플 수 : 10995


샘플 수가 일부 줄어든 것을 확인할 수 있습니다. NLTK에서 정의한 불용어 리스트를 사용하여 불용어를 제거합니다.

stop_words = stopwords.words('english') # NLTK로부터 불용어를 받아옵니다.
tokenized_doc = news_df['clean_doc'].apply(lambda x: x.split()) # 토큰화

# 불용어를 제거합니다.
tokenized_doc = tokenized_doc.apply(lambda x: [item for item in x if item not in stop_words])

tokenized_doc = tokenized_doc.to_list()


불용어를 제거하였으므로 단어의 수가 줄어들었습니다. 모든 샘플 중 단어가 1개 이하인 경우를 모두 찾아 제거하겠습니다.

# 단어가 1개 이하인 경우 중심 단어, 주변 단어가 존재하지 않으므로 불가.
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))
[output]
총 샘플 수 : 10940


샘플 수가 다시 줄어들었습니다. 단어 집합을 생성하고, 정수 인코딩을 진행하겠습니다.

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)


상위 2개의 샘플을 출력해보겠습니다.

print(encoded[:5])
[output]
[[9, 59, 603, 207, 3278, 1495, 474, 702, 9470, 13686, 5533, 15227, 702, 442, 702, 70, 1148, 1095, 1036, 20294, 984, 705, 4294, 702, 217, 207, 1979, 15228, 13686, 4865, 4520, 87, 1530, 6, 52, 149, 581, 661, 4406, 4988, 4866, 1920, 755, 10668, 1102, 7837, 442, 957, 10669, 634, 51, 228, 2669, 4989, 178, 66, 222, 4521, 6066, 68, 4295], [1026, 532, 2, 60, 98, 582, 107, 800, 23, 79, 4522, 333, 7838, 864, 421, 3825, 458, 6488, 458, 2700, 4730, 333, 23, 9, 4731, 7262, 186, 310, 146, 170, 642, 1260, 107, 33568, 13, 985, 33569, 33570, 9471, 11491],
 [262, 1036, 2223, 7839, 387, 1, 36, 3, 4, 69, 345, 901, 944, 20, 709, 6, 1662, 24704, 20295, 223, 40, 409, 52, 170, 585, 345, 189, 901, 944, 9, 1036, 1, 24, 901, 944, 1188, 222, 42, 125, 3279, 20295, 223, 1, 1037, 66, 3, 3278, 641, 295, 116, 8994, 1027, 258, 604, 218, 135, 3280, 71, 12465, 11492, 223], [8530, 1430, 11493, 1241, 13, 185, 42, 605, 271, 4627, 958, 340, 1921, 191, 3517, 2071, 33571, 51, 1514, 363, 1674, 3050, 20296, 33572, 8165, 340, 92, 113, 1328, 277, 1308, 62, 279, 6067, 3135, 3462, 548, 722, 35, 1420, 1269, 1128, 381, 75, 310, 1155, 25, 109, 69, 30, 4121, 718, 410, 255, 85, 512, 5892, 9472, 4523, 11, 2581, 1751, 61, 33573, 5112, 20297], [9, 185, 1531, 2204, 2517, 729, 7, 18, 303, 121, 1531, 479, 2413, 260, 1593, 310, 10, 2134, 6489, 1261, 6490, 6733, 55, 4296, 397, 5534]]


단어 집합의 크기를 확인하겠습니다.

vocab_size = len(word2idx) + 1 
print('단어 집합의 크기 :', vocab_size)
[output]
단어 집합의 크기 : 64277


총 64,277개의 단어가 존재합니다.


4.2 네거티브 샘플링을 통한 데이터셋 구성하기

토큰화, 정제, 정규화, 불용어 제거, 정수 인코딩까지 일반적인 전처리 과정을 거쳤습니다. 네거티브 샘플링을 통한 데이터셋을 구성할 차례입니다. 이를 위해서는 네거티브 샘플링을 위해서 케라스에서 제공하는 전처리 도구인 skipgrams를 사용합니다. 어떤 전처리가 수행되는지 그 결과를 확인하기 위해서 꽤 시간이 소요되는 작업이므로 상위 10개의 뉴스그룹 샘플에 대해서만 수행해보겠습니다.

from tensorflow.keras.preprocessing.sequence import skipgrams

# 네거티브 샘플링
skip_grams = [skipgrams(sample, vocabulary_size=vocab_size, window_size=10) for sample in encoded[:10]]


결과를 확인합니다. 10개의 뉴스그룹 샘플에 대해서 모두 수행되었지만, 첫번째 뉴스그룹 샘플에 대해서만 확인해보겠습니다.

# 첫번째 샘플인 skip_grams[0] 내 skipgrams로 형성된 데이터셋 확인
pairs, labels = skip_grams[0][0], skip_grams[0][1]
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]))
[output]
(media (702), reputation (5533)) -> 1
(away (178), accelerates (31494)) -> 0
(lived (1148), media (702)) -> 1
(seem (207), disagree (1495)) -> 1
(soldiers (957), lineno (8526)) -> 0


윈도우 크기 내에서 중심 단어, 주변 단어의 관계를 가지는 경우에는 1의 레이블을 갖도록 하고, 그렇지 않은 경우는 0의 레이블을 가지도록 하여 데이터셋을 구성합니다. 이 과정은 각각의 뉴스그룹 샘플에 대해서 동일한 프로세스로 수행됩니다.

print(len(skip_grams))
[output]
10


encoded 중 상위 10개의 뉴스그룹 샘플에 대해서만 수행하였으므로 10이 출력됩니다. 그리고 10개의 뉴스그룹 샘플 각각은 수많은 중심 단어, 주변 단어의 쌍으로 된 샘플들을 갖고 있습니다. 첫번째 뉴스그룹 샘플이 가지고 있는 pairs와 labels의 개수를 출력해봅시다.

# 첫번째 샘플에 대해서 생긴 pairs와 labels
print(len(pairs))
print(len(labels))
[output]
2220
2220


이제 이 작업을 모든 뉴스그룹 샘플에 대해서 수행하겠습니다.

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


4.3 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


하이퍼파라미터인 임베딩 벡터의 차원은 100으로 정하고, 두 개의 임베딩 층을 추가합니다.

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)


각 임베딩 테이블은 중심 단어와 주변 단어 각각을 위한 임베딩 테이블이며 각 단어는 임베딩 테이블을 거쳐서 내적을 수행하고, 내적의 결과는 1 또는 0을 예측하기 위해서 시그모이드 함수를 활성화 함수로 거쳐 최종 예측값을 얻습니다.

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='skip_gram.png', show_shapes=True, show_layer_names=True, rankdir='TB')
[output]
Model: "model"
__________________________________________________________________________________________________
 Layer (type)                   Output Shape         Param #     Connected to                     
==================================================================================================
 input_3 (InputLayer)           [(None, 1)]          0           []                               
                                                                                                  
 input_4 (InputLayer)           [(None, 1)]          0           []                               
                                                                                                  
 embedding_2 (Embedding)        (None, 1, 100)       6427700     ['input_3[0][0]']                
                                                                                                  
 embedding_3 (Embedding)        (None, 1, 100)       6427700     ['input_4[0][0]']                
                                                                                                  
 dot_2 (Dot)                    (None, 1, 1)         0           ['embedding_2[0][0]',            
                                                                  'embedding_3[0][0]']            
                                                                                                  
 reshape_1 (Reshape)            (None, 1)            0           ['dot_2[0][0]']                  
                                                                                                  
 activation_1 (Activation)      (None, 1)            0           ['reshape_1[0][0]']              
                                                                                                  
==================================================================================================
Total params: 12,855,400
Trainable params: 12,855,400
Non-trainable params: 0
__________________________________________________________________________________________________

image


모델의 학습은 9에포크 수행하겠습니다.

for epoch in range(1, 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, 'Loss :', loss)
[output]
Epoch : 1 Loss : 4626.981844887137
Epoch : 2 Loss : 3665.3362488895655
Epoch : 3 Loss : 3505.8999227024615
Epoch : 4 Loss : 3313.1815411988646
Epoch : 5 Loss : 3090.51806515269
Epoch : 6 Loss : 2854.1424025101587
Epoch : 7 Loss : 2647.802926401724
Epoch : 8 Loss : 2477.3305542107555
Epoch : 9 Loss : 2327.644738064613


4.4 결과 확인하기

학습된 모델의 결과를 확인해보겠습니다. 학습된 임베딩 벡터들을 vector.txt 에 저장합니다. 그 후 이를 gensimmodels.KeyedVectors.load_word2vec_format() 으로 로드하면 쉽게 단어 벡터 간 유사도를 구할 수 있습니다.

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=['disease'])
[output]
[('infection', 0.6975768804550171),
 ('intestinal', 0.6435999870300293),
 ('inflammation', 0.6353795528411865),
 ('diseases', 0.6294456124305725),
 ('patients', 0.6278392672538757),
 ('candida', 0.6135035157203674),
 ('systemic', 0.6041629910469055),
 ('chronic', 0.5998706221580505),
 ('migraine', 0.5968449711799622),
 ('symptoms', 0.5910621881484985)]


w2v.most_similar(positive=['soldiers'])
[output]
[('massacred', 0.7330883145332336),
 ('civilians', 0.726263701915741),
 ('shelling', 0.7134998440742493),
 ('refugees', 0.712714672088623),
 ('brutally', 0.7074486613273621),
 ('towns', 0.6952769160270691),
 ('massacre', 0.6930119395256042),
 ('snipers', 0.6848074793815613),
 ('azarbaijan', 0.6819743514060974),
 ('fighters', 0.6770718097686768)]


w2v.most_similar(positive=['police'])
[output]
[('damages', 0.5372452735900879),
 ('court', 0.5066943764686584),
 ('brutality', 0.49538081884384155),
 ('democratic', 0.4885564148426056),
 ('illegally', 0.4825240671634674),
 ('deprivation', 0.4776268005371094),
 ('authorize', 0.4710679054260254),
 ('enemy', 0.4705543518066406),
 ('refusal', 0.46288490295410156),
 ('rifles', 0.4595275819301605)]


w2v.most_similar(positive=['hero'])
[output]
[('betrayed', 0.6657158732414246),
 ('districts', 0.6575151085853577),
 ('nichols', 0.6299615502357483),
 ('harassed', 0.628355860710144),
 ('racked', 0.6282691359519958),
 ('tzeghagrons', 0.6266265511512756),
 ('hairenik', 0.6266023516654968),
 ('caucasus', 0.619964063167572),
 ('yerevan', 0.6187404990196228),
 ('laughter', 0.6154467463493347)]


w2v.most_similar(positive=['engine'])
[output]
[('sharpened', 0.5512747168540955),
 ('pathfinder', 0.5506775975227356),
 ('inline', 0.5288227200508118),
 ('shocks', 0.5259482264518738),
 ('rear', 0.4990272521972656),
 ('brake', 0.47791412472724915),
 ('rebuilt', 0.47202664613723755),
 ('slip', 0.4708540439605713),
 ('trunk', 0.47066107392311096),
 ('fairing', 0.4700755178928375)]


w2v.most_similar(positive=['doctor'])
[output]
[('patient', 0.5729305744171143),
 ('disease', 0.5609087347984314),
 ('intestinal', 0.532063901424408),
 ('infection', 0.5251743197441101),
 ('sumatriptin', 0.514649510383606),
 ('infections', 0.5072507262229919),
 ('seizures', 0.5061989426612854),
 ('antibiotics', 0.4987868666648865),
 ('patients', 0.497864693403244),
 ('migraine', 0.4913448691368103)]


5. SGNS Summaray

  • Embedding vector의 차원을 정하는 것은 결국 사용자의 몫입니다.
  • CBoW보다는 SGNS(Skipgram with Negative Sampling)이 가장 많이 선호됩니다.
  • 작은 윈도우 크기(2~7)를 가질수록, 상호 교환할 수 있을 정도의 높은 유사도를 가집니다.
    • 여기서 상호 교환이 가능하다는 것은 어쩌면 반의어도 포함될 수 있습니다.
    • 예를 들어, 친절한 ↔ 불친절한
  • 반면, 커다란 윈도우 크기(7~25)는 관련 있는 단어들을 군집하는 효과를 가집니다.

image


  • 또한 데이터셋을 위한 Negative Sampling의 비율 또한 성능에 영향을 주는 또 다른 결정요소입니다.
  • 논문에서는 5-20을 최적의 숫자로 정의하고 있습니다.
  • 데이터가 방대하다면 2-5로 충분합니다.

image

Read more

한국어 Word2Vec 실습

|

  1. gensim 패키지 버전확인
  2. 한국어 Word2Vec 만들기
    • 2.1 Libaray Import
    • 2.2 훈련 데이터 이해
    • 2.3 훈련 데이터 전처리
    • 2.4 Word2Vec 훈련
    • 2.5 Word2Vec 모델 저장 및 로드
  3. 한국어 Word2Vec 임베딩 벡터의 시각화(Embedding Visualization)
    • 3.1 워드 임베딩 모델로부터 2개의 tsv 파일 생성하기
    • 3.2 임베딩 프로젝터를 사용하여 시각화하기


gensim 패키지에서 제공하는 이미 구현된 Word2Vec 을 사용하여 한국어 데이터를 학습하겠습니다.



1. gensim 패키지 버전확인

파이썬의 gensim 패키지에는 Word2Vec 을 지원하고 있어, gensim 패키지를 이용하면 손쉽게 단어를 임베딩 벡터로 변환시킬 수 있습니다. Word2Vec 을 학습하기 위한 gensim 패키지 버전을 확인합니다.

import gensim
gensim.__version__
[output]
'4.3.1'


만약 gensim 패키지가 없다면 설치해줍니다.

pip install gensim


2. 한국어 Word2Vec 만들기

한국어로 된 코퍼스를 다운받아 전처리를 수행하고, 전처리한 데이터를 바탕으로 Word2Vec 작업을 진행하겠습니다.


2.1 Libaray Import

우선 KoNLPyOKT 등은 형태소 분석 속도가 너무 느립니다. 그래서 Mecab 을 설치하겠습니다. 단, 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
import matplotlib.pyplot as plt
import urllib.request
from tqdm import tqdm
from konlpy.tag import Mecab
from gensim.models.word2vec import Word2Vec


2.2 훈련 데이터 다운로드

Word2Vec 을 학습하기 위해서 데이터를 다운로드합니다. 사용할 훈련 데이터는, 네이버 영화 리뷰 데이터입니다.

urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings.txt", filename="ratings.txt")
[output]
('ratings.txt', <http.client.HTTPMessage at 0x7f17bdc04700>)


2.3 훈련 데이터 전처리

데이터를 읽고 상위 5개를 출력하겠습니다.

train_data = pd.read_table('ratings.txt')
train_data[:5] # 상위 5개 출력

image


이후 리뷰 개수를 확인해보고 Null 값이 있으면 제거하겠습니다.

print('리뷰 개수 :', len(train_data)) # 리뷰 개수 출력

# NULL 값 존재 유무
print('NULL 값 존재 유무 :', train_data.isnull().values.any())

train_data = train_data.dropna(how = 'any') # Null 값이 존재하는 행 제거
print('NULL 값 존재 유무 :', train_data.isnull().values.any()) # Null 값이 존재하는지 확인

print('리뷰 개수 :', len(train_data)) # 리뷰 개수 출력
[output]
리뷰 개수 : 200000
NULL 값 존재 유무 : True
NULL 값 존재 유무 : False
리뷰 개수 : 199992


또한 정규표현식으로 한글 외 문자들읊 제거하겠습니다.

# 정규 표현식을 통한 한글 외 문자 제거
train_data['document'] = train_data['document'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
train_data[:5] # 상위 5개 출력

image


이제 mecab 을 이용하여 토큰화 작업을 수행하겠습니다.

# 불용어 정의
stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게']

# 형태소 분석기 mecab을 사용한 토큰화 작업 (다소 시간 소요)
mecab = Mecab()

tokenized_data = []
for sentence in tqdm(train_data['document']):
    temp_X = mecab.morphs(sentence) # 토큰화
    temp_X = [word for word in temp_X if not word in stopwords] # 불용어 제거
    tokenized_data.append(temp_X)
[output]
100%|██████████| 199992/199992 [00:23<00:00, 8652.42it/s]


print(tokenized_data[:3])
[output]
[['어릴', '때', '보', '지금', '다시', '봐도', '재밌', '어요', 'ㅋㅋ'],
 ['디자인', '배우', '학생', '으로', '외국', '디자이너', '그', '일군', '전통', '통해', '발전', '해', '문화', '산업', '부러웠', '는데', '사실', '우리', '나라', '에서', '그', '어려운', '시절', '끝', '까지', '열정', '지킨', '노라노', '같', '전통', '있', '어', '저', '같', '사람', '꿈', '꾸', '이뤄나갈', '수', '있', '다는', '것', '감사', '합니다'], 
 ['폴리스', '스토리', '시리즈', '부터', '뉴', '까지', '버릴', '께', '하나', '없', '음', '최고']]


상위 3개 문장만 출력해보았는데 토큰화가 잘 수행되었음을 볼 수 있습니다. 리뷰 길이 분포도 한번 확인해보겠습니다.

# 리뷰 길이 분포 확인
print('리뷰의 최대 길이 :',max(len(l) for l in tokenized_data))
print('리뷰의 평균 길이 :',sum(map(len, tokenized_data))/len(tokenized_data))
plt.hist([len(s) for s in tokenized_data], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()
[output]
리뷰의 최대 길이 : 74
리뷰의 평균 길이 : 11.996394855794232

image


이제 Word2Vec 모델에 텍스트 데이터를 훈련시킵니다.


2.4 Word2Vec 훈련

여기서 Word2Vec 의 하이퍼파라미터값은 다음과 같습니다.

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


from gensim.models import Word2Vec

model = Word2Vec(
    sentences=tokenized_data,
    vector_size=100,
    window=5,
    min_count=5,
    workers =4,
    sg=0)


Word2Vec 에 대해서 학습을 진행하였습니다. 완성된 임베딩 매트릭스의 크기를 확인해보겠습니다.

# 완성된 임베딩 매트릭스의 크기 확인
print('완성된 임베딩 매트릭스의 크기 확인 :', model.wv.vectors.shape)
[output]
완성된 임베딩 매트릭스의 크기 확인 : (18134, 100)


Word2Vec 는 입력한 단어에 대해서 가장 유사한 단어들을 출력하는 model.wv.most_similar을 지원합니다. 특정 단어와 가장 유사한 단어들을 추출해보겠습니다. 이때 코사인 유사도라는 것을 유사도 메트릭으로 사용하며, 값의 범위는 -1 ~ 1 입니다.

print(model.wv.most_similar("최민식"))
[output]
[('한석규', 0.8795050978660583), ('드니로', 0.8788982629776001), ('안성기', 0.8662292957305908), ('박중훈', 0.8494662046432495), ('조재현', 0.844645082950592), ('송강호', 0.8443252444267273), ('주진모', 0.8288795351982117), ('채민서', 0.826660692691803), ('설경구', 0.8246625661849976), ('혼신', 0.8212024569511414)]


print(model.wv.most_similar("히어로"))
[output]
[('호러', 0.8805227279663086), ('슬래셔', 0.8623908758163452), ('무비', 0.8325807452201843), ('고어', 0.8201742768287659), ('느와르', 0.8061329126358032), ('무협', 0.7969907522201538), ('정통', 0.7941304445266724), ('블록버스터', 0.7905427813529968), ('괴수', 0.7877203822135925), ('로코', 0.7815171480178833)]


print(model.wv.most_similar("발연기"))
[output]
[('조연', 0.7612935304641724), ('사투리', 0.7608969807624817), ('발음', 0.7429847121238708), ('아역', 0.742458701133728), ('연기력', 0.7332570552825928), ('신하균', 0.7288188338279724), ('주연', 0.7250414490699768), ('연기', 0.7174587845802307), ('김민준', 0.7035930752754211), ('연기파', 0.6986960172653198)]


Word2Vec를 통해 단어의 유사도를 계산할 수 있게 되었습니다.

각 단어의 임베딩 값을 확인할 수 있습니다.

model.wv['최민식']
[output]
array([ 0.12769549,  0.07311668, -0.09119831, -0.23128814, -0.20396759,
       -0.38492376,  0.15919358,  0.18515116, -0.05406173,  0.02143147,
        0.0847561 , -0.35062462, -0.25053436,  0.063058  ,  0.10669395,
        0.10934699, -0.02529504, -0.01488628,  0.21295786, -0.3048591 ,
        0.10559981, -0.13670704,  0.1679281 ,  0.38849354, -0.10775245,
        0.26664627,  0.18594052, -0.25520116, -0.08447041,  0.16291223,
        0.2878455 , -0.01453895,  0.44054905,  0.0901166 ,  0.06951199,
        0.36122474,  0.27994072,  0.07385039, -0.40010566, -0.52587914,
       -0.02900855, -0.4807919 ,  0.2886666 ,  0.06037587,  0.39145902,
       -0.32478467, -0.19919494, -0.2002729 ,  0.18941337, -0.06913789,
        0.06830232,  0.15130404, -0.05708817,  0.01178154, -0.15022099,
       -0.2102085 , -0.06560393,  0.08470158,  0.23514558, -0.12844469,
        0.24766013,  0.3250303 , -0.4092613 ,  0.13039684, -0.31366074,
        0.18714847,  0.06172501,  0.15533729, -0.4577842 ,  0.4986381 ,
       -0.46604767, -0.1577858 ,  0.31474996,  0.03983723,  0.12968569,
        0.41637075,  0.2854629 , -0.07649355,  0.01544307, -0.0667455 ,
       -0.10259806, -0.25724038,  0.07584251,  0.2599289 , -0.16078846,
       -0.26536906,  0.02234764,  0.23640175,  0.11873386,  0.00365566,
        0.00673907,  0.06015598, -0.05594924,  0.23348501,  0.5042513 ,
        0.05479302, -0.06083839,  0.18337795, -0.14395966,  0.03753674],
      dtype=float32)


2.5 Word2Vec 모델 저장 및 로드

학습한 모델을 언제든 나중에 다시 사용할 수 있도록 컴퓨터 파일로 저장하고 다시 로드해보겠습니다.

from gensim.models import KeyedVectors

model.wv.save_word2vec_format('kor_w2v') # 모델 저장

# loaded_model = KeyedVectors.load_word2vec_format("kor_w2v") # 모델 로드


3. 한국어 Word2Vec 임베딩 벡터의 시각화(Embedding Visualization)

구글은 임베딩 프로젝터(embedding projector) 라는 데이터 시각화 도구를 지원합니다. 학습한 임베딩 벡터들을 시각화해보겠습니다.


3.1 워드 임베딩 모델로부터 2개의 tsv 파일 생성하기

학습한 임베딩 벡터들을 시각화해보겠습니다. 시각화를 위해서는 이미 모델을 학습하고, 파일로 저장되어져 있어야 합니다. 모델이 저장되어져 있다면 아래 커맨드를 통해 시각화에 필요한 파일들을 생성할 수 있습니다.

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


여기서는 위에서 실습한 한국어 Word2Vec 모델인 ‘kor_w2v’ 를 사용하겠습니다. kor_w2v 라는 Word2Vec 모델이 이미 존재한다는 가정 하에 아래 커맨드를 수행합니다.

!python -m gensim.scripts.word2vec2tensor --input kor_w2v --output kor_w2v


위 명령를 수행하면 기존에 있던 kor_w2v 외에도 두 개의 파일이 생깁니다. 새로 생긴 kor_w2v_metadata.tsvkor_w2v_tensor.tsv 이 두 개 파일이 임베딩 벡터 시각화를 위해 사용할 파일입니다. 만약 kor_w2v 모델 파일이 아니라 다른 모델 파일 이름으로 실습을 진행하고 있다면, ‘모델 이름_metadata.tsv’‘모델 이름_tensor.tsv’ 라는 파일이 생성됩니다.


3.2 임베딩 프로젝터를 사용하여 시각화하기

구글의 임베딩 프로젝터를 사용해서 워드 임베딩 모델을 시각화해보겠습니다. 아래의 링크에 접속합니다.


사이트에 접속해서 좌측 상단을 보면 Load라는 버튼이 있습니다.

image


Load라는 버튼을 누르면 아래와 같은 창이 뜨는데 총 두 개의 Choose file 버튼이 있습니다.

image


위에 있는 Choose file 버튼을 누르고 kor_w2v_tensor.tsv 파일을 업로드하고, 아래에 있는 Choose file 버튼을 누르고 kor_w2v_metadata.tsv 파일을 업로드합니다. 두 파일을 업로드하면 임베딩 프로젝터에 학습했던 워드 임베딩 모델이 시각화됩니다.

image

Read more

영어 Word2Vec 실습

|

  1. gensim 패키지 버전확인
  2. 영어 Word2Vec 만들기
    • 2.1 Libaray Import
    • 2.2 훈련 데이터 이해
    • 2.3 훈련 데이터 전처리
    • 2.4 Word2Vec 훈련
    • 2.5 Word2Vec 모델 저장 및 로드
  3. 영어 Word2Vec 임베딩 벡터의 시각화(Embedding Visualization)
    • 3.1 워드 임베딩 모델로부터 2개의 tsv 파일 생성하기
    • 3.2 임베딩 프로젝터를 사용하여 시각화하기
  4. 사전 훈련된 Word2Vec 임베딩(Pre-trained Word2Vec embedding) 소개


gensim 패키지에서 제공하는 이미 구현된 Word2Vec을 사용하여 영어 데이터를 학습하겠습니다.



1. gensim 패키지 버전확인

파이썬의 gensim 패키지에는 Word2Vec 을 지원하고 있어, gensim 패키지를 이용하면 손쉽게 단어를 임베딩 벡터로 변환시킬 수 있습니다. Word2Vec 을 학습하기 위한 gensim 패키지 버전을 확인합니다.

import gensim
gensim.__version__
[output]
'4.3.1'


만약 gensim 패키지가 없다면 설치해줍니다.

pip install gensim


2. 영어 Word2Vec 만들기

영어로 된 코퍼스를 다운받아 전처리를 수행하고, 전처리한 데이터를 바탕으로 Word2Vec 작업을 진행하겠습니다.


2.1 Libaray Import

우선 필요 라이브러리를 불러옵니다.

import re
from lxml import etree
import urllib.request
import zipfile

import nltk
nltk.download('punkt')

from nltk.tokenize import word_tokenize, sent_tokenize
[output]
[nltk_data] Downloading package punkt to /root/nltk_data...
[nltk_data]   Unzipping tokenizers/punkt.zip.


2.2 훈련 데이터 이해

Word2Vec을 학습하기 위해서 데이터를 다운로드합니다. 사용할 훈련 데이터는, ted 영상들의 자막 데이터입니다. 파일의 형식은 xml 파일입니다. 해당 링크를 통해 내려받아 ted_en-20160408.xml 라는 이름의 파일을 설치할 수도 있고, 파이썬 코드를 통해 자동으로 설치할 수도 있습니다.

urllib.request.urlretrieve("https://raw.githubusercontent.com/GaoleMeng/RNN-and-FFNN-textClassification/master/ted_en-20160408.xml", filename="ted_en-20160408.xml")
[output]
('ted_en-20160408.xml', <http.client.HTTPMessage at 0x7fbaa9a92620>)


위의 코드를 통해 xml 파일을 내려받으면, 다음과 같은 파일을 볼 수 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<xml language="en"><file id="1">
  <head>
    <url>http://www.ted.com/talks/knut_haanaes_two_reasons_companies_fail_and_how_to_avoid_them</url>

    ...

    <content>Here are two reasons companies fail: they only do more of the same, or they only do what's new.

    ...

    So let me leave you with this. Whether you're an explorer by nature or whether you tend to exploit what you already know, don't forget: the beauty is in the balance.
    Thank you.
    (Applause)</content>
</file>
<file id="2">
  <head>
    <url>http://www.ted.com/talks/lisa_nip_how_humans_could_evolve_to_survive_in_space</url>
    
    ...

    (Applause)</content>
</file>
</xml>


훈련 데이터 파일은 xml 문법으로 작성되어 있어 자연어를 얻기 위해서는 전처리가 필요합니다. 얻고자 하는 실질적 데이터는 영어문장으로만 구성된 내용을 담고 있는 ’<content>‘’</content>‘ 사이의 내용입니다. 전처리 작업을 통해 xml 문법들은 제거하고, 해당 데이터만 가져와야 합니다. 뿐만 아니라, ’<content>‘’</content>‘ 사이의 내용 중에는 (Laughter)나 (Applause)와 같은 배경음을 나타내는 단어도 등장하는데 이 또한 제거해야 합니다.


2.3 훈련 데이터 전처리

targetXML = open('ted_en-20160408.xml', 'r', encoding='UTF8')
target_text = etree.parse(targetXML)

# xml 파일로부터 <content>와 </content> 사이의 내용만 가져온다.
parse_text = '\n'.join(target_text.xpath('//content/text()'))

# 정규 표현식의 sub 모듈을 통해 content 중간에 등장하는 (Audio), (Laughter) 등의 배경음 부분을 제거.
# 해당 코드는 괄호로 구성된 내용을 제거.
content_text = re.sub(r'\([^)]*\)', '', parse_text)


현재 영어 텍스트가 content_text 에 저장되어져 있습니다. 이에 대해서 NLTKsent_tokenize 를 통해서 문장을 구분해보겠습니다.

print('영어 텍스트의 개수 : {}'.format(len(content_text)))
[output]
영어 텍스트의 개수 : 24062319


# 입력 코퍼스에 대해서 NLTK를 이용하여 문장 토큰화를 수행.
sent_text = sent_tokenize(content_text)

# 각 문장에 대해서 구두점을 제거하고, 대문자를 소문자로 변환.
normalized_text = []
for string in sent_text:
    tokens = re.sub(r"[^a-z0-9]+", " ", string.lower())
    normalized_text.append(tokens)

# 각 문장에 대해서 NLTK를 이용하여 단어 토큰화를 수행.
result = [word_tokenize(sentence) for sentence in normalized_text]

print('총 샘플의 개수 : {}'.format(len(result)))
[output]
총 샘플의 개수 : 273424


총 문장 샘플의 개수는 273,424개입니다. 샘플 3개만 출력해보겠습니다.

for line in result[:3]: # 샘플 3개만 출력
    print(line)
[output]
['here', 'are', 'two', 'reasons', 'companies', 'fail', 'they', 'only', 'do', 'more', 'of', 'the', 'same', 'or', 'they', 'only', 'do', 'what', 's', 'new']
['to', 'me', 'the', 'real', 'real', 'solution', 'to', 'quality', 'growth', 'is', 'figuring', 'out', 'the', 'balance', 'between', 'two', 'activities', 'exploration', 'and', 'exploitation']
['both', 'are', 'necessary', 'but', 'it', 'can', 'be', 'too', 'much', 'of', 'a', 'good', 'thing']


상위 3개 문장만 출력해보았는데 토큰화가 잘 수행되었음을 볼 수 있습니다. 이제 Word2Vec 모델에 텍스트 데이터를 훈련시킵니다.


2.4 Word2Vec 훈련

여기서 Word2Vec 의 하이퍼파라미터값은 다음과 같습니다.

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


from gensim.models import Word2Vec

model = Word2Vec(
    sentences=result,
    vector_size=100,
    window=5,
    min_count=5,
    workers=4,
    sg=0
)


Word2Vec 에 대해서 학습을 진행하였습니다. Word2Vec 는 입력한 단어에 대해서 가장 유사한 단어들을 출력하는 model.wv.most_similar을 지원합니다. 특정 단어와 가장 유사한 단어들을 추출해보겠습니다. 이때 코사인 유사도라는 것을 유사도 메트릭으로 사용하며, 값의 범위는 -1 ~ 1 입니다.

model_result = model.wv.most_similar("man")
print(model_result)
[output]
[('woman', 0.8425011038780212), ('guy', 0.8098958134651184), ('lady', 0.781208872795105), ('boy', 0.7525790929794312), ('girl', 0.7469564080238342), ('gentleman', 0.7207304835319519), ('soldier', 0.7077170014381409), ('kid', 0.6885204315185547), ('david', 0.6624912023544312), ('friend', 0.6530545353889465)]


man과 유사한 단어로 woman, guy, boy, lady, girl, gentleman, soldier, kid 등을 출력하는 것을 볼 수 있습니다. Word2Vec를 통해 단어의 유사도를 계산할 수 있게 되었습니다.

각 단어의 임베딩 값을 확인할 수 있습니다.

model.wv["man"]
[output]
array([ 0.54919225, -2.5828376 , -0.05627956, -0.72883856,  0.5080902 ,
       -0.588747  ,  1.1527569 ,  0.6861413 ,  0.20908435,  0.5790621 ,
       -0.76411045, -1.2080296 , -0.9166982 ,  0.6161433 , -0.32686922,
        0.3346195 ,  0.47164342, -0.30977565,  0.360217  , -0.6516018 ,
       -0.06280681,  0.9388923 ,  0.6213905 , -0.4060864 ,  0.8803398 ,
        0.4036564 , -1.8721576 , -0.5711301 ,  0.92875475, -1.4228262 ,
        0.76451683, -0.4689635 ,  1.478043  , -0.3736253 ,  0.24919653,
       -1.2209562 , -2.0871649 , -0.64423513, -1.8315326 , -1.0469043 ,
        1.3488007 , -2.40771   , -0.8882299 ,  1.0518845 ,  0.3505911 ,
       -0.5359099 , -0.11452804, -1.7889714 , -0.50420225,  0.13257498,
        0.46635804, -1.5578051 , -0.40210238,  0.41704193, -0.16498177,
       -1.7667351 , -0.42030132, -0.89286804, -1.9498727 ,  0.31205317,
        0.1363872 , -0.32287887,  0.83032966,  1.0676957 , -2.0881174 ,
        0.5390724 , -1.3501817 , -0.15355064, -0.42196646,  0.5385719 ,
        0.7717964 ,  0.42193443,  2.9974504 , -1.1239656 , -0.8758551 ,
       -1.6787865 , -0.30246603,  0.6885682 , -0.5081502 ,  1.6597394 ,
        0.7549413 ,  0.6027066 , -1.0214967 , -0.23701903, -0.37534398,
        1.4510185 ,  0.13622098,  0.79067725, -0.89343023, -0.14235029,
        0.707251  ,  0.40881404,  0.00797209, -0.5443254 ,  2.3957598 ,
       -0.40322962,  0.37388444,  0.57005996, -2.089544  ,  2.3334754 ],
      dtype=float32)


2.5 Word2Vec 모델 저장 및 로드

학습한 모델을 언제든 나중에 다시 사용할 수 있도록 컴퓨터 파일로 저장하고 다시 로드해보겠습니다.

from gensim.models import KeyedVectors

model.wv.save_word2vec_format('eng_w2v') # 모델 저장
loaded_model = KeyedVectors.load_word2vec_format("eng_w2v") # 모델 로드


eng_w2v 라는 모델 파일이 저장되었으며, 로드한 모델에 대해서 다시 man과 유사한 단어를 출력해보겠습니다.

model_result = loaded_model.most_similar("man")
print(model_result)
[output]
[('woman', 0.845443844795227), ('guy', 0.8228113651275635), ('lady', 0.7647513151168823), ('boy', 0.7602432370185852), ('gentleman', 0.7566214799880981), ('girl', 0.7488241195678711), ('soldier', 0.7224786877632141), ('kid', 0.7164652347564697), ('king', 0.6694438457489014), ('poet', 0.6539930105209351)]


3. 영어 Word2Vec 임베딩 벡터의 시각화(Embedding Visualization)

구글은 임베딩 프로젝터(embedding projector) 라는 데이터 시각화 도구를 지원합니다. 학습한 임베딩 벡터들을 시각화해보겠습니다.


3.1 워드 임베딩 모델로부터 2개의 tsv 파일 생성하기

학습한 임베딩 벡터들을 시각화해보겠습니다. 시각화를 위해서는 이미 모델을 학습하고, 파일로 저장되어져 있어야 합니다. 모델이 저장되어져 있다면 아래 커맨드를 통해 시각화에 필요한 파일들을 생성할 수 있습니다.

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


여기서는 위에서 실습한 영어 Word2Vec 모델인 ‘eng_w2v’ 를 사용하겠습니다. eng_w2v 라는 Word2Vec 모델이 이미 존재한다는 가정 하에 아래 커맨드를 수행합니다.

!python -m gensim.scripts.word2vec2tensor --input eng_w2v --output eng_w2v


위 명령를 수행하면 기존에 있던 eng_w2v 외에도 두 개의 파일이 생깁니다. 새로 생긴 eng_w2v_metadata.tsveng_w2v_tensor.tsv 이 두 개 파일이 임베딩 벡터 시각화를 위해 사용할 파일입니다. 만약 eng_w2v 모델 파일이 아니라 다른 모델 파일 이름으로 실습을 진행하고 있다면, ‘모델 이름_metadata.tsv’‘모델 이름_tensor.tsv’ 라는 파일이 생성됩니다.


3.2 임베딩 프로젝터를 사용하여 시각화하기

구글의 임베딩 프로젝터를 사용해서 워드 임베딩 모델을 시각화해보겠습니다. 아래의 링크에 접속합니다.


사이트에 접속해서 좌측 상단을 보면 Load라는 버튼이 있습니다.

image


Load라는 버튼을 누르면 아래와 같은 창이 뜨는데 총 두 개의 Choose file 버튼이 있습니다.

image


위에 있는 Choose file 버튼을 누르고 eng_w2v_tensor.tsv 파일을 업로드하고, 아래에 있는 Choose file 버튼을 누르고 eng_w2v_metadata.tsv 파일을 업로드합니다. 두 파일을 업로드하면 임베딩 프로젝터에 학습했던 워드 임베딩 모델이 시각화됩니다.

image


그 후에는 임베딩 프로젝터의 다양한 기능을 사용할 수 있습니다. 예를 들어 임베딩 프로젝터는 복잡한 데이터를 차원을 축소하여 시각화 할 수 있도록 도와주는 PCA, t-SNE 등을 제공합니다. 위의 그림은 ‘man’ 이라는 단어를 선택하고, 코사인 유사도를 기준으로 가장 유사한 상위 10개 벡터들을 표시해봤습니다.


4. 사전 훈련된 Word2Vec 임베딩(Pre-trained Word2Vec embedding) 소개

자연어 처리 작업을 할때, 케라스의 Embedding() 를 사용하여 갖고 있는 훈련 데이터로부터 처음부터 임베딩 벡터를 훈련시키기도 하지만, 위키피디아 등의 방대한 데이터로 사전에 훈련된 워드 임베딩(pre-trained word embedding vector)를 가지고 와서 해당 벡터들의 값을 원하는 작업에 사용 할 수도 있습니다.

예를 들어서 감성 분류 작업을 하는데 훈련 데이터의 양이 부족한 상황이라면, 다른 방대한 데이터를 Word2Vec이나 GloVe 등으로 사전에 학습시켜놓은 임베딩 벡터들을 가지고 와서 모델의 입력으로 사용하는 것이 때로는 더 좋은 성능을 얻을 수 있습니다. 사전 훈련된 워드 임베딩을 가져와서 간단히 단어들의 유사도를 구해보는 실습을 해보겠습니다.

구글이 제공하는 사전 훈련된(미리 학습되어져 있는) Word2Vec 모델을 사용하는 방법에 대해서 알아보겠습니다. 구글은 사전 훈련된 3백만 개의 Word2Vec 단어 벡터들을 제공합니다. 각 임베딩 벡터의 차원은 300입니다. gensim 을 통해서 이 모델을 불러오는 건 매우 간단합니다. 이 모델을 다운로드하고 파일 경로를 기재하면 됩니다.


압축 파일의 용량은 약 1.5GB이지만, 파일의 압축을 풀면 약 3.3GB의 파일이 나옵니다.

import gensim
import urllib.request

# 구글의 사전 훈련된 Word2Vec 모델 다운로드.
!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=1Av37IVBQAAntSe1X3MOAl5gvowQzd2_j' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=1Av37IVBQAAntSe1X3MOAl5gvowQzd2_j" -O GoogleNews-vectors-negative300.bin.gz && rm -rf /tmp/cookies.txt

# 구글의 사전 훈련된 Word2vec 모델을 로드합니다.
word2vec_model = gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin.gz', binary=True) 


모델의 크기(shape)를 확인해보겠습니다.

print(word2vec_model.vectors.shape)
[output]
(3000000, 300)


모델의 크기는 $3,000,000 × 300$ 입니다. 즉, 3백만 개의 단어와 각 단어의 차원은 300입니다. 파일의 크기가 3기가가 넘는 이유를 계산해보면 아래와 같습니다.

  • 3 million words * 300 features * 4bytes/feature = ~3.35GB


사전 훈련된 임베딩을 사용하여 두 단어의 유사도를 계산해보겠습니다.

print(word2vec_model.similarity('this', 'is'))
print(word2vec_model.similarity('post', 'book'))
[output]
0.40797037
0.057204384


단어 ‘book’의 벡터를 출력해보겠습니다.

print(word2vec_model['book'])
[output]
[ 0.11279297 -0.02612305 -0.04492188  0.06982422  0.140625    0.03039551
 -0.04370117  0.24511719  0.08740234 -0.05053711  0.23144531 -0.07470703
  0.21875     0.03466797 -0.14550781  0.05761719  0.00671387 -0.00701904
  0.13183594 -0.25390625  0.14355469 -0.140625   -0.03564453 -0.21289062
 -0.24804688  0.04980469 -0.09082031  0.14453125  0.05712891 -0.10400391
 -0.19628906 -0.20507812 -0.27539062  0.03063965  0.20117188  0.17382812
  0.09130859 -0.10107422  0.22851562 -0.04077148  0.02709961 -0.00106049
  0.02709961  0.34179688 -0.13183594 -0.078125    0.02197266 -0.18847656
 -0.17480469 -0.05566406 -0.20898438  0.04858398 -0.07617188 -0.15625
 -0.05419922  0.01672363 -0.02722168 -0.11132812 -0.03588867 -0.18359375
  0.28710938  0.01757812  0.02185059 -0.05664062 -0.01251221  0.01708984
 -0.21777344 -0.06787109  0.04711914 -0.00668335  0.08544922 -0.02209473
  0.31835938  0.01794434 -0.02246094 -0.03051758 -0.09570312  0.24414062
  0.20507812  0.05419922  0.29101562  0.03637695  0.04956055 -0.06689453
  0.09277344 -0.10595703 -0.04370117  0.19726562 -0.03015137  0.05615234
  0.08544922 -0.09863281 -0.02392578 -0.08691406 -0.22460938 -0.16894531
  0.09521484 -0.0612793  -0.03015137 -0.265625   -0.13378906  0.00139618
  0.01794434  0.10107422  0.13964844  0.06445312 -0.09765625 -0.11376953
 -0.24511719 -0.15722656  0.00457764  0.12988281 -0.03540039 -0.08105469
  0.18652344  0.03125    -0.09326172 -0.04760742  0.23730469  0.11083984
  0.08691406  0.01916504  0.21386719 -0.0065918  -0.08984375 -0.02502441
 -0.09863281 -0.05639648 -0.26757812  0.19335938 -0.08886719 -0.25976562
  0.05957031 -0.10742188  0.09863281  0.1484375   0.04101562  0.00340271
 -0.06591797 -0.02941895  0.20019531 -0.00521851  0.02355957 -0.13671875
 -0.12597656 -0.10791016  0.0067749   0.15917969  0.0145874  -0.15136719
  0.07519531 -0.02905273  0.01843262  0.20800781  0.25195312 -0.11523438
 -0.23535156  0.04101562 -0.11035156  0.02905273  0.22460938 -0.04272461
  0.09667969  0.11865234  0.08007812  0.07958984  0.3125     -0.14941406
 -0.234375    0.06079102  0.06982422 -0.14355469 -0.05834961 -0.36914062
 -0.10595703  0.00738525  0.24023438 -0.10400391 -0.02124023  0.05712891
 -0.11621094 -0.16894531 -0.06396484 -0.12060547  0.08105469 -0.13769531
 -0.08447266  0.12792969 -0.15429688  0.17871094  0.2421875  -0.06884766
  0.03320312  0.04394531 -0.04589844  0.03686523 -0.07421875 -0.01635742
 -0.24121094 -0.08203125 -0.01733398  0.0291748   0.10742188  0.11279297
  0.12890625  0.01416016 -0.28710938  0.16503906 -0.25585938  0.2109375
 -0.19238281  0.22363281  0.04541016  0.00872803  0.11376953  0.375
  0.09765625  0.06201172  0.12109375 -0.24316406  0.203125    0.12158203
  0.08642578  0.01782227  0.17382812  0.01855469  0.03613281 -0.02124023
 -0.02905273 -0.04541016  0.1796875   0.06494141 -0.13378906 -0.09228516
  0.02172852  0.02099609  0.07226562  0.3046875  -0.27539062 -0.30078125
  0.08691406 -0.22949219  0.0546875  -0.34179688 -0.00680542 -0.0291748
 -0.03222656  0.16210938  0.01141357  0.23339844 -0.0859375  -0.06494141
  0.15039062  0.17675781  0.08251953 -0.26757812 -0.11669922  0.01330566
  0.01818848  0.10009766 -0.09570312  0.109375   -0.16992188 -0.23046875
 -0.22070312  0.0625      0.03662109 -0.125       0.05151367 -0.18847656
  0.22949219  0.26367188 -0.09814453  0.06176758  0.11669922  0.23046875
  0.32617188  0.02038574 -0.03735352 -0.12255859  0.296875   -0.25
 -0.08544922 -0.03149414  0.38085938  0.02929688 -0.265625    0.42382812
 -0.1484375   0.14355469 -0.03125     0.00717163 -0.16601562 -0.15820312
  0.03637695 -0.16796875 -0.01483154  0.09667969 -0.05761719 -0.00515747]


참고로, Word2vec 모델은 자연어 처리에서 단어를 밀집 벡터로 만들어주는 단어 임베딩 방법론이지만 최근에 들어서는 자연어 처리를 넘어서 추천 시스템에도 사용되고 있는 모델입니다. 적당하게 데이터를 나열해주면 Word2vec 은 위치가 근접한 데이터를 유사도가 높은 벡터를 만들어준다는 점에서 착안된 아이디어 item2vec 방법론도 있습니다.

Read more