#!/usr/bin/env python # coding: utf-8 # # **나이브 베이즈 모델을 이용한 스팸메일 분류기** # Calssification # ## **1 분류기 Classification** # 1. **Binary Classification** (이진 분류기) : **True / False 조건을** 구분한다 # 1. **Multiclass Classification** (다변량 분류) : **다양한 클래스간의 조건을** 구분한다 # 1. **Multi-label Classification** (다중 클래스 레이블 분류) : 다중의 클래스간 **겹치는 조건에서** 구분을 한다 # # # ## **2 텍스트 분류기 Classification** # 1. **긍정/ 부정, 긍정/ 중립/ 부정** 분류기 # 1. **뉴스의 토픽** 분류기 (**class 간 중첩되어** 분류가 가능하다) # 1. **Named Entity Recognition** (개체명 분류기) : ex) Naive Bayse, Support Vector Machine # ## **3 Naive Bayse Classification 개념** # 1. 확률 기반의 분류기 # 1. **Naive :** 예측을 위한 Token 들이 **Mutually Independent** (상호독립적)을 가정 # 1. **Bayse :** 관찰한 Token이 **클래스 전체 대비, 특정 클래스 속할 확률을 Bayse 기반** 으로 계산 # # > **Naive Bayse 메커니즘** # # 1. 스팸메일과, 정상메일로 구분된 데이터를 사용한다 [download](http://www.aueb.gr/users/ion/data/enron-spam/preprocessed/enron1.tar.gz) # 1. 단어 **Token을** 대상으로 **스팸여부를** 학습한다. # 1. Data 추가시 잘못 예측한 결과에 대해 **Laplace Smoothing** 으로 보완한 값을 **Bayse 로 공식을** 수정한다 # ## **4 Naive Bayse 구현하기** # 스펨메일 데이터 다운받기 [download](http://www.aueb.gr/users/ion/data/enron-spam/preprocessed/enron1.tar.gz) #
# ### **01 enron 메일데이터 살펴보기** # 1. **Summary.txt** 파일에 저장된 내용 살펴보기 # 1. **정상메일 (3,672개)** 와 **스펨메일 (1,500)개로** 약 1:2의 비율로 구분이 된다 # In[2]: # 스팸메일 데이터 Summary with open('./data/enron1/Summary.txt', 'r') as f: summary = f.read() print(summary) # In[3]: # ham 폴더에 저장된 메일내용 확인 (정상으로 분류된 메일) file_path = './data/enron1/ham/0007.1999-12-14.farmer.ham.txt' with open(file_path, 'r') as infile: ham_sample = infile.read() print(ham_sample) # In[4]: # spam 폴더에 저장된 메일내용 확인 (스팸으로 분류된 메일) file_path = './data/enron1/spam/0058.2003-12-21.GP.spam.txt' with open(file_path, 'r') as infile: spam_sample = infile.read() print(spam_sample) # ### **02 enron 메일 데이터 분류하기** # 1. 스펨메일과 정상메일을 레이블을 사용하여 분류한다 # 1. 1 : 스펨메일, 0 : 정상메일 # 1. 분류된 데이터를 전처리 과정을 진행한다 # In[5]: import glob,os # 정상매일은 0, 스펨매일은 1 emails, labels = [], [] for no, file_path in enumerate(['./data/enron1/ham/','./data/enron1/spam/']): for filename in glob.glob(os.path.join(file_path, '*.txt')): with open(filename, 'r', encoding = "ISO-8859-1") as infile: emails.append(infile.read()) labels.append(no) # ### **03 enron 메일 데이터 임베딩** # 1. Chapter 2 에서 진행한 내용을 바탕으로 전처리 작업을 진행한다 # 1. **숫자와 구두점** 제거, **StopWords** 제거, **표제어 원형** 복원 # 1. 정제된 데이터로 **희소벡터 (Sparse Vector)** 로 임베딩 ex) (**row index, feacture/term index**) # In[6]: from nltk.corpus import names from nltk.stem import WordNetLemmatizer all_names = set(names.words()) lemmatizer = WordNetLemmatizer() # 표제어 복원작업 def clean_text(docs): cleaned = [' '.join([lemmatizer.lemmatize(word.lower()) for word in doc.split() if word.isalpha() and word not in all_names]) for doc in docs] return cleaned # 사용자 함수를 활용하여 전처리 작업을 진행한다 cleaned_emails = clean_text(emails) cleaned_emails[0] # In[7]: get_ipython().run_cell_magic('time', '', '# 출현빈도가 높은 상위 500개의 Token을 대상으로 임베딩 한다\n# 희소벡터(Sparse Vector)로 변환 : (row index, feacture/term index)\nfrom sklearn.feature_extraction.text import CountVectorizer\ncv = CountVectorizer(stop_words="english", max_features=500)\nterm_docs = cv.fit_transform(cleaned_emails)\nprint("모델의 Type: {}\\n임베딩의 크기: {}\\n0번문장 내용보기: \\n{}".format(\n type(term_docs),\n term_docs.shape, # 5,172개 문장을 500개 단어로 생성\n term_docs [0])) # 0번 문장의 단어 Vector 목록을 출력\n') # In[8]: # cv 모델로 인덱스별 단어 Token 내용보기 # feature_mapping = cv.vocabulary_ # dict 로 내용출력 (key:value) print(cv.get_feature_names()[:7]) feature_names = cv.get_feature_names() # List 로 내용출력 (인덱스별 value) for indx in [0, 162, 481, 357, 125]: print(indx, ":", feature_names[indx]) # ### **04-1 Naive Bayse 학습을 위한 준비작업** # 모델의 학습을 위한 준비작업으로 데이터를 그룹화 한다 # In[9]: # 레이블을 기준으로 데이터를 그룹화 한다 # defaultdict : 스팸여부 0,1 Tag 로 Token Index List 생성 def get_label_index(labels): from collections import defaultdict label_index = defaultdict(list) for index, label in enumerate(labels): label_index[label].append(index) return label_index # 0 ~ 3600 : 정상메일[0], 3600 ~ 나머지 : 스팸메일[1] label_index = get_label_index(labels) print(label_index.keys()) label_index[1][:10] # ### **04-2 Naive Bayse 위한 사전확률/ 우도값 계산** # **사전확률 및 우도값을** 계산하는 함수를 정의한다 # In[10]: # 학습 샘플을 활용하여 사전 확률을 계산 def get_prior(label_index): """ Compute prior based on training samples Args: label_index (grouped sample indices by class) Returns: { 단어 key : corresponding prior } """ prior = {label: len(index) for label, index in label_index.items()} total_count = sum(prior.values()) for label in prior: prior[label] /= float(total_count) return prior # 위의 인덱스 데이터를 활용하여 사전확률을 계산한다 prior = get_prior(label_index) prior # In[11]: # 확률적 유사가능도(최대 가능도 추정)를 계산: 빈도상위 500개의 단어로 조건부 확률 p(feature|spam)을 계산 import numpy as np def get_likelihood(term_document_matrix, label_index, smoothing=0): """ 훈련 데이터로 우도값 측정 Args: term_document_matrix, label_index, smoothing Returns: { 단어 key, 동시확률 P(feature|class) } """ likelihood = {} for label, index in label_index.items(): likelihood[label] = term_document_matrix[index, :].sum(axis=0) + smoothing likelihood[label] = np.asarray(likelihood[label])[0] total_count = likelihood[label].sum() likelihood[label] = likelihood[label] / float(total_count) return likelihood smoothing = 1 # 라플라스 스무딩 likelihood = get_likelihood(term_docs, label_index, smoothing) print("우도값 shape : {}\n단어 내용보기 : {}\n우도값 array :\n{}".format( likelihood[0].shape, # 0번 레이블일 때 단어별 우도값 계산 feature_names[:5], # 인덱스별 단어 확인 likelihood[0][:20])) # 0번 레이블의 단어별 우도값 샘플 [:20] # ### **04-3 자연 Log 를 활용한 예측함수 구현하기** # - 앞에서 측정한 **사전확률과 및 우도값을** 활용하여 예측함수를 정의 합니다 # - 단어들의 확률을 합치기 위해, **Log()** 로 변환 후 **경우의 수를 모두 합칩니다** # In[12]: # OverFlow가 발생 가능하므로, 데이터를 Log() 자연로그로 변환 후 덧셈 계산, # 계산이 끝난 뒤, 로그의 역함수 (exp()) 를 활용하여 실수로 변환한다 def get_posterior(term_document_matrix, prior, likelihood): """ 사전확률과 유사가능도를 바탕으로 샘플 데이터의 사후확률을 계산 Args: term_document_matrix (sparse matrix) prior { 단어 Key : 사전확률 } likelihood { 단어 Key : 조건부 확률 } Returns: { 단어 Key : 관련 사후 확률값 } """ # 확률의 연산시 log() 로 변환한 후 합친다 num_docs, posteriors = term_document_matrix.shape[0], [] for i in range(num_docs): # 사후확률 : 사전확률 X 유사가능도(최대 가능도 추정량) posterior = {key: np.log(prior_label) for key, prior_label in prior.items()} for label, likelihood_label in likelihood.items(): term_document_vector = term_document_matrix.getrow(i) counts = term_document_vector.data indices = term_document_vector.indices for count, index in zip(counts, indices): posterior[label] += np.log(likelihood_label[index]) * count # exp(-1000):exp(-999) 는 분모가 0이 되는 문제가 발생 # 하지만 exp(0):exp(1)과 동치가 된다. min_log_posterior = min(posterior.values()) for label in posterior: try: posterior[label] = np.exp(posterior[label] - min_log_posterior) except: posterior[label] = float('inf') # 값이 너무 클때 # 전체 합이 1이 되도록 정규화 sum_posterior = sum(posterior.values()) for label in posterior: if posterior[label] == float('inf'): posterior[label] = 1.0 else: posterior[label] /= sum_posterior posteriors.append(posterior.copy()) return posteriors # In[13]: # 테스트 메일을 사용하여 알고리즘을 검증 emails_test = [ '''Subject: flat screens hello , please call or contact regarding the other flat screens requested . trisha tlapek - eb 3132 b michael sergeev - eb 3132 a also the sun blocker that was taken away from eb 3131 a . trisha should two monitors also michael .thanks kevin moore''', '''Subject: having problems in bed ? we can help ! cialis allows men to enjoy a fully normal sex life without having to plan the sexual act . if we let things terrify us , life will not be worth living . brevity is the soul of lingerie . suspicion always haunts the guilty mind .'''] cleaned_test = clean_text(emails_test) term_docs_test = cv.transform(cleaned_test) posterior = get_posterior(term_docs_test, prior, likelihood) from pprint import pprint pprint(posterior) # 검증결과 0번 메일은 0.98로 정상, 1번 메일은 0.99로 스펨에 해당 # ### **04-4 학습을 위해 Train / Test 데이터를 나눈다** # scikit-learn 모듈 **train_test_split** 을 사용한다 # In[14]: from collections import Counter Counter(labels) # In[15]: from sklearn.model_selection import train_test_split X_train, X_test, Y_train, Y_test = train_test_split( cleaned_emails, # X_train, X_test 로 추출 labels, # Y_train, Y_test 로 추출 test_size = 0.33, random_state = 42) print("Train 'email':{:,}, 'label':{:,}\nTest 'email':{:,}, 'label':{:,}".format( len(X_train), len(Y_train), len(X_test), len(Y_test))) # In[16]: # 데이터 Set의 사후 확률을 예측한다 term_docs_train = cv.fit_transform(X_train) label_index = get_label_index(Y_train) prior = get_prior(label_index) likelihood = get_likelihood(term_docs_train, label_index, smoothing) # Test / 신규 데이터 Set의 사후확률을 예측한다 term_docs_test = cv.transform(X_test) posterior = get_posterior(term_docs_test, prior, likelihood) correct = 0.0 for pred, actual in zip(posterior, Y_test): if actual == 1: if pred[1] >= 0.5: correct += 1 elif pred[0] > 0.5: correct += 1 # dtype 을 128 이상으로 지정할 것 # https://stackoverflow.com/questions/40726490/overflow-error-in-pythons-numpy-exp-function/40726641 print('{:,} 개의 테스트 데이터(Y_test)의 정확도는: {:.1f} %'.format( len(Y_test), correct/len(Y_test)*100)) #
# # ## **5 Sklearn 을 활용한 Naive Bayse 구현하기** # - 위에서 복잡한 과정을 sklearn으로 실습 합니다 # - nltk 모듈을 활용한 예제 [nltk_tutorial](https://nbviewer.jupyter.org/github/YongBeomKim/nltk_tutorial/blob/master/ipython/03-2.Bayse.ipynb) # ### **01 데이터 전처리 및 모델학습** # 모델을 학습한 뒤 정확도를 측정한다 # ```python # # cleaned_emails[0] : 전처리된 텍스트 List # from sklearn.feature_extraction.text import CountVectorizer # cv = CountVectorizer(stop_words="english", max_features=500) # term_docs_test = cv.transform(cleaned_test) # ``` # In[17]: from sklearn.naive_bayes import MultinomialNB clf = MultinomialNB(alpha = 1.0, # 라플라스 Smoothing 값 fit_prior = True) # Data Set로 학습된 사전확률 사용 clf.fit(term_docs_train, Y_train) prediction_prob = clf.predict_proba(term_docs_test) prediction_prob[0:6] # In[18]: # 예측한 클래스 값을 바로 계산하여 출력한다 # 역치값은 0.5로 0.5보다 크면 1, 작으면 0을 출력 prediction = clf.predict(term_docs_test) prediction[:10] # In[19]: # test 값을 활용하여 모델의 정확도 측정 accuracy = clf.score(term_docs_test, Y_test) print('The accuracy using MultinomialNB is: {0:.1f}%'.format(accuracy*100)) # ### **02 분류기의 성능 평가** # **혼동행렬(Confusion Matrix) 분할표로** 예측값을 테스트하여 출력한다 # # # In[20]: # 혼동행렬을 계산 from sklearn.metrics import confusion_matrix confusion_matrix(Y_test, prediction, labels=[0, 1]) # In[21]: # f1 Score 를 측정하여 정밀도, 재연율을 계산 from sklearn.metrics import precision_score, recall_score, f1_score print("""Precesion(정밀도) : {:.4}\nRecall(재현율) : {:.4} f1 score (1) : {:.4} \nf1 score (0) : {:.4}""".format( precision_score(Y_test, prediction, pos_label=1), recall_score(Y_test, prediction, pos_label=1), f1_score(Y_test, prediction, pos_label=1), f1_score(Y_test, prediction, pos_label=0))) # In[22]: # 위 내용을 한꺼번에 실행해본다 from sklearn.metrics import classification_report report = classification_report(Y_test, prediction) print(report) # ### **03 분류기의 성능 평가** # 1. **정확도**(훈련데이터 적합도) 와 **재현율**(일반화 정도)이 **모두 높은 경우가 없기** 때문에 f1-score를 측정한다 # 1. 하지만 모델의 **평균값과,** 모델의 **f1-score** 둘 다 높은 모델은 없으므로 별도 기준이 필요 # 1. 대표적인 대안으로 **ROC (Receiver Operation Characteristic), AUC (Area Under the Curve)** 가 있다 # 1. 이번 예제에서는 **ROC**를 그려보자 # In[23]: get_ipython().run_cell_magic('time', '', '# ROC Curve 값들을 계산합니다\npos_prob = prediction_prob[:, 1]\nthresholds = np.arange(0.0, 1.2, 0.1)\ntrue_pos = [0]*len(thresholds) \nfalse_pos = [0]*len(thresholds)\n\nfor pred, y in zip(pos_prob, Y_test):\n for i, threshold in enumerate(thresholds):\n if pred >= threshold:\n if y == 1: true_pos[i] += 1\n else: false_pos[i] += 1\n else: break\n\n# 임계치를 설정하기 위해 양성비율과, 음성 비율을 계산한다\n# 양성 테스트 샘플이 516개, 음성 테스트 샘플이 1,191개 이다\ntrue_pos_rate = [tp / 516.0 for tp in true_pos]\nfalse_pos_rate = [fp / 1191.0 for fp in false_pos]\n') # In[24]: get_ipython().run_line_magic('matplotlib', 'inline') # ROC Curve 를 출력한다 import matplotlib.pyplot as plt lw = 2 # BaseLine을 그린다 plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--') plt.plot(false_pos_rate, true_pos_rate, color = 'darkorange', lw = lw) plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver Operating Characteristic') plt.show() # In[25]: from sklearn.metrics import roc_auc_score roc_auc_score(Y_test, pos_prob) #
# # ## **6 Sklearn 을 활용한 모델의 튜닟 및 교차검증** # 1. 모델이 실질적으로 잘 작동하는지 **K-fold 검정을** 적용한다 # 1. **AUC 값의** 측정 : **ROC 커브의** 밑면적을 구한 값으로 **1에 가까울수록** 성능이 좋다.[참고](http://newsight.tistory.com/53) # # # In[26]: # 전체 10개의 폴드 생성기로 초기화 후 파라미터 분석을 진행합니다 from sklearn.model_selection import StratifiedKFold k = 10 k_fold = StratifiedKFold(n_splits=k) # 연산을 위해 Numpy 객체로 변환한다 cleaned_emails_np = np.array(cleaned_emails) labels_np = np.array(labels) # 10 폴드 생성기 학습을 위한 파라미터를 정의합니다 max_features_option = [2000, 4000, 8000] # 가장 많이 사용되는 N개 단어를 선택 smoothing_factor_option = [0.5, 1.0, 1.5, 2.0] # Smoothing Parameter : 초기값 fit_prior_option = [True, False] # 사전 확률을 사용할지 여부 # In[31]: get_ipython().run_cell_magic('time', '', 'auc_record = {} # k_fold 분리된 객체를 활용하여 개별 환경에서 AUC를 측정\nfor train_indices, test_indices in k_fold.split(cleaned_emails, labels):\n X_train, X_test = cleaned_emails_np[train_indices], cleaned_emails_np[test_indices]\n Y_train, Y_test = labels_np[train_indices], labels_np[test_indices]\n\n # max_features_option 환경값을 바꿔가면서 AUC 테스트\n for max_features in max_features_option: \n if max_features not in auc_record:\n auc_record[max_features] = {}\n cv = CountVectorizer(stop_words="english", max_features=max_features)\n term_docs_train = cv.fit_transform(X_train)\n term_docs_test = cv.transform(X_test)\n \n # smoothing_factor_option 초기값을 바꾸며 AUC 테스트\n for smoothing_factor in smoothing_factor_option:\n if smoothing_factor not in auc_record[max_features]:\n auc_record[max_features][smoothing_factor] = {}\n \n # fit_prior_option : 사전확률을 바꾸며 AUC 테스트\n for fit_prior in fit_prior_option:\n clf = MultinomialNB(alpha=smoothing_factor, fit_prior=fit_prior)\n clf.fit(term_docs_train, Y_train)\n prediction_prob = clf.predict_proba(term_docs_test)\n pos_prob = prediction_prob[:, 1]\n auc = roc_auc_score(Y_test, pos_prob)\n auc_record[max_features][smoothing_factor][fit_prior] \\\n = auc + auc_record[max_features][smoothing_factor].get(fit_prior, 0.0) \n\n# 위에서 계산한 결과를 출력합니다\nauc_result = []\nfor max_features, max_feature_record in auc_record.items():\n for smoothing, smoothing_record in max_feature_record.items():\n for fit_prior, auc in smoothing_record.items():\n auc_result.append([max_features, smoothing, fit_prior, auc/k])\n') # In[32]: import pandas as pd auc_result = pd.DataFrame(auc_result) auc_result.columns = ['max features', 'smoothing', 'fit prior', 'auc'] auc_result = auc_result.sort_values('auc', ascending=False).reset_index(drop=True) auc_result.head()