Go together

If you want to go fast, go alone. If you want to go far, go together.

파이썬 머신 러닝 완벽 가이드

텍스트 분석

NowChan 2021. 12. 3. 23:12

배운 내용

  1. nltk.sent_tokenize()
  2. nltk.word_tokenize()
  3. nltk.corpus.stopwords.words()
  4. nltk.stem.LancasterStemmer
  5. nltk.stem.WordNetLemmatizer
  6. TF-IDF
  7. CountVectorizer
  8. TfidfVectorizer
  9. scipy.sparse.coo_matrix()
  10. scipy.sparse.csr_matrix()
  11. re 모듈
  12. wordnet.synsets()
  13. Synset.path_similarity()
  14. nltk.corpus.sentiwordnet
  15. SentiSynset.pos_score()
  16. SentiSynset.neg_score()
  17. SentiSynset.obj_score()
  18. SentimentIntensityAnalyzer
  19. SentimentIntensityAnalyzer.polarity_scores()
  20. pos_tag
  21. LatentDirichletAllocation
  22. glob
  23. os
  24. ndarray.argsort()
  25. CSR.todense()
  26. sklearn.metrics.pairwise.cosine_similarity
  27. KoNLPy
  28. Okt.morphs()

NLP이냐 텍스트 분석이냐?

머신러닝이 보편화 되면서 NLP와 TA(Text Analytics)를 구분하는 게 희미해졌습니다. 굳이 구분하자면 NLP는 머신이 인간의 언어를 이해하고 해석하는 데 더 중점을 두고 기술이 발전해왔고, 텍스트 마이닝(Text Mining = TA)은 비정형 텍스트에서 의미 있는 정보를 추출하는 것에 더 중점을 두고 발전해왔습니다. 

 

텍스트 분석은 머신러닝, 언어 이해, 통계 등을 활용해 정보를 추출하는 비즈니스 인텔리전스(BI)나 예측 분석 등의 분석 작업을 주로 수행합니다. 텍스트 분석은 머신러닝 기술로 다음과 같은 영역에 집중해왔습니다.

  • 텍스트 분류(Text Classification): Text Categorization이라고도 한다. 문서가 특정 분류(카테고리)에 속하는 지 예측하는 기법을 통칭합니다. 신문 기사 내용을 정치/연애/사회 등으로 분류하거나 스팸 메일 검출 프로그램이 이에 속합니다. 지도학습을 적용합니다.
  • 감성 분석(Sentiment Analysis): 텍스트에 나타나는 감정/기분 등 주관적인 요소를 분석하는 기법입니다. 리뷰 분석, 여론 조사 분석 등에 사용되며 TA에서 가장 활발하게 사용되는 분야입니다. 지도/비지도 학습을 이용합니다.
  • 텍스트 요약(Summarization): 텍스트 내에서 주제, 중심 사상을 추출하는 기법을 말합니다. 대표적으로 토픽 모델링(Topic Modeling)이 있습니다.
  • 텍스트 군집화(Clustering)와 유사도 측정: 비슷한 유형의 문서에 대해 군집화를 수행하는 기법을 말합니다. 텍스트 분류를 비지도 학습으로 수행하는 방법의 일환입니다. 유사도 측정 역시 문서들 간 유사도를 측정해 비슷한 문서끼리 모을 수 있습니다.

 

 

텍스트 분석 이해 

텍스트 분석은 비정형 데이터인 텍스트를 분석하는 것인데, 머신러닝 알고리즘은 숫자형 피처 기반 데이터만 입력받을 수 있기 때문에, 비정형 텍스트 데이터를 어떻게 피처 형태로 추출하고 추출된 피처에 의미 있는 값을 부여하는 가는 매우 중요한 요소입니다.

 

텍스트를 word(또는 word의 일부분) 기반의 다수의 피처로 추출하고 이 피처에 단어 빈도수와 같은 숫자 값을 부여하면 텍스트는 단어의 조합인 벡터값으로 표현될 수 있는데, 이렇게 텍스트를 변환하는 것을 피처의 벡터화(Feature Vectorization) 또는 피처 추출(Feature Extraction)이라고 합니다. 텍스트를 피처 백터화해서 변환하는 방법에는 BOW(Bag of Words)Word2Vec 방법이 있습니다. BOW만 설명하겠습니다.

 

텍스트를 벡터값을 가지는 피처로 변환하는 것은 머신러닝 모델을 적용하기 전에 수행해야 할 매우 중요한 요소입니다.

 

 

텍스트 분석 수행 프로세스

머신러닝 기반의 텍스트 분석 프로세스는 다음과 같은 프로세스 순으로 수행합니다.

  1. 텍스트 전처리(사전 작업): 텍스트를 피처로 만들기 전에 대/소문자 변경, 특수문자 삭제 등의 클렌징 작업, 단어(Word) 등의 토큰화 작업, 의미 없는 단어(Stop word) 제거 작업, 어근 추출(Stemming/Lemmatization) 등의 텍스트 정규화 작업을 수행하는 것을 통칭합니다.
  2. 피처 벡터화/추출: 전처리가 완료된 텍스트에서 피처를 추출하고 여기에 벡터 값을 할당합니다. 대표적인 방법은 BOW와 Word2Vec이 있으며, BOW는 대표적으로 Count 기반과 TF-IDF 기반 벡터화가 있습니다.
  3. ML 모델 수립 및 학습/예측/평가: 피처 벡터화된 데이터 세트에 ML 모델을 적용해 학습/예측 및 평가를 수행합니다.

 

 

 

파이썬 기반의 NLP, 텍스트 분석 패키지

파이썬 기반에서 NLP와 텍스트 분석을 위해 텍스트 전처리, 피처 벡터화/추출, ML 모델을 지원하는 훌륭한 라이브러리가 많습니다.(대부분 영어 기반 라이브러리) 대표적인 파이썬 기반의 NLP와 텍스트 분석 패키지를 소개합니다.

  • NLTK(Nautral Language Toolkit for Python): 파이썬의 가장 대표적인 NLP 패키지입니다. 방대한 데이터 세트와 서브 모듈을 가지고 있으며 NLP의 거의 모든 영역을 커버하고 있습니다. 수행 속도 측면에서 아쉬운 부분이 있어서 실제 대량의 데이터 기반에서는 제대로 활용되지 못하고 있습니다.
  • Gensim: 토픽 모델링 분야에서 가장 두각을 나타내는 패키지입니다. 오래 전부터 토픽 모델링을 쉽게 구현할 수 있는 기능을 제공해왔고, Word2Vec 구현 등의 신기능도 제공합니다. SpaCy와 함께 많이 사용되는 NLP 패키지입니다.
  • SpaCy: 뛰어난 수행 성능으로 주목받는 NLP 패키지입니다.

 

 

사이킷런은 머신러닝 위주의 라이브러리여서 NLP에 특화된 라이브러리(예를 들면, 어근 처리 등)를 가지고 있지 않습니다. 하지만, 텍스트 데이터를 피처로 처리하기 위한 기능을 제공하고 있어 사이킷런으로도 대부분의 텍스트 분석 기능을 수행할 수 있습니다. 한편, 더 다양한 텍스트 분석을 위해선 NLTK, Gensim, SpaCy 와 함께 작성해야합니다.

 

 

 

텍스트 사전 준비 작업(텍스트 전처리) - 텍스트 정규화

텍스트 정규화는 텍스트를 ML 모델, NLP 어플리케이션에 입력 데이터로 사용하기 위해 클렌징, 정제, 토큰화, 어근화 등 전처리를 수행하는 것을 말합니다. 텍스트 정규화 작업은 크게 다음과 같이 분류할 수 있습니다.

  • 클렌징(Cleansing)
  • 토큰화(Tokenization)
  • 필터링/스톱 워드 제거/철자 수정
  • Stemming
  • Lemmatization

 

텍스트 정규화의 주요 작업을 NLTK 패키지를 이용해 실습해보겠습니다.

 

 

클렌징

텍스트 분석에 방해가 되는 불필요한 문자, 기호 등을 사전에 제거하는 작업입니다. 예를 들어 HTML, XML 태그나 특정 기호를 사전에 제거합니다.

 

텍스트 토큰화

토큰화의 유형은 문서에서 문장을 분리하는 문장 토큰화와 문장에서 단어를 토큰으로 분리하는 단어 토큰화로 나눌 수 있습니다. NLTK는 이를 위해 다양한 API를 제공합니다.

 

문장 토큰화

문장 토큰화(sentence tokenization)은 문장의 마침표(.), 개행 문자(\n) 등 문장의 마지막을 뜻하는 기호에 따라 분리하는 것이 일반적입니다. 또한 정규 표현식에 따른 문장 토큰화도 가능합니다. 

 

3가지 문장으로 이루어진 텍스트 문서를 문장으로 분리하는 예제입니다. NLTK의 경우 단어 사전과 같이 참조가 필요한 데이터 세트를 다운로드 받을 수 있습니다. 최초 다운로드가 필요하고, 다운로드 완료 후에는 다시 다운로드하지 않습니다.

from nltk import sent_tokenize
import nltk
nltk.download('punkt')

text_sample = 'The Matrix is everywhere its all around us, here even in this room. \
              You can see it out your window or on your television. \
              you feel it when you go to work, or go to church or pay your taxes.'
sentences = sent_tokenize(text=text_sample)
print(type(sentences), len(sentences))
print(sentences)
'''
<class 'list'> 3
['The Matrix is everywhere its all around us, here even in this room.',
'You can see it out your window or on your television.',
'you feel it when you go to work, or go to church or pay your taxes.']
'''

 

 

단어 토큰화

단어 토큰화(Word Tokenization)는 문장을 단어로 토큰화하는 것입니다. 기본적으로 공백, 콤마(,), 마침표(.), 개행문자 등으로 단어를 분리하지만, 정규 표현식을 이용해 다양한 유형으로 토큰화할 수 있습니다.

 

마침표(.)나 개행문자와 같이 문장을 분리하는 구분자를 이용해 단어를 토큰화할 수 있으므로 Bag of Word와 같이 단어의 순서가 중요하지 않은 경우 문장 토큰화없이 단어 토큰화만 사용해도 충분합니다. 문장 토큰화는 각 문장이 가지는 시맨틱적인 의미가 중요한 요소로 사용될 때 사용합니다.

 

nltk.word_tokenize()

NTLK에서 기본으로 제공하는 word_tokenize()를 이용해 단어로 토큰화해보겠습니다.

from nltk import word_tokenize

sentence = "The Matrix is everywhere its all around us, here even in this room."
words = word_tokenize(sentence)
print(type(words), len(words))
print(words)
'''

from nltk import word_tokenize

sentence = "The Matrix is everywhere its all around us, here even in this room."
words = word_tokenize(sentence)
print(type(words), len(words))
print(words)
<class 'list'> 15
['The', 'Matrix', 'is', 'everywhere', 'its', 'all', 'around', 'us', ','
, 'here', 'even', 'in', 'this', 'room', '.']
'''

 

sent_tokenize()와 word_tokenize()를 조합해 문서에 대해서 모든 단어를 토큰화해보겠습니다. tokenize_text() 함수를 만들겠습니다.

from nltk import word_tokenize, sent_tokenize

# 여러 개의 문장으로 된 입력 데이터를 문장 별로 단어 토큰화하게 만드는 함수 생성
def tokenize_text(text):
  # 문장 별로 분리 토큰
  sentences = sent_tokenize(text)
  # 분리된 문장 별로 단어 토큰화
  word_tokens = [word_tokenize(sentence) for sentence in sentences]
  return word_tokens

# 여러 문장에 대해 문장 별 단어 토큰화 수행.
word_tokens = tokenize_text(text_sample)
print(type(word_tokens), len(word_tokens))
print(word_tokens)
'''
<class 'list'> 3
[['The', 'Matrix', 'is', 'everywhere', 'its', 'all', 'around', 'us', 
',', 'here', 'even', 'in', 'this', 'room', '.'], ['You', 'can', 'see',
'it', 'out', 'your', 'window', 'or', 'on', 'your', 'television', '.'], 
['you', 'feel', 'it', 'when', 'you', 'go', 'to', 'work', ',', 'or', 'go',
'to', 'church', 'or', 'pay', 'your', 'taxes', '.']]
'''

문장을 단어 별로 토큰화할 경우 문맥이 무시되는데 이를 해결하고자 도입된 것이 n-gram입니다. n-gram은 n개의 단어를 하나의 토큰화 단위로 분리해내는 것입니다. n개 단어 크기 윈도우를 만들어 문장의 처음부터 오른쪽으로 움직이면서 토큰화를 수행합니다. 'Agent Smith knocks the door'를 2-gram으로 만들면 (Agent, Smith), (Smith, knocks), (knocks, the), (the, door)와 같이 토큰화합니다.

 

 

스톱 워드 제거

