Text Classification

( 참고 : “딥러닝을 이용한 자연어 처리 입문” (https://wikidocs.net/book/2155) )

Text Classification : 텍스트를 특정 카테고리로 분류하는 것

  • ex1) 이 메세지는 스팸 메세지인가 아닌가?
  • ex2) 이 기사는 ‘A~F’까지의 주제 중, 어느 주제 에 속하는가? (multi-class/ multi-label classification)


이번 포스트에서는 RNN, LSTM,Naive Bayes을 이용하여 총 3 가지의 Text Classification 실습을 할 것이다.

  • 1) RNN을 사용하여 Spam Mail 분류하기
  • 2) LSTM을 사용하여 Reuter News 분류하기
  • 3) Naive Bayes를 사용하여 News Group 분류하기


1. [RNN] Spam Mail Classification

GOAL : RNN을 사용하여 Spam Mail 분류하기

a) Import Dataset

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
data= pd.read_csv('spam.csv', encoding='latin1')
data.head()
v1 v2 Unnamed: 2 Unnamed: 3 Unnamed: 4
0 ham Go until jurong point, crazy.. Available only ... NaN NaN NaN
1 ham Ok lar... Joking wif u oni... NaN NaN NaN
2 spam Free entry in 2 a wkly comp to win FA Cup fina... NaN NaN NaN
3 ham U dun say so early hor... U c already then say... NaN NaN NaN
4 ham Nah I don't think he goes to usf, he lives aro... NaN NaN NaN

첫 번째 열에는 해당 메시지의 ‘spam’,’ham’여부인 label 값이, 두 번째 열에는 해당 메세지의 내용이 담겨있다.

( ham을 0으로, spam을 1로 치환해준다 )

X = data['v2']
y = data['v1']
y = y.replace({'ham':0, 'spam':1})


b). Tokenize

메세지 내의 모든 문자들을 tokenize 해준다

t = Tokenizer()
t.fit_on_texts(X)
sequences = t.texts_to_sequences(X)


ex) 3번째 메세지의 구성 token들은?

sequences[3]
[6, 245, 152, 23, 379, 2989, 6, 140, 154, 57, 152]


word2index = t.word_index
vocab_size = len(word2index)+1


train : test를 8:2의 비율로 나눠준다.

n_train = int(X.shape[0]*0.8)
n_test = int(X.shape[0]-n_train)


평균적으로 하나의 메세지는 약 16개의 단어로 구성되어 있고, 가장 긴 메세지는 189개의 단어로 구성 되어있다.

X_tokenized = sequences
print('MAX mail length :', max(len(l) for l in X_tokenized))
print('AVG mail length :', (sum(map(len,X_tokenized)) / len(X_tokenized)))
MAX mail length : 189
AVG mail length : 15.794867193108399


문장 별로 단어 개수(문장의 길이)가 다르기 때문에, 가장 긴 문장의 길이(=189)를 기준으로 padding 해준다.

max_len = 189
data = pad_sequences(X_tokenized, maxlen=max_len)
data.shape
(5572, 189)


X_train = data[:n_train]
y_train = np.array(y[:n_train])

X_test = data[n_train:]
y_test = np.array(y[n_train:])


c) Text classification with RNN

from tensorflow.keras.layers import SimpleRNN, Embedding, Dense
from tensorflow.keras.models import Sequential

단어 벡터를 32차원으로 Embedding 해주고, 32개의 neuron을 가진 RNN layer를 더해준다. 마지막 output neuron의 activation function으로는 sigmoid를 사용한다.

model = Sequential()
model.add(Embedding(vocab_size,32))
model.add(SimpleRNN(32))
model.add(Dense(1,activation='sigmoid'))


모델의 구조를 간략하게 파악해보면 다음과 같다.

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 32)          285472    
_________________________________________________________________
simple_rnn (SimpleRNN)       (None, 32)                2080      
_________________________________________________________________
dense (Dense)                (None, 1)                 33        
=================================================================
Total params: 287,585
Trainable params: 287,585
Non-trainable params: 0
_________________________________________________________________


