정리정돈

텍스트 전처리(정수 인코딩(Integer Encoding)) 본문

AI/자연어처리

텍스트 전처리(정수 인코딩(Integer Encoding))

XZXXZX 2021. 7. 8. 15:32
728x90
반응형

컴퓨터는 텍스트를 처리하는 것 보다 숫자를 잘 처리하기 떄문에 자연어 처리에서는 텍스틀 숫자로 바꾸는 다양한 기법이 있다. 그러한 기법들을 본격적으로 적용시키기 위한 첫 단계로 각 단어를 고유한 정수에 맵핑(mapping)시키는 전처리 작업이 필요할 때가 있다.

 

예시로 갖고 있는 텍스트에 단어가 5000개가 있다면, 5,000개의 단어들 각각에 1번부터 5,000번까지 단어와 맵핑되는 고유한 정수, 즉 인덱스를 부여한다. 가령 book은 150번, dog은 171번, love는 192번, books는 212번과 같이 숫자를 부여하게된다. 인덱스를 부여하는 방법은 여러가지가 있을 수 있다. 랜덤으로 부여하기도 하지만, 보통은 전처리 또는 빈도수가 높은 단어들만 사용하기 위해서 단어에 대한 빈도수를 기준으로 정렬한 뒤에 부여한다.

 

1. 정수 인코딩(Integer Encoding)

단어에 정수를 부여하는 방법 중 하나로 빈도수 순으로 정렬한 단어 집합(vocabulary)을 만들고, 빈도수가 높은 순서대로 차례로 낮은 숫자부터 정수를 부여하는 방법이 있다.

 

1) dictionary 사용하기

 

from nltk.tokenize import sent_tokenize
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
text = "A barber is a person. a barber is good person. a barber is huge person. he Knew A Secret! The Secret He Kept is huge secret. Huge secret. His barber kept his word. a barber kept his word. His barber kept his secret. But keeping and keeping such a huge secret to himself was driving the barber crazy. the barber went up a huge mountain."
# 문장 토큰화
text = sent_tokenize(text)
print(text)
['A barber is a person.', 'a barber is good person.', 'a barber is huge person.', 'he Knew A Secret!', 'The Secret He Kept is huge secret.', 'Huge secret.', 'His barber kept his word.', 'a barber kept his word.', 'His barber kept his secret.', 'But keeping and keeping such a huge secret to himself was driving the barber crazy.', 'the barber went up a huge mountain.']
# 정제와 단어 토큰화
vocab = {} # 파이썬의 dictionary 자료형
sentences = []
stop_words = set(stopwords.words('english')) # 불용어

for i in text:

    sentence = word_tokenize(i) # 단어 토큰화를 수행
    result = []
    for word in sentence:
        word = word.lower() # 모든 단어를 소문자화하여 단어의 개수를 줄인다.
        if word not in stop_words: # 단어 토큰화된 결과에 대해서 불용어를 제거한다.
            if len(word) > 2:
                result.append(word)
                if word not in vocab:
                    
                    vocab[word] = 0
                    
                vocab[word] += 1
    sentences.append(result)
print(sentences)
[['barber', 'person'], ['barber', 'good', 'person'], ['barber', 'huge', 'person'], ['knew', 'secret'], ['secret', 'kept', 'huge', 'secret'], ['huge', 'secret'], ['barber', 'kept', 'word'], ['barber', 'kept', 'word'], ['barber', 'kept', 'secret'], ['keeping', 'keeping', 'huge', 'secret', 'driving', 'barber', 'crazy'], ['barber', 'went', 'huge', 'mountain']]
print(vocab) # 중복을 제거한 단어와 각 단어에 대한 빈도수
{'barber': 8, 'person': 3, 'good': 1, 'huge': 5, 'knew': 1, 'secret': 6, 'kept': 4, 'word': 2, 'keeping': 2, 'driving': 1, 'crazy': 1, 'went': 1, 'mountain': 1}
vocab_sorted = sorted(vocab.items(), key = lambda x:x[1], reverse = True) # 빈도수가 높은 순으로 정렬
print(vocab_sorted)
[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3), ('word', 2), ('keeping', 2), ('good', 1), ('knew', 1), ('driving', 1), ('crazy', 1), ('went', 1), ('mountain', 1)]
# 높은 빈도수를 가진 단어에게 낮은 정수 인덱스 부여
word_to_index = {}
i=0
for (word, frequency) in vocab_sorted :
    if frequency > 1 : # 정제(Cleaning) 챕터에서 언급했듯이 빈도수가 적은 단어는 제외한다.
        i=i+1
        word_to_index[word] = i
