이번 글에서는 2017년쯤 핫했던(걸로 기억되는) fastText와 그 사용법에 대해서 정리한다.

fastText

fastText의 기원으로 꼽히는 논문은 2016년 7월경에 공개된 Facebook AI Research의 Enriching Word Vectors with Subword Information이며, 완성은 Advances in Pre-Training Distributed Word Representations로 본다.

Word2vec을 제안한 T. Mikolov가 저자로 들어있으며 세줄로 요약하면 다음과 같다.

  • Word embedding (Distributed vector represenatation of words)에는 다양한 방법이 있지만, 대부분의 방법들은 언어의 형태학적(Morpological)인 특성을 반영하지 못하고, 또 희소한 단어에 대해서는 Embedding이 되지 않음
  • 본 연구에서는 단어를 Bag-of-Characters로 보고, 개별 단어가 아닌 n-gram의 Charaters를 Embedding함 (Skip-gram model 사용)
  • 최종적으로 각 단어는 Embedding된 n-gram의 합으로 표현됨, 그 결과 빠르고 좋은 성능을 나타냈음

특히 기존연구의 한계점들에 대해서 설명하자면,

  • “단어의 형태학적 특성을 반영하지 못했다”라는 것은 예를들어, teach와 teacher, teachers 세 단어는 의미적으로 유사한 단어임이 분명하다. 그런데 과거의 Word2Vec이나 Glove등과 같은 방법들은 이러한 단어들을 개별적으로 Embedding하기 때문에 셋의 Vector가 유사하게 구성되지 않는다는 점이다.
  • “희소한 단어를 Embedding하기 어렵다”라는 것은 Word2Vec등과 같은 기존의 방법들은 Distribution hypothesis를 기반으로 학습하는 것이기 때문에, 출현횟수가 많은 단어에 대해서는 잘 Embedding이 되지만, 출현횟수가 적은 단어에 대해서는 제대로 Embedding이 되지 않는다는 점이다. (Machine learning의 용어로 설명하자면, Sample이 적은 단어에 대해서는 Underfitting이 되는 것으로 이해할 수 있겠다.)

또한 부수적으로, n-gram의 Charactors를 Embedding하게 되면 Out-of-Vocabulary(OOV)를 처리할 수 있는 장점도 있다.
기존의 방법들은 단어단위로 어휘집(Vocabulary)를 구성하기 때문에, 어휘집에 없는 새로운 단어가 등장하면 전체를 다시 학습시켜야 했는 반면, 위의 연구에서는 새로운 단어가 등장해도 기존의 n-gram vector를 찾아서(Lookup) Summation하면 재학습과정 없이 대응할 수 있다.

본 글을 읽으시는 분들은 이미 T. Mikolov의 Word2vec, 또 Y. Bengio의 NNLM에 대해 기본적인 개념들은 알고 있다고 가정하여 Word embedding이나 Skip-gram에 대해서는 설명하지 않는다.

Library

현재 Python을 기준으로 fastText를 사용할 수 있는 library는 크게 Facebook에서 자체적으로 공개한것과, Gensim에 포함된 것으로 나뉜다.
어떤것을 사용해도 큰 차이는 없고, 특히 Pre-trained vector의 경우에는 두 라이브러리가 공유하기에 (Facebook을 Gensim에서 호환하는 거지만..) 본인이 편한것을 사용하면 되겠다.
근데, Facebook API는 윈도우를 지원하지 않기 때문에(윈도우를 위해 수정한 것도 있긴 하지만…) 본 글에서는 Gensim을 기준으로 설명한다.

Gensim

Gensim은 “Topic modeling for humans”이라는 비전처럼, 처음에는 LSA, LDA등과 같이 Topic modeling을 지원하는 Python library로 출발했다.
그러나 최근에는 토픽 모델링 뿐만 아니라 Vector representation과 관련한 기능들을 대폭 추가하여 고급 자연어처리 도구로 진화하고 있다.

본 글에서는 Gensim fastText Documentation을 참조하였다.

Self-traning

Gensim을 사용해 본 사람은 알겠지만, Topic modeling을 포함하여 Gensim의 기본 Input data 구조는 List of List를 기본으로 한다 (개별 Component는 Space로 분리).
예를 들어 This is an apple은 [“This”, “is”, “an”, “apple”]과 같은 식으로 분리되며,
This is an apple, This is a pen 과 같이 여러 문장이 있을 경우 [[“This”, “is”, “an”, “apple”], [“This”, “is”,”a”, “pen”]] 과 같은 식으로 처리하여 사용할 수 있다.

간단한 코드는 아래와 같으며, 각 단어의 Vector를 얻어올 때는 model[‘is’]와 같은 식으로 얻을 수 있다. 또한 OOV단어에 대해서도 Vector를 얻어올 수 있는 것을 볼 수 있다.
다만, OOV단어가 기존에 학습된 n-gram으로 구성이 불가할 때에는 에러가 발생하는 것을 볼 수 있으나, 현실에서는 수 많은 문장을 거쳐 학습하게 되면 가능한 대부분 경우의수에 해당하는 n-gram이 학습되기 때문에 에러가 발생하는 일은 많지 않을 것이다.

from gensim.models import FastText
sentences = [["This", "is", "an", "apple"], ["This", "is","a", "pen"]]

model = FastText(sentences, min_count=1)
is_vector = model['is']
apples_vector = model['apples']

Pre-trained vector

위와 같이 직접 학습을 통해 모형을 구성할 수도 있지만, fastText의 꽃은 뭐니뭐니해도 Pre-trained vector를 사용하는 것이라고 생각한다.
뭐, 일단 학습에 사용되는 데이터도 데이터지만…내가한것보다 Facebook이 한게 더 믿음직스럽잖아…?

각설하고, fastText는 영어뿐만 아니라 294개 언어에 대해 Wikipedia를 통해 학습한 결과를 제공한다.
https://fasttext.cc/docs/en/pretrained-vectors.html 에서 확인할 수 있으며, 이쪽 결과물로는 보기드물게 한국어도 있다!

binary(.bin 확장자)와 text(.vec 확장자)를 제공하며, 본 글에서는 .bin을 사용한다.
해당 파이썬 코드와 같은 경로에 wiki.en.bin (약 7.91GB)가 위치한다고 가정하고 아래와 같은 코드를 실행시키면 된다.

from gensim.models import FastText

model = FastText.load_fasttext_format('./wiki.en')

print(model.most_similar('teacher'))
# Output = [('headteacher', 0.8075869083404541), ('schoolteacher', 0.7955552339553833), ('teachers', 0.733420729637146), ('teaches', 0.6839243173599243), ('meacher', 0.6825737357139587), ('teach', 0.6285147070884705), ('taught', 0.6244685649871826), ('teaching', 0.6199781894683838), ('schoolmaster', 0.6037642955780029), ('lessons', 0.5812176465988159)]
print(model.similarity('teacher', 'teaches'))
# Output = 0.683924396754

Document representation with fastText

fastText의 원리를 이해하면 Word representation 뿐만 아니라, Document represenatation도 얻을 수 있겠다고 생각할 수 있다.
관련해서 당연히 토론도 있었고, Facebook의 구현체를 참고하면 (fastText/src/fasttext.cc) getSentenceVector라는 Method에서 같은 방식으로 Sentence vector를 계산하는 것을 알 수 있다.

그러면 이렇게 하면 되겠지

from gensim.models import FastText

model = FastText.load_fasttext_format('./wiki.en')
sentence_vector = model['This is a pen']