아이티윌_데이터 분석 55기/강의내용_딥러닝

#5 5일차_감성 분석 (나이브베이즈, RNN)

ecosso 2026. 7. 29. 16:16

01 쇼핑몰 후기 감성 분석

 

02 참고 : 최신 java 설치

 

03 쇼핑몰 후기 감성 분석 - 모델링(나이브베이즈)

 

04 쇼핑몰 후기 감성 분석 - 모델링(RNN)


01 쇼핑몰 후기 감성 분석

# 1. 데이터 로딩
import pandas as pd
df = pd.read_table('shopping_ratings_total.txt', names = ['ratings', 'reviews'])

df.head()
df.shape          # (200000, 2)

# 2. target 변수 변환(평점 -> 이진데이터)
import numpy as np
np.unique(df['ratings'])                                        # [1, 2, 4, 5]

df['ratings'] = np.where(df['ratings'] <= 2, 0, 1)              # 평점 1,2 -> 0(부정) 
                                                                                      # 평점 4,5 -> 1(긍정) 
df['ratings'].value_counts()


# 3. 샘플링(실제 분석과정에서는 생략)
# 20만건 학습 불가 -> 1만건만 랜덤 샘플링해서 진행
df = df.sample(n=10000, random_state=0).reset_index(drop=True)  # 랜덤 샘플링 -> index 초기화 

# 4. 데이터 분리(train/test) 
from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y = train_test_split(df['reviews'], df['ratings'], random_state=0, stratify=df['ratings'])

# 5. 전처리(정제 -> 형태소 분석)
# pip install konlpy
import konlpy.tag
Okt = konlpy.tag.Okt()                         # RuntimeError: Java version too old. Java 9 or later is required
                                               # cmd에서 java -version
# sample 리뷰로 형태소 분석
sample = train_x.values[4]

# 1) 정제 : 불필요한 글자 제거 -> 영어, 숫자, 특수기호 등
from pandas import Series
Series(sample).replace('[^가-힣]','',regex=True)

Series(sample).replace('[^가-힣]','',regex=True)
Out[21]: 
0    살키로남자아이구매했어요타이트하게잘맞아요무엇보다소재가너무좋네요세탁후에도보풀이안나고신축...
dtype: object

 

# 내 컴퓨터에서 오류가 계속 발생해서 아래 코드로 진행하였음
import konlpy.tag

Okt = konlpy.tag.Okt()

sample_clean = re.sub(r'[^가-힣\s]', '', sample)
sample_token = Okt.morphs(sample_clean)

print(sample_token)

import konlpy.tag

Okt = konlpy.tag.Okt()

sample_clean = re.sub(r'[^가-힣\s]', '', sample)
sample_token = Okt.morphs(sample_clean)

print(sample_token)
['살', '키로', '남자', '아이', '구매', '했어요', '타이', '트', '하', '게', '잘', '맞아요', '무엇', '보다', '소재', '가', '너무', '좋네요', '세탁', '후', '에도', '보풀', '이', '안나', '고', '신축', '성도', '좋습니다', '기모', '라', '축구', '할', '때', '땀', '흘리며', '열심히', '뛰며', '공차', '네', '요']

 

# 3) 어간추출
# 먹었습니다 => 먹(어간) + 었 + 습니다
stem_result = Okt.morphs(sample_clean, stem=True) # 토큰화 + 어간추출 동시 진행

Okt.morphs(sample_clean, stem=True)
Out[48]: 
['살',
 '키로',
 '남자',
 '아이',
 '구매',
 '하다',
 '타이',
 '트',
 '하',
 '게',
 '자다',
 '맞다',
 '무엇',
 '보다',
 '소재',
 '가',
 '너무',
 '좋다',
 '세탁',
 '후',
 '에도',
 '보풀',
 '이',
 '안나',
 '고',
 '신축',
 '성도',
 '좋다',
 '기모',
 '라',
 '축구',
 '하다',
 '때',
 '땀',
 '흘리다',
 '열심히',
 '뛰다',
 '공차',
 '네',
 '요']

 

# 4) 품사태깅 : 품사(동사/명사/형용사 등) 확인 과정 => 조사를 제거(불용어 제거)
# 불용어를 태그한 뒤 삭제하기 위해서 진행하는 과정이다.
# (명사 : Noun, 조사 : Josa, 형용사 : Adjective, 부사 : Adverb 등)
pos_result = Okt.pos(sample_clean)

