본문 바로가기
아티피쎨 인텔리젼쓰/자연어 처리 (Natural Language Process)

[Python/NLP] 네이버 쇼핑 리뷰 감성분석

by Data Park 2023. 1. 6.

귀찮음 주의!!

# 한글폰트 설정

## colab 환경에서 한글 폰트 설정
!sudo apt-get install -y fonts-nanum
!sudo fc-cache -fv
!rm ~/.cache/matplotlib -rf
import matplotlib.pyplot as plt
plt.rc('font', family='NanumBarunGothic') 
plt.title('안녕')

참고 문서

딥러닝을 이용한 자연어 처리 e-book : https://wikidocs.net/94600
박성호의 심화기계학습 팀프젝 코랩 코드 : https://colab.research.google.com/drive/1951HcDnn8PfdC7ucUpFkz4BnmVLhD3ed?usp=sharing
핸즈온 머신러닝 2판 github : https://nbviewer.org/github/rickiepark/handson-ml2/tree/master/

 

Jupyter Notebook Viewer

nbviewer.org

import re
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import urllib.request
from collections import Counter
#from konlpy.tag import okt
from sklearn.model_selection import train_test_split
urllib.request.urlretrieve(
    "https://raw.githubusercontent.com/bab2min/corpus/master/sentiment/naver_shopping.txt",
     filename="ratings_total.txt")

total_data = pd.read_table('ratings_total.txt', names=['ratings', 'reviews'])
total_data[:5]

네이버 영화리뷰와 다른점은 75:25비율로 학습 검증 셋이 나뉘어져있었는데
네이버 쇼핑리뷰 데이터는 초기에 urllib으로 가져왔을때의 원본 데이터가 total data인 것이다.

# 전처리 1
## 레이블 설정

# 평점 1,2,4,5 의 비율(value_counts)로 Bar Graph
total_data['ratings'].value_counts().plot(kind = 'bar')

# ratings 4, 5는 긍정(1), 1, 2는 부정(0)
total_data['label'] = np.select([total_data.ratings > 3], [1], default=0) 
total_data[:5]

## 중복 확인

# unique한 값이 얼마나 있는지 출력
total_data['ratings'].nunique(), total_data['reviews'].nunique(), total_data['label'].nunique()

ratings열의 경우 1, 2, 4, 5라는 네 가지 값을 가지고 있습니다. reviews열에서 중복을 제외한 경우 199,908개입니다. 현재 20만개의 리뷰가 존재하므로 이는 현재 갖고 있는 데이터에 중복인 샘플들이 있다는 의미입니다. 중복인 샘플들을 제거해줍니다.

total_data.drop_duplicates(subset=['reviews'], inplace=True) 
# reviews 열에서 중복인 내용이 있다면 중복 제거
print('총 샘플의 수 :',len(total_data)) 
# total_data의 row 수(length) 출력

total_data['label'].value_counts()

