00. 데이터셋(캐글)
https://www.kaggle.com/uciml/sms-spam-collection-dataset
환경
1. 주피터노트북
2. 파이썬
01. 임포트
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 # padding
Tokenizer : 문장을 단어로 토큰화
pad_sequencse(패딩) : 여러 문장의 길이를 임의로 동일하게 맞춰주는 작업에 사용
02. 데이터 불러오기
data = pd.read_csv('spam.csv', encoding='latin1')
print('총 샘플의 수 :',len(data))
>>> 총 샘플의 수 : 5572
data.head()
v1열에서 ham은 정상메일, spam은 스팸 메일을 의미한다
v2열은 본문을 담고 있다
그 외 unnamed 2 , 3 , 4 는 불필요한 열이다
unnamed 열은 제거하고, ham = 0 , spam = 1로 변환한다
# 컬럼 삭제
del data['Unnamed: 2']
del data['Unnamed: 3']
del data['Unnamed: 4']
data['v1'] = data['v1'].replace(['ham','spam'], [0, 1])
data[:5]
03. 결측치 확인 & 유니크 값 확인
print('결측값 여부 :', data.isnull().values.any())
>>> 결측값 여부 : False
print('v1열의 유니크한 값 :', data['v1'].nunique())
>>> v1열의 유니크한 값 : 2
print('v2열의 유니크한 값 :', data['v2'].nunique())
>>> v2열의 유니크한 값 : 5169
04. v2 열 중복내용 제거
# v2 열(메일 내용)에서 중복인 내용이 있다면 중복 제거
data.drop_duplicates(subset=['v2'], inplace=True)
print('총 샘플의 수 :', len(data))
>>> 총 샘플의 수 : 5169
총 샘플의 수가 5,572개에서 5,169개로 줄었습니다
05. v1 열 데이터 분포확인 & 시각화
data['v1'].value_counts().plot(kind='bar')
print('정상 메일과 스팸 메일의 개수')
data.groupby('v1').size().reset_index(name='count')
06. 정상 메일 & 스팸 메일 비율 확인
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%
07. X_data , Y_data로 구분
# v2열과 v1열을 X데이터와 y데이터라는 X_data, y_data로 저장합니다.
X_data = data['v2']
y_data = data['v1']
print('메일 본문의 개수: {}'.format(len(X_data)))
print('레이블의 개수: {}'.format(len(y_data)))
메일 본문의 개수: 5169
레이블의 개수: 5169
08. 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)
stratify: stratify 파라미터는 분류 문제를 다룰 때 매우 중요하게 활용되는 파라미터 값 입니다. stratify 값으로는 target 값을 지정해주면 됩니다.
stratify값을 target 값으로 지정해주면 target의 class 비율을 유지 한 채로 데이터 셋을 split 하게 됩니다. 만약 이 옵션을 지정해주지 않고 classification 문제를 다룬다면, 성능의 차이가 많이 날 수 있습니다.
09. split 후 train, test 데이터 비율 확인
print('--------훈련 데이터의 비율-----------')
print(f'정상 메일 = {round(y_train.value_counts()[0]/len(y_train) * 100,3)}%')
print(f'스팸 메일 = {round(y_train.value_counts()[1]/len(y_train) * 100,3)}%')
--------훈련 데이터의 비율-----------
정상 메일 = 87.376%
스팸 메일 = 12.624%
print('--------테스트 데이터의 비율-----------')
print(f'정상 메일 = {round(y_test.value_counts()[0]/len(y_test) * 100,3)}%')
print(f'스팸 메일 = {round(y_test.value_counts()[1]/len(y_test) * 100,3)}%')
--------테스트 데이터의 비율-----------
정상 메일 = 87.331%
스팸 메일 = 12.669%
-> 훈련 데이터, 테스트 데이터 분리
훈련 데이터와 테스트 데이터 모두 정상 메일은 87%, 스팸 메일은 12%가 존재합니다.
10. 토큰화 & 정수 인코딩
이제 케라스 토크나이저를 통해 훈련 데이터에 대해서 토큰화와 정수 인코딩 과정을 수행합니다. X_train_encoded에는 X_train의 각 단어들이 맵핑되는 정수로 인코딩되어 저장됩니다.
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
X_train_encoded = tokenizer.texts_to_sequences(X_train)
print(X_train_encoded[:5])
위에서 부여된 각 정수는 각 단어의 빈도수가 높을 수록 낮은 정수가 부여됩니다.
다시 말해, 단어 i는 현재 전체 훈련 데이터에서 빈도수가 가장 높은 단어입니다.
각 단어에 대한 등장 빈도수는 tokenizer.word_counts.items()를 출력해서 확인할 수 있습니다. 이를 응용하여 빈도수가 낮은 단어들이 훈련 데이터에서 얼만큼의 비중을 차지하는지 확인해볼 수 있습니다.
fit_on_texts() : 문자 데이터를 입력받아서 리스트의 형태로 변환합니다
sentences = [
'I love my dog',
'I love my cat',
'You love my dog!'
]
tokenizer = Tokenizer(num_words = 100)
tokenizer.fit_on_texts(sentences)
word_index = tokenizer.word_index
print(word_index)
{'love': 1, 'my': 2, 'i': 3, 'dog': 4, 'cat': 5, 'you': 6}
texts_to_sequences() : 텍스트 안의 단어들을 숫자의 시퀀스의 형태로 변환합니다
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.preprocessing.text import Tokenizer
sentences = [
'I love my dog',
'I love my cat',
'You love my dog!',
'Do you think my dog is amazing?'
]
tokenizer = Tokenizer(num_words = 100)
tokenizer.fit_on_texts(sentences)
word_index = tokenizer.word_index
sequences = tokenizer.texts_to_sequences(sentences)
print(word_index)
print(sequences)
{'my': 1, 'love': 2, 'dog': 3, 'i': 4, 'you': 5, 'cat': 6, 'do': 7, 'think': 8, 'is': 9, 'amazing': 10}
[[4, 2, 1, 3], [4, 2, 1, 6], [5, 2, 1, 3], [7, 5, 8, 1, 3, 9, 10]]
[4, 2, 1, 3] -> [ 4( i ) , 2( love ) , 1( my ) , 3( dog) ] -> i love my dog
11. 등장 빈도가 1번 이하인 희귀 단어 찾기 & 시각화
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('단어 사전의 총 단어수: ', len(word_to_index))
print('등장 빈도가 %s번 이하인 희귀 단어의 수: %s'%(threshold - 1, rare_cnt))
print("단어 집합(vocabulary)에서 희귀 단어의 비율:", (rare_cnt / total_cnt)*100)
print("전체 등장 빈도에서 희귀 단어 등장 빈도 비율:", (rare_freq / total_freq)*100)
단어 사전의 총 단어수: 7821
등장 빈도가 1번 이하인 희귀 단어의 수: 4337
단어 집합(vocabulary)에서 희귀 단어의 비율: 55.45326684567191
전체 등장 빈도에서 희귀 단어 등장 빈도 비율: 6.65745644331875
등장 빈도가 threshold 값인 2회 미만. 즉, 1회 밖에 되지 않는 단어들은 단어 집합에서 무려 절반 이상을 차지합니다.
하지만, 실제로 훈련 데이터에서 등장 빈도로 차지하는 비중은 상대적으로 적은 수치인 6%밖에 되지 않습니다.
만약, 이러한 분석을 통해 등장 빈도가 지나치게 낮은 단어들은 자연어 처리에서 제외하고 싶다면
케라스 토크나이저 선언 시에 단어 집합의 크기를 제한할 수 있습니다.
가령, 아래의 코드로 등장 빈도가 1회인 단어들을 제외할 수 있을 겁니다.
tokenizer = Tokenizer(num_words = total_cnt - rare_cnt + 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이며, 전체 데이터의 길이 분포는 대체적으로 약 50이하의 길이를 가집니다.
12. 패딩 : 긴 텍스트는 자르고, 짧은 텍스트는 패딩을 더해서 늘리고.
# 패딩 : 긴 텍스트는 자르고, 짧은 텍스트는 패딩을 더해서 늘리고.
max_len = 189
X_train_padded = pad_sequences(X_train_encoded, maxlen = max_len)
print("패딩 전 훈련 데이터의 크기(len):", len(X_train_encoded))
print("패딩 후 훈련 데이터의 크기(shape):", X_train_padded.shape)
패딩 전 훈련 데이터의 크기(len): 4135
패딩 후 훈련 데이터의 크기(shape): (4135, 189)
maxlen에는 가장 긴 메일의 길이였던 189이라는 숫자를 넣었습니다.
이는 4,135개의 X_train_encoded의 길이를 전부 189로 바꿉니다.
189보다 길이가 짧은 메일 샘플은 전부 숫자 0이 패딩되어 189의 길이를 가집니다
X_train_encoded 데이터는 4,135 × 189의 크기를 갖게됩니다. 모델을 설계해보겠습니다.
X_train_padded[0]
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
102, 1, 210, 230, 3, 17, 39])
# RNN으로 스팸 메일 분류하기
하이퍼파라미터인 임베딩 벡터의 차원은 64, 은닉 상태의 크기는 64입니다.
모델은 다 대 일 구조의 RNN을 사용합니다.
해당 모델은 마지막 시점에서 두 개의 선택지 중 하나를 예측하는 이진 분류 문제를 수행하는 모델입니다.
이진 분류 문제의 경우, 출력층에 로지스틱 회귀를 사용해야 하므로 활성화 함수로는 시그모이드 함수를 사용하고, 손실 함수로 binary 크로스 엔트로피 함수를 사용하여 8번의 에포크를 수행합니다.
하이퍼파라미터인 배치 크기는 128이며, validation_split=0.2을 사용하여 훈련 데이터의 20%를 검증 데이터로 분리해서 사용하고, 검증 데이터를 통해서 훈련이 적절히 되고 있는지 확인합니다. 검증 데이터는 기계가 훈련 데이터에 과적합되고 있지는 않은지 확인하기 위한 용도로 사용됩니다.
from tensorflow.keras.layers import SimpleRNN, Embedding, Dense
from tensorflow.keras.models import Sequential
embedding_dim = 64
hidden_units = 64
model = Sequential()
model.add(Embedding(vocab_size, embedding_dim, input_length=189))
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=13, batch_size=128, validation_split=0.2)
layer : SimpleRNN , Embedding, Dense
SimpleRNN
: timestep의 출력이 다음 timestep으로 완전히 연결된 모델
Embedding
첫번째 인자 = 단어 집합의 크기. 즉, 총 단어의 개수
두번째 인자 = 임베딩 벡터의 출력 차원. 결과로서 나오는 임베딩 벡터의 크기
input_length = 입력 시퀀스의 길이
Dense Layer (거의 맨 마지막 레이어)
Dense Layer는 간단히 말해서 추출된 정보들을 하나의 레이어로 모으고, 우리가 원하는 차원으로 축소시켜서 표현하기 위한 레이어입니다. 보통 맨 마지막 레이어로 분류기의 경우 softmax를 사용하는데, softmax의 인풋으로 들어가는 inputput layer로 자주 사용됩니다.
13. 테스트 정확도
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 [==============================] - 0s 12ms/step - loss: 0.1281 - acc: 0.9574
테스트 정확도: 0.9574
14. 테스트 시각화
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 right')
plt.show()