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 Mecab
from sklearn.model_selection import train_test_split
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
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'])
len(total_data)
총 200,000개 데이터
데이터의 구조
중복된 데이터 제거
total_data.drop_duplicates(subset=['reviews'], inplace=True)
len(total_data)
중복된 데이터 제거 후 데이터 개수 199,908개
훈련, 테스트 데이터 분리
train_data, test_data=train_test_split(total_data, test_size=0.25, random_state=42)
레이블 분포 확인
train_data['label'].value_counts().plot(kind='bar')
train_data.groupby('label').size().reset_index(name='count')
label count
0 0 74918
1 1 75013
정규표현식을 이용한 텍스트 전처리
train_data['reviews']=train_data['reviews'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣] ','', regex=True)
train_data['reviews'].replace('', np.nan, inplace=True)
테스트 데이터에도 앞 과정과 같은 방법 수행
test_data.drop_duplicates(subset = ['reviews'], inplace=True) # 중복 제거
test_data['reviews'] = test_data['reviews'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","", regex=True) # 정규 표현식 수행
test_data['reviews'].replace('', np.nan, inplace=True) # 공백은 Null 값으로 변경
test_data = test_data.dropna(how='any') # Null 값 제거
print('전처리 후 테스트용 샘플의 개수 :',len(test_data))
전처리 후 테스트용 샘플의 개수 : 49977
mecab=Mecab()
stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게']
# 훈련 데이터
train_data['tokenized']=train_data['reviews'].apply(mecab.morphs)
train_data['tokenized']=train_data['tokenized'].apply(lambda x:[item for item in x if item not in stopwords])
# 테스트 데이터
test_data['tokenized'] = test_data['reviews'].apply(mecab.morphs)
test_data['tokenized'] = test_data['tokenized'].apply(lambda x: [item for item in x if item not in stopwords])
부정리뷰에서 많이 쓰인 단어
negative_words=np.hstack(train_data[train_data.label==0]['tokenized'].values)
print(negative_word_count.most_common(20))
[('.', 54967), ('네요', 31738), ('는데', 20229), ('안', 19731), ('어요', 15095), ('있', 13201), ('너무', 12983), ('했', 11885), ('좋', 9801), ('배송', 9673), ('같', 9004), ('어', 8968), ('거', 8867), ('구매', 8867), ('없', 8674), ('아요', 8620), ('습니다', 8429), ('그냥', 8353), ('되', 8346), ('잘', 8020)]
긍정리뷰에서 많이 쓰인 단어
positive_words=np.hstack(train_data[train_data.label==1]['tokenized'].values)
positive_word_count = Counter(positive_words)
print(positive_word_count.most_common(20))
[('좋', 39438), ('아요', 21107), ('네요', 19783), ('.', 19403), ('어요', 19115), ('잘', 18598), ('구매', 16153), ('습니다', 13319), ('있', 12389), ('배송', 12258), ('는데', 11660), ('했', 10158), ('합니다', 9808), ('먹', 9642), ('재', 9258), ('너무', 8388), ('!', 8339), ('같', 7866), ('만족', 7246), ('~', 6940)]
긍정, 부정의 길이 분포 확인
fig,(ax1,ax2) = plt.subplots(1,2,figsize=(10,5))
text_len = train_data[train_data['label']==1]['tokenized'].map(lambda x: len(x))
ax1.hist(text_len, color='red')
ax1.set_title('Positive Reviews')
ax1.set_xlabel('length of samples')
ax1.set_ylabel('number of samples')
print('긍정 리뷰의 평균 길이 :', np.mean(text_len))
text_len = train_data[train_data['label']==0]['tokenized'].map(lambda x: len(x))
ax2.hist(text_len, color='blue')
ax2.set_title('Negative Reviews')
fig.suptitle('Words in texts')
ax2.set_xlabel('length of samples')
ax2.set_ylabel('number of samples')
print('부정 리뷰의 평균 길이 :', np.mean(text_len))
plt.show()
긍정 리뷰의 평균 길이 : 14.391118872728727
부정 리뷰의 평균 길이 : 18.350836915027095
토큰화된 데이터로 훈련, 테스트 데이터 분리
X_train = train_data['tokenized'].values
y_train = train_data['label'].values
X_test= test_data['tokenized'].values
y_test = test_data['label'].values
tokenizer=Tokenizer()
tokenizer.fit_on_texts(X_train)
희귀 단어 비율 확인
threshold=2
total_cnt=len(tokenizer.word_index)
rare_cnt=0
total_freq=0
rare_freq=0
for key, value in tokenizer.word_counts.items():
total_freq+=value
if value<threshold:
rare_cnt+=1
rare_freq+=value
print('단어 집합(vocabulary)의 크기 :',total_cnt)
print('등장 빈도가 %s번 이하인 희귀 단어의 수: %s'%(threshold - 1, rare_cnt))
print("단어 집합에서 희귀 단어의 비율:", (rare_cnt / total_cnt)*100)
print("전체 등장 빈도에서 희귀 단어 등장 빈도 비율:", (rare_freq / total_freq)*100)
단어 집합(vocabulary)의 크기 : 43031
등장 빈도가 1번 이하인 희귀 단어의 수: 20236
단어 집합에서 희귀 단어의 비율: 47.02656224582278
전체 등장 빈도에서 희귀 단어 등장 빈도 비율: 0.8245023385210378
vocab_size=total_cnt-rare_cnt+2
vocab_size
22797
22,797 보다 큰 숫자들은 OOV로 변환
tokenizer=Tokenizer(vocab_size, oov_token='OOV')
tokenizer.fit_on_texts(X_train)
X_train=tokenizer.texts_to_sequences(X_train)
X_test=tokenizer.texts_to_sequences(X_test)
print(X_train[:3])
[[71, 147, 2062, 315, 14826, 276, 77, 7, 251, 180, 146, 834, 3038, 658, 3, 83, 67, 218, 42, 1398, 167, 4, 7], [509, 2760, 2, 8838, 2734, 2, 2511, 352, 3039, 266, 2438, 39, 493, 3], [48, 25, 868, 102, 36, 2421, 172, 8, 11, 8366, 5, 1369, 31, 149, 336, 45, 63, 172, 149, 8, 1977, 3, 2, 2, 118, 174, 1437, 302, 2, 2, 128, 144, 2, 6476]]
데이터 길이 분포 확인
print(max((len(review) for review in X_train)))
print(sum(map(len, X_train))/len(X_train))
plt.hist([len(review) for review in X_train], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()
def below_threshold_len(max_len, nested_list):
count=0
for sentence in nested_list:
if len(sentence)<=max_len:
count+=1
print('전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s'%(max_len, (count / len(nested_list))*100))
max_len=80
below_threshold_len(max_len, X_train)
전체 샘플 중 길이가 80 이하인 샘플의 비율: 99.99866605305107
X_train=pad_sequences(X_train, maxlen=max_len)
X_test=pad_sequences(X_test, maxlen=max_len)
from tensorflow.keras.layers import Embedding, Dense, GRU
from tensorflow.keras.models import Sequential
from tensorflow.keras.models import load_model
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
embedding_dim=100
hidden_units=128
model=Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(GRU(hidden_units))
model.add(Dense(1, activation='sigmoid'))
es=EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc=ModelCheckpoint('best_model.keras', monitor='val_acc', mode='max', verbose=1, save_best_only=True)
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history=model.fit(X_train, y_train, batch_size=64, epochs=15, validation_split=0.2, callbacks=[es, mc])
loaded_model=load_model('best_model.keras')
print(model.evaluate(X_test, y_test))
0.9139603972434998
def sentiment_predict(new_sentence):
new_sentence = re.sub(r'[^ㄱ-ㅎㅏ-ㅣ가-힣 ]','', new_sentence)
new_sentence = mecab.morphs(new_sentence)
new_sentence = [word for word in new_sentence if not word in stopwords]
encoded = tokenizer.texts_to_sequences([new_sentence])
pad_new = pad_sequences(encoded, maxlen = max_len)
score = float(loaded_model.predict(pad_new))
if(score > 0.5):
print("{:.2f}% 확률로 긍정 리뷰입니다.".format(score * 100))
else:
print("{:.2f}% 확률로 부정 리뷰입니다.".format((1 - score) * 100))
sentiment_predict('이 상품 진짜 좋아요... 저는 강추합니다. 대박')
97.58% 확률로 긍정 리뷰입니다.
sentiment_predict('진짜 배송도 늦고 개짜증나네요. 뭐 이런 걸 상품이라고 만듬?')
99.70% 확률로 부정 리뷰입니다.
sentiment_predict('ㅁㄴㅇㄻㄴㅇㄻㄴㅇ리뷰쓰기도 귀찮아')
88.34% 확률로 부정 리뷰입니다.