d) train model

위에서 짠 모델을 대상으로 training을 진행한다.

  • optimizer = Adam이고

  • loss function = binary cross entropy (spam/ham 두 개 중 하나로 분류하는 것)

  • epoch = 4

  • batch size = 64로 한다

    ( train data의 20%는 떼어내에서 validation data로 사용 )

model.compile(optimizer='adam', loss='binary_crossentropy',metrics=['acc'])
history = model.fit(X_train, y_train, epochs=4, batch_size=64, validation_split=0.2)

그 결과, train dataset의 정확도는 99.35%, validation dataset의 정확도는 0.9843으로 매우 정확히 분류함을 확인할 수 있다.

Train on 3565 samples, validate on 892 samples
Epoch 1/4
3565/3565 [==============================] - 8s 2ms/sample - loss: 0.4226 - acc: 0.8555 - val_loss: 0.3812 - val_acc: 0.8599
Epoch 2/4
3565/3565 [==============================] - 7s 2ms/sample - loss: 0.2788 - acc: 0.8979 - val_loss: 0.1863 - val_acc: 0.9540
Epoch 3/4
3565/3565 [==============================] - 6s 2ms/sample - loss: 0.0751 - acc: 0.9857 - val_loss: 0.0747 - val_acc: 0.9809
Epoch 4/4
3565/3565 [==============================] - 7s 2ms/sample - loss: 0.0293 - acc: 0.9935 - val_loss: 0.0638 - val_acc: 0.9843


e) test accuracy

model.evaluate(X_test,y_test)
1115/1 [======================================] - 1s 725us/sample - loss: 0.0424 - acc: 0.9812

[0.05866116133806684, 0.98116595]


2. [LSTM] Reuter News Classification

GOAL : LSTM을 사용하여 Reuter News 분류하기

a) Import Dataset

from tensorflow.keras.datasets import reuters
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns


전체 데이터의 80%를 train data로 한다.

(X_train, y_train), (X_test, y_test) = reuters.load_data(num_words=None, test_split=0.2)
print('# of Train dataset: {}'.format(len(X_train)))
print('# of Test dataset : {}'.format(len(X_test)))
# of Train dataset: 8982
# of Test dataset : 2246


이 8982편의 뉴스는 총 46개의 카테고리로 구성되어 있음을 확인할 수 있다.

num_classes = max(y_train) + 1
print('# of Categories : {}'.format(num_classes))
# of Categories : 46


하나의 뉴스는 평균적으로 146개의 단어로 구성되어있고, 가장 긴 뉴스는 무려 2376개의 단어로 구성되어 있다!

print('MAX news length :{}'.format(max(len(l) for l in X_train)))
print('AVG news length :{}'.format(sum(map(len, X_train))/len(X_train)))
MAX news length :2376
AVG news length :145.5398574927633


word2index = reuters.get_word_index()
index2word = {}
for key, value in word2index.items():
    index2word[value] = key


b) Text Classification with LSTM

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

등장 빈도 상위 1,000등 까지의 단어들 만을 사용한다.

(X_train, y_train), (X_test, y_test) = reuters.load_data(num_words=1000, test_split=0.2)


마찬가지로 문장별로 길이가 다르기 때문에 padding을 해준다.

이 때, padding의 최대 길이를 100으로 한다.

( = 즉, 100단어의 길이를 넘어가는 기사는 앞에서 100번쨰 단어까지만 나오고 뒷부분은 잘린다 )

max_len = 100
X_train = pad_sequences(X_train, maxlen=max_len) 
X_test = pad_sequences(X_test, maxlen=max_len) 
y_train = to_categorical(y_train)
y_test = to_categorical(y_test) 


기존에 1000차원이었던 단어 벡터를 120차원으로 임베딩해준다.

이어서 120개의 neuron을 가진 LSTM layer을 통과하고, 마지막으로 softmax function을 통해서 각각 46개의 주제에 속하게 될 확률값을 반환한다.

