본 글은 위키독스 딥러닝을 이용한 자연어처리 입문을 참고했다.
말 그대로 해당 단어가 몇개인지 세는 작업이다. 이는 추후에 응용이 좋으므로 필수적이다. 직접 코드를 짜서 할 수도 있고 Counter,NLTK의 FreqDist, keras 등으로 시도할 수 있다. 코드를 직접 짜는 경우는 너무 길고 실용성이 떨어지기 때문에 딥러닝을 이용한 자연어처리 입문을 참고하자
아래 텍스트는 직접 코드를 짜서 실행하는 과정에 만들어진 생긴 텍스트이다.
from collections import Counter
import numpy as np
preprocessed_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']]
위는 토큰화된 결과가 [],로 묶여 있다. 단어 집합을 만들기 위해서 []를 제거하고 하나의 리스트로 만들 필요가 있다.
all_words_list = sum(preprocessed_sentences, [])
all_words_list = np.hstack(preprocessed_sentences)
#output
#['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 사용이다.
vocab_len = Counter(all_words_list)
print(vocab_len)
#output
#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})
이렇게 all_words_list에 있는 단어의 수를 세준것을 볼 수 있다.
vocab_size= 5
vocab = vocab_len.most_common(vocab_size)
word_to_frequency= {}
i=0
for text, num in vocab:
i+= 1
word_to_frequency[text] = i
word_to_frequency
#output
#{'barber': 1, 'secret': 2, 'huge': 3, 'kept': 4, 'person': 5}
그 이후로 .most_common()으로 상위빈도수를 가진 주어진 수의 단어를 리턴한다. 해당 코드에서 vocab_size가 5 이므로 5개의 단어를 리턴한다. 이제 높은 빈도수를 가진 단어일수록 낮은 정수 인덱스를 부여한다.
다음은 NLTK에서 빈도수를 계산해주는 FreqDist를 사용하겠다.
vocab = FreqDist(np.hstack(preprocessed_sentences))
vocab_size=5
vocab = vocab.most_common(vocab_size)
print(vocab)
#output
#[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3)]
아래는 enumerate를 이용한 더 짧은 코드를 만드는 법이다
word_to_index = {word[0]:idx for idx,word in enumerate(vocab)}
#output
#{'barber': 0, 'secret': 1, 'huge': 2, 'kept': 3, 'person': 4}
위와 같이 인덱스를 부여할 때는 enumerate()를 사용하는 것이 편리하다다.
사실 이 단어는 이미지처리할 때 들어봤다. 하지만 수업을 열심히 안들은 관계로 잘 모른다...ㅋ
기계는 병렬 연산이 효율적이다. 병렬 연산에서는 문서의 길이가 반드시 동일해야한다. 이처럼 병렬 연산을 위해서는 각기 다른 길이의 문장을 같은 길이로 맞춰 줄 필요가 있다. 이때 하는 작업이 패딩이다.
preprocessed_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(preprocessed_sentences)
encoded = tokenizer.texts_to_sequences(preprocessed_sentences)
print(encoded)
#output
#[[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]]
max_len = max(len(item) for item in encoded)
print('최대 길이 :',max_len)
#output
#최대 길이 : 7
이렇게 가장 긴 문장의 길이가 7인 것을 알 수 있다. 여기에 패딩을 더해줄 건데 길이가 7보다 짧은 문장에 0으로 채워준다. 그러면 기계는 0을 무시하고 지나간다. 이처럼 0으로 padding을 하는 것을 제로패딩이라고 한다.
for i in encoded:
while len(i) < 7 :
i.append(0)
print(encoded)
#output
#[[1, 5, 0, 0, 0, 0, 0], [1, 8, 5, 0, 0, 0, 0], [1, 3, 5, 0, 0, 0, 0], [9, 2, 0, 0, 0, 0, 0], [2, 4, 3, 2, 0, 0, 0], [3, 2, 0, 0, 0, 0, 0], [1, 4, 6, 0, 0, 0, 0], [1, 4, 6, 0, 0, 0, 0], [1, 4, 2, 0, 0, 0, 0], [7, 7, 3, 2, 10, 1, 11], [1, 12, 3, 13, 0, 0, 0]]
from tensorflow.keras.preprocessing.sequence import pad_sequences
encoded = tokenizer.texts_to_sequences(preprocessed_sentences)
위에서 encoded 변수가 zero padding으로 채워져서 다시 선언해준다.
padded=pad_sequences(encoded)
padded
#output
#array([[ 0, 0, 0, 0, 0, 1, 5],
[ 0, 0, 0, 0, 1, 8, 5],
[ 0, 0, 0, 0, 1, 3, 5],
[ 0, 0, 0, 0, 0, 9, 2],
[ 0, 0, 0, 2, 4, 3, 2],
[ 0, 0, 0, 0, 0, 3, 2],
[ 0, 0, 0, 0, 1, 4, 6],
[ 0, 0, 0, 0, 1, 4, 6],
[ 0, 0, 0, 0, 1, 4, 2],
[ 7, 7, 3, 2, 10, 1, 11],
[ 0, 0, 0, 1, 12, 3, 13]], dtype=int32)
이번에는 제로패딩이 앞부터 된 것을 볼 수 있다. 그 이유는 pad_sequences가 numpy와 달리 제로패딩을 앞부터 채우기 때문이다. 뒤에 0을 채우고 싶으면 padding='post'를 하면된다.
padded = pad_sequences(encoded, padding='post')
padded
#output
#array([[ 1, 5, 0, 0, 0, 0, 0],
[ 1, 8, 5, 0, 0, 0, 0],
[ 1, 3, 5, 0, 0, 0, 0],
[ 9, 2, 0, 0, 0, 0, 0],
[ 2, 4, 3, 2, 0, 0, 0],
[ 3, 2, 0, 0, 0, 0, 0],
[ 1, 4, 6, 0, 0, 0, 0],
[ 1, 4, 6, 0, 0, 0, 0],
[ 1, 4, 2, 0, 0, 0, 0],
[ 7, 7, 3, 2, 10, 1, 11],
[ 1, 12, 3, 13, 0, 0, 0]], dtype=int32)
이때 꼭 가장 긴문서를 기준으로 할 필요는 없다. 만약 평균 문서 길이가 20이고 하나의 문서의 길이가 5000이라면 하나 때문에 전체에 패딩을 적용하면 비효율적일 수 있다. 이때 maxlen으로 길이를 제한할 수 있다.
padded = pad_sequences(encoded, padding='post', maxlen=5)
padded
###output
###array([[ 1, 5, 0, 0, 0],
[ 1, 8, 5, 0, 0],
[ 1, 3, 5, 0, 0],
[ 9, 2, 0, 0, 0],
[ 2, 4, 3, 2, 0],
[ 3, 2, 0, 0, 0],
[ 1, 4, 6, 0, 0],
[ 1, 4, 6, 0, 0],
[ 1, 4, 2, 0, 0],
[ 3, 2, 10, 1, 11],
[ 1, 12, 3, 13, 0]], dtype=int32)
짧아진걸 확인할 수 있다. 길이가 5보다 짧았던 문장은 0으로 채워지고 5보다 길었던 문장은 데이터가 손실된 것을 볼 수 있다. 데이터가 손실되는 것이 앞의 단어가 아니라 뒤의 단어가 되게 하고싶으면 truncating 인자를 사용하면된다. truncating='post'를 사용할 경우 뒤의 단어가 삭제된다.
padded = pad_sequences(encoded, padding='post',maxlen=5,truncating='post')
padded
#output
#array([[ 1, 5, 0, 0, 0],
[ 1, 8, 5, 0, 0],
[ 1, 3, 5, 0, 0],
[ 9, 2, 0, 0, 0],
[ 2, 4, 3, 2, 0],
[ 3, 2, 0, 0, 0],
[ 1, 4, 6, 0, 0],
[ 1, 4, 6, 0, 0],
[ 1, 4, 2, 0, 0],
[ 7, 7, 3, 2, 10],
[ 1, 12, 3, 13, 0]], dtype=int32)
알게된 점