GRU 기법을 사용해 주제를 분류해보자.

앞선 GRU기법에 대한 포스팅은 조만간 하겠다.

우선 필요한 라이브러리를 불러오자.

 

import pandas as pd
import urllib.request
%matplotlib inline
import matplotlib.pyplot as plt
import re
from konlpy.tag import Okt
from tensorflow.keras.preprocessing.text import Tokenizer
import numpy as np
from tensorflow.keras.preprocessing.sequence import pad_sequences
import os
from tensorflow.keras.datasets import reuters
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,LSTM,Embedding
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import load_model
from tensorflow.keras.layers import Embedding,Dense,GRU
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tqdm import tqdm

데이터를 불러오고,

train_data=pd.read_csv('C:/data/Bluehouse/train.csv',encoding='utf-8')
test_data=pd.read_csv('C:/data/Bluehouse/test.csv',encoding='utf-8')
sample_submission=pd.read_csv('C:/data/Bluehouse/sample_submission.csv',encoding='utf-8')

어떤 데이터인지 확인해보자.

우선 train_data부터.

train_data

[40000열*3행]

우선 인덱스와 real_index는 같지만(!?) 카테고리와 데이터는 정확하게 나왔다.

 

 

테스트 데이터를 살펴보면,

test_data

[5000열&2행]

위 데이터는 실제 청원의 대한 내용이다.

 

이제 train_data,test_data에 대한 전처리 및 불용어 처리를 시행해보자. 

앞서 설명했던 포스트처럼 불용어 처리할때, 개인의 dictionary를 사용하는게 좋다. 

여기서 주의점은 사용하는 용도에 따라 dictionary는 바뀌어야한다. 

train_data=train_data.dropna(how='any')
train_data['data']=train_data['data'].str.replace("[^ㄱ-ㅎㅏ-ㅣ가-힣]","")
test_data['data']=test_data['data'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣]','')
stopwords=['의','가','이','은','들','는','좀','질','걍','과','도','를','으로','자','에','와','한','하다','을','도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게','요','거','로','으로',
            '것','수','할','하는','제','에서','그','데','번','해도','죠','된','건','바','구','세']

 

가장 기본적인 전처리 작업 후, 본인이 애용하는 pykospacing을 사용해보자

from pykospacing import spacing
train_data['data']=train_data['data'].apply(spacing)
test_data['data']=test_data['data'].apply(spacing)

 

앞선 포스팅을 봤다면 알것이다. 이쯤되면 데이터 처리속도가 상당히 느려질 뿐더러 위 데이터 전처리 과정조차도 상당한 시간이 걸린다. 

이에 대해 전처리된 데이터를 저장하자.

 

import pickle
train_data.to_pickle("C:/data/Bluehouse/train_data.pkl")
test_data.to_pickle("C:/data/Bluehouse/test_data.pkl")

 

이제 자연어 처리를 해보자.

okt=Okt()
test_data['tokenized']=test_data['data'].apply(okt.morphs)
test_data['tokenized']=test_data['tokenized'].apply(lambda x:[item for item in x if item not in stopwords])
train_data['tokenized']=train_data['data'].apply(okt.morphs)
train_data['tokenized']=train_data['tokenized'].apply(lambda x:[item for item in x if item not in stopwords])

 

 

단어 집합의 크기와 회귀수 등 여러가지로 중요한 점을 파악해보자.

X_Train=train_data['tokenized'].values
X_Test=test_data['tokenized'].values
y_Train=to_categorical(train_data['category'])

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=total_freq+value
    if(value<threshold):
        rare_cnt=rare_cnt+1
        rare_freq=rare_freq+value
        
print("단어집합의 크기:",total_cnt)
print("등장 빈도가 %s번 이하인 회귀 단어의 수: %s"%(threshold-1,rare_cnt))
print("단어집합에서 회귀 단어의 비율:",(rare_cnt/total_cnt)*100)
print("전체 등장 빈도에서 회귀 단어 등장 빈도 비율:",(rare_freq/total_freq)*100)

