[자연어 처리 스터디 기록] 3월 19일~3월 26일(2주차)

김수빈·2022년 3월 28일
0

자연어 공부하기

목록 보기
1/4

[자연어 처리 입문과정 공부하는 기록입니다.]

전문가가 아닌 학부 1학년이 혼자 공부하고 남기는 기록이다 보니 잘못된 정보가 다소 포함되어 있을수도 있습니다. 잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

PC환경에서 읽어주시면 감사하겠습니다.

1. 토큰화(Tokenization)

정제되지 않은 코퍼스에서 토큰이라고 불리는 단어로 불리는 단위로 나누는 작업을 토큰화라고 한다. 이 토큰이라는 것은 대게로 의미있는 단위로 토큰을 정의하게 된다.

  • 단어토큰화 : 위에서 말했듯 의미있는 단위로 토큰화를 진해하게 되는데 단어토큰화는 '단어'를 기준으로 토큰화를 진행하게 된다. 하지만 이렇게 단어만을 기준으로 토큰화를 했을때 문제점이 발생한다. 따라서 그러한 문자를 제외시킨 뒤 토큰화를 진행하게 된다. 예시를 통해 살펴보도록 하자.

[ 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!

  • 고려사항 : 토큰은 단순히 구두점이나 공백을 제외하는것이 전부는 아니다. 구두점('.', ',', '!', '~')이 문장의 경계를 파악하는데 도움이 되는 기호로 무조건적으로 제외하는것은 옳지않다. 또한 숫자를 나타낼때 $ 12,345를 나타낼때 하나의 의미로 붙이는것이 토큰개념에서 볼때 유리한데 이를 따로 분리하는것은 올바른 토큰화는 아니다. 그래서 가장 표준적으로 사용하는 "Penn TreebankTokenization"을 알아보도록 하겠다.

표준 토큰화도구인 "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', '.'

이렇게

  • word to sentence : 지금까지 단어토큰화에 대해 알아보고 다양한 도구를 사용해봤다면, 이젠 단어의 조합인 문장으로 좀 더 확장된 개념에서 살펴보도록 하겠다.

예제문장:

"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로 분리 된것을 알수있다.

  • 한국어 자연어 처리가 어려운 이유 : 영어와 달리 띄어쓰기만으로 토큰화를 진행하기에는 부족할뿐만 아니라 어절단위의 토큰화는 단어토큰화와 같지 않기때문에 한국어 NLP에서 지양하는 부분이기 때문이다. 게다가 대부분의 경우 영어에 비해 띄어쓰기가 올바르게 진행되지 않기때문에 의미없는 코퍼스가 많다.

2. 정제 및 정규화

  • 정의 : 토큰화 작업 전, 후 용도에 맞게 텍스트데이터를 정제 및 정규화작업을 꼭 하게 된다. 왜냐하면, 가지고 있는 코퍼스로부터 노이즈를 제거하여 오차율을 줄이기 위해, 같은 의미여도 표현이 다른 단어들을 통합시켜 한 단어로 만들어 다음 단계를 수월하게 진행하기 위해서 정제 및 정규화 단계를 진행한다.
  • 규칙에 기반하여 단어 통합 :주로 대소문자 통합/불필요한 단어 제거를 통해 정규화한다.
  • 노이즈데이터 제거: 너무 적게 등장해 자연어처리에 도움이 되지않는 단어라던지, 길이가 짧아 의미가 없는 단어들. 추가로 한국어의 경우 평균단어 길이는 3-4자라고 한다 하지만 영어의 경우 평균적으로 7-8자인데, 한국어는 단어속 함축적 의미가 들어있어 짧게 끊어지지만 영어의 경우 짧은 단어는 대게로 의미를 갖지 못하는 단어들이기에 이를 제거하는게 효율적이다.

3. 어간 추출 및 표제어 추출

  • 표제어에 대해: 한국어에서 표제어라고 하면, 기본 사전형 단어 정도의 의미라고 한다. 표제어 추출은 단어들이 서로 다른 형태를 가지더라도 그 뿌리단어를 찾아 단어의 개수를 줄이는 역할을 한다. 예를들어 are, am, is는 모두 다른 형태이지만 이 뿌리는 be동사라고 말할수있듯이 단어의 개수를 줄이는 것이다. 그 중 가장 섬세한?방법은 형태학적 파싱이라고 한다.
  • 형태학적 파싱: 형태학적 피싱은 어간과 접사, 이 두가지 요소로 분리하는 작업을 하게 된다. 만약 cats라는 단어에 대해 형태학적 파싱을 하게 된다면 'cat' 's'로 분리되는 것을 볼수있을것이다. 추가로 fox라는 단어가 들어왔을때는 분리하지 않을것이다. 왜냐 ? fox는 독립형태소로 cat을 더이상 분리하지 않는 이유와 같다.
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도 있지만 추후에 업데이트를 통해 올리도록 하겠습니다. 아직 제대로 이해하지 못해서 정확하게 남기는것은 좀 무리인것 같습니다.

4. 불용어

  • 유의미한 단어 토큰만 남기기 위해 의미없는 단어 토큰을 제거하는 작업을 필요로 하여 불용어를 확인하고 그것을 제거하는 작업을 하는데 우선 불용어를 확인하기 위해 NLTK를 이용하여 샘플 데이터를 제거해보았다.
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'과 같이 긍정인지 부정인지를 명확히 나타내는 단어를 지우면 정확한 의도를 파악하는데 과연 문제가 없을까?에 대한 의문을 가지게 되었다.]!!

5. 정규 표현식

  • 파이썬의 정규표현식 모듈 re를 사용하여 특적 규칙이 있는 텍스트를 빠르게 정제할수있습니다. 정규표현식 관련 기본 문법과 모듈함수에 대한 내용은 너무 나도 많기에 주차에 상관없이 따로 하나의 에피소드형식으로 만드는게 나을것같다.

6. 정수 인코딩

  • 컴퓨터가 자료를 더 빠르고 정확하게 처리하기에 앞서 텍스트에 숫자를 부여하여 숫자를 통해 계산하는 방식으로 전처리 작업이 진행되게 되는데 이때 텍스트에 숫자를 랜덤으로 부여하는 경우도 있지만 보통은 빈도수가 많은것을 정렬시켜 단어 빈도수를 기준으로 숫자를 부여하게 된다.

6-1) dictionary 사용

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에 단어를 입력하면 빈도수를 리턴하게 된다. 빈도수가 높은 순으로 다시 정렬 할 수도 있다.

6-2) counter 사용

  • 조금 더 쉽게 정수 인코딩을 하기 위해 아래의 방법이 있다.
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']]

6-3) NLTK의 FreqDist 사용

  • NLTK에서 지원하는 빈도수 계산 도구, counter()와 동일한 방법으로 사용할수있음.
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)]

*높은 빈도수일 경우 낮은 인덱스 번호 부여.

6-4) enumerate 이해

  • 순서가 있는 자료형을 입력받아 그 순서에 맞게 인덱스를 함께 다시 반환.
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

7. 패딩(padding)

  • 여러 문장의 길이를 모두 같은 길이의 문장으로 만드는 작업. (하나의 행렬로 한번에 처리(병렬연산)를 위해 여러문장의 "길이" 를 임의로 같게 함)
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)])

8. 원-핫 인코딩

  • 단어 집합의 크기를 벡터화 하는 작업, 원핫 인코딩은 두가지 과정으로 이루어진다. 첫째, 정수 인코딩 실행(고유한 정수 부여). 둘째, 표현하고 싶은 단어에 고유한 정수를 인덱스로 간주하고 해당 위치에 1을 부여. 다른 단어의 인덱스 위치에는 0을 부여.

*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' 등이 있다.

0개의 댓글