<jupyter_start><jupyter_text>Imports<jupyter_code>from tqdm import tqdm
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
from konlpy.tag import Okt
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences<jupyter_output><empty_output><jupyter_text>Loading Data<jupyter_code>df = pd.read_csv('./data/processed/df.csv')
df.head(3)
# Stop words

stopwords = pd.read_table('./data/stopwords.txt')
# ['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다']
stopwords[:5]<jupyter_output><empty_output><jupyter_text>Data Preprocessing<jupyter_code>okt = Okt()
okt.morphs('와 이런 것도 영화라고 차라리 뮤직비디오를 만드는 게 나을 뻔', stem = True)
X_train = []
for sentence in tqdm(df['review'][:7465]): # 데이터의 80%를 train 데이터로 쪼갬
    tokenized_sentence = okt.morphs(sentence, stem=True) # 토큰화
    stopwords_removed_sentence = [word for word in tokenized_sentence if not word in stopwords] # 불용어 제거
    X_train.append(stopwords_removed_sentence)
print(X_train[:3])
X_test = []
for sentence in tqdm(df['review'][7465:]): # 데이터의 20%를 test 데이터로 쪼갬
    tokenized_sentence = okt.morphs(sentence, stem=True) # 토큰화
    stopwords_removed_sentence = [word for word in tokenized_sentence if not word in stopwords] # 불용어 제거
    X_test.append(stopwords_removed_sentence)<jupyter_output>100%|██████████| 1866/1866 [00:03<00:00, 546.59it/s]<jupyter_text>**Intger Encoding**<jupyter_code>tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train) # 단어 집합이 생성되는 동시에 고유한 정수가 부여됨
print(list(tokenizer.word_index)[:5]) # 단어가 11,000 개가 넘게 존재하며, 부여된 고유한 정수를 확인할 수 있음<jupyter_output>['이', '하다', '영화', '보다', '의']<jupyter_text>각 정수는 전체 훈련 데이터에서 등장 빈도수가 높은 순서대로 부여되었기 때문에, 높은 정수가 부여된 단어들은 등장 빈도수가 매우 낮다는 것을 의미함 여기서는 빈도수가 낮은 단어들은 자연어 처리에서 배제하고자 함 등장 빈도수가 3회 미만인 단어들이 이 데이터에서 얼만큼의 비중을 차지하는지 확인해볼 것임<jupyter_code>threshold = 3
total_cnt = len(tokenizer.word_index) # 단어의 수
rare_cnt = 0 # 등장 빈도수가 threshold보다 작은 단어의 개수를 카운트
total_freq = 0 # 훈련 데이터의 전체 단어 빈도수 총 합
rare_freq = 0 # 등장 빈도수가 threshold보다 작은 단어의 등장 빈도수의 총 합

# 단어와 빈도수의 쌍(pair)을 key와 value로 받는다.
for key, value in tokenizer.word_counts.items():
    total_freq = total_freq + value

    # 단어의 등장 빈도수가 threshold보다 작으면
    if(value < threshold):
        rare_cnt = rare_cnt + 1
        rare_freq = 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)
# 전체 단어 개수 중 빈도수 2이하인 단어는 제거.
# 0번 패딩 토큰을 고려하여 + 1
vocab_size = total_cnt - rare_cnt + 1
print('단어 집합의 크기 :',vocab_size)
# 이를 케라스 토크나이저의 인자로 넘겨주고 텍스트 시퀀스를 정수 시퀀스로 변환함

tokenizer = Tokenizer(vocab_size) 
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])
print(X_test[:3])
y_train = np.array(df['sentiment'][:7465])
y_test = np.array(df['sentiment'][7465:])

print(len(y_train),len(y_test)) # 길이를 보아하니 잘 나누어 진 것 같다.<jupyter_output>7465 1866<jupyter_text>remove empty samples<jupyter_code># remove sentence which length is less than 1
drop_train = [index for index, sentence in enumerate(X_train) if len(sentence) < 1] 
print(len(drop_train)) # 길이가 0인 샘플은 10개가 있다.
# drop
print(len(X_train))
print(len(y_train))

X_train = np.delete(X_train, drop_train, axis=0)
y_train = np.delete(y_train, drop_train, axis=0)

