import pandas as pd
# 영화 리뷰를 load한다. 사랑/장르라는 단어를 포함하고 있는 document를 load 한다.
reviews = pd.read_csv('./inputs/ratings_train.txt', delimiter='\t')
# 데이터 확인
reviews.head(10)
id | document | label | |
---|---|---|---|
0 | 9976970 | 아 더빙.. 진짜 짜증나네요 목소리 | 0 |
1 | 3819312 | 흠...포스터보고 초딩영화줄....오버연기조차 가볍지 않구나 | 1 |
2 | 10265843 | 너무재밓었다그래서보는것을추천한다 | 0 |
3 | 9045019 | 교도소 이야기구먼 ..솔직히 재미는 없다..평점 조정 | 0 |
4 | 6483659 | 사이몬페그의 익살스런 연기가 돋보였던 영화!스파이더맨에서 늙어보이기만 했던 커스틴 ... | 1 |
5 | 5403919 | 막 걸음마 뗀 3세부터 초등학교 1학년생인 8살용영화.ㅋㅋㅋ...별반개도 아까움. | 0 |
6 | 7797314 | 원작의 긴장감을 제대로 살려내지못했다. | 0 |
7 | 9443947 | 별 반개도 아깝다 욕나온다 이응경 길용우 연기생활이몇년인지..정말 발로해도 그것보단... | 0 |
8 | 7156791 | 액션이 없는데도 재미 있는 몇안되는 영화 | 1 |
9 | 5912145 | 왜케 평점이 낮은건데? 꽤 볼만한데.. 헐리우드식 화려함에만 너무 길들여져 있나? | 1 |
neg = reviews[(reviews.document.str.len() >= 30) & (reviews.label == 0)].sample(3000, random_state=43)
pos = reviews[(reviews.document.str.len() >= 30) & (reviews.label == 1)].sample(3000, random_state=43)
# 형태소 분석기
import re
import konlpy
from konlpy.tag import Twitter
okt = Twitter()
/anaconda3/lib/python3.6/site-packages/konlpy/tag/_okt.py:16: UserWarning: "Twitter" has changed to "Okt" since KoNLPy v0.4.5. warn('"Twitter" has changed to "Okt" since KoNLPy v0.4.5.')
def parse(s):
s = re.sub(r'[?$.!,-_\'\"(){}~]+', '', s)
try:
return okt.nouns(s)
except:
return []
neg['parsed_doc'] = neg.document.apply(parse)
pos['parsed_doc'] = pos.document.apply(parse)
neg.head()
id | document | label | parsed_doc | |
---|---|---|---|---|
149713 | 6674102 | 정말 댓글에 속아서 보게된 시간 아까운 영화. 전체적으로 코믹적인 요소를 많이 넣으... | 0 | [정말, 댓글, 속, 시간, 영화, 전체, 코믹, 요소, 정말, 웃기, 스토리] |
52541 | 8514375 | 점수를 줘야 하는거임? 30년후 구구절절 설명하며 끝나는모습 이라니... | 0 | [점수, 임, 년후, 절절, 설명, 모습] |
72078 | 1173600 | 주제는 좋으나 영화 자체는 주제를 포함하기엔 좀 조잡하다 | 0 | [주제, 영화, 자체, 주제, 포함, 좀] |
19403 | 8384839 | 이걸 영화라고 만들다니...말그대로 '돈만 떡으로 바르고 여운의 감동, 의미는 없는... | 0 | [걸, 영화, 그대로, 돈, 떡, 여운, 감동, 의미, 영화, 이건, 그냥, 영화,... |
106111 | 8997490 | 과장되고 어설픈 연기에 인류의 보편적 정서와는 거리가 먼 일본식 정서간의 괴리감, ... | 0 | [과장, 연기, 인류, 보편, 정서, 거리, 일본, 정서, 괴리감, 무엇, 공포영화... |
pos.head()
id | document | label | parsed_doc | |
---|---|---|---|---|
148005 | 8918849 | 마지막에 눈물이 흐를뻔... 너무 감동적이고 재미있는 영화 | 1 | [마지막, 눈물, 뻔, 감동, 영화] |
41010 | 9304354 | 오! 평점 높네요. 저도 진짜 좋아하는 영화에요. 소소한 일상의 반전. 아즈키판다 짱! | 1 | [오, 평점, 저, 진짜, 영화, 일상, 반전, 키, 판다, 짱] |
97924 | 9324301 | 전 아프리카 구호 활동중 에볼라에 걸려 눈을 감는 순간 누군가 틀어준 이 영화를 보... | 1 | [전, 아프리카, 구호, 활동, 에볼라, 눈, 순간, 누군가, 이, 영화, 보고, ... |
68271 | 10274621 | 전작을 본적이 없는데도 너무 재미있게 봤습니다. 크리스 에반스와 사무엘 L.잭슨의 ... | 1 | [전작, 본적, 크리스, 에반스, 사무엘, 잭슨, 카리스마, 그, 외, 스토리, 액... |
41106 | 9908746 | 신선하고 환경파괴에대해서 자연과 인간의 공존 전쟁 등등 많은 교훈을 주는 애니메이션... | 1 | [환경, 파괴, 대해, 자연, 인간, 공존, 전쟁, 등등, 교훈, 애니메이션, 원작... |
# 학습 데이터 : 5,800개 / 테스트 데이터 : 200개
neg_train = neg[:2900]
pos_train = pos[:2900]
neg_test = neg[2900:]
pos_test = pos[2900:]
neg_corpus = set(neg_train.parsed_doc.sum())
pos_corpus = set(pos_train.parsed_doc.sum())
corpus = list((neg_corpus).union(pos_corpus))
print('corpus 길이', len(corpus))
corpus 길이 9836
corpus[:10]
['끝', '쿵푸', '탈출', '조교', '슬', '김호진', '작문', '흡혈귀', '만족', '거슬러']
from collections import OrderedDict
neg_bow_vecs = []
for _, doc in neg.parsed_doc.items():
bow_vecs = OrderedDict()
for w in corpus:
if w in doc:
bow_vecs[w] = 1
else:
bow_vecs[w] = 0
neg_bow_vecs.append(bow_vecs)
# neg_bow_vecs[0]
pos_bow_vecs = []
for _, doc in pos.parsed_doc.items():
bow_vecs = OrderedDict()
for w in corpus:
if w in doc:
bow_vecs[w] = 1
else:
bow_vecs[w] = 0
pos_bow_vecs.append(bow_vecs)
# pos_bow_vecs[0]
$n$ : Document의 차원 즉 전체 corpus의 크기
$$p(pos|doc) = \frac{p(doc|pos) \times p(pos)}{p(doc)} = \frac{\Pi_{i=1}^{n}p(word_i|pos) \times p(pos)}{p(doc)}$$$$p(neg|doc) = \frac{p(doc|neg) \times p(neg)}{p(doc)} = \frac{\Pi_{i=1}^{n}p(word_i|neg) \times p(neg)}{p(neg)}$$$p(word_i|pos), p(word_i|neg), p(pos), p(neg)$를 구하는 것이 모델의 학습이다.
$p(word_i|pos)$
$p(word_i|neg)$
import numpy as np
neg_words_likelihood_cnts = {}
for w in corpus:
cnt = 0
for _, doc in neg_train.parsed_doc.items():
if w in doc:
cnt += 1
neg_words_likelihood_cnts[w] = cnt
pos_words_likelihood_cnts = {}
for w in corpus:
cnt = 0
for _, doc in pos_train.parsed_doc.items():
if w in doc:
cnt += 1
pos_words_likelihood_cnts[w] = cnt
import operator
sorted(neg_words_likelihood_cnts.items(), key=operator.itemgetter(1), reverse=True)[:10]
[('영화', 1052), ('진짜', 254), ('스토리', 228), ('이', 199), ('점', 189), ('평점', 186), ('감독', 186), ('정말', 185), ('것', 184), ('왜', 173)]
sorted(pos_words_likelihood_cnts.items(), key=operator.itemgetter(1), reverse=True)[:10]
[('영화', 1135), ('정말', 319), ('이', 249), ('최고', 221), ('것', 212), ('연기', 209), ('생각', 196), ('때', 188), ('진짜', 185), ('감동', 182)]
$p(pos)$
$p(neg)$
Document는 0, 1의 bag of word vector로 표현한다. Corpus의 크기가 $n$이면, 각 문장의 bag of word vector의 크기는 $n$이다.
cat, love, i, do, like, him, you
라고 하자. 문장이 i love you
이면, 이 문장의 bag of word vector는 (0, 1, 1, 0, 0, 0, 1)
이다.단어가 없는 경우는 Laplacian smoothing 을 사용한다.
test_data = pd.concat([neg_test, pos_test], axis=0)
def predict(doc):
pos_prior, neg_prior = 1/2, 1/2
# Posterior of pos
pos_prob = np.log(1)
for word in corpus:
if word in doc:
# 단어가 현재 문장에 존재하고, pos 문장에 나온적이 있는 경우
if word in pos_words_likelihood_cnts:
pos_prob += np.log((pos_words_likelihood_cnts[word] + 1) / (len(pos_train) + len(corpus)))
else:
# 단어가 현재 문장에 존재하고, pos 문장에서 한 번도 나온적이 없는 경우 : 라플라시안 스무딩
pos_prob += np.log(1 / (len(pos_train) + len(corpus)))
else:
# 단어가 현재 문장에 존재하지 않고, pos 문장에 나온적이 있는 경우 (pos에서 해당단어가 없는 확률을 구할 수 있다.)
if word in pos_words_likelihood_cnts:
pos_prob += \
np.log((len(pos_train) - pos_words_likelihood_cnts[word] + 1) / (len(pos_train) + len(corpus)))
else:
# 단어가 현재 문장에 존재하지 않고, pos 문장에서 단 한 번도 나온적이 없는 경우 : 라플라시안 스무딩
pos_prob += np.log((len(pos_train) + 1) / (len(pos_train) + len(corpus)))
pos_prob += np.log(pos_prior)
# Posterior of neg
neg_prob = 1
for word in corpus:
if word in doc:
# 단어가 현재 문장에 존재하고, neg 문장에 나온적이 있는 경우
if word in neg_words_likelihood_cnts:
neg_prob += np.log((neg_words_likelihood_cnts[word] + 1) / (len(neg_train) + len(corpus)))
else:
# 단어가 현재 문장에 존재하고, neg 문장에서 한 번도 나온적이 없는 경우 : 라플라시안 스무딩
neg_prob += np.log(1 / (len(neg_train) + len(corpus)))
else:
# 단어가 현재 문장에 존재하지 않고, neg 문장에 나온적이 있는 경우 (neg에서 해당단어가 없는 확률을 구할 수 있다.)
if word in neg_words_likelihood_cnts:
neg_prob += \
np.log((len(neg_train) - neg_words_likelihood_cnts[word] + 1) / (len(neg_train) + len(corpus)))
else:
# 단어가 현재 문장에 존재하지 않고, pos 문장에서 단 한 번도 나온적이 없는 경우 : 라플라시안 스무딩
neg_prob += np.log((len(neg_train) + 1) / (len(neg_train) + len(corpus)))
neg_prob += np.log(neg_prior)
if pos_prob >= neg_prob:
return 1
else:
return 0
test_data['pred'] = test_data.parsed_doc.apply(predict)
test_data.head()
id | document | label | parsed_doc | pred | |
---|---|---|---|---|---|
91417 | 5079332 | 쉴새없이 떠드는 폴도 싫지만...영화를 이렇게 만든 감독은 더 싫다. | 0 | [새, 폴, 영화, 감독, 더] | 0 |
122641 | 2776735 | 별표 반개도 아깝다..2시간 동안 본 짜증이 확밀려오네.. | 0 | [별표, 반개, 시간, 동안, 짜증, 확] | 0 |
72606 | 8477996 | 내용은 봐줄만해. 하지만 15금과 19금 사이에 서있다. 포스터는 호러 귀신나올것같... | 0 | [내용, 만해, 금, 금, 사이, 포스터, 호러, 귀신, 막상, 차마, 두, 최악,... | 0 |
138243 | 7923759 | 진정한 수면제...졸다가 비명지르는 소리에 깼다가 다시 자다 | 0 | [수면제, 비명, 소리, 다시] | 0 |
118247 | 8142893 | "진짜 슈스케 버스커 버스커, 허각, 울랄라세션 나올땐 재밌는데 갈수록 재미가 없어... | 0 | [진짜, 슈스케, 버스커, 버스커, 허각, 울랄라세션, 땐, 갈수록, 재미, 제, ... | 1 |
sum(test_data.label ^ test_data.pred)
46
1 - sum(test_data.label ^ test_data.pred)/len(test_data)
0.77