Showing
1 changed file
with
192 additions
and
0 deletions
code/sentimentAnalysis.py
0 → 100644
1 | +import pandas as pd | ||
2 | +import numpy as np | ||
3 | +%matplotlib inline | ||
4 | +import matplotlib.pyplot as plt | ||
5 | +import re | ||
6 | +import urllib.request | ||
7 | +from konlpy.tag import Okt | ||
8 | +from tensorflow.keras.preprocessing.text import Tokenizer | ||
9 | +from tensorflow.keras.preprocessing.sequence import pad_sequences | ||
10 | +from tensorflow.keras.layers import Embedding, Dense, LSTM | ||
11 | +from tensorflow.keras.models import Sequential | ||
12 | +from tensorflow.keras.models import load_model | ||
13 | +from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint | ||
14 | + | ||
15 | + | ||
16 | +urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt", filename="ratings_train.txt") | ||
17 | +urllib.request.urlretrieve("https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt", filename="ratings_test.txt") | ||
18 | + | ||
19 | +train_data = pd.read_table('ratings_train.txt') | ||
20 | +test_data = pd.read_table('ratings_test.txt') | ||
21 | + | ||
22 | +# 리뷰 내용이 잘리지 않도록 전체 샘플 중 길이가 max_len 이하인 샘플의 비율이 몇 %인지 확인하는 함수를 만든다. | ||
23 | +def below_threshold_len(max_len, nested_list): | ||
24 | + cnt = 0 | ||
25 | + for s in nested_list: | ||
26 | + if(len(s) <= max_len): | ||
27 | + cnt = cnt + 1 | ||
28 | + print('전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s'%(max_len, (cnt / len(nested_list))*100)) | ||
29 | + | ||
30 | +# 예측 함수 | ||
31 | +def sentiment_predict(new_sentence): | ||
32 | + new_sentence = okt.morphs(new_sentence, stem=True) # 토큰화 | ||
33 | + new_sentence = [word for word in new_sentence if not word in stopwords] # 불용어 제거 | ||
34 | + encoded = tokenizer.texts_to_sequences([new_sentence]) # 정수 인코딩 | ||
35 | + pad_new = pad_sequences(encoded, maxlen = max_len) # 패딩 | ||
36 | + score = float(loaded_model.predict(pad_new)) # 예측 | ||
37 | + if(score > 0.5): | ||
38 | + print("{:.2f}% 확률로 긍정 리뷰입니다.\n".format(score * 100)) | ||
39 | + else: | ||
40 | + print("{:.2f}% 확률로 부정 리뷰입니다.\n".format((1 - score) * 100)) | ||
41 | + | ||
42 | +print('훈련용 리뷰 개수 :',len(train_data)) # 훈련용 리뷰 개수 출력 | ||
43 | + | ||
44 | +train_data[:5] # 상위 5개 출력 | ||
45 | +print('테스트용 리뷰 개수 :',len(test_data)) # 테스트용 리뷰 개수 출력 | ||
46 | + | ||
47 | +test_data[:5] | ||
48 | +train_data['document'].nunique(), train_data['label'].nunique() | ||
49 | + | ||
50 | +train_data.drop_duplicates(subset=['document'], inplace=True) # document 열에서 중복인 내용이 있다면 중복 제거 | ||
51 | + | ||
52 | +print('총 샘플의 수 :',len(train_data)) | ||
53 | + | ||
54 | +train_data['label'].value_counts().plot(kind = 'bar') | ||
55 | + | ||
56 | +print(train_data.groupby('label').size().reset_index(name = 'count')) | ||
57 | + | ||
58 | +print(train_data.isnull().values.any()) | ||
59 | + | ||
60 | +print(train_data.isnull().sum()) | ||
61 | + | ||
62 | +train_data.loc[train_data.document.isnull()] | ||
63 | + | ||
64 | +train_data = train_data.dropna(how = 'any') # Null 값이 존재하는 행 제거 | ||
65 | +print(train_data.isnull().values.any()) # Null 값이 존재하는지 확인 | ||
66 | + | ||
67 | +print(len(train_data)) | ||
68 | + | ||
69 | +text = 'do!!! you expect... people~ to~ read~ the FAQ, etc. and actually accept hard~! atheism?@@' | ||
70 | +re.sub(r'[^a-zA-Z ]', '', text) #알파벳과 공백을 제외하고 모두 제거 | ||
71 | + | ||
72 | +train_data['document'] = train_data['document'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","") | ||
73 | +# 한글과 공백을 제외하고 모두 제거 | ||
74 | +train_data[:5] | ||
75 | + | ||
76 | +train_data['document'].replace('', np.nan, inplace=True) | ||
77 | +print(train_data.isnull().sum()) | ||
78 | + | ||
79 | +train_data.loc[train_data.document.isnull()][:5] | ||
80 | + | ||
81 | +train_data = train_data.dropna(how = 'any') | ||
82 | +print(len(train_data)) | ||
83 | + | ||
84 | +test_data.drop_duplicates(subset = ['document'], inplace=True) # document 열에서 중복인 내용이 있다면 중복 제거 | ||
85 | +test_data['document'] = test_data['document'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣 ]","") # 정규 표현식 수행 | ||
86 | +test_data['document'].replace('', np.nan, inplace=True) # 공백은 Null 값으로 변경 | ||
87 | +test_data = test_data.dropna(how='any') # Null 값 제거 | ||
88 | +print('전처리 후 테스트용 샘플의 개수 :',len(test_data)) | ||
89 | + | ||
90 | +stopwords = ['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다'] | ||
91 | + | ||
92 | +okt = Okt() | ||
93 | +okt.morphs('와 이런 것도 영화라고 차라리 뮤직비디오를 만드는 게 나을 뻔', stem = True) | ||
94 | + | ||
95 | +X_train = [] | ||
96 | +for sentence in train_data['document']: | ||
97 | + temp_X = [] | ||
98 | + temp_X = okt.morphs(sentence, stem=True) # 토큰화 | ||
99 | + temp_X = [word for word in temp_X if not word in stopwords] # 불용어 제거 | ||
100 | + X_train.append(temp_X) | ||
101 | + | ||
102 | +print(X_train[:3]) | ||
103 | + | ||
104 | +X_test = [] | ||
105 | +for sentence in test_data['document']: | ||
106 | + temp_X = [] | ||
107 | + temp_X = okt.morphs(sentence, stem=True) # 토큰화 | ||
108 | + temp_X = [word for word in temp_X if not word in stopwords] # 불용어 제거 | ||
109 | + X_test.append(temp_X) | ||
110 | + | ||
111 | +# 정수 인코딩 | ||
112 | +tokenizer = Tokenizer() | ||
113 | +tokenizer.fit_on_texts(X_train) | ||
114 | +print(tokenizer.word_index) | ||
115 | + | ||
116 | +threshold = 3 | ||
117 | +total_cnt = len(tokenizer.word_index) # 단어의 수 | ||
118 | +rare_cnt = 0 # 등장 빈도수가 threshold보다 작은 단어의 개수를 카운트 | ||
119 | +total_freq = 0 # 훈련 데이터의 전체 단어 빈도수 총 합 | ||
120 | +rare_freq = 0 # 등장 빈도수가 threshold보다 작은 단어의 등장 빈도수의 총 합 | ||
121 | + | ||
122 | +# 단어와 빈도수의 쌍(pair)을 key와 value로 받는다. | ||
123 | +for key, value in tokenizer.word_counts.items(): | ||
124 | + total_freq = total_freq + value | ||
125 | + | ||
126 | + # 단어의 등장 빈도수가 threshold보다 작으면 | ||
127 | + if(value < threshold): | ||
128 | + rare_cnt = rare_cnt + 1 | ||
129 | + rare_freq = rare_freq + value | ||
130 | + | ||
131 | +print('단어 집합(vocabulary)의 크기 :',total_cnt) | ||
132 | +print('등장 빈도가 %s번 이하인 희귀 단어의 수: %s'%(threshold - 1, rare_cnt)) | ||
133 | +print("단어 집합에서 희귀 단어의 비율:", (rare_cnt / total_cnt)*100) | ||
134 | +print("전체 등장 빈도에서 희귀 단어 등장 빈도 비율:", (rare_freq / total_freq)*100) | ||
135 | + | ||
136 | +# 전체 단어 개수 중 빈도수 2이하인 단어 개수는 제거. | ||
137 | +# 0번 패딩 토큰과 1번 OOV 토큰을 고려하여 +2 | ||
138 | +vocab_size = total_cnt - rare_cnt + 2 | ||
139 | +print('단어 집합의 크기 :',vocab_size) | ||
140 | + | ||
141 | +tokenizer = Tokenizer(vocab_size, oov_token = 'OOV') | ||
142 | +tokenizer.fit_on_texts(X_train) | ||
143 | +X_train = tokenizer.texts_to_sequences(X_train) | ||
144 | +X_test = tokenizer.texts_to_sequences(X_test) | ||
145 | +print(X_train[:3]) | ||
146 | + | ||
147 | +y_train = np.array(train_data['label']) | ||
148 | +y_test = np.array(test_data['label']) | ||
149 | + | ||
150 | +drop_train = [index for index, sentence in enumerate(X_train) if len(sentence) < 1] | ||
151 | + | ||
152 | +# 빈 샘플들을 제거 | ||
153 | +X_train = np.delete(X_train, drop_train, axis=0) | ||
154 | +y_train = np.delete(y_train, drop_train, axis=0) | ||
155 | +print(len(X_train)) | ||
156 | +print(len(y_train)) | ||
157 | + | ||
158 | +# 패딩 | ||
159 | +print('리뷰의 최대 길이 :',max(len(l) for l in X_train)) | ||
160 | +print('리뷰의 평균 길이 :',sum(map(len, X_train))/len(X_train)) | ||
161 | +plt.hist([len(s) for s in X_train], bins=50) | ||
162 | +plt.xlabel('length of samples') | ||
163 | +plt.ylabel('number of samples') | ||
164 | +plt.show() | ||
165 | + | ||
166 | +max_len = 30 | ||
167 | +below_threshold_len(max_len, X_train) | ||
168 | +X_train = pad_sequences(X_train, maxlen = max_len) | ||
169 | +X_test = pad_sequences(X_test, maxlen = max_len) | ||
170 | + | ||
171 | +# LSTM 영화 리뷰 감성 분류 모델 제작 | ||
172 | +model = Sequential() | ||
173 | +model.add(Embedding(vocab_size, 100)) | ||
174 | +model.add(LSTM(128)) | ||
175 | +model.add(Dense(1, activation='sigmoid')) | ||
176 | + | ||
177 | +es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4) | ||
178 | +mc = ModelCheckpoint('best_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True) | ||
179 | +model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc']) | ||
180 | +history = model.fit(X_train, y_train, epochs=15, callbacks=[es, mc], batch_size=60, validation_split=0.2) | ||
181 | + | ||
182 | +# best model load | ||
183 | +loaded_model = load_model('best_model.h5') | ||
184 | +print("\n 테스트 정확도: %.4f" % (loaded_model.evaluate(X_test, y_test)[1])) | ||
185 | + | ||
186 | +sentiment_predict('이 영화 넘재밌어') | ||
187 | +sentiment_predict('감독 뭐하는 놈이냐?') | ||
188 | +sentiment_predict('와 개쩐다 정말 세계관 최강자들의 영화다') | ||
189 | +sentiment_predict('눈물이 주륵주륵 흐르네 그냥 ㅋㅋ') | ||
190 | +sentiment_predict('진짜 개재밌다 ㅋㅋㅋㅋㅋ') | ||
191 | +sentiment_predict('절대 보지마라 개 쓰레기영화 ㅋㅋ') | ||
192 | +sentiment_predict('아유 개 씹노잼 영화 돈버린다') | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or login to post a comment