단어집합의 크기: 133738
등장 빈도가 1번 이하인 회귀 단어의 수: 61680
단어집합에서 회귀 단어의 비율: 46.120025721933935
전체 등장 빈도에서 회귀 단어 등장 빈도 비율: 1.0407867591945446

단어집합의 크기는 1333738개 존재한다. 등장빈도가 threshold값인 2회 미만, 즉, 1회인 단어들은 집합에서 단어 집합에서 약 46퍼센트 차치한다. 실제로 훈련 데이터에서 등장빈도로 차지하는 비중은 매우 작은 수치인 1퍼센트 밖에 되지 않는다. 이 계산법 및 코드에 대해서는 차후 포스팅 하겠다.

 

vocab_size=total_cnt-rare_cnt+2
print("단어 집합의 크기:",vocab_size)

단어 집합의 크기: 72060

위 단어집합 크기를 사용해 토크나이징을 시행해보자.

 

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_Test[:2])
print(X_Train[:2])

text_to_sequences메서드를 시행함으로써 다음 결과는 이러하다.

[[435, 49, 1075, 3863, 799, 113, 577, 8, 27, 265, 113, 436, 12, 2, 9, 413, 27, 926, 829, 15, 2790, 2917, 6420, 9, 435, 49, 192], [357, 684, 105, 5549, 303, 23, 357, 684, 105, 3131, 35906, 3641, 64, 5549, 298, 866, 5061, 3131, 298, 19646, 1087, 803, 2801, 36356, 64, 2591, 1146, 12988, 5447, 1226, 3131, 1411, 7766, 3077, 819, 16487, 236, 970, 3301, 400, 1769, 319, 4050, 670]]
[[14825, 406, 4960, 2644, 131, 129, 254, 527, 5118, 16, 340, 381, 42, 131, 17717, 1300, 4289, 4, 89, 16, 7791, 7, 714, 131, 54362, 125, 11, 6880, 746, 6010, 45, 10, 4248, 45, 38, 2644, 131, 169, 3, 16, 340, 381, 10, 13, 5494, 2183, 3145, 382, 14, 4062, 340, 41, 8, 2945, 2869, 7, 254, 527, 574, 894, 66, 1882, 14826, 65, 89, 648, 229, 197, 894, 174, 23799, 231, 129, 344, 2869, 719, 1302, 2828, 45, 24, 11852, 4885, 182, 522, 3934, 575, 3035, 522, 2080, 174, 892, 2253, 404, 20871, 3035, 41, 8, 141, 1019, 6829, 213, 61, 5119, 515, 594, 2612, 246, 39625, 2796, 1221, 2167, 5495, 62, 131, 58, 12, 2309, 246, 2796, 338, 7342, 25011, 20872, 10427, 11853], [22, 533, 158, 20078, 2529, 137, 506, 1858, 864, 17, 5, 442, 1949, 26428, 6, 127, 41, 2529, 672, 2529, 506, 183, 2182, 78, 151, 11173, 151, 8478, 167, 4455, 41, 22, 15, 137, 30057, 5825, 6197, 158, 10168, 3178, 31, 54363, 22, 159, 155, 1293, 2859, 2529, 506, 11173, 116, 158, 7046, 2529, 158, 506, 1725, 66, 2029, 22, 533, 4710, 2663, 31, 116, 41, 22, 1177, 136, 60, 11173, 338, 1177, 1737, 159, 17205, 199, 26, 328, 2014, 3707, 578, 698, 54364, 1293, 187, 1729, 3609, 31, 170, 10285, 4063, 54, 5579, 10556, 357, 86, 38, 22, 54365, 2529, 225, 806, 148, 842, 81, 60, 258, 7, 1284, 29, 214, 1228, 3]]

