텍스트는 단어의 시퀀스나 문자의 시퀀스로 이해할 수 있고 따라서 가장 흔한 시퀀스 형태의 데이터다.
시퀀스를 처리하는 딥러닝 모델로 문서 분류, 감성 분석, 저자 식별, 질문 응답 등 기본적인 자연어 이해 문제를 해결할 수 있다. 물론 이러한 딥러닝 모델이 사람처럼 진짜 텍스트를 이해하는 것은 아니고 문자 언어에 대한 통계적 구조를 만들어 간단한 텍스트 문제를 해결할 수 있는 것이다.
컴퓨터 비전이 픽셀에 적용한 패턴 인식인 것처럼 자연어 처리를 위한 딥러닝은 단어, 문장, 문단에 적용한 패턴 인식이다.
다른 모든 신경망과 마찬가지로 원본 텍스트 데이터를 입력으로 사용할 수 없다. 앞서 말했듯이 모델이 텍스트 자체를 이해할 수 없기 때문이다. 딥러닝 모델은 수치형 텐서만 다룰 수 있다.
텍스트를 수치형 텐서로 변환하는 과정을 텍스트 벡터화라고 한다. 아래와 같은 여러 가지 방식으로 텍스트를 벡터화할 수 있다.
텍스트를 단어, 문자 또는 n-gram으로 나누는 단위를 토큰(token)이라고 한다. 텍스트를 토큰으로 나누는 작업을 토큰화(tokenization)라고 한다. 모든 텍스트 벡터화 과정은 토큰화를 적용하여 생성된 토큰에 수치형 벡터를 연결하는 것으로 이루어진다. 이러한 벡터는 시퀀스 텐서로 묶여 심층 신경망에 주입된다. 토큰과 벡터를 연결하는 방법은 여러 가지가 있다. 이번 포스팅에서는 원-핫 인코딩과 토큰 임베딩(워드 임베딩)을 다뤄보겠다.
원-핫 인코딩은 토큰을 벡터로 변환하는 가장 일반적이고 방법이다. 모든 단어에 고유한 정수 인덱스를 부여하고 이 정수 인덱스 i를 크기가 n(어휘 사전 크기)인 이진 벡터로 변환한다. 이 벡터는 i번째 원소만 1이고 나머지는 모두 0이다.
import numpy as np
samples = ["AlphaGo has intelligized through deep learning.", "Recurrent neural networks are a kind of deep learning algorithm."]
# 데이터에 있는 모든 토큰의 인덱스 구축
token_index = {}
for sample in samples:
for word in sample.split(): # 단어 토큰화
if word not in token_index:
token_index[word] = len(token_index) + 1 # 단어마다 고유 인덱스 할당, 인덱스 0은 사용하지 않음.
max_length = 10 # 각 샘플(여기서는 한 문장)에서 max_length까지의 단어만 사용
results = np.zeros(shape=(len(samples),
max_length,
max(token_index.values()) + 1)) # 결과를 저장할 넘파이 배열. max_length x max(token_index.values() + 1) 배열이 len(samples)개 있는 3차원 배열
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = token_index.get(word)
results[i, j, index] = 1.
print(results)
샘플 문장 데이터를 단어로 토큰화하고 넘파이 배열로 벡터화한 코드이다. 벡터화 결과는 다음과 같다.
[[[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
[[0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]]]
Keras에서 Tokenizer 객체를 생성하여 토큰화 및 다양한 기능을 사용할 수 있다. 다음은 Tokenizer를 이용하여 원-핫 인코딩을 구현한 코드이다.
from keras.preprocessing.text import Tokenizer
samples = ["AlphaGo has intelligized through deep learning.", "Recurrent neural networks are a kind of deep learning algorithm."]
tokenizer = Tokenizer(num_words=1000) # 가장 빈도가 높은 1000개의 단어만 선택하도록 Tokenizer 객체 생성
tokenizer.fit_on_texts(samples) # 단어 인덱스 구축
sequences = tokenizer.texts_to_sequences(samples) # 문자열을 정수 인덱스 리스트로 변환
one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary') # 원-핫 이진 벡터
word_index = tokenizer.word_index # 계산된 단어 인덱스 구하기
word_index
Tokenizer를 이용하여 단어 인덱스를 구하면 다음과 같은 결과가 나온다.
{'deep': 1,
'learning': 2,
'alphago': 3,
'has': 4,
'intelligized': 5,
'through': 6,
'recurrent': 7,
'neural': 8,
'networks': 9,
'are': 10,
'a': 11,
'kind': 12,
'of': 13,
'algorithm': 14}
반면에 첫 코드처럼 딕셔너리를 만들어 토큰 인덱스를 일일이 구했을 때의 단어 인덱스는 다음과 같다.
{'AlphaGo': 1,
'has': 2,
'intelligized': 3,
'through': 4,
'deep': 5,
'learning.': 6,
'Recurrent': 7,
'neural': 8,
'networks': 9,
'are': 10,
'a': 11,
'kind': 12,
'of': 13,
'learning': 14,
'algorithm.': 15}
차이점이 있다. Tokenizer를 이용했을 땐 특수 문자를 제거하는 기능이 자동으로 실행되어 "learning."과 "learning"을 따로 구분하지 않았지만 Tokenizer를 이용하지 않았을 때는 "learning."과 "learning"이 다른 단어로 구분되었다.
Tokenizer를 이용하면 특수 문자를 제거하거나 빈도가 높은 n개의 단어만 선택하는 등 여러 가지 중요한 기능이 있다. 따라서 원-핫 인코딩을 이용하여 토큰화 및 벡터화를 할 땐 Tokenizer 유틸리티를 적극적으로 사용하자.
원-핫 인코딩의 변종 중 하나는 원-핫 해싱 기법이다. 이 방식은 어휘 사전에 있는 고유한 토큰 수가 너무 커서 모두 다루기 어려울 때 사용한다. 각 단어에 명시적으로 인덱스를 할당하고 이 인덱스를 딕셔너리에 저장하는 대신 단어를 해싱하여 고정된 크기의 벡터로 변환한다.
간단한 해싱 함수를 사용하여 원-핫 해싱을 구현할 수 있다. 원-핫 해싱의 주요 장점은 명시적인 단어 인덱스가 필요 없으므로 메모리를 절약하고 온라인 방식으로 데이터를 인코딩할 수 있다는 점으로 이는 전체 데이터를 확인하지 않고도 토큰을 생성할 수 있다는 것이다. 단점은 해시 충돌이다. 2개의 단어가 같은 해시를 만들면 이를 바라보는 머신 러닝 모델은 단어 사이의 차이를 인식하지 못한다. 해싱 공간의 차원이 해싱될 고유 토큰의 전체 개수보다 훨씬 크다면 해시 충돌 가능성은 감소한다.
import numpy as np
samples = ["AlphaGo has intelligized through deep learning.", "Recurrent neural networks are a kind of deep learning algorithm."]
dimensionality = 1000 # 크기가 1000인 벡터로 단어를 저장. 1000개 이상의 단어가 있다면 해싱 충돌이 늘어나고 인코딩의 정확도가 감소
max_len = 10 # 샘플에서 max_len까지의 단어만 사용
results = np.zeros((len(samples), max_len, dimensionality)) # 해싱 결과를 저장할 넘파이 배열
for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_len]:
print("word: ", word)
hash_word = hash(word)
print("hash(word): ", hash_word)
abs_hash_word = abs(hash_word)
print("abs(hash(word)): ", abs_hash_word)
index = abs_hash_word % dimensionality
print("index: ", index)
results[i, j, index] = 1.
print(results)
결과는 다음과 같다.
word: AlphaGo
hash(word): -6478401417523475540
abs(hash(word)): 6478401417523475540
index: 540
word: has
hash(word): 3003061812357373790
abs(hash(word)): 3003061812357373790
index: 790
word: intelligized
hash(word): 6108892223904572912
abs(hash(word)): 6108892223904572912
index: 912
word: through
hash(word): -6005411517147695028
abs(hash(word)): 6005411517147695028
index: 28
word: deep
hash(word): -6275315076995335766
abs(hash(word)): 6275315076995335766
index: 766
word: learning.
hash(word): -5153763055654257247
abs(hash(word)): 5153763055654257247
index: 247
word: Recurrent
hash(word): -3137305838437918874
abs(hash(word)): 3137305838437918874
index: 874
word: neural
hash(word): -2291973141328787303
abs(hash(word)): 2291973141328787303
index: 303
word: networks
hash(word): 792144246047159534
abs(hash(word)): 792144246047159534
index: 534
word: are
hash(word): 4811852616200324968
abs(hash(word)): 4811852616200324968
index: 968
word: a
hash(word): -1381678511694179621
abs(hash(word)): 1381678511694179621
index: 621
word: kind
hash(word): -6611088079687422480
abs(hash(word)): 6611088079687422480
index: 480
word: of
hash(word): -8395606047607423480
abs(hash(word)): 8395606047607423480
index: 480
word: deep
hash(word): -6275315076995335766
abs(hash(word)): 6275315076995335766
index: 766
word: learning
hash(word): -7535715173001044899
abs(hash(word)): 7535715173001044899
index: 899
word: algorithm.
hash(word): 6979526981548750387
abs(hash(word)): 6979526981548750387
index: 387
[[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
...
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
[[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
...
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]]
딥러닝 모델은 수치형 텐서를 입력받는다. 따라서 텍스트를 학습하기 위해서는 토큰화 및 벡터화 과정이 필요하다. 원-핫 인코딩은 이러한 과정 중 하나다. 하지만 원-핫 인코딩으로 만든 벡터는 대부분 0으로 채워지고 고차원이다. word_index의 단어 수와 차원이 같고 이 말은 10,000개의 토큰으로 이루어진 word_index라면 10,000차원이나 그 이상의 벡터일 경우가 많다는 것이다. 하지만 워드 임베딩은 밀집 단어 벡터를 사용하여 원-핫 인코딩보다 더 많은 정보를 적은 차원에 저장할 수 있다. 다음 포스팅은 워드 임베딩에 대해서 정리하겠다.