자연어처리(NLP) 텍스트전처리/벡터화

생각하는 마리오네트·2021년 8월 19일
0

딥러닝

목록 보기
2/5
post-thumbnail

<기본용어 >

말뭉치(Corpus) : 특정한 목적을 가지고 수집한 텍스트 데이터
문서(Document) : 문장(Sentence)들의 집합
문장(Sentence) : 여러 개의 토큰(단어, 형태소)으로 구성된 문자열, 마침표, 느낌표 등의 기호로 구분
어휘집합(Vocabulary) : 코퍼스에 있는 모든 문서, 문장을 토큰화 하고 중복을 제거한 토큰의 집합
토큰(Token) : 의미를 가진 가장 작은 단위
토큰화(Tokenization) : 토큰단위로 나누는 것


자연어 처리(Natural Language Processing, NLP)

자연어(사람들이 일상적으로 사용하는 언어를 인공적으로 만들어진 언어인 인공어와 구분하여 부르는 개념)를 컴퓨터로 처리하는 기술


벡터화(Vectorize)

자연어를 컴퓨터가 이해할 수 있도록 벡터로 만들어주는 것을 뜻하며 자연어를 벡터화를 하는 방법은 크게 두가지로 나눌 수 있다.

1. 등장 횟수 기반의 단어 표현(Counte-based Representation) : 단어가 문서(혹은 문장)에 등장하는 횟수를 기반으로 벡터화 하는 방법
	- Bag-of-Words
	- TF-IDF
    
2. 분포 기반의 단어 표현 ( Distributed Representation) : 타겟 단어 주변에 있는 단어를 벡터화 하는 방법
	- Word2Vec
    	- Glove
    	- fastText

텍스트 전처리(Text Preprocessing)

자연어 처리 에서는 텍스트 전처리 과정이 절반 이상을 차지하는 중요한 과정이다.

1. 차원의 저주(Curse of Dimensionality)

  • 차원이 늘어날수록 설명력이 떨어진다.
  • 횟수 기반의 벡터 표현에서는 전체 말뭉치에 존재하는 단어 종류가 데이터셋의 Feature, 즉 차원이 된다. 따라서 단어의 종류를 줄여야 차원을 줄일 수 있다.

2. 대,소문자 통일 + 정규표현식(Regex)사용

3. SpaCy라이브러리를 사용하여 토큰화

# 자연어 처리를 위한 모듈과 모델 가져오기
import spacy
from spacy.tokenizer import Tokenizer
nlp = spacy.load("en_core_web_sm")
tokenizer = Tokenizer(nlp.vocab)

# 토큰화를 위한 파이프라인 구성
tokens = []
for doc in tokenizer.pipe(df['reviews.text']):
	doc_tokens = [re.sub(r"[^a-z0-9]", "", token.text.lower()) for token in doc]
	tokens.append(doc_tokens)
    
df['tokens'] = tokens
df['tokens'].head()