스톱 워드(Stop word)는 분석에 큰 의미가 없는 단어를 지칭합니다. 가령 is, the, a, will 등 문장을 구성하는 문법 요소이지만 문맥적으로 큰 의미가 없는 단어가 이에 해당합니다. 이러한 단어들은 그 빈번함으로 오히려 중요한 단어로 인지될 수 있습니다. 따라서 이러한 단어들을 제거하는 것은 중요한 전처리 작업입니다.

 

언어 별로 이러한 스톱 워드가 목록화돼 있습니다. NLTK의 경우 다양한 언어의 스톱워드를 제공합니다. 이를 위해 NLTK의 stopwords 목록을 내려받습니다.

import nltk
nltk.download('stopwords')

 

nltk.corpus.stopwords.words()

NLTK의 Enlish의 stopwords가 몇 개 있는지 알아보고 그 중 20개를 확인해보겠습니다.

print('영어 stop words 개수:', len(nltk.corpus.stopwords.words('english')))
print(nltk.corpus.stopwords.words('english')[:20])
'''
영어 stop words 개수: 179
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', "you're", "you've",
"you'll", "you'd", 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his']
'''

앞에 3개의 문장 별로 단어를 토큰화해 생성한 word_tokens 리스트에 stopwords를 필터링해보겠습니다.

import nltk

stopwords = nltk.corpus.stopwords.words('english')
all_tokens = []
# 위 예제에서 3개의 문장 별로 얻은 word_tokens list에 대해 스톱 워드를 제거하는 반복문
for sentence in word_tokens:
  filtered_words=[]
  # 개별 문장 별로 토큰화된 문장 list에 대해 스톱 워드를 제거하는 반복문
  for word in sentence:
    # 소문자로 모두 변환합니다.
    word = word.lower()
    # 토큰화된 개별 단어가 스톱 워드의 단어에 포함되지 않으면 filtered_words에 추가
    if word not in stopwords:
      filtered_words.append(word)
  all_tokens.append(filtered_words)

print(all_tokens)
'''
[['matrix', 'everywhere', 'around', 'us', ',', 'even', 'room', '.'],
['see', 'window', 'television', '.'],
['feel', 'go', 'work', ',', 'go', 'church', 'pay', 'taxes', '.']]
'''

is, this와 같은 스톱 워드가 필터링을 통해 제거 됐음을 알 수 있습니다.

 

 

Stemming과 Lemmatization

많은 언어에서 문법적인 요소에 따라 단어가 다양하게 변하는데, 영어의 경우 과거/현재, 3인칭 단수, 진행형 등 많은 조건에 따라 원래 단어가 변화합니다. 가령 work는 동사 원형인 단어이지만, 과거형은 worked, 3인칭 단수일 땐 works, 진행형인 경우 working 등 다양하게 달라집니다. Stemming과 Lemmatization은 문법적 또는 의미적으로 변화하는 단어의 원형을 찾는 것입니다.

 

Stemming, Lemmatization 모두 원형 단어를 찾는다는 목적은 유사하지만, Lemmatization이 정교하며 의미론적인 기반에서 단어의 원형을 찾습니다. Stemming은 원형 단어로 변환시 더 단순화된 방법을 적용해 일부 철자가 훼손된 어근 단어를 추출하는 경향이 있습니다. Lemmatization은 품사와 같은 문법적인 요소와 의미적인 부분을 감안해 정확한 철자로 된 어근 단어를 찾아줍니다. 대신 시간이 보다 더 걸립니다.

 

NLTK는 다양한 Stemmer를 제공합니다. 대표적으로 Porter, Lancaster, Snowball Stemmer가 있습니다. Lemmatization을 위해서는 WordNetLemmatizer를 제공합니다.

 

먼저 NLTK의 LancasterStemmer를 이용해 Stemmer부터 살펴보겠습니다. 

 

nltk.stem.LancasterStemmer

from nltk.stem import LancasterStemmer
stemmer = LancasterStemmer()

print(stemmer.stem('working'), stemmer.stem('works'), stemmer.stem('worked'))
print(stemmer.stem('amusing'), stemmer.stem('amuses'), stemmer.stem('amused'))
print(stemmer.stem('happier'), stemmer.stem('happiest'))
print(stemmer.stem('fancier'), stemmer.stem('fanciest'))
'''
work work work
amus amus amus
happy happiest
fant fanciest
'''

work의 경우 진행형, 단수, 과거형 모두 원형 단어를 제대로 인식합니다. amuse의 경우는 amus를 원형으로 인식합니다. fancy의 경우 비교급, 최상급 등을 잘 이해하지 못하고 철자가 다른 어근으로 인식했습니다.

 

nltk.stem.WordNetLemmatizer

이번엔 WordNetLemmatizer를 이용해 Lemmatization을 수행해보겠습니다. Lemmatization은 정확한 원형 단어 추출을 위해 '품사'를 입력해줘야 합니다. lemmatize()의 파라미터로 동사의 경우 'v', 형용사의 경우 'a'를 입력합니다.

from nltk.stem import WordNetLemmatizer
import nltk
nltk.download('wordnet')

lemma = WordNetLemmatizer()
print(lemma.lemmatize('amusing', 'v'), lemma.lemmatize('amuses', 'v'),\ 
						lemma.lemmatize('amused', 'v'))
print(lemma.lemmatize('happier', 'a'), lemma.lemmatize('happiest', 'a'))
print(lemma.lemmatize('fancier', 'a'), lemma.lemmatize('fanciest', 'a'))
'''
amuse amuse amuse
happy happy
fancy fancy
'''

Stemmer보다 정확하게 원형 단어를 추출함을 알 수 있습니다.

 

 

Bag of Words - BOW

Bag of Words 모델은 문서가 가지는 모든 단어(Words)를 문맥, 순서를 무시한 채로 단어의 빈도값을 피처값으로 추출하는 모델입니다. 문서 내 모든 단어를 한꺼번에 봉투(Bag)에 넣어 흔들어 섞는다는 의미에서 BOW 모델이라고 합니다.

 

DOC 1, 2, 3에 있는 모든 단어의 중복을 제외하고 각 단어를 칼럼 형태로 나열합니다.

  • Doc 1: I love dogs.
  • Doc 2: I hate dogs and knitting.
  • Doc 3: Knitting is my hobby and passion.

 

 

BOW 설명

BOW 모델은 쉽고 빠른 구축에 있습니다. 예상보다 문서의 특징을 잘 나타낼 수 있는 모델입니다. 하지만, 단점 역시 존재하는데, 그 단점들은 아래와 같습니다.

  • 문맥 의미(Semantic Context) 반영 부족: 문장 내 단어 순서를 반영하지 않아서 문맥적인 의미가 무시됩니다. 이를 보완하기 위해 n_gram 기법을 활용할 수 있지만, 한계가 있습니다.
  • 희소 행렬 문제(희소성, 희소 행렬): 문서마다 서로 다른 단어로 구성될 확률이 높아 대부분의 칼럼이 0으로 채워지게 됩니다. 이러한 희소 행렬은 ML 알고리즘의 수행 시간과 예측 성능을 떨어뜨리기 때문에 희소 행렬을 위한 특별한 기법이 마련돼 있습니다.

 

 

BOW 피처 벡터화

텍스트는 머신러닝 알고리즘에 입력하기 위해 특정 의미를 가지는 숫자형 값인 벡터값으로 변환해야 하는데, 이러한 변환을 피처 벡터화라고 합니다. 각 문서(Document)의 텍스트를 단어로 추출해 피처로 할당하고, 각 문서는 단어 피처의 발생 빈도 값으로 구성된 벡터를 만들어집니다. Text Analysis에서는 피처 벡터화와 피처 추출을 같은 의미로 사용합니다.

 

일반적으로 BOW의 피처 벡터화는 두 가지 방식이 있습니다.

  • 카운트 기반의 벡터화
  • TF-IDF(Term Frequency - Inverse Document Frequency) 기반의 벡터화

 

카운트 기반의 벡터화는 문서의 단어가 나타나는 횟수에 count를 부여하고 count값이 높을 수록 중요한 단어로 인식합니다. 이러한 방법은 언어의 특성상 문장에서 자주 사용되는 단어까지 높은 중요도를 부여할 수 밖에 없습니다. 이러한 문제를 보완하기 위해 TF-IDF 벡터화를 사용합니다.

 

TF-IDF는 개별 문서에 자주 나타나는 단어에 높은 가중치를 주되, 모든 문서에서 전반적으로 자주 나타나는 단어에 대해서는 페널티를 주는 방식으로 값을 부여합니다. 문서마다 텍스트가 길고, 문서의 개수가 많은 경우 카운트 방식보다는 TF-IDF 방식을 사용하는 것이 더 좋은 예측 성능을 보장할 수 있습니다.

 

TF, IDF 설명

예를 들면 위 그림에서 생성된 피처를 아래 표처럼 나타낼 수 있습니다. 

  a
한 개의 문서(Document) 1
모든 문서들(Corpus) 4

식은 아래와 같이 나타낼 수 있습니다. 위 표에서는 단어i = a, TF_i = 1, DF_i = 4, N = 6입니다.

$$TFIDF_i=TF_i\cdot \log \frac{N}{DF_i}$$ $$\begin{cases}TF_i:개별\ 문서에서의\ 단어\ i\ 빈도\\DF_i:단어\ i를\ 가지고\ 있는\ 문서\ 개수\\N:전체\ 문서\ 개수\end{cases}$$

 

 

 

사이킷런의 Count 및 TF-IDF 벡터화 구현: CountVectorizer, TfidfVectorizer

 

사이킷런의 CountVectorizer 클래스는 카운트 기반의 벡터화를 구현한 클래스입니다. CountVectorizer 클래스는 단지 피처 벡터화만 수행하지 않고 소문자 일괄 변환, 토큰화, 스톱 워드 필터링 등의 텍스트 전처리도 함께 수행합니다. CountVectorizer 역시 사이킷런의 다른 피처 변환 클래스와 마찬가지로 fit()과 transform()을 통해 피처 벡터화된 객체를 반환합니다. 주요 입력 파라미터는 아래와 같습니다.

파라미터 명 파라미터 설명
max_df 스톱 워드와 같이 너무 자주 반복되는 단어를 필터링할 기준 빈도수를 정합니다. int형은 갯수, float형은 예를 들어, 0.95라고 한다면 전체 문서에 걸쳐 빈도수 0~95%까지 단어만 피처로 추출한다는 뜻입니다.
min_df 너무 낮은 빈도수의 단어를 필터링할 기준 빈도수를 정합니다. int형은 갯수, float형은 예를 들어, 0.02라고 하면, 하위 빈도수 2%의 단어는 피처로 추출하지 않는다는 뜻입니다.
max_features 추출하는 피처의 개수를 지정합니다. 가장 높은 빈도를 가지는 단어 순으로 피처를 추출합니다.
stop_words 'english'로 지정하면 영어 스톱 워드로 지정된 단어는 추출에서 제외합니다.
n_gram_range n_gram 범위를 설정합니다. 튜플 형으로 (범위 최솟값, 범위 최댓값)을 지정합니다. (1, 1)은 토큰화된 단어를 1개씩 피처로 추출하고, (1, 2)는 토큰화된 단어를 1개씩, 그리고 순서대로 2개씩 묶어서 피처로 추출합니다.
analyzer 피처 추출을 수행할 단위를 지정합니다. default는 'word'이고, character의 특정 범위를 피처로 만들 때 사용
token_pattern 토큰화를 수행하는 정규 표현식 패턴을 지정합니다. default는 '\b\w\w+\b'또는 개행 문자 등으로 구분된 단어 분리자(\b) 사이의 2문자 이상의 단어를 토큰으로 분리합니다.
tokenizer 토큰화를 별도의 커스텀 함수로 이용시 적용합니다. 일반적으로 CountTokenizer 클래스에서 어근 변환시 이를 수행하는 별도의 함수를 tokenizer 파라미터에 적용하면 됩니다.

 

CountVectorizer를 이용한 피처 벡터화는 아래의 과정으로 진행됩니다.

  1. 사전 데이터 가공: 모든 문자를 소문자로 변환합니다.(default는 lowercase=True)
  2. 토큰화: n_gram_range를 반영해 토큰화 수행(default는 analyzer=True)
  3. 텍스트 정규화: Stop Words 필터링만 수행(Stemmer, Lemmatize는 이를 위한 함수를 tokenizer에 입력하거나, 미리 Text Normalization 수행)
  4. 피처 벡터화: max_df, min_df, max_features 등을 반영해 Token된 단어들을 feature extraction 후 vectorization 적용

 

TF-IDF 벡터화TfidfVectorizer 클래스를 이용합니다. 주요 파라미터와 변환 방법은 CountVectorizer와 동일합니다.

 

 

 

BOW 벡터화를 위한 희소 행렬

사이킷런의 CountVectorizer/TfidfVectorizer를 이용해 텍스트를 피처 단위로 벡터화해 변환하고 CSR 형태의 희소 행렬을 반환합니다. 단어를 피처로 벡터화 변환할 때 불필요한 너무 많은 0 값이 존재하게 되는데, 이러한 희소 행렬을 물리적으로 적은 메모리 공간을 차지할 수 있도록 변환해야합니다. 대표적인 방법으로는 COO 형식과 CSR 형식이 있습니다.

 

