본문 바로가기
Study/NLP

01. 텍스트 전처리 - 정수 인코딩

by GodKim 2020. 3. 13.

 

www.wikidocs.net/book/2155 의 [딥 러닝을 이용한 자연어 처리 입문]을 공부하고 정리한 글입니다.


정수 인코딩(Integer Encoding)

 

 컴퓨터는 직접적인 텍스트 입력보다는 숫자를 더 잘 처리할 수 있다고 한다. 이러하기에 NLP분야에선 텍스트를 숫자로 바꾸는 여러가지 기법이 존재한다. 가장 첫 단계가 각 단어에 고유한 정수를 부여하는 것이다. 학교에서 우리 이름과 함께 주어지는 학번이나, 군대에서의 군번과 비슷하다고 생각하면 된다. 예컨데, 보유하고 있는 코퍼스에 단어가 1000개 있다면 각 단어들에 1부터 1000까지의 번호를 부여해 주는 것이다.  번호를 부여하는데에는 여러가지 방법이 있다. 랜덤하게 부여하기도하지만, 주로 전처리 후 빈도수에 따라 부여한다.

 

 지금부터 4가지 방법을 통해 단어에 정수를 부여해 보겠다.

  • Python dictionary 자료형 사용하기
  • Counter() 사용하기
  • NLTK의 FreqDist 사용하기
  • Keras 사용하기

 

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 
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 barber crazy.', 
'the barber went up a huge mountain.']

 

단어 토큰화 하기

 

## 정제와 단어 토큰화
vocab = {}
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: ##단어 길이가 2이하인 경우에 대하여 추가로 단어를 제거하기
                result.append(word)
                if word not in vocab:
                    vocab[word] = 0
                vocab[word] += 1
    sentences.append(result)

print(sentences)

print(vocab)
[['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']]


 ##단어(key)에 따라 빈도수가 (value)로 들어감
{'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 에는 중복을 제거한 단어와 각 단어의 빈도수가 dictionary 형태로 저장되어있다.

 

빈도수 별로 정렬하기

 

##빈도수가 높은대로 정렬하기
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: ##빈도수가 적은 단어 제외하기
        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}

 

 자연어 처리를 하다보면 텍스트 데이터에 있는 모든 단어를 사용하기 보다 빈고수가 가장 높은 n개의 단어만 사용하고 싶은 경우가 다수이다. 다음 코드는 상위 5개의 단어만 사용하기 위해 처리하는 코드이다.

 

##빈도 순위 상위 5개만 사용
vocab_size = 5
words_frequency = [w for w,c in word_to_index.items() if c >= vocab_size + 1]
for w in words_frequency:
    del word_to_index[w]
    
print(word_to_index)
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5}

 

 처리된 상위 5개만의 단어와 인덱스 정보로 기존의 sentences 를 처리하려하면 없던 단어들(ex: 'good')과 같은 단어를 처리하기 힘들어진다. 이처럼 훈련 혹은 테스트 데이터에 대해 단어 집합에 존재하지 않는 단어들을 'Out-Of-Vocabulary(단어집합에 없는 단어), OOV'라고 한다. 아래는 단어 집합인 word_to_index의 OOV를 추가하는 코드이다.

 

##단어 집합(word_to_index)에 OOV 추가
word_to_index['OOV'] = len(word_to_index) + 1

print(word_to_index)
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5, 'OOV': 7}

 

 이를 바탕으로 sentences의 모든 단어들을 정수로 인코딩해보자.

 

#토큰화된 문장 인덱스 값으로 전환하기
encoded = []
for s in sentences:
    temp = []
    for w in s:
        try:
            temp.append(word_to_index[w])
        except KeyError:
            temp.append(word_to_index['OOV'])
    encoded.append(temp)
    
print(encoded)
[[1, 5], [1, 7, 5, 1, 3, 5], [7, 2], [2, 4, 3, 2], [3, 2], [1, 4, 7], [1, 4, 7], [1, 4, 2],
[7, 7, 3, 2, 7, 1, 7], [1, 7, 3, 7]]

 

2) Counter() 사용하기

 

 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']]

 

 현재 sentences에는 토큰화 된 결과가 저장되어 있다. 단어 집합을 만들기 위해서는 list를 단일화하고 '[,]'를 제거하여야한다.

 

from collections import Counter

#sentences에서 문장의 경계인 ,를 제거하고 단어들을 하나의 리스트로 만들기
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']

 

 이를 Counter()에 적용하면 알아서 중복을 제거하고 빈도수를 기록한다.

 

#Counter()로 중복 제거하고 빈도수 계산
vocab = Counter(words)

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_sorted와 동일하게 단어를 key로 빈도수를 value로 나타낸다.

 

 상위 5개의 빈두수만 출력해서 저장해보자.

 

vocab_size = 5
vocab = vocab.most_common(vocab_size) ##등장 빈도 순위 상위 5개만 저장

print(vocab)
[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3)]

 

 높은 빈도수에 낮은 인덱스 부여하기

 

##높은 빈도순으로 1부터 정수 값 부여
word_to_index = {}
i = 0
for (word, frequency) in vocab:
    i += 1
    word_to_index[word] = i
    
print(word_to_index)
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5}

 

 이를 바탕으로 'OOV'를 추가해주고 sentences를 정수 인코딩하면 dictionary기법과 똑같이 출력된다.

 

#토큰화된 문장 인덱스 값으로 전환하기
encoded = []
for s in sentences:
    temp = []
    for w in s:
        try:
            temp.append(word_to_index[w])
        except KeyError:
            temp.append(word_to_index['OOV'])
    encoded.append(temp)
    