Okt.pos(sample_clean)
Out[50]: 
[('살', 'Noun'),
 ('키로', 'Suffix'),
 ('남자', 'Noun'),
 ('아이', 'Noun'),
 ('구매', 'Noun'),
 ('했어요', 'Verb'),
 ('타이', 'Noun'),
 ('트', 'Noun'),
 ('하', 'Suffix'),
 ('게', 'Josa'),
 ('잘', 'Verb'),
 ('맞아요', 'Verb'),
 ('무엇', 'Noun'),
 ('보다', 'Josa'),
 ('소재', 'Noun'),
 ('가', 'Josa'),
 ('너무', 'Adverb'),
 ('좋네요', 'Adjective'),
 ('세탁', 'Noun'),
 ('후', 'Noun'),
 ('에도', 'Josa'),
 ('보풀', 'Noun'),
 ('이', 'Josa'),
 ('안나', 'Noun'),
 ('고', 'Josa'),
 ('신축', 'Noun'),
 ('성도', 'Noun'),
 ('좋습니다', 'Adjective'),
 ('기모', 'Noun'),
 ('라', 'Josa'),
 ('축구', 'Noun'),
 ('할', 'Verb'),
 ('때', 'Noun'),
 ('땀', 'Noun'),
 ('흘리며', 'Verb'),
 ('열심히', 'Adverb'),
 ('뛰며', 'Verb'),
 ('공차', 'Noun'),
 ('네', 'Suffix'),
 ('요', 'Josa')]

 

# 조사 사전을 만들기 위한 작업

[ word for word, pos in pos_result if pos == 'Josa' ]

[ word for word, pos in pos_result if pos == 'Josa' ]
Out[55]: ['게', '보다', '가', '에도', '이', '고', '라', '요']

 

# *** 전체 리뷰(train)에 대한 형태소 분석
Josa = []             # 각 문장마다의 조사를 누적
stops = []            # 각 문장마다의 불용어 누적
train_token = []      # 각 문장마다의 토큰화(어간추출 적용)된 단어 누적

for vtext in train_x.values :
    vtext = Series(vtext).replace('[^가-힣 ]', '', regex=True)[0]           # 정제작업(한글과 공백이 아닌 모든 글자 제거)
    
    # 조사 수집(각 문장마다 조사를 수집)
    pos_result = Okt.pos(vtext)                                             # 품사태깅
    Josa = Josa + [ word for word, pos in pos_result if pos == 'Josa']      # 조사사전 생성
    
    # 토큰화 + 어간추출
    vtext = Okt.morphs(vtext, stem=True) 
    
    # 불용어 사전 : 조사 + 한글자 글자 + 빈도수 1인 단어
    stops = stops + [word for word in vtext if len(word) == 1]              # 토큰화 된 단어 중 길이가 1인 단어 고르기
    
    # 토큰화 된 단어 저장(각 문장마다 수집 필요)
    train_token.append(vtext)
    

# 결과 확인
stops
Josa
train_token


# 4. 불용어 제거
# 1) 불용어 사전 생성 : 조사 + 한글자 단어 + 빈도수 1개인 단어
import numpy as np
all_train_words = [w for token in train_token for w in token ]                         # 모든 문장의 토큰화 결과 합치기
freq1_words = [word for word in all_train_words if all_train_words.count(word) == 1]   # 모든 문장에서 빈도수가 1인 단어만 골라내기

stops = np.unique(Josa + stops + freq1_words)                                          # 불용어 사전 생성
len(stops)

len(stops)
Out[71]: 15

 

# 2) 각 토큰화 된 결과에서 불용어 제거(train/test 각각 제거)
from numpy import nan as NA

def token_clean_func(text, stops) :
    result = [] 
    for vtext in text.values :
        vtext = Okt.morphs(vtext, stem=True) 
        vtext = Series(vtext).replace('[^가-힣 ]', '', regex=True)    
        vtext_rm = [word for word in vtext if word not in stops]                  # 불용어 제거
        vtext_rm = Series(vtext_rm).replace('',NA).dropna().to_list()             # 빈문자열 제거
        result.append(vtext_rm)
    return result
        
train_token = token_clean_func(train_x, stops)
test_token = token_clean_func(test_x, stops)


02 참고 : 최신 java 설치

# 1) 프로그램 다운로드 / 설치
https://www.oracle.com/java/technologies/downloads/#jdk26-windows

# 2) 시스템 PATH 등록
# 검색창 > 시스템 환경 변수 > PATH 등록