print(len(X_train))
print(len(y_train))<jupyter_output>7465
7465
7455
7455<jupyter_text>add padding<jupyter_code>print('리뷰의 최대 길이 :',max(len(review) for review in X_train))
print('리뷰의 평균 길이 :',sum(map(len, X_train))/len(X_train))

# check sentence length distribution
plt.hist([len(review) for review in X_train], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()<jupyter_output>리뷰의 최대 길이 : 339
리뷰의 평균 길이 : 18.23849765258216<jupyter_text>가장 긴 리뷰의 길이는 339이며, 그래프를 봤을 때 전체 데이터의 길이 분포는 대체적으로 약 25내외의 길이를 가지는 것을 볼 수 있슴 모델이 처리할 수 있도록 X_train과 X_test의 모든 샘플의 길이를 특정 길이로 동일하게 맞춰줄 필요가 있슴 특정 길이 변수를 max_len으로 정함 대부분의 리뷰가 내용이 잘리지 않도록 할 수 있는 최적의 max_len의 값은 얼마인지 확인하기 위해, 전체 샘플 중 길이가 max_len 이하인 샘플의 비율이 몇 %인지 확인하는 함수를 만듦<jupyter_code>def below_threshold_len(max_len, nested_list):
    count = 0
    for sentence in nested_list:
        if(len(sentence) <= max_len):
            count = count + 1
    print('전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s'%(max_len, (count / len(nested_list))*100))
max_len = 50

below_threshold_len(max_len, X_train)<jupyter_output>전체 샘플 중 길이가 50 이하인 샘플의 비율: 94.19181757209925<jupyter_text>전체 훈련 데이터 중 약 94%의 리뷰가 50이하의 길이를 가지는 것을 확인함 모든 샘플의 길이를 50으로 맞추어 보겠다.<jupyter_code>X_train = pad_sequences(X_train, maxlen=max_len)
X_test = pad_sequences(X_test, maxlen=max_len)<jupyter_output><empty_output><jupyter_text>LSTM Modeling<jupyter_code>from tensorflow.keras.layers import Embedding, Dense, LSTM
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(LSTM(hidden_units))
model.add(Dense(1, activation='sigmoid'))

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('./model/lnh_lstm_model.h5', 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, epochs=15, callbacks=[es, mc], batch_size=64, validation_split=0.2)
print("\n 테스트 정확도: %.4f" % (model.evaluate(X_test, y_test)[1]))<jupyter_output>59/59 [==============================] - 0s 4ms/step - loss: 0.5907 - acc: 0.7583

 테스트 정확도: 0.7583<jupyter_text>Evaluation<jupyter_code>def sentiment_predict(new_sentence):
    loaded_model = load_model('./model/lnh_lstm_model.h5')
    
    new_sentence = re.sub(r'[^ㄱ-ㅎㅏ-ㅣ가-힣 ]','', new_sentence)
    new_sentence = okt.morphs(new_sentence, stem=True) # 토큰화
    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}% 확률로 긍정 리뷰입니다.\n".format(score * 100))
    else:
        print("{:.2f}% 확률로 부정 리뷰입니다.\n".format((1 - score) * 100))
sentiment_predict('이 영화 개꿀잼 ㅋㅋㅋ')
sentiment_predict('이 영화 핵노잼 ㅠㅠ')
sentiment_predict('이딴게 영화냐 ㅉㅉ')
sentiment_predict('감독 뭐하는 놈이냐?')
sentiment_predict('와 개쩐다 정말 세계관 최강자들의 영화다')<jupyter_output>89.73% 확률로 긍정 리뷰입니다.

97.07% 확률로 부정 리뷰입니다.

96.69% 확률로 부정 리뷰입니다.

77.51% 확률로 부정 리뷰입니다.

66.25% 확률로 긍정 리뷰입니다.
{'fraction_non_alphanumeric': 0.1309786607799853, 'fraction_numerical': 0.034142752023546726, 'mean_word_length': 5.28677150786309, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 38, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '493663', 'license': 'MIT', 'n_tokens_mistral': 3783, 'n_tokens_neox': 4002, 'n_words': 683, 'path': '2_3_lnh_example_okt.ipynb', 'repo_name': 'tmxkqotnl/project_mini_nlp', 'size': '6795'}