워드투벡터(Word2Vec)
앞서 원-핫 인코딩 챕터에서 원-핫 인코딩으로 표현된 원-핫 벡터는 단어 간 유사성을 계산할 수 없다는 단점이 있음을 언급한 적이 있다. 이는 원-핫 인코딩으로 단어를 표현하면 벡터에 단어의 의미가 들어가 있지 않기 때문이다. 의미가 담겨있지 않은 벡터들을 비교하려고 해도 비교할 수는 없다. 그렇다면, 단어 간 유사성을 고려하기 위해서는 단어의 의미를 벡터화하면 되겠다.
그리고 이를 가능하게 하는 방법이 워드투벡터(Word2Vec)이다. 워드투벡터의 개념을 설명하기에 앞서, 워드투벡터가 어떤일을 할 수 있는지 먼저 확인하고 간다.
위 사이트는 한국어로 워드투벡터 연산을 해볼 수 있는 사이트다. 위 사이트에서는 단어들로 더하기, 빼기 연산을 할 수 있다. 예를 들어 아래의 식에서 좌변을 집어 넣으면, 우변의 답들이 나온다.
고양이 + 애교 = 강아지
한국 - 서울 + 도쿄 = 일본
박찬호 - 야구 + 축구 = 호나우두
신기하게도 단어가 갖고 있는 의미들을 가지고 연산을 하고 있는 것처럼 보인다. 이런 연산이 가능한 이유는 각 단어에 있는 어떤 상징성, 의미가 벡터로 계산되었기 때문이다. 어떻게 이런 일이 가능한 걸까?
1. 희소 표현(Sparse Representation)
앞서 원-핫 인코딩을 통해서 나온 원-핫 벡터들은 표현하고자 하는 단어의 인덱스의 값만 1이고, 나머지 인덱스에는 전부 0으로 표현되는 벡터 표현 방법이었다. 이렇게 벡터 또는 행렬(Matrix)의 값이 대부분 0으로 표현되는 방법을 희소 표현(Sparse Representation)이라고 한다. 그러니까 원-핫 벡터는 희소 벡터(Sparse Vector)이다.
하지만 이러한 표현 방법은 각 단어간 유사성을 표현할 수 없다는 단점이 있었고, 이를 위한 대안으로 단어의 ‘의미’를 다차원 공간에 벡터화하는 방법을 찾게 되는데, 이러한 표현 방법을 분산 표현(Distributed Representation)이라고 한다. 그리고 이렇게 분산 표현을 이용하여 단어의 의미를 벡터화하는 작업은 워드 임베딩(Embedding) 작업에 속하기 때문에 이렇게 표현된 벡터 또한 임베딩 벡터(Embedding Vector)라고 하며, 저차원을 가지므로 바로 앞의 챕터에서 배운 밀집 벡터(Dense Vector)에도 속한다.
2. 분산 표현(Distributed Representation)
분산 표현(Distributed Representation) 방법은 기본적으로 분포 가설(Distributional Hypothesis)이라는 가정 하에 만들어진 표현 방법이다. 이 가정은 ‘비슷한 위치에서 등장하는 단어들은 비슷한 의미를 가진다’ 라는 가정이다. 강아지란 단어는 귀엽다, 예쁘다, 애교 등의 단어가 주로 함께 등장하게 되는데 분포 가설에 따라서 저런 내용을 가진 텍스트를 벡터화한다면 저 단어들은 의미적으로 가까운 단어가 되는 것이다. 분산 표현은 분포 가설 가정을 이용하여 코퍼스로부터 단어들의 데이터 셋을 학습하고, 벡터에 단어의 의미를 여러 차원에 분산하여 벡터로 표현한다.
이렇게 표현된 벡터들은 원-핫 벡터처럼 굳이 벡터의 차원이 단어 집합(Vocabulary)의 크기일 필요가 없으므로, 벡터의 차원이 저차원으로 줄어든다. 예를 들어 단어가 10,000개 있고 인덱스가 1부터 시작한다고 했을 때, 강아지란 단어의 인덱스는 5였다면 원-핫 벡터는 이렇게 표현되어야 했다.
ex) 강아지 = [ 0 0 0 0 1 0 0 0 0 0 .. 중략.. 0 ]
1이란 값 뒤에는 0이 9995개 등장하는 벡터가 된다. 하지만 워드투벡터로 임베딩 된 벡터는 굳이 벡터의 차원이 단어 집합의 크기가 될 필요가 없다. 강아지란 단어를 표현하기 위해 사용자가 설정한 차원의 크기만 가지면서 각 차원은 실수형의 값을 가진다.
ex) 강아지 = [0.2 0.3 0.5 0.7 0.2 … 중략 … 0.2]
요약하면 희소 표현이 고차원에 각 차원이 분리된 표현 방법이었다면, 분산 표현은 저차원에 단어의 의미를 여러 차원에다가 분산하여 표현한다. 그리고 이러 표현 방법을 사용하면 단어간 유사도를 계산할 수 있다.
이를 위하 학습 방법으로는 전통적인 NNLM, RNNLM이 있었으나 현재에 와서는 해당 방법들의 속도를 대폭 개선시킨 워드투벡터(Word2Vec)가 쓰이고 있다. 이 챕터에서는 워드투벡터에 대해서 배워보도록 한다. 또한, 주로 영어로 기재되는 방법이기에 Word2Vec으로 표기하고 설명했다.
3. CBOW(Countinous Bag of Words)
Word2Vec에는 CBOW(Continous Bag of Words)와 Skip-Gram 두 가지 방식이 있다. CBOW는 주변에 있는 단어들을 가지고, 중간에 있는 단어들을 예측하는 방법이다. 반대로, Skip-Gram은 중간에 있는 단어로 주변 단어들을 예측하는 방법이다. 메커니즘 자체는 거의 동일하기 때문에 CBOW를 이ㅐ한다면 Skip-Gram도 손쉽게 이해 가능하다. 우선 CBOW에 대해서 알아보도록 하자. 이해를 위해 매우 간소화 된 형태의 CBOW로 설명한다.
예문 : “The fat cat sat on the mat”
예를 들어서 우리가 갖고 있는 코퍼스에 위와 같은 문장이 있다고 하자. 가운데 단어를 예측하는 것이 CBOW라고 했다. {“The”, “fat”, “cat”, “on”, “the”, “mat”}으로부터 sat을 예측하는 것은 CBOW가 하는 일이다. 이 때 예측해야하는 단어 sat을중심 단어(Center word)라고 하고, 예측에 사용되는 단어들을 주변 단어(Context word)라고 한다.
중심 단어를 예측하기 위해서 앞, 뒤로 몇 개의 단어를 볼지를 결정했다면 이 범위를 윈도우(window)라고 한다. 예를 들어서 윈도우 크기가 2이고, 예측하고자 하는 중심 단어가 sat이라고 한다면 앞의 두 단어인 The와 fat, 그리고 뒤의 두 단어인 on, the를 참고한다. (여기서는 The와 the를 다른 단어라고 생각하겠다. 보통은 같은 단어에 대해서 대,소문자가 다르다면 동일한 단어로 바꿔주는 2챕터에서 배운 정규화 과정을 적용하는 것이 보통이다.) 향후, 수학적 일반화를 위해 미리 언급하자면 윈도우 크기가 m이라고 한다면, 실제 중심 단어를 예측하기 위해 참고하려고 하는 주변 단어의 개수는 2m이 될 것이다.
윈도우 크기를 정했다면, 윈도우를 계속 움직여서 주변 단어와 중심 단어 선택을 바꿔가며 학습을 위한 데이터 셋을 만들 수 있는데, 이 방법을 슬라이딩 윈도우(Sliding Window)라고 한다.
위 그림에서 좌측의 중심 단어와 주변 단어의 변화는 윈도우 킉가 2일때, 슬라이딩 윈도우가 어떤 식으로 이루어지면서 데이터 셋을 만드는지 보여준다. 또한 Word2Vec에서 입력은 모두 원-핫 벡터가 되어야 하는데, 우측 그림은 중심 단어와 주변 단어를 어떻게 선택했을 때에 따라서 각각 어떤 원-핫 벡터가 되는지를 보여준다. 위 그림은 결국 CBOW를 위한 전체 데이터셋을 보여주는 것이다.
CBOW의 인공 신경망을 간단히 도식화하면 다음과 같다. 입력층(Input layer)의 입력으로서 앞, 뒤로 사용자가 정한 윈도우 크기 범위 안에 있는 주변 단어들의 원-핫 벡터가 들어가게 되고, 출력층(Output layer)에서 예측하고자 하는 중간 단어의 원-핫 벡터가 필요하다. 뒤에서 설명하겠지만, Word2Vec의 학습을 위해서 이 중간 단어의 원-핫 벡터가 필요하다.
또한 이미지를 통해 알 수 있는 사실은, Word2Vec은 딥 러닝 모델(Deep Learning Model)이 아니라는 점이다. 보통 딥 러닝이라함은, 은닉층의 개수가 충분히 쌓인 신경망을 학습할 때를 말하며 Word2Vec은 오직 하나의 은닉층만이 존재한다. 이렇게 은닉층(Hidden Layer)이 1개인 경우에는 심층신경망(Deep Neural Network)이 아니라 얕은 신경망(Shallow Neural Network)이라고 할 수 있다.
CBOW의 인공 신경망을 좀 더 확대하여, 동작 메커니즘에 대해서 상세하게 알아보도록 한다. 이 그림에서 주목해야할 것은 두 가지이다. 하나는 은닉층의 크기가 N이라는 점이다. CBOW에서 은닉층의 크기 N은 임베딩하고 난 벡터의 크기가 된다. 다시 말해, 위의 그림에서 은닉층의 크기는 N=5이기 때문에 해당 CBOW를 수행하고나서 나오는 벡터의 크기는 5가 될 것이다.
두번째는 입력층과 은닉층 사이의 가중치 W는 V × N 행렬이며, 은닉층에서 출력층 사이의 가중치 W’은 N × V 행렬이라는 점이다. 여기서 V는 단어 집합의 크기를 의미한다. 즉, 위의 그림처럼 원-핫 벡터의 차원이 7이고, N은 5라면 가중치 W는 7 × 5 행렬이고, W’는 5 × 7 행렬이 될 것이다.
주의할 점은 이 두 행렬은 동일한 행렬을 전치(Transpose)한 것이 아니라, 서로 다른 행렬이라는 점이다. 인공 신경망의 훈련 전에 이 가중치 행렬 W와 W’는 대게 굉장히 작은 랜덤 값을 가지게 된다. CBOW는 주변 단어로 중심 단어를 더 정확히 맞추기 위해서 계속해서 이 W와 W’를 학습해가는 구조이다.
입력으로 들어오는 주변 단어의 원-핫 벡터와 가중치 W 행렬의 곱이 어떻게 이루어지는 보자. 위 그림에서는 각 주변 단어의 원-핫 벡터를 x로 표기하였다. 입력 벡터는 원-핫 벡터이기 때문에 i번째 인덱스에 1이라는 값을 가지고 그 외의 0의 값을 가지는 입력 벡터오 가중치 W 행렬의 곱은 사실 W행렬의 i번째 행을 그대로 읽어오는 것과(look up) 동일하다. 그래서 이 작업을 테이블 룩업(Table lookup)이라고 부른다. 앞서 CBOW의 목적은 W와 W’을 잘 훈련시키는 것이라고 언급한 적이 있는데, 사실 그 이유가 여기서 lookup해온 W의 각 행벡터가 사실 Word2Vec을 수행한 후의 각 단어의 N차원의 크기를 갖는 임베딩 벡터들이기 때문이다.
이렇게 각 주변 단어의 원-핫 벡터에 대해서 가중치 W가 곱해서 생겨진 결과 벡터들은 은닉층에서 만나 이 벡터들의 평균인 벡터를 구하게 된다. 만약 윈도우 크기가 2라면, 입력 벡터의 총 개수는 2m이기 때문에 중간 단어를 예측하기 위해서는 총 4개가 입력 벡터로 들어가게 된다. 은닉층에서 벡터의 평균을 구하는 부분은 CBOW가 Skip-Gram과 다른 차이점이기도 하다. 뒤에서 보게 되겠지만, Skip-Gram은 입력이 중심 단어 하나이기 때문에 은닉층에서 벡터의 평균을 구하지 않는다.
이렇게 구해진 평균 벡터는 두번째 가중치 행렬인 W’와 곱해지게 된다. 이렇게 되면 크기가 V와 동일한 벡터. 즉, 인풋이었던 원-핫 벡터들과 차원이 동일한 벡터가 나오게 된다. 만약 입력 벡터의 차원이 7이었다면 여기서 나오는 벡터도 마찬가지다.
이 벡터에 CBOW는 소프트맥스(softmax) 함수를 취하는데, 소프트맥스 함수로 인한 출력값은 0과 1 사이의 실수로, 각 원소의 총 합은 1이 되는 상태로 바꾼다. 이렇게 나온 벡터를 스코어 벡터(score vector)라고 한다. 스코어 벡터의 각 차원 안에서의 값이 의미하는 것은 이와 같다.
스코어 벡터의 j번째 인덱스가 가진 0과 1 사이의 값은 j번째 단어가 중심 단어일 확률을 나타낸다. 그리고 이 스코어 벡터는 우리가 실제로 값을 알고 있는 벡터인 중심 단어 원-핫 벡터의 값에 가까워져야한다. 스코어 벡터를 y-hat 이라고 한다. 중심 단어를 y로 했을 때, 이 두 벡터가 가까워지게 하기 위해서 CBOW는 cross-entropy 함수를 사용한다. 즉, 다른 말로 loss function으로 cross-entropy 함수를 사용하는 것이다.
cross-entropy 함수에 실제 중심 단어인 원-핫 벡터와 스코어 벡터를 입력값으로 넣고, 이를 식으로 표현하면 위와 같다.
그런데 y가 원-핫 벡터라는 점을 고려하면, 이 식은 위와 같이 간소화시킬 수 있다. 이 식이 왜 loss function으로 적합하지 알아보자. c를 중심 단어에서 1을 1을 가진 차원의 값의 인덱스라고 한다면, y-hat_c = 1은 y-hat이 y를 정확하게 예측한 경우가 된다. 이를 식에 대입해보면 -1log(1) = 0이 되기 때문에, 결과적으로 y-hat이 y를 정확하게 예측한 경우의 cross-entropy의 값은 0이 된다.
즉, 위의 식의 값을 최소화하는 방향으로 학습해야 한다.
이제 역전파(Back Propagation)를 수행하면 W와 W’이 학습 되는데, 학습이 다 되었다면 N차원의 크기를 갖는 W의 행이나 W’의 열로부터 어떤 것을 임베딩 벡터로 사용할지를 결정하면 된다. 때로는 W와 W’의 평균치를 가지고 임베딩 벡터를 선택하기도 한다.
4. Skip-gram
Skip-gram은 CBOW를 이해했다면, 메커니즘 자체는 동일하기 때문에 쉽게 이해할 수 있다. 앞서 CBOW에서는 주변 단어를 통해 중심 단어를 예측했다면, Skip-gram은 중심 단어에서 주변 단어를 예측하려고 한다.
앞서 언급한 동일한 예문에 대해서 인공 신경망을 도식화해보면 위와 같다. 이제 중심 단어에 대해서 주변 단어를 예측하기 때문에, 은닉층에서 벡터들의 평균을 구하는 과정은 없다.
여러 논문에서 성능 비교를 진행했을 때, 전반적으로 Skip-gram이 CBOW보다 성능이 좋다고 알려져있다.
5. 네거티브 샘플링(Negative Sampling)
대체적으로 요즘에 Word2Vec을 사용한다고 하면 SGNS(Skip-Gram with Negative Sampling)을 사용하는 것이 보통이다. Skip-gram을 사용하는데, 네거티브 샘플링(Negative Sampling)이란 방법까지 추가로 사용한다는 것이다. Skip-gram을 전제로 네거티브 샘플링에 대해서 알아보자.
위에서 배운 Word2Vec 모델에는 한 가지 문제점이 있다. 바로 속도이다. Word2Vec의 마지막 단계를 주목해보자. 출력층에 있는 소프트맥스 함수는 단어 집합 크기의 벡터 내의 모든 값을 0과 1 사이의 값이면서 모두 더하면 1이 되도록 바꾸는 작업을 수행한다. 그리고 이에 대한 오차를 구하고 모든 단어에 대한 임베딩을 조정한다. 그 단어가 중심 단어나 주변 단어와 전혀 상관없는 단어라도 마찬가지다. 그런데 만약 단어 집합의 크기가 수백만에 달한다면 이 작업은 굉장히 무거운 작업이다.
여기서 중요한 건 Word2Vec이 모든 단어 집합에 대해서 소프트맥스 함수를 수행하고, 역전파를 수행하므로 주변 단어와 상관 없는 모든 단어까지의 워드 임베딩 조정 작업을 수행한다는 것이다. 만약 마지막 단계에서 ‘강아지’와 ‘고양이’와 같은 단어에 집중하고 있다면, Word2Vec은 사실 ‘돈가스’나 ‘컴퓨터’와 같은 연관 관계가 없는 수많은 단어의 임베딩을 조정할 필요가 없다.
이를 조금 더 효율적으로 할 수 있는 방법이 없을까? 전체 단어 집합이 아니라 일부 단어 집합에 대해서만 고려하면 안 될까? 이렇게 일부 단어 집합을 만들어보자. ‘강아지’, ‘고양이’, ‘애교’와 같은 주변 단어들을 가져온다. 그리고 여기에 ‘돈가스’, ‘컴퓨터’, ‘회의실’과 같은 랜덤으로 선택된 주변 단어가 아닌 상관없는 단어들을 일부만 갖고 온다. 이렇게 전체 단어 집합보다 훨씬 작은 단어 집합을 만들어놓고 마지막 단계를 이진 분류 문제로 바꿔버리는 것이다. 즉, Word2Vec은 주변 단어들을 긍정(positive)으로 두고 랜덤으로 샘플링 된 단어들을 부정(negative)으로 둔 다음에 이진 분류 문제를 수행한다.
이는 기존의 다중 클래스 분류 문제를 이진 분류 문제로 바꾸면서도 연산량에 있어 훨씨 효율적이다.
6. 영어 Word2Vec 만들기
이번에는 영어 데이터를 다운로드 받아 직접 Word2Vec 작업을 진행해보도록 한다. 파이썬의 gensim 패키지에는 Word2Vec을 지원하고 있어, gensim 패키지를 이용하면 손쉽게 단어를 임베딩 벡터로 변환시킬 수 있다.
코퍼스 파일 다운로드 : https://wit3.fbk.eu/get.php?path=XML_releases/xml/ted_en-20160408.zip&filename=ted_en-20160408.zip
해당 파일을 열어보면 xml 문법으로 작성되어 있어 자연어를 얻기 위해서는 전처리가 필요함을 알 수 있다. 우리가 얻고자 하는 데이터는 영어 문장으로만 구성된 내용을 담고 있는 <content>와 </content> 사이의 내용이다. 데이터 전처리 작업을 통해 다른 xml 문법들은 제외시키고, 해당 내용만 가져와야 한다. 뿐만 아니라, <content>와 </content> 사이의 내용 중에는 (Laughter)나 (Applause)와 같은 배경음을 나타내는 단어도 등장하는데 이 또한 제거해야 한다.
해당 전처리의 데이터를 위한 코드는 다음과 같다.
import re
from lxml import etree
import nltk
from nltk.tokenize import word_tokenize, sent_tokenizetargetXML = open('./ted_en-20160408.xml', 'r', encoding='UTF8')
target_text = etree.parse(targetXML)parse_text = '\n'.join(target_text.xpath('//content/text()'))
# xml 파일로부터 <content>와 </content> 사이의 내용만 가져온다.content_text = re.sub(r'\([^)]*\)', '', parse_text)
# 정규 표현식의 sub 모듈을 통해 content 중간에 등장하는 (Audio), (Laughter) 등의 배경음 부분을 제거.sent_text = sent_tokenize(content_text)
# 입력 코퍼스에 대해서 NLTK를 이용하여 문장 토큰화를 수행한다.normalized_text = []
for string in sent_text:
tokens = re.sub(r"[^a-z0-9]+", " ", string.lower())
normalized_text.append(tokens)result = []result = [word_tokenize(sentence) for sentence in normalized_text]print(result[:10])
[['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'], ['consider', 'facit'], ['i', 'm', 'actually', 'old', 'enough', 'to', 'remember', 'them'], ['facit', 'was', 'a', 'fantastic', 'company'], ['they', 'were', 'born', 'deep', 'in', 'the', 'swedish', 'forest', 'and', 'they', 'made', 'the', 'best', 'mechanical', 'calculators', 'in', 'the', 'world'], ['everybody', 'used', 'them'], ['and', 'what', 'did', 'facit', 'do', 'when', 'the', 'electronic', 'calculator', 'came', 'along'], ['they', 'continued', 'doing', 'exactly', 'the', 'same']]
### 해당 데이터를 가지고 Word2Vec 수행from gensim.models import Word2Vec
model = Word2Vec(sentences=result, size=100, window=5, min_count=5, workers=4, sg=0)
여기서 Word2Vec의 하이퍼파라미터는 다음과 같다.
- size = 워드 벡터의 특징 값. 즉, 임베딩 된 벡터의 차원.
- window = 컨텍스트 윈도우 크기
- min_count = 단어 최소 빈도 수 제한 (빈도가 적은 단어들은 학습하지 않는다.)
- workers = 학습을 위한 프로세스 수
- sg = 0는 CBOW, 1은 Skip-gram.
# 입력한 단어에 대해서 가장 유사한 단어들을 출력하기a = model.wv.most_similar("man")
a
[('woman', 0.8369926810264587),
('guy', 0.8076319694519043),
('boy', 0.7761013507843018),
('lady', 0.7671903371810913),
('gentleman', 0.7483106851577759),
('girl', 0.7448023557662964),
('soldier', 0.7427793741226196),
('kid', 0.7089123129844666),
('poet', 0.6792005896568298),
('surgeon', 0.6496701240539551)]