결과는 위와 같으며, 실제 각 단어마다 정수 인코딩을 시행함으로써 각 단어에 numbering을 한것이라 볼 수 있다.

그렇다면 청원의 평균 길이는 얼마나 될까?

흔히 이것을 패딩이라한다. 이는 앞선 포스트에서 찾아볼 수 있다.

print("청원의 최대 길이:",max(len(i) for i in X_Train))
print("청원의 평균 길이:",sum(map(len,X_Train))/len(X_Train))
plt.hist([len(s) for s in X_Train],bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

청원의 최대 길이: 9005

청원의 평균 길이: 148.1867873574715

대략, 청원의 최대 길이는 9005이며 평균 150 길이정도 된다는 것을 그림 및 수치적으로 알 수 있다.

 

이제 category에 대한 labeling을 시행해보자.

 

Y_Train=to_categorical(train_data['category'])
def below_threshold_len(max_len,nested_list):
    cnt=0
    for s in nested_list:
        if(len(s)<=max_len):
            cnt+=1
    print("전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s"%(max_len,(cnt/len(nested_list))*100))

최대 길이가 5000인 경우 몇 개의 샘플들을 온전히 보전할 수 있는지 확인해보자.

 

max_len=5000
below_threshold_len(max_len,X_Train)

전체 샘플 중 길이가 5000 이하인 샘플의 비율: 99.99249849969995

test용 청원은 99.99퍼센트가 5000이하의 길이를 가진다. 그러므로, 훈련용 리뷰의 길이를 5000으로 지정하겠다. 

 

X_Train=pad_sequences(X_Train,maxlen=max_len)
X_Test=pad_sequences(X_Test,maxlen=max_len)

 

이제 GRU기법을 통해 얼마나 category에 근접한지 확인해보자.

modeling에 대해서는 가장 간단한 기초이므로 생략하겠다. 이는 앞선 딥러닝 기초에 대한 포스팅을 읽기 바란다.

 

model=Sequential()
model.add(Embedding(vocab_size,100))
model.add(GRU(120))
model.add(Dense(3,activation='softmax'))

es=EarlyStopping(monitor='val_loss',mode='min',verbose=1,patience=4)
mc=ModelCheckpoint('best_blue_GRU.h5',monitor='val_acc',mode='max',verbose=1,save_best_only=True)

model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['acc'])
history=model.fit(X_Train,Y_Train,epochs=5,batch_size=32)

Epoch 1/5 1250/1250 [==============================] - 9332s 7s/step - loss: 0.4483 - acc: 0.8139 Epoch 2/5 1250/1250 [==============================] - 9820s 8s/step - loss: 0.2283 - acc: 0.9159 Epoch 3/5 1250/1250 [==============================] - 8772s 7s/step - loss: 0.1363 - acc: 0.9493 Epoch 4/5 1250/1250 [==============================] - 8487s 7s/step - loss: 0.0820 - acc: 0.9689 Epoch 5/5 1250/1250 [==============================] - 8341s 7s/step - loss: 0.0526 - acc: 0.9796

 

 

y_pred=model.predict_classes(X_Test)
sample_submission['category']=y_pred
sample_submission.to_csv('c:/data/Bluehouse/submission.csv',encoding='utf-8',index=False)

 

이후 제출을 함으로써 얼마나 정확성이 나오나 확인해보자.

 

여기서 알 수 있는점은 단순히 불용어처리 즉 dictionary를 얼마나 알차게 짯느냐, 또한 알고리즘을 LSTM으로 짯느냐 혹은 GRU기법을 짯는냐에 따라 정확도는 올라가고 내려간다는것을 알 수 있다. 본인이 이 포스팅을 짜기전에 불용어(stopwords)는 똑같이 지정하고,  기법만 다르게 해봤다. 무려 0.4퍼센트 차이가 난다. 그러므로 다시한번 느끼지만 NLP는 얼마나 사전작업을 치느냐가 관건이라고 생각한다.

+ Recent posts