# 3) 파이썬 PATH 등록
import os
os.environ['JAVA_HOME'] = r'C:\Program Files\Java\jdk-26.0.2'
print(os.environ.get('JAVA_HOME'))


03 쇼핑몰 후기 감성 분석 - 모델링(나이브베이즈)

# 나이브베이즈 모델
- 단어 기반으로 사후 확률을 계산
- 각 문장에서 각 단어가 서로 독립적이라고 가정 (실제로는 가정 성립 자체가 어려움) => 문맥 파악 어려움
- TF-IDF 변환으로 문장을 벡터화 -> 각 문장을 단어 빈도수 기반으로 숫자로 변경

** TF-IDF 변환
1) TF(Term Frequency) 변환
    - 단어의 빈도수에 가중치를 두고 각 단어가 얼마나 중요한지를 수치로 나타내는 작업

2) IDF(Inverse Document Frequency) 변환
    - 단어가 문서 전체에서 얼마나 희귀한가를 나타내는 작업
    
# 1. 데이터 로딩

# 2. 전처리
# 1) 정제
# 2) 토큰화 + 어간추출
# 3) 불용어 제거

len(train_token)
len(test_token)

len(train_token)
Out[75]: 7500

len(test_token)
Out[76]: 2500

 

# 3. TF-IDF 변환
# 1) 토큰화 -> 문장
' '.join(train_token[0])

train_join = [' '.join(token_list) for token_list in train_token]
test_join = [' '.join(token_list) for token_list in test_token]

# 2) TF-IDF 변환(불용어 제거 후 문장으로 변환하여 입력)
from sklearn.feature_extraction.text import TfidfVectorizer
m_tfidf = TfidfVectorizer()
train_x_tfidf = m_tfidf.fit_transform(train_join)
test_x_tfidf  = m_tfidf.transform(test_join)

# 4. 모델링 / 평가
import sklearn.naive_bayes
from sklearn.naive_bayes import MultinomialNB, CategoricalNB, BernoulliNB

# - MultinomialNB : 카운트 데이터에 적합
# - BernoulliNB : 이진 데이터 분류 적합
# - CategoricalNB : 다중 분류 적합

model = MultinomialNB()
model.fit(train_x_tfidf, train_y)

# 평가
model.score(train_x_tfidf, train_y)        # 0.8952
model.score(test_x_tfidf, test_y)          # 0.8588

model.predict_proba(test_x_tfidf[0])

# 5. 리뷰 테스트
test = '배송도 빠르고 실물이 너무 예뻐요 대만족입니다'
def f_review(text, model) :
    vtext = Series(text).replace('[^가-힣]','',regex=True)[0]
    vtest = Okt.morphs(vtext, stem=True)
    vtext = [word for word in vtext if word not in stops]
    vtext = ' '.join(vtext)
    test_tfidf = m_tfidf.transform([vtext])
    proba = model.predict_proba(test_tfidf)[0]
    print(f'긍정 확률 : {proba[1]}')
    
    
f_review('배송도 빠르고 실물이 너무 예뻐요 대만족입니다', model)
f_review('............')
f_review('')

import pandas as pd
from konlpy.tag import Okt

okt = Okt()

test = '배송도 빠르고 실물이 너무 예뻐요 대만족입니다'

def f_review(text, model):
    # 한글만 남기기
    vtext = pd.Series([text]).replace(
        r'[^가-힣\s]',
        '',
        regex=True
    ).iloc[0]

    # 형태소 분석
    tokens = okt.morphs(vtext, stem=True)

    # 불용어 제거
    tokens = [word for word in tokens if word not in stops]

    # TF-IDF 입력용 문자열로 변환
    clean_text = ' '.join(tokens)

    # 기존에 학습한 TF-IDF 객체로 변환
    test_tfidf = m_tfidf.transform([clean_text])

    # 긍정 확률 계산
    proba = model.predict_proba(test_tfidf)[0]

    print('전처리 결과:', clean_text)
    print(f'부정 확률: {proba[0]:.4f}')
    print(f'긍정 확률: {proba[1]:.4f}')

    return proba

f_review('배송도 빠르고 실물이 너무 예뻐요 대만족입니다', model)

f_review('배송도 빠르고 실물이 너무 예뻐요 대만족입니다', model)
전처리 결과: 배송 빠르다 실물 너무 예쁘다 대 만족 이다
부정 확률: 0.0884
긍정 확률: 0.9116
Out[128]: array([0.08844871, 0.91155129])