>>>
0    [though, i, have, got, it, for, cheap, price, ...
1    [i, purchased, the, 7, for, my, son, when, he,...
2    [great, price, and, great, batteries, i, will,...
3    [great, tablet, for, kids, my, boys, love, the...
4    [they, lasted, really, little, some, of, them,...
Name: tokens, dtype: object

< 코드 설명 >
tokenizer.pipe(text) : text를 토큰화한다.
re.sub(정규표현식, 대상 문자열, 치환 문자)
위의 코드에서는 Tokenizer(text)이런식으로 했지만 nlp(text) 로도 가능하다.

4. 불용어 처리

'i', 'and', 'of' 와 같은 단어들은 리뷰텍스트 데이터 관점에서 아무런 의미가 없다. 이러한 단어들을 'Stop words(불용어)'라고 한다.

대부분의 NLP라이브러리는 접속사, 관사, 부사, 대명사, 일반동사등을 포함해서 불용어를 내장하고 있다.s
spacy를 통해 기본으로 제공하는 불용어를 알아볼 수 있다.

print(nlp.Defaults.stop_words)
  • 불용어를 제외하고 토크나이징을 해보았다.
tokens = []

# 토큰에서 불용어 제거, 소문자화 후 업데이트
for doc in tokenizer.pipe(df['reviews.text']):
	doc_tokens = []
    
    # 토큰이 불용어와 구두점이 아닌경우 소문자로 변경후 적용
    for token in doc:
    	if (token.is_stop == False) & (token.is_punct == False):
        	doc_tokens.append(token.text.lower())
            
    tokens.append(doc_tokens)
    
df['tokens'] = tokens
df['tokens'].head()



tokens = []
# 토큰에서 불용어 제거, 소문자화 하여 업데이트
for doc in tokenizer.pipe(df['reviews.text']):
    doc_tokens = []

    # A doc is a sequence of Token(<class 'spacy.tokens.doc.Doc'>)
    for token in doc:
        # 토큰이 불용어와 구두점이 아니면 저장
        if (token.is_stop == False) & (token.is_punct == False):
            doc_tokens.append(token.text.lower())

0    [got, cheap, price, black, friday,, fire, grea...
1    [purchased, 7", son, 1.5, years, old,, broke, ...
2    [great, price, great, batteries!, buying, anyt...
3         [great, tablet, kids, boys, love, tablets!!]
4    [lasted, little.., (some, them), use, batterie...
Name: tokens, dtype: object
  • 불용어를 커스터마이징 하기
    다루어야 하는 데이터에 따라 불용어를 커스터마이징 해야할 경우가 있다. 중요하지는 않지만 많이 나오는 몇몇
    단어들을 불용어로 추가해보겠다.
STOP_WORDS = nlp.Defaults.stop_words.union(['batteries','I', 'amazon', 'i', 'Amazon', 'it', "it's", 'it.', 'the', 'this'])

tokens = []

for doc in tokenizer.pipe(df['reviews.text']):
	doc_tokens = []
   
	for token in doc:
            if token.text.lower() not in STOP_WORDS:
            	doc_tokens.append(token.text.lower())
        tokens.append(doc_tokens)
df['tokens'] = tokens

5. 통계적 트리밍(Trimming)

불용어를 직접적으로 제거하는 대신 통계적인 방법을 통해 말뭉치 내에서 너무 많거나, 너무 적은 토큰을 제거하는 방법도 있다.

6. 어간 추출(Stemming)과 표제어 추출(Lemmatization)

위에서 토큰화 된 단어들을 보면, 조금 더 수정이 필요한 부분이 보인다.
예를 들어 'batteries'와 'battery'를 보면 이 둘은 어근(root)이 같은 다어이다.
이런 단어는 어간 추출(stemming)이나 표제어 추출(lemmatization)을 통해 정규화(Normalization)해준다.

  • 어간 추출(Stemming)
    단어의 의미가 포함된 부분으로 접사등이 제거된 형태, 어근이나 단어의 원형이 같지 않을 수 있다.
    예를 들어, argue, argued, arguing, argus의 어간은 단어들의 뒷 부분이 제거된 argu가 어간이다.
    어간 추출은 'ing', 'ed', 's' 등과 같은 부분을 제거하게 된다.
    어간 추출의 경우 Spacy는 Stemming을 제공하지 않고 Lemmatization만 제공하기 때문에 nltk를 사용해서 Stemming을 해볼 수 있다.
from nltk.stem import PorterStemmer
ps = PorterStemmer()
words = ['wolf', 'wolves']

for word in words:
	print(ps.stem(word))

>>>
wolf
wolv

Steeming에서 해본 Porter 알고리즘은 단어의 끝 부분을 자르는 역할을 한다. 그래서 사전에 없는 단어가 많이 나오게 된다. 예를들어 batteries의 경우 batteri라고 출력이 되는경우가 있따.
이렇게 조금 이상한 부분이 있긴하지만 현실적으로 사용하기에 Stemming은 성능이 그렇게 나쁘지는 않다.
알고리즘이 간단하여 속도가 빠르기때문에 속도가 중요한 검색 분야에서 많이 사용하기도 한다.

  • 표제어 추출(Lemmatization)
    표제어 추출(Lemmatization)은 어간추출보다 체계적입니다.
    단어들은 기본 산전형 단어 형태인 Lemma(표제어)로 변환됩니다.
    명사의 복수형은 다수형으로, 동사는 모두 타동사로 변환됩니다.
    이런식으로 단어에서 표제어로 찾아가기 때문에 Stemming보다는 많은 연산을 필요로 한다.
lem = 'The social wolf. Wolves are complex.'
nlp = spacy.load('en_core_web_sm')
doc = nlp(lem)

# 그냥 추출한것과 Lemma로 추출된것 비교
for token in doc:
	print(token.text, '  ', token.lemma_)
>>>
The    the
social    social
wolf    wolf
.    .
Wolves    wolf
are    be
complex    complex
.	 .

Stemming과 Lemmatization의 출력물을 비교해 보면 Stemming의 경우 wolf -> wolf, wolves -> wolv로 변형되었고, Lemmatization에서는 wolf -> wolf, wolves -> wolf로 변형된 차이가 있다.

Lemmatization과정을 함수로 만들어 보기

def get_lemmas(text):
	lemmas = []
	doc = nlp(text)
    
    	for token in doc:
        	if ((token.is_stop == False) and (token.is_punct == False)) and (token.pos_ != 'PRON'):
            		lemmas.append(token.lemma_)
        return lemmas

< 코드 보충 >
token.pos_ : 품사를 나타낸다.
PRON : 명사


벡터화 첫번째, 등장 횟수 기반의 단어 표현(Count-based Representation)

텍스트 문서를 벡터로 표현해 보자.

텍스트를 분석하기 위해서는 컴퓨터가 계산할 수 있도록 수치정보로 변환하는 벡터화 과정이 필요하다.
그 중 등장 횟수기반의 단어표현은 단어가 특정 문서(문장)에 들어있는 획수를 바탕으로 해당 문서를 벡터화 한다.
대표적으로 두가지 방법이 있다. (Bag-of-Words 와 (TF, TF-IDF))

문서-단어 행렬(Document-Term Matrix, DTM)
: 벡터화된 문서는 문서-단어 행렬의 형태로 나타난다. 각 행에는 문서, 열에는 단어가 있는 행렬이다.

Word_1Word_2Word_3Word_4Word_5Word_6
Docu_1120100
Docu_2000111
Docu_3100101

1. Bag-of-Words(BOW)

BOW는 가장 단순한 벡터화 방법중 하나이다. 문서(혹은 문장)에서 문법이나 단어의 순서 등을 무시하고 단순히 단어의 빈도만 고려하여 벡터화 하는 방법이다.

단점 :
단어가 많아질 수록 차원이 무한하게 늘어나서 계산량이 높아져서 메모리를 많이 사용하게 된다.
많이 출연한 단어의 경우 힘이 좋아지지만, 적게 출연하면 너무 약하다.
단어의 순서를 철저하게 무시한다. (home run 과 run home 둘은 문장의 뜻은 다르지만 문장의 순서(문맥)가 무시된다.
보지못한 단어의 경우는 처리를 못한다.(오타, 줄임표현)

# CountVectorizer를 이용하여 Bag-of-Words예제
# 필요한 모듈 불러오기

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.neighbors import NearestNeighbors
from sklearn.decomposition import PCA

import spacy
nlp = spacy.load("en_core_web_sm")
text = "~~~~"
# 토큰화 해서 단어확인
doc = nlp(text)
print([token.lemma_ for token in doc if (token.is_stop !=True) and (token.is_punct != True)])
from sklearn.feature_extraction.text import CountVectorizer

# 문장으로 이루어진 리스트를 저장한다.
sentences_lst = text.split('wn')

# CountVectorizer를 변수에 저장한다.
vect =CountVectorizer()

# 어휘 사전을 생성한다.
vect.fit(sentences_lst)

# text를 DTM으로 변환(transform)
dtm_count = vect.transform(sentences_lst)
# dtm_count 출력해 보기
dtm_count.todense() # .todense()를 넣어줘야 우리가 아는 matrix형태로 출력이된다.

# vect에 맵핑된 인덱스 정보확인
vect.vocabulary_
# 아마존 리뷰데이터 CountVectorizer 적용하기
from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer(stop_words = 'english', max_features = 100)

# Fit후 dtm을 만들어준다.
dtm_count_amazon = count_vect.fit_transform(df['reviews.text'])

dtm_count_amazon = pd.DataFrame(dtm_count_amazon.todense(), columns=count_vect.get_feature_names())
dtm_count_amazon

2. TF-IDF(Term Frequency - Inverse Document Frequency)

각 문서마다 중요한 단어와 그렇지 않은 단어가 있습니다.
학생시절 급식표를 생각해보면 이해가 쉬울거같습니다. 우리는 학창시절 친구들에게 "오늘 밥뭐야??" 라고하면
매일나오는 우유나 흰쌀밥, 국 같은 음식 대신 돈까스, 생선까스, 스파게티 등등 매일 나오는 반찬이 아닌 대표할 수 있는 메뉴를 말하게 됩니다. 이렇게 말하는 이유는 다른 날에는 잘 등장하지 않는 메뉴이기 때문입니다. 그렇기 때문에 그날을 대표할 수 있는 메뉴가 되는것입니다.

단어를 벡터화 하는것도 비슷합니다. 모든문서에 등장하는 단어는 중요도가 떨어지겠죠?? 그래서 해당 단어에 대해서는 가중치를 적게 두고 특정 문서에만 등장하는 단어에 가중치를 더 두는것을 TF-IDF라고 부릅니다.

즉, 문서의 연관성을 알고싶을때 TF-IDF를 사용할 수 있다.

  • 수식
TF-IDF(w)=TF(w)×IDF(w)\text{TF-IDF(w)} = \text{TF(w)} \times \text{IDF(w)}
TF(w)=특정 문서 내 단어 w의 수\text{TF(w)} = \text{특정 문서 내 단어 w의 수}
IDF(w)=log(분류 대상이 되는 모든 문서의 수단어 w가 들어있는 문서의 수)\text{IDF(w)} = \log \bigg(\frac{\text{분류 대상이 되는 모든 문서의 수}}{\text{단어 w가 들어있는 문서의 수}}\bigg)

실제로 계산을 할때에는 0이되는것을 방지하고자 분모에 1을 더해줍니다 그렇게 하여 IDF는 아래와 같이 됩니다.(0이되는것을 방지하기 위해 1을 더해주는것을 smoothing이라고한다.)

분류 대상이 되는 모든 문서의 수:n단어 w가 들어있는 문서의 수:df(w)\text{분류 대상이 되는 모든 문서의 수} : n \\ \text{단어 w가 들어있는 문서의 수} : df(w)
IDF(w)=log(n1+df(w))\text{IDF(w)} = \log \bigg(\frac{n}{1 + df(w)}\bigg)

수식을 정리해 보면 자주 사용하는 단어라도, 많은 문서에 나오는 단어들은 IDF가 낮아지기 때문에 TF-IDF로 벡터화 했을때 작은 값을 가지게 됩니다.

# TF-IDF vectorizer
tfidf = TfidfVectorizer(stop_words='english', max_features=15)

# Fit 후 dtm을 만듭니다
dtm_tfidf = tfidf.fit_transform(sentences_lst)

dtm_tfidf = pd.DataFrame(dtm_tfidf.todense(), columns=tfidf.get_feature_names())
dtm_tfidf

아마존 리뷰 TfidfVectorizer적용

tfidf_vect = TfidfVectorizer(stop_words='english', max_features=100)


dtm_tfidf_amazon = tfidf_vect.fit_transform(df['reviews.text'])

dtm_tfidf_amazon = pd.DataFrame(dtm_tfidf_amazon.todense(), columns=tfidf_vect.get_feature_names())
dtm_tfidf_amazon

파라미터 튜닝


def tokenize(document):
    doc = nlp(document)
    return [token.lemma_.strip() for token in doc if (token.is_stop != True) and (token.is_punct != True) and (token.is_alpha == True)]

tfidf_tuned = TfidfVectorizer(stop_words = 'english',tokenizer = tokenize, ngram_range=(1,2), max_df = .7, min_df = 3)

dtm_tfidf_tuned = tfidf_tuned.fit_transform(df['reviews.text'])
dtm_tfidf_tuned = pd.DataFrame(dtm_tfidf_tuned.todense(), columns=tfidf_tuned.get_feature_names())
dtm_tfidf_tuned.head()

n-gram : 각 단어 하나하나 뿐만 아니라 연속된 단어까지 하나의 토큰
(1-gram : b,a,n,n,a/ 2-gram : ba, an,nn,na)

n-gram을 사용하는 이유 
1. Bag-of-words의 단점을 커버할 수 있다. 단어의 순서가 무시되는 BOW를 ngram을 사용함으로서 해결할 수 있다.
2. 다음단어가 무엇이 올지 예측가능
3. 오타를 발견할 수 있다.
참고영상<https://www.youtube.com/watch?v=4f9XC8HHluE&list=PLVNY1HnUlO26qqZznHVWAqjS1fWw0zqnT&index=2>

max_df : 모든 문서에 등장하는 단어가 토큰화 되는걸 피하기 위해 전체 문서의 x%이상에 등장하는 단어는 토큰화에서 제외한다.
min_df : 특정 문서에만 등장하는 단어가 토큰화 되는것을 피하기 위해 x개 문서에 등장하는 단어는 제외

profile
문제를해결하는도구로서의"데이터"

0개의 댓글