AIFFEL/Going Deeper(NLP)

[Going Deeper(NLP)] 5. 워드 임베딩

알밤바 2022. 3. 28. 10:32
728x90
반응형

해당 포스팅은 AIFFEL에서 제공한 학습자료를 통해 공부한 것을 정리한 것임을 밝힙니다.


1. 벡터화

기계는 텍스트보다 수치화된 숫자를 더 잘 처리할 수 있기에 기계가 자연어 처리를 원활히 할 수 있도록, 전처리 과정에서 텍스트를 벡터로 변환하는 벡터화(Vectorization)라는 과정을 거친다.

텍스트를 벡터화하는 방법을 정리해보자.

 

1.1 Bag of words / DTM(Document-Term Matrix)

 

[Going Deeper(NLP)] 3. 텍스트의 분포로 벡터화 하기

해당 포스팅은 AIFFEL에서 제공한 학습자료를 통해 공부한 것을 정리한 것임을 밝힙니다. 목차 단어 빈도를 이용한 벡터화 (1) Bag of Words (2) Bag of Words 구현해보기 (3) DTM과 코사인 유사도 (4) DTM의 구

ars420.tistory.com

Bag of words는 단어의 순서를 고려하지 않고, 단어의 등장 빈도만을 고려해서 단어를 벡터화하는 방법이다.

Bag of words를 사용하여 만든 문서 간 유사도를 비교하기 위한 행렬을 DTM(문서 단어 행렬)이라고 한다. 문서를 행으로, 단어를 열로 구성한 행렬이다.

 

bag of words는 단어를 카운트하는 방법론이기 때문에 사용할 때 주로 불필요한 단어를 제거하거나 표현은 다르지만 같은 단어를 통합시켜주는 정규화와 같은 전처리를 한다.

 

ex.

여기서는 길이가 1인 단어를 제거하는 전처리를 했다고 가정한다.

문서1 : you know I want your love
문서2 : I like you
문서3 : what should I do

 

완성된 DTM은 다음과 같다.

DTM의 문서 벡터나 단어 벡터는 대부분의 값이 0이라는 특징을 가지고 있는데, 이런 벡터를 희소벡터(Sparse vector)라고 한다. 문서의 수나 단어의 수가 많아질수록 행렬에서 대부분의 값이 0이 되는 희소 문제는 점점 심화된다.

중복 카운트를 배제한 단어들의 집합(set)을 자연어 처리에서는 단어장(vocabulary)이라고 한다.

 

[bag of words 강의]

 

1.2 TF-IDF

DTM을 이용해서 문서의 유사도를 비교할 경우, 문제점이 발생한다. 불용어와 같은 단어들은 별로 중요하지 않은 단어인데 모든 문서에서 공통적으로 등장하는 단어가 있다.

그래서 단어마다 중요 가중치를 다르게 주는 방법인 TF-IDF가 발생하였다.

 

하지만, TF-IDF를 사용한다고 하더라도, 여전히 문서 벡터의 크기가 단어장의 크기이고, 문서 벡터와 단어 벡터 둘 다 여전히 희소 벡터다.

 

[TF-IDF 강의]

 

1.3 원-핫 인코딩(one-hot encoding)

원-핫 인코딩은 모든 단어의 관계를 독립적으로 정의하는 방식이다.

원-핫 인코딩을 하기 위해서는 우선 갖고 있는 텍스트 데이터에서 단어들의 집합인 단어장(vocabulary)을 만든다. 이후 단어장에 있는 모든 단어에 대해서 고유한 정수를 부여하며, 이 정수는 단어장에 있는 단어의 인덱스 역할을 한다.

 

정수 부여에 정해진 규칙은 없지만 관례적으로 빈도수가 높은 단어들로부터 낮은 숫자를 부여한다. 그러므로 추가적인 전처리로 정수가 아주 큰 숫자는 단어장에서 제거해버리는 선택을 할 수도 있다.

 

강아지 : [1, 0, 0, 0, 0]
고양이 : [0, 1, 0, 0, 0]
애교 : [0, 0, 1, 0, 0]
컴퓨터 : [0, 0, 0, 1, 0]
노트북 : [0, 0, 0, 0, 1]

위와 같이 원-핫 인코딩을 통해 얻은 벡터를 원-핫 벡터(one-hot vector)라고 한다.

 

TF(Term Frequency)란, 문장을 구성하는 단어들의 원-핫 벡터들을 모두 더해서 문장의 단어 갯수로 나눈 것과 같다.

 

1.3 원-핫 인코딩 구현해보기

Step1. 패키지 설치하기

한국어로 실습하기 위해서는 한국어 형태소 분석기 패키지 KoNLPy가 필요하다.

$ pip install konlpy

 

실습에 필요한 도구를 임포트한다.

import re
from konlpy.tag import Okt
from collections import Counter
print("임포트 완료")

 

전처리와 토큰화가 전혀 되어 있지 않은 텍스트가 있다고 해보자.

