컴퓨터는 텍스트보다는 숫자를 더 잘 처리할 수 있다. 이를 위해 자연어 처리에서는 텍스트를 숫자로 바꾸는 여러 가지 기법들이 있다. 그리고 그러한 기법들을 본격적으로 적용시키기 위한 첫 단계로 각 단어를 고유한 숫자에 맵핑(Mapping)시키는 전처리 작업이 필요할 때가 있다.
예를 들어 갖고 있는 텍스트에 단어가 5,000개 있다면, 5,000개의 단어들 각각에 0번부터 4,999번까지 단어와 맵핑되는 고유한 숫자, 다른 말로는 인덱스를 부여한다. 인덱스를 부여하는 방법은 여러 가지가 있을 수 있는데, 랜덤으로 부여하기도 하지만 보통은 전처리도 같이 겸하기 위해 단어에 대한 빈도수로 정렬한 뒤에 부여한다.
정수 인코딩 (Integer Encoding)
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: # 단어 길이가 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에는 중복을 제거한 단어와 각 단어에 대한 빈도수가 기록되어져 있다.
빈도수가 높은 순서대로 정렬해보자.
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
}
1의 인덱스를 가진 단어가 가장 빈도수가 높은 단어가 된다. 등장 빈도가 낮은 단어는 자연어 처리에서 의미를 가지지 않을 가능성이 높기 때문에 빈도수가 1인 단어들은 전부 제외했다.
자연어 처리를 하다보면, 텍스트 데이터에 있는 단어를 모두 사용하기 보다는 빈도수가 가장 높은 n개의 단어만 사용하고 싶은 경우가 많다. 위 단어들은 빈도수가 높은 순으로 낮은 정수가 부여되어져 있으므로 빈도수 상위 n개의 단어만 사용하고 싶다고하면 vocab에서 정수값이 1부터 n까지인 단어들만 사용하면 된다.
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)
{
'barber': 1,
'secret': 2,
'huge': 3,
'kept': 4,
'person': 5,
}
word_to_index를 사용하여 단어 토큰화가 된 상태로 저장된 sentences에 있는 각 단어를 정수로 바꾸는 작업을 해보자.
단어 집합에 존재하지 않는 단어들을 Out-Of-Vocabulary(단어 집합에 없는 단어)의 약자로 'OOV'라고 한다. word_to_index에 'OOV'란 단어를 새롭게 추가하고, 단어 집합에 없는 단어들은 'OOV'의 인덱스로 인코딩한다.
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])
except KeyError:
temp.append(word_to_index['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
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'
]
sentences를 이용하여 단어집합을 만들어준다.
이 단어집합을 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
})
most_common()는 상위 빈도수를 가진 주어진 수의 단어만을 리턴해준다. 이를 사용하여 등장 빈도수가 높은 단어들을 원하는 개수만큼만 얻을 수 있다.
vocab_size = 5
vocab = vocab.most_common(vocab_size) # 등장 빈도수가 높은 상위 5개의 단어만 저장
vocab
[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3)]
이제 높은 빈도수를 가진 단어일수록 낮은 정수 인덱스를 부여한다.
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 사용하기
NLTK에서는 빈도수 계산 도구인 FreqDist()를 지원한다.
from nltk import FreqDist
import numpy as np
vocab = FreqDist(np.hstack(sentences)) # np.hstack으로 단어 집합을 만들어 넣어준다.
vocab_size = 5
vocab = vocab.most_common(vocab_size) # 등장 빈도수가 높은 상위 5개의 단어만 저장
print(vocab)
[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3)]
높은 빈도수를 가진 단어일수록 낮은 정수 인덱스를 부여하기 위해 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}
케라스(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']]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentences)
print(tokenizer.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
}
fit_on_texts는 입력한 텍스트로부터 단어 빈도수가 높은 순으로 낮은 정수 인덱스를 부여하는데, 앞서 설명한 정수 인코딩 작업이 이루어진다고 보면된다.
각 단어에 인덱스가 어떻게 부여되었는지를 보려면, word_index를 사용하면 된다. 각 단어의 빈도수가 높은 순서대로 인덱스가 부여된 것을 확인할 수 있다.
각 단어가 카운트를 수행하였을 때 몇 개였는지를 보고자 한다면 word_counts를 사용한다.
print(tokenizer.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()는 입력으로 들어온 코퍼스에 대해서 각 단어를 이미 정해진 인덱스로 변환해준다.
print(tokenizer.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()을 사용했었다.
케라스 토크나이저에서는 tokenizer = Tokenizer(num_words=숫자)와 같은 방법으로 빈도수가 높은 상위 몇 개의 단어만 사용하겠다고 지정할 수 있다.
vocab_size = 5
tokenizer = Tokenizer(num_words=vocab_size + 1) # 상위 5개 단어만 사용
tokenizer.fit_on_texts(sentences)
print(tokenizer.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
}
print(tokenizer.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)])
print(tokenizer.texts_to_sequences(sentences))
[[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개의 단어만 사용하겠다고 선언하였는데 여전히 13개의 단어가 모두 출력된다. word_counts에서도 마찬가지로 13개의 단어가 모두 출력된다. 실제 적용은 texts_to_sequences를 사용할 때 적용이 된다.
코퍼스에 대해서 각 단어를 이미 정해진 인덱스로 변환하는데, 상위 5개의 단어만을 사용하겠다고 지정하였으므로 1번 단어부터 5번 단어까지만 보존되고 나머지 단어들은 제거된 것을 볼 수 있다.
단어 집합에 없는 단어들은 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)
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]]
빈도수 상위 5개의 단어는 2 ~ 6까지의 인덱스를 가졌으며, 그 외 단어 집합에 없는 'good'과 같은 단어들은 전부 'OOV'의 인덱스인 1로 인코딩되었다.
<참고 사이트> https://wikidocs.net/31766
'NLP' 카테고리의 다른 글
카운트 기반의 단어 표현(Count based word Representation) (0) | 2022.01.28 |
---|---|
패딩(Padding) (0) | 2022.01.28 |
어간 추출(Stemming)과 표제어 추출(Lemmatization) (0) | 2022.01.27 |
토큰화 Tokenization (0) | 2022.01.27 |
불용어(Stop word) 제거 (0) | 2022.01.27 |