코딩일기

[NLP] 1. Text Preprocessing(feat. Tokenization, Cleaning, Normalization, Stopwords, Building Vocab, Integer Encoding, Padding, Vectorization, 텍스트 전처리 과정) 본문

Code/딥러닝(NL)

[NLP] 1. Text Preprocessing(feat. Tokenization, Cleaning, Normalization, Stopwords, Building Vocab, Integer Encoding, Padding, Vectorization, 텍스트 전처리 과정)

daje 2022. 2. 13. 19:42
728x90
반응형

 

안녕하십니까 다제 입니다. 

오늘부터 시리즈로 NLP에 대해 전반적으로 공부를 진행해보고자 합니다. 

 

해당 시리즈는 위키독스 딥러닝을 이용한 자연어 처리 입문 오프라인 강의, Andrew Ng 교수님 강의 내용을 함께 혼용하여 정리한 내용입니다. 

 

요즘 많은 강의를 듣고 코딩을 하면서 "NLP가 과연 무엇일까?"에 대한 질문을 해보았습니다. 

제가 생각하는 NLP는 현재까지 언어를 수학으로 이해하는 과정이다. 라고 저만의 정의를 지어보았습니다. 

향후 NLP에 대한 개인적인 견해는 변동될 수 있지만 지금까지는 이렇게 이해하고 있습니다. 

 

오늘은 일반적인 텍스트 전처리가 어떻게 이루어지고 있는지, 한국어에는 어떠한 전처리가 추가될 수 있는지도 함께 알아보도록 하겠습니다. 

 


 

** 목차 ** 

1. Tokenization 

2. Cleaning(&stopwords)

3. Normalization(Stemming & Lemmatization)

4. Encoding 

5. Padding 

6. Vectorization

7. 한국어에서 추가될 수 있는 전처리 

 -. 형태소 분석기 

 -. 단어띄어쓰기 

 -. 맞춤법 검사  

 

 

1. Tokenization(토큰화)

 1) 정의 

  -. 말뭉치(Corpus, 문장, 문서 등)로부터 토큰(문법적으로 더 이상 나눌 수 없는 언어요소)으로 분리하는 것을 말합니다. 

  -. 일반적으로는 단어 단위로 나누는 토큰화를 지칭하나, 문서를 문장으로, 문장을 단어로 분리하는 것도 토큰화의 큰 범주에 속하게 됩니다. 

  -. 여러가지 토큰나이저(Tokenizer)가 있으며, 내가 하고자 하는 분석에 맞게 토큰나이저를 비교하여 선택해야한다. 

 

2) 토큰화 시, 주의해야할 점 

  -. 구두점이나 특수 문자를 단순 제외해서는 안된다. ex) 8.19mm의 경우 하나의 단위로 인식 

  -. 영어의 경우, '(어포스토로피) 등으로 표현되는 줄임말이 있는 경우 주의가 필요함 

 

 3) 한국어의 토큰화가 어려운 이유 

  3-1) 허사 

  -. 한국어의 형태적 특징은 단어 풍부한 어휘 생성이 가능한 단어 형성법과 다양한 허사가 발달된 교착어라는 점입니다.

  • 허사란? 단어가 그 본래 목적이 퇴화되어 자립적으로 쓰이지 않고, 다른 단어의 문법적, 의미적 보충 역할을 하는 단어를 의미함

  -. 허사는 조사, 어미, 접사로 구성되어 있습니다.

  • 조사 : 체언(주어, 목적어, 보어 자리에 오는 명사, 대명사, 수사) 뒤에 붙어 체언의 문장 성분과 성격을 결정함
  • 어미 : 용언에 여러가지 문법적, 의미적 형태소로 붙어 단어의 뜻을 구체적이고 풍부하게 함
  • 접사 : 접두사와 접미사로 어근에 첨가되어 새로운 단어를 생성함

  -. 즉, 이러한 허사로 인해 "그"라는 단어가 "그는", "그가", "그를", "그와", "그에게" 등의 다양한 의미를 갖게 됩니다. 

  -. 이를 숫자로 변형하게 되면 컴퓨터는 다 다른 단어로 인식을 하게 됩니다. 그래서 한국어는 형태소 분석을 반드시 해주어야 합니다. 

 

 3-2) 관형어와 관형사

  -. 우리가 알고 있는 한국어 또 다른 특징은 동사에 "ㄴ"을 붙히면 형용사로 변한다는 특징이 있습니다. 

  -. 우리는 이것을 어려운 말로 관형사에 관형어적 전성어미를 붙히면 관형어가 된다라고 이야기합니다. 

    ex) ex_ 아름답다 → 아름다운, 달리다 → 달리는

 -. 조금 더 풀어서 설명을 드려보겠습니다. 영어는 형용사가 명사를 수식 합니다. 한국어에서는 관형어가 명사를 수식 합니다. 그런데 관형어는 관형어적 전성어미와 관형사로 나눌 수 있습니다. 다른 말로는 관형사에 관형어적 전성어미를 붙히면 관형어가 됩니다.