text = "임금님 귀는 당나귀 귀! 임금님 귀는 당나귀 귀! 실컷~ 소리치고 나니 속이 확 뚫려 살 것 같았어."
text

 

Step 2. 전처리 이야기

정규 표현식을 사용하여 특수문자를 제거하고자 한다.

일반적으로 자음의 범위는 'ㄱ~ㅎ', 모음의 범위는 'ㅏ~ㅣ'와 같이 지정할 수 있다. 또한 완성형 항글의 범위는 '가 ~ 힣'과 같이 사용한다.

한글과 공백을 제외하고 모든 문자를 표현하는 정규 표현식은 다음과 같다.

▶ 한글, 공백을 제외한 모든 문자를 표현하는 regex : [^ㄱ-하-ㅣ가-힣]

 

reg = re.compile("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]")
text = reg.sub('', text)
print(text)

임금님 귀는 당나귀 귀 임금님 귀는 당나귀 귀 실컷 소리치고 나니 속이 확 뚫려 살 것 같았어

 

Step 3. 토큰화 이야기

단어장을 구성하기 위해서는 단어장의 원소인 토큰(token)이라는 단위를 정해줄 필요가 있다.

한국어는 주로 형태소 분석기를 통해서 토큰 단위로 나눠준다.

KoNLPy에 내장된 Okt 형태소 분석기를 사용해보자.

 

okt=Okt()
tokens = okt.morphs(text)
print(tokens)

['임금님', '귀', '는', '당나귀', '귀', '임금님', '귀', '는', '당나귀', '귀', '실컷', '소리', '치고', '나니', '속이', '확', '뚫려', '살', '것', '같았어']

 

Step 4. 단어장 만들기

빈도수가 높은 단어일수록 낮은 정수를 부여하려고 한다. 그렇기에 파이썬의 Counter 서브클래스를 사용해서 단어의 빈도를 카운트해보자.

 

vocab = Counter(tokens)
print(vocab)

Counter({'귀': 4, '임금님': 2, '는': 2, '당나귀': 2, '실컷': 1, '소리': 1, '치고': 1, '나니': 1, '속이': 1, '확': 1, '뚫려': 1, '살': 1, '것': 1, '같았어': 1})

 

단어가 키(key)로, 단어에 대한 빈도수가 값(value)으로 저장되어 있다. 그래서 vocab에 단어를 입력하면 빈도수를 리턴한다.

 

vocab['임금님']

2

 

most_common()은 상위 빈도수를 가진 단어를 주어진 수만큼 리턴한다.

이를 사용하여 등장 빈도수가 높은 단어들을 원하는 개수만큼 얻을 수 있다.

vocab_size = 5
vocab = vocab.most_common(vocab_size) # 등장 빈도수가 높은 상위 5개의 단어만 저장
print(vocab)

[('귀', 4), ('임금님', 2), ('는', 2), ('당나귀', 2), ('실컷', 1)]

 

빈도수 상위 5개의 단어만 남아 있는 것을 확인할 수 있다.

이제 높은 빈도수를 가진 단어일수록 낮은 정수 인덱스를 부여해보자.

word2idx={word[0] : index+1 for index, word in enumerate(vocab)}
print(word2idx)

{'귀': 1, '임금님': 2, '는': 3, '당나귀': 4, '실컷': 5}

 

word2idx를 최종 단어장으로 사용하자.

 

Step 5. 원-핫 벡터 만들기

다음은 특정 단어와 단어장을 입력하면 해당 단어의 원-핫 벡터를 리턴하는 함수이다.

def one_hot_encoding(word, word2index):
       one_hot_vector = [0]*(len(word2index))
       index = word2index[word]
       one_hot_vector[index-1] = 1
       return one_hot_vector
print("슝=3")

 

임금님이라는 단어의 원-핫 벡터를 출력해보자.

one_hot_encoding("임금님", word2idx)

 

[+] 케라스를 통한 원-핫 인코딩(one-hot encoding)

원-핫 인코딩을 지원하는 패키지 중 텐서플로의 케라스API를 사용해보자.

from tensorflow.keras.preprocessing.text import Tokenizer    # 단어장을 만드는 역할
from tensorflow.keras.utils import to_categorical       # 원-핫 인코딩을 위한 도구
print("임포트 완료")

 

3개의 문서를 text에 저장한다.

text = [['강아지', '고양이', '강아지'],['애교', '고양이'], ['컴퓨터', '노트북']]
text

[['강아지', '고양이', '강아지'], ['애교', '고양이'], ['컴퓨터', '노트북']]

 

케라스 토크나이저를 사용하면 주어진 텍스트로부터 단어장을 만들고, 단어장의 각 단어에 고유한 정수를 맵핑해준다.

t = Tokenizer()
t.fit_on_texts(text)
print(t.word_index) # 각 단어에 대한 인코딩 결과 출력.

{'강아지': 1, '고양이': 2, '애교': 3, '컴퓨터': 4, '노트북': 5}

 

