19.06.08
본 글은 https://wikidocs.net/book/2155을 참고하여 작성되었음을 밝힙니다.
핵심키워드
- 데이터의 분리(Splitting Data)
- 정수 인코딩(Integer Encoding)
1) 문장 토큰화
2) 단어 토큰화
3) 소문자 변환
4) 불용어 제거 / 단어 길이 필터링
5) 단어 빈도수 확인 및 나열
6) 높은 빈도수 순으로 인덱스 부여
- 케라스(Keras)의 텍스트 전처리
- enumerate()
- FreqDist from NLTK
데이터의 분리(Splitting Data)
머신 러닝(딥러닝 포함) 모델에 데이터를 사용하기 위해서는 데이터를 적절히 분리해 놓는 작업을 필요로 한다.
1. X와 y분리하기
1) zip 함수를 이용하여 분리하기
zip()함수는 동일한 개수를 가지는 시퀀스 자료형에서 각 순서에 등장하는 원소들끼리 묶어주는 역할을 한다. 리스트의 리스트 구성에서 zip 함수는 X와 y를 분리하는 데 유용하다. 우선 zip함수에 대해서 알아보자.
X, y = zip(['a', 1], ['b', 2], ['c', 3])
X
('a', 'b', 'c')y
(1, 2, 3)
각 데이터에서 첫번째로 등장한 원소들끼리 묶이고, 두번째로 등장한 원소들끼리 묶인 것을 볼 수 있다.
sequence=[['a', 1], ['b', 2], ['c', 3]]
# 리스트의 리스트 또는 행렬 또는 뒤에서 배울 개념인 2D 텐서X, y = zip(*sequence)
X
('a', 'b', 'c')y
(1, 2, 3)
각 데이터에서 첫번째로 등장한 원소들끼리 묶고, 두번째로 등장한 원소들끼리 묶었다.
2. 테스트 데이터 분리하기
이번에는 이미 X와 y가 분리된 데이터에 대해서 테스트 데이터를 분리하는 법을 알아보자.
1) 사이킷 런을 이용하여 분리하기
train_test_split() 을 이용한다.
정수 인코딩(Integer Encoding)
컴퓨터는 텍스트보다는 숫자를 더 잘 처리할 수 있다. 이를 위해 자연어 처리에서는 텍스트를 숫자로 바꾸는 여러가지 기법들이 있다. 그리고 그러한 기법들을 본격적으로 적용시키기 위한 첫 단계로 각 단어를 고유한 숫자에 맵핑(Mapping)시키는 전처리 작업이 필요할 때가 있다.
예를 들어 갖고 있는 텍스트에 단어가 5,000개 있다면, 5,000개의 단어들 각각에 0번부터 4,999번까지 단어와 맵핑되는 고유한 숫자, 다른 말로는 인덱스를 부여한다. 인덱스를 부여하는 방법은 여러 가지가 있을 수 있는데, 랜덤으로 부여하기도 하지만 보통은 전처리도 같이 겸하기 위해 단어에 대한 빈도수로 정렬한 뒤에 부여한다.
1. 정수 인코딩(Integer Encoding)
왜 이러한 작업이 필요한 지에 대해서는 뒤에서 원-핫 인코딩과 이를 이용한 딥러닝 실습이나 워드 임베딩 챕터에서 배워보기로 하고, 여기서는 단어를 어떻게 순차적으로 인덱스를 부여하는지에 대해서만 정리한다.
단어에 숫자를 부여하는 방법 중 하나로 단어를 빈도수 순으로 정렬하여 단어 집합(Vocabulary)을 만들고, 빈도수가 높은 순서대로 차례로 낮은 숫자부터 정수를 부여하는 방법이 있다.
예제를 통해 확인해보자.
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."
우선 여러 문장이 함께 있는 텍스트 데이터로부터 문장 토큰화를 수행해보도록 한다.
from nltk.tokenize import sent_tokenize
tokenized_text = sent_tokenize(text)
print(tokenized_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.']
기존의 텍스트 데이터가 문장 단위로 토큰화 된 것을 확인할 수 있다. 이제 정제 작업을 병행하며, 단어 토큰화를 수행해본다. 또한, 단어 토큰화를 수행하면서 각 단어에 대한 빈도수 또한 같이 계산해보자.
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from collections import Countervocab = Counter() # Counter() 모듈은 단어의 빈도를 쉽게 계산할 수 있다sentences = []
stop_words = set(stopwords.words('english'))for i in tokenized_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)
vocab[word] = vocab[word]+1 # 각 단어의 빈도를 Count한다
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)
Counter({'barber': 16, 'secret': 12, 'huge': 10, 'kept': 8, 'person': 6, 'word': 4, 'keeping': 4, 'good': 2, 'knew': 2, 'driving': 2, 'crazy': 2, 'went': 2, 'mountain': 2})
이제 vocab에는 각 단어에 대한 빈도수가 기록되었다. 각 단어들을 빈도수 순으로 정렬해 보도록한다.
vocab_sorted = sorted(vocab.items(), key=lambda x:x[1], reverse=True)print(vocab_sorted)
[('barber', 16), ('secret', 12), ('huge', 10), ('kept', 8), ('person', 6), ('word', 4), ('keeping', 4), ('good', 2), ('knew', 2), ('driving', 2), ('crazy', 2), ('went', 2), ('mountain', 2)]
이제 각 단어가 등장 빈도수 순으로 정렬된 것을 확인할 수 있다.
word_to_index = {}
i = 0
for (word, frequency) in vocab_sorted:
if frequency > 1: # 빈도수가 적은 단어는 제외
i += 1
word_to_index[word] = iprint(word_to_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}
높은 빈도수에 따라서 인덱스를 부여했다. 즉, 1의 인덱스를 가진 단어가 가장 빈도수가 높은 단어가 된다. 그리고 이러한 작업을 수행하는 동시에 각 단어의 빈도수를 알 경우에만 할 수 있는 전처리인 빈도수가 적은 단어를 제외시키는 작업을 한다. 바로 단어의 빈도수를 계산할 때의 이점이 여기에 있다. 등장 빈도가 낮은 단어는 자연어 처리에서 의미를 가지지 않을 가능성이 높기 때문이다. 여기서는 빈도수가 1인 단어들은 전부 제외되었다.
2. 케라스(Keras)의 텍스트 전처리
케라스(Keras)는 기본적인 전처리를 위한 도구들을 제공한다. 때로는 정수 인코딩을 위해서 케라스의 전처리 도구인 토크나이저를 사용하기도 하는데, 사용 방법과 그 한계점에 대해서 이해해보자.
from keras.preprocessing.text import Tokenizertext=["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."]t = Tokenizer()
t.fit_on_texts(text)
fit_on_texts 는 텍스트의 리스트(list)를 가지고 단어 빈도수에 기반한 사전을 만듭니다. 빈도수 순으로 단어에게 인덱스를 부여하는데, 정확하게 위에서 설명한 정수 인코딩 작업이 이루어진다고 보면된다. 각 단어에 인덱스가 어떻게 부여되었는지를 보려면, word_index를 사용한다.
print(t.word_index)
{'a': 1, 'barber': 2, 'secret': 3, 'huge': 4, 'his': 5, 'is': 6, 'kept': 7, 'person': 8, 'the': 9, 'he': 10, 'word': 11, 'keeping': 12, 'good': 13, 'knew': 14, 'but': 15, 'and': 16, 'such': 17, 'to': 18, 'himself': 19, 'was': 20, 'driving': 21, 'crazy': 22, 'went': 23, 'up': 24, 'mountain': 25}
각 단어의 빈도수가 높은 순서대로 인덱스가 부여되기 때문에, a가 1번 인덱스를 가지는 것을 볼 수 있다. 각 단어가 몇 개였는지 카운트된 결과를 보려면 word_counts를 사용한다.
print(t.word_counts)
OrderedDict([('a', 8), ('barber', 8), ('is', 4), ('person', 3), ('good', 1), ('huge', 5), ('he', 2), ('knew', 1), ('secret', 6), ('the', 3), ('kept', 4), ('his', 5), ('word', 2), ('but', 1), ('keeping', 2), ('and', 1), ('such', 1), ('to', 1), ('himself', 1), ('was', 1), ('driving', 1), ('crazy', 1), ('went', 1), ('up', 1), ('mountain', 1)])
각 단어가 몇 개였는지를 카운트한 결과를 보여줄 뿐만 아니라, 가장 등장 빈도 수가 높은 순서대로 출력한다. 이 경우에는 a와 barber가 둘 다 8회 등장하였으나, 알파벳 순서로 인해 a가 가장 먼저 등장했다.
texts_to_sequences()는 입력으로 들어온 코퍼스에 대해서 각 단어를 word_index에서 이미 정해진 인덱스로 변환하여 출력한다.
print(t.texts_to_sequences(text))
[[1, 2, 6, 1, 8, 1, 2, 6, 13, 8, 1, 2, 6, 4, 8, 10, 14, 1, 3, 9, 3, 10, 7, 6, 4, 3, 4, 3, 5, 2, 7, 5, 11, 1, 2, 7, 5, 11, 5, 2, 7, 5, 3, 15, 12, 16, 12, 17, 1, 4, 3, 18, 19, 20, 21, 9, 2, 22, 9, 2, 23, 24, 1, 4, 25]]
Counter()를 사용했을 때의 최종 결과와는 달리 총 25개의 단어가 모두 존재하고 25개의 인덱스가 모두 나온다. 별도로 빈도수에 기반한 단어들을 삭제하거나, 불용어 처리를 해주지 않았기 때문이다.
사실 위 코드에서 t = Tokenizer() 대신 t = Tokenizer(num_words = 숫자) 와 같은 방법으로 빈도수가 높은 상위 몇 개의 단어만 남기고 진행시키는 방법이 케라스 토크나이저에도 공식적으로 존재는 하지만, 이를 사용하면 t.text_to_sequences(text)에서는 적용이되면서 t_word_index와 t.word_counts에는 여전히 모든 단어가 인식되는 등의 사용자에게 혼란을 주는 문제가 존재한다. 물론, 정확하게 이를 이해하고 사용한다면 큰 문제는 없다.
케라스 토크나이저의 도구를 사용하면서 사용자가 추가로 정제를 할 수는 있지만, 케라스 전처리 도구에 맞춰서 전처리를 해주는 작업이 직관적이지 않기 때문에 Counter() 등의 다른 방법을 사용할 때보다 오히려 더 복잡해질 수도 있다. 예를 들어 빈도수가 1인 단어에 대해서 word_index와 word_counts까지 고려하여 아예 제거하는 전처리 방법은 다음과 같이 수행할 수 있다.
words_frequency = [w for w,c in t.word_counts.items() if c < 2]
# 빈도수가 2미만인 단어를 w라고 저장for w in words_frequency:
del t.word_index[w]
del t.word_counts[w]print(t.texts_to_sequences(text))
[[1, 2, 6, 1, 8, 1, 2, 6, 8, 1, 2, 6, 4, 8, 10, 1, 3, 9, 3, 10, 7, 6, 4, 3, 4, 3, 5, 2, 7, 5, 11, 1, 2, 7, 5, 11, 5, 2, 7, 5, 3, 12, 12, 1, 4, 3, 9, 2, 9, 2, 1, 4]]print(t.word_index)
{'a': 1, 'barber': 2, 'secret': 3, 'huge': 4, 'his': 5, 'is': 6, 'kept': 7, 'person': 8, 'the': 9, 'he': 10, 'word': 11, 'keeping': 12}
3. enumerate
파이썬의 enumerate()를 사용하면 단어의 리스트에 대해서 쉽게 인덱스를 부여할 수 있다. enumerate()는 주어진 입력에 인덱스를 부여하여 인덱스도 함께 리턴해준다는 특징이 있다. 우선 예제를 통해 enumerate()를 이해해보자.
test=[8, 2, 5, 1, 3, 7, 9, 4, 6, 10]for index, value in enumerate(test):
print("indedx : {}, value : {}".format(index, value))indedx : 0, value : 8
indedx : 1, value : 2
indedx : 2, value : 5
indedx : 3, value : 1
indedx : 4, value : 3
indedx : 5, value : 7
indedx : 6, value : 9
indedx : 7, value : 4
indedx : 8, value : 6
indedx : 9, value : 10
즉, 단어를 우선 정렬하고나서 enumerate()를 사용하면 정수 인코딩 결과 자체는 굉장히 쉽게 얻을 수 있다. 주어진 데이터로부터 단어를 빈도수로 정렬하고, enumerate()로 정수 인코딩을 하는 과정까지 진행해보자.
# 문장 토큰화까지는 되어 있다고 가정함
text=[['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']]
단어 집합(Vocabulary)을 만들기 위해서 문장의 경계인 [, ]를 제거하고 단어들을 하나의 리스트로 만든다.
vocab = sum(text, [])
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']
이제 빈도 순대로 정렬하고, 중복도 제거한 결과를 만들어본다.
vocab_sorted = (sorted(set(vocab)))
print(vocab_sorted)
['barber', 'crazy', 'driving', 'good', 'huge', 'keeping', 'kept', 'knew', 'mountain', 'person', 'secret', 'went', 'word']
enumerate()를 사용하기 위한 전처리가 끝났다. 이제 enumerate()를 통해 순서대로 인덱스를 부여하기만 하면 된다.
word_to_index = {word : index+1 for index, word in enumerate(vocab_sorted)}
# 인덱스를 0이 아닌 1부터 부여한다.print(word_to_index)
{'barber': 1, 'crazy': 2, 'driving': 3, 'good': 4, 'huge': 5, 'keeping': 6, 'kept': 7, 'knew': 8, 'mountain': 9, 'person': 10, 'secret': 11, 'went': 12, 'word': 13}
4. NLTK의 FreqDist 클래스
NLTK에서는 토큰의 빈도를 손쉽게 셀 수 있도록 빈도수 계산 클래스인 FreqDist를 지원한다. FreqDist 클래스는 토큰들을 입력으로 받아 해당 토큰의 빈도 정보를 계산한다. 즉 FreqDist의 입력으로는 반드시 토큰화가 이루어진 상태여야 한다.
test_list = ['barber', 'barber', 'person', 'barber', 'good', 'person']from nltk import FreqDist
fdist = FreqDist(test_list)
이제 fdist에는 각 단어들의 빈도 정보가 저장되어있다. 이를 다양한 형태의 정보로 출력해보도록 한다. FreqDist 클래스는 단어를 키(Key), 출현빈도를 값(Value)으로 가지는 파이썬의 딕셔너리(dict) 자료형의 형태를 가진다. 즉, 단어를 입력하면 출현빈도의 값을 얻을 수 있다.
fdist.N()
6
전체 단어의 개수인 6이 출력되었다.
fdist.freq("barber") # "barber"라는 단어의 확률
0.5
총 6개의 단어 중 3번 등장하였으므로 0.5의 값을 가진다.
fdist['barber'] # 'barber'라는 단어의 빈도수 출력
3
barber란 단어가 총 3번 등장하였음을 의미한다.
fdist.most_common(3) # 등장 빈도수가 높은 상위 3개의 단어 출력
[('barber', 3), ('person', 2), ('good', 1)]
등장 빈도수가 가장 높은 barber와 person이 각각의 빈도수와 함께 출력된 것을 확인할 수 있다.