그렇다면 이게 왜 중요한가? 라는 생각을 해보아야 합니다.
이것을 어떻게 인공지능 학습에 적용해야하는 것인가? 하는 생각에 잠겼습니다.
먼저 인공지능을 학습하기 위해 만들어지는 순서를 살펴보겠습니다.

문장 → 단어 → 형태소 → 후처리(관형어 조합) → 훈련 
큰 흐름으로 살펴보면 위와 같습니다.

아니? 왜 다 분해를 하고 다시 후처리를 해서 관형어를 조합하는지에 대한 의문이 드실 수 있습니다.
이렇게 관형사와 관형어적 전성어미를 분리하지 않고 관형어를 찾을 수 없습니다.

관형어 ↔ 용언
관형사 ↔ 동사

기술적인 한계(의존 구문 분석)에 봉착하였습니다. 
이에, 이를 나누고 붙히는 번거로운 작업이 이루어지고 있습니다. 
사실 영어는 방대한 데이터를 기반으로 self-attention을 통해 이러한 문제를 해결하고 있지만, 
한국어는 아직까지 self-attention을 통해 문맥적인 정보를 정확히 파악하지 못하는 문제점을 앉고 있습니다.
print('토큰화 :',text_to_word_sequence("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']

  -. 또 다른 한국어의 특징은 생각보다 띄어쓰기가 잘 지켜지지 않습니다. 그 이유에는 다양한 견해가 있지만, 띄어쓰기가 잘 안돼어 있어도 읽는데 크게 불편함이 없다는 점입니다. 

 

 

2. 정제(Cleaning) & 불용어 처리(Stopwods)

 1) 정제(Cleaning)

  -. 정제는 큰 의미로 대소문자 통합, 불용어처리로 볼 수 있습니다. 

  -. 같은 단어라도 컴퓨터의 입장에서는 He와 he는 다른 단어로 인식되기 때문입니다. 

  -. 우리가 영화리뷰 데이터를 크롤링하였다고 했을 때, ㅋㅋㅋㅋ, 졸라, ㅎㅎㅎㅎ, !!! 등의 의미 없는 단어들이 다수 포함될 수 있습니다. 이러한 단어는 한 두개를 제외하고는 제외하는 방법, 아에 제거하는 방법 등을 고려해볼 수 있습니다. 

  -. 또한, 분석에 필요하지 않다 생각되는 단어들을 불용어로 처리하여 데이터의 분포를 균일화하는 작업이 필요합니다. 

 

 2) 불용어(Stopword)

  -. 분석을 하는데 의미없다고 생각되는 단어들을 이야기합니다. 

  -. 또는 들어가서는 안돼는 단어들을 제작자가 직접 제작하기도 합니다. 

  -. 영어에서 불용어가 어떻게 되어 있는지를 한번 코드로 살펴보겠습니다. 

from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize 
from konlpy.tag import Okt

stop_words_list = stopwords.words('english')
print('불용어 개수 :', len(stop_words_list))
print('불용어 10개 출력 :',stop_words_list[:10])

불용어 개수 : 179
불용어 10개 출력 : ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're"]
example_sent = "This is a sample sentence. kaka, yoyo"
word_tokens = word_tokenize(example_sent)

filtered_sentence1 = [w for w in word_tokens if not w in stop_words_list]
print(filtered_sentence1)
# 출력 : ['This', 'sample', 'sentence', '.', 'kaka', ',', 'yoyo']

stop_words_list.extend(["kaka", "yoyo"])
print(stop_words_list[-3:-1]) 
# 출력 : ['yoyo', 'kaka']

filtered_sentence1 = [w for w in word_tokens if not w in stop_words_list]
print(filtered_sentence1)
# 출력 : ['This', 'sample', 'sentence', '.', ',']
# 불용어에 추가한 단어가 출력되지 않는 것을 볼 수 있다.

 

 

3. Normalization(Stemming & Lemmatization)