단어장의 크기를 vocab_size라는 변수에 저장해두자.

vocab_size = len(t.word_index) + 1
print("슝=3")

vocab_size에 1을 더해주는 이유는 케라스 토크나이저는 각 단어에 고유한 정수를 부여할 때, 숫자 1부터 부여하지만 실제로 자연어 처리를 할 때는 특별 토큰으로 0번 단어로 단어장에 추가로 사용하는 경우가 많기 때문이다.

주로 0번은 패딩(padding) 작업을 위한 패딩 토큰으로 사용되는데, 여기서는 0번 단어도 고려해주는 것이 좋다는 정도로 이해하고 1을 더해서 단어장의 크기를 저장하겠다.

 

단어장에 속한 던어들로 구성된 텍스트 시퀀스는 케라스 토크나이저를 통해 정수 시퀀스로 변환할 수 있다.

sub_text = ['강아지', '고양이', '강아지', '컴퓨터']
encoded = t.texts_to_sequences([sub_text])
print(encoded)

[[1, 2, 1, 4]]

 

이렇게 변환된 정수 시퀀스는 to_categorical()을 사용해 원-핫 벡터의 시퀀스로 변환할 수 있다.

one_hot = to_categorical(encoded, num_classes = vocab_size)
print(one_hot)

[[[0. 1. 0. 0. 0. 0.]
  [0. 0. 1. 0. 0. 0.]
  [0. 1. 0. 0. 0. 0.]
  [0. 0. 0. 0. 1. 0.]]]

 

각 단어가 단어장의 크기인 6차원의 벡터로 변환된 것을 확인할 수 있다.


2. 워드 임베딩

2.1 희소 벡터(Sparse Vector)의 문제점

DTM, TF-IDF, 원-핫 벡터는 단어장의 크기에 영향을 받는 희소 벡터이다.

희소벡터에는 차원의 저주(curse of dimensionality)라는 문제가 있다.

[차원의 저주]https://leedakyeong.tistory.com/entry/curse-of-dimensionality-%EC%B0%A8%EC%9B%90%EC%9D%98-%EC%A0%80%EC%A3%BC 정보 밀도가 작아지는 것, 즉 차원이 커지는 것과 머신 러닝 모델의 성능

같은 정보를 저차원과 고차원에 각각 표현한다고 하였을 때, 저차원에서는 정보의 밀도가 상대적으로 커지지만, 고차원에서는 정보가 흩어지며 밀도가 작아진다.

 

원-핫 벡터는 두 단어의 의미적 유사성을 반영하지 못한다는 다른 문제점도 있다.

벡터 간 유사도를 구하는 방법으로는 대표적으로 내적(inner product)이 있다. 임의의 두개의 원-핫 벡터 간 내적을 구해보면, 대부분 서로 직교하여 그 값은 0이다. 거의 모든 원-핫 벡터의 상호 유사도가 0임을 의미하며 원-핫 벡터를 통해서는 단어 벡터 간 유사도를 구할 수 없음을 의미한다.

[내적이 0인 두 벡터는 서로 직교합니다] https://math.libretexts.org/Bookshelves/Calculus/Book%3A_Vector_Calculus_(Corral)/01%3A_Vectors_in_Euclidean_Space/1.03%3A_Dot_Product

이러한 대안으로, '기계가 단어장 크기보다 적은 차원의 밀집 벡터(dense vector)를 학습'하는 워드 임베딩(word embedding)이 제안되었다.

이를 통해 얻는 밀집 벡터는 각 차원이 0과 1이 아닌 다양한 실숫값을 가지며, 이 밀집 벡터는 임베딩 벡터(embedding vector)라고 한다.

 

2.2 워드 임베딩(Word Embedding)

벡터의 길이가 단어장 크기보다 매우 작기 때문에 각 벡터 값에 정보가 축약되어야 하고 결국 밀집 벡터(dense vector)가 된다.

밀집벡터는 각 벡터 값의 의미가 파악하기 어려울 정도로 많은 의미를 함축하고 있다.

[희소 벡터(sparse vector)와 밀집 벡터(dense vector)] https://www.pinecone.io/learn/dense-vector-embeddings-nlp/

워드 임베딩에서는 단어가 갖는 특성을 계산할 수 있는 방법이 제안된다. 단어 사이의 관계나 문장에서 단어가 갖는 특징을 수식으로 나타내고 계산해서 정확한 숫자로 나타내도록 하는 것이다. 내적을 활용하거나 인공 신경망을 활용하는 등 사용되는 방법이나 수식은 다양하게 있다.

 

워드 임베딩에서 중요한 것은 2가지이다.

  • 한 단어를 길이가 비교적 짧은 밀집 벡터로 나타낸다.
  • 이 밀집 벡터는 단어가 갖는 의미나 단어 간의 관계 등을 어떤 식으로든 내포하고 있다.