# 한글과 공백을 제외하고 모두 제거
total_data['reviews'] = total_data['reviews'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","")
total_data['reviews'].replace('', np.nan, inplace=True)
print(total_data.isnull().sum())

# EDA
## 워드 클라우드

from wordcloud import WordCloud, STOPWORDS

stopwords = ['다','아' ,'그냥','진짜','너무','의',
             '정말', '가','이','은','들','는','좀','잘','걍','과','도','를',
             '으로','자','에','와','한','하다']
%matplotlib inline

wordcloud1 = WordCloud(font_path='NanumBarunGothic.ttf',
                       stopwords = stopwords, 
                       colormap='Set3', 
                       background_color = 'black', 
                       width = 1200, height = 600).generate(' '.join(total_data['reviews']))

plt.figure(figsize = (15, 20))
plt.imshow(wordcloud1)
plt.axis("on")
plt.show()

wordcloud1.to_image()

train_data, test_data = train_test_split(total_data,
                                         test_size = 0.1, 
                                         random_state = 42)

print('훈련용 리뷰의 개수 :', len(train_data))
print('테스트용 리뷰의 개수 :', len(test_data))

train_data['label'].value_counts().plot(kind = 'bar')

test_data['label'].value_counts().plot(kind = 'bar')

# 전처리 2
## konlpy install

# Konlpy 설치
!pip install konlpy

## tokenizer 정의

from konlpy.tag import Okt
okt = Okt()
def okt_tokenizer(text):
    tokens = okt.morphs(text)
    return tokens

## TF-IDF
시도 1 : 이번 프로젝트에서는 train_test를 하기전에 tf-idf를 먼저한다.
그 이유는 train_test의 분할 비율이 어떠할때 정확도가 최대가 되는 것인지를
보고싶은데 train_test를 먼저하고 tf-idf를 하면 비율이 정해져 있을때
tf-idf를 하기때문에 시간이 굉장히 오래 걸린다.
시간도 비용이기 때문에 비용의 최소화를 위해서는 tf-idf를 하고 train_test_split을 한다.
=> 이렇게 하면 dimension 오류로 실행안됨

# 토크나이저, TFIDF
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(tokenizer=okt_tokenizer, 
                        stop_words=stopwords,
                        ngram_range=(1,3), min_df=3 , max_df=0.90)

tfidf.fit(total_data['reviews'])
train_tfidf = tfidf.transform(train_data['reviews'])
test_tfidf = tfidf.transform(test_data['reviews'])
#----------------9:1로 나눴을때 18분 걸림-------
end = time.time() 
print('토큰화 수행 시간 :') 
print(end - start)

tfidf_vocab = pd.DataFrame(sorted(tfidf.vocabulary_.items()),
                           columns=['vocab','counts'])
tfidf_vocab.to_csv('tfidf_vocabulary_items.csv')
tfidf_vocab

이런 쓸따리 없을 것 같은 vocab들은 제거하면 성능이 좋아질 것 같은데 언제 다 삭제할까,, 최대 난제다

# 모델링 : 정확도 91.1%
## 로지스틱 회귀

### 로지스틱 회귀 ###
import time
start = time.time() 
#----------------------------------------------
lr = LogisticRegression(C=3, max_iter=50, random_state=0) # 모델 생성

lr.fit( train_tfidf, y_train ) # 모델 -> train data 학습
lr_pred = lr.predict( test_tfidf ) # 모델 -> test data 예측
lr_acc = lr.score( test_tfidf, y_test ) # 정확도 반환
#----------------------------------------------
print("로지스틱 회귀 training accuracy :", lr.score( train_tfidf , y_train )*100, "%")
print("로지스틱 회귀 testing accuracy :", lr_acc * 100, "%")
print("--------------------------------------------------------------------------")
print(classification_report( y_test, lr_pred )) # 모델 보고서 출력
print("--------------------------------------------------------------------------")
print(confusion_matrix( y_test, lr_pred )) # 오차행렬 출력
print("--------------------------------------------------------------------------")
#----------------------------------------------
end = time.time() 
print('로지스틱 회귀 수행시간 :') 
print(end - start) 

plt.figure(figsize=(20, 20))
plot_confusion_matrix(lr , test_tfidf , y_test , cmap='Blues') # 오차행렬 시각화
plt.title("<< Logistic Regression >>")

from sklearn.model_selection import GridSearchCV   
#효과적인 하이퍼 파라미터 세팅을 찾아줌
import numpy as np

lr = LogisticRegression( random_state = 0 )

params = {'C' : [3, 3.5, 4, 4.5, 5, 6],
          'max_iter' : [10, 50, 100, 1000],
          "penalty":["l1","l2"]
          }
lr_grid_cv = GridSearchCV(lr, 
                          param_grid=params, 
                          cv=3, 
                          scoring='accuracy', 
                          verbose=1)
lr_grid_cv

start = time.time()
#----------------------------------------------
lr_grid_cv.fit(train_tfidf, y_train)
print("로지스틱 최적 점수 : {}".format(lr_grid_cv.best_score_))
print("로지스틱 최적 파라미터 : {}".format(lr_grid_cv.best_params_))
print(lr_grid_cv.best_estimator_)
#----------------------------------------------
end = time.time() 
print("--------------------------------------------------------------------")
print('Execution time is:') 
print(end - start)

이후 나이브 베이즈, 디시젼트리, 랜덤포레스트, SVM, KNN 등 해보았지만 로지스틱 회귀가 가장 성능이 좋았음


# 모델 평가

import pandas as pd
models_acc = {'로지스틱 회귀': lr_acc*100,  
              '나이브 베이즈':	nb_acc*100  ,
              '의사결정나무' : dt_clf_acc*100,
              'K-최근접 이웃': knn_acc*100 ,
              '랜덤포레스트': rnd_clf_acc*100,
              '서포트 벡터 머신' : svm_acc*100

              }
models_acc_df = pd.DataFrame(pd.Series(models_acc))
models_acc_df.columns = ['정확도']
models_acc_df['모델'] = ['로지스틱 회귀',
                       '나이브 베이즈',
                       '의사결정나무',
                       'K-최근접 이웃',
                       '랜덤포레스트',
                       '서포트 벡터 머신'
                       ]
models_acc_df.set_index(pd.Index([1, 2, 3, 4 , 5, 6]))

import plotly.express as px
fig = px.bar(models_acc_df, 
             x='정확도', 
             y='모델' ,
             color='모델', 
             range_x=(50, 100), 
             template="plotly_white", 
             text_auto='.4s',
             title="<< 최종 모델평가 >>" )

fig.update_traces(textfont_size=15, textangle=0, textposition="inside")
fig.update_yaxes(categoryorder="total ascending")
fig.update_layout(height = 600, width = 1200, hovermode = 'closest')

# 새로운 텍스트를 직접 입력해 감성 예측 수행해봅시다!
st = input("감성을 분석할 문장을 입력하세요: ")

st = re.compile(r'[ㄱ-ㅣ가-힣]+').findall(st)
print(st)

st = [" ".join(st)]
print(st)

# 입력 텍스트의 벡터화
st_tfidf = tfidf.transform(st)
st_tfidf

# 감성 분석 모델에 적용하여 예측
st_predict = lr.predict(st_tfidf)
st_predict

>>> array([0])
# 예측값 출력
if(st_predict ==0):
    print(st, "->> 부정 감성")
else:
    print(st, "->> 긍정 감성")
['배송이 왜이렇게 느린거야'] ->> 부정 감성
import os
import sys
import urllib.request
import datetime
import time
import json

# 각자가 발급받은 Naver API 정보를 입력합니다- https://developers.naver.com/apps/#/myapps/0Oa0z62LyTJsVFqAPBZW/overview
client_id = ''
client_secret = ''

#[CODE 1]
def getRequestUrl(url):    
    req = urllib.request.Request(url)
    req.add_header("X-Naver-Client-Id", client_id)
    req.add_header("X-Naver-Client-Secret", client_secret)
    
    try: 
        response = urllib.request.urlopen(req)
        if response.getcode() == 200:
            print ("[%s] Url Request Success" % datetime.datetime.now())
            return response.read().decode('utf-8')
    except Exception as e:
        print(e)
        print("[%s] Error for URL : %s" % (datetime.datetime.now(), url))
        return None

#[CODE 2]
def getNaverSearch(node, srcText, start, display):    
    base = "https://openapi.naver.com/v1/search"
    node = "/%s.json" % node
    parameters = "?query=%s&start=%s&display=%s" % (urllib.parse.quote(srcText), start, display)
    
    url = base + node + parameters    
    responseDecode = getRequestUrl(url)   #[CODE 1]
    
    if (responseDecode == None):
        return None
    else:
        return json.loads(responseDecode)

#[CODE 3]
def getPostData(post, jsonResult, cnt):    
    title = post['title']
    description = post['description']
    org_link = post['originallink']
    link = post['link']
    
    pDate = datetime.datetime.strptime(post['pubDate'],  '%a, %d %b %Y %H:%M:%S +0900')
    pDate = pDate.strftime('%Y-%m-%d %H:%M:%S')
    
    jsonResult.append({'cnt':cnt, 'title':title, 'description': description, 
'org_link':org_link,   'link': org_link,   'pDate':pDate})
    return    

#[CODE 0]
def main():
    node = 'news'   # 크롤링 할 대상
    srcText = input('검색어를 입력하세요: ')
    cnt = 0
    jsonResult = []

    jsonResponse = getNaverSearch(node, srcText, 1, 100)  #[CODE 2]
    total = jsonResponse['total']
 
    while ((jsonResponse != None) and (jsonResponse['display'] != 0)):         
        for post in jsonResponse['items']:
            cnt += 1
            getPostData(post, jsonResult, cnt)  #[CODE 3]       
        
        start = jsonResponse['start'] + jsonResponse['display']
        jsonResponse = getNaverSearch(node, srcText, start, 100)  #[CODE 2]
       
    print('전체 검색 : %d 건' %total)
    
    with open('%s_naver_%s.json' % (srcText, node), 'w', encoding='utf8') as outfile:
        jsonFile = json.dumps(jsonResult,  indent=4, sort_keys=True,  ensure_ascii=False)
                        
        outfile.write(jsonFile)
        
    print("가져온 데이터 : %d 건" %(cnt))
    print ('%s_naver_%s.json SAVED' % (srcText, node))
    
if __name__ == '__main__':
    main()

우린 '겨울 and 쇼핑'으로 네이버 뉴스를 크롤링하여 다시 예측해보았다

import json
file_name = '겨울_쇼핑_naver_news.json'

with open(file_name, encoding='utf8') as j_f:
    data = json.load(j_f)
import pandas as pd
data = pd.read_json('겨울_쇼핑_naver_news.json', orient='records')
data.to_csv('겨울_쇼핑_naver_news.csv')
data = pd.read_csv('겨울_쇼핑_naver_news.csv')
data

# title 부분에 대한 데이터의 피처 벡터화를 통한 감성 분석
data_title_tfidf = tfidf.transform(data['title'])
data_title_predict = lr.predict(data_title_tfidf)
data['title_label'] = data_title_predict
data

# description 부분에 대한 데이터의 피처 벡터화를 통한 감성 분석
data_description_tfidf = tfidf.transform(data['description'])
data_description_predict = lr.predict(data_description_tfidf)
data['description_label'] = data_description_predict
data

import pandas as pd
columns_name = ['title','title_label','description','description_label']
NEG_data_df = pd.DataFrame(columns=columns_name)
POS_data_df = pd.DataFrame(columns=columns_name)

for i, data in data.iterrows(): 
    title = data['title'] 
    description = data['description'] 
    t_label = data['title_label'] 
    d_label = data['description_label'] 
    
    if d_label == 0: # 부정 감성 샘플만 추출
        NEG_data_df = NEG_data_df.append(pd.DataFrame([[title, t_label, description, d_label]],columns=columns_name),
                                         ignore_index=True)
    else : # 긍정 감성 샘플만 추출
        POS_data_df = POS_data_df.append(pd.DataFrame([[title, t_label, description, d_label]],columns=columns_name),
                                         ignore_index=True)
     
# 파일에 저장.
NEG_data_df.to_csv('predict_news_NES.csv', encoding='utf-8') 
POS_data_df.to_csv('predict_news_POS.csv', encoding='utf-8')
len(NEG_data_df), len(POS_data_df)

부정 기사가 545건, 긍정 기사가 455건

POS_description = POS_data_df['description']

POS_description_noun_tk = []

for d in POS_description:
    POS_description_noun_tk.append(okt.nouns(d)) #형태소가 명사인 것만 추출

print(POS_description_noun_tk)

POS_description_noun_join = []

for d in POS_description_noun_tk:
    d2 = [w for w in d if len(w) > 1] #길이가 1인 토큰 제외
    POS_description_noun_join.append(" ".join(d2)) # 토큰을 연결(join)하여 리스트 구성

POS_tfidf = TfidfVectorizer(tokenizer = okt_tokenizer, min_df=2 )
POS_dtm = POS_tfidf.fit_transform(POS_description_noun_join)

POS_vocab = dict() 

for idx, word in enumerate(POS_tfidf.get_feature_names()):
    POS_vocab[word] = POS_dtm.getcol(idx).sum()
    
POS_words = sorted(POS_vocab.items(), key=lambda x: x[1], reverse=True)

POS_words  #긍정 단어들 갯

긍정기사에 있는 단어들중에 가장 많이 출몰한 단어들이다

import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings(action='ignore') 

max = 15  #바 차트에 나타낼 단어의 수 
# 긍정 뉴스 단어 시각화
plt.bar(range(max), [i[1] for i in POS_words[:max]], color="blue")
plt.title("긍정 뉴스의 단어 상위 %d 개" %max, fontsize=15)
plt.xlabel("Word", fontsize=12)
plt.ylabel("Sum of TF-IDF", fontsize=12)
plt.xticks(range(max), [i[0] for i in POS_words[:max]], rotation=70)

plt.show()

같은 방식으로 부정 단어들도 시각화

# 부정 뉴스 단어 시각화 
max = 15
plt.bar(range(max), [i[1] for i in NEG_words[:max]], color="red")
plt.title("부정 뉴스의 단어 상위 %d개" %max, fontsize=25)
plt.xlabel("단어", fontsize=12)
plt.ylabel("TF-IDF의 합", fontsize=12)
plt.xticks(range(max), [i[0] for i in NEG_words[:max]], rotation=70, fontsize=25)

plt.show()