04 쇼핑몰 후기 감성 분석 - 모델링(RNN)

# 1. 데이터 로딩

# 2. 공통 전처리
# 1) 정제
# 2) 토큰화 + 어간추출
# 3) 불용어 제거
train_token
test_token

# 3. 벡터화
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

model_token = Tokenizer()
model_token.fit_on_texts(train_token)
model_token.word_index  # 정수 인코딩 결과 ('마사지':999, '게임':1000)

model_token.word_index  # 정수 인코딩 결과 ('마사지':999, '게임':1000)
Out[141]: 
{'하다': 1,
 '좋다': 2,
 '에': 3,
 '너무': 4,
 '배송': 5,
 '은': 6,
 '자다': 7,
 '있다': 8,
 '같다': 9,
 '안': 10,
 ...

 

# 정수인코딩 룰에 의한 텍스트 변환
train_num = model_token.texts_to_sequences(train_token)
test_num = model_token.texts_to_sequences(test_token)

 

# 4. 패딩
# - 서로 길이가 다른 문장을 동일 길이로 맞추는 작업
# - padding size 결정
wc = [len(text_list) for text_list in train_token]

# ** 히스토그램 시각화 (문장 길이 분포 확인 목적)
import matplotlib.pyplot as plt
plt.hist(wc, bins=30)

 

* 제로 패딩 : 짧은 문장을 긴 문장의 길이에 맞추는 과정에서 짧은 문장의 뒷쪽을 0으로 채워 길이를 맞추는 작업

 

# ** 상위 95%를 커버하는 길이로 설정
import numpy as np
maxlen = int(np.percentile(wc, 95))

maxlen
Out[153]: 33

 

# 2) 패딩 변환
train_pad = pad_sequences(train_num, maxlen = maxlen)
test_pad = pad_sequences(test_num, maxlen = maxlen)

# 5. 모델링
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, SimpleRNN, LSTM, Embedding
from tensorflow.keras.callbacks import EarlyStopping
import tensorflow as tf

# seed 고정
seed = 0
np.random.seed(seed)
tf.random.set_seed(seed)

# 임베딩 차원 (hyper parameter)
# 2, 4, 8, 16, 32, 64, 128 등 결정
embedding_dim = 64
hidden_units = 128

# target 변수 변환
train_y_dm = pd.get_dummies(train_y).astype(int).values
test_y_dm = pd.get_dummies(test_y).astype(int).values

# 모델 생성
model_rnn = Sequential()
model_rnn.add(Embedding(word_cnt, embedding_dim))
model_rnn.add(SimpleRNN(hidden_units, activation = 'tanh'))
model_rnn.add(Dense(2, activation = 'softmax'))

model_rnn.compile('adam', loss='binary_crossentropy', metrics=['accuracy'])
es = EarlyStopping(monitor='val_loss', patience = 10, restore_best_weights = True)
model_rnn.fit(train_pad, train_y_dm, epochs=100, validation_split=0.25, batch_size = 32, callbacks = [es] )

# 모델 평가
model_rnn.evaluate(train_pad, train_y_dm)[1] # train accuracy
model_rnn.evaluate(test_pad, test_y_dm)[1]   # test accuracy

model_rnn.evaluate(train_pad, train_y_dm)[1] # train accuracy
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.9336 - loss: 0.1916 
Out[166]: 0.9336000084877014

model_rnn.evaluate(test_pad, test_y_dm)[1]   # test accuracy
79/79 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - accuracy: 0.8596 - loss: 0.3522 
Out[167]: 0.8596000075340271

 

model_rnn.predict(train_pad[[0]])[0][1]

# 6. 리뷰 테스트
def f_review_rnn(text, model) :
    vtext = Series(text).replace('[^가-힣 ]', '', regex=True)[0]        # 정제
    vtext = Okt.morphs(vtext, stem=True)                                   # 토큰화 + 어간추출
    vtext = [word for word in vtext if word not in stops]               # 불용어 제거

    train_pad = ......
    proba = model.predict(train_pad)
    print(f'긍정 확률 : {proba[1]}')


f_review_rnn('배송도 빠르고 실물이 너무 예뻐요 대만족입니다', model)
f_review_rnn('초등학생들이나 좋아하겠네요', model)
f_review_rnn('강의가 깔끔하네요... 근데 시험은 떨어진것같아요ㅜㅜㅜㅜㅜ ', model)