이렇게 만들어진 밀집 벡터를 임베딩 벡터라고 하며 임베딩 벡터의 값은 훈련 데이터로부터 어떤 모델을 학습하는 과정에서 '자동'으로 얻어지는데, 주로 언어 모델(Language Model)을 학습하는 가운데 얻어진다.

 

워드 임베딩은 NPLM이란 모델로 제안되었는데, 학습 속도가 지나치게 느리다는 단점으로 여러가지 임베딩 방법이 추가로 제안되었다.


3. Word2Vec

http://w.elnn.kr/search/

Korean Word2Vec

 

위 사이트는 한국어 데이터로 Word2Vec을 학습하여, 학습된 Word2Vec 벡터들로 연산한 결과를 제공하는 사이트이다.

 

[Word2Vec 강의]

 

3.1 분포 가설(Distributional Hypothesis)

Word2Vec의 핵심 아이디어는 분포가설을 따른다.

분포가설 : 비슷한 문맥에서 같이 등장하는 경향이 있는 단어들은 비슷한 의미를 가진다.

 

분포가설에 따르는 Word2Vec은 같이 등장하는 경향이 적은 단어들에 비해 '강아지', '귀여운'과 같은 상대적으로 유사도가 높은 벡터로 만든다.

 

3.2 CBoW (Continuous Bag of words)

Word2Vec에는 크게 CBoWSkip-gram이라는 두 가지 방법이 있다.

CBoW는 주변에 있는 단어들을 통해 중간에 있는 단어들을 예측하는 방법이다.

Skip-gram은 중간에 있는 단어로 주변 단어들을 예측하는 방법이다.

 

예문 : "I like natural language processing."

 

CBoW는 중간에 있는 단어를 예측하는 방법이므로  {"i", "like", "language", "processing"}으로부터 "natural"을 예측한다.

이 때 예측해야 하는 단어 'natural'을 중심단어(center word), 예측에 사용되는 단어들을 주변 단어(context word)라고 한다.

중심단어를 예측하기 위해 앞, 뒤로 몇 개의 단어를 볼 지, 그 범위를 윈도우(window)라고 한다.

만약 윈도우 크기가 1이고, 예측하고자 하는 중심단어가 'language'라면 앞 뒤의 한 단어를 참고한다. 윈도우 크기가 m일 때, 중심 단어를 예측하기 위해 참고하는 주변 단어의 개수는 2m이다.

https://medium.com/analytics-vidhya/word2vector-using-gensim-e055d35f1cb4

윈도우 크기를 정했다면, 윈도우를 계속 움직여서 주변 단어와 중심 단어를 바꿔가며 학습을 위한 데이터 셋을 만드는 방법을 '슬라이딩 윈도우(sliding window)'라고 한다.

슬라이딩 윈도우를 처음부터 끝까지 마친다면 다음과 같은 데이터셋을 얻을 수 있다.

→ ((like), I), ((I, natural), like), ((like, language), natural), ((natural, processing), language), ((language), processing)

 

이렇게 선택된 데이터셋에서 각 단어들은 원-핫 인코딩이 되어 원-핫 벡터가 되고 원-핫 벡터가 CBoW나 Skip-gram의 입력이 된다.

 

https://lilianweng.github.io/lil-log/2017/10/15/learning-word-embedding.html

원-핫 벡터로 변환된 다수의 주변 단어를 이용해 원-핫 벡터로 변환된 중심 단어를 예측할 때의 CBoW의 동작 메커니즘을 보여준다.

윈도우 크기가 m이라면 2m개의 주변 단어를 이용해 1개의 중심 단어를 예측하는 과정에서 두 개의 가중치 행렬(matrix)을 학습하는 것이 목적이다.

 

CBoW는 입력층, 은닉층, 출력층 3개의 층으로 구성된 인공 신경망이다.

Word2Vec은 은닉층이 1개라서 딥러닝이라기보다는 얕은 신경망(Shallow Neural Network)을 학습한다고 볼 수 있다.

 

CBoW에서 입력층과 출력층의 크기는 단어 집합의 크기인 V로 고정되어 있다. 하지만 은닉층의 크기는 사용자가 정의해주는 하이퍼파라미터이다. (여기서 은닉층의 크기를 N이라고 해보자.)

 

입력층에서 은닉층으로 가는 과정을 살펴보자.

https://towardsdatascience.com/what-the-heck-is-word-embedding-b30f67f01c81

주변 단어로 선택된 각각의 원-핫 벡터는 첫 번째 가중치 행렬(V x N)과 곱해지게 된다.

원-핫 벡터와 가중치 행렬과의 곱은 가중치 행렬의 i 위치에 있는 행을 그대로 가져오는 것과 동일하다. 이를 마치 테이블에서 값을 그대로 룩업(lookup)해오는 것과 같다고 하여 룩업 테이블(lookup table)이라고 한다.

(위의 그림은 단어장의 크기(V)가 5, 은닉층의 크기(N)가 4이다.)

 

