愛林

[Python/NLP] 스팸 메일 분류하기 (Spam Detection) 본문

Data Science/Text Mining, 자연어처리

[Python/NLP] 스팸 메일 분류하기 (Spam Detection)

愛林 2023. 1. 9. 11:00
반응형

NLP 를 이용한 스팸 메일 분류


NLP 를 이용해서 스팸 메일을 분류해보자.

kaggle 에서 제공하는 스팸 메일 데이터를 이용했다.

 

https://www.kaggle.com/datasets/uciml/sms-spam-collection-dataset

 

SMS Spam Collection Dataset

Collection of SMS messages tagged as spam or legitimate

www.kaggle.com

 

여기서 csv파일을 다운받거나 urllib 를 사용해서 다운받으면 된다.

 

 

Enviroment


Python 3.9.12

Tensorflow 2.9.3

numpy 1.21.0

pandas 1.4.4

Tensorflow.keras 2.9.0

 

에서 문제없이 실행 가능했습니다.

 

 

Library Import & Data Import 


필요한 라이브러리를 import 해주고 데이터를 불러온다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import urllib.request
from sklearn.model_selection import train_test_split
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

tokenizer 는 keras 의 Tokenizer 를 사용해주었다.

 

 

EDA, Data Processing


data = pd.read_csv('spam.csv', encoding = 'latin1')
print("총 샘플의 수 : ", len(data))
총 샘플의 수 :  5572

 

5572개의 sample 이 존재한다.

 

불러온 데이터를 간단하게 확인해준다.

data[:5]

필요없는 Unnamed column 들이 보인다.

삭제해주도록 한다.

ham 은 스팸이 아닌 메일, spam 은 스팸 메일이다.  ham 실화냐

del data['Unnamed: 2']
del data['Unnamed: 3']
del data['Unnamed: 4']
data['v1'] = data['v1'].replace(['ham','spam'],[0,1])
data[:5]

 

data.info()

간단하게 정보를 확인해주었다.

5572 개의 데이터 중 Non-Null 이 두 칼럼 모두 5572개이므로 Null 값은 없는 데이터라는 것을 확인했다.

 

데이터에 중복이 없는 지 확인해보았다.

내용 기준으로 중복을 확인해야하기 때문에 v2 칼럼을 기준으로 

nunique()  함수를 사용해주었다.

print('v2열의 유니크한 값 :',data['v2'].nunique())
v2열의 유니크한 값 : 5169

데이터의 총 개수는 5572개인데

유니크 값은 5169개이다.

중복값에 대한 처리가 필요할 것 같다.

# 중복 제거
data.drop_duplicates(subset = ['v2'],inplace = True)
print(len(data))
5169

drop_duplicates 함수를 이용하여 v2 칼럼 기준으로 중복값을 drop 시켜주었다.

데이터가 5169개. 유니크한 값의 개수와 같아진 것을 확인할 수 있다.

 

스팸 데이터와 스팸 데이터가 아닌 데이터의 비율을 확인하기 위해 barplot 을 그려 확인해주었다.

data['v1'].value_counts().plot(kind = 'bar')
plt.show()

 

대부분이 정상 데이터(0) 이었다.

정확한 개수를 파악해주었다.

# 정확한 개수 파악
data.groupby('v1').size().reset_index(name = 'count')

%로 환산하여 비율을  확인했다.

# %로 환산
print(f'정상메일의 비율 = {round(data["v1"].value_counts()[0]/len(data) * 100,3)}%')
print(f'스팸 메일의 비율 = {round(data["v1"].value_counts()[1]/len(data) * 100,3)} %')
정상메일의 비율 = 87.367%
스팸 메일의 비율 = 12.633 %

 

X 데이터와 y 데이터를 분리해주었다.

X_data = data['v2']
y_data = data['v1']
# train test split
X_train, X_test, y_train, y_test = train_test_split(X_data, y_data, test_size = 0.2,
                                                   random_state = 0, stratify = y_data)

train_test_split 을 이용하여 검증용 데이터와 학습용 데이터를 나누어주었다.

 

Keras Tokenizer 를 사용하여 훈련 데이터를 Tokenize 해주고 정수 인코딩까지 진행해주었다.

tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
X_train_encoded = tokenizer.texts_to_sequences(X_train)
print(X_train_encoded[:5])
[[102, 1, 210, 230, 3, 17, 39], [1, 59, 8, 427, 17, 5, 137, 2, 2326], [157, 180, 12, 13, 98, 93, 47, 9, 40, 3485, 247, 8, 7, 87, 6, 80, 1312, 5, 3486, 7, 2327, 11, 660, 306, 20, 25, 467, 708, 1028, 203, 129, 193, 800, 2328, 23, 1, 144, 71, 2, 111, 78, 43, 2, 130, 11, 800, 186, 122, 1512], [1, 1154, 13, 104, 292], [222, 622, 857, 540, 623, 22, 23, 83, 10, 47, 6, 257, 32, 6, 26, 64, 936, 407]]

정수 인코딩이 잘 되었다.

 

토큰화의 결과와 각 토큰이 어떻게 정수화되었는지 확인하기 위해 word_index 를 사용하여 확인했다.

word_to_index = tokenizer.word_index
print(word_to_index)
{'i': 1, 'to': 2, 'you': 3, 'a': 4, 'the': 5, 'u': 6, 'and': 7, 'in': 8, 'is': 9, 'me': 10, 
'my': 11, 'for': 12, 'your': 13, 'it': 14, 'of': 15, 'have': 16, 'on': 17, 'call': 18, 
'that': 19, 'are': 20, '2': 21, 'now': 22, 'so': 23, 'but': 24, 'not': 25, 'can': 26, 'or': 27,
"i'm": 28, 'get': 29, 'at': 30, 'do': 31, 'if': 32, 'be': 33, 'will': 34, 'just': 35, 
'with': 36, 'we': 37, 'no': 38, 'this': 39, 'ur': 40, 'up': 41, '4': 42, 'how': 43, 
'gt': 44, 'lt': 45, 'go': 46, 'when': 47, 'from': 48, 'what': 49, 'ok': 50, 'out': 51,
..
중략

너무 길어서 중략해주었다.

이 정수 인코딩은 빈도가 많을수록 숫자가 적다.

i 가 가장 빈도가 높고, to 가 그 다음, you 가 3번째로 빈도 수가 높은 것이다.

 

등장 빈도 수가 낮은 토큰은 중요하지 않을 가능성이 크므로 제거해주도록 하자.

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

등장빈도가 1번밖에 되지 않는 단어를 제거해주도록 하자.

단어와 빈도수의 쌍을 key 와 value로 받는다.

value . 그 단어의 빈도수가 threshold (우리가 설정한 등장 빈도. 2번보다 낮아야 하니 2이다.) 보다 작으면

그 단어는 rare_cnt 에 들어간다.

등장 빈도가 1번 이하인 희귀 단어의 수: 4337
단어 집합(vocabulary)에서 희귀 단어의 비율: 55.45326684567191
전체 등장 빈도에서 희귀 단어 등장 빈도 비율: 6.65745644331875

등장 빈도가 1번 이하인 희귀 단어의 수는 4337개였다.

단어 집합에서 꽤나 비율이 높았다.

하지만 실제로 전체 단어들이 등장하는 빈도에서 등장하는 비율은 6.66% 밖에 되지 않는다.

만약 이런 분석을 통해서 등장 빈도가 지나치게 낮은 단어들은 처리에서 제외하고 싶을 때는

keras Tokenizer 선언 시에 단어 집합의 크기를 

  • tokenizer = Tokenizer(num_words = total_cnt - rare_cnt + 1)

와 같은 코드로 제한할 수 있다.

위 코드는 등장 빈도가 1회인 단어를 제외하고 토큰화시키는 코드이다.

 

이번 실습에서는 따로 단어 집합의 크기를 제한하지는 않고, 단어 집합의 크기를 vocab_size 에 저장한다.

길이를 맞추어주는 패딩 작업을 위해서 토큰이 0번인 단어를 고려한 +1 를 해 저장해준다.

vocab_size = len(word_to_index) + 1
print('단어 집합의 크기 : {}'.format((vocab_size)))
단어 집합의 크기 : 7822

 

이제 전체 데이터의 길이 분포를  확인해준다.

print("메일의 최대 길이 : %d " % max(len(sample) for sample in X_train_encoded))
print("메일의 평균 길이 : %f" % (sum(map(len, X_train_encoded))/len(X_train_encoded)))
plt.hist([len(sample) for sample in X_data], bins = 50)
plt.xlabel("length of samples")
plt.ylabel("Number of Samples")
plt.show()
메일의 최대 길이 : 189 
메일의 평균 길이 : 15.754534

제일 긴 메일은 189, 평균적으로는 15정도의 길이를 가지고 있으며,

0 - 100 사이의 길이가 가장 흔하다.

max_len = 189
X_train_padded = pad_sequences(X_train_encoded, maxlen = max_len)
print("훈련 데이터의 크기(shape) : ", X_train_padded.shape)
훈련 데이터의 크기(shape) :  (4135, 189)

padding 을 진행했다.

max_len 에는 189 를 넣어주었다.

이는 4135개의 X_train_encoded 의 길이를 전부 189로 맞추어주는 것이다.

189보다 길이가 짧은 메일 샘플은 전부 숫자 0이 패딩되어 189의 길이를 가진다.

 

 

Modeling


Tensorflow 의 RNN 을 사용하여 모델링 해주었다.

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

다 대 일 구조의 RNN 을 사용한다.

해당 문제는 마지막 시점에서 두 개의 선택지 중 하나를 예측하여 선정하는 이진분류 문제이다.

그러므로 출력층에서 Rogistic Regrssion 을 사용해야 하므로 활성화 함수로는 Sigmoid 함수를 사용하고,

손실 함수로는 Cross-Entropy 함수를 사용한다. 

4번의 Epoch 을 수행한다.

 

하이퍼파라미터인 Batch size 는 64이며, validation_split = 0.2 를 사용하여 훈련 데이터의 20%를 검증 데이터로 분리해서 사용하고, 검증 데이터를 사용하여 훈련이 적절하게 되고 있는 지를 확인한다.

검증 데이터는 기계가 훈련 데이터에 과적합 되고 있는 것이 아닌지를 확인하는 용도로 사용한다.

embedding_dim = 32
hidden_units = 32

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

model.compile(optimizer = 'rmsprop', loss = 'binary_crossentropy', metrics = ['acc'])
history = model.fit(X_train_padded, y_train, epochs = 4, batch_size = 64, validation_split = 0.2)
Epoch 1/4
52/52 [==============================] - 6s 85ms/step - loss: 0.4452 - acc: 0.8455 - val_loss: 0.2675 - val_acc: 0.9021
Epoch 2/4
52/52 [==============================] - 4s 72ms/step - loss: 0.1581 - acc: 0.9640 - val_loss: 0.1539 - val_acc: 0.9480
Epoch 3/4
52/52 [==============================] - 4s 72ms/step - loss: 0.1460 - acc: 0.9525 - val_loss: 0.1085 - val_acc: 0.9734
Epoch 4/4
52/52 [==============================] - 4s 72ms/step - loss: 0.0967 - acc: 0.9692 - val_loss: 0.1548 - val_acc: 0.9468

 

test 데이터에도 위와 똑같은 처리를 해주고, 테스트 데이터를 모델에 넣어 정확도를 확인해준다.

X_test_encoded = tokenizer.texts_to_sequences(X_test)
X_test_padded = pad_sequences(X_test_encoded, maxlen = max_len)
print("\n 테스트 정확도 : %.4f" % (model.evaluate(X_test_padded, y_test)[1]))
33/33 [==============================] - 1s 15ms/step - loss: 0.1577 - acc: 0.9420

 테스트 정확도 : 0.9420

약 94% 의 정확도를 보여주었다.

이번 실습에서는 훈련 데이터와 검증 데이터에 대해 같이 정확도를 확인하며 훈련했기 때문에,

이를 비교해서 loss 그래프를 그려서 과적합이 되지는 않는지 확인해주었다.

 

epochs = range(1, len(history.history['acc']) + 1)
plt.plot(epochs, history.history['loss'])
plt.plot(epochs, history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train','val'], loc = 'upper left')
plt.show()

이번 실습 데이터는 데이터의 양이 적어서 과적합이 빠르게 시작된다.

그러므로 검증 데이터에 대한 오차가 증가하기 시작하는 3 Epoch 정도가 적절하다.

 

 

 

 

Review


DL, NLP 에 대한 이해가 아직도 부족하다는 생각이 들었다.

DL 에 대한 공부도 많이 해야겠다는 생각이 든다.

Tensorflow , keras 를 이용한 NLP 가 편하지만 한국어를 할 수 없어 아쉬웠다.

 

다음 시간에는 한국어 자연어처리를 해보자.

 

 

 

참조


해당 책 필사, 참조

 

10-02 스팸 메일 분류하기(Spam Detection)

캐글에서 제공하는 스팸 메일 데이터를 학습시켜 스팸 메일 분류기를 구현해보겠습니다. ## 1. 스팸 메일 데이터에 대한 이해 * 다운로드 링크 : https://www.k…

wikidocs.net

 

Comments