[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🛠 기타/Data & AI

LSTM을 활용한 스팸분류

inu 2020. 8. 5. 00:18
반응형

데이터 전처리

  • 데이터를 불러오고 전처리하는 과정이다.
  • 데이터에 기본적인 필요없는 부분을 제거하고, 문장을 단어들로 나눈다.
import pandas as pd
import numpy as np

from sklearn.model_selection import train_test_split

spam_data = pd.read_csv("../data/spam.csv", encoding="latin1")
# spam, ham 값을 숫자로 변환
spam_data['v1'] = spam_data['v1'].replace('spam', 1)
spam_data['v1'] = spam_data['v1'].replace('ham', 0)
# 정규표현식을 써서, 단어가 아니면 공백으로
spam_data['v2'] = spam_data['v2'].str.replace("[^\w]|br", " ")
# 혹시 공백이 있으면 제거
spam_data['v2'] = spam_data['v2'].replace("", np.nan)
spam_data['v1'] = spam_data['v1'].replace("", np.nan)
# null array 없애는 함수
spam_data = spam_data.dropna(axis=1)
spam_data = spam_data.dropna(how='any')
spam_data.columns = ["label", "mail"]

print("# preprocessing done")
  • 현재 spam.csv에는 v1에 spam여부가, v2에 내용이 적혀있다.
  • 결과(스팸여부)를 숫자형으로 바꿔 머신러닝을 진행할 수 있는 형태로 한다. (spam->1, ham->0)
  • 단어가 아닌 부분은 공백으로 바꾸고, 내용이 공백인 부분은 nan로 결측치로 취급한다.
  • 결측치를 제거하고 columns 이름을 알아보기 쉽게 label과 mail로 바꿨다.
review_train, review_test, y_train, y_test = train_test_split(spam_data['mail'], spam_data['label'], test_size=0.25, shuffle=False, random_state=23)

print("# split done")
  • train_test_split 메소드를 사용해 주어진 데이터를 학습 데이터와 테스트 데이터로 분류한다.
stopwords = ['a', 'an']

X_train = []
for stc in review_train:
    token = []
    words = stc.split()
    for word in words:
        if word not in stopwords:
            token.append(word.lower())
    X_train.append(token)

X_test = []
for stc in review_test:
    token = []
    words = stc.split()
    for word in words:
        if word not in stopwords:
            token.append(word.lower())
    X_test.append(token)

print("# tokenization done")
  • stopwords를 제거한다. (임의로 'a'와 'an'만을 stopword 취급했다.)
  • 그와 동시에 문장으로 되어 있던 데이터들을 단어들의 리스트들로 변경한다.

단어 인덱싱

  • 머신러닝에 사용될 수 있도록 각 단어를 숫자정보로 변경한다.
  • 추후 Word Embedding 과정에 들어가 적절히 임베딩될 수 있도록 하는 것이다.
from tensorflow.keras.preprocessing.text import Tokenizer

# 인덱스 개수 기준
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
print(len(tokenizer.word_counts))
==결과==
7512
  • Tokenizer 메소드를 불러와 객체를 만들고, X_train에 fitting한다.
  • 해당 객체의 word_counts에는 인덱싱한 총 단어가 적혀있다.
  • 우리는 전체 단어를 인덱싱하지 않는다. 빈도수가 낮은 단어까지 학습에 이용할 필요는 없기 때문이다.
  • 따라서 적절한 값을 탐색한다.
count = 0
for word, word_count in tokenizer.word_counts.items():
    if word_count > 1:
        count += 1
print(count)
==결과==
3607
  • 빈도수가 1초과, 즉 적어도 2번 이상 등장한 단어만 카운팅한다.
  • 3607, 약 4000개 정도만 인덱싱하기로 하자.
tokenizer = Tokenizer(4000)
tokenizer.fit_on_texts(X_train)
  • tokenizer 객체가 X_train 내부의 4000개 단어를 인덱싱하도록 fitting 했다.
from tensorflow.keras.preprocessing.sequence import pad_sequences

# 부여된 정수 인덱스로 변환
X_train = tokenizer.texts_to_sequences(X_train)
X_test = tokenizer.texts_to_sequences(X_test)

print("# int_encoding done")
  • 최종적으로 데이터에 대해 정수 인코딩을 진행한다.
  • X_train에 대해서만 fitting을 진행했기 때문에 X_test도 그를 기준으로 인덱싱될 것이다.
  • X_train과 X_test로 나누기 전에 전체 X에 대해 fitting을 시켜줘도 프로세스 상 큰 문제는 없다.

데이터 패딩

  • 각 벡터가 크기가 다르면 원활한 진행을 할 수 없으므로 크기를 통일화해준다.
# 패딩 결정, 임베딩 레이어로 넣을 벡터 길이를 정함
# 해당 벡터길이는 최대길이 or 평균길이
max_len = 50
X_train = pad_sequences(X_train, maxlen=max_len)
X_test = pad_sequences(X_test, maxlen=max_len)
  • pad_sequences 메소드를 활용해 max_len값만큼 데이터를 늘리거나 줄인다. (늘릴 경우 앞을 0으로 채운다.)
  • 보통 최대길이 혹은 평균길이를 활용한다. 각각 장단점이 있다.
  • 최대길이 : 데이터 손실은 없지만, 효율 떨어짐
  • 평균길이 : 데이터 손실은 있지만, 효율이 좋음
  • 본 코드에서는 최대길이도 평균길이 사이 정도의 값을 선택했다.

모델 구축

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

# 모델 구축
# 레이어들을 쌓을 모델을 생성
model = Sequential()
model.add(Embedding(4000, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))
  • Sequential에 각 레이어를 쌓는다.
  • 지금까지 구축한 데이터(단어들이 인덱싱된 데이터)가 Embedding에 들어가 워드 임베딩화 된다. (4000개의 단어를 32차원으로 보낸다.)
  • 그렇게 임베딩화된 데이터가 LSTM을 통화하며 특정 값들을 도출한다. (입력은 32차원으로 보내온 임베딩 데이터)
  • 그들을 sigmoid함수에 통과시켜 최종 값을 찾는다.

최적 모델 찾기

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

# 테스트 데이터 손실함수값(val_loss)이 patience회 이상 연속 증가하면 학습을 조기 종료하는 콜백
early_stop = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=5)
# 훈련 도중 테스트 데이터 정확도(val_acc)가 높았던 순간을 체크포인트로 저장해 활용하는 콜백
model_check = ModelCheckpoint('the_best.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)
  • 학습을 무조건 많이 시킨다고 좋은 것은 아니다.
  • 학습이 과해지면 오히려 테스트 데이터에 대한 손실함수와 정확도가 떨어질 수 있다.

모델 학습

model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['acc'])
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=64, callbacks=[early_stop, model_check])
  • 손실함수는 교차엔트로피(binary_crossentropy), 최적화방법은 아담(adam), 평가기준은 정확도(acc)로 한다.
  • validation_data엔 검증 데이터가 들어간다.
  • 앞서 만든 콜백도 callbacks 변수에 넣어준다.

정확도 측정

# 정확도 측정
# 출력하면 [loss, acc]
print(model.evaluate(X_test, y_test))
  • 정확도를 측정한다.

테스트

user_input = input().split()
user_data = [[]]
for word in user_input:
    if word not in stopwords:
        user_data[0].append(word.lower())
user_data = tokenizer.texts_to_sequences(user_data)
user_data = pad_sequences(user_data, maxlen=maxlen)

if (model.predict(user_data) > 0.5):
    print(f"[{user_input}] is spam")
else:
    print(f"[{user_input}] is non-spam")
  • predict에 들어가는 데이터는 전처리 및 토큰화, 정수인코딩이 완료된 데이터이어야 한다.
  • 따라서 사용자 입력 문장에 그에 맞는 처리를 하고 최종 평가를 한다.
  • 스팸문자로 인식할 것으로 예상되는 'lucky strike jackpot gamble free' 문장을 입력했더니 [['lucky', 'strike', 'jackpot', 'gamble', 'free']] is spam 문장을 출력했다.
  • 반면 'hi my name is inwoo'는 [['hi', 'my', 'name', 'is', 'inwoo']] is non-spam 문장을 출력했다.
반응형