[은닉층 연산] https://medium.com/@jonathan_hui/nlp-word-embedding-glove-5e7f523999f6

룩업 테이블을 거쳐서 생긴 2m개의 주변 단어 벡터들은 각각 N의 크기를 가진다. 

CBoW에서는 이 벡터들을 모두 합하거나 평균을 구한 값을 최종 은닉층의 결과로 한다. 그러면 최종 은닉층의 결과도 N차원의 벡터가 된다. (Word2Vec에서는 은닉층에서 활성화 함수나 편향(bias)을 더하는 연산을 하지 않는다.)

단순히 가중치 행렬과의 곱셈만을 수행하기에 기존 신경망의 은닉층과 구분 지어 Word2Vec의 은닉층을 투사층(projection layer)이라고 한다.

 

은닉층에서 출력층으로 가는 과정과 출력층의 연산을 보자.

[출력층 연산] https://lilianweng.github.io/lil-log/2017/10/15/learning-word-embedding.html

은닉층에서 생성된 N차원의 벡터는 두 번째 가중치 행렬(N x V)과 곱해진다. 벡터의 차원은 V가 된다.

출력층은 활성화 함수로 소프트맥스 함수를 사용하므로 이 V차원의 벡터는 활성화 함수를 거쳐 모든 차원의 총합이 1이 되는 벡터로 변경된다.

 

CBoW는 출력층의 벡터를 중심 단어의 원-핫 벡터와의 손실(loss)을 최소화하도록 학습시킨다.

이 과정에서 첫 번째, 두 번째 가중치 행렬이 업데이트 되는데, 학습이 다 되었다면 N차원의 크기를 갖는 첫 번째 가중치 행렬의 행이나 두 번째 가중치 행렬의 열로부터 어떤 것을 임베딩 벡터로 사용할지를 결정하면 된다. 때로는 2개의 가중치 행렬의 평균치를 임베딩 벡터로 선택하기도 한다.

 

3.3 Skip-gram

중심 단어로부터 주변 단어를 예측하는 것이 Skip-gram이다.

https://medium.com/analytics-vidhya/word2vector-using-gensim-e055d35f1cb4

중심단어로부터 주변 단어 각각을 예측하기 때문에 CBoW와 데이터셋 구성이 다르다.

→ (i, like) (like, I), (like, natural), (natural, like), (natural, language), (language, natural), (language, processing), (processing, language)

 

https://lilianweng.github.io/lil-log/2017/10/15/learning-word-embedding.html

중심 단어로부터 주변 단어를 예측한다는 점, 그로 인해 중간에 은닉층에서 다수의 벡터의 덧셈과 평균을 구하는 과정이 없어졌다는 점만 제외하면 CBoW와 메커니즘 자체는 동일하다.

Skip-gram도 학습 후에 첫 번째 가중치 행렬의 행 또는 두 번째 가중치 행렬의 열로부터 임베딩 벡터를 얻을 수 있다.

 

3.4 네거티브 샘플링(negative sampling)

Word2Vec을 사용할 때 SGNS(Skip-gram with Negative Sampling)을 사용한다. 즉, Skip-gram을 사용하면서 네거티브 샘플링이라는 방법도 사용한다는 것이다.

 

출력층에서 소프트맥스 함수를 통과한 V차원의 벡터와 레이블에 해당되는 V차원의 주변 단어의 원-핫 벡터와의 오차를 구하고 역전파를 통해 모든 단어에 대한 임베딩 벡터를 조정한다. 단어장의 크기가 매우 많다면 이 작업이 매우 느릴 것이다.

하지만 중심 단어와 연관 관계가 없는 단어들의 임베딩 값은 굳이 업데이트할 필요가 없기에 네거티브 샘플링은 연산량을 줄이기 위해서 소프트맥스 함수를 사용한 V개 중 1개를 고르는 다중 클래스 분류 문제를 시그모이드 함수를 사용한 이진 분류 문제로 바꾸기로 한다.

 

[Skip-gram] http://jalammar.github.io/illustrated-word2vec/

기존의 skip-gram은 중심 단어로부터 주변 단어를 예측하는 방식이다.

 

[네거티브 샘플링] http://jalammar.github.io/illustrated-word2vec/

그러나 네거티브 샘플링을 사용하면 위와 같이 바뀐다.

중심 단어와 주변 단어를 입력값으로 받아 두 단어가 이웃 관계면 1을 출력하는 문제로 바꾼다. (다중 분류 문제에서 이진 분류 문제로 바뀐 것임)

 

예문 : Thou shalt not make a machine in the likeness of a human mind

윈도우 크기가 2일 때, 슬라이딩 윈도우를 통해서 만들어지는 skip-gram의 데이터셋은 다음과 같다.

(skip-gram 방식이기에 input word는 중심단어, target word는 주변 단어를 의미한다.)

[skip-gram의 데이터셋] http://jalammar.github.io/illustrated-word2vec/