희소 행렬 - COO 형식

COO(Cordinate: 좌표) 형식은 0이 아닌 데이터만 별도의 데이터 배열(Array)에 저장하고 그 데이터의 행, 열의 위치를 배열에 저장하는 방식입니다. 예를 들어 [[3, 0, 1], [0, 2, 0]]의 데이터가 있을 때 0이 아닌 Data는 [3, 1, 2]이고 이들의 위치를 (row, col)로 표시하면 (0, 0), (0, 2), (1, 1)로 나타낼 수 있습니다. 행과 열을 별도의 배열에 저장하면 row는 [0, 0, 1]이고 칼럼은 [0, 2, 1] 입니다.

 

파이썬에서 희소 행렬 변환을 위해 주로 사이파이(Scipy)를 이용합니다. 사이파이의 sparse 패키지는 희소 행렬 변환을 위한 다양한 모듈을 제공합니다. 예제를 통해 사용법을 알아보겠습니다.

 

scipy.sparse.coo_matrix()

import numpy as np

dense = np.array([[3, 0, 1], [0, 2, 0]])

from scipy import sparse
# 0이 아닌 데이터 추출
data = np.array([3, 1, 2])

# 행 위치와 열 위치를 각각 배열로 생성
row_pos = np.array([0, 0, 1])
col_pos = np.array([0, 2, 1])

# sparse 패키지의 coo_matrix를 이용해 COO 형식으로 희소 행렬 생성
sparse_coo = sparse.coo_matrix((data, (row_pos, col_pos)))

sparse_coo는 COO 형식의 희소 행렬 객체 변수입니다. 이를 toarray() 메서드를 이용해 다시 밀집 형태 행렬로 출력해보겠습니다.

sparse_coo.toarray()
'''
array([[3, 0, 1],
       [0, 2, 0]])
'''

다시 원래 데이터 행렬로 추출됨을 알 수 있습니다.

 

 

 

희소 행렬 - CSR 형식

CSR(Compressed Sparse Row) 형식은 COO 형식이 행, 열의 위치를 나타내기 위해 반복적인 위치 데이터를 사용해야 하는 문제점을 해결한 방식입니다. CSR은 COO의 행 위치 배열이 순차적으로 커지는 특성을 이용해 값(0, 1, 2 ..)들의 시작 위치와 총 항목 수로 이루어진 배열로 변환하는 방식입니다.

 

예를 들면, 행 위치 배열 [0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5]를 CSR로 변환하면 [0, 2, 7, 9, 10, 12]가 되고 데이터의 총 항목 개수를 배열에 추가해 최종적으로 [0, 2, 7, 9, 10, 12, 13]이 됩니다. 예제를 통해 알아보겠습니다.

 

scipy.sparse.csr_matrix()

from scipy import sparse

dense2 = np.array([[0, 0, 1, 0, 0, 5],
                   [1, 4, 0, 3, 2, 5],
                   [0, 6, 0, 3, 0, 0],
                   [2, 0, 0, 0, 0, 0],
                   [0, 0, 0, 7, 0, 8],
                   [1, 0, 0, 0, 0, 0]])

# 0이 아닌 데이터 추출
data2 = np.array([1, 5, 1, 4, 3, 2, 5, 6, 3, 2, 7, 8, 1])

# 행 위치와 열 위치를 각각 array로 생성
row_pos = np.array([0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 4, 4, 5])
col_pos = np.array([2, 5, 0, 1, 3, 4, 5, 1, 3, 0, 3, 5, 0])

# COO 형식으로 변환
sparse_coo = sparse.coo_matrix((data2, (row_pos, col_pos)))

# 행 위치 배열의 고유한 값의 시작 위치 인덱스를 배열로 생성
row_pos_ind = np.array([0, 2, 7, 9, 10, 12, 13])

# CSR 형식으로 변환
sparse_csr = sparse.csr_matrix((data2, col_pos, row_pos_ind))

print('COO 변환된 데이터가 제대로 되었는지 다시 Dense로 출력 확인')
print(sparse_coo.toarray())
print('CSR 변환된 데이터가 제대로 되었는지 다시 Dense로 출력 확인')
print(sparse_csr.toarray())
'''
COO 변환된 데이터가 제대로 되었는지 다시 Dense로 출력 확인
[[0 0 1 0 0 5]
 [1 4 0 3 2 5]
 [0 6 0 3 0 0]
 [2 0 0 0 0 0]
 [0 0 0 7 0 8]
 [1 0 0 0 0 0]]
CSR 변환된 데이터가 제대로 되었는지 다시 Dense로 출력 확인
[[0 0 1 0 0 5]
 [1 4 0 3 2 5]
 [0 6 0 3 0 0]
 [2 0 0 0 0 0]
 [0 0 0 7 0 8]
 [1 0 0 0 0 0]]
'''

COO와 CSR이 어떻게 희소 행렬의 메모리를 줄일 수 있는지 예제를 통해 살펴봤습니다. 실제 사용시에 아래와 같이 밀집 행렬을 생성 파라미터로 입력하면 COO나 CSR 희소 행렬로 생성합니다.

dense3 = np.array([[0, 0, 1, 0, 0, 5],
                   [1, 4, 0, 3, 2, 5],
                   [0, 6, 0, 3, 0, 0],
                   [2, 0, 0, 0, 0, 0],
                   [0, 0, 0, 7, 0, 8],
                   [1, 0, 0, 0, 0, 0]])
                   
coo = sparse.coo_matrix(dense3)
csr = sparse.csr_matrix(dense3)

 

사이킷런의 CounterVectorizer나 TfidfVectorizer 클래스로 변환된 피처 벡터와 행렬은 모두 scipy의 CSR 형태의 희소 행렬입니다. 지금까지 텍스트 분석을 위한 기반 지식을 설명했습니다.

 

 

감성 분석

감성 분석(Sentiment Analysis)은 문서의 주관적인 감성/의견/감정/기분 등을 파악하기 위한 방법으로 여론 조사, 소셜 미디어에서 자주 활동되고 있습니다. 감성 분석은 문서 내 단어와 문맥을 기반으로 감성(Sentiment) 수치를 계산합니다. 이러한 감성 지수는 긍정 감성 지수와 부정 감성 지수로 구성되며 이들 지수를 합산해 긍정 감성인지 부정 감성인지 결정합니다.

 

이러한 감성 분석은 머신러닝 관점에서 지도학습과 비지도학습 방식으로 나눌 수 있습니다.

  • 지도학습은 학습 데이터와 타깃 레이블 값을 기반으로 감성 분석 학습을 수행한 뒤 다른 데이터의 감성 분석을 예측하는 방법으로 일반적인 텍스트 기반 분류와 거의 동일합니다.
  • 비지도학습은 'Lexicon'이라는 일종의 감성 어휘 사전을 이용합니다. Lexicon은 감성 분석을 위한 용어와 문맥에 대한 다양한 정보를 가지고 있으며, 문서의 긍정, 부정적 감성 여부를 판단합니다.

 

 

 

지도학습 기반 감성 분석 실습 - IMDB 영화평

지도학습 기반 감성 분석은 텍스트 기반의 이진 분류와 같습니다. IMDB 영화 사이트의 영화평의 텍스트를 분석해 감성 분석 결과가 긍정 또는 부정인지 예측하는 모델을 만들어보겠습니다. 데이터는 https://www.kaggle.com/c/word2vec-nlp-tutorial/data 에서 내려받을 수 있습니다.

 

Bag of Words Meets Bags of Popcorn | Kaggle

 

www.kaggle.com

 

import pandas as pd

review_df = pd.read_csv('/content/drive/MyDrive/military/IMDB/labeledTrainData.tsv', \
                        header=0, sep='\t', quoting=3)
review_df.head(3)
'''
결과1
'''

결과1

sentiment는 1이 긍정적 평가, 0이 부정적 평가를 의미합니다. review는 영화평의 텍스트입니다. review의 텍스트가 어떻게 구성돼있는지 보겠습니다. 요약해서 적었지만 엄청 긴 텍스트이다. HTML 형식에서 추출해 <br /> 태그가 여전히 존재합니다. <br/ > 피처 및 특수 문자, 숫자 등의 Sentiment를 위한 피처로는 별 의미가 없어보이므로 이들을 모두 공란으로 변경하겠습니다. Pandas의 Series 객체에서 str을 적용하면 문자열 연산을 할 수 있습니다.

print(review_df['review'][0])
'''
"With all this stuff going down at the moment with MJ i've started listening to ...
'''

 

re 모듈

데이터를 정규 표현식을 이용해 전처리하겠습니다. 간단한 정규 표현식은 인터넷 등을 통해 익혀 두는 것이 유용합니다. 파이썬의 re 모듈은 편리하게 정규 표현식을 지원합니다. 정규 표현식 [^a-zA-Z]의 의미는 영 대/소문자가 아닌 모든 문자를 찾는 것입니다.

import re
# <br> html 태그는 replace 함수로 공백으로 변환
review_df['review'] = review_df['review'].str.replace('<br />', ' ')

# 파이썬의 정규 표현식 모듈인 re를 이용해 영어 문자열이 아닌 문자는 모두 공백으로 변환
review_df['review'] = review_df['review'].apply( lambda x : re.sub("[^a-zA-Z]", " ", x))

결정값 클래스인 sentiment 칼럼을 추출해 결정값 데이터 세트로 만들고 원본 데이터 세트에서 id와 sentiment 칼럼을 삭제해 피처 데이터 세트를 생성하고 학습/테스트용 데이터 세트로 분리하겠습니다.

from sklearn.model_selection import train_test_split

