Warning) 아래 코드들은 Google Colab 환경에서 작성된 것이기에 거의 그럴 일은 없지만 다른 환경에서는 정상적으로 작동되지 않을 수도 있음을 알려드립니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 


STEP 3. 로튼 토마토 사이트에 있는 Critic리뷰 감성 분석

STEP 2에서 영화 '탑건'의 리뷰 데이터를 스크래핑하는 작업을 진행하였습니다. 지금부터는 리뷰 데이터를 토대로 감성 분석을 진행하여 영화 '탑건'이 Critic(비평가)들로부터 전반적으로 어떤 평가를 받고 있는지 살펴보도록 하겠습니다. 이번 리뷰 분석에서는 알고리즘 모델에 학습시킬 output변수가 따로 없기 때문에 비지도학습 기반의 감성 분석(Lexicon based approach)을 사용하도록 하겠습니다. 

앞으로 진행될 과정에 대해 간략히 설명 드리면, 2가지 감성 사전(SentiWordNet, VADER)을 이용해서 리뷰 분석을 진행하고 2가지 분석 결과를 적절히 혼합하여 최종적으로 리뷰의 평가를 GOOD/BAD로 분류하게 됩니다. SentiWordNet, VADER 이 2가지 사전을 이용하는 큰 이유는 없습니다. SentiWordNet이 VADER에 비해 예측 정확도가 현저히 떨어진다고 알고 있는데 단지 이를 한번 확인해보고자 해서 다음과 같이 진행하게 되는 것입니다.

STEP 3에서는 SentiWordNet, VADER 각각의 사전을 통해 리뷰를 분석할 수 있는 함수를 만드는 것으로 마무리될 것입니다. 함수를 이용한 분석은 STEP 4에서 진행하겠습니다. 지금부터 감성 분석에 사용된 방법들은 [파이썬 머신러닝 완벽 가이드, 권철민 저]를 기반으로 하고 있습니다. 


(1) SentiWordNet Lexicon을 이용한 리뷰 감성 분석 함수 생성

SentiWordNet을 이용하기 위해 우선 nltk의 모든 데이터와 패키지를 다운로드하겠습니다. 다 설치하려다 보니 시간이 조금 걸립니다.

import nltk 
nltk.download('all')

nltk 설치가 완료되었으면 필요한 모듈들을 import 하겠습니다. (1) 토큰화된 단어들의 원형을 가져오기 위해  nltk 패키지에 있는 WordNetLemmatizer를 import하고 (2) SentiWordNet기반의 Lexicon사전을 import하고 (3) 문장 토큰화, 단어 토큰화, 그리고 품사 태깅을 위해 각각 필요한 모듈을 가져오겠습니다. 

from nltk.stem import WordNetLemmatizer #(1)
from nltk.corpus import sentiwordnet as swn #(2)
from nltk import sent_tokenize, word_tokenize, pos_tag #(3)

본격적으로 SentiWordNet 기반의 분석 함수를 만들기 전에 penn_to_wn() 함수를 만들겠습니다. 이 함수를 만드는 이유는 WordNet에 사용할 수 있는 품사형태로 만들어주기 위해서입니다. 품사를 구분하는 방식은 언어나 학자마다 다른데 NLTK에서는 PennTreeBank형식의 태그를 가져오게 됩니다. 따라서, NLTK의 품사 태깅된 결과를 WordNet의 품사 태깅으로 바꾸어 주어 이후에 Lemmatize와 Synset 객체를 만들 수 있도록 해야 합니다.

[파이썬 머신러닝 완벽 가이드]의 저자는 형용사, 명사, 부사, 동사 이렇게 4가지 품사만 변환하도록 하였습니다. 이 과정에서 인칭 대명사, 한정사, 'to' 등 일부 단어들이 손실됩니다. 아마 형용사, 명사, 부사, 동사들이 감성에 많은 영향을 주는 단어이기 때문에 이 4가지 품사만을 한정하여 분석에 활용하고자 한 것으로 생각됩니다. 

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
    return

이제 대망의 swn_polarity() 함수를 살펴보겠습니다. 정말 복잡한 함수입니다. 해당 함수의 로직을 중간 중간 주석을 통해 설명하였습니다. 그 과정이 상당히 길고 복잡하기에 한 번에 이해가 안 될 수도 있습니다. 가장 중요한 부분이라고 할 수 있는 두 번째 for문에 대해 정리하면 다음과 같습니다. 주석 처리된 각 단계마다 어떤 결과가 도출되는지 예시를 통해 설명하도록 하겠습니다. 문장 토큰화가 완료된 상태라 가정하고 아래 한 문장을 가지고 각 단계의 출력을 보여드리겠습니다.

ex) She wishes he came to visit every week because he tells great stories and makes her favorite dinner.

(1) 단어 토큰화 후 (단어, NTLK 기반의 품사 태그)의 튜플 추출