이 데이터셋에 새롭게 레이블을 달아보자. 슬라이딩 윈도우를 통해 만들어진 정상적인 데이터셋에는 1이라는 레이블을 달아준다.

http://jalammar.github.io/illustrated-word2vec/

랜덤으로 단어장에 있는 아무 단어를 가져와 target word로 하는 거짓 데이터셋을 만들고 0으로 레이블링을 하여 거짓(negative) 데이터셋을 만든다.

 

http://jalammar.github.io/illustrated-word2vec/

이렇게 완성된 데이터셋으로 학습하면 Word2Vec은 더 이상 다중 클래스 분류 문제가 아니라 이진 분류 문제로 간주할 수 있다.

중심 단어와 주변 단어를 내적하고, 출력층의 시그모이드 함수를 지나게 하여 1 또는 0의 레이블로부터 오차를 구해서 역전파를 수행한다.

http://jalammar.github.io/illustrated-word2vec/

이런 학습 방식은 기존의 소프트맥스 함수를 사용했던 방식보다 상당량의 연산량을 줄일 수 있는 효과를 가지고 있다. 다양한 분야에 손쉽게 응용할 수 있는 아이디어이다.

 

3.5 영어 Word2Vec 실습

영어 데이터를 다운로드 받아 직접 Word2Vec을 훈련시켜보자.

Word2Vec을 별도로 구현할 필요없이 파이썬의 gensim 패키지를 통해 이미 구현된 Word2Vec 모델을 사용할 수 있다.

(훈련 데이터는 NLTK에서 제공하는 코퍼스이며, gensim 패키지는 토픽 모델링을 위한 NLP 패키지다.)

 

$ pip install nltk
$ pip install gensim

 

NLTK에 내장된 코퍼스를 다운로드한다.

import nltk
nltk.download('abc')
nltk.download('punkt')

 

NLTK의 코퍼스를 불러와 corpus라는 변수에 저장한다.

from nltk.corpus import abc
corpus = abc.sents()
print("슝~")

 

코퍼스가 정상적으로 로딩되었는지 확인해보자.

print(corpus[:3])

[['PM', 'denies', 'knowledge', 'of', 'AWB', 'kickbacks', 'The', 'Prime', 'Minister', 'has', 'denied', 'he', 'knew', 'AWB', 'was', 'paying', 'kickbacks', 'to', 'Iraq', 'despite', 'writing', 'to', 'the', 'wheat', 'exporter', 'asking', 'to', 'be', 'kept', 'fully', 'informed', 'on', 'Iraq', 'wheat', 'sales', '.'], ['Letters', 'from', 'John', 'Howard', 'and', 'Deputy', 'Prime', 'Minister', 'Mark', 'Vaile', 'to', 'AWB', 'have', 'been', 'released', 'by', 'the', 'Cole', 'inquiry', 'into', 'the', 'oil', 'for', 'food', 'program', '.'], ['In', 'one', 'of', 'the', 'letters', 'Mr', 'Howard', 'asks', 'AWB', 'managing', 'director', 'Andrew', 'Lindberg', 'to', 'remain', 'in', 'close', 'contact', 'with', 'the', 'Government', 'on', 'Iraq', 'wheat', 'sales', '.']]

 

코퍼스의 크기를 확인해보자.

print('코퍼스의 크기 :',len(corpus))

코퍼스의 크기 : 29059

 

29000개의 코퍼스를 가지고 Word2Vec을 훈련시켜보자.

from gensim.models import Word2Vec

model = Word2Vec(sentences = corpus, vector_size = 100, window = 5, min_count = 5, workers = 4, sg = 0)
print("모델 학습 완료!")

 

▼ 파라미터의 의미 ▼
vector size = 학습 후 임베딩 벡터의 차원
window = 컨텍스트 윈도우 크기
min_count = 단어 최소 빈도수 제한 (빈도가 적은 단어들은 학습하지 않음)
workers = 학습을 위한 프로세스 수
sg = 0은 CBoW, 1은 Skip-gram.

 

Word2Vec은 입력한 단어에 대해 가장 코사인 유사도가 높은 단어들을 출력하는 model.wv.most_similar를 지원한다.

'man'과 가장 유사한 단어들을 확인해보자.

model_result = model.wv.most_similar("man")
print(model_result)

[('woman', 0.9233373999595642), ('skull', 0.911032497882843), ('Bang', 0.9056490063667297), ('asteroid', 0.9051957130432129), ('third', 0.9020178318023682), ('baby', 0.8993921279907227), ('dog', 0.8985978364944458), ('bought', 0.8975234031677246), ('rally', 0.8912491798400879), ('disc', 0.8888981342315674)]

 

이렇게 학습한 모델을 저장하고 로드하는 방법을 알아보자.

from gensim.models import KeyedVectors

model.wv.save_word2vec_format('~/aiffel/word_embedding/w2v') 
loaded_model = KeyedVectors.load_word2vec_format("~/aiffel/word_embedding/w2v")
print("모델  load 완료!")

 