구분 Stemming Lemmatization
am am be
the going the go  the going
having hav have

  -. Stemming, Lemmatization이 어떤 차이가 있는지를 명확히 보여주는 표롤 살펴본 후 개념을 살펴보는 것이 도움이 되기에 표를 준비해 보았습니다. 

 

1) 어간 추출(stemming) 

-.  코퍼스에 있는 단어의 개수를 줄일 수 있는 기법 -> 이러한 특징 때문에 Nomaralization이라고도 함 

-. 어간 추출을 수행한 결과는 품사 정보가 보존되지 않음 -> 사전에 존재하지 않는 단어일 경우가 많다. 

from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
stemmer = PorterStemmer()
text = "This was not the map we found in Billy Bones's chest, but an accurate copy, complete in all things--names and heights and soundings--with the single exception of the red crosses and the written notes."
words = word_tokenize(text)
print([stemmer.stem(w) for w in words])

# 결과값 
['thi', 'wa', 'not', 'the', 'map', 'we', 'found', 'in', 'billi', 'bone', "'s", 'chest', ',', 'but', 'an', 'accur', 'copi', ',', 'complet', 'in', 'all', 'thing', '--', 'name', 'and', 'height', 'and', 'sound', '--', 'with', 'the', 'singl', 'except', 'of', 'the', 'red', 'cross', 'and', 'the', 'written', 'note', '.']

 

2) 표제어 추출(lemmatization)

 -.  코퍼스에 있는 단어의 개수를 줄일 수 있는 기법 

-. 문맥을 고려하며, 수행했을 때의 결과는 해당 단어의 품사 정보를 보존 

-. 단어들로부터 표제어를 찾아가는 과정

-. 단어들이 다른 형태를 가지더라도, 그 뿌리 단어를 찾아가서 단어의 개수를 줄일 수 있는지 판단 

-. ex) is, are, am -> be로 변환 

-. 형태학적 파싱 : 어간(단어의 의미를 담고 있는 단어의 핵심 부분)과 접사(단어에 추가적인 의미를 주는 부분)를 분리하는 작업 

from nltk.stem import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
words = ['policy', 'doing', 'organization', 'have', 'going', 'love', 'lives', 'fly', 'dies', 'watched', 'has', 'starting']
print([lemmatizer.lemmatize(w) for w in words])


#결과값
# ['policy', 'doing', 'organization', 'have', 'going', 'love', 'life', 'fly', 'dy', 'watched', 'ha', 'starting']

 

 

4. Encoding 

  -. 너무나 당연한 이야기지만, 컴퓨터는 사람보다 정확하고 빠르게 숫자를 처리할 수 있습니다. 

  -. 또한, 텍스트보다는 숫자를 더 빠르게 처리할 수 있습니다. 

  -. 이에, 우리는 학습 및 훈련 시키고자 하는 데이터를 숫자로 변환해주는 작업을 거쳐야합니다. 

  -. 텍스트를 숫자로 바꾸어주는 작업은 위와 같은 기준으로 분류를 해볼 수 있습니다. 

  -. 당연히 신경망을 사용하지 않는 경우, 문맥적 정보를 파악할 수 없겠죠? 저기에서 의문을 가지시면 안돼요~

  -. 여기에서는 Integer Encoding과 One-Hot Encoding만 살펴보도록 하겠습니다.(다른 친구들은 별도 포스팅 예정)

 

 

1) 정수인코딩(Integer Encoding)

 -. 정수인코딩을 할 수 있는 방법은 크게 4가지가 있습니다. 

 -. 4가지 방법 : dictionary, Counter, NLTK FreqDist,  Keras의 텍스트 전처리

 

 1-1) dictionary 사용하기

from nltk.tokenize import sent_tokenize
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords

text = "I thank my God every time I remember you. \
In all my prayers for all of you, \
I always pray with joy because of your partnership in the gospel from the first day until now, \
being confident of this, that he who began a good work in you will carry it on to completion until the day of Christ Jesus."

sentences = sent_tokenize(text)
print(sentences)

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()
    # stop_words에 없으면 넣기 
    if word not in stop_words:
      # 길이가 2보다 큰 단어들만 넣을 것이다 
      if len(word) > 2: 
        result.append(word)
        # vocab없다면 dict로 만들어 주고 
        if word not in vocab :
          vocab[word] = 0 
        # 있다면 1을 추가해주는 작업을 해주세요   
        vocab[word] += 1
  preprocessed_sentences.append(result) 