print(encoded)
[[1, 5], [1, 7, 5, 1, 3, 5], [7, 2], [2, 4, 3, 2], [3, 2], [1, 4, 7], [1, 4, 7], [1, 4, 2],
[7, 7, 3, 2, 7, 1, 7], [1, 7, 3, 7]]

 

3) NLTK의 FreqDist 사용하기

 

 NLTK에는 빈도수 계산 도구인 FreqDist()를 지원한다.

 

from nltk import FreqDist
import numpy as np

vocab = FreqDist(np.hstack(sentences)) ##np.hstack 으로 문장 구분을 제거하고 하나의 리스토로 만들기

print(vocab['barber'])
##단어의 빈도수가 value로 저장됨

8

##print(vocab)은 dictionary가 아닌 <FreqDist with 13 samples and 36 outcomes> 로 정보가 출력된다.

 

위의 Counter()방법 처럼 most_common으로 상위 빈도수만 출력하여 단어 집합으로 저장하고 enumerate()를 사용하여 기존의 코드보다 더 짧은 코드로 인덱스를 부여한다.

 

vocab_size = 5
vocab = vocab.most_common(vocab_size)

####enumerate()를 사용하여 높은 빈도수일수록 낮은 인덱스 값 무여하기####
word_to_index = {word[0] : index + 1 for index, word in enumerate(vocab)}

print(word_to_index)
{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5}
enumerate()란??


 -순서가 있는 자료형(list, tuple, set, dictionary, string)을 입력으로 받아 인덱스를 순차적으로 함께 부여한다


test = ['a', 'b', 'c', 'd', 'e'] 

for index, value in enumerate(test):       ##알파벳 순이 아닌 입력의 순서대로 0부터 인덱스 부여 
    print("value : {}, index: {}".format(value, index)) 

print(tuple(enumerate(test)))

=> value : a, index: 0
     value : b, index: 1  
     value : c, index: 2  
     value : d, index: 3 
     value : e, index: 4    
 
    ((0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e'))

 

4. Keras의 텍스트 전처리

 

이전의 sentences 데이터를 동일하게 사용한다.

from tensorflow.keras.preprocessing.text import Tokenizer
[['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']]

 

단어 토큰화하기

 

tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentences)  ##빈도수를 기준으로 단어 집합을 생성한다

print(tokenizer.word_index) 

print(tokenizer.word_counts)

print(tokenizer.texts_to_sequences(sentences))
##단어에 부여된 인덱스 확인하기 (높은 순으로 낮은 정수 인덱스 부여)
{'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', 16), ('person', 6), ('good', 2), ('huge', 10), ('knew', 2), ('secret', 12),
('kept', 8), ('word', 4), ('keeping', 4), ('driving', 2), ('crazy', 2), ('went', 2), ('mountain', 2)])

##입력으로 들어온 코퍼스를 단어에 할당된 인덱스로 전환해 주기
[[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]]

 

 앞의 코드들에서는 상위 빈도수만 해당되는 단어들을 구하기 위해 most_common()을 사용했다. 케라스에는 Tokenizer()에서 num_words=n 을 통해 빈도수가 높은 상위 n개의 단어만 사용하겠다고 선언할 수 있다. 

 

vocab_size = 5
tokenizer = Tokenizer(num_words=vocab_size + 1)
tokenizer.fit_on_texts(sentences)

print(tokenizer.word_index)

print(tokenizer.word_counts)

print(tokenizer.texts_to_sequences(sentences))

  1을 더하는 이유는  num_words가 0부터 카운트하기 때문이다. 실질적으로 숫자 0에 해당하는 단어는 존재하지 않는데도 keras에 0이 있는 이유는 패딩(padding)이라는 자연어처리 작업 때문이다. 

##1~5만 적용되고 나머지는 제거됨
{'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)])

#실제 적용은 texts_to_sequence에서만 적용된다
##5개만 사용한다고 설정했는데도 전체가 출력된다
[[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]]

 

 굳이 필요하진 않지만 word_index와 word_count에도 지정된 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]]

 

 위의 결과를 보다보면 알 수 있다시피 케라스 tokenizer는 정수 인코딩 과정 중 단어 집합에 없는 단어(OOV)에 대해서는 아예 해당 단어를 제거해버린다는 특징이 있다. 단어 집합에 없는 단어를 OOV로 간주해서 보존하고 싶다면 oov_token을 사용하면 된다.

 

vocab_size = 5
tokenizer = Tokenizer(num_words= vocab_size + 2, oov_token= "OOV")
tokenizer.fit_on_texts(sentences)

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

 빈도수는 상위 5개만 사용하며, 0과 oov를 고려해서 단어 집합의 크기는 +2를 해준것이다.

단어 OOV의 인덱스: 1

 

OOV까지 포함된 인덱스 정보다 부여된 단어 집합을 사용하여 코퍼스를 인코딩해 보자.

print(tokenizer.word_index)

print(tokenizer.texts_to_sequences(sentences))
#인데스 정보가 부여된 단어 집합
{'OOV': 1, 'barber': 2, 'secret': 3, 'huge': 4, 'kept': 5, 'person': 6, 'word': 7, 'keeping': 8,
'good': 9, 'knew': 10, 'driving': 11, 'crazy': 12, 'went': 13, 'mountain': 14}

#인코딩 결과 값
[[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]]

 

 상위 5개의 단어에만 2~6의 인덱스가 정보가 대체되고 그 외는 모두 1로 인코딩 되었다.

반응형

댓글