loaded_model에는 저장되었던 모델이 다시 로드된 상태이다. 로드한 모델이 이전과 동일한 결과를 출력하는지 테스트해보자.

model_result = loaded_model.most_similar("man")
print(model_result)

[('woman', 0.9233373999595642), ('skull', 0.911032497882843), ('Bang', 0.9056490063667297), ('asteroid', 0.9051957130432129), ('third', 0.9020178318023682), ('baby', 0.8993921279907227), ('dog', 0.8985978364944458), ('bought', 0.8975234031677246), ('rally', 0.8912491798400879), ('disc', 0.8888981342315674)]

 


4. 임베딩 벡터의 시각화

구글이 공개한 임베딩 벡터의 시각화 오픈 소스인 임베딩 프로젝터(embedding projector)를 사용해서 임베딩 벡터들을 시각화해보자.

임베딩 프로젝터를 통해 어떤 임베딩 벡터들이 가까운 거리에 군집이 되어 있고, 특정 임베딩 벡터와 유클리드 거리나 코사인 유사도가 높은지 확인할 수 있다.

 

Step 1. 필요한 파일 만들기

임베딩 프로젝터를 통해 임베딩 벡터를 시각화하기 위해서는 이미 저장된 모델이 필요하다. 이미 저장된 모델로부터 벡터값이 저장된 파일과 메타파일을 얻어야 하기 때문이다. 위에서 'w2v'로 저장한 모델을 활용하자. 

 

아래의 커맨드를 실행해보자.

$ python -m gensim.scripts.word2vec2tensor --input ~/aiffel/word_embedding/w2v --output ~/aiffel/word_embedding/w2v

해당 커맨드를 수행하면 w2v_metadata.tsv와 w2v_tensor.tsv 파일이 ~/aiffel/word_embedding 경로에 생성된다. 이 2개의 파일을 인터넷 환경에 업로드할 수 있도록 준비하자.

 

Step 2. 임베딩 프로젝터에 tsv 파일 업로드하기

 

Embedding projector - visualization of high-dimensional data

Visualize high dimensional data.

projector.tensorflow.org

 

https://wikidocs.net/50704

좌측 상단의 화면에서 Load라는 버튼을 누르면 아래와 같은 창이 뜬다.

 

Step 1에는 각각의 벡터값이 저장된 tsv 파일을 업로드하고, Step 2에는 메타 데이터의 tsv 파일을 업로드하라고 되어 있다.

파일을 모두 업로드하면 임베딩 프로젝터에 학습했던 워드 임베딩 모델이 시각화된다.

Search 버튼 또는 그래프의 포인트를 클릭해 원하는 단어를 선택하고, neightbors에 몇 개까지의 이웃을 검색할 지 선택한다.

distance에서 COSINE 또는 EUCLIDEAN을 통해서 거리 측정 메트릭을 코사인 유사도로할 것인지, 유클리드 거리로 할 것인지 선택할 수 있다.


5. FastText

페이스북에서 개발한 FastText는 Word2Vec 이후에 등장한 워드 임베딩 방법으로, 메커니즘 자체는 Word2Vec을 따르고 있지만 문자 단위 n-gram(character-level n-gram) 표현을 학습한다는 점에서 다르다.

 

Word2Vec은 단어를 더 이상 깨질 수 없는 단위로 구분하는 반면에 FastText는 단어 내부의 내부 단어(subwords)들을 학습한다는 아이디어를 가지고 있다.

 

여기서 n은 단어들이 얼마나 분리되는지 결정하는 하이퍼파라미터이다.

n = 3인 경우, FastText는 단어 partial에 대해 임베딩되는 n-gram 토큰들은 다음과 같다.

<pa, art, rti, tia, ial, al>, <partial>

 

실제 사용할 때는 n의 최솟값과 최댓값으로 범위를 설정할 수 있다. (gensim 패키지에서는 기본값으로 각각 3과 6으로 설정되어 있다.)

최솟값이 3, 최댓값이 6인 경우 토큰들은 다음과 같다.

<pa, art, rti, ita, ial, al>, <par, arti, rtia, tial, ial>, <part, ...중략... , <partial>

 

내부 단어들을 벡터화한다는 의미는 저 단어들 각각에 대해서 Word2Vec을 수행한다는 의미이다.

최종적으로 벡터화된 n-gram 벡터들의 총합을 해당 단어의 벡터로 취한다.

# 각 원소는 벡터임을 가정함
partial = <pa + art + rti + ita + ial + al> + <par + arti + rtia + tial + ial> + <part + ...중략... + <partial>

 

5.1 FastText의 학습 방법

FastText도 Word2Vec과 마찬가지로 네거티브 샘플링을 사용하여 학습한다.

그러나 Word2Vec과 다른 점은 학습 과정에서 중심 단어에 속한 문자 단위 n-gram 단어 벡터들을 모두 업데이트한다는 점이다.

 

5.2 OOV와 오타에 대한 대응