print(word_to_index)
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5, 'word': 6, 'keeping': 7}
vocab_size = 5
words_frequency = [w for w,c in word_to_index.items() if c >= vocab_size + 1] # 인덱스가 5 초과인 단어 제거
for w in words_frequency:
    del word_to_index[w] # 해당 단어에 대한 인덱스 정보를 삭제
print(word_to_index)
# 단어집합에 존재하지 않는 단어(빈도수가 높은 상위 단어가 아닌단어)들을 OOV(Out-Of-Vocabulary)로 지칭하기로 하며 word_to_index에 추가해준다.
word_to_index['OOV'] = len(word_to_index) + 1
encoded = [] # 정수 인코딩이 된 걸 넣어주긴 위한 빈배열
for s in sentences:
    temp = []
    for w in s:
        try:
            temp.append(word_to_index[w]) # 단어 사전에 있는 단어라면 해당 단어의 인덱스로 temp배열에 넣어줌
        except KeyError:
            temp.append(word_to_index['OOV']) # 단어 사전에 없기 떄문에 KeyError발생할 시 OOV의 인덱스를 넣어줌
    encoded.append(temp)
print(encoded)
# 정수인코딩 결과
[[1, 5], [1, 6, 5], [1, 3, 5], [6, 2], [2, 4, 3, 2], [3, 2], [1, 4, 6], [1, 4, 6], [1, 4, 2], [6, 6, 3, 2, 6, 1, 6], [1, 6, 3, 6]]

2) Counter

from collections import Counter
print(sentences) # 현재 단어 토큰화가 된 결과가 현재 이중배열의 형태로 저장되어있다.
[['barber', 'person'], ['barber', 'good', 'person'], ['barber', 'huge', 'person'], ['knew', 'secret'], ['secret', 'kept', 'huge', 'secret'], ['huge', 'secret'], ['barber', 'kept', 'word'], ['barber', 'kept', 'word'], ['barber', 'kept', 'secret'], ['keeping', 'keeping', 'huge', 'secret', 'driving', 'barber', 'crazy'], ['barber', 'went', 'huge', 'mountain']]
# 이중배열을 풀어 단어들을 하나의 리스트로 저장하기 위한 작업
words = sum(sentences, [])
# 위의 작업은 words = np.hstack(sentences)로도 수행 가능하다
print(words)
['barber', 'person', 'barber', 'good', 'person', 'barber', 'huge', 'person', 'knew', 'secret', 'secret', 'kept', 'huge', 'secret', 'huge', 'secret', 'barber', 'kept', 'word', 'barber', 'kept', 'word', 'barber', 'kept', 'secret', 'keeping', 'keeping', 'huge', 'secret', 'driving', 'barber', 'crazy', 'barber', 'went', 'huge', 'mountain']
vocab = Counter(words) # Counter를 사용하여 빈도수 체크
print(vocab)
Counter({'barber': 8, 'secret': 6, 'huge': 5, 'kept': 4, 'person': 3, 'word': 2, 'keeping': 2, 'good': 1, 'knew': 1, 'driving': 1, 'crazy': 1, 'went': 1, 'mountain': 1})
vocab_size = 5
vocab = vocab.most_common(vocab_size) # 등장 빈도수가 높은 상위 5개의 단어만 저장 ()안에 숫자 넣어도 됨
vocab
[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3)]
# dictionary를 사용할때와 같이 높은빈도수를 가질 수록 낮은 인덱스 부여
word_to_index = {}
i = 0
for (word, frequency) in vocab :
    i = i+1
    word_to_index[word] = i
