자연어처리(NLP) 6일차 (원-핫 인코딩)

정민수
8 min readJun 8, 2019

--

19.06.08

본 글은 https://wikidocs.net/book/2155을 참고하여 작성되었음을 밝힙니다.

핵심키워드

  • One-hot encoding
  • 단어 집합(Vocabulary)

원-핫 인코딩(One-hot encoding)

원-핫 인코딩(One-hot encoding)은 자연어처리를 위해서 문자를 숫자로 처리하는 기법 중 하나이며, 특히 자연어처리를 위한 머신러닝, 딥러닝에 대한 이해를 위해서는 반드시 필요한 개념이다.

원-핫 인코딩에 대해서 배우기에 앞서 단어 집합(Vocabulary)에 대해서 정의해보도록 한다. 단어 집합은 앞으로 자연어 처리에서 계속 나오는 개념이기 때문에 여기서 이해하고 가야한다. 단어 집합은 서로 다른 단어들의 집합이다. 여기서 혼동이 없도록 서로 다른 언어라는 정의에 대해서 좀 더 주목할 필요가 있다.

단어 집합(Vocabulary)에서는 기본적으로 book과 books와 같이 단어의 변형 형태도 다른 단어로 간주한다. 앞으로 단어 집합에 있는 단어들을 가지고, 문자를 숫자(더 구체적으로는 벡터)로 바꾸는 원-핫 인코딩을 포함한 여러 방법에 대해서 배우게 된다.

원-핫 인코딩을 위해서 먼저 해야할 일은 갖고 있는 우선 단어 집합을 만드는 일이다. 텍스트의 모든 단어를 중복을 허락하지 않고 모아놓은 것을 단어 집합이라고 한다. 그리고 이 단어 집합에 고유한 숫자를 부여하는 일을 진행한다. 갖고 있는 텍스트에 단어가 5,000개가 등장했다면, 단어 집합의 크기를 5,000이라고 한다. 5,000개의 단어가 있는 이 단어 집합의 단어들마다 0번부터 4,999번까지 인덱스를 부여한다고 해보자. 가령, book은 150번, dog는 171번, love는 192번, books는 212번과 같이 부여된다.

이제 각 단어에 고유한 인덱스를 붙였다면 이 숫자로 바뀐 단어들을 벡터로 다루고 싶다면 어떻게 해야할까?

1. 원-핫 인코딩(One-hot encoding)이란?

원-핫 인코딩은 단어 집합의 크기를 벡터의 차원으로 하고, 표현하고 싶은 단어의 인덱스에 1의 값을 부여하고, 다른 인덱스에는 0을 부여하는 단어의 벡터 표현 방식이다. 이렇게 표현된 벡터를 원-핫 벡터(One-hot vector)라고 한다.

원-핫 인코딩의 과정을 두 가지 과정으로 정리해본다.

(1) 각 단어에 고유한 인덱스를 부여한다. (정수 인코딩)

(2) 표현하고 싶은 단어의 인덱스의 위치에 1을 부여하고, 다른 단어의 인덱스의 위치에는 0을 부여한다.

한국어 문장을 예제로 원-핫 벡터를 만들어본다.

문장 : 나는 자연어 처리를 배운다

from konlpy.tag import Okt
okt = Okt()
token = okt.morphs("나는 자연어 처리를 배운다")
print(token)
['나', '는', '자연어', '처리', '를', '배운다']

코엔엘파이의 Okt 형태소 분석기를 통해서 우선 문장에 대해서 형태소 토큰화를 수행하였다.

word2index={}
for voca in token:
if voca not in word2index.keys():
word2index[voca] = len(word2index)
print(word2index)
{'나': 0, '는': 1, '자연어': 2, '처리': 3, '를': 4, '배운다': 5}

각 토큰에 대해서 고유한 인덱스(index)를 부여했다. 지금은 문장이 짧기 때문에 각 단어의 빈도수를 고려하지 않지만, 빈도수 순대로 단어를 정렬하여 고유한 인덱스를 부여하는 작업이 사용되기도 한다.

def one_hot_encoding(word, word2index):
one_hot_vector = [0]*(len(word2index))
index = word2index[word]
one_hot_vector[index] = 1
return one_hot_vector
one_hot_encoding("자연어", word2index)
[0, 0, 1, 0, 0, 0]

해당 함수에 ‘자연어’라는 토큰을 입력으로 넣어봤더니 [0, 0, 1, 0, 0, 0]라는 벡터가 나왔다. ‘자연어’는 단어 집합에서 인덱스가 2이므로, ‘자연어’를 표현하는 원-핫 벡터는 인덱스 2의 값이 1이며, 나머지 값은 0인 벡터가 나온다.

2. 케라스(Keras)를 이용한 원-핫 인코딩(One-hot encoding)

케라스에서는 자동으로 원-핫 인코딩을 만들어 주는 유용한 도구 to_categorical()을 지원한다. 이번에는 케라스만으로 정수 인코딩과 원-핫 인코딩을 순차적으로 진행하도록 한다.

text="오늘 저녁도 참치먹어야 하나 점심 메뉴도 참치먹었는데 참치는 이제 질려"from keras_preprocessing.text import Tokenizer
t = Tokenizer()
t.fit_on_texts([text])
# 입력으로 [text]가 아닌 text를 넣을 경우 한 글자 단위 인코딩이 되버린다. ex 오:1, 늘:2
print(t.word_index)
{'오늘': 1, '저녁도': 2, '참치먹어야': 3, '하나': 4, '점심': 5, '메뉴도': 6, '참치먹었는데': 7, '참치는': 8, '이제': 9, '질려': 10}