FastText는 Word2Vec과 달리 OOV와 오타에 강건하다는 특징이 있다. 단어장에 없는 단어라도 해당 단어의 n-gram이 다른 단어에 존재하면 이로부터 벡터값을 얻는다는 원리에 기인한다.

 

Word2Vec에서 사용했던 동일한 훈련 데이터를 corpus에 저장했다고 가정하자. 이를 가지고 FastText를 학습해보자.

from gensim.models import FastText
fasttext_model = FastText(corpus, window=5, min_count=5, workers=4, sg=1)
print("FastText 학습 완료!")

 

Word2Vec에서 에러가 발생했던 단어들을 FastText 모델에 입력해보자.

fasttext_model.wv.most_similar('overacting')

[('extracting', 0.9430216550827026),
 ('lifting', 0.9349257946014404),
 ('overwhelming', 0.9336735010147095),
 ('tapping', 0.9326133131980896),
 ('resolving', 0.9321481585502625),
 ('shooting', 0.9293471574783325),
 ('fluctuating', 0.9293177723884583),
 ('attracting', 0.9290770888328552),
 ('melting', 0.9262456297874451),
 ('malting', 0.9250458478927612)]

 

'overactiong'이 단어장에 없던 단어임에도 정상적으로 임베딩 벡터값이 계산되어 유의미한 단어 10개를 출력하는 것을 볼 수 있다.

 

fasttext_model.wv.most_similar('memoryy')

[('memory', 0.9497514963150024),
 ('musical', 0.8836714029312134),
 ('mechanisms', 0.8654826879501343),
 ('music', 0.8631953001022339),
 ('technical', 0.8557988405227661),
 ('intelligence', 0.8551561832427979),
 ('mechanism', 0.8545279502868652),
 ('basic', 0.8518893718719482),
 ('imagine', 0.8458789587020874),
 ('athletic', 0.8325231075286865)]

 

오타를 가정한 단어 'memoryy'도 마찬가지로 유의미한 단어가 출력이 되고, 가장 유사한 단어로 memory가 출력이 되는 것을 확인할 수 있다.

 

5.3 한국어에서의 FastText

영어의 경우 알파벳 단위가 n-gram이었다면 한국어의 경우 음절 단위라고 볼 수 있다.

 

음절 단위 FastText

n = 3일때, 단어 '텐서플로우'의 트라이그램 벡터들은 다음과 같다.

→ <텐서, 텐서플, 서플로, 플로우, 로우>, <텐서플로우>

 

하지만 한국어에서는 자소 단위인 경우 FastText가 꽤 잘 작동한다.

 

자소 단위 FastText

단어에 대해서 초성, 중성, 종성을 분리한다고 하고, 종성이 존재하지 않는 경우에는 _라는 토큰을 대신 사용해보자.

n = 3일 때, 단어 '텐서플로우'의 트라이그램 벡터들은 다음과 같다.

<ㅌㅔ,ㅌㅔㄴ,ㅔㄴㅅ,ㄴㅅㅓ,ㅅㅓ_, ...중략... >

 

'프랑스'와 같은 고유 명사는 분해해도 이득을 볼 수 없다. 그 이유는 한국어 어휘를 자모 수준으로 분리하여 학습하는 것은 일부 어휘의 특성을 학습하는데 큰 도움이 되지 않거나 가끔 성능의 저하를 일으키기 때문이다.

 

 

한국어를 위한 어휘 임베딩의 개발 -2-

한국어 자모의 FastText의 결합 | 이 글은 Subword-level Word Vector Representations for Korean (ACL 2018)을 다룹니다. 두 편에 걸친 포스팅에서는 이 프로젝트를 시작하게 된 계기, 배경, 개발 과정의 디테일을 다

brunch.co.kr


6. GloVe

글로브(Global Vectors for Word Representation, GloVe)는 2014년에 미국 스탠포드 대학에서 개발한 워드 임베딩 방법론이다. 워드 임베딩의 두 가지 접근 방법인 카운트 기반예측 기반 두 가지 방법을 모두 사용했다는 것이 특징이다.

 

단어의 빈도를 수치화한 방법인 DTM(카운트 기반 방법)의 경우, 단어 간 유사도를 반영할 수 없을 뿐만 아니라 대부분의 값이 0인 희소 표현이라는 특징이 있다. DTM을 차원 축소하여 밀집 표현(dense representation)으로 임베딩 하는 방법이 LSA(Latent Semantic Analysis)이다.

 

6.1 잠재 의미 분석(LSA, Latent Semantic Analysis)

LSA를 요약하면 DTM에 특잇값 분해를 사용하여 잠재된 의미를 이끌어내는 방법론이다. 그 결과의 행벡터를 사용해서 임베딩 벡터를 얻을 수 있다.

LSA는 단어를 카운트해서 만든 DTM을 입력으로 하므로 카운트 기반의 임베딩 방법이라고 볼 수 있는데, 이 방법은 몇 가지 한계가 있다.

 

728x90
반응형