print(preprocessed_sentences)
# 결과 
# [['thank', 'god', 'every', 'time', 'remember'],
   ['prayers', 'always', 'pray', 'joy', 'partnership', 'gospel', 'first', 'day', 'confident', 'began', 'good', 'work', 'carry', 'completion', 'day', 'christ', 'jesus']]

print(vocab)
# 결과 
{'thank': 1, 'god': 1, 'every': 1, 'time': 1, 
'remember': 1, 'prayers': 1, 'always': 1, 'pray': 1, 'joy': 1, 
'partnership': 1, 'gospel': 1, 'first': 1, 'day': 2, 'confident': 1, 'began': 1, 
'good': 1, 'work': 1, 'carry': 1, 'completion': 1, 'christ': 1, 'jesus': 1}

 

 1-2) Counter 사용하기

from collections import Counter 
all_words_list = sum(preprocessed_sentences, [])
print(all_words_list)

vocab = Counter(all_words_list)

vocab_size = 5
vocab = vocab.most_common(vocab_size)
vocab 
# 결과 
# ['thank', 'god', 'every', 'time', 'remember', 'prayers', 'always', 'pray', 'joy', 'partnership', 'gospel', 'first', 'day', 'confident', 'began', 'good', 'work', 'carry', 'completion', 'day', 'christ', 'jesus']
# [('day', 2), ('thank', 1), ('god', 1), ('every', 1), ('time', 1)]

word_to_index = {}

i = 0
for (word, frequency) in vocab :
  i += 1 
  word_to_index[word] = i
print(word_to_index )
# {'day': 1, 'thank': 2, 'god': 3, 'every': 4, 'time': 5}

 

 1-3) NLTK FreqDist 사용하기 

from nltk import FreqDist 
import numpy as np 

vocab = FreqDist(np.hstack(preprocessed_sentences))


vocab_size = 5
vocab = vocab.most_common(vocab_size)

word_to_index = {word[0] : index + 1 for index, word in enumerate(vocab)}
print(word_to_index)

 

 1-4) Keras의 텍스트 전처리 사용하기 

from tensorflow.keras.preprocessing.text import Tokenizer 
tokenizer = Tokenizer()
tokenizer.fit_on_texts(preprocessed_sentences)

print(tokenizer.word_index)
print(tokenizer.index_word)
print(tokenizer.word_counts)

# 결과 
# {'day': 1, 'thank': 2, 'god': 3, 'every': 4, 'time': 5, 'remember': 6, 'prayers': 7, 'always': 8, 'pray': 9, 'joy': 10, 'partnership': 11, 'gospel': 12, 'first': 13, 'confident': 14, 'began': 15, 'good': 16, 'work': 17, 'carry': 18, 'completion': 19, 'christ': 20, 'jesus': 21}
# {1: 'day', 2: 'thank', 3: 'god', 4: 'every', 5: 'time', 6: 'remember', 7: 'prayers', 8: 'always', 9: 'pray', 10: 'joy', 11: 'partnership', 12: 'gospel', 13: 'first', 14: 'confident', 15: 'began', 16: 'good', 17: 'work', 18: 'carry', 19: 'completion', 20: 'christ', 21: 'jesus'}
# OrderedDict([('thank', 1), ('god', 1), ('every', 1), ('time', 1), ('remember', 1), ('prayers', 1), ('always', 1), ('pray', 1), ('joy', 1), ('partnership', 1), ('gospel', 1), ('first', 1), ('day', 2), ('confident', 1), ('began', 1), ('good', 1), ('work', 1), ('carry', 1), ('completion', 1), ('christ', 1), ('jesus', 1)])

 

2) 원핫인코딩(One-Hot Encoding)

  -. 원핫인코딩은 머신러닝에서 많이 다루어보았기 때문에 익숙할 것이라고 생각합니다. 

  -. 그러나, 언어를 인코딩하는 과정에서 차원이라는 개념이 추가되기 때문에 그 부분도 함께 다루어서 앞으로 공부할 때 혼동이 없게 하고자 합니다. 

  -. 원핫이코딩은 단어 집합의 크기를 벡터의 차원으로 하고, 표현하고 싶은 단어의 인덱스에 ㅂ의 값을 부여하고 다른 인덱스에는 0을 부여하는 단어 베터 표현 방식입니다. 이렇게 표현된 벡터를 당연히 원핫벡터(One-Hot Vector)라고 하겠죠?

  -. 코드로 바로 살펴보겠습니다. 

  2-1) Keras를 사용하지 않는 경우 

