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로 인코딩 되었다.
'Study > NLP' 카테고리의 다른 글
자연어 처리의 꽃! - 워드 투 벡터(Word2Vec) (0) | 2020.08.10 |
---|---|
BERT를 이용한 한국어 자연어처리 (0) | 2020.08.02 |
개체명 인식 학습 기법 (0) | 2020.07.17 |
형태소 분석기 (Kiwi와 Soynlp) (0) | 2020.05.07 |
01. 텍스트 전처리 - 원-핫 인코딩 (0) | 2020.03.14 |
댓글