[자연어 처리 입문과정 공부하는 기록입니다.]
전문가가 아닌 학부 1학년이 혼자 공부하고 남기는 기록이다 보니 잘못된 정보가 다소 포함되어 있을수도 있습니다. 잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다.
PC환경에서 읽어주시면 감사하겠습니다.
정제되지 않은 코퍼스에서 토큰이라고 불리는 단어로 불리는 단위로 나누는 작업을 토큰화라고 한다. 이 토큰이라는 것은 대게로 의미있는 단위로 토큰을 정의하게 된다.
[ what a wonderful day. do you? ]
라는 문장을 단어토큰화로 진행한다면, 구두점을 지운뒤 띄어쓰기를 기준으로 자르면 아래의 내용과 같다.
{what}{a}{wonderful}{day}{do}{you}
이런식으로 토큰화가 되게 되는데 이러한 토큰화는 정말 기초적인 토큰화에 해당한다. 보통은 위와 같은 방법으로 정제되지 않는다. 왜냐면 구두점이나 띄어쓰기만으로 구분한게 된다면 본래의 의미를 잃어버리는 경우도 있기 때문이다.
[Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop.]
라는 문장을 word_tokenize 와 WordPunctTokenizer 두가지 도구가 어퍼스트로피를 어떻게 분류해내는지 확인해보도록 하자.
[1] word_tokenize의 결과값 'Do', "n't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr.', 'Jone', "'s", 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.'
[2] WordPunctTokenizer의 결과값 'Don', "'", 't', 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr', '.', 'Jone', "'", 's', 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.'
확연히 결과값만 보더라도 두 도구의 차이점을 알수있다. 관전포인트 두 도구가 어퍼스트로피(')를 어떻게 처리하는지를 잘 살펴보면 재밌다.
더 재밌는 결과가 있는 케라스를 보도록 하겠다
["don't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', 'mr', "jone's", 'orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop']
케라스의 경우 대문자를 소문자로 바꾸면서 구두점을 제거하지만, don't나 jone's와 같이 의미있는 축약목적의 어퍼스트로피는 살리는것을 볼수있다. GOOD!
표준 토큰화도구인 "Penn TreebankTokenization"에는 규칙이 있다고 한다.
규칙 1. 하이픈(-)으로 구성된 단어는 하나로 유지한다.
규칙 2. doesn't와 같이 아포스트로피로 '접어'가 함께하는 단어는 분리해준다.
규칙에 따라 예제를 살펴보도록 하겠다.
"Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own."를 토큰화 하게 되면,,
'Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", 'have', 'a', 'food', 'chain', 'or', 'restaurant', 'of', 'their', 'own', '.'
이렇게
예제문장:
"Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own."
트리뱅크 워드토크나이저 : ['Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", 'have', 'a', 'food', 'chain', 'or', 'restaurant', 'of', 'their', 'own', '.']
Treebank Tokenization의 규칙에 따라 home-based는 하나의 토큰으로 취급하는것을 알수있고, dosen't의 경우 does와 n't로 분리 된것을 알수있다.
from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
words = ['policy', 'doing', 'organization', 'have', 'going', 'love', 'lives', 'fly', 'dies', 'watched', 'has', 'starting']
print('표제어 추출 전 :',words)
print('표제어 추출 후 :',[lemmatizer.lemmatize(word) for word in words])
표제어 추출 전 : ['policy', 'doing', 'organization', 'have', 'going', 'love', 'lives', 'fly', 'dies', 'watched', 'has', 'starting']
표제어 추출 후 : ['policy', 'doing', 'organization', 'have', 'going', 'love', 'life', 'fly', 'dy', 'watched', 'ha', 'starting']
위의 결과에 따르면 dy나 ha와 같이 적절하지 못한 단어를 출력하고 있다. 이는 표제어 추출기가 품사의 정보를 알아야 정확한 결과를 알수있기 때문이다.
-정확한 품사 종류를 알고있을때의 예시.-
lemmatizer.lemmatize('dies', 'v')
'die'
이렇게 동사라는 것을 알려주게 된다면 원형인 'die'로 출력하는 것을 알수있습니다.
원래 어간 추출을 할때 사용하는 알고리즘중 하나인 poter Algorithm도 있지만 추후에 업데이트를 통해 올리도록 하겠습니다. 아직 제대로 이해하지 못해서 정확하게 남기는것은 좀 무리인것 같습니다.
example = "Family is not an important thing. It's everything."
stop_words = set(stopwords.words('english'))
word_tokens = word_tokenize(example)
result = []
for word in word_tokens:
if word not in stop_words:
result.append(word)
print('불용어 제거 전 :',word_tokens)
print('불용어 제거 후 :',result)
불용어 제거 전 : ['Family', 'is', 'not', 'an', 'important', 'thing', '.', 'It', "'s", 'everything', '.']
불용어 제거 후 : ['Family', 'important', 'thing', '.', 'It', "'s", 'everything', '.']
결과를 보면 'is','not',an'과 같은 단어들이 문장에서 제거된것을 볼수있다.
!![하지만 여기서 의문이 들었던것. 다른 단어는 몰라도 'not'과 같이 긍정인지 부정인지를 명확히 나타내는 단어를 지우면 정확한 의도를 파악하는데 과연 문제가 없을까?에 대한 의문을 가지게 되었다.]!!
from nltk.tokenize import sent_tokenize
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
raw_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."
# 빈도수가 적당히 분포되도록 의도적으로 만들어진 텍스트 데이터.
sentences = sent_tokenize(raw_text) #문장 토큰화
print(sentences)
['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 = {}
preprocessed_sentences = []
stop_words = set(stopwords.words('english'))
for sentence in sentences:
# 단어 토큰화
tokenized_sentence = word_tokenize(sentence)
result = []
for word in tokenized_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
preprocessed_sentences.append(result)
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에 단어를 입력하면 빈도수를 리턴하게 된다. 빈도수가 높은 순으로 다시 정렬 할 수도 있다.
from collections import Counter
print(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']]
from nltk import FreqDist
import numpy as np
vocab = FreqDist(np.hstack(preprocessed_sentences))
print(vocab["barber"]) # "barber"라는 단어 빈도수를 출력
8
*빈도수가 높은 5개만 추출.
vocab_size = 5
vocab = vocab.most_common(vocab_size)
print(vocab)
[('barber', 8), ('secret', 6), ('huge', 5), ('kept', 4), ('person', 3)]
*높은 빈도수일 경우 낮은 인덱스 번호 부여.
test_input = ['a', 'b', 'c', 'd', 'e']
for index, value in enumerate(test_input): # 배열과 동일하게 시작 인덱스는 0부터 시작.
print("value : {}, index: {}".format(value, index))
value : a, index: 0
value : b, index: 1
value : c, index: 2
value : d, index: 3
value : e, index: 4
import numpy as np
from tensorflow.keras.preprocessing.text import # 위에서 정수 인코딩 할때 사용했던 단어 집합체를 사용.Tokenizer
tokenizer = Tokenizer()
tokenizer.fit_on_texts(preprocessed_sentences)
print(tokenizer.word_index) # 각 단어에 인덱스 부여
print(tokenizer.word_counts) # 빈도수가 높은 순서로 빈도수와 함께 출력
{'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)])
*Okt형태소 분석기를 사용
from konlpy.tag import Okt
okt = Okt()
tokens = okt.morphs("나는 자연어 처리를 배운다") # 이 문장을 토큰화 진행
word_to_index = {word : index for index, word in enumerate(tokens)} # 각 토큰에 대하여 정수를 부여
print('단어 집합 :',word_to_index) #출력
단어 집합 : {'나': 0, '는': 1, '자연어': 2, '처리': 3, '를': 4, '배운다': 5} #인덱스 고유번호 부여
def one_hot_encoding(word, word_to_index): # 원-핫 벡터 함수
one_hot_vector = [0]*(len(word_to_index))
index = word_to_index[word]
one_hot_vector[index] = 1
return one_hot_vector
one_hot_encoding("배운다", word_to_index) # '배운다'의 원-핫 벡터 출력
[0, 0, 0, 0, 0, 1] # 배운다에 해당하는 5번 인덱스를 제외한 나머지 인덱스는 0
** 원-핫 인코딩의 한계점: 단어의 개수가 많아질수록 공간에 저장해야하기 때문에 다차원 공간을 가지는 원 - 핫 인코딩은 저장공간 측면에서 굉장히 비효율적인 방법이다. 게다가 단어간의 유사도를 알수없기때문에 검색시스템과 같은 환경에서 문제의 소지가 될수있다.
이를 보완하기 위해 단어의 잠재 의미를 포함하여 다차원 공간에 벡터화 하는 기법으로 두가지가 있다.
첫째, 카운터기반의 벡터화 방법 LSA(잠재의미분석), 두번째로 예측기반으로 벡터화하는 'NNLM, RNNLM, Word2Vec, FastText' 등이 있다.