from konlpy.tag import Okt 

okt = Okt()
tokens = okt.morphs("아메리카노를 좋아합니다.")
print(tokens)

word_to_index = { word : index for index, word in enumerate(tokens)}
print(word_to_index)

from keras_preprocessing.text import one_hot
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)

 

  2-2) Keras를 사용한 경우 

from tensorflow.keras.preprocessing.text import Tokenizer 
from tensorflow.keras.utils import to_categorical

text = "아메리카노를 좋아합니다."

tokenizer = Tokenizer()
tokenizer.fit_on_texts([text])
print(tokenizer.word_index)

# 결과 : {'아메리카노를': 1, '좋아합니다': 2}
# 주의 : fit_on_texts를 할 때 text를 리시트로 감싸주지 않으면 아래와 같은 결과가 나오니 주의해야합니다.
# {'아': 1, '메': 2, '리': 3, '카': 4, '노': 5, '를': 6, '좋': 7, '합': 8, '니': 9, '다': 10}

 

 

5. Padding 

 -. 이제 패딩에 대해서 알아보도록 하겠습니다. 

 -. 우리는 자연어 처리를 진행하다보면 각기 다른 길이를 가진 문장들을 만나게 됩니다. 

 -. 아시는 분들은 아시겠지만, 딥러닝에서 병렬 연산 처리를 하기 위해서는 행렬 곱을 사용하게 되기에 행렬의 크기를 맞춰주는 작업은 반드시 필요합니다. 좀 더 풀어서 설명해보자면, 딥러닝의 입력으로 문장이 사용되기에 동일한 크기로 입력 크기를 조절해주어야 한다는 말입니다. 

 -. Padding은 max_len보다 길이가 짧은 문장들에게 0을 추가해 주어 길이를 동일하게 맞추는 작업입니다. 

 -. Padding을 하는 방법도 여러가지가 있는데 Numpy와 Keras를 이용하는 방법을 간단히 살펴보겠습니다. 

#Numpy를 사용하는 방법 
import numpy as np
from tensorflow.keras.preprocessing.text import Tokenizer

tokenizer = Tokenizer()
tokenizer.fit_on_texts(preprocessed_sentences)
encoded = tokenizer.texts_to_sequences(preprocessed_sentences)
print(encoded)
#결과 : [[2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12, 13, 1, 14, 15, 16, 17, 18, 19, 1, 20, 21]]

max_len = max(len(item) for item in encoded)
print('최대 길이 :',max_len)
# 결과 : 최대 길이 : 17

for sentence in encoded:
    while len(sentence) < max_len:
        sentence.append(0)

padded_np = np.array(encoded)
padded_np
# 결과 
array([[ 2,  3,  4,  5,  6,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
         0],
       [ 7,  8,  9, 10, 11, 12, 13,  1, 14, 15, 16, 17, 18, 19,  1, 20,
        21]])
# Keras를 사용하는 방법

from tensorflow.keras.preprocessing.sequence import pad_sequences
encoded = tokenizer.texts_to_sequences(preprocessed_sentences)
padded = pad_sequences(encoded)
padded
# 위와 동일한 결과가 출력됨

 

 6. Vectorization

 -. 단어띄어쓰기  : PyKoSpacing 참조 

 -. 맞춤법 검사 : Py-Hanspell 참조 

 -. 형태소 분석기 : Mecab 등 참조 

 -. 신조어 문제 : soynlp 참조 

 -. 위 패키지에 대한 설명은 해당 링크 참조 부탁드립니다. 

 -. 이러한 패키지를 실무에 적용할 수 있는지에 대한 여부는 데이터 형식을 잘 따져보아야 합니다. 

 -. 해당 패키지가 제공하는 규칙과 내가 시도해야하는 분석이 유사한 방향이다라고 하면 사용 여부를 검토해 볼 수 있지만, 고민 없이 해당 패키지를 사용하는 것은 바람직하지 않다고 생각합니다. 

 


 

그 외 정규식에 관련된 자료는 제가 별도로 포스팅을 해놓았습니다.

다른 곳에 잘 나와 있는 링크도 함께 전달드리니 참고 부탁드립니다. 

-. https://wikidocs.net/21703

-. 2022.02.13 - [Code/기타] - 정규식 파해쳐보기

 

 

부족하지만, 공부한 내용을 함께 나누고 정리해보았습니다. 

계속 빡공 진행하시죠! 화이팅 입니다!

728x90
반응형
Comments