[('She', 'PRP'), ('wishes', 'VBZ'), ('he', 'PRP'), ('came', 'VBD'), ('to', 'TO'), ('visit', 'VB'), ('every', 'DT'), ('week', 'NN'), ('because', 'IN'), ('he', 'PRP'), ('tells', 'VBZ'), ('great', 'JJ'), ('stories', 'NNS'), ('and', 'CC'), ('makes', 'VBZ'), ('her', 'PRP$'), ('favorite', 'JJ'), ('dinner', 'NN'), ('.', '.')]

(2) NTLK 품사 태그를 WordNet 품사 태그로 변환

[('She', None), ('wishes', 'v'), ('he', None), ('came', 'v'), ('to', None), ('visit', 'v'), ('every', None), ('week', 'n'), ('because', None), ('he', None), ('tells', 'v'), ('great', 'a'), ('stories', 'n'), ('and', None), ('makes', 'v'), ('her', None), ('favorite', 'a'), ('dinner', 'n'), ('.', None)]

(3) 명사, 형용사, 부사, 동사 아닌 것은 패스하고 lemmatizer로 원형 단어 추출

['wish', 'come', 'visit', 'week', 'tell', 'great', 'story', 'make', 'favorite', 'dinner']


(4) 추출된 원형 단어와 품사 태그를 입력하여 Synset 객체 생성 (output 일부 생략)

[[Synset('wish.v.01'), Synset('wish.v.02'), Synset('wish.v.03'), Synset('wish.v.04'), Synset('wish.v.05'), Synset('wish.v.06')],  [Synset('come.v.01'), Synset('arrive.v.01'), Synset('come.v.03'), Synset('come.v.04'), Synset('come.v.05'), Synset('come.v.06'), ....(중략)...[Synset('favorite.s.01'), Synset('favored.s.01')], [Synset('dinner.n.01'), Synset('dinner.n.02')]]


(5) sentiwordnet의 감성 분석으로 SentiSynset 추출, 이때 synset 리스 중 첫 번째 값을 사용

[SentiSynset('wish.v.01'), SentiSynset('come.v.01'), SentiSynset('visit.v.01'), SentiSynset('week.n.01'), SentiSynset('state.v.01'), SentiSynset('great.s.01'), SentiSynset('narrative.n.01'), SentiSynset('make.v.01'), SentiSynset('favorite.s.01'), SentiSynset('dinner.n.01')]


(6) 감성 분석 결과는 (긍정 - 부정)을 더해가며 계산

[wish.v.01]에 대한 pos점수: 0.125, neg점수: 0.375 , [come.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [visit.v.01]에 대한 pos점수: 0.0, neg점수: 0.0,[week.n.01]에 대한 pos점수: 0.0, neg점수: 0.0, [state.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [great.s.01]에 대한 pos점수: 0.0, neg점수: 0.0, [narrative.n.01]에 대한 pos점수: 0.0, neg점수: 0.0, [make.v.01]에 대한 pos점수: 0.0, neg점수: 0.0, [favorite.s.01]에 대한 pos점수: 0.125, neg점수: 0.0 , [dinner.n.01]에 대한 pos점수: 0.0, neg점수: 0.0

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

    lemmatizer = WordNetLemmatizer() # WordNetLemmatizer 객체 생성
    raw_sentences = sent_tokenize(text) # 문장 토큰화
    
    for raw_sentence in raw_sentences:
        # (1)단어 토큰화 후 (단어, NTLK 기반의 품사 태그) 추출 
        tagged_sentence = pos_tag(word_tokenize(raw_sentence))

        for word , tag in tagged_sentence:
            # (2)NTLK 품사 태그를 WordNet 품사 태그로 변환
            wn_tag = penn_to_wn(tag) 

            # (3)명사, 형용사, 부사, 동사 아닌 것은 패스하고 lemmatizer로 원형 단어 추출
            if wn_tag not in (wn.NOUN , wn.ADJ, wn.ADV, wn.VERB): 
                continue                  
            lemma = lemmatizer.lemmatize(word, pos=wn_tag)
            if not lemma:
                continue

            # (4)추출된 원형 단어와 품사 태그를 입력하여 Synset 객체 생성 
            synsets = wn.synsets(lemma , pos=wn_tag)
            if not synsets:
                continue

            # (5)sentiwordnet의 감성 분석으로 감성 synset 추출, 이때 synset 리스 중 첫번째 값을 사용
            synset = synsets[0]
            swn_synset = swn.senti_synset(synset.name())
            
            # (6)감성 분석 결과는 (긍정 - 부정)을 더해가며 계산
            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

(2) VADER Lexicon을 이용한 리뷰 감성 분석 함수 생성

vader_polarity() 함수는 간단합니다. SentimentIntensityAnalyzer() 객체만 생성해주면 polarity_scores() 메서드를 통해 알아서 감성 점수 결과값을 가져오게 됩니다. 쉽게 말하면 앞에서 SentiWordNet 기반의 복잡한 함수 과정을 한 번에 메서드 하나로 해결이 가능하다는 것입니다. 이상으로 STEP 3를 마치고 다음 STEP 4에서는 앞서 생성한 2개의 함수를 통해 본격적으로 감성분석에 들어가도록 하겠습니다. 

from nltk.sentiment.vader import SentimentIntensityAnalyzer

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

+ Recent posts