print(word_to_index)
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5}

3) NLTK의 FreqDist 사용

from nltk import FreqDist
import numpy as np

# np.hstack으로 문장 구분을 제거하여 입력으로 사용
vocab = FreqDist(np.hstack(sentences))
print(vocab)
print(vocab["barber"], ':barber 라는 단어의 빈도수 출력')
<FreqDist with 13 samples and 36 outcomes>
8 :barber 라는 단어의 빈도수 출력

4) enumerate

enumerate()는 순서가 있는 자료형(list, set, tuple, dictionary, string)을 입력으로 받아 인덱스를 순차적으로 함께 리턴한다는 특징이 있다.

test=['a', 'b', 'c', 'd', 'e']
for index, value in enumerate(test): # 입력의 순서대로 0부터 인덱스를 부여함.
  print("value : {}, index: {}".format(value, index))
value : a, index: 0
value : b, index: 1
value : c, index: 2
value : d, index: 3
value : e, index: 4

2. 케라스(Keras)의 텍스트 전처리

케라스(Keras)는 기본적인 전처리 도구를 제공한다. 정수 인코딩을 진행 하기 위해서 케라스의 전처리 도구인 토크나이저를 사용하기도 한다.

from tensorflow.keras.preprocessing.text import Tokenizer
sentences=[['barber', 'person'], ['barber', 'good', 'person'], ['barber', 'huge', 'person'], ['knew', 'secret'], ['secret', 'kept', 'huge', 'secret'], ['huge', 'secret'], ['barber', 'kept', 'word'], ['barber', 'kept', 'word'], ['barber', 'kept', 'secret'], ['keeping', 'keeping', 'huge', 'secret', 'driving', 'barber', 'crazy'], ['barber', 'went', 'huge', 'mountain']]
# fit_on_texts는 입력한 텍스트로부터 단어 빈도수가 높은 순으로 낮은 정수 인덱스를 부여한다.
tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentences)# fit_on_texts()안에 코퍼스를 입력으로 하면 빈도수를 기준으로 단어 집합을 생성
# 각 단어에 대한 인덱스가 어떻게 부여되었는지 보려면 word_index를 사용한다.
print(tokenizer.word_index)
# 각 단어가 몇개였느지를 보고자 한다면 word_counts를 사용한다.
print(tokenizer.word_counts)
# texts_to_sequences()는 입력으로 들어온 코퍼스에 대해서 각 단어를 이미 정해진 인덱스로 반환한다.
print(tokenizer.texts_to_sequences(sentences))
# word_index
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5, 'word': 6, 'keeping': 7, 'good': 8, 'knew': 9, 'driving': 10, 'crazy': 11, 'went': 12, 'mountain': 13}
# word_counts
OrderedDict([('barber', 8), ('person', 3), ('good', 1), ('huge', 5), ('knew', 1), ('secret', 6), ('kept', 4), ('word', 2), ('keeping', 2), ('driving', 1), ('crazy', 1), ('went', 1), ('mountain', 1)])
# texts_to_sequences(sentences)
[[1, 5], [1, 8, 5], [1, 3, 5], [9, 2], [2, 4, 3, 2], [3, 2], [1, 4, 6], [1, 4, 6], [1, 4, 2], [7, 7, 3, 2, 10, 1, 11], [1, 12, 3, 13]]

위에는 같이 빈도수가 가장 높은 단어 n개만을 사용하기 위해서 most_common() 을 사용했었다.(Counter) 케라스 토크나이저에서는 tokenizer = Tokenizer(num_words = 숫자)와 같은 방식으로 빈도수가 높은 상위 몇 개의 단어만 사용하겠다고 지정할 수 있다. 

vocab_size = 5
tokenizer = Tokenizer(num_words = vocab_size + 1) # 상위 5개 단어만 사용
tokenizer.fit_on_texts(sentences)

num_words에서 +1을 더해주는 것은 num_words는 숫자를 0부터 카운트하기 때무이다. 만약 5를 넣게 된다면 0~4번 단어를 보존하게 된다.