위와 같이 생성된 단어 집합(Vocabulary)에 있는 단어들로만 구성된 텍스트가 있다면, texts_to_sequences()를 통해서 이를 바로 인덱스의 나열로 변환가능하다. 생성된 단어 집합의 단어들로만 구성된 서브 텍스트 text2를 가지고 확인한다.

text2 = "저녁도 참치먹어야 하나"
x = t.texts_to_sequences([text2])
print(x)
[[2, 3, 4]]

이 때 결과가 리스트의 리스트로 나오는 것이 불편하다면, 아래와 같이 수정할 수 있다.

text2 = "저녁도 참치먹어야 하나"
x = t.texts_to_sequences([text2])[0]
print(x)
[2, 3, 4]

지금까지 진행한 것은 이미 정수 인코딩 챕터에서 배운 정수 인코딩 과정이다. 이제 해당 결과를 가지고, 원-핫 인코딩을 진행해본다. 케라스는 정수 인코딩 결과를 입력으로 받아 바로 원-핫 인코딩 과정을 수행하는 to_categoricla()을 지원한다.

vocab_size = len(t.word_index)
# 단어 집합의 크기, 이 경우는 단어의 개수가 10개 이므로 10.
from keras.utils import to_categorical
x = to_categorical(x, num_classes=vocab_size+1)
# 실제 단어 집합의 크기보다 +1로 크기를 만들어야함.
print(x)
[[0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.] # 인덱스 2의 원-핫 벡터
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0.] # 인덱스 3의 원-핫 벡터
[0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]]# 인덱스 4의 원-핫 벡터

여기서 주의할 점은 to_categorical()은 정수 인코딩으로 부여된 인덱스를 그대로 배열의 인덱스로 사용하기 때문에, 실제 단어 집합의 크기보다 +1의 크기를 인자로 주어야 한다는 점이다.

t.fit_on_texts()는 인덱스를 1부터 부여한다. 하지만 배열의 인덱스는 0부터 시작하므로 맨 마지막 인덱스를 가진 단어의 인덱스가 7인데, 이를 원-핫 벡터로 만들기 위해서는 총 8의 크기를 가진 배열이 필요하다.

위의 결과는 “저녁도 참치먹어야 하나” 라는 문장이 [2, 3, 4]로 정수 인코딩 되고 나서, 각각의 인코딩 된 결과를 인덱스로 원-핫 인코딩이 수행된 모습을 보여준다.

3. 원-핫 인코딩(One-hot encoding)의 한계

이러한 표현 방식은 단어의 개수가 늘어날 수록, 벡터를 저장하기 위해 필요한 공간이 계속 늘어난다는 단점이 있다. 다른 말로 벡터의 차원이 계속 늘어난다고도 표현한다. 원-핫 벡터는 단어 집합의 크기가 곧 벡터의 차원 수가 된다. 가령, 단어가 1,000개의 코퍼스를 가지고 원-핫 벡터를 만들면, 모든 단어는 모두 1,000개의 차원을 가진 벡터가 된다. 다시 말해 모든 단어각각은 하나의 값만 1을 가지고, 999개의 값은 0의 값을 가지는 벡터가 되는데, 이는 매우 비효율적인 표현 방법이다.

또한 이런 방식은 단어의 유사성을 전혀 표현하지 못한다는 단점이 있다. 예를 들어서 늑대, 호랑이, 강아지, 고양이라는 4개의 단어에 대해서 원-핫 인코딩을 해서 각각, [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]이라는 원-핫 벡터를 부여받았다고 하자. 이 때 이 원-핫 벡터 표현 방법을 통해서는 강아지와 늑대가 유사하고, 호랑이와 고양이가 유사하다는 것을 표현할 수가 없다. 좀 더 극단적인 예를 들어보면, 원-핫 벡터로 유사성을 표현할 수 없기 때문에, 강아지와 개라는 단어 유사 정도나 강아지와 냉장고라는 뜬금없는 유사 관계나 차이를 알아볼 수가 없다.

단어 간 유사성을 표현, 계산할 수 없다는 단점은 검색 시스템 등에서 심각한 문제다. 가령, 여행을 가려고 웹 검색창에 ‘삿포로 숙소’라는 단어를 검색한다고 하면, 제대로 된 검색 시스템이라면 ‘삿포로 숙소’라는 검색어에 대해서 ‘삿포로 게스트 하우스’, ‘삿포로 료칸’, ‘삿포로 호텔’과 같은 유사 단어에 대한 결과도 함께 보여줄 수 있어야 한다. 하지만 단어간 유사성을 계산할 수 없다면, ‘게스트 하우스’와 ‘료칸’과 ‘호텔’이라는 연관 검색어를 보여줄 수 없다.

이러한 단점을 없앨 수 있는, 단어의 ‘의미’를 다차원 공간에 벡터화 하는 기법으로는 두 가지가 있다. 첫째는 카운트 기반으로 단어의 의미를 벡터화하는 LSA, HAL 등이 있으며, 둘째는 예측 기반으로 단어의 의미를 벡터화하는 전통 NNLM, RNNLM, Word2Vec, FastText 등이 있다. 그리고 카운트 기반과 예측 기반 두 가지 방법을 모두 사용하는 방법으로 Glove라는 방법이 존재한다.

이 책(딥 러닝을 이용한 자연어 처리 입문)에서는 이 중에서 6챕터에서 LSA를 다룰 예정이며, 9챕터에서는 Word2Vec, FastText, Glove를 다룬다.

--

--

정민수
정민수

No responses yet