class_df = review_df['sentiment']
feature_df = review_df.drop(['id', 'sentiment'], axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(feature_df, class_df, test_size=0.3,\
                                                    random_state=156)
X_train.shape, X_test.shape
'''
((17500, 1), (7500, 1))
'''

감상평(Review) 텍스트를 피처 벡터화한 후 ML 알고리즘을 적용해 예측 성능을 평가해보겠습니다. 예측 성능 평가는 이진 분류임을 고려해 ROC-AUC, 정확도를 모두 측정하겠습니다.

 

Count 기반 피처 벡터화

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score

# 스톱 워드는 English, filtering, ngram은 (1, 2)로 설정해 CountVectorization 수행.
# LogisticRegression의 C는 10으로 설정.
pipeline = Pipeline([
      ('cnt_vect', CountVectorizer(stop_words='english', ngram_range=(1, 2))),
      ('lr_clf', LogisticRegression(C=10))
])

# Pipeline 객체를 이용해 fit(), predict()로 학습/예측 수행. predict_proba()는 roc_auc때문에 수행.
pipeline.fit(X_train['review'], y_train)
pred = pipeline.predict(X_test['review'])
pred_probs = pipeline.predict_proba((X_test['review']))[:, 1]

print('예측 정확도는 {0:.4f}, ROC-AUC는 {1:.4f}'.format(accuracy_score(y_test, pred),\
                                                 roc_auc_score(y_test, pred_probs)))
'''
예측 정확도는 0.8860, ROC-AUC는 0.9503
'''

 

TF-IDF 기반 피처 벡터화

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score

# 스톱 워드는 English, filtering, ngram은 (1, 2)로 설정해 CountVectorization 수행.
# LogisticRegression의 C는 10으로 설정.
pipeline = Pipeline([
      ('tfidf_vect', TfidfVectorizer(stop_words='english', ngram_range=(1, 2))),
      ('lr_clf', LogisticRegression(C=10))
])

# Pipeline 객체를 이용해 fit(), predict()로 학습/예측 수행. predict_proba()는 roc_auc때문에 수행.
pipeline.fit(X_train['review'], y_train)
pred = pipeline.predict(X_test['review'])
pred_probs = pipeline.predict_proba((X_test['review']))[:, 1]

print('예측 정확도는 {0:.4f}, ROC-AUC는 {1:.4f}'.format(accuracy_score(y_test, pred),\
                                                 roc_auc_score(y_test, pred_probs)))
'''
예측 정확도는 0.8936, ROC-AUC는 0.9598
'''

TF-IDF 기반 피처 벡터화의 예측 성능이 조금 더 나아졌습니다.

 

 

비지도학습 기반 감성 분석 소개

비지도 감성 분석은 Lexicon을 기반으로 하는 것입니다. Lexicon은 한글을 지원하지 않습니다. Lexicon은 일반적으로 어휘집을 의미하지만, 여기선 감성 어휘를 분석하기 위해 지원하는 감성 사전입니다. 감성 사전은 긍정, 부정 감성의 정도를 의미하는 수치를 가지고 있으며 이를 감성 지수(Polarity score)라고 합니다. 이 감성 지수는 단어의 위치, 주변 단어, 문맥, POS(Part of Speech) 등을 참고해 결정됩니다. 감성 사전의 대표적인 패키지는 NLTK 패키지이고, NLTK는 감성 사전인 Lexicon 모듈 외 많은 서브 모듈을 가지고 있습니다.

 

감성 사전을 좀 더 상세히 이해하기 위해 NLP 패키지의 WordNet을 먼저 설명하겠습니다. WordNet은 단순한 어휘 사전이 아닌 시맨틱 분석(문맥 분석)을 제공하는 어휘 사전입니다. 언어학에서 정의한 시맨틱을 표현하기 위해 여러 가지 규칙을 정해왔으며, NLP 패키지는 시맨틱을 프로그램적으로 인터페이스할 수 있는 다양한 방법을 제공합니다. 

 

WordNet은 같은 어휘라도 각 상황에서 다르게 사용되는 어휘의 시맨틱 정보를 제공합니다. 이를 위해 각각의 품사(명사, 동사, 형용사, 부사 등)로 구성된 개별 단어를 Synset(Sets of cognitive synonyms)라는 개념을 이용해 표현합니다. Synset은 단어뿐만 아니라 그 단어가 가지는 문맥, 시멘틱 정보를 제공하는 WordNet의 핵심 개념입니다.

 

WordNet의 예측 성능이 그리 좋지 못해 실무에서 다른 감성 사전을 적용합니다. NLTK를 포함한 대표적인 감성 사전은 아래와 같습니다.

  • SentiWordNet: WordNet의 Synset 개념을 감성 분석에 적용한 것입니다. Synset 별로 3가지 감성 점수(sentiment score)를 할당합니다. 긍정/부정/객관성 지수가 그것입니다. 객관성 지수는 단어가 얼마나 객관적인지를 나타내는 수치입니다. 문장 별로 단어들의 긍정 + 부정 감성 지수를 합산해 감성이 긍정, 부정인지 결정합니다.
  • VADER: 주로 소셜 미디어의 텍스트에 대한 감성 분석을 위해 사용됩니다. 뛰어난 감성 분석 결과를 제공하며, 대용량 텍스트 데이터에서 비교적 빠른 수행시간을 보장합니다.
  • Pattern: 예측 성능 측면에서 가장 주목받는 패키지입니다. 파이썬 2.X 버전에서만 동작합니다.

 

SentiWordNet과 VADER 감성 사전을 이용해 감성 분석을 수행한 뒤 지도학습 기반의 분류와 비교해보겠습니다. SentiWordNet은 시멘틱 기반의 사전 구축 방식을 더 자세히 이해할 수 있지만, VADER를 이용한 감성 분석만 참조해도 비지도학습 기반 감성 분석을 이해하는 데 무리가 없습니다.

 

 

SentiWordNet을 이용한 감성 분석

WordNet Synset과 SentiWordNet SentiSynset 클래스 이해하기

SentiWordNet은 WordNet 기반의 synset을 이용하므로 synset에 대한 개념부터 이해하겠습니다. WordNet을 이용하기 위해 NLTK를 셋업한 후 WordNet 서브 패키지와 데이터 세트를 내려받습니다. 

import nltk
nltk.download('all')

NLTK의 WordNet 모듈을 임포트해서 'present' 단어에 대한 Synset을 추출하겠습니다. 

 

wordnet.synsets()

wordnet.synsets()는 WordNet에 등재된 모든 Synset 객체를 반환합니다.

from nltk.corpus import wordnet as wn
term = 'present'

# 'present'라는 단어로 wordnet의 synsets 생성.
synsets = wn.synsets(term)
print('synsets() 반환 type :', type(synsets))
print('synsets() 반환 값 개수 :', len(synsets))
print('synsets() 반환 값 :', synsets)
'''
synsets() 반환 type : <class 'list'>
synsets() 반환 값 개수 : 18
synsets() 반환 값 : [Synset('present.n.01'), Synset('present.n.02'), 
Synset('present.n.03'), Synset('show.v.01'), Synset('present.v.02'), 
Synset('stage.v.01'), Synset('present.v.04'), Synset('present.v.05'), 
Synset('award.v.01'), Synset('give.v.08'), Synset('deliver.v.01'), 
Synset('introduce.v.01'), Synset('portray.v.04'), Synset('confront.v.03'), 
Synset('present.v.12'), Synset('salute.v.06'), Synset('present.a.01'), 
Synset('present.a.02')]
'''

총 18개의 서로 다른 semantic을 가지는 synset 객체가 반환됐습니다. Synset('present.n.01')의 파라미터 'present.n.01'은 POS 태그를 나타냅니다. present는 의미, n은 명사 품사, 01은 present가 명사로서 가지는 의미를 구분하는 인덱스입니다.

 

synset 객체가 가지는 여러 가지 속성을 살펴보겠습니다. Synset은 POS(품사), 정의(Definition), 부명제(Lemma) 등의 시맨틱적인 요소를 표현할 수 있습니다.

for synset in synsets:
  print('##### Synset name : ', synset.name(), '#####')
  print('POS :', synset.lexname())
  print('Definition :', synset.definition())
  print('Lemmas :', synset.lemma_names(), '\n')
'''
##### Synset name :  present.n.01 #####
POS : noun.time
Definition : the period of time that is happening now; any continuous stretch of time including the moment of speech
Lemmas : ['present', 'nowadays'] 

##### Synset name :  present.n.02 #####
POS : noun.possession
Definition : something presented as a gift
Lemmas : ['present'] 

...

##### Synset name :  show.v.01 #####
POS : verb.perception
Definition : give an exhibition of to an interested audience
Lemmas : ['show', 'demo', 'exhibit', 'present', 'demonstrate'] 

...

'''

Synset('present.n.01')과 Synset('present.n.02')는 명사지만 서로 다른 의미(Definition)를 가지고 있습니다. POS는 noun.time, noun.possession으로 다릅니다. Synset('show.v.01')은 동사로서 POS가 verb.perception이며, Definition은 '관객에게 전시물 등을 보여주다'라는 뜻입니다. 

 

Synset.path_similarity()

WordNet은 어휘 간의 관계를 유사도로 나타낼 수 있습니다. Synset.path_similarity() 메서드를 이용해 예제 단어의 상호 유사도를 겠습니다.

import pandas as pd
# synset 객체를 단어 별로 생성합니다.
tree = wn.synset('tree.n.01')
lion = wn.synset('lion.n.01')
tiger = wn.synset('tiger.n.02')
cat = wn.synset('cat.n.01')
dog = wn.synset('dog.n.01')

entities = [tree, lion, tiger, cat, dog]
similarities = []
entity_names = [entity.name().split('.')[0] for entity in entities]

# 단어별 synset을 반복하면서 다른 단어의 synset과 유사도를 측정합니다.
for entity in entities:
  similarity = [round(entity.path_similarity(compared_entity), 2)
                for compared_entity in entities]
  similarities.append(similarity)

# 개별 단어별 synset과 다른 단어의 synset과의 유사도를 DataFrame 형태로 저장합니다.
similarity_df = pd.DataFrame(similarities, columns=entity_names, index=entity_names)
similarity_df
'''
결과2
'''

결과2

lion은 tiger와 0.33으로 가장 유사하고, tree와 0.07로 가장 다르다.

 

nltk.corpus.sentiwordnet

SentiWordNet은 WordNet의 Synset과 유사한 Senti_synset 클래스를 가지고 있습니다. senti_synsets()는 wordnet.synsets()와 비슷하게 Senti_Synset 클래스를 리스트 형태로 반환합니다.

import nltk
from nltk.corpus import sentiwordnet as swn

senti_synsets = list(swn.senti_synsets('slow'))
print('senti_synsets() 반환 type :', type(senti_synsets))
print('senti_synsets() 반환 값 개수 :', len(senti_synsets))
print('senti_synsets() 반환 값 :', senti_synsets)
'''
senti_synsets() 반환 type : <class 'list'>
senti_synsets() 반환 값 개수 : 11
senti_synsets() 반환 값 : [SentiSynset('decelerate.v.01'), SentiSynset('slow.v.02'),
SentiSynset('slow.v.03'), SentiSynset('slow.a.01'), SentiSynset('slow.a.02'),
SentiSynset('dense.s.04'), SentiSynset('slow.a.04'), SentiSynset('boring.s.01'),
SentiSynset('dull.s.08'), SentiSynset('slowly.r.01'), SentiSynset('behind.r.03')]
'''

SentiSynset 객체는 객관성 지수와 감성 지수를 가지고 있습니다. 감정 지수는 다시 긍정, 부정 지수로 나뉩니다. 감성적이지 않은 단어는 객관성 지수가 1이되고, 감성 지수는 모두 0이 됩니다. 'father'와 'fabulous'라는 두 단어의 감성/객관성 지수를 구해보겠습니다.

 

 

SentiSynset.pos_score() & SentiSynset.neg_score() & SentiSynset.obj_score()

import nltk
from nltk.corpus import sentiwordnet as swn

father = swn.senti_synset('father.n.01')
print('father 긍정 감성 지수:', father.pos_score())
print('father 부정 감성 지수:', father.neg_score())
print('father 객관성 지수:', father.obj_score())
print('\n')

fabulous = swn.senti_synset('fabulous.a.01')
print('fabulous 긍정 감성 지수:', fabulous.pos_score())
print('fabulous 부정 감성 지수:', fabulous.neg_score())
print('fabulous 객관성 지수:', fabulous.obj_score())
'''
father 긍정 감성 지수: 0.0
father 부정 감성 지수: 0.0
father 객관성 지수: 1.0


fabulous 긍정 감성 지수: 0.875
fabulous 부정 감성 지수: 0.125
fabulous 객관성 지수: 0.0
'''

father라는 단어는 긍정/부정과는 상관 없는 단어로 정의돼있고, fabulous는 긍정적인 단어로 정의돼있습니다.

 

 

SentiWordNet을 이용한 영화 감상평 감성 분석

이제 SentWordNet을 이용해 IMDB 영화 감상평 감성 분석을 SentiWordNet Lexicon 기반으로 수행해보겠습니다.

개략적인 순서는 다음과 같습니다.

  1. 문서(Document)를 문장(Sentence) 단위로 분해
  2. 다시 문장을 단어(Word) 단위로 토큰화하고 품사 태깅
  3. 품사 태깅된 단어 기반으로 synset 객체와 senti_synset 객체를 생성
  4. Senti_synset에서 긍정/부정 감성 지수를 구하고 이를 모두 합산해 특정 임계값 이상 시 긍정감성, 그렇지 않으면 부정 감성으로 결정

 

SentiWordNet을 이용하기 위해서 WordNet을 이용해 문서를 단어로 토큰화하고 어근 추출(Lemmatization)과 품사 태깅(POS Tagging)을 적용해야 합니다. 먼저 품사 태깅을 수행하는 내부 함수를 생성하겠습니다.

from nltk.corpus import wordnet as wn

# 간단한 NTLK PennTreebank Tag를 기반으로 WordNet기반의 품사 Tag로 변환
def penn_to_wn(tag):
  if tag.startswith('J'):
    return wn.ADJ
  elif tag.startswith('N'):
    return wn.NOUN
  elif tag.startswith('R'):
    return wn.ADV
  elif tag.startswith('V'):
    return wn.VERB

이제 문서를 문장 → 단어 토큰 → 품사 태깅  SentiSynset 클래스를 생성하고 Polarity Score를 합산하는 함수를 생성하겠습니다.  각 단어는 (긍정 지수 - 부정 지수) > 0일 때 긍정, 그렇지 않으면 부정으로 예측합니다. 

 

pos_tag

from nltk.stem import WordNetLemmatizer
from nltk.corpus import sentiwordnet as swn
from nltk import sent_tokenize, word_tokenize, pos_tag

def swn_polarity(text):
  # 감성 지수 초기화
  sentiment = 0.0
  tokens_count = 0

  lemmatizer = WordNetLemmatizer()
  raw_sentences = sent_tokenize(text)
  # 분해된 문장 별로 단어 토큰 -> 품사 태깅 후에 SentiSynset 생성 -> 감성 지수 합산
  for raw_sentence in raw_sentences:
    # NLTK 기반의 품사 태깅 문장 추출
    tagged_sentence = pos_tag(word_tokenize(raw_sentence))
    for word, tag in tagged_sentence:
      # WordNet 기반 품사 태깅과 어근 추출
      wn_tag = penn_to_wn(tag)
      if wn_tag not in (wn.NOUN, wn.ADJ, wn.ADV):
        continue
      lemma = lemmatizer.lemmatize(word, pos=wn_tag)
      if not lemma:
        continue
      # 어근을 추출한 단어와 WordNet 기반 품사 태깅을 입력해 Synset 객체를 생성.
      synsets = wn.synsets(lemma, pos=wn_tag)
      if not synsets:
        continue
      # sentiwordnet의 감성 단어 분석으로 감성 synset 추출
      # 모든 단어에 대해 긍정 감성 지수는 +로, 부정 감성 지수는 -로 합산해 감성 지수 계산.
      synset = synsets[0]
      swn_synset = swn.senti_synset(synset.name())
      sentiment += (swn_synset.pos_score() - swn_synset.neg_score())
      tokens_count += 1

  if not tokens_count:
    return 0

  # 총 score가 0 이상일 경우 긍정(Positive) 1, 그렇지 안을 경우 부정(Negative) 0 반환
  if sentiment >=0:
    return 1
  
  return 0

swn_polarity()함수를 IMDB 감상평의 개별 문서에 적용해 긍/부정 감성을 예측하겠습니다. Pandas의 apply lambda 구문으로 개별 감상평 텍스트에 적용합니다. 위에서 생성한 review_df에 'preds'를 추가해 swn_polarity()로 반환된 결과를 저장하고 실제 감성 평가인 'sentiment' 칼럼과 비교해 정확도, 정밀도, 재현율 값을 모두 추출해보겠습니다.

review_df['preds'] = review_df['review'].apply(lambda x : swn_polarity(x))
y_target = review_df['sentiment'].values
preds = review_df['preds'].values

SentiWordNet의 감성 분석 예측 성능을 살펴보겠습니다.

from sklearn.metrics import accuracy_score, confusion_matrix, precision_score
from sklearn.metrics import recall_score, f1_score, roc_auc_score
import numpy as np

print(confusion_matrix(y_target, preds))
print("정확도:", np.round(accuracy_score(y_target, preds), 4))
print('정밀도:', np.round(precision_score(y_target, preds), 4))
print("재현율:", np.round(recall_score(y_target, preds), 4))
'''
[[7668 4832]
 [3636 8864]]
정확도: 0.6613
정밀도: 0.6472
재현율: 0.7091
'''

정확도, 재현율이 0.66, 0.7로 그렇게 높지 않은 수치이빈다. SentiWordNet은 WordNet의 하위 모듈로서 감정 분석을 위한 프레임워크입니다. 이번엔 VADER로 IMDB의 영화평을 감성 분석해보겠습니다.

 

 

VADER를 이용한 감성 분석

VADER는 소셜 미디어의 감성 분석 용도로 만들어진 룰 기반의 Lexicon입니다. VADER는 NLTK 패키지의 서브 모듈, 단독 패키지로 제공됩니다.

 

NLTK 패키지 서브 모듈 다운로드

import nltk
nltk.download('vader_lexicon')

 

단독 패키지 다운로드

pip install vaderSentiment
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer

 

SentimentIntensityAnalyzer & SentimentIntensityAnalyzer.polarity_scores()

NLTK 서브 모듈로 간략하게 IMDB의 감상평 한 개를 감상 분석해 보겠습니다.

from nltk.sentiment.vader import SentimentIntensityAnalyzer

senti_analyzer = SentimentIntensityAnalyzer()
senti_scores = senti_analyzer.polarity_scores(review_df['review'][0])
print(senti_scores)
'''
{'neg': 0.065, 'neu': 0.732, 'pos': 0.203, 'compound': 0.9944}
'''

VADER를 이용하면 매우 쉽게 감성 분석을 수행할 수 있습니다. SentimentIntensityAnalyzer.polarity_scores() 메서드는 감성 점수를 구하고, 감성 점수가 특정 임계값 이상이면 긍정, 그렇지 않으면 부정으로 판단합니다. 반환값은 딕셔너리 형태의 감성 점수를 반환합니다. neg는 부정 감성, neu는 중립적인 감성, pos는 긍정 감성, compound는 neg, neu, pos를 적절히 조합해 -1 ~ 1 사이의 감성 지수를 표현한 값입니다. compound score을 기반으로 부정/긍정 감성 여부를 결정합니다.

 

compound가 보통 0.1 이상이면 긍정으로 판단하지만, 임계값을 적절히 조정할 수 있습니다. VADER를 이용해 IMDB의 감성 분석을 수행하겠습니다. 이를 위해 vader_polarity()함수를 새롭게 만들겠습니다. 입력 파라미터로 문서 Series와 threshold를 가집니다.

def vader_polarity(review, threshold=0.1):
    analyzer = SentimentIntensityAnalyzer()
    scores = analyzer.polarity_scores(review)
    
    # compound 값에 기반해 threshold 입력값보다 크면 1, 그렇지 않으면 0을 반환
    agg_score = scores['compound']
    final_sentiment = 1 if agg_score >= threshold else 0
    return final_sentiment

# apply lambda 식을 이용해 레코드별로 vader_polarity()를 수행하고 결과를 'vader_preds'에 저장
review_df['vader_preds'] = review_df['review'].apply(lambda x : vader_polarity(x, 0.1))
y_target = review_df['sentiment'].values
vader_preds = review_df['vader_preds'].values

print(confusion_matirx(y_target, vader_preds))
print("정확도:", np.round(accuracy_score(y_target, vader_preds), 4))
print("정밀도:", np.round(precision_score(y_target, vader_preds), 4))
print("재현율:", np.round(recall_score(y_target, vader_preds), 4))
'''
[[ 6736  5764]
 [ 1867 10633]]
정확도: 0.6948
정밀도: 0.6485
재현율: 0.8506
'''

SentimentWordNet을 사용했을 때보다 정확도, 재현율에서 그 값이 상승했습니다. 감성 사전을 이용한 감성 분석 즉, 비지도학습 분류 기반은 지도학습 분류 기반보다 예측 성능이 떨어지지만, 결정 클래스 값이 없는 걸 고려한다면 일정 수준의 예측 성능에 만족할 수 있을 것입니다.

 

 

토픽 모델링(Topic Modeling) - 20 뉴스그룹

토픽 모델링(Topic Modleing)이란 문서 집합에 숨어 있는 주제를 찾는 것입니다. 주제가 단지 더 함축적인 의미로 문장을 요약하는 것이 아닌 숨겨진 주제를 효과적으로 표현할 수 있는 중심 단어를 추출합니다.

 

머신러닝 기반의 토픽 모델링에 자주 사용되는 기법은 LSA(Latent Semantic Analysis)와 LDA(Latent Dirichlet Allocation)입니다.  이번엔 LDA만 이용해 토픽 모델링을 수행하겠습니다. 토픽 모델링의 LDA는 앞서 차원 축소의 LDA(Linear Discriminant Analysis)와는 서로 다른 알고리즘임에 유의하기 바랍니다.

 

토픽 모델링은 20 뉴스 그룹 데이터 세트를 이용해 적용해 보겠습니다. 이는 20가지 주제의 뉴스 데이터를 가지고 있습니다. 

['alt.atheism', 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware',
'comp.sys.mac.hardware', 'comp.windows.x', 'misc.forsale', 'rec.autos', 'rec.motorcycles',
'rec.sport.baseball', 'rec.sport.hockey', 'sci.crypt', 'sci.electronics', 'sci.med', 'sci.space',
'soc.religion.christian', 'talk.politics.guns', 'talk.politics.mideast', 'talk.politics.misc',
'talk.religion.misc']

이 중 8가지(모토사이클, 야구, 그래픽스, 윈도우, 중동, 기독교, 전자공학, 의학) 주제를 추출하고 이들 텍스트에 LDA 기반의 토픽 모델링을 적용해보겠습니다.

 

 

LatentDirichletAllocation

사이킷런은 LDA(Latent Dirichlet Allocation) 기반의 토픽 모델링을 LatentDirichletAllocation 클래스로 제공합니다. fetch_20newsgroups() API의 categories 파라미터를 통해 필요한 주제만 추출하고, 텍스트를 Count 기반으로 벡터화 변환하겠습니다. LDA는 Count 기반 벡터화만 사용합니다.

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# 모토 사이클, 야구, 그래픽스, 윈도우즈, 중동, 기독교, 전자공학, 의학 8개 주제를 추출.
cats = ['rec.motorcycles', 'rec.sport.baseball', 'comp.graphics','comp.windows.x',\
        'talk.politics.mideast', 'soc.religion.christian', 'sci.electronics', 'sci.med']

# 위에서 cats 변수로 기재된 카테고리만 추출. fetch_20newsgroups()의 categories에 cats 입력
news_df = fetch_20newsgroups(subset='all', remove=('headers', 'footers', 'quotes'),\
                             categories = cats, random_state=0)
# LDA는 Count기반의 벡터화만 적용합니다.
count_vect = CountVectorizer(max_df=0.95, max_features=1000, min_df=2, stop_words='english',\
                             ngram_range=(1, 2))
feat_vect = count_vect.fit_transform(news_df.data)
print('CountVectorizer Shape', feat_vect.shape)
'''
CountVectorizer Shape (7862, 1000)
'''

CountVectorizer 객체 변수인 feat_vect는 모두 7862개의 문서가 1000개의 피처로 구성된 행렬 데이터입니다. LatentDirichletAllocation 클래스의 n_components 파라미터를 통해 토픽의 개수를 조정합니다. 

lda = LatentDirichletAllocation(n_components=8, random_state=0)
lda.fit(feat_vect)

LatentDirichletAllocation.fit을 수행하면 LatentDerichletAllocation 객체는 components_ 속성값을 가지는데, 이는 각 토픽 별로 각 피처(word)가 얼마나 많이 그 토픽에 할당됐는지에 대한 수치를 가지고 있습니다. 높은 값을 가질 수록 해당 피처(word)는 그 토픽의 중심 word가 됩니다.

print(lda.components_.shape)
lda.components_
'''
(8, 1000)
array([[3.60992018e+01, 1.35626798e+02, 2.15751867e+01, ...,
        3.02911688e+01, 8.66830093e+01, 6.79285199e+01],
       [1.25199920e-01, 1.44401815e+01, 1.25045596e-01, ...,
        1.81506995e+02, 1.25097844e-01, 9.39593286e+01],
       [3.34762663e+02, 1.25176265e-01, 1.46743299e+02, ...,
        1.25105772e-01, 3.63689741e+01, 1.25025218e-01],
       ...,
       [3.60204965e+01, 2.08640688e+01, 4.29606813e+00, ...,
        1.45056650e+01, 8.33854413e+00, 1.55690009e+01],
       [1.25128711e-01, 1.25247756e-01, 1.25005143e-01, ...,
        9.17278769e+01, 1.25177668e-01, 3.74575887e+01],
       [5.49258690e+01, 4.47009532e+00, 9.88524814e+00, ...,
        4.87048440e+01, 1.25034678e-01, 1.25074632e-01]])
'''

components_의 shape는 (8, 1000)으로 8개 토픽에 대한 피처의 연관도 값을 가지고 있습니다. 이렇게 출력하면 각 토픽별 word(피처) 연관도를 보기 어려우므로 각 토픽별로 연관도가 높은 순으로 word를 나열해주는 display_topics()함수를 만들겠습니다.

def display_topics(model, feature_names, no_top_words):
  for topic_index, topic in enumerate(model.components_):
    print('Topic #', topic_index)

    # components_ array에서 가장 값이 큰 순으로 정렬했을 때, 그 값이 array 인덱스를 반환.
    topic_word_indexes = topic.argsort()[::-1]
    top_indexes = topic_word_indexes[:no_top_words]

    # top_indexes 대상인 인덱스 별로 feature_names에 해당하는 word feature 추출 후 join으로 concat
    feature_concat = ' '.join([feature_names[i] for i in top_indexes])
    print(feature_concat)

# CountVectorizer 객체 내의 전체 word의 명칭을 get_features_names()를 통해 추출
feature_names = count_vect.get_feature_names()

# 토픽 별 가장 연관도가 높은 word를 15개만 추출
display_topics(lda, feature_names, 15)
'''
Topic # 0
year 10 game medical health team 12 20 disease cancer 1993 games years patients good
Topic # 1
don just like know people said think time ve didn right going say ll way
Topic # 2
image file jpeg program gif images output format files color entry 00 use bit 03
Topic # 3
like know don think use does just good time book read information people used post
Topic # 4
armenian israel armenians jews turkish people israeli jewish government war dos dos turkey arab armenia 000
Topic # 5
edu com available graphics ftp data pub motif mail widget software mit information version sun
Topic # 6
god people jesus church believe christ does christian say think christians bible faith sin life
Topic # 7
use dos thanks windows using window does display help like problem server need know run
'''

20 뉴스그룹에서 8개 주제로 문서를 추출했는데, Topic #0은 얼핏 의학처럼 보이고, Topic #1은 불명확, Topic #2는 컴퓨터 그래픽스 관련 주제로 명확해보입니다. Topic #3은 불명확, Topic #4는 중동 관련 주제로 명확해보입니다. Topic #5은 불명확, Topic #6은 종교로 명확해보이고, Topic 7은 얼핏 window 같아보이기도 합니다. 특히, 모토사이클, 야구 주제의 경우 명확한 주제어가 추출되지 않았습니다.

 

 

문서 군집화 소개와 실습(Opinion Review 데이터 세트)

문서 군집화(Document)는 비슷한 텍스트 구성의 문서를 군집화(Clustering)하는 것입니다. 앞서 소개한 텍스트 분류 기반의 문서 분류와 비슷하지만, 차이점은 문서 군집화는 학습 데이터 세트가 필요 없는 비지도학습 기반으로 동작한다는 것입니다.

 

 

Opinion Review 데이터 세트를 이용한 문서 군집화 수행하기

문서 군집화를 수행할 데이터 세트는 UCI 머신러닝 레포지토리에 있는 Opinion Review 데이터 세트입니다. 해당 데이터 세트는 51개의 텍스트 파일로 구성돼 있고, 각 파일은 Tripadvisor(호텔), Edmunds.com(자동차), Amazon.com(전자제품) 사이트에서 가져온 리뷰 문서입니다. 각 문서는 약 100개 정도의 문장을 가지고 있습니다.

 

해당 데이터 세트는 https://archive-beta.ics.uci.edu/ml/datasets/opinosis+opinion+frasl+review 사이트에서 내려받을 수 있습니다.

 

UCI Machine Learning Repository

 

archive-beta.ics.uci.edu

 

glob, os

import pandas as pd
import glob, os

path = r'/content/drive/MyDrive/military/topics'
# path로 지정한 디렉터리 밑에 있는 모든 .data 파일의 파일명을 리스트로 취합.
all_files = glob.glob(os.path.join(path, "*data"))
filename_list = []
opinion_text = []

# 개별 파일의 파일명은 filename_list로 취합,
# 개별 파일 내용을 DataFrame 로딩 후 다시 string으로 변환해 opinion_text list로 취합
for file_ in all_files:
  # 개별 파일을 읽어서 DataFrame으로 생성
  df = pd.read_table(file_, index_col=None, header=0, encoding='latin1')

  # 절대 경로로 주어진 파일명을 가공. 리눅스에서 수행할 때는 다음 \\을 /로 변경
  # 맨 마지막 .data 확장자도 제거
  filename_ = file_.split('/')[-1]
  filename = filename_.split('.')[0]

  # 파일명 list와 파일 내용 list에 파일명과 파일 내용을 추가.
  filename_list.append(filename)
  opinion_text.append(df.to_string())

# 파일명 list와 파일 내용 list 객체를 DataFrame으로 생성
document_df = pd.DataFrame({'filename':filename_list, 'opinion_text':opinion_text})
document_df.head()
'''
결과1
'''

결과1

filename만으로 opinion_text가 어떤 제품/서비스에 대한 리뷰인지 알 수 있습니다. 

 

문서를 TF-IDF 형태로 피처 벡터화하겠습니다. tokenizer는 이전에 만든 LemNormalize()함수를 이용할 것이며 ngram은 (1, 2)로 하고, min_df와 max_df 범위를 설정해 피처의 개수를 제한하겠습니다. document_df['opinion_text']가 분석할 문서이므로 TF-IDF 변환해 피처 벡터화 시키겠습니다.

from nltk.stem import WordNetLemmatizer
import nltk
import string

remove_punct_dict = dict((ord(punct), None) for punct in string.punctuation)
lemmar = WordNetLemmatizer()

def LemTokens(tokens):
    return [lemmar.lemmatize(token) for token in tokens]

def LemNormalize(text):
    return LemTokens(nltk.word_tokenize(text.lower().translate(remove_punct_dict)))
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vect = TfidfVectorizer(tokenizer=LemNormalize, stop_words='english',\
                             ngram_range=(1, 2), min_df=0.05, max_df=0.85)
# opinion_text 칼럼 값으로 피처 벡터화 수행
feature_vect = tfidf_vect.fit_transform(document_df['opinion_text'])

TF-IDF 피처 벡터화된 행렬 데이터에 대해 군집화를 해보겠습니다. 군집화 기법은 K-평균 기법을 사용하겠습니다. 문서의 유형은 전자제품, 자동차, 호텔로 돼 있습니다. 더 작은 범주 나눌 수도 있는데, 예를 들어 전자 제품은 네비게이션, 아이팟 .. 등으로 분류할 수 있습니다. 

 

5개의 Centroid 기반으로 어떻게 군집화되는지 확인해보겠습니다. 최대 반복 횟수 max_iter는 10000으로 설정합니다.

from sklearn.cluster import KMeans

# 5개 집합으로 군집화 수행.
km_cluster = KMeans(n_clusters=5, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_

document_df['cluster_label'] = cluster_label
document_df.head()
'''
결과2
'''

결과2

 

Cluster #0은 호텔에 대한 리뷰로 군집화 돼 있음을 알 수 있습니다.

document_df[document_df['cluster_label']==0].sort_values(by='filename')
'''
결과3
'''

결과3

 

Cluster #1은 자동차와 전자제품이 섞인 리뷰로 군집화 돼 있습니다.

document_df[document_df['cluster_label']==1].sort_values(by='filename')
'''
결과4
'''

결과4

Cluster #2는 전자제품 리뷰로 잘 군집화 돼 있음을 알 수 있습니다.

document_df[document_df['cluster_label']==2].sort_values(by='filename')
'''
결과5
'''

결과5

 

Cluster #3은 전자제품 리뷰(특히 차량용 네비)로 이루어진 군집임을 알 수 있습니다. 파일명 xxx_garmin_nuvi_xxx는 차량용 네비게이션입니다.

document_df[document_df['cluster_label']==3].sort_values(by='filename')
'''
결과6
'''

 

Cluster #4는 kindle로 전자제품 리뷰로 잘 군집화 됐음을 알 수 있습니다.

document_df[document_df['cluster_label']==4].sort_values(by='filename')
'''
결과7
'''

결과7

 

전반적으로 군집 개수가 많게 설정돼 있어서 세분화되어 군집화된 경향이 있습니다. 중심 개수를 3개로 낮춰서 군집화 결과를 확인해보겠습니다.

from sklearn.cluster import KMeans

# 3개의 집합으로 군집화
km_cluster = KMeans(n_clusters=3, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_

# 소속 군집을 cluster_label 칼럼으로 할당하고 cluster_label 값으로 정렬
document_df['cluster_label'] = cluster_label
document_df.sort_values(by='cluster_label')
'''
결과8
'''

결과8

전자제품, 호텔, 자동차로 잘 군집화 됐음을 잘 알 수 있습니다.

 

 

군집별 핵심 단어 추출하기

각 군집(Cluster)에 속한 문서는 핵심 단어를 주축으로 준집화 돼 있을 것인데, 각 군집의 핵심 단어를 확인해보겠습니다. KMeans 객체는 각 군집을 구성하는 단어 피처가 군집의 중심(Centorid)을 기준으로 얼마나 가깝게 위치해 있는지 cluster_centers_라는 속성으로 제공합니다. 행은 개별 군집을, 열은 개별 피처를 의미합니다. 이 배열 내의 값은 개별 군집 내의 상대 위치를 숫자로 표현한 일종의 좌표값입니다. cluster_centers_[0, 1]은 0번 군집에서 두 번째 피처의 위치값입니다.

cluster_centers = km_cluster.cluster_centers_
print('cluster_centers shape :', cluster_centers.shape)
print(cluster_centers)
'''
cluster_centers shape : (3, 4611)
[[0.01005322 0.         0.         ... 0.00706287 0.         0.        ]
 [0.         0.00099499 0.00174637 ... 0.         0.00183397 0.00144581]
 [0.         0.00092551 0.         ... 0.         0.         0.        ]]
'''

각 피처의 위치가 개별 군집의 중심과 얼마나 가까운가를 상대 값으로 나타낸 것입니다. 0 ~ 1까지의 값을 가질 수 있으며 1에 가까울 수록 중심과 가깝다는 의미입니다.

 

ndarray.argsort()

cluster_centers 배열 내에서 각 군집 별 상대 위치값이 큰 순서대로 핵심 단어(피처)를 키 값으로 갖는 Dict 객체 변수를 반환하는 함수 get_cluster_details()함수를 만들겠습니다. 주요 로직은 cluster_centers_은 ndarray인데, ndarray.argsort()[:, ::-1]를 이용하면 cluster_centers_ 내에 값이 큰 순으로 정렬된 위치 인덱스 값을 반환합니다.(큰 값으로 정렬한 값을 반환하는게 아니라 큰 값을 가진 배열 내 위치 인덱스 값을 반환하는 것입니다.) 이 위치 인덱스를 통해 핵심 단어(피처)명과 상대 위치값을 추출해 Dict객체 변수에 값을 기록하고 이를 반환합니다.

# 군집별 top n 핵심 단어, 그 단어의 중심 위치 상댓값, 대상 파일명을 반환함.
def get_cluster_details(cluster_model, cluster_data, feature_names, clusters_num,\
                        top_n_features=10):
  cluster_details = {}
  # cluster_centers array의 값이 큰 순으로 정렬된 인덱스 값을 반환
  # 군집 중심점(centroid)별 할당된 word 피처들의 거리값이 큰 순으로 값을 구하기 위함.
  centroid_feature_ordered_ind = cluster_model.cluster_centers_.argsort()[:, ::-1]

  # 개별 군집별로 반복하면서 핵심 단어, 그 단어의 중심 위치 상댓값, 대상 파일명 입력
  for cluster_num in range(clusters_num):
    cluster_details[cluster_num] = {}
    cluster_details[cluster_num]['cluster'] = clusters_num

    # cluster_centers_.argsort()[:, ::-1]로 구한 인덱스를 이용해 top n 피처 단어를 구함.
    top_feature_indexes = centroid_feature_ordered_ind[cluster_num, :top_n_features]
    top_features = [feature_names[ind] for ind in top_feature_indexes]

    # top_feature_indexes를 이용해 해당 피처 단어의 중심 위치 상대값을 구함.
    top_features_values = cluster_model.cluster_centers_[cluster_num,\
                                                        top_feature_indexes].tolist()
    
    # cluster_details 딕셔너리 객체에 개별 군집별 핵심 단어와 중심위치 상대값, 해당 파일명 입력
    cluster_details[cluster_num]['top_features'] = top_features
    cluster_details[cluster_num]['top_features_values'] = top_features_values
    filenames = cluster_data[cluster_data['cluster_label'] == cluster_num]['filename']
    filenames = filenames.values.tolist()

    cluster_details[cluster_num]['filenames'] = filenames

  return cluster_details

get_cluster_details()가 반환하는 딕셔러닉 객체의 군집 번호, 핵심 단어, 핵심 단어 중심 위치 상대값, 파일명 값을 보기 좋게 출력하는 print_cluster_details()함수를 만들겠습니다.

def print_cluster_details(cluster_details):
  for cluster_num, cluster_detail in cluster_details.items():
    print('###### Cluster {0}'.format(cluster_num))
    print('Top features:', cluster_detail['top_features'])
    print('Reviews 파일명:', cluster_detail['filenames'][:7])
    print('===============================================')

이제 위에서 생성한 get_cluster_details(), prinr_cluster_details() 함수를 실행시켜보겠습니다. get_cluster_details()함수의 인자로는 KMeans 객체, document_df, 피처명 리스트, 전체 군집 개수, 핵심 단어 추출 개수가 있습니다. 피처명 리스트는 tfidf_vect 객체에서 get_feature_names()로 추출하겠습니다.

feature_names = tfidf_vect.get_feature_names()
cluster_details = get_cluster_details(cluster_model=km_cluster,\ 
	cluster_data=document_df, feature_names=feature_names, clusters_num=3, top_n_features=10)
print_cluster_details(cluster_details)
'''
###### Cluster 0
Top features: ['screen', 'battery', 'keyboard', 'battery life', 'life', 'kindle', 
'direction', 'video', 'size', 'voice']
Reviews 파일명: ['battery-life_amazon_kindle', 'accuracy_garmin_nuvi_255W_gps', 
'battery-life_ipod_nano_8gb', 'battery-life_netbook_1005ha', 'buttons_amazon_kindle', 
'display_garmin_nuvi_255W_gps', 'directions_garmin_nuvi_255W_gps']
===============================================
###### Cluster 1
Top features: ['room', 'hotel', 'service', 'staff', 'food', 'location', 'bathroom', 
'clean', 'price', 'parking']
Reviews 파일명: ['bathroom_bestwestern_hotel_sfo', 'food_holiday_inn_london', 
'food_swissotel_chicago', 'free_bestwestern_hotel_sfo', 'location_bestwestern_hotel_sfo',
'price_holiday_inn_london', 'parking_bestwestern_hotel_sfo']
===============================================
###### Cluster 2
Top features: ['interior', 'seat', 'mileage', 'comfortable', 'gas', 'gas mileage', 
'transmission', 'car', 'performance', 'quality']
Reviews 파일명: ['comfort_toyota_camry_2007', 'comfort_honda_accord_2008', 
'interior_toyota_camry_2007','gas_mileage_toyota_camry_2007', 'interior_honda_accord_2008',
'mileage_honda_accord_2008',
'performance_honda_accord_2008']
===============================================
'''

포터블 전자제품 리뷰 군집 Cluster #0에서는 'screen', 'battery', 'life' 등 화면, 배터리 수명 등이 핵심 단어로 군집화되었습니다. 아무래도 모바일형이고 엔터테이먼트용 전자제품의 경우 화면 크기, 배터리가 주요 관심사인 것 같습니다.

 

호텔 리뷰 군집 Cluster #1에서는 'room', 'hotel', 'service' 등 방과 서비스 등이 핵심 단어로 군집화되었습니다.

 

자동차 리뷰 군집 Cluster #2에서는 'interior', 'seat', 'mileage', 'comfortable' 등 실내 인테리어, 좌석, 연료 효율 등이 핵심 단어로 군집화되었습니다. 토요타-혼다 같은 일본 자동차의 경우는 실내 인테리어, 연료 효율, 편안함이 주요 관심사로 보입니다.

 

 

 

문서 유사도

문서 유사도 측정 방법 - 코사인 유사도

문서와 문서 간의 유사도 비교는 일반적으로 코사인 유사도(Cosine Similarity)를 사용합니다. 코사인 유사도는 벡터와 벡터간 상호 방향성, 즉 두 벡터 사이의 사잇각을 구해 얼마나 유사한지 수치로 적용한 것입니다.

코사인 유사도

유사도 cosθ은 두 벡터의 내적을 두 벡터 크기로 나눈 것입니다.(즉, 내적 결과를 총 벡터의 크기로 정규화(L2 Norm)한 것)

$$similarity=A\cdot B\ =\cos \theta =\frac{A\cdot B}{\left|\left|{A}\right|\right|\left|\left|{B}\right|\right|}=\frac{\sum _{i\ =1}^nA_i\cdot B_i}{\sqrt{\sum _{i=1}^nA_i^2}\sqrt{\sum _{i=1}^nB_i^2}}$$

코사인 유사도가 문서의 유사도를 구하는 데 많이 사용되는 이유가 있습니다. 먼저, 문서를 피처 벡터화하면 차원이 매우 많은 희소 행렬이 되기 쉬운데, 이러한 희소 행렬 기반에서 문서와 문서의 벡터 간 크기를 이용한 유사도 지표(유클리드 거리 기반 지표)는 정확도가 떨어지기 쉽습니다. 또한, 문서가 매우 긴 경우 짧은 경우보다 단어의 빈도수도 더 많을 것이기에 빈도수(크기)에만 기반해서 공정한 비교를 할 수 없습니다. 

 

간단한 문서에 대해서 서로 간의 문서 유사도를 코사인 유사도를 이용해 구해보겠습니다. 먼저 두 개의 넘파이 배열에 대한 코사인 유사도를 구하는 cos_similarity()함수를 작성하겠습니다.

import numpy as np

def cos_similarity(v1, v2):
  dot_product = np.dot(v1, v2)
  l2_norm = (np.sqrt(sum(np.square(v1))*np.sqrt(sum(np.square(v2)))))
  similarity = dot_product / l2_norm

  return similarity

3개의 간단한 문서를 TF-IDF로 백터화시켜 행렬로 변환하겠습니다.

from sklearn.feature_extraction.text import TfidfVectorizer

doc_list = ['if you take the blue pill, the story ends',
            'if you take the red pill, you stay in Wonderland',
            'if you take the red pill, I show you how deep the rabbit\
             hole goes']
tfidf_vect_simple = TfidfVectorizer()
feature_vect_simple = tfidf_vect_simple.fit_transform(doc_list)
print(feature_vect_simple.shape)
'''
(3, 18)
'''

 

CSR.todense()

feature_vect_simple은 CSR형식의 희소행렬 이므로 todense()로 밀집 행렬(원래 상태)로 바꾸고 이를 다시 ndarray로 변경해줍니다. (CSR → np.matrix → ndarray로 바뀝니다.)

# TfidfVectorizer로 transform()한 결과는 희소 행렬이므로 밀집 행렬로 변환.
feature_vect_dense = feature_vect_simple.todense()

# 첫 번째 문장과 두 번째 문장의 피처 벡터 추출
vect1 = np.array(feature_vect_dense[0]).reshape(-1,)
vect2 = np.array(feature_vect_dense[1]).reshape(-1,)

# 첫 번째 문장과 두 번째 문장의 피처 벡터로 두 개 문장의 코사인 유사도 추출.
similarity_simple = cos_similarity(vect1, vect2)
print('문장 1, 문장 2 Cosine 유사도: {0:.3f}'.format(similarity_simple))
'''
문장 1, 문장 2 Cosine 유사도: 0.402
'''

feature_vect_dense[0]은 첫 번째 문장, [1]은 두 번째 문장입니다. 다음으로 첫 번째 문장, 세 번째 문장 그리고 두 번째 문장, 세 번째 문장의 유사도를 측정해보겠습니다.

vect1 = np.array(feature_vect_dense[0]).reshape(-1,)
vect3 = np.array(feature_vect_dense[2]).reshape(-1,)
similarity_simple = cos_similarity(vect1, vect3)
print('문장 1, 문장 3 Cosine 유사도: {0:.3f}'.format(similarity_simple))

vect2 = np.array(feature_vect_dense[1]).reshape(-1,)
vect3 = np.array(feature_vect_dense[2]).reshape(-1,)
similarity_simple = cos_similarity(vect2, vect3)
print('문장 2, 문장 3 Cosine 유사도: {0:.3f}'.format(similarity_simple))
'''
문장 1, 문장 3 Cosine 유사도: 0.404
문장 2, 문장 3 Cosine 유사도: 0.456
'''

 

sklearn.metrics.pairwise.cosine_similarity

사이킷런은 코사인 유사도를 측정해주는 API를 제공합니다. 첫 번째 파라미터는 비교 기준이 되는 문서의 피처 행렬, 두 번째는 비교 당하는 문서의 피처 행렬입니다. 이 API는 희소 행렬, 밀집 행렬 모두 가능하고, 행렬 또는 배열도 모두 가능합니다. 따라서 앞서 한 것과 같이 별도의 변환 작업이 필요 없습니다. 첫 번째, 두 번째, 세번째 문서의 유사도를 측정해보겠습니다.

from sklearn.metrics.pairwise import cosine_similarity

similarity_simple_pair = cosine_similarity(feature_vect_simple[0], feature_vect_simple)
print(similarity_simple_pair)
'''
[[1.         0.40207758 0.40425045]]
'''

첫 번째 유사도 값인 1은 비교기준과 첫 번째 문서(자기 자신)에 대한 유사도 측정값, 두 번째 유사도 값 0.4는 두 번째 문서의 유사도 측정값, 세 번째는 세 번째 문서의 유사도 측정값입니다. 자기 자신에 대한 유사도값은 feature_vect_simple[1:]로 제외할 수 있습니다. 

 

cosine_similarity()함수는 기준 벡터를 행렬로 입력할 수 있어서 각 원소가 기준이 될 때 비교 문서에 대한 코사인 유사도를 구할 수 있습니다. 하나만 설명해 보자면, 두 번째 로우는 기준 벡터가 두 번째 문서일 때 각 문서에 대한 코사인 유사도를 나타냅니다.

similarity_simple_pair = cosine_similarity(feature_vect_simple, feature_vect_simple)
print(similarity_simple_pair)
print('shape:', similarity_simple_pair.shape)
'''
[[1.         0.40207758 0.40425045]
 [0.40207758 1.         0.45647296]
 [0.40425045 0.45647296 1.        ]]
shape: (3, 3)
'''

 

 

Opinion Review 데이터 세트를 이용한 문서 유사도 측정

앞서 문서 군집화에서 사용한 Opinion Review 데이터 세트를 이용해 이들 문서 간에 유사도를 측정해보겠습니다. 다시 데이터 세트를 로딩하고 문서 군집화를 적용해보겠습니다.

import pandas as pd
import glob, os
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from nltk.stem import WordNetLemmatizer
import nltk
import string

remove_punct_dict = dict((ord(punct), None) for punct in string.punctuation)
lemmar = WordNetLemmatizer()

def LemTokens(tokens):
    return [lemmar.lemmatize(token) for token in tokens]

def LemNormalize(text):
    return LemTokens(nltk.word_tokenize(text.lower().translate(remove_punct_dict)))


path = r'/content/drive/MyDrive/military/topics'
all_files = glob.glob(os.path.join(path, '*.data'))
filename_list = []
opinion_text = []

for file_ in all_files:
  df = pd.read_table(file_, index_col=None, header=0, encoding='latin1')
  filename_ = file_.split('/')[-1]
  filename = filename_.split('.')[0]
  filename_list.append(filename)
  opinion_text.append(df.to_string())

document_df = pd.DataFrame({'filename':filename_list, 'opinion_text':opinion_text})
tfidf_vect = TfidfVectorizer(tokenizer=LemNormalize, stop_words='english',\
                             ngram_range=(1, 2), min_df=0.05, max_df=0.85)
feature_vect = tfidf_vect.fit_transform(document_df['opinion_text'])
km_cluster = KMeans(n_clusters=3, max_iter=10000, random_state=0)
km_cluster.fit(feature_vect)
cluster_label = km_cluster.labels_
cluster_centers = km_cluster.cluster_centers_
document_df['cluster_label'] = cluster_label

문서 군집화 절에서는 전자제품, 호텔, 자동차로 군집화됐는데, 이 중 호텔로 군집화된 문서를 이용해 문서 간 유사도를 측정해보겠습니다. 문서를 피처 백터화하면 문서 속 단어의 빈도수를 피처(단어)의 값으로 벡터화됩니다. 이 피처 벡터화된 테이터를 cosine_simularity()를 이용해 유사도를 구하겠습니다.

from sklearn.metrics.pairwise import cosine_similarity

# cluster_label=1인 데이터는 호텔로 군집화된 데이터임. DataFrame에서 해당 인덱스를 추출
hotel_indexes = document_df[document_df['cluster_label']==1].index
print('호텔로 군집화된 문서들의 DataFrame Index:', hotel_indexes)

# 호텔로 군집화된 데이터 중 첫 번째 문서를 추출해 파일명 표시.
comparison_docname = document_df.iloc[hotel_indexes[0]]['filename']
print('##### 비교 기준 문서명:', comparison_docname, ' 와 타 문서 유사도 #####')

'''document_df에서 추출한 Index 객체를 feature_vect로 입력해 호텔 군집화된 feature_vect 추출
이를 이용해 호텔로 군집화된 문서 중 첫 번째 문서와 다른 문서 간의 코사인 유사도 측정.'''
similarity_pair = cosine_similarity(feature_vect[hotel_indexes[0]], feature_vect[hotel_indexes])
print(similarity_pair)
'''
호텔로 군집화된 문서들의 DataFrame Index: Int64Index([0, 15, 17, 18, 21, 24, 27,
28, 31, 32, 33, 38, 41, 43, 44, 45], dtype='int64')
##### 비교 기준 문서명: bathroom_bestwestern_hotel_sfo  와 타 문서 유사도 #####
[[1.         0.0430688  0.05221059 0.06189595 0.05846178 0.11742762
  0.03638665 0.51442299 0.06193118 0.32619948 0.38038865 0.11282857
  0.13989623 0.07049362 0.1386783  0.09518068]]
'''

숫자로만 봐서는 직관도가 떨어지므로 첫 번째 문서와 다른 문서 간의 유사도가 높은 순으로 정렬하고 시각화해보겠습니다.

import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

# 첫 번째 문서와 타 문서 간 유사도가 큰 순으로 정렬한 인덱스를 추출하되 자기 자신은 제외.
sorted_index = similarity_pair.argsort()[:, ::-1]
sorted_index = sorted_index[:, 1:]

# 유사도가 큰 순으로 hotel_indexes를 추출해 재정렬
hotel_sorted_indexes = hotel_indexes[sorted_index.reshape(-1)]

# 유사도가 큰 순으로 유사도 값을 재정렬하되 자기 자신은 제외
hotel_1_sim_value = np.sort(similarity_pair.reshape(-1))[::-1]
hotel_1_sim_value = hotel_1_sim_value[1:]

# 유사도가 큰 순으로 정렬된 인덱스와 유사도 값을 이용해
# 파일명과 유사도 값을 막대 그래프로 시각화
hotel_1_sim_df = pd.DataFrame()
hotel_1_sim_df['filename'] = document_df.iloc[hotel_sorted_indexes]['filename']
hotel_1_sim_df['similarity'] = hotel_1_sim_value

sns.barplot(x='similarity', y='filename', data=hotel_1_sim_df)
plt.title(comparison_docname)
'''
결과1
'''

결과1

샌프란시스코의 Best Western Hotel Bathroom Review인 bathroom_bestwestrn_hotel_sfo와 가장 비슷한 문서는 room_holiday_inn_london입니다. 0.514의 코사인 유사도 값을 나타내고 있습니다.

 

 

 

한글 텍스트 처리 - 네이버 영화 평점 감성 분석

한글 NLP 처리의 어려움

한글의 경우 '띄어 쓰기', '다양한 조사' 등을 잘못 사용하면 그 의미가 왜곡될 수 있습니다. 영어의 경우 띄어쓰기를 잘못하면 의미가 왜곡되는게 아니라 잘못된 또는 없는 단어로 인식하는게 대부분입니다. 예를 들면 '아버지 가방에 들어가신다', '아버지가 방에 들어가신다.'가 있고, 영어는 'My fatherenters a room', 'My father enters a room'으로 잘못된 띄어쓰기는 잘못된 단어로 분석에서 제외할 수 있습니다.

 

조사는 주어나 목적어를 위해 추가되며, 경우의 수가 워낙 많아서 어근 추출(Stemming/Lemmatization) 등의 전처리를 할 때 제거하기 까다롭습니다. '집'이라는 어근 단어를 기준으로 집은, 집이, 집으로, 집에서, 집에 등 다양한 형태의 조사가 존재합니다. 예를 들어 '집은' 이라는 뜻이 '은銀'을 의미하는 것인지 구분하기도 어렵고, '집은'을 '집 은'으로 띄어쓰기를 잘못하면 더욱 그렇습니다.

 

 

KoNLPy 소개

KoNLPy는 파이썬의 대표적인 한글 형태소 패키지입니다. 형태소란 '단어로서 의미를 가지는 최소 단위'로 주먹밥의 경우 '주먹' , '밥'이 형태소입니다. 형태소 분석(Morphological analysis)이란 말뭉치를 형태소 어근 단위로 쪼개고, 각 형태소에 품사 태깅(POS tagging)을 부착하는 작업을 말합니다.

 

KoNLPy는 C/C++, Java로 잘 만들어진 한글 형태소 엔진을 파이썬 래퍼(Wrapper) 기반으로 재작성한 패키지입니다. 꼬꼬마(Kkma), 한나눔(Hannanum), Komoran, 은전 한닢 프로젝트(Mecab), Twitter와 같이 5개 형태소 분석 모듈을 KoNLPy에서 이용할 수 있습니다. Mecab은 리눅스 환경의 KoNLPy에서만 가능합니다.

 

KoNLPy 설치는 https://konlpy.org/ko/latest/install/의 공식문서를 참조하면 됩니다. colab에서 실행할 경우 아래 명령어를 입력하시면 됩니다.

pip install konlpy

 

데이터 로딩

네이버 영화 평점 데이터는 https://github.com/e9t/nsmc에서 내려받을 수 있습니다. ratings.txt(전체 데이터), rating_train.txt(학습 데이터), rating_test.txt(테스트 데이터)를 모두 내려받습니다. 

 

rating_train.txt파일은 '\t'로 칼럼이 분리돼 있으므로 sep을 '\t'로 설정해 DataFrame을 로딩합니다.

import pandas as pd

train_df = pd.read_csv('/content/drive/MyDrive/military/NaverSentiment/ratings_train.txt', sep='\t')
train_df.head(3)
'''
결과1
'''

결과1

학습 데이터 세트의 0과 1의 Label 값 비율을 살펴보겠습니다. 1이 긍정, 0이 부정 감성입니다.

train_df['label'].value_counts()
'''
0    75173
1    74827
Name: label, dtype: int64
'''

train_df의 경우 리뷰 텍스트를 가지는 document에 Null 값이 존재하고, 문자가 아닌 숫자의 경우 단어의 의미가 부족하므로 re 모듈을 이용해 공백으로 변환합니다. 테스트 데이터 세트도 로딩한 후 같은 가공을 반복합니다.

import re

train_df = train_df.fillna(' ')
# 정규 표현식을 이용해 숫자를 공백으로 변경(정규 표현식으로 \d는 숫자를 의미함.)
train_df['document'] = train_df['document'].apply(lambda x : re.sub(r"\d+", " ", x))

# 테스트 데이터 세트를 로딩하고 동일하게 Null 및 숫자를 공백으로 변환
test_df = pd.read_csv('/content/drive/MyDrive/military/NaverSentiment/ratings_test.txt', sep='\t')
test_df = test_df.fillna(' ')
test_df['document'] = test_df['document'].apply(lambda x : re.sub(r"\d+", " ", x))

# id 칼럼 삭제 수행
train_df.drop('id', axis=1, inplace=True)
test_df.drop('id', axis=1, inplace=True)

 

피처 벡터화

Okt.morphs()

TF-IDF 방식으로 단어 벡터화를 하겠습니다. 그 전에 KoNLPy에서 한글 형태소 엔진은 SNS 분석에 적합한 Okt 클래스를 이용하겠습니다. Okt 객체의 morphs() 메서드는 입력 인자로 들어온 문장을 형태소 단어 형태로 토큰화해 list 객체로 반환합니다. Twitter 객체가 KoNLPy v0.4.5 이후로 Okt 클래스로 변경됐습니다. TfidfVectorizer의 tokenzer 파라미터에 입력할 tw_tokenizer()를 만들겠습니다.

from konlpy.tag import Okt

okt = Okt()
def tw_tokenizer(text):
  # 입력 인자로 들어온 텍스트를 형태소 단어로 토큰화해 리스트 형태로 반환
  tokens_ko = okt.morphs(text)
  return tokens_ko

피처 벡터화를 TfidfVectorizer를 이용해 수행하겠습니다.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV

# Okt 객체의 morphs() 객체를 이용한 tokenizer를 사용, ngram_range는 (1, 2)
tfidf_vect = TfidfVectorizer(tokenizer=tw_tokenizer, ngram_range=(1, 2), min_df=3, max_df=0.9)
tfidf_vect.fit(train_df['document'])
tfidf_matrix_train = tfidf_vect.transform(train_df['document'])

로지스틱 회귀를 이용해 분류 기반 감성을 분석을 수행하겠습니다. 로지스틱 회귀 하이퍼 파라미터 C의 최적화를 위해 GridSearchCV를 이용하겠습니다.

# 로지스틱 회귀를 이용해 감성 분석 분류 수행.
lg_clf = LogisticRegression(random_state=0)

# 파라미터 C 최적화를 위해 GridSearchCV를 이용.
params = { 'C': [1, 3.5, 4.5, 5.5, 10]}
grid_cv = GridSearchCV(lg_clf, param_grid=params, cv=3, scoring='accuracy', verbose=1)
grid_cv.fit(tfidf_matrix_train, train_df['label'])
print(grid_cv.best_params_, round(grid_cv.best_score_, 4))
'''
{'C': 3.5} 0.8593
'''

C: 3.5일 때 최고 0.8593의 정확도를 보였습니다. 테스트 세트를 이용해 최종 감정 분석 예측을 할 때, 학습 데이터로 학습한 TfidfVectorizer.transform() 해야 학습시 설정된 피처 개수가 같아집니다.

from sklearn.metrics import accuracy_score

# 학습 데이터를 적용한 TfidfVectorizer를 이용해 테스트 데이터를 TF-IDF 값으로 피처 변환함.
tfidf_matrix_test = tfidf_vect.transform(test_df['document'])

# classifier는 GridSearchCV에서 최적 파라미터로 학습된 classifier를 그대로 이용
best_estimator = grid_cv.best_estimator_
preds = best_estimator.predict(tfidf_matrix_test)
print('Logistic Regression 정확도: ', accuracy_score(test_df['label'], preds))
'''
Logistic Regression 정확도:  0.86186
'''

전처리 과정이 중요하다고 느낀게, test_df의 전처리를 하지 않고 Logistic Regression의 정확도를 구하면 0.5가 나온다.

 

 

 

정리

이 장에서는 텍스트 분석을 위한 기반 프로세스를 상세히 알아보고, 이를 통해 텍스트 분류, 감성 분석, 토픽 모델링, 텍스트 군집화 및 유사도 측정 등을 직접 구현해봤습니다.

 

머신러닝 기반의 텍스트 분석 프로세스는 아래와 같습니다.

  1. 텍스트 정규화 작업(텍스트 사전 정제)을 수행합니다.
  2. 이 단어들을 벡터화 합니다.
  3. 피처 벡터 데이터 세트에 머신러닝 모델을 학습하고 예측, 평가합니다.

 

텍스트 정규화 작업은 텍스트 클렌징 및 대소문자 변경, 단어 토큰화, 의미 없는 단어 필터링, 어근 추출 등 피처를 벡터화할 때 중요한 단어만 필터링하게끔 사전에 정리해주는 것을 의미합니다.

 

피처 벡터화는 BOW의 대표 방식인 Count 기반과 TF-IDF 기반 피처 벡터화를 설명했습니다. 일반적으로 문서의 문장이 긴 경우 TF-IDF 기반의 피처 벡터가 더 정확한 결과를 도출하는 데 도움이 됩니다. 이렇게 만들어진 피처 벡터 데이터 세트는 희소 행렬이며, 머신러닝 모델은 이러한 희소 행렬 기반에서 최적화돼야 합니다.

 

텍스트 분류절에서는 문서들을 피처 벡터화한 후 로지스틱 회귀를 적용해 문서를 지도학습 방식으로 예측 분류해봤습니다. 감성 분석절에서는 지도 학습 기반으로 긍정/부정 이진 분류를 적용한 방식과 SentiWordNet, VADER와 같은 감성 사전 Lexicon을 이용한 방식 두 가지를 살펴봤습니다. 토픽 모델링은 LDA(Latent Dirichlet Allocation)를 이용해 뉴스그룹 내 많은 문서들이 공통적으로 가지는 토픽을 추출해봤습니다. 텍스트 군집화는 K-평균 군집화 기법을 이용해 비슷한 문서들끼리 군집화했고 텍스트 유사도 측정에서는 코사인 유사도를 이용해 문서들끼리 얼마나 비슷한지를 측정해봤습니다. 파이썬의 대표적인 한글 형태소 분석기인 KoNLPy 패키지를 이용해 네이버에서 제공하는 한글로 된 영화 리뷰에 긍정/부정 이진 분류를 적용해봤고, 마지막으로 Mercari Price Challenge 실습에서는 정형 피처들과 텍스트와 같은 비정형 피처를 결합해 효과적으로 회귀 예측 모델을 구현했습니다.

 

텍스트 분석과 같은 비정형 데이터와 기존의 정형 데이터들의 분석이 결합돼 제공되는 결과는 기존 분석이 가져다 주지 못한 새로운 인사이트를 제공할 수 있게 됐습니다.

 

 


궁금한 점

  1. ngram_range는 (1, 2)라면, 단어를 1개 또는 2개로 묶어서 단어를 토큰화해서 피처로 추출합니다.
  2. CSR 사용시 희소 행렬 중간에 0이 없는 행이 존재하면 어떡하지? 
  3. pd.read_csv()의 header, sep, quoting 파라미터, header: 칼럼명으로 적용할 행, sep : 열 구분자, quoting: 인용구 설정 (3은 인용구의 따옴표("인용구")를 없애준다. 0은 Minial, 1은 All, 2는 Nonumeric)
  4. re.sub("[^a-zA-Z]", " ", x)에서 x가 뭘 의미하는지 보기, x는 re.sub함수를 적용할 문자열이다.
  5. .startswith()함수가 뭔지 찾아보기, str.startswith(str or tuple)이고 True/False를 반환. tuple은 튜플값에 시작하는 단어가 1개라도 있으면 True 반환.
  6. swn_polarity(x) 함수 다시 보기
  7. pattern 패키지 - 감성사전으로 감성 분석 해보기 
    1. https://www.clips.uantwerpen.be/pattern
  • NLTK PennTreebank Tag이 뭐지?
    • POS Tag List in NLTK(The Penn Treebank POS tagset)는 JJ: 형용사 등의 키워드로 돼있다.
  • CountVector.get_feature_names()은 피처 명(단어)을 추출
  • ' '.join(feature_names[i])는 스페이스 단위로 피처 명(단어)을 합친다(concat). list원소 [0] + ' ' + [1] ... 이런 식인듯?
  • glob.glob()
    • glob 모듈의 glob 함수 정규조건식에 맞는 파일명을 list으로 반환한다.
  • DataFrame.to_string(): DataFrame의 표를 string 형태로 바꿔준다. 출력해보면 표 형태를 유지한다.
  • string.punctuation
    • punctuation은 문장부호(", ! ') 등인데, 이것들이 들어있는 함수이다.
  • string.translate()
    • string.replace와 비슷한데, dictionary나 mapping table(maketrans()로 만듦)을 입력 받는다. dictionary의 경우 key값을 value 값으로 바꿔준다. ord()는 아스키코드로 바꿔주는 함수이다.
    • dict('a', 'b') = {'a': 'b'}로 만들어진다.
  • CSR 형태로 된 행렬도 ML 알고리즘에 넣으면 알아서 잘 풀어지나?
  • ndarray.argsort()[:, ::-1], 배열 내에값이 큰 순으로 정렬된 위치 인덱스 값을 반환합니다.(큰 값으로 정렬한 값을 반환하는게 아니라 큰 값을 가진 배열 내 위치 인덱스 값을 반환하는 것입니다.)
  • Dict.items()
    • dict 객체의 key값의 순서대로 정렬하고, (key, value) 들의 리스트를 반환한다.
  • sns.barplot(x='similarity', y='filename', data=hotel_1_sim_df) 이렇게 barplot에 data=DataFrame으로 쓸 수도 있구나

 

 

출처: 파이썬 머신러닝 완벽 가이드(권철민)

사진 출처:

https://wikidocs.net/24603

https://medium.com/codex/document-indexing-using-tf-idf-189afd04a9fc

https://dschloe.github.io/python/nlp/ch02_bag_of_words/