실질적으로 숫자 0에 지정된 단어가 존재하지 않지만 케라스의 토크나이저가 숫자 0까지 단어집합크기를 산정하는 이유는 자연어 처리에서 패딩(padding)이라는 작업때문이다.

print(tokenizer.word_index) # 상위 5개 단어로만 하려고 했으나 여전히 13개의 단어가 출력됨
print(tokenizer.word_counts) # 마찬가지로 13개의 단어가 모두 출력됨
print(tokenizer.texts_to_sequences(sentences)) # 실제 적용은 texts_to_sequences를 사용할 때 적용됨
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5, 'word': 6, 'keeping': 7, 'good': 8, 'knew': 9, 'driving': 10, 'crazy': 11, 'went': 12, 'mountain': 13}
OrderedDict([('barber', 8), ('person', 3), ('good', 1), ('huge', 5), ('knew', 1), ('secret', 6), ('kept', 4), ('word', 2), ('keeping', 2), ('driving', 1), ('crazy', 1), ('went', 1), ('mountain', 1)])
[[1, 5], [1, 5], [1, 3, 5], [2], [2, 4, 3, 2], [3, 2], [1, 4], [1, 4], [1, 4, 2], [3, 2, 1], [1, 3]]

코퍼스에서 각다언를 이미 정해진 인덱스로 반환하는데 상위 5개의 단어만 사용하겠다고 하였기 때문에 1번 ~5번 단어까지만 산정되고 나머지 단어들은 제거되었다. word_index와 word_counts에서도 지정된 num_words만큼의 단어만 남기려면

tokenizer = Tokenizer() # num_words를 여기서는 지정하지 않은 상태
tokenizer.fit_on_texts(sentences)
vocab_size = 5
words_frequency = [w for w,c in tokenizer.word_index.items() if c >= vocab_size + 1] # 인덱스가 5 초과인 단어 제거
for w in words_frequency:
    del tokenizer.word_index[w] # 해당 단어에 대한 인덱스 정보를 삭제
    del tokenizer.word_counts[w] # 해당 단어에 대한 카운트 정보를 삭제
print(tokenizer.word_index)
print(tokenizer.word_counts)
print(tokenizer.texts_to_sequences(sentences))
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5}
OrderedDict([('barber', 8), ('person', 3), ('huge', 5), ('secret', 6), ('kept', 4)])
[[1, 5], [1, 5], [1, 3, 5], [2], [2, 4, 3, 2], [3, 2], [1, 4], [1, 4], [1, 4, 2], [3, 2, 1], [1, 3]]

케라스 토크나이저는 기본적으로 단어 집합에 없는 단어인 OOV에 대해서 단어를 정수로 바꾸는 과정에서 아예 단어를 제거한다. 단어 집합에 없는 단어들은 OOV로 간주하여 보존하고 싶다면 Tokenizer의 인자 oov_token을 사용하면 된다.

vocab_size = 5
tokenizer = Tokenizer(num_words = vocab_size + 2, oov_token = 'OOV')
# 빈도수 상위 5개 단어만 사용. 숫자 0과 OOV를 고려해서 단어 집합의 크기는 +2
tokenizer.fit_on_texts(sentences)

만약 oov_token을 사용하려고 했다면 케라스 토크나이저는 기본적으로 'OOV'의 인덱스를 1로 한다.

print('단어 OOV의 인덱스 : {}'.format(tokenizer.word_index['OOV']))
단어 OOV의 인덱스 : 1

코퍼스에 대해서 정수인코딩을 진행하면

print(tokenizer.texts_to_sequences(sentences))
[[2, 6], [2, 1, 6], [2, 4, 6], [1, 3], [3, 5, 4, 3], [4, 3], [2, 5, 1], [2, 5, 1], [2, 5, 3], [1, 1, 4, 3, 1, 2, 1], [2, 1, 4, 1]]

상위 단어들에 대해서는 2~6으로 인덱싱 되며 그외 단어 집합에 없는 단어들은 전부 OOV의 인덱스인 1로 인코딩 된다.

728x90
반응형