model = Sequential()
model.add(Embedding(1000, 120))
model.add(LSTM(120))
model.add(Dense(46, activation='softmax'))


c) Train model

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(X_train, y_train, batch_size=100, epochs=20, validation_data=(X_test, y_test))
Train on 8982 samples, validate on 2246 samples
Epoch 1/20
8982/8982 [==============================] - 45s 5ms/sample - loss: 2.5350 - accuracy: 0.3625 - val_loss: 2.0720 - val_accuracy: 0.4947
...
Epoch 19/20
8982/8982 [==============================] - 34s 4ms/sample - loss: 0.9454 - accuracy: 0.7577 - val_loss: 1.2212 - val_accuracy: 0.6923
Epoch 20/20
8982/8982 [==============================] - 39s 4ms/sample - loss: 0.9251 - accuracy: 0.7620 - val_loss: 1.1980 - val_accuracy: 0.6959


d) Test accuracy

model.evaluate(X_test, y_test)[1]
2246/1 [============================] - 4s 2ms/sample - loss: 1.5107 - accuracy: 0.6959

0.69590384


3. [Naive Bayes] News Group Classification

GOAL : Naive Bayes를 활용하여 News Group 분류하기

이 모델은 기존 앞의 두 모델들 (RNN,LSTM)에 비해 정교하지는 않지만 간단한 모델이라는 장점이 있다.

a) Import Dataset

from sklearn.datasets import fetch_20newsgroups
newsdata = fetch_20newsgroups(subset='train')


다음과 같이 총 20개의 뉴스 카테고리가 있다.

newsdata.target_names
['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']


총 11314개의 뉴스가 있다.

len(newsdata.data)
11314


첫 번째 뉴스의 내용을 들여다 보면 다음과 같다.

newsdata.data[0]
"From: lerxst@wam.umd.edu (where's my thing)\nSubject: WHAT car is this!?\nNntp-Posting-Host: rac3.wam.umd.edu\nOrganization: University of Maryland, College Park\nLines: 15\n\n I was wondering if anyone out there could enlighten me on this car I saw\nthe other day. It was a 2-door sports car, looked to be from the late 60s/\nearly 70s. It was called a Bricklin. The doors were really small. In addition,\nthe front bumper was separate from the rest of the body. This is \nall I know. If anyone can tellme a model name, engine specs, years\nof production, where this car is made, history, or whatever info you\nhave on this funky looking car, please e-mail.\n\nThanks,\n- IL\n   ---- brought to you by your neighborhood Lerxst ----\n\n\n\n\n"


b) Data Preprocessing

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score


텍스트 데이터를 DTM(Document-Term Matrix)로 변환해준다

dtmvector = CountVectorizer()
X_train_dtm = dtmvector.fit_transform(newsdata.data)


11314편의 뉴스에 총 130107개(종류)의 단어가 사용된 것을 알 수 있다.

X_train_dtm.shape
(11314, 130107)


이 DTM을 TF-IDF로 변환해준다 ( shape는 동일 )

tfidf_transformer = TfidfTransformer()
tfidv = tfidf_transformer.fit_transform(X_train_dtm)


c) Modeling

  • alpha=1 : Laplace Smoothing

    ( 분모에 매우 작은 수를 더해주는 것! 자세한 것은 Naive Bayes 참고 )

mod = MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True) # alpha=1 : Laplace Smoothing
mod.fit(tfidv, newsdata.target)


이제 테스트 데이터를 통해 얼마나 이 분류기가 잘 작동하는지 확인해보자.

newsdata_test = fetch_20newsgroups(subset='test', shuffle=True)
X_test_dtm = dtmvector.transform(newsdata_test.data)
tfidv_test = tfidf_transformer.transform(X_test_dtm)


  • 예측값
predicted = mod.predict(tfidv_test)
predicted
array([ 7, 11,  0, ...,  9,  3, 15])


  • 실제값
newsdata_test.target
array([ 7,  5,  0, ...,  9,  6, 15])


정확도는 약 77.4%된다.

accuracy_score(newsdata_test